From 6ecd5b6819b7a36c9480df6b973ca8df56c75643 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Mon, 15 Mar 2021 19:32:10 +0000 Subject: [PATCH 1/4] Change MultiDataEntry to FC with hooks --- .../multi-data-entry/multi-data-entry.tsx | 157 +++++++----------- .../frontend/building/data-containers/age.tsx | 2 +- .../dynamics/dynamics-data-entry.tsx | 2 +- .../frontend/building/data-containers/use.tsx | 3 +- 4 files changed, 66 insertions(+), 98 deletions(-) diff --git a/app/src/frontend/building/data-components/multi-data-entry/multi-data-entry.tsx b/app/src/frontend/building/data-components/multi-data-entry/multi-data-entry.tsx index c55f1026..3614e642 100644 --- a/app/src/frontend/building/data-components/multi-data-entry/multi-data-entry.tsx +++ b/app/src/frontend/building/data-components/multi-data-entry/multi-data-entry.tsx @@ -1,4 +1,4 @@ -import React, { Component, Fragment } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import './multi-data-entry.css'; @@ -6,94 +6,66 @@ import { BaseDataEntryProps } from '../data-entry'; import { DataEntryInput, TextDataEntryInputProps } from '../data-entry-input'; import { DataTitleCopyable } from '../data-title'; - interface MultiDataEntryProps extends BaseDataEntryProps, TextDataEntryInputProps { value: string[]; - copyable: boolean; - editableEntries: boolean; - confirmOnEnter: boolean; - - addOnAutofillSelect: boolean; - acceptAutofillValuesOnly: boolean; + copyable?: boolean; + editableEntries?: boolean; + confirmOnEnter?: boolean; } -interface MultiDataEntryState { - newValue: string; -} +export const MultiDataEntry: React.FC = ({ + editableEntries = false, + copyable = false, + confirmOnEnter = true, + ...props +}) => { + const [newValue, setNewValue] = useState(); -class MultiDataEntry extends Component { + const values = useMemo(() => props.value ?? [], [props.value]); - static defaultProps = { - editableEntries: false, - copyable: false, - confirmOnEnter: true, - addOnAutofillSelect: false, - acceptAutofillValuesOnly: false - }; + const edit = useCallback((index: number, value: string) => { + const editedValues = values.slice(); + editedValues.splice(index, 1, value); + props.onChange(props.slug, editedValues); + }, [values, props.onChange, props.slug]); - constructor(props) { - super(props); - this.state = { - newValue: null - }; - - this.setNewValue = this.setNewValue.bind(this); - this.edit = this.edit.bind(this); - this.addNew = this.addNew.bind(this); - this.remove = this.remove.bind(this); - } + /* accept a newValue parameter to handle cases where the value is set and submitted at the same time + * (like with autofill select enabled) - but otherwise use the current newValue saved in state + */ + const addNew = useCallback((newValueArg?: string) => { + const val = newValueArg ?? newValue; + if(val == undefined) return; - getValues() { - return this.props.value == undefined ? [] : this.props.value; - } + const editedValues = values.slice().concat(val); - cloneValues() { - return this.getValues().slice(); - } + setNewValue(null); + props.onChange(props.slug, editedValues); + }, [newValue, values, props.onChange, props.slug]); - setNewValue(value: string) { - this.setState({newValue: value}); - } - edit(index: number, value: string) { - let values = this.cloneValues(); - values.splice(index, 1, value); - this.props.onChange(this.props.slug, values); - } - addNew(newValue?: string) { - // accept a newValue parameter to handle cases where the value is set and submitted at the same time - // (like with autofill select enabled) - but otherwise use the current newValue saved in state - const val = newValue ?? this.state.newValue; - if (val == undefined) return; - const values = this.cloneValues().concat(val); - this.setState({newValue: null}); - this.props.onChange(this.props.slug, values); - } + const remove = useCallback((index: number) => { + const editedValues = values.slice(); + editedValues.splice(index, 1); - remove(index: number){ - const values = this.cloneValues(); - values.splice(index, 1); - this.props.onChange(this.props.slug, values); - } + props.onChange(props.slug, editedValues); + }, [values, props.onChange, props.slug]); - render() { - const values = this.getValues(); - const props = this.props; - const isEditing = props.mode === 'edit'; - const isDisabled = !isEditing || props.disabled; - const slugWithModifier = props.slug + (props.slugModifier ?? ''); - return - -
-
    + const isEditing = props.mode === 'edit'; + const isDisabled = !isEditing || props.disabled; + const slugWithModifier = props.slug + (props.slugModifier ?? ''); + + return (<> + +
    +
      { values.length === 0 && !isEditing &&
      @@ -108,8 +80,8 @@ class MultiDataEntry extends Component name={`${slugWithModifier}-${i}`} id={`${slugWithModifier}-${i}`} value={val} - disabled={!props.editableEntries || isDisabled} - onChange={(key, val) => this.edit(i, val)} + disabled={!editableEntries || isDisabled} + onChange={(_key, val) => edit(i, val)} maxLength={props.maxLength} isUrl={props.isUrl} @@ -121,7 +93,7 @@ class MultiDataEntry extends Component { !isDisabled &&
      -
      @@ -129,25 +101,25 @@ class MultiDataEntry extends Component )) } -
    - { - !isDisabled && +
+ { + !isDisabled &&
this.setNewValue(val)} - onConfirm={(key, val) => this.addNew(val)} + onChange={(_key, val) => setNewValue(val)} + onConfirm={(_key, val) => addNew(val)} maxLength={props.maxLength} placeholder={props.placeholder} isUrl={props.isUrl} valueTransform={props.valueTransform} - confirmOnEnter={props.confirmOnEnter} + confirmOnEnter={confirmOnEnter} autofill={props.autofill} showAllOptionsOnEmpty={props.showAllOptionsOnEmpty} @@ -157,15 +129,12 @@ class MultiDataEntry extends Component
- } - -
; - } -} - -export default MultiDataEntry; + } + + ) +}; diff --git a/app/src/frontend/building/data-containers/age.tsx b/app/src/frontend/building/data-containers/age.tsx index 2ac2f4a0..7175d54d 100644 --- a/app/src/frontend/building/data-containers/age.tsx +++ b/app/src/frontend/building/data-containers/age.tsx @@ -1,7 +1,7 @@ import React, { Fragment } from 'react'; import { dataFields } from '../../config/data-fields-config'; -import MultiDataEntry from '../data-components/multi-data-entry/multi-data-entry'; +import { MultiDataEntry } from '../data-components/multi-data-entry/multi-data-entry'; import NumericDataEntry from '../data-components/numeric-data-entry'; import SelectDataEntry from '../data-components/select-data-entry'; import TextboxDataEntry from '../data-components/textbox-data-entry'; diff --git a/app/src/frontend/building/data-containers/dynamics/dynamics-data-entry.tsx b/app/src/frontend/building/data-containers/dynamics/dynamics-data-entry.tsx index 3bcaa6a3..193ee940 100644 --- a/app/src/frontend/building/data-containers/dynamics/dynamics-data-entry.tsx +++ b/app/src/frontend/building/data-containers/dynamics/dynamics-data-entry.tsx @@ -6,7 +6,7 @@ import { FieldRow } from '../../data-components/field-row'; import DataEntry, { BaseDataEntryProps } from '../../data-components/data-entry'; import { dataFields } from '../../../config/data-fields-config'; import SelectDataEntry from '../../data-components/select-data-entry'; -import MultiDataEntry from '../../data-components/multi-data-entry/multi-data-entry'; +import { MultiDataEntry } from '../../data-components/multi-data-entry/multi-data-entry'; import { NumberRangeDataEntry } from './number-range-data-entry'; import './dynamics-data-entry.css'; diff --git a/app/src/frontend/building/data-containers/use.tsx b/app/src/frontend/building/data-containers/use.tsx index 545b569a..26ff6e11 100644 --- a/app/src/frontend/building/data-containers/use.tsx +++ b/app/src/frontend/building/data-containers/use.tsx @@ -3,7 +3,7 @@ import React, { Fragment } from 'react'; import InfoBox from '../../components/info-box'; import { dataFields } from '../../config/data-fields-config'; import DataEntry from '../data-components/data-entry'; -import MultiDataEntry from '../data-components/multi-data-entry/multi-data-entry'; +import { MultiDataEntry } from '../data-components/multi-data-entry/multi-data-entry'; import withCopyEdit from '../data-container'; import { CategoryViewProps } from './category-view-props'; @@ -28,7 +28,6 @@ const UseView: React.FunctionComponent = (props) => ( copyable={true} autofill={true} showAllOptionsOnEmpty={true} - addOnAutofillSelect={true} /> Date: Mon, 15 Mar 2021 20:49:28 +0000 Subject: [PATCH 2/4] Disable prop types eslint rule --- app/.eslintrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/.eslintrc b/app/.eslintrc index 32a67b77..397886bd 100644 --- a/app/.eslintrc +++ b/app/.eslintrc @@ -39,7 +39,8 @@ "camelcase": ["warn", {"ignoreDestructuring": true ,"properties": "never"}], "eol-last": ["warn", "always"], "no-multiple-empty-lines": ["warn", {"max": 1}], - "prefer-const": "warn" + "prefer-const": "warn", + "react/prop-types": "off" }, "settings": { "react": { From c9176f96ac90e55bd70b29495ff52ad5e5268afb Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Tue, 16 Mar 2021 19:00:54 +0000 Subject: [PATCH 3/4] Prevent form submit on enter without JavaScript --- app/src/frontend/building/data-components/data-entry-input.tsx | 2 -- app/src/frontend/building/data-container.tsx | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/frontend/building/data-components/data-entry-input.tsx b/app/src/frontend/building/data-components/data-entry-input.tsx index 8bcd9360..b768a831 100644 --- a/app/src/frontend/building/data-components/data-entry-input.tsx +++ b/app/src/frontend/building/data-components/data-entry-input.tsx @@ -64,8 +64,6 @@ export const DataEntryInput: React.FC { if(e.keyCode === 13) { - // prevent form submit on enter - e.preventDefault(); if(props.confirmOnEnter) { handleConfirm(); } diff --git a/app/src/frontend/building/data-container.tsx b/app/src/frontend/building/data-container.tsx index 0ef6a3dd..a74b5d79 100644 --- a/app/src/frontend/building/data-container.tsx +++ b/app/src/frontend/building/data-container.tsx @@ -364,6 +364,8 @@ const withCopyEdit: (wc: React.ComponentType) => DataContaine action={`/edit/${this.props.cat}/${this.props.building.building_id}`} method="POST" onSubmit={this.handleSubmit}> + {/* this disabled button prevents form submission on enter - see https://stackoverflow.com/a/51507806/1478817 */} + { (this.props.mode === 'edit' && !this.props.inactive) ?
From 27b7b72a576f1e47dcfee4ca499b900a6e5ac4d7 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Tue, 16 Mar 2021 19:02:11 +0000 Subject: [PATCH 4/4] Improve data netry list UI for new entry --- .../multi-data-entry/multi-data-entry.tsx | 95 +++++++++++-------- .../dynamics/dynamics-data-entry.tsx | 5 +- app/src/frontend/building/sidebar.css | 8 +- app/src/frontend/styles/forms.css | 4 +- 4 files changed, 64 insertions(+), 48 deletions(-) diff --git a/app/src/frontend/building/data-components/multi-data-entry/multi-data-entry.tsx b/app/src/frontend/building/data-components/multi-data-entry/multi-data-entry.tsx index 3614e642..50e38742 100644 --- a/app/src/frontend/building/data-components/multi-data-entry/multi-data-entry.tsx +++ b/app/src/frontend/building/data-components/multi-data-entry/multi-data-entry.tsx @@ -5,6 +5,7 @@ import './multi-data-entry.css'; import { BaseDataEntryProps } from '../data-entry'; import { DataEntryInput, TextDataEntryInputProps } from '../data-entry-input'; import { DataTitleCopyable } from '../data-title'; +import { CloseIcon, SaveIcon } from '../../../components/icons'; interface MultiDataEntryProps extends BaseDataEntryProps, TextDataEntryInputProps { value: string[]; @@ -42,6 +43,9 @@ export const MultiDataEntry: React.FC = ({ props.onChange(props.slug, editedValues); }, [newValue, values, props.onChange, props.slug]); + const clearNew = useCallback(() => { + setNewValue(null); + }, []); const remove = useCallback((index: number) => { const editedValues = values.slice(); @@ -65,13 +69,13 @@ export const MultiDataEntry: React.FC = ({ copy={copyable ? props.copy : undefined} />
-
    - { - values.length === 0 && !isEditing && -
    - -
    - } + { + values.length === 0 && !isEditing && +
    + +
    + } +
      { values.map((val, i) => (
    • @@ -95,46 +99,55 @@ export const MultiDataEntry: React.FC = ({
      + data-index={i} className="btn btn-outline-dark data-entry-list-button">
      }
    • )) } + { + !isDisabled && +
    • + setNewValue(val)} + onConfirm={(_key, val) => addNew(val)} + + maxLength={props.maxLength} + placeholder={props.placeholder} + isUrl={props.isUrl} + valueTransform={props.valueTransform} + confirmOnEnter={confirmOnEnter} + + autofill={props.autofill} + showAllOptionsOnEmpty={props.showAllOptionsOnEmpty} + confirmOnAutofillSelect={true} + /> + { + newValue != undefined && + <> +
      + +
      +
      + +
      + + } +
    • + }
    - { - !isDisabled && -
    - setNewValue(val)} - onConfirm={(_key, val) => addNew(val)} - - maxLength={props.maxLength} - placeholder={props.placeholder} - isUrl={props.isUrl} - valueTransform={props.valueTransform} - confirmOnEnter={confirmOnEnter} - - autofill={props.autofill} - showAllOptionsOnEmpty={props.showAllOptionsOnEmpty} - confirmOnAutofillSelect={true} - /> -
    - -
    -
    - }
) }; diff --git a/app/src/frontend/building/data-containers/dynamics/dynamics-data-entry.tsx b/app/src/frontend/building/data-containers/dynamics/dynamics-data-entry.tsx index 193ee940..53e7d780 100644 --- a/app/src/frontend/building/data-containers/dynamics/dynamics-data-entry.tsx +++ b/app/src/frontend/building/data-containers/dynamics/dynamics-data-entry.tsx @@ -10,6 +10,7 @@ import { MultiDataEntry } from '../../data-components/multi-data-entry/multi-dat import { NumberRangeDataEntry } from './number-range-data-entry'; import './dynamics-data-entry.css'; +import { CloseIcon } from '../../../components/icons'; type DemolishedBuilding = (BuildingAttributes['demolished_buildings'][number]); @@ -211,7 +212,7 @@ export const DynamicsDataEntry: React.FC = (props) => { } -
    +
      { values.length === 0 &&
      @@ -229,7 +230,7 @@ export const DynamicsDataEntry: React.FC = (props) => { title="Delete Record" onClick={() => remove(id)} data-index={id} - >x + > }