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": { 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-components/multi-data-entry/multi-data-entry.tsx b/app/src/frontend/building/data-components/multi-data-entry/multi-data-entry.tsx index c55f1026..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 @@ -1,105 +1,81 @@ -import React, { Component, Fragment } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; 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[]; - 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}); - } + const clearNew = useCallback(() => { + setNewValue(null); + }, []); - 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 - -
-
    - { - values.length === 0 && !isEditing && -
    - -
    - } + const isEditing = props.mode === 'edit'; + const isDisabled = !isEditing || props.disabled; + const slugWithModifier = props.slug + (props.slugModifier ?? ''); + + return (<> + +
    + { + values.length === 0 && !isEditing && +
    + +
    + } +
      { values.map((val, i) => (
    • @@ -108,8 +84,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,51 +97,57 @@ class MultiDataEntry extends Component { !isDisabled &&
      - + data-index={i} className="btn btn-outline-dark data-entry-list-button">
      }
    • )) } -
    { !isDisabled && -
    - this.setNewValue(val)} - onConfirm={(key, val) => this.addNew(val)} +
  • + setNewValue(val)} + onConfirm={(_key, val) => addNew(val)} - maxLength={props.maxLength} - placeholder={props.placeholder} - isUrl={props.isUrl} - valueTransform={props.valueTransform} - confirmOnEnter={props.confirmOnEnter} + maxLength={props.maxLength} + placeholder={props.placeholder} + isUrl={props.isUrl} + valueTransform={props.valueTransform} + confirmOnEnter={confirmOnEnter} - autofill={props.autofill} - showAllOptionsOnEmpty={props.showAllOptionsOnEmpty} - confirmOnAutofillSelect={true} - /> -
    - -
    -
  • + autofill={props.autofill} + showAllOptionsOnEmpty={props.showAllOptionsOnEmpty} + confirmOnAutofillSelect={true} + /> + { + newValue != undefined && + <> +
    + +
    +
    + +
    + + } + } -
    - ; - } -} - -export default MultiDataEntry; +
+
+ ) +}; 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) ?
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..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 @@ -6,10 +6,11 @@ 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'; +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 + > } = (props) => ( copyable={true} autofill={true} showAllOptionsOnEmpty={true} - addOnAutofillSelect={true} />