From ce473cb453172834efa379cd3253fd93ea24369e Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Tue, 15 Oct 2019 13:20:09 +0100 Subject: [PATCH 01/19] Fix checkbox inputs --- .../building/data-containers/planning.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/frontend/building/data-containers/planning.tsx b/app/src/frontend/building/data-containers/planning.tsx index 3a093138..1457010d 100644 --- a/app/src/frontend/building/data-containers/planning.tsx +++ b/app/src/frontend/building/data-containers/planning.tsx @@ -26,7 +26,7 @@ const PlanningView = (props) => ( value={props.building.planning_in_conservation_area} mode={props.mode} copy={props.copy} - onChange={props.onChange} + onChange={props.onCheck} /> ( value={props.building.planning_in_list} mode={props.mode} copy={props.copy} - onChange={props.onChange} + onChange={props.onCheck} /> ( value={props.building.planning_in_glher} mode={props.mode} copy={props.copy} - onChange={props.onChange} + onChange={props.onCheck} /> ( value={props.building.planning_in_apa} mode={props.mode} copy={props.copy} - onChange={props.onChange} + onChange={props.onCheck} /> ( value={props.building.planning_in_local_list} mode={props.mode} copy={props.copy} - onChange={props.onChange} + onChange={props.onCheck} /> ( value={props.building.planning_in_historic_area_assessment} mode={props.mode} copy={props.copy} - onChange={props.onChange} + onChange={props.onCheck} /> ( value={props.building.planning_demolition_proposed} mode={props.mode} copy={props.copy} - onChange={props.onChange} + onChange={props.onCheck} disabled={true} /> ( value={props.building.planning_demolition_complete} mode={props.mode} copy={props.copy} - onChange={props.onChange} + onChange={props.onCheck} disabled={true} /> Date: Tue, 15 Oct 2019 14:37:23 +0100 Subject: [PATCH 02/19] Type, simplify, fix data containers This contains a couple fixes for minor bugs that were discovered after adding static types to the category data editing code. The other changes are mostly refactoring and styling --- app/src/frontend/building/building-view.tsx | 13 +- .../frontend/building/container-header.tsx | 12 +- app/src/frontend/building/data-container.tsx | 125 ++++++++++-------- app/src/frontend/building/sidebar.css | 9 +- app/src/frontend/map-app.tsx | 4 +- app/src/frontend/models/building.ts | 8 ++ app/src/frontend/models/user.ts | 8 ++ 7 files changed, 115 insertions(+), 64 deletions(-) create mode 100644 app/src/frontend/models/building.ts create mode 100644 app/src/frontend/models/user.ts diff --git a/app/src/frontend/building/building-view.tsx b/app/src/frontend/building/building-view.tsx index 0b7a97a2..753973b8 100644 --- a/app/src/frontend/building/building-view.tsx +++ b/app/src/frontend/building/building-view.tsx @@ -14,13 +14,24 @@ import StreetscapeContainer from './data-containers/streetscape'; import CommunityContainer from './data-containers/community'; import PlanningContainer from './data-containers/planning'; import LikeContainer from './data-containers/like'; +import { Building } from '../models/building'; + + +interface BuildingViewProps { + cat: string; + mode: 'view' | 'edit' | 'multi-edit'; + building: Building; + building_like: boolean; + user: any; + selectBuilding: (building:any) => void +} /** * Top-level container for building view/edit form * * @param props */ -const BuildingView = (props) => { +const BuildingView: React.FunctionComponent = (props) => { switch (props.cat) { case 'location': return = (props) => ( +const ContainerHeader: React.FunctionComponent = (props) => (
diff --git a/app/src/frontend/building/data-container.tsx b/app/src/frontend/building/data-container.tsx index 1547f9fc..66bcb098 100644 --- a/app/src/frontend/building/data-container.tsx +++ b/app/src/frontend/building/data-container.tsx @@ -5,6 +5,28 @@ import { Redirect } from 'react-router-dom'; import ContainerHeader from './container-header'; import ErrorBox from '../components/error-box'; import InfoBox from '../components/info-box'; +import { Building } from '../models/building'; +import { User } from '../models/user'; + +interface DataContainerProps { + title: string; + cat: string; + intro: string; + help: string; + inactive?: boolean; + + user: User; + mode: 'view' | 'edit' | 'multi-edit'; + building: Building; + building_like: boolean; +} + +interface DataContainerState { + error: string; + copying: boolean; + keys_to_copy: object; + building: Building +} /** * Shared functionality for view/edit forms @@ -15,14 +37,15 @@ import InfoBox from '../components/info-box'; * @param WrappedComponent */ const withCopyEdit = (WrappedComponent) => { - return class extends React.Component { // TODO: add proper types + return class DataContainer extends React.Component { // TODO: add proper types + static displayName = 'DataContainer'; + static propTypes = { // TODO: generate propTypes from TS title: PropTypes.string, slug: PropTypes.string, intro: PropTypes.string, help: PropTypes.string, inactive: PropTypes.bool, - building_id: PropTypes.number, children: PropTypes.node }; @@ -30,8 +53,7 @@ const withCopyEdit = (WrappedComponent) => { super(props); this.state = { - error: this.props.error || undefined, - like: this.props.like || undefined, + error: undefined, copying: false, keys_to_copy: {}, building: this.props.building @@ -62,7 +84,7 @@ const withCopyEdit = (WrappedComponent) => { * @param {string} key */ toggleCopyAttribute(key: string) { - const keys = this.state.keys_to_copy; + const keys = {...this.state.keys_to_copy}; if(this.state.keys_to_copy[key]){ delete keys[key]; } else { @@ -181,7 +203,7 @@ const withCopyEdit = (WrappedComponent) => { } render() { - if (this.state.mode === 'edit' && !this.props.user){ + if (this.props.mode === 'edit' && !this.props.user){ return } @@ -198,60 +220,17 @@ const withCopyEdit = (WrappedComponent) => { } return (
+
{ - this.props.building != undefined ? -
- { - (this.props.inactive) ? - - : null - } - { - (this.props.mode === 'edit' && !this.props.inactive) ? - - - { - this.props.slug === 'like' ? // special-case for likes - null : -
- -
- } -
- : null - } - - - : -
- { - (this.props.inactive)? - + this.props.inactive ? + @@ -265,12 +244,44 @@ const withCopyEdit = (WrappedComponent) => { onLike={this.handleLike} onUpdate={this.handleUpdate} /> - - : + : + this.props.building != undefined ? + + { + (this.props.mode === 'edit' && !this.props.inactive) ? + + + { + this.props.cat === 'like' ? // special-case for likes + null : +
+ +
+ } +
+ : null + } + + : - } - } +
); } diff --git a/app/src/frontend/building/sidebar.css b/app/src/frontend/building/sidebar.css index 53caf48e..7ce6542c 100644 --- a/app/src/frontend/building/sidebar.css +++ b/app/src/frontend/building/sidebar.css @@ -139,6 +139,11 @@ /** * Data list sections */ + + .section-body { + margin-top: 0.75em; + padding: 0 0.75em; + } .data-section .h3 { margin: 0; } @@ -162,9 +167,7 @@ padding-left: 0.75rem; padding-right: 0.75rem; } -.data-section form { - padding: 0 0.75rem; -} + .data-list a { color: #555; } diff --git a/app/src/frontend/map-app.tsx b/app/src/frontend/map-app.tsx index 5191f3fe..4dcd2b76 100644 --- a/app/src/frontend/map-app.tsx +++ b/app/src/frontend/map-app.tsx @@ -199,7 +199,7 @@ class MapApp extends React.Component { } render() { - const mode = this.props.match.params.mode || 'basic'; + const mode = this.props.match.params.mode; let category = this.state.category || 'age'; @@ -240,7 +240,7 @@ class MapApp extends React.Component { Date: Tue, 15 Oct 2019 14:38:07 +0100 Subject: [PATCH 03/19] Improve scroll area UI --- app/src/frontend/building/sidebar.css | 10 +++++++++- app/src/frontend/pages/welcome.css | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/frontend/building/sidebar.css b/app/src/frontend/building/sidebar.css index 7ce6542c..95f829a4 100644 --- a/app/src/frontend/building/sidebar.css +++ b/app/src/frontend/building/sidebar.css @@ -5,7 +5,7 @@ order: 1; padding: 0 0 2em; background: #fff; - overflow-y: scroll; + overflow-y: auto; height: 40%; } @@ -34,6 +34,14 @@ text-decoration: none; color: #222; padding: 0.75rem 0.25rem 0.5rem 0; + z-index: 1000; +} + +@media (min-width: 768px) { + .section-header { + position: sticky; + top: 0; + } } .section-header h2, .section-header .icon-buttons { diff --git a/app/src/frontend/pages/welcome.css b/app/src/frontend/pages/welcome.css index d43293a7..88095379 100644 --- a/app/src/frontend/pages/welcome.css +++ b/app/src/frontend/pages/welcome.css @@ -9,7 +9,7 @@ max-height: 100%; border-radius: 0; padding: 1.5em 2.5em 2.5em; - overflow-y: scroll; + overflow-y: auto; } .welcome-float.jumbotron { background: #fff; From f498f4730b23ddd7bacf6171cccc09713c4057c0 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Tue, 15 Oct 2019 14:53:01 +0100 Subject: [PATCH 04/19] Fix view/edit route redirect --- app/src/frontend/app.tsx | 2 +- app/src/frontend/map-app.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/frontend/app.tsx b/app/src/frontend/app.tsx index 5e681574..f00428cb 100644 --- a/app/src/frontend/app.tsx +++ b/app/src/frontend/app.tsx @@ -105,7 +105,7 @@ class App extends React.Component { // TODO: add proper types - ( + ( { /> - - - + ()} + /> Date: Tue, 15 Oct 2019 15:44:22 +0100 Subject: [PATCH 05/19] Add DataTitle types --- .../building/data-components/data-title.tsx | 22 ++++++++++++++++--- app/src/frontend/building/data-container.tsx | 17 +++++++++++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/app/src/frontend/building/data-components/data-title.tsx b/app/src/frontend/building/data-components/data-title.tsx index f36022c3..d108adf3 100644 --- a/app/src/frontend/building/data-components/data-title.tsx +++ b/app/src/frontend/building/data-components/data-title.tsx @@ -3,7 +3,13 @@ import PropTypes from 'prop-types'; import Tooltip from '../../components/tooltip'; -const DataTitle: React.FunctionComponent = (props) => { + +interface DataTitleProps { + title: string; + tooltip: string; +} + +const DataTitle: React.FunctionComponent = (props) => { return (
{ props.title } @@ -17,7 +23,16 @@ DataTitle.propTypes = { tooltip: PropTypes.string } -const DataTitleCopyable: React.FunctionComponent = (props) => { // TODO: remove any + +interface DataTitleCopyableProps { + title: string; + tooltip: string; + slug: string; + disabled?: boolean; + copy: any; // TODO: type should be CopyProps, but that clashes with propTypes in some obscure way +} + +const DataTitleCopyable: React.FunctionComponent = (props) => { // TODO: remove any return (
{ props.tooltip? : null } @@ -48,7 +63,8 @@ DataTitleCopyable.propTypes = { copy: PropTypes.shape({ copying: PropTypes.bool, copyingKey: PropTypes.func, - toggleCopyAttribute: PropTypes.func + toggleCopyAttribute: PropTypes.func, + toggleCopying: PropTypes.func }) } diff --git a/app/src/frontend/building/data-container.tsx b/app/src/frontend/building/data-container.tsx index 66bcb098..821a4374 100644 --- a/app/src/frontend/building/data-container.tsx +++ b/app/src/frontend/building/data-container.tsx @@ -24,10 +24,17 @@ interface DataContainerProps { interface DataContainerState { error: string; copying: boolean; - keys_to_copy: object; + keys_to_copy: {[key: string]: boolean}; building: Building } +interface CopyProps { + copying: boolean; + toggleCopying: () => void; + toggleCopyAttribute: (key: string) => void; + copyingKey: (key: string) => boolean; +} + /** * Shared functionality for view/edit forms * @@ -212,11 +219,11 @@ const withCopyEdit = (WrappedComponent) => { values_to_copy[key] = this.state.building[key] } const data_string = JSON.stringify(values_to_copy); - const copy = { + const copy: CopyProps = { copying: this.state.copying, toggleCopying: this.toggleCopying, toggleCopyAttribute: this.toggleCopyAttribute, - copyingKey: (key) => this.state.keys_to_copy[key] + copyingKey: (key: string) => this.state.keys_to_copy[key] } return (
{ } export default withCopyEdit; + +export { + CopyProps +}; From b81d49df4370ca3c5a47c414207c560e6288f24c Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Tue, 15 Oct 2019 19:16:48 +0100 Subject: [PATCH 06/19] Store only current edits in data container state --- app/src/frontend/building/building-view.tsx | 14 +-- .../building/data-components/data-title.tsx | 2 +- app/src/frontend/building/data-container.tsx | 93 +++++++++++++------ app/src/frontend/helpers.ts | 14 ++- 4 files changed, 78 insertions(+), 45 deletions(-) diff --git a/app/src/frontend/building/building-view.tsx b/app/src/frontend/building/building-view.tsx index 753973b8..d322d984 100644 --- a/app/src/frontend/building/building-view.tsx +++ b/app/src/frontend/building/building-view.tsx @@ -23,7 +23,7 @@ interface BuildingViewProps { building: Building; building_like: boolean; user: any; - selectBuilding: (building:any) => void + selectBuilding: (building: Building) => void } /** @@ -36,7 +36,6 @@ const BuildingView: React.FunctionComponent = (props) => { case 'location': return = (props) => { case 'use': return = (props) => { case 'type': return = (props) => { case 'age': return = (props) => { case 'size': return = (props) => { case 'construction': return = (props) => { case 'team': return = (props) => { case 'sustainability': return = (props) => { case 'streetscape': return = (props) => { case 'community': return = (props) => { case 'planning': return = (props) => { case 'like': return = (props) => { // TODO: remove any diff --git a/app/src/frontend/building/data-container.tsx b/app/src/frontend/building/data-container.tsx index 821a4374..f3955f36 100644 --- a/app/src/frontend/building/data-container.tsx +++ b/app/src/frontend/building/data-container.tsx @@ -7,6 +7,7 @@ import ErrorBox from '../components/error-box'; import InfoBox from '../components/info-box'; import { Building } from '../models/building'; import { User } from '../models/user'; +import { compareObjects } from '../helpers'; interface DataContainerProps { title: string; @@ -19,13 +20,15 @@ interface DataContainerProps { mode: 'view' | 'edit' | 'multi-edit'; building: Building; building_like: boolean; + selectBuilding: (building: Building) => void } interface DataContainerState { error: string; copying: boolean; keys_to_copy: {[key: string]: boolean}; - building: Building + currentBuildingId: number; + buildingEdits: Partial; } interface CopyProps { @@ -63,7 +66,8 @@ const withCopyEdit = (WrappedComponent) => { error: undefined, copying: false, keys_to_copy: {}, - building: this.props.building + buildingEdits: {}, + currentBuildingId: undefined }; this.handleChange = this.handleChange.bind(this); @@ -76,6 +80,17 @@ const withCopyEdit = (WrappedComponent) => { this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this); } + static getDerivedStateFromProps(props, state) { + if(props.building != undefined && props.building.building_id !== state.currentBuildingId) { + return { + buildingEdits: {}, + currentBuildingId: props.building.building_id + }; + } + + return null; + } + /** * Enter or exit "copying" state - allow user to select attributes to copy */ @@ -102,18 +117,33 @@ const withCopyEdit = (WrappedComponent) => { }) } - updateBuildingState(key, value) { - const building = {...this.state.building}; - building[key] = value; + isEdited() { + const edits = this.state.buildingEdits; + // check if the edits object has any fields + return Object.entries(edits).length !== 0; + } + + getEditedBuilding() { + if(this.isEdited()) { + return Object.assign({}, this.props.building, this.state.buildingEdits); + } else { + return {...this.props.building}; + } + } + + updateBuildingState(key: string, value: any) { + const newBuilding = this.getEditedBuilding(); + newBuilding[key] = value; + const [forwardPatch] = compareObjects(this.props.building, newBuilding); this.setState({ - building: building + buildingEdits: forwardPatch }); } /** * Handle changes on typical inputs - * - e.g. input[type=text], radio, select, textare + * - e.g. input[type=text], radio, select, textarea * * @param {*} event */ @@ -185,28 +215,29 @@ const withCopyEdit = (WrappedComponent) => { ); } - handleSubmit(event) { + async handleSubmit(event) { event.preventDefault(); - this.setState({error: undefined}) + this.setState({error: undefined}); - fetch(`/api/buildings/${this.props.building.building_id}.json`, { - method: 'POST', - body: JSON.stringify(this.state.building), - headers:{ - 'Content-Type': 'application/json' - }, - credentials: 'same-origin' - }).then( - res => res.json() - ).then(function(res){ - if (res.error) { - this.setState({error: res.error}) + try { + const res = await fetch(`/api/buildings/${this.props.building.building_id}.json`, { + method: 'POST', + body: JSON.stringify(this.state.buildingEdits), + headers:{ + 'Content-Type': 'application/json' + }, + credentials: 'same-origin' + }); + const data = await res.json(); + + if (data.error) { + this.setState({error: data.error}) } else { - this.props.selectBuilding(res); + this.props.selectBuilding(data); } - }.bind(this)).catch( - (err) => this.setState({error: err}) - ); + } catch(err) { + this.setState({error: err}); + } } render() { @@ -214,9 +245,11 @@ const withCopyEdit = (WrappedComponent) => { return } + const currentBuilding = this.getEditedBuilding(); + const values_to_copy = {} for (const key of Object.keys(this.state.keys_to_copy)) { - values_to_copy[key] = this.state.building[key] + values_to_copy[key] = currentBuilding[key] } const data_string = JSON.stringify(values_to_copy); const copy: CopyProps = { @@ -262,21 +295,21 @@ const withCopyEdit = (WrappedComponent) => { { - this.props.cat === 'like' ? // special-case for likes - null : + this.isEdited() && this.props.cat !== 'like' ? // special-case for likes
-
+
: + null } : null } Date: Wed, 16 Oct 2019 13:11:25 +0100 Subject: [PATCH 07/19] Clear all state fields on select change --- app/src/frontend/building/data-container.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/frontend/building/data-container.tsx b/app/src/frontend/building/data-container.tsx index f3955f36..593c7112 100644 --- a/app/src/frontend/building/data-container.tsx +++ b/app/src/frontend/building/data-container.tsx @@ -81,10 +81,14 @@ const withCopyEdit = (WrappedComponent) => { } static getDerivedStateFromProps(props, state) { - if(props.building != undefined && props.building.building_id !== state.currentBuildingId) { + const newBuildingId = props.building == undefined ? undefined : props.building.building_id; + if(newBuildingId !== state.currentBuildingId) { return { + error: undefined, + copying: false, + keys_to_copy: {}, buildingEdits: {}, - currentBuildingId: props.building.building_id + currentBuildingId: newBuildingId }; } From 9d4d24aefc34bce753ed1a766aca23c427d7f9ae Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Wed, 16 Oct 2019 13:30:59 +0100 Subject: [PATCH 08/19] Fix label click on like checkbox with non-uniq id The label for the like checkbox was not clickable because the ID #like was not unique on the website. The ID has been changed to like_check to avoid that. --- app/src/frontend/building/data-components/like-data-entry.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/frontend/building/data-components/like-data-entry.tsx b/app/src/frontend/building/data-components/like-data-entry.tsx index 201f6e83..1e2c7c0c 100644 --- a/app/src/frontend/building/data-components/like-data-entry.tsx +++ b/app/src/frontend/building/data-components/like-data-entry.tsx @@ -29,12 +29,12 @@ const LikeDataEntry: React.FunctionComponent = (props) => { // TODO: remove }

-