Merge pull request #669 from mz8i/feature/dynamics-boolean-field
Dynamics boolean field
This commit is contained in:
commit
52750c85b6
@ -508,6 +508,10 @@
|
|||||||
</Rule>
|
</Rule>
|
||||||
</Style>
|
</Style>
|
||||||
<Style name="dynamics_demolished_count">
|
<Style name="dynamics_demolished_count">
|
||||||
|
<Rule>
|
||||||
|
<Filter>[dynamics_has_demolished_buildings] = false</Filter>
|
||||||
|
<PolygonSymbolizer fill="#0C7BDC" />
|
||||||
|
</Rule>
|
||||||
<Rule>
|
<Rule>
|
||||||
<Filter>[demolished_buildings_count] >= 7</Filter>
|
<Filter>[demolished_buildings_count] >= 7</Filter>
|
||||||
<PolygonSymbolizer fill="#bd0026" />
|
<PolygonSymbolizer fill="#bd0026" />
|
||||||
|
@ -239,6 +239,11 @@ export const dataFieldsConfig = valueType<DataFieldConfig>()({ /* eslint-disable
|
|||||||
verify: false,
|
verify: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
dynamics_has_demolished_buildings: {
|
||||||
|
edit: true,
|
||||||
|
verify: true
|
||||||
|
},
|
||||||
|
|
||||||
demolished_buildings: {
|
demolished_buildings: {
|
||||||
edit: true,
|
edit: true,
|
||||||
verify: false,
|
verify: false,
|
||||||
@ -246,3 +251,6 @@ export const dataFieldsConfig = valueType<DataFieldConfig>()({ /* eslint-disable
|
|||||||
sqlCast: 'jsonb',
|
sqlCast: 'jsonb',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type Building = { [k in keyof typeof dataFieldsConfig]: any };
|
||||||
|
export type BuildingUpdate = Partial<Building>;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { hasAnyOwnProperty } from '../../../helpers';
|
import { hasAnyOwnProperty } from '../../../helpers';
|
||||||
|
import { Building, BuildingUpdate } from '../../config/dataFields';
|
||||||
import { getBuildingData } from '../../dataAccess/building';
|
import { getBuildingData } from '../../dataAccess/building';
|
||||||
import { ArgumentError } from '../../errors/general';
|
import { ArgumentError } from '../../errors/general';
|
||||||
|
|
||||||
@ -36,10 +37,30 @@ async function processCurrentLandUseClassifications(buildingId: number, building
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process Dynamics data - sort past buildings by construction date
|
* Process Dynamics data - check field relationships and sort demolished buildings by construction date
|
||||||
*/
|
*/
|
||||||
async function processDynamicsPastBuildings(buildingId: number, buildingUpdate: any): Promise<any> {
|
async function processDynamicsDemolishedBuildings(buildingId: number, buildingUpdate: BuildingUpdate): Promise<BuildingUpdate> {
|
||||||
|
const currentBuildingData = await getBuildingData(buildingId);
|
||||||
|
|
||||||
|
const afterUpdate: Building = Object.assign({}, currentBuildingData, buildingUpdate);
|
||||||
|
|
||||||
|
const hasDemolished: boolean = afterUpdate.dynamics_has_demolished_buildings;
|
||||||
|
const demolishedList: any[] = afterUpdate.demolished_buildings;
|
||||||
|
|
||||||
|
if(currentBuildingData.date_year == undefined) {
|
||||||
|
throw new ArgumentError('Cannot edit demolished buildings data if data on current building age is missing', 'buildingUpdate');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hasDemolished === false || hasDemolished == undefined) {
|
||||||
|
if(demolishedList.length > 0) {
|
||||||
|
throw new ArgumentError('Inconsistent data on whether there were any other buildings on this site', 'buildingUpdate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(buildingUpdate.demolished_buildings != undefined) {
|
||||||
buildingUpdate.demolished_buildings = buildingUpdate.demolished_buildings.sort((a, b) => b.year_constructed.min - a.year_constructed.min);
|
buildingUpdate.demolished_buildings = buildingUpdate.demolished_buildings.sort((a, b) => b.year_constructed.min - a.year_constructed.min);
|
||||||
|
}
|
||||||
|
|
||||||
return buildingUpdate;
|
return buildingUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,12 +68,12 @@ async function processDynamicsPastBuildings(buildingId: number, buildingUpdate:
|
|||||||
/**
|
/**
|
||||||
* Define any custom processing logic for specific building attributes
|
* Define any custom processing logic for specific building attributes
|
||||||
*/
|
*/
|
||||||
export async function processBuildingUpdate(buildingId: number, buildingUpdate: any): Promise<any> {
|
export async function processBuildingUpdate(buildingId: number, buildingUpdate: BuildingUpdate): Promise<any> {
|
||||||
if(hasAnyOwnProperty(buildingUpdate, ['current_landuse_group'])) {
|
if(hasAnyOwnProperty(buildingUpdate, ['current_landuse_group'])) {
|
||||||
buildingUpdate = await processCurrentLandUseClassifications(buildingId, buildingUpdate);
|
buildingUpdate = await processCurrentLandUseClassifications(buildingId, buildingUpdate);
|
||||||
}
|
}
|
||||||
if('demolished_buildings' in buildingUpdate) {
|
if(hasAnyOwnProperty(buildingUpdate, ['demolished_buildings', 'dynamics_has_demolished_buildings'])) {
|
||||||
buildingUpdate = await processDynamicsPastBuildings(buildingId, buildingUpdate);
|
buildingUpdate = await processDynamicsDemolishedBuildings(buildingId, buildingUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildingUpdate;
|
return buildingUpdate;
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { BaseDataEntryProps } from '../data-entry';
|
||||||
|
import { DataTitleCopyable } from '../data-title';
|
||||||
|
|
||||||
|
interface ToggleButtonProps {
|
||||||
|
value: string;
|
||||||
|
checked: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
onChange: React.ChangeEventHandler<HTMLInputElement>;
|
||||||
|
checkedClassName: string;
|
||||||
|
uncheckedClassName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToggleButton: React.FC<ToggleButtonProps> = ({
|
||||||
|
value,
|
||||||
|
checked,
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
checkedClassName,
|
||||||
|
uncheckedClassName,
|
||||||
|
children
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
/**
|
||||||
|
* If the toggle button is both checked and disabled, we can't set disabled CSS class
|
||||||
|
* because then Bootstrap makes the button look like it wasn't checked.
|
||||||
|
* So we skip the class and force cursor type manually so it doesn't look clickable.
|
||||||
|
*/
|
||||||
|
<label className={`btn ${checked ? checkedClassName : uncheckedClassName} ${disabled && !checked && 'disabled'}`}
|
||||||
|
{...disabled && checked && {
|
||||||
|
style: { cursor: 'default'}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input type="radio" name="options" value={value + ''}
|
||||||
|
autoComplete="off"
|
||||||
|
checked={checked}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogicalDataEntryProps extends BaseDataEntryProps {
|
||||||
|
value: boolean;
|
||||||
|
disallowTrue?: boolean;
|
||||||
|
disallowFalse?: boolean;
|
||||||
|
disallowNull?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogicalDataEntry: React.FC<LogicalDataEntryProps> = (props) => {
|
||||||
|
function handleValueChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
props.onChange?.(props.slug, e.target.value === 'null' ? null : e.target.value === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDisabled = props.mode === 'view' || props.disabled;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DataTitleCopyable
|
||||||
|
slug={props.slug}
|
||||||
|
title={props.title}
|
||||||
|
tooltip={props.tooltip}
|
||||||
|
disabled={props.disabled || props.value == undefined}
|
||||||
|
copy={props.copy}
|
||||||
|
/>
|
||||||
|
<div className="btn-group btn-group-toggle">
|
||||||
|
<ToggleButton
|
||||||
|
value="true"
|
||||||
|
checked={props.value === true}
|
||||||
|
disabled={isDisabled || props.disallowTrue}
|
||||||
|
checkedClassName='btn-outline-dark active'
|
||||||
|
uncheckedClassName='btn-outline-dark'
|
||||||
|
onChange={handleValueChange}
|
||||||
|
>Yes</ToggleButton>
|
||||||
|
|
||||||
|
<ToggleButton
|
||||||
|
value="null"
|
||||||
|
checked={props.value == null}
|
||||||
|
disabled={isDisabled || props.disallowNull}
|
||||||
|
checkedClassName='btn-secondary active'
|
||||||
|
uncheckedClassName='btn-outline-dark'
|
||||||
|
onChange={handleValueChange}
|
||||||
|
>?</ToggleButton>
|
||||||
|
|
||||||
|
<ToggleButton
|
||||||
|
value="false"
|
||||||
|
checked={props.value === false}
|
||||||
|
disabled={isDisabled || props.disallowFalse}
|
||||||
|
checkedClassName='btn-outline-dark active'
|
||||||
|
uncheckedClassName='btn-outline-dark'
|
||||||
|
onChange={handleValueChange}
|
||||||
|
>No</ToggleButton>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -6,7 +6,7 @@ import { apiPost } from '../apiHelpers';
|
|||||||
import ErrorBox from '../components/error-box';
|
import ErrorBox from '../components/error-box';
|
||||||
import InfoBox from '../components/info-box';
|
import InfoBox from '../components/info-box';
|
||||||
import { compareObjects } from '../helpers';
|
import { compareObjects } from '../helpers';
|
||||||
import { Building, UserVerified } from '../models/building';
|
import { Building, BuildingAttributes, UserVerified } from '../models/building';
|
||||||
import { User } from '../models/user';
|
import { User } from '../models/user';
|
||||||
|
|
||||||
import ContainerHeader from './container-header';
|
import ContainerHeader from './container-header';
|
||||||
@ -72,6 +72,7 @@ const withCopyEdit: (wc: React.ComponentType<CategoryViewProps>) => DataContaine
|
|||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
this.handleVerify = this.handleVerify.bind(this);
|
this.handleVerify = this.handleVerify.bind(this);
|
||||||
this.handleSaveAdd = this.handleSaveAdd.bind(this);
|
this.handleSaveAdd = this.handleSaveAdd.bind(this);
|
||||||
|
this.handleSaveChange = this.handleSaveChange.bind(this);
|
||||||
|
|
||||||
this.toggleCopying = this.toggleCopying.bind(this);
|
this.toggleCopying = this.toggleCopying.bind(this);
|
||||||
this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this);
|
this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this);
|
||||||
@ -190,14 +191,12 @@ const withCopyEdit: (wc: React.ComponentType<CategoryViewProps>) => DataContaine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSubmit(event) {
|
async doSubmit(edits: Partial<BuildingAttributes>) {
|
||||||
event.preventDefault();
|
|
||||||
this.setState({error: undefined});
|
this.setState({error: undefined});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await apiPost(
|
const data = await apiPost(
|
||||||
`/api/buildings/${this.props.building.building_id}.json`,
|
`/api/buildings/${this.props.building.building_id}.json`,
|
||||||
this.state.buildingEdits
|
edits
|
||||||
);
|
);
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
@ -210,6 +209,44 @@ const withCopyEdit: (wc: React.ComponentType<CategoryViewProps>) => DataContaine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.doSubmit(this.state.buildingEdits);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSaveAdd(slug: string, newItem: any) {
|
||||||
|
if(this.props.building[slug] != undefined && !Array.isArray(this.props.building[slug])) {
|
||||||
|
this.setState({error: 'Unexpected error'});
|
||||||
|
console.error(`Trying to add a new item to a field (${slug}) which is not an array`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.isEdited()) {
|
||||||
|
this.setState({error: 'Cannot save a new record when there are unsaved edits to existing records'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const edits = {
|
||||||
|
[slug]: [...(this.props.building[slug] ?? []), newItem]
|
||||||
|
};
|
||||||
|
|
||||||
|
this.doSubmit(edits);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSaveChange(slug: string, value: any) {
|
||||||
|
if(this.isEdited()) {
|
||||||
|
this.setState({ error: 'Cannot change this value when there are other unsaved edits. Save or discard the other edits first.'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const edits = {
|
||||||
|
[slug]: value
|
||||||
|
};
|
||||||
|
|
||||||
|
this.doSubmit(edits);
|
||||||
|
}
|
||||||
|
|
||||||
async handleVerify(slug: string, verify: boolean, x: number, y: number) {
|
async handleVerify(slug: string, verify: boolean, x: number, y: number) {
|
||||||
const verifyPatch = {};
|
const verifyPatch = {};
|
||||||
if (verify) {
|
if (verify) {
|
||||||
@ -242,40 +279,6 @@ const withCopyEdit: (wc: React.ComponentType<CategoryViewProps>) => DataContaine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSaveAdd(slug: string, newItem: any) {
|
|
||||||
if(this.props.building[slug] != undefined && !Array.isArray(this.props.building[slug])) {
|
|
||||||
this.setState({error: 'Unexpected error'});
|
|
||||||
console.error(`Trying to add a new item to a field (${slug}) which is not an array`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.isEdited()) {
|
|
||||||
this.setState({error: 'Cannot save a new record when there are unsaved edits to existing records'});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const edits = {
|
|
||||||
[slug]: [...(this.props.building[slug] ?? []), newItem]
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setState({error: undefined});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await apiPost(
|
|
||||||
`/api/buildings/${this.props.building.building_id}.json`,
|
|
||||||
edits
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.error) {
|
|
||||||
this.setState({error: data.error});
|
|
||||||
} else {
|
|
||||||
this.props.onBuildingUpdate(this.props.building.building_id, data);
|
|
||||||
}
|
|
||||||
} catch(err) {
|
|
||||||
this.setState({error: err});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const currentBuilding = this.getEditedBuilding();
|
const currentBuilding = this.getEditedBuilding();
|
||||||
|
|
||||||
@ -356,6 +359,7 @@ const withCopyEdit: (wc: React.ComponentType<CategoryViewProps>) => DataContaine
|
|||||||
onLike={undefined}
|
onLike={undefined}
|
||||||
onVerify={undefined}
|
onVerify={undefined}
|
||||||
onSaveAdd={undefined}
|
onSaveAdd={undefined}
|
||||||
|
onSaveChange={undefined}
|
||||||
user_verified={[]}
|
user_verified={[]}
|
||||||
/>
|
/>
|
||||||
</Fragment> :
|
</Fragment> :
|
||||||
@ -408,6 +412,7 @@ const withCopyEdit: (wc: React.ComponentType<CategoryViewProps>) => DataContaine
|
|||||||
onLike={this.handleLike}
|
onLike={this.handleLike}
|
||||||
onVerify={this.handleVerify}
|
onVerify={this.handleVerify}
|
||||||
onSaveAdd={this.handleSaveAdd}
|
onSaveAdd={this.handleSaveAdd}
|
||||||
|
onSaveChange={this.handleSaveChange}
|
||||||
user_verified={this.props.user_verified}
|
user_verified={this.props.user_verified}
|
||||||
user={this.props.user}
|
user={this.props.user}
|
||||||
/>
|
/>
|
||||||
|
@ -21,6 +21,9 @@ interface CategoryViewProps {
|
|||||||
/* Special handler for adding and immediately saving a new item of an array-like attribute */
|
/* Special handler for adding and immediately saving a new item of an array-like attribute */
|
||||||
onSaveAdd: (slug: string, newItem: any) => void;
|
onSaveAdd: (slug: string, newItem: any) => void;
|
||||||
|
|
||||||
|
/* Special handler for setting a value and immediately saving */
|
||||||
|
onSaveChange: (slug: string, value: any) => void;
|
||||||
|
|
||||||
user_verified: any;
|
user_verified: any;
|
||||||
user?: any;
|
user?: any;
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import { NumberRangeDataEntry } from './number-range-data-entry';
|
|||||||
|
|
||||||
import './dynamics-data-entry.css';
|
import './dynamics-data-entry.css';
|
||||||
import { CloseIcon } from '../../../components/icons';
|
import { CloseIcon } from '../../../components/icons';
|
||||||
|
import DataTitle, { DataTitleCopyable } from '../../data-components/data-title';
|
||||||
|
|
||||||
type DemolishedBuilding = (BuildingAttributes['demolished_buildings'][number]);
|
type DemolishedBuilding = (BuildingAttributes['demolished_buildings'][number]);
|
||||||
|
|
||||||
@ -147,6 +148,7 @@ const DynamicsDataRow: React.FC<DynamicsDataRowProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface DynamicsDataEntryProps extends BaseDataEntryProps {
|
interface DynamicsDataEntryProps extends BaseDataEntryProps {
|
||||||
|
title: string;
|
||||||
value: DemolishedBuilding[];
|
value: DemolishedBuilding[];
|
||||||
editableEntries: boolean;
|
editableEntries: boolean;
|
||||||
maxYear: number;
|
maxYear: number;
|
||||||
@ -206,11 +208,13 @@ export const DynamicsDataEntry: React.FC<DynamicsDataEntryProps> = (props) => {
|
|||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
isEditing &&
|
isEditing ?
|
||||||
<>
|
<>
|
||||||
<h6 className="h6">Existing records for demolished buildings</h6>
|
<h6 className="h6">Existing records for demolished buildings</h6>
|
||||||
<label>Please supply sources for any edits of existing records</label>
|
<label>Please supply sources for any edits of existing records</label>
|
||||||
</>
|
</> :
|
||||||
|
|
||||||
|
<DataTitleCopyable slug={props.slug} title={props.title} tooltip={null}/>
|
||||||
}
|
}
|
||||||
<ul className="data-entry-list">
|
<ul className="data-entry-list">
|
||||||
{
|
{
|
||||||
|
@ -13,6 +13,7 @@ import NumericDataEntry from '../../data-components/numeric-data-entry';
|
|||||||
import withCopyEdit from '../../data-container';
|
import withCopyEdit from '../../data-container';
|
||||||
|
|
||||||
import { CategoryViewProps } from '../category-view-props';
|
import { CategoryViewProps } from '../category-view-props';
|
||||||
|
import { LogicalDataEntry } from '../../data-components/logical-data-entry/logical-data-entry';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamics view/edit section
|
* Dynamics view/edit section
|
||||||
@ -60,7 +61,23 @@ const DynamicsView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
|||||||
</FieldRow>
|
</FieldRow>
|
||||||
</DynamicsBuildingPane>
|
</DynamicsBuildingPane>
|
||||||
{
|
{
|
||||||
currentBuildingConstructionYear != undefined ?
|
currentBuildingConstructionYear == undefined ?
|
||||||
|
<InfoBox>To add historical records, fill in the <Link to={ageLinkUrl}>Age</Link> data first.</InfoBox> :
|
||||||
|
|
||||||
|
<>
|
||||||
|
<LogicalDataEntry
|
||||||
|
slug='dynamics_has_demolished_buildings'
|
||||||
|
title={dataFields.dynamics_has_demolished_buildings.title}
|
||||||
|
value={building.dynamics_has_demolished_buildings}
|
||||||
|
disallowFalse={(building.demolished_buildings?.length ?? 0) > 0}
|
||||||
|
disallowNull={(building.demolished_buildings?.length ?? 0) > 0}
|
||||||
|
|
||||||
|
onChange={props.onSaveChange}
|
||||||
|
mode={props.mode}
|
||||||
|
copy={props.copy}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
building.dynamics_has_demolished_buildings &&
|
||||||
<>
|
<>
|
||||||
<DynamicsDataEntry
|
<DynamicsDataEntry
|
||||||
|
|
||||||
@ -73,7 +90,7 @@ const DynamicsView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
|||||||
value={building.demolished_buildings}
|
value={building.demolished_buildings}
|
||||||
editableEntries={true}
|
editableEntries={true}
|
||||||
slug='demolished_buildings'
|
slug='demolished_buildings'
|
||||||
title={undefined}
|
title={dataFields.demolished_buildings.title}
|
||||||
mode={props.mode}
|
mode={props.mode}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
onSaveAdd={props.onSaveAdd}
|
onSaveAdd={props.onSaveAdd}
|
||||||
@ -85,10 +102,10 @@ const DynamicsView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
|||||||
props.mode === 'view' &&
|
props.mode === 'view' &&
|
||||||
<InfoBox>Switch to edit mode to add/edit past building records</InfoBox>
|
<InfoBox>Switch to edit mode to add/edit past building records</InfoBox>
|
||||||
}
|
}
|
||||||
</> :
|
</>
|
||||||
<InfoBox>To add historical records, fill in the <Link to={ageLinkUrl}>Age</Link> data first.</InfoBox>
|
}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
</DataEntryGroup>
|
</DataEntryGroup>
|
||||||
|
|
||||||
<DataEntryGroup name="Future planned data collection" collapsed={false} showCount={false}>
|
<DataEntryGroup name="Future planned data collection" collapsed={false} showCount={false}>
|
||||||
|
@ -188,7 +188,7 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition} = {
|
|||||||
mapStyle: 'dynamics_demolished_count',
|
mapStyle: 'dynamics_demolished_count',
|
||||||
legend: {
|
legend: {
|
||||||
title: 'Dynamics',
|
title: 'Dynamics',
|
||||||
description: 'Past (demolished) buildings on site',
|
description: 'Demolished buildings on the same site',
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
text: '7+',
|
text: '7+',
|
||||||
@ -211,6 +211,9 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition} = {
|
|||||||
}, {
|
}, {
|
||||||
text: '1',
|
text: '1',
|
||||||
color: '#ffe8a9',
|
color: '#ffe8a9',
|
||||||
|
}, {
|
||||||
|
text: 'None',
|
||||||
|
color: '#0C7BDC'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -7,11 +7,44 @@ import { Category } from './categories-config';
|
|||||||
* e.g. dataFields.foo_bar would not be highlighted as an error.
|
* e.g. dataFields.foo_bar would not be highlighted as an error.
|
||||||
*/
|
*/
|
||||||
export interface DataFieldDefinition {
|
export interface DataFieldDefinition {
|
||||||
|
/**
|
||||||
|
* The primary category to which the field belongs.
|
||||||
|
*
|
||||||
|
* A field could be displayed in several categories, but this value will be used
|
||||||
|
* when a single category needs to be shown in the context of a field, e.g.
|
||||||
|
* in the case of edit history or the copy-paste tool (multi-edit)
|
||||||
|
* */
|
||||||
category: Category;
|
category: Category;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The human-readable title of the field to be displayed as label.
|
||||||
|
*/
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text to be displayed in the hint tooltip next to the input field.
|
||||||
|
*
|
||||||
|
* This supports a simple Markdown-like syntax for embedding clickable URLs in the hint
|
||||||
|
* for example "see [here](http://example.com/info.html) for more information"
|
||||||
|
*/
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
properties?: { [key: string]: DataFieldDefinition};
|
|
||||||
example: any; // the example field is used to automatically determine the type of the properties in the Building interface
|
/**
|
||||||
|
* If the defined type is an array, this describes the fields of each array item.
|
||||||
|
* The nested fields don't currently need a category field to be defined.
|
||||||
|
*/
|
||||||
|
items?: { [key: string]: Omit<DataFieldDefinition, 'category'> };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The example is used to determine the runtime type in which the attribute data is stored (e.g. number, string, object)
|
||||||
|
* This gives the programmer auto-complete of all available building attributes when implementing a category view.
|
||||||
|
*
|
||||||
|
* E.g. if the field is a text value, an empty string ("") is the simplest example.
|
||||||
|
*
|
||||||
|
* Making it semantically correct is useful but not necessary.
|
||||||
|
* E.g. for building attachment form, you could use "Detached" as example
|
||||||
|
*/
|
||||||
|
example: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
||||||
@ -412,6 +445,12 @@ export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
|||||||
example: 100,
|
example: 100,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
dynamics_has_demolished_buildings: {
|
||||||
|
category: Category.Dynamics,
|
||||||
|
title: 'Where any other buildings ever built on this site?',
|
||||||
|
example: true,
|
||||||
|
},
|
||||||
|
|
||||||
demolished_buildings: {
|
demolished_buildings: {
|
||||||
category: Category.Dynamics,
|
category: Category.Dynamics,
|
||||||
title: 'Past (demolished) buildings on this site',
|
title: 'Past (demolished) buildings on this site',
|
||||||
|
@ -24,7 +24,7 @@ export function parseJsonOrDefault(jsonString: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasAnyOwnProperty(obj: {}, keys: string[]) {
|
export function hasAnyOwnProperty<T>(obj: T, keys: (keyof T)[]) {
|
||||||
return keys.some(k => obj.hasOwnProperty(k));
|
return keys.some(k => obj.hasOwnProperty(k));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,10 +136,11 @@ const LAYER_QUERIES = {
|
|||||||
dynamics_demolished_count: `
|
dynamics_demolished_count: `
|
||||||
SELECT
|
SELECT
|
||||||
geometry_id,
|
geometry_id,
|
||||||
jsonb_array_length(demolished_buildings) as demolished_buildings_count
|
jsonb_array_length(demolished_buildings) as demolished_buildings_count,
|
||||||
|
dynamics_has_demolished_buildings
|
||||||
FROM
|
FROM
|
||||||
buildings
|
buildings
|
||||||
WHERE jsonb_array_length(demolished_buildings) > 0`,
|
WHERE jsonb_array_length(demolished_buildings) > 0 OR dynamics_has_demolished_buildings = FALSE`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const GEOMETRY_FIELD = 'geometry_geom';
|
const GEOMETRY_FIELD = 'geometry_geom';
|
||||||
|
1
migrations/unreleased/0xx.dynamics-bool.down.sql
Normal file
1
migrations/unreleased/0xx.dynamics-bool.down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE buildings DROP COLUMN IF EXISTS dynamics_has_demolished_buildings;
|
7
migrations/unreleased/0xx.dynamics-bool.up.sql
Normal file
7
migrations/unreleased/0xx.dynamics-bool.up.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE buildings
|
||||||
|
ADD COLUMN dynamics_has_demolished_buildings BOOLEAN;
|
||||||
|
|
||||||
|
|
||||||
|
UPDATE buildings
|
||||||
|
SET dynamics_has_demolished_buildings = TRUE
|
||||||
|
WHERE jsonb_array_length(demolished_buildings) > 0;
|
Loading…
Reference in New Issue
Block a user