diff --git a/src/client/app/components/conversion/CreateConversionModalComponent.tsx b/src/client/app/components/conversion/CreateConversionModalComponent.tsx index cb4ba8bd1..613b29963 100644 --- a/src/client/app/components/conversion/CreateConversionModalComponent.tsx +++ b/src/client/app/components/conversion/CreateConversionModalComponent.tsx @@ -77,9 +77,6 @@ export default function CreateConversionModalComponent() { setConversionState(defaultValues); }; - // Unlike edit, we decided to discard and inputs when you choose to leave the page. The reasoning is - // that create starts from an empty template. - // Submit const handleSubmit = () => { if (validConversion) { diff --git a/src/client/app/components/conversion/EditConversionModalComponent.tsx b/src/client/app/components/conversion/EditConversionModalComponent.tsx index 156f52d74..b73538e81 100644 --- a/src/client/app/components/conversion/EditConversionModalComponent.tsx +++ b/src/client/app/components/conversion/EditConversionModalComponent.tsx @@ -103,8 +103,8 @@ export default function EditConversionModalComponent(props: EditConversionModalC }; const handleClose = () => { - resetState(); props.handleClose(); + resetState(); }; // Save changes diff --git a/src/client/app/components/groups/CreateGroupModalComponent.tsx b/src/client/app/components/groups/CreateGroupModalComponent.tsx index 5eddfe35f..25b33e403 100644 --- a/src/client/app/components/groups/CreateGroupModalComponent.tsx +++ b/src/client/app/components/groups/CreateGroupModalComponent.tsx @@ -178,9 +178,6 @@ export default function CreateGroupModalComponent() { setGraphicUnitsState(graphicUnitsStateDefaults); }; - // Unlike edit, we decided to discard inputs when you choose to leave the page. The reasoning is - // that create starts from an empty template. - // Save changes const handleSubmit = () => { // Close modal first to avoid repeat clicks diff --git a/src/client/app/components/meters/CreateMeterModalComponent.tsx b/src/client/app/components/meters/CreateMeterModalComponent.tsx index 1c0ba7702..1673b05f3 100644 --- a/src/client/app/components/meters/CreateMeterModalComponent.tsx +++ b/src/client/app/components/meters/CreateMeterModalComponent.tsx @@ -102,9 +102,6 @@ export default function CreateMeterModalComponent(props: CreateMeterModalProps): resetState(); }; - // Unlike edit, we decided to discard and inputs when you choose to leave the page. The reasoning is - // that create starts from an empty template. - // Submit const handleSubmit = async () => { // Close modal first to avoid repeat clicks @@ -683,4 +680,4 @@ export default function CreateMeterModalComponent(props: CreateMeterModalProps): ); -} \ No newline at end of file +} diff --git a/src/client/app/components/unit/CreateUnitModalComponent.tsx b/src/client/app/components/unit/CreateUnitModalComponent.tsx index 621f71b23..0079079c0 100644 --- a/src/client/app/components/unit/CreateUnitModalComponent.tsx +++ b/src/client/app/components/unit/CreateUnitModalComponent.tsx @@ -15,6 +15,8 @@ import { tooltipBaseStyle } from '../../styles/modalStyle'; import { unitsApi } from '../../redux/api/unitsApi'; import { useTranslate } from '../../redux/componentHooks'; import { showSuccessNotification, showErrorNotification } from '../../utils/notifications'; +import { LineGraphRates } from '../../types/redux/graph'; +import { customRateValid, isCustomRate } from '../../utils/unitInput'; /** * Defines the create unit modal form @@ -23,6 +25,7 @@ import { showSuccessNotification, showErrorNotification } from '../../utils/noti export default function CreateUnitModalComponent() { const translate = useTranslate(); const [submitCreateUnit] = unitsApi.useAddUnitMutation(); + const CUSTOM_INPUT = '-77'; const defaultValues = { name: '', @@ -31,7 +34,7 @@ export default function CreateUnitModalComponent() { unitRepresent: UnitRepresentType.quantity, displayable: DisplayableType.all, preferredDisplay: true, - secInRate: 3600, + secInRate: LineGraphRates.hour * 3600, suffix: '', note: '', // These two values are necessary but are not used. @@ -45,14 +48,21 @@ export default function CreateUnitModalComponent() { // Unlike EditUnitModalComponent, there are no props so we don't pass show and close via props. // Modal show const [showModal, setShowModal] = useState(false); - const handleClose = () => { - setShowModal(false); - resetState(); - }; - const handleShow = () => setShowModal(true); // Handlers for each type of input change + // Current unit values const [state, setState] = useState(defaultValues); + // If user can save + const [canSave, setCanSave] = useState(false); + // Sets the starting rate for secInRate box, value of 3600 is chosen as default to result in Hour as default in dropdown box. + const [rate, setRate] = useState(String(defaultValues.secInRate)); + // Holds the value during custom value input and it is separate from standard choices. + // Needs to be valid at start and overwritten before used. + const [customRate, setCustomRate] = useState(1); + // should only update customRate when save all is clicked + // This should keep track of rate's value and set custom rate equal to it when custom rate is clicked + // True if custom value input is active. + const [showCustomInput, setShowCustomInput] = useState(false); const handleStringChange = (e: React.ChangeEvent) => { setState({ ...state, [e.target.name]: e.target.value }); @@ -62,46 +72,103 @@ export default function CreateUnitModalComponent() { setState({ ...state, [e.target.name]: JSON.parse(e.target.value) }); }; - const handleNumberChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: Number(e.target.value) }); + /** + * Updates the rate (both custom and regular state) including setting if custom. + * @param newRate The new rate to set. + */ + const updateRates = (newRate: number) => { + const isCustom = isCustomRate(newRate); + setShowCustomInput(isCustom); + if (newRate !== Number(CUSTOM_INPUT)) { + // Should only update with the new rate if did not just select custom + // input from the menu. + setCustomRate(newRate); + } + setRate(isCustom ? CUSTOM_INPUT : newRate.toString()); + }; + + // Keeps react-level state, and redux state in sync for sec. in rate. + // Two different layers in state may differ especially when externally updated (chart link, history buttons.) + React.useEffect(() => { + updateRates(state.secInRate); + }, [state.secInRate]); + + /* + UI events: + - When the user selects a new rate from the dropdown,`rate` is updated. + - If the user selects the custom value option, `showCustomInput` is set to true. + - When the user enters a custom value, `customRate` is updated. + - The initial value of `customRate` is set to the previously chosen value of `rate` + - Make sure that when submit button is clicked, that the state.secInRate is set to the correct value. + */ + const handleRateChange = (e: React.ChangeEvent) => { + const { value } = e.target; + // The input only allows a number so this should be safe. + setState({ ...state, secInRate: Number(value) }); + }; + + const handleCustomRateChange = (e: React.ChangeEvent) => { + const { value } = e.target; + // Don't update state here since wait for enter to allow to enter custom value + // that starts the same as a standard value. + setCustomRate(Number(value)); }; - /* Create Unit Validation: - Name cannot be blank - Sec in Rate must be greater than zero - If type of unit is suffix their must be a suffix - */ - const [validUnit, setValidUnit] = useState(false); + const handleEnter = (key: string) => { + // This detects the enter key and then uses the previously entered custom + // rate to set the rate as a new value. + if (key === 'Enter') { + // Form only allows integers so this should be safe. + setState({ ...state, secInRate: Number(customRate) }); + } + }; + + // Keeps canSave state up to date. Checks if valid and if edit made. useEffect(() => { - setValidUnit(state.name !== '' && state.secInRate > 0 && - (state.typeOfUnit !== UnitType.suffix || state.suffix !== '')); - }, [state.name, state.secInRate, state.typeOfUnit, state.suffix]); + // This checks: + // - Name cannot be blank + // - If type of unit is suffix there must be a suffix + // - The rate is set so not the custom input value. This happens if select custom value but don't input with enter. + // - The custom rate is a positive integer + const validUnit = state.name !== '' && + (state.typeOfUnit !== UnitType.suffix || state.suffix !== '') && state.secInRate !== Number(CUSTOM_INPUT) + && customRateValid(Number(state.secInRate)); + setCanSave(validUnit); + }, [state]); + /* End State */ // Reset the state to default values + // To be used for the discard changes and save button const resetState = () => { setState(defaultValues); + updateRates(state.secInRate); }; - // Unlike edit, we decided to discard inputs when you choose to leave the page. The reasoning is - // that create starts from an empty template. + const handleShow = () => { + setShowModal(true); + }; - // Submit - const handleSubmit = () => { + const handleClose = () => { + setShowModal(false); + resetState(); + }; + + // Save + const handleSaveChanges = () => { // Close modal first to avoid repeat clicks setShowModal(false); - // Set default identifier as name if left blank - state.identifier = (!state.identifier || state.identifier.length === 0) ? state.name : state.identifier; - // set displayable to none if unit is meter - if (state.typeOfUnit == UnitType.meter && state.displayable != DisplayableType.none) { - state.displayable = DisplayableType.none; - } - // set unit to suffix if suffix is not empty - if (state.typeOfUnit != UnitType.suffix && state.suffix != '') { - state.typeOfUnit = UnitType.suffix; - } + const submitState = { + ...state, + // Set default identifier as name if left blank + identifier: !state.identifier || state.identifier.length === 0 ? state.name : state.identifier, + // set displayable to none if unit is meter + displayable: (state.typeOfUnit == UnitType.meter && state.displayable != DisplayableType.none) ? DisplayableType.none : state.displayable, + // set unit to suffix if suffix is not empty + typeOfUnit: (state.typeOfUnit != UnitType.suffix && state.suffix != '') ? UnitType.suffix : state.typeOfUnit + }; // Add the new unit and update the store - submitCreateUnit(state) + submitCreateUnit(submitState) .unwrap() .then(() => { showSuccessNotification(translate('unit.successfully.create.unit')); @@ -120,171 +187,262 @@ export default function CreateUnitModalComponent() { return ( <> {/* Show modal button */} - - + - +
- +
{/* when any of the unit properties are changed call one of the functions. */} - - - {/* Identifier input */} - - - handleStringChange(e)} - value={state.identifier} /> - - {/* Name input */} - - - handleStringChange(e)} - value={state.name} - invalid={state.name === ''} /> - - - - - - - {/* Type of unit input */} - - - handleStringChange(e)} - value={state.typeOfUnit} - invalid={state.typeOfUnit != UnitType.suffix && state.suffix != ''}> - {Object.keys(UnitType).map(key => { - return (); - })} - - - - - - {/* Unit represent input */} - - - handleStringChange(e)} - value={state.unitRepresent}> - {Object.keys(UnitRepresentType).map(key => { - return (); - })} - - - - - {/* Displayable type input */} - - - handleStringChange(e)} - value={state.displayable} - invalid={state.displayable != DisplayableType.none && (state.typeOfUnit == UnitType.meter || state.suffix != '')}> - {Object.keys(DisplayableType).map(key => { - return (); - })} - - - {state.displayable !== DisplayableType.none && state.typeOfUnit == UnitType.meter ? ( - - ) : ( - - )} - - - {/* Preferred display input */} - - - handleBooleanChange(e)}> - {Object.keys(TrueFalseType).map(key => { - return (); - })} - - - - - - {/* Seconds in rate input */} - - - handleNumberChange(e)} - defaultValue={state.secInRate} - min='1' - invalid={state.secInRate <= 0} /> - - - - - {/* Suffix input */} - - + + + + {/* Identifier input */} + + + + handleStringChange(e)} + value={state.identifier} + /> + + + {/* Name input */} + + + + handleStringChange(e)} + value={state.name} + invalid={state.name === ''} + /> + + + + + + + + {/* Type of unit input */} + + + + handleStringChange(e)} + value={state.typeOfUnit} + invalid={state.typeOfUnit != UnitType.suffix && state.suffix != ''} + > + {Object.keys(UnitType).map(key => { + return ( + + ); + })} + + + + + + + {/* Unit represent input */} + + + + handleStringChange(e)} + value={state.unitRepresent} + > + {Object.keys(UnitRepresentType).map(key => { + return ( + + ); + })} + + + + + + {/* Displayable type input */} + + + + handleStringChange(e)} + value={state.displayable} + invalid={ + state.displayable != DisplayableType.none && + (state.typeOfUnit == UnitType.meter || state.suffix != '') + } + > + {Object.keys(DisplayableType).map(key => { + return ( + + ); + })} + + + {state.displayable !== DisplayableType.none && state.typeOfUnit == UnitType.meter ? ( + + ) : ( + + )} + + + + {/* Preferred display input */} + + + + handleBooleanChange(e)} + > + {Object.keys(TrueFalseType).map(key => { + return ( + + ); + })} + + + + + + {/* Seconds in rate input */} + + + + handleRateChange(e)} + > + {Object.entries(LineGraphRates).map( + ([rateKey, rateValue]) => ( + + ) + )} + + + {showCustomInput && ( + <> + + handleCustomRateChange(e)} + // This grabs each key hit and then finishes input when hit enter. + onKeyDown={e => { handleEnter(e.key); }} + /> + + )} + + + {translate('and')}{translate('an.integer')} + + + + {/* Suffix input */} + + + + handleStringChange(e)} + invalid={state.typeOfUnit === UnitType.suffix && state.suffix === '' + } + /> + + + + + + + {/* Note input */} + + handleStringChange(e)} - value={state.suffix} - invalid={state.typeOfUnit === UnitType.suffix && state.suffix === ''} /> - - - - - - {/* Note input */} - - - handleStringChange(e)} - value={state.note} /> - - + /> + + + {/* Hides the modal */} - {/* On click calls the function handleSaveChanges in this component */} - diff --git a/src/client/app/components/unit/EditUnitModalComponent.tsx b/src/client/app/components/unit/EditUnitModalComponent.tsx index 046a7e0b1..5bb6673bb 100644 --- a/src/client/app/components/unit/EditUnitModalComponent.tsx +++ b/src/client/app/components/unit/EditUnitModalComponent.tsx @@ -21,6 +21,8 @@ import { conversionArrow } from '../../utils/conversionArrow'; import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import { LineGraphRates } from '../../types/redux/graph'; +import { customRateValid, isCustomRate } from '../../utils/unitInput'; interface EditUnitModalComponentProps { show: boolean; @@ -40,13 +42,29 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp const translate = useTranslate(); const [submitEditedUnit] = unitsApi.useEditUnitMutation(); const [deleteUnit] = unitsApi.useDeleteUnitMutation(); + const CUSTOM_INPUT = '-77'; // Set existing unit values const values = { ...props.unit }; /* State */ // Handlers for each type of input change + // Current unit values const [state, setState] = useState(values); + // Stores if save should be allowed but check for use by a meter is delayed until + // save is hit to avoid doing a lot and to give error message then. + const [canSave, setCanSave] = useState(false); + // The rate for the unit + const [rate, setRate] = useState(String(state.secInRate)); + // Holds the value during custom value input and it is separate from standard choices. + // Needs to be valid at start and overwritten before used. + const [customRate, setCustomRate] = useState(1); + // should only update customRate when save all is clicked + // This should keep track of rate's value and set custom rate equal to it when custom rate is clicked + // True if custom value input is active. + const [showCustomInput, setShowCustomInput] = useState(false); + + // State needed to verify input const conversionData = useAppSelector(selectConversionsDetails); const meterDataByID = useAppSelector(selectMeterDataById); const unitDataByID = useAppSelector(selectUnitDataById); @@ -59,8 +77,47 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp setState({ ...state, [e.target.name]: JSON.parse(e.target.value) }); }; - const handleNumberChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: Number(e.target.value) }); + /** + * Updates the rate (both custom and regular state) including setting if custom. + * @param newRate The new rate to set. + */ + const updateRates = (newRate: number) => { + const isCustom = isCustomRate(newRate); + setShowCustomInput(isCustom); + if (newRate !== Number(CUSTOM_INPUT)) { + // Should only update with the new rate if did not just select custom + // input from the menu. + setCustomRate(newRate); + } + setRate(isCustom ? CUSTOM_INPUT : newRate.toString()); + }; + + // Keeps react-level state, and redux state in sync for sec. in rate. + // Two different layers in state may differ especially when externally updated (chart link, history buttons.) + React.useEffect(() => { + updateRates(state.secInRate); + }, [state.secInRate]); + + const handleRateChange = (e: React.ChangeEvent) => { + const { value } = e.target; + // The input only allows a number so this should be safe. + setState({ ...state, secInRate: Number(value) }); + }; + + const handleCustomRateChange = (e: React.ChangeEvent) => { + const { value } = e.target; + // Don't update state here since wait for enter to allow to enter custom value + // that starts the same as a standard value. + setCustomRate(Number(value)); + }; + + const handleEnter = (key: string) => { + // This detects the enter key and then uses the previously entered custom + // rate to set the rate as a new value. + if (key === 'Enter') { + // Form only allows integers so this should be safe. + setState({ ...state, secInRate: Number(customRate) }); + } }; /* Confirm Delete Modal */ @@ -93,18 +150,18 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp let error_message = ''; for (const value of Object.values(meterDataByID)) { // This unit is used by a meter so cannot be deleted. Note if in a group then in a meter so covers both. - if (value.unitId == state.id) { + if (value.unitId === state.id) { // TODO see EditMeterModalComponent for issue with line breaks. Same issue in strings below. error_message += ` ${translate('meter')} "${value.name}" ${translate('uses')} ${translate('unit')} ` + `"${state.name}" ${translate('as.meter.unit')};`; } - if (value.defaultGraphicUnit == state.id) { + if (value.defaultGraphicUnit === state.id) { error_message += ` ${translate('meter')} "${value.name}" ${translate('uses')} ${translate('unit')} ` + `"${state.name}" ${translate('as.meter.defaultgraphicunit')};`; } } for (let i = 0; i < conversionData.length; i++) { - if (conversionData[i].sourceId == state.id) { + if (conversionData[i].sourceId === state.id) { // This unit is the source of a conversion so cannot be deleted. error_message += ` ${translate('conversion')} ${unitDataByID[conversionData[i].sourceId].name}` + `${conversionArrow(conversionData[i].bidirectional)}` + @@ -112,7 +169,7 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp ` "${state.name}" ${translate('unit.source.error')};`; } - if (conversionData[i].destinationId == state.id) { + if (conversionData[i].destinationId === state.id) { // This unit is the destination of a conversion so cannot be deleted. error_message += ` ${translate('conversion')} ${unitDataByID[conversionData[i].sourceId].name}` + `${conversionArrow(conversionData[i].bidirectional)}` + @@ -132,17 +189,31 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp } }; - /* Edit Unit Validation: - Name cannot be blank - Sec in Rate must be greater than zero - Unit type mismatches checked on submit - If type of unit is suffix their must be a suffix - */ - const [validUnit, setValidUnit] = useState(false); + // Keeps canSave state up to date. Checks if valid and if edit made. useEffect(() => { - setValidUnit(state.name !== '' && state.secInRate > 0 && - (state.typeOfUnit !== UnitType.suffix || state.suffix !== '')); - }, [state.name, state.secInRate, state.typeOfUnit, state.suffix]); + // This checks: + // - Name cannot be blank + // - If type of unit is suffix there must be a suffix + // - The rate is set so not the custom input value. This happens if select custom value but don't input with enter. + // - The custom rate is a positive integer + const validUnit = state.name !== '' && + (state.typeOfUnit !== UnitType.suffix || state.suffix !== '') && state.secInRate !== Number(CUSTOM_INPUT) + && customRateValid(Number(state.secInRate)); + // Compare original props to state to see if edit made. Check above avoids thinking edit happened if + // custom edit started without enter hit. + const editMade = + props.unit.name !== state.name + || props.unit.identifier !== state.identifier + || props.unit.typeOfUnit !== state.typeOfUnit + || props.unit.unitRepresent !== state.unitRepresent + || props.unit.displayable !== state.displayable + || props.unit.preferredDisplay !== state.preferredDisplay + || props.unit.secInRate !== state.secInRate + || props.unit.suffix !== state.suffix + || props.unit.note !== state.note; + setCanSave(validUnit && editMade); + }, [state]); + /* End State */ // Reset the state to default values @@ -152,6 +223,7 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp // Failure to edit units will not trigger a re-render, as no state has changed. Therefore, we must manually reset the values const resetState = () => { setState(values); + updateRates(state.secInRate); }; const handleShow = () => { @@ -166,7 +238,7 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp // Validate the changes and return true if we should update this unit. // Two reasons for not updating the unit: // 1. typeOfUnit is changed from meter to something else while some meters are still linked with this unit - // 2. There are no changes + // 2. There are no changes but save button should stop this. const shouldUpdateUnit = (): boolean => { // true if inputted values are okay and there are changes. let inputOk = true; @@ -183,17 +255,10 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp } } if (inputOk) { - // The input passed validation so check if changes exist. - // Check for case 2 by comparing state to props - return props.unit.name != state.name - || props.unit.identifier != state.identifier - || props.unit.typeOfUnit != state.typeOfUnit - || props.unit.unitRepresent != state.unitRepresent - || props.unit.displayable != state.displayable - || props.unit.preferredDisplay != state.preferredDisplay - || props.unit.secInRate != state.secInRate - || props.unit.suffix != state.suffix - || props.unit.note != state.note; + // The input passed validation so return if canSave set. + // In principle this should always be true since hit save + // be here to be safe and due to old logic setup. + return canSave; } else { // Tell user that not going to update due to input issues. showErrorNotification(`${translate('unit.input.error')}`); @@ -210,6 +275,18 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp props.handleClose(); if (shouldUpdateUnit()) { + + const submitState = { + ...state, + // The updated unit is not fetched to save time. However, the identifier might have been + // automatically set if it was empty. Mimic that here. + identifier: (state.identifier === '') ? state.name : state.identifier, + // set displayable to none if unit is meter + displayable: (state.typeOfUnit === UnitType.meter && state.displayable !== DisplayableType.none) ? DisplayableType.none : state.displayable, + // set unit to suffix if suffix is not empty + typeOfUnit: (state.typeOfUnit !== UnitType.suffix && state.suffix !== '') ? UnitType.suffix : state.typeOfUnit + }; + // Need to redo Cik if the suffix, displayable, or type of unit changes. // For displayable, it only matters if it changes from/to NONE but a more general check is used here for simplification. const shouldRedoCik = props.unit.suffix !== state.suffix @@ -219,16 +296,10 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp const shouldRefreshReadingViews = props.unit.unitRepresent !== state.unitRepresent || (props.unit.secInRate !== state.secInRate && (props.unit.unitRepresent === UnitRepresentType.flow || props.unit.unitRepresent === UnitRepresentType.raw)); - // set displayable to none if unit is meter - if (state.typeOfUnit == UnitType.meter && state.displayable != DisplayableType.none) { - state.displayable = DisplayableType.none; - } - // set unit to suffix if suffix is not empty - if (state.typeOfUnit != UnitType.suffix && state.suffix != '') { - state.typeOfUnit = UnitType.suffix; - } + + // Save our changes by dispatching the submitEditedUnit mutation - submitEditedUnit({ editedUnit: state, shouldRedoCik, shouldRefreshReadingViews }) + submitEditedUnit({ editedUnit: submitState, shouldRedoCik, shouldRefreshReadingViews }) .unwrap() .then(() => { showSuccessNotification(translate('unit.successfully.edited.unit')); @@ -236,11 +307,6 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp .catch(() => { showErrorNotification(translate('unit.failed.to.edit.unit')); }); - // The updated unit is not fetched to save time. However, the identifier might have been - // automatically set if it was empty. Mimic that here. - if (state.identifier === '') { - state.identifier = state.name; - } } }; @@ -279,157 +345,227 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp {/* when any of the unit are changed call one of the functions. */} - - - {/* Identifier input */} - - - handleStringChange(e)} - value={state.identifier} - placeholder='Identifier' /> - - {/* Name input */} - - - handleStringChange(e)} - value={state.name} - invalid={state.name === ''} /> - - - - - - - {/* Type of unit input */} - - - handleStringChange(e)} - value={state.typeOfUnit} - invalid={state.typeOfUnit != UnitType.suffix && state.suffix != ''}> - {Object.keys(UnitType).map(key => { - return (); - })} - - - - - - {/* Unit represent input */} - - - handleStringChange(e)}> - {Object.keys(UnitRepresentType).map(key => { - return (); - })} - - - - - {/* Displayable type input */} - - - handleStringChange(e)} - invalid={state.displayable != DisplayableType.none && (state.typeOfUnit == UnitType.meter || state.suffix != '')}> - {Object.keys(DisplayableType).map(key => { - return (); - })} - - - {state.displayable !== DisplayableType.none && state.typeOfUnit == UnitType.meter ? ( - - ) : ( - - )} - - - {/* Preferred display input */} - - - handleBooleanChange(e)}> - {Object.keys(TrueFalseType).map(key => { - return (); - })} - - - - - - {/* Seconds in rate input */} - - - handleNumberChange(e)} - placeholder='Sec In Rate' - min='1' - invalid={state.secInRate <= 0} /> - - - - - {/* Suffix input */} - - + + + + {/* Identifier input */} + + + + handleStringChange(e)} + value={state.identifier} /> + + + {/* Name input */} + + + + handleStringChange(e)} + value={state.name} + invalid={state.name === ''} /> + + + + + + + + {/* Type of unit input */} + + + + handleStringChange(e)} + value={state.typeOfUnit} + invalid={state.typeOfUnit !== UnitType.suffix && state.suffix !== ''} + > + {Object.keys(UnitType).map(key => { + return ( + + ); + })} + + + + + + + {/* Unit represent input */} + + + + handleStringChange(e)} + > + {Object.keys(UnitRepresentType).map(key => { + return ( + ); + })} + + + + + + {/* Displayable type input */} + + + + handleStringChange(e)} + invalid={ + state.displayable !== DisplayableType.none && + (state.typeOfUnit === UnitType.meter || state.suffix !== '') + } + > + {Object.keys(DisplayableType).map(key => { + return ( + ); + })} + + + {state.displayable !== DisplayableType.none && state.typeOfUnit === UnitType.meter ? ( + + ) : ( + + )} + + + + {/* Preferred display input */} + + + + handleBooleanChange(e)}> + {Object.keys(TrueFalseType).map(key => { + return ( + + ); + })} + + + + + + {/* Seconds in rate input */} + + + + handleRateChange(e)}> + {Object.entries(LineGraphRates).map( + ([rateKey, rateValue]) => ( + + ) + )} + + + {showCustomInput && ( + <> + + handleCustomRateChange(e)} + // This grabs each key hit and then finishes input when hit enter. + onKeyDown={e => { handleEnter(e.key); }} + /> + + )} + + + {translate('and')}{translate('an.integer')} + + + + {/* Suffix input */} + + + + handleStringChange(e)} + invalid={state.typeOfUnit === UnitType.suffix && state.suffix === ''} /> + + + + + + + {/* Note input */} + + handleStringChange(e)} - invalid={state.typeOfUnit === UnitType.suffix && state.suffix === ''} /> - - - - - - {/* Note input */} - - - handleStringChange(e)} /> - - + id='note' + name='note' + type='textarea' + value={state.note} + onChange={e => handleStringChange(e)} /> + + + {/* On click calls the function handleSaveChanges in this component */} - diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index b757f61e8..5d0858c6c 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -16,6 +16,8 @@ const LocaleTranslationData = { "admin.only": "Admin Only", "admin.settings": "Admin Settings", "alphabetically": "Alphabetically", + "an.integer": "an integer", + "and": " and ", "area": "Area:", "area.but.no.unit": "You have entered a nonzero area but no area unit.", "area.calculate.auto": "Calculate Group Area", @@ -29,6 +31,7 @@ const LocaleTranslationData = { "as.meter.unit": "as meter unit", "ascending": "Ascending", "bar": "Bar", + "bar.days.enter": "Input days and then hit enter", "bar.interval": "Bar Interval", "bar.raw": "Cannot create bar graph on raw units such as temperature", "bar.stacking": "Bar Stacking", @@ -485,6 +488,7 @@ const LocaleTranslationData = { "unit.preferred.display": "Preferred Display:", "unit.represent": "Unit Represent:", "unit.sec.in.rate": "Sec in Rate:", + "unit.sec.in.rate.enter": "Input seconds in rate and then hit enter:", "unit.source.error": "as the source unit", "unit.submit.new.unit": "Submit New Unit", "unit.successfully.create.unit": "Successfully created a unit.", @@ -550,6 +554,8 @@ const LocaleTranslationData = { "admin.only": "Uniquement pour Les Administrateurs", "admin.settings": "Admin Settings\u{26A1}", "alphabetically": "Alphabétiquement", + "an.integer": "an integer\u{26A1}", + "and": " and\u{26A1} ", "area": "Région:", "area.but.no.unit": "You have entered a nonzero area but no area unit.\u{26A1}", "area.calculate.auto": "Calculate Group Area\u{26A1}", @@ -563,6 +569,7 @@ const LocaleTranslationData = { "as.meter.unit": "as meter unit\u{26A1}", "ascending": "Ascendant", "bar": "Bande", + "bar.days.enter": "Input days and then hit enter\u{26A1}", "bar.interval": "Intervalle du Diagramme à Bandes", "bar.raw": "Cannot create bar graph on raw units such as temperature\u{26A1}", "bar.stacking": "Empilage de Bandes", @@ -1016,6 +1023,7 @@ const LocaleTranslationData = { "unit.preferred.display": "Preferred Display:\u{26A1}", "unit.represent": "Unit Represent:\u{26A1}", "unit.sec.in.rate": "Sec in Rate:\u{26A1}", + "unit.sec.in.rate.enter": "Input seconds in rate and then hit enter:\u{26A1}", "unit.source.error": "as the source unit\u{26A1}", "unit.submit.new.unit": "Submit New Unit\u{26A1}", "unit.successfully.create.unit": "Successfully created a unit.\u{26A1}", @@ -1084,6 +1092,8 @@ const LocaleTranslationData = { "admin.only": "Solo administrador", "admin.settings": "Admin Settings\u{26A1}", "alphabetically": "Alfabéticamente", + "an.integer": "an integer\u{26A1}", + "and": " y ", "area": "Área:", "area.but.no.unit": "Ha ingresado un área distinta a cero sin unidad de área.", "area.calculate.auto": "Calcular el área del grupo", @@ -1097,6 +1107,7 @@ const LocaleTranslationData = { "as.meter.unit": "como unidad de medidor", "ascending": "Ascendiente", "bar": "Barra", + "bar.days.enter": "input\u{26A1} los días y presione \"Enter\"", "bar.interval": "Intervalo de barra", "bar.raw": "No se puede crear un gráfico de barras con unidades crudas como la temperatura", "bar.stacking": "Apilamiento de barras", @@ -1554,6 +1565,7 @@ const LocaleTranslationData = { "unit.preferred.display": "Visualización preferida:", "unit.represent": "Unidad representa:", "unit.sec.in.rate": "Segundos en tasa:", + "unit.sec.in.rate.enter": "Input seconds in rate and then hit enter:\u{26A1}", "unit.source.error": "como la unidad de la fuente", "unit.submit.new.unit": "Ingresar una nueva unidad", "unit.successfully.create.unit": "Unidad creada con éxito.", diff --git a/src/client/app/utils/unitInput.ts b/src/client/app/utils/unitInput.ts new file mode 100644 index 000000000..01a275a0a --- /dev/null +++ b/src/client/app/utils/unitInput.ts @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { LineGraphRates } from '../types/redux/graph'; + +// Checks if custom rate is valid by verifying that it is a positive integer. +export const customRateValid = (customRate: number) => { + return Number.isInteger(customRate) && customRate >= 1; +}; + +/** + * Determines if the rate is custom. + * @param rate The rate to check + * @returns true if the rate is custom and false if it is a standard value. + */ +export const isCustomRate = (rate: number) => { + // Check if the rate is a custom rate. + return !Object.entries(LineGraphRates).some( + ([, rateValue]) => { + // Multiply each rate value by 3600, round it to the nearest integer, + // and compare it to the given rate + return Math.round(rateValue * 3600) === rate; + }); +};