From 51d8ac3ed7eeb0f7711ba7181a95d356f518a0fb Mon Sep 17 00:00:00 2001 From: Tom Russell Date: Wed, 21 Aug 2019 22:20:31 +0100 Subject: [PATCH] Fold edit-handling functionality into data-container The intention is to wrap components using withCopyEdit and to use a 'mode' prop to control view/edit/copy/paste mode. Next steps to unpack form field data components and update in eaech category container. --- app/src/frontend/building/building-edit.tsx | 368 +----------------- app/src/frontend/building/data-container.tsx | 210 +++++++++- .../building/data-containers/location.tsx | 6 +- 3 files changed, 203 insertions(+), 381 deletions(-) diff --git a/app/src/frontend/building/building-edit.tsx b/app/src/frontend/building/building-edit.tsx index 5af6c1fd..2c4f7ab5 100644 --- a/app/src/frontend/building/building-edit.tsx +++ b/app/src/frontend/building/building-edit.tsx @@ -1,367 +1,8 @@ import React, { Component, Fragment } from 'react'; -import { Link, NavLink, Redirect } from 'react-router-dom'; +import { NavLink } from 'react-router-dom'; import PropTypes from 'prop-types'; -import Sidebar from './sidebar'; -import BuildingNotFound from './building-not-found'; -import ErrorBox from '../components/error-box'; -import InfoBox from '../components/info-box'; import Tooltip from '../components/tooltip'; -import { BackIcon, SaveIcon } from '../components/icons'; - -import CONFIG from './fields-config.json'; - -const BuildingEdit = (props) => { - if (!props.user){ - return - } - const cat = props.match.params.cat; - const sections = CONFIG.filter((d) => d.slug === cat) - - if (!props.building_id || sections.length !== 1){ - return (); - } - - const section = sections[0]; - return ( - - - - ); -} - -BuildingEdit.propTypes = { - user: PropTypes.object, - match: PropTypes.object, - building_id: PropTypes.number -} - -class EditForm extends Component { // TODO: add proper types - static propTypes = { // TODO: generate propTypes from TS - title: PropTypes.string, - slug: PropTypes.string, - cat: PropTypes.string, - help: PropTypes.string, - error: PropTypes.object, - like: PropTypes.bool, - building_like: PropTypes.bool, - selectBuilding: PropTypes.func, - building_id: PropTypes.number, - inactive: PropTypes.bool, - fields: PropTypes.array - }; - - constructor(props) { - super(props); - - // create object and spread into state to avoid TS complaining about modifying readonly state - let fieldsObj = {}; - for (const field of props.fields) { - fieldsObj[field.slug] = props[field.slug]; - } - - this.state = { - error: this.props.error || undefined, - like: this.props.like || undefined, - copying: false, - keys_to_copy: {}, - ...fieldsObj - } - - this.handleChange = this.handleChange.bind(this); - this.handleCheck = this.handleCheck.bind(this); - this.handleLike = this.handleLike.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleUpdate = this.handleUpdate.bind(this); - - this.toggleCopying = this.toggleCopying.bind(this); - this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this); - } - - /** - * Enter or exit "copying" state - allow user to select attributes to copy - */ - toggleCopying() { - this.setState({ - copying: !this.state.copying - }) - } - - /** - * Keep track of data to copy (accumulate while in "copying" state) - * - * Note that we track keys only - values are already held in state - * - * @param {string} key - */ - toggleCopyAttribute(key) { - const keys = this.state.keys_to_copy; - if(this.state.keys_to_copy[key]){ - delete keys[key]; - } else { - keys[key] = true; - } - this.setState({ - keys_to_copy: keys - }) - } - - /** - * Handle changes on typical inputs - * - e.g. input[type=text], radio, select, textare - * - * @param {DocumentEvent} event - */ - handleChange(event) { - const target = event.target; - let value = (target.value === '')? null : target.value; - const name = target.name; - - // special transform - consider something data driven before adding 'else if's - if (name === 'location_postcode' && value !== null) { - value = value.toUpperCase(); - } - this.setState({ - [name]: value - }); - } - - /** - * Handle changes on checkboxes - * - e.g. input[type=checkbox] - * - * @param {DocumentEvent} event - */ - handleCheck(event) { - const target = event.target; - const value = target.checked; - const name = target.name; - - this.setState({ - [name]: value - }); - } - - /** - * Handle update directly - * - e.g. as callback from MultiTextInput where we set a list of strings - * - * @param {String} key - * @param {*} value - */ - handleUpdate(key, value) { - this.setState({ - [key]: value - }); - } - - /** - * Handle likes separately - * - like/love reaction is limited to set/unset per user - * - * @param {DocumentEvent} event - */ - handleLike(event) { - event.preventDefault(); - const like = event.target.checked; - - fetch(`/api/buildings/${this.props.building_id}/like.json`, { - method: 'POST', - headers:{ - 'Content-Type': 'application/json' - }, - credentials: 'same-origin', - body: JSON.stringify({like: like}) - }).then( - res => res.json() - ).then(function(res){ - if (res.error) { - this.setState({error: res.error}) - } else { - this.props.selectBuilding(res); - this.setState({ - likes_total: res.likes_total - }) - } - }.bind(this)).catch( - (err) => this.setState({error: err}) - ); - } - - handleSubmit(event) { - event.preventDefault(); - this.setState({error: undefined}) - - fetch(`/api/buildings/${this.props.building_id}.json`, { - method: 'POST', - body: JSON.stringify(this.state), - headers:{ - 'Content-Type': 'application/json' - }, - credentials: 'same-origin' - }).then( - res => res.json() - ).then(function(res){ - if (res.error) { - this.setState({error: res.error}) - } else { - this.props.selectBuilding(res); - } - }.bind(this)).catch( - (err) => this.setState({error: err}) - ); - } - - render() { - const match = this.props.cat === this.props.slug; - const cat = this.props.cat; - const buildingLike = this.props.building_like; - - const values_to_copy = {} - for (const key of Object.keys(this.state.keys_to_copy)) { - values_to_copy[key] = this.state[key] - } - const data_string = JSON.stringify(values_to_copy); - - return ( -
-
- - - -

{this.props.title}

- -
- { - match? ( - !this.props.inactive? -
- { - this.props.slug === 'location'? - - : null - } - - { - this.props.fields.map((props) => { - switch (props.type) { - case 'text': - return - case 'text_list': - return - case 'text_long': - return - case 'number': - return - case 'year_estimator': - return - case 'text_multi': - return - case 'checkbox': - return - case 'like': - return - default: - return null - } - }) - } - - { - (this.props.slug === 'like')? // special-case for likes - null : -
- -
- } - - :
- - - ) : null - } -
- ) - } -} const TextInput = (props) => ( @@ -598,9 +239,12 @@ class YearEstimator extends Component { // TODO: add proper types // TODO handle changes internally, reporting out date_year, date_upper, date_lower render() { return ( - + value={this.props.value} + key={this.props.slug} + /> ) } } diff --git a/app/src/frontend/building/data-container.tsx b/app/src/frontend/building/data-container.tsx index ec0c5026..407c1a58 100644 --- a/app/src/frontend/building/data-container.tsx +++ b/app/src/frontend/building/data-container.tsx @@ -1,9 +1,12 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; +import { Redirect } from 'react-router-dom'; import BuildingNotFound from './building-not-found'; import ContainerHeader from './container-header'; import Sidebar from './sidebar'; +import ErrorBox from '../components/error-box'; +import InfoBox from '../components/info-box'; /** * Shared functionality for view/edit forms @@ -27,10 +30,27 @@ const withCopyEdit = (WrappedComponent) => { constructor(props) { super(props); + + // create object and spread into state to avoid TS complaining about modifying readonly state + let fieldsObj = {}; + for (const field of props.fields) { + fieldsObj[field.slug] = props[field.slug]; + } + this.state = { + error: this.props.error || undefined, + like: this.props.like || undefined, copying: false, - values_to_copy: {} + keys_to_copy: {}, + ...fieldsObj }; + + this.handleChange = this.handleChange.bind(this); + this.handleCheck = this.handleCheck.bind(this); + this.handleLike = this.handleLike.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleUpdate = this.handleUpdate.bind(this); + this.toggleCopying = this.toggleCopying.bind(this); this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this); } @@ -49,21 +69,134 @@ const withCopyEdit = (WrappedComponent) => { * * @param {string} key */ - toggleCopyAttribute(key) { - const value = this.props.building[key]; - const values = this.state.values_to_copy; - if(Object.keys(this.state.values_to_copy).includes(key)){ - delete values[key]; + toggleCopyAttribute(key: string) { + const keys = this.state.keys_to_copy; + if(this.state.keys_to_copy[key]){ + delete keys[key]; } else { - values[key] = value; + keys[key] = true; } this.setState({ - values_to_copy: values + keys_to_copy: keys }) } + /** + * Handle changes on typical inputs + * - e.g. input[type=text], radio, select, textare + * + * @param {DocumentEvent} event + */ + handleChange(event) { + const target = event.target; + let value = (target.value === '')? null : target.value; + const name = target.name; + + // special transform - consider something data driven before adding 'else if's + if (name === 'location_postcode' && value !== null) { + value = value.toUpperCase(); + } + this.setState({ + [name]: value + }); + } + + /** + * Handle changes on checkboxes + * - e.g. input[type=checkbox] + * + * @param {DocumentEvent} event + */ + handleCheck(event) { + const target = event.target; + const value = target.checked; + const name = target.name; + + this.setState({ + [name]: value + }); + } + + /** + * Handle update directly + * - e.g. as callback from MultiTextInput where we set a list of strings + * + * @param {String} key + * @param {*} value + */ + handleUpdate(key, value) { + this.setState({ + [key]: value + }); + } + + /** + * Handle likes separately + * - like/love reaction is limited to set/unset per user + * + * @param {DocumentEvent} event + */ + handleLike(event) { + event.preventDefault(); + const like = event.target.checked; + + fetch(`/building/${this.props.building_id}/like.json`, { + method: 'POST', + headers:{ + 'Content-Type': 'application/json' + }, + credentials: 'same-origin', + body: JSON.stringify({like: like}) + }).then( + res => res.json() + ).then(function(res){ + if (res.error) { + this.setState({error: res.error}) + } else { + this.props.selectBuilding(res); + this.setState({ + likes_total: res.likes_total + }) + } + }.bind(this)).catch( + (err) => this.setState({error: err}) + ); + } + + handleSubmit(event) { + event.preventDefault(); + this.setState({error: undefined}) + + fetch(`/building/${this.props.building_id}.json`, { + method: 'POST', + body: JSON.stringify(this.state), + headers:{ + 'Content-Type': 'application/json' + }, + credentials: 'same-origin' + }).then( + res => res.json() + ).then(function(res){ + if (res.error) { + this.setState({error: res.error}) + } else { + this.props.selectBuilding(res); + } + }.bind(this)).catch( + (err) => this.setState({error: err}) + ); + } + render() { - const data_string = JSON.stringify(this.state.values_to_copy); + if (this.state.mode === 'edit' && !this.props.user){ + return + } + + const values_to_copy = {} + for (const key of Object.keys(this.state.keys_to_copy)) { + values_to_copy[key] = this.state[key] + } + const data_string = JSON.stringify(values_to_copy); const copy = { copying: this.state.copying, toggleCopying: this.toggleCopying, @@ -72,14 +205,55 @@ const withCopyEdit = (WrappedComponent) => { } return this.props.building? -
- - -
+
+
+ + + { + (this.props.mode === 'edit' && this.props.inactive)? + + : null + } + + { + (this.props.mode === 'edit' && !this.props.inactive)? + + + { + this.props.slug === 'like'? // special-case for likes + null : +
+ +
+ } +
+ : null + } + +
: } diff --git a/app/src/frontend/building/data-containers/location.tsx b/app/src/frontend/building/data-containers/location.tsx index 4638d596..60a6c3b4 100644 --- a/app/src/frontend/building/data-containers/location.tsx +++ b/app/src/frontend/building/data-containers/location.tsx @@ -1,10 +1,13 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import withCopyEdit from '../data-container'; import DataEntry from '../data-components/data-entry'; import UPRNsDataEntry from '../data-components/uprns-data-entry'; +import InfoBox from '../../components/info-box'; const LocationView = (props) => ( + +
( // "placeholder": 0 }
+
) const LocationContainer = withCopyEdit(LocationView);