import React, { Fragment } from 'react'; import { NavLink, Redirect } from 'react-router-dom'; import Confetti from 'canvas-confetti'; import _ from 'lodash'; import { apiPost } from '../apiHelpers'; import { sendBuildingUpdate } from '../api-data/building-update'; import ErrorBox from '../components/error-box'; import InfoBox from '../components/info-box'; import { compareObjects } from '../helpers'; import { Building, BuildingEdits, BuildingUserAttributes, UserVerified } from '../models/building'; import { User } from '../models/user'; import ContainerHeader from './container-header'; import { CategoryViewProps, CopyProps } from './data-containers/category-view-props'; import { CopyControl } from './header-buttons/copy-control'; import { ViewEditControl } from './header-buttons/view-edit-control'; import './data-container.css'; import { dataFields } from '../config/data-fields-config' import { EditHistoryLatest } from './edit-history/edit-history-latest'; interface DataContainerProps { title: string; cat: string; intro: string; help: string; inactive?: boolean; user?: User; mode: 'view' | 'edit'; building?: Building; user_verified?: any; onBuildingUpdate: (buildingId: number, updatedData: Building) => void; onUserVerifiedUpdate: (buildingId: number, updatedData: UserVerified) => void; } interface DataContainerState { error: string; copying: boolean; keys_to_copy: {[key: string]: boolean}; currentBuildingId: number; currentBuildingRevisionId: number; buildingEdits: BuildingEdits; } export type DataContainerType = React.ComponentType; /** * Shared functionality for view/edit forms * * See React Higher-order-component docs for the pattern * - https://reactjs.org/docs/higher-order-components.html * * @param WrappedComponent */ const withCopyEdit: (wc: React.ComponentType) => DataContainerType = (WrappedComponent: React.ComponentType) => { return class DataContainer extends React.Component { constructor(props) { super(props); this.state = { error: undefined, copying: false, keys_to_copy: {}, buildingEdits: {}, currentBuildingId: undefined, currentBuildingRevisionId: undefined }; this.handleChange = this.handleChange.bind(this); this.handleReset = this.handleReset.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleVerify = this.handleVerify.bind(this); this.handleSaveAdd = this.handleSaveAdd.bind(this); this.handleSaveChange = this.handleSaveChange.bind(this); this.toggleCopying = this.toggleCopying.bind(this); this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this); } static getDerivedStateFromProps(props, state): DataContainerState { const newBuildingId = props.building == undefined ? undefined : props.building.building_id; const newBuildingRevisionId = props.building == undefined ? undefined : props.building.revision_id; const categoryKeys = {}; const blackListedKeys = ['current_landuse_order', 'current_landuse_verified', 'planning_list_grade', 'likes_total', 'community_local_significance_total', 'community_expected_planning_application_total' ] for (let key in dataFields) { let fieldName = props.building == undefined ? undefined : props.building[key]; if (dataFields[key].category == props.cat && fieldName != null && !blackListedKeys.includes(key)){ categoryKeys[key] = true; } if (props.cat == 'team' && key == 'date_year' && fieldName != null && !blackListedKeys.includes(key)){ categoryKeys[key] = true; } } if(newBuildingId !== state.currentBuildingId || newBuildingRevisionId > state.currentBuildingRevisionId) { return { error: undefined, copying: false, keys_to_copy: categoryKeys, buildingEdits: {}, currentBuildingId: newBuildingId, currentBuildingRevisionId: newBuildingRevisionId }; } return null; } /** * 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) * * @param {string} key */ toggleCopyAttribute(key: string) { 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 }); } isEdited() { // check if the edits object has any fields return !_.isEmpty(this.state.buildingEdits); } clearEdits() { this.setState({ buildingEdits: {} }); } 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({ buildingEdits: forwardPatch }); } /** * Handle update directly * - e.g. as callback from MultiTextInput where we set a list of strings * * @param {String} name * @param {*} value */ handleChange(name: string, value: any) { this.updateBuildingState(name, value); } handleReset() { this.clearEdits(); } async doSubmit(edits: Partial) { this.setState({error: undefined}); try { const buildingUpdate = await sendBuildingUpdate(this.props.building.building_id, edits); const updatedBuilding = Object.assign({}, this.props.building, buildingUpdate); this.props.onBuildingUpdate(this.props.building.building_id, updatedBuilding); } catch(error) { this.setState({ error }); } } 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) { const verifyPatch = {}; if (verify) { verifyPatch[slug] = this.props.building[slug]; } else { verifyPatch[slug] = null; } try { const data = await apiPost( `/api/buildings/${this.props.building.building_id}/verify.json`, verifyPatch ); if (data.error) { this.setState({error: data.error}); } else { if (verify) { Confetti({ angle: 60, disableForReducedMotion: true, particleCount: 200, ticks: 300, origin: {x, y}, zIndex: 2000 }); } this.props.onUserVerifiedUpdate(this.props.building.building_id, data); } } catch(err) { this.setState({error: err}); } if (slug == 'current_landuse_group'){ const edits = { 'current_landuse_verified': true }; this.doSubmit(edits); } console.log(slug + " verify button clicked") } render() { const currentBuilding = this.getEditedBuilding(); const values_to_copy = {}; for (const key of Object.keys(this.state.keys_to_copy)) { values_to_copy[key] = currentBuilding[key]; } const data_string = JSON.stringify(values_to_copy); const copy: CopyProps = { copying: this.state.copying, toggleCopying: this.toggleCopying, toggleCopyAttribute: this.toggleCopyAttribute, copyingKey: (key: string) => this.state.keys_to_copy[key] }; const headerBackLink = `/${this.props.mode}/categories${this.props.building != undefined ? `/${this.props.building.building_id}` : ''}`; const edited = this.isEdited(); return (
{ this.props.help && !copy.copying? Info : null } { this.props.building != undefined && !this.props.inactive ? <> { !copy.copying ? <> Edit History : null } : null }
{ this.props.inactive ? : this.props.building != undefined ?
{/* this disabled button prevents form submission on enter - see https://stackoverflow.com/a/51507806/1478817 */} { (this.props.mode === 'edit' && !this.props.inactive) ?
{ this.props.cat !== 'like' && // special-case for likes
{ edited ? : null }
}
: null } : }
); } }; }; export default withCopyEdit;