From 2baeda65fffdea9e54a5ca6ce75ede2d959a3d61 Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Thu, 21 Nov 2024 14:19:35 +0100 Subject: [PATCH 01/12] fix: refactor to remove useField dependencies [DHIS2-18373] --- .../section-filter-selector-bar-item.js | 2 +- .../data-entry-cell/data-entry-field.js | 10 +- .../data-entry-cell/entry-field-input.js | 37 +++++- .../data-entry-cell/inner-wrapper.js | 47 ++------ src/data-workspace/entry-form.js | 28 +---- src/data-workspace/final-form-wrapper.js | 14 ++- src/data-workspace/inputs/boolean-radios.js | 110 ++++++----------- src/data-workspace/inputs/date-input.js | 50 ++++---- src/data-workspace/inputs/date-input.test.js | 2 + src/data-workspace/inputs/datetime-input.js | 53 ++++---- .../inputs/datetime-input.test.js | 2 + src/data-workspace/inputs/file-inputs.js | 82 +++++++------ src/data-workspace/inputs/generic-input.js | 113 +++++++++--------- src/data-workspace/inputs/long-text.js | 38 +++--- src/data-workspace/inputs/option-set.js | 96 +++++++-------- .../inputs/true-only-checkbox.js | 48 ++++---- src/data-workspace/inputs/utils.js | 12 ++ src/shared/stores/data-value-store.js | 8 ++ src/shared/stores/entry-form-store.js | 43 ++++--- 19 files changed, 389 insertions(+), 406 deletions(-) diff --git a/src/context-selection/section-filter-selector-bar-item/section-filter-selector-bar-item.js b/src/context-selection/section-filter-selector-bar-item/section-filter-selector-bar-item.js index cdac27f94..99239a69b 100644 --- a/src/context-selection/section-filter-selector-bar-item/section-filter-selector-bar-item.js +++ b/src/context-selection/section-filter-selector-bar-item/section-filter-selector-bar-item.js @@ -29,7 +29,7 @@ export default function SectionFilterSelectorBarItem() { useEffect(() => { const sections = dataSet?.sections if (sectionFilter === undefined && sections?.length) { - setSectionFilter(sections[0].id) + // setSectionFilter(sections[0].id) } // clear out section if it is invalid if ( diff --git a/src/data-workspace/data-entry-cell/data-entry-field.js b/src/data-workspace/data-entry-cell/data-entry-field.js index b05c8b156..9b81a13a4 100644 --- a/src/data-workspace/data-entry-cell/data-entry-field.js +++ b/src/data-workspace/data-entry-cell/data-entry-field.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types' -import React from 'react' +import React, { useState } from 'react' import { useLockedContext, useHighlightedFieldStore, @@ -25,6 +25,9 @@ export const DataEntryField = React.memo(function DataEntryField({ const { locked } = useLockedContext() + const [active, setActive] = useState(false) + const [valueSynced, setValueSynced] = useState(false) + return ( ) diff --git a/src/data-workspace/data-entry-cell/entry-field-input.js b/src/data-workspace/data-entry-cell/entry-field-input.js index f198fffc9..621299df4 100644 --- a/src/data-workspace/data-entry-cell/entry-field-input.js +++ b/src/data-workspace/data-entry-cell/entry-field-input.js @@ -7,6 +7,7 @@ import { dataDetailsSidebarId, useHighlightedFieldStore, useComponentWillUnmount, + useValueStore, } from '../../shared/index.js' import { focusNext, focusPrev } from '../focus-utils/index.js' import { @@ -69,6 +70,7 @@ InputComponent.propTypes = { valueType: PropTypes.string, }).isRequired, sharedProps: PropTypes.object.isRequired, + initialValue: PropTypes.string, } export function EntryFieldInput({ @@ -78,7 +80,16 @@ export function EntryFieldInput({ disabled, locked, highlighted, + setValueSynced, + setActive, }) { + const initialValue = useValueStore((state) => + state.getInitialDataValue({ + dataElementId: de.id, + categoryOptionComboId: coc.id, + }) + ) + const setHighlightedFieldId = useHighlightedFieldStore( (state) => state.setHighlightedField ) @@ -119,7 +130,12 @@ export function EntryFieldInput({ dataElementId: de.id, categoryOptionComboId: coc.id, }) - }, [de.id, coc.id, setHighlightedFieldId]) + setActive(true) + }, [de.id, coc.id, setHighlightedFieldId, setActive]) + + const onBlur = useCallback(() => { + setActive(false) + }, [setActive]) const sharedProps = useMemo( () => ({ @@ -130,9 +146,24 @@ export function EntryFieldInput({ disabled, locked, onFocus, + onBlur, onKeyDown, + setValueSynced, + initialValue, }), - [fieldname, form, de, coc, disabled, locked, onFocus, onKeyDown] + [ + fieldname, + form, + de, + coc, + disabled, + locked, + onFocus, + onBlur, + onKeyDown, + setValueSynced, + initialValue, + ] ) return @@ -154,4 +185,6 @@ EntryFieldInput.propTypes = { fieldname: PropTypes.string, highlighted: PropTypes.bool, locked: PropTypes.bool, + setActive: PropTypes.func, + setValueSynced: PropTypes.func, } diff --git a/src/data-workspace/data-entry-cell/inner-wrapper.js b/src/data-workspace/data-entry-cell/inner-wrapper.js index 5ca5b79b7..9761afaf9 100644 --- a/src/data-workspace/data-entry-cell/inner-wrapper.js +++ b/src/data-workspace/data-entry-cell/inner-wrapper.js @@ -3,7 +3,6 @@ import { useIsMutating } from '@tanstack/react-query' import cx from 'classnames' import PropTypes from 'prop-types' import React, { useEffect } from 'react' -import { useField, useForm } from 'react-final-form' import { mutationKeys as dataValueMutationKeys, useDataValueParams, @@ -58,6 +57,8 @@ export function InnerWrapper({ deId, cocId, highlighted, + valueSynced, + active, }) { const hasComment = useValueStore((state) => state.hasComment({ @@ -66,24 +67,6 @@ export function InnerWrapper({ }) ) - const { - input: { value }, - meta: { invalid, active, data, dirty, error }, - } = useField(fieldname, { - // by default undefined is formatted to '' - // this preserves the format used in the input-component - format: (v) => v, - subscription: { - value: true, - invalid: true, - active: true, - data: true, - dirty: true, - error: true, - }, - }) - const form = useForm() - const dataValueParams = useDataValueParams({ deId, cocId }) // Detect if this field is sending data const activeMutations = useIsMutating({ @@ -96,6 +79,10 @@ export function InnerWrapper({ (state) => state.clearErrorByDataValueParams ) const warning = useEntryFormStore((state) => state.getWarning(fieldname)) + const error = useEntryFormStore((state) => + state.getIndividualError(fieldname) + ) + const fieldErrorMessage = error ?? warning const errorMessage = @@ -108,11 +95,11 @@ export function InnerWrapper({ fieldErrorMessage ?? syncError?.displayMessage ) - const valueSynced = data.lastSyncedValue === value - const showSynced = dirty && valueSynced + const showSynced = valueSynced + // todo: maybe use mutation state to improve this style handling // see https://dhis2.atlassian.net/browse/TECH-1316 - const cellStateClassName = invalid + const cellStateClassName = error ? styles.invalid : warning ? styles.warning @@ -120,18 +107,6 @@ export function InnerWrapper({ ? styles.synced : null - // initalize lastSyncedValue - useEffect( - () => { - if (!syncError) { - form.mutators.setFieldData(fieldname, { - lastSyncedValue: value, - }) - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ) // clear error if reset to initital-value useEffect(() => { if (valueSynced) { @@ -143,7 +118,7 @@ export function InnerWrapper({
)} -
{ - const setFormErrors = useEntryFormStore((state) => state.setErrors) - - useFormState({ - onChange: (formState) => { - setFormErrors(formState.errors) - }, - subscription: { - errors: true, - }, - }) - return null -} - EntryForm.propTypes = { dataSet: PropTypes.shape({ displayName: PropTypes.string, diff --git a/src/data-workspace/final-form-wrapper.js b/src/data-workspace/final-form-wrapper.js index 81df658d9..2d63020db 100644 --- a/src/data-workspace/final-form-wrapper.js +++ b/src/data-workspace/final-form-wrapper.js @@ -1,7 +1,8 @@ import setFieldData from 'final-form-set-field-data' import PropTypes from 'prop-types' -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { Form } from 'react-final-form' +import { useValueStore } from '../shared/index.js' function mapObject(input, callback) { return Object.fromEntries(Object.entries(input).map(callback)) @@ -31,6 +32,17 @@ export function FinalFormWrapper({ children, dataValueSet }) { // TODO: Reinitialize form `onSuccess` of dataValueSets query // See https://dhis2.atlassian.net/browse/TECH-1357 const [initialValues] = useState(() => createInitialValues(dataValueSet)) + + const setInitialDataValues = useValueStore( + (state) => state.setInitialDataValues + ) + + useEffect(() => { + if (setInitialDataValues && initialValues) { + setInitialDataValues(initialValues) + } + }, [initialValues, setInitialDataValues]) + return (
{ - const useFieldWithPatchedOnFocus = (...args) => { - const { input, ...rest } = useField(...args) - return { - ...rest, - input: { - ...input, - onFocus: (...args) => { - input.onFocus(...args) - onFocus?.(...args) - }, - }, - } - } - const yesField = useFieldWithPatchedOnFocus(fieldname, { - type: 'radio', - value: 'true', - subscription: { value: true }, - }) - - const noField = useFieldWithPatchedOnFocus(fieldname, { - type: 'radio', - value: 'false', - subscription: { value: true }, - }) - - // Used for the 'clear' button, but works - const clearField = useFieldWithPatchedOnFocus(fieldname, { - type: 'radio', - value: '', - subscription: { value: true }, - }) + const [value, setValue] = useState(() => convertBooleanValue(initialValue)) + const [lastSyncedValue, setLastSyncedValue] = useState(() => + convertBooleanValue(initialValue) + ) + const [syncTouched, setSyncTouched] = useState(false) - const { - input: { value: fieldvalue }, - meta: { valid, data }, - } = useField(fieldname, { - subscription: { value: true, valid: true, data: true }, - }) + useEffect(() => { + if (syncTouched) { + setValueSynced(value === lastSyncedValue) + } + }, [value, lastSyncedValue, syncTouched]) const { mutate } = useSetDataValueMutation({ deId, cocId }) - const syncData = (value) => { + + const syncData = (newValue) => { + setSyncTouched(true) // todo: Here's where an error state could be set: ('onError') mutate( // Empty values need an empty string - { value: value || '' }, + { value: newValue || '' }, { onSuccess: () => { - form.mutators.setFieldData(fieldname, { - lastSyncedValue: value, - }) + setLastSyncedValue(newValue) }, } ) } - const clearButtonProps = convertCallbackSignatures(clearField.input) - delete clearButtonProps.type - - const handleChange = (value) => { + const handleChange = (newValue) => { + setValue(newValue) // If this value has changed, sync it to server if valid - if (valid && value !== data.lastSyncedValue) { - syncData(value) + // we skip validation as the values are limited to 'false', 'true', '' + if (newValue !== lastSyncedValue) { + syncData(newValue) } } return ( -
+
{ + checked={value === 'true'} + onChange={() => { handleChange('true') - yesField.input.onChange(e) }} onKeyDown={onKeyDown} disabled={disabled || locked} @@ -106,10 +75,9 @@ export const BooleanRadios = ({ dense label={i18n.t('No')} value={'false'} - {...convertCallbackSignatures(noField.input)} - onChange={(_, e) => { + checked={value === 'false'} + onChange={() => { handleChange('false') - noField.input.onChange(e) }} onKeyDown={onKeyDown} disabled={disabled || locked} @@ -119,15 +87,11 @@ export const BooleanRadios = ({ secondary className={cx(styles.whiteButton, { // If no value to clear, hide but still reserve space - [styles.hidden]: !fieldvalue, + [styles.hidden]: value === '', })} - // Callback signatures are transformed above - {...clearButtonProps} - // On click, set field value to '', sync, and blur onClick={() => { - clearField.input.onChange('') - syncData('') - clearField.input.onBlur() + handleChange('') + onFocus() }} onKeyDown={onKeyDown} disabled={disabled || locked} diff --git a/src/data-workspace/inputs/date-input.js b/src/data-workspace/inputs/date-input.js index 555c8b11e..f4388b1bb 100644 --- a/src/data-workspace/inputs/date-input.js +++ b/src/data-workspace/inputs/date-input.js @@ -1,7 +1,6 @@ import { useConfig } from '@dhis2/app-runtime' import { CalendarInput } from '@dhis2/ui' -import React from 'react' -import { useField } from 'react-final-form' +import React, { useState, useEffect } from 'react' import { useSetDataValueMutation, useUserInfo, @@ -15,47 +14,49 @@ export const DateInput = ({ cocId, deId, disabled, - fieldname, - form, locked, onFocus, + onBlur, onKeyDown, + initialValue, + setValueSynced, }) => { + const [value, setValue] = useState(initialValue) + const [lastSyncedValue, setLastSyncedValue] = useState(initialValue) + const [syncTouched, setSyncTouched] = useState(false) + + useEffect(() => { + if (syncTouched) { + setValueSynced(value === lastSyncedValue) + } + }, [value, lastSyncedValue, syncTouched]) + const { data: userInfo } = useUserInfo() const keyUiLocale = userInfo?.settings?.keyUiLocale const { systemInfo } = useConfig() const { calendar } = systemInfo - const { input, meta } = useField(fieldname, { - subscription: { - value: true, - dirty: true, - valid: true, - data: true, - }, - }) - const { mutate } = useSetDataValueMutation({ deId, cocId }) - const syncData = (value) => { + const syncData = (newValue) => { + setSyncTouched(true) mutate( // Empty values need an empty string - { value: value || '' }, + { value: newValue || '' }, { onSuccess: () => { - form.mutators.setFieldData(fieldname, { - lastSyncedValue: value, - }) - input.onBlur() + setLastSyncedValue(newValue) + onBlur() }, } ) } const handleChange = (value) => { + setValue(value) // If this value has changed, sync it to server if valid - if (meta.valid && value !== meta.data.lastSyncedValue) { + if (value !== lastSyncedValue) { syncData(value) } } @@ -64,22 +65,16 @@ export const DateInput = ({
{ onFocus() - input.onFocus() }} className={styles.dateInputContainer} > { const selectedDate = date?.calendarDateString @@ -88,7 +83,6 @@ export const DateInput = ({ calendar ) : '' - input.onChange(selectedDate) handleChange(selectedDate) }} locale={keyUiLocale} diff --git a/src/data-workspace/inputs/date-input.test.js b/src/data-workspace/inputs/date-input.test.js index 27905501e..2d223f244 100644 --- a/src/data-workspace/inputs/date-input.test.js +++ b/src/data-workspace/inputs/date-input.test.js @@ -54,6 +54,8 @@ describe('date input field', () => { locked: false, onFocus: jest.fn(), onKeyDown: jest.fn(), + onBlur: jest.fn(), + setValueSynced: jest.fn(), } const mutate = jest.fn() diff --git a/src/data-workspace/inputs/datetime-input.js b/src/data-workspace/inputs/datetime-input.js index 312fa2534..7b3ff76eb 100644 --- a/src/data-workspace/inputs/datetime-input.js +++ b/src/data-workspace/inputs/datetime-input.js @@ -1,8 +1,7 @@ import { useConfig } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' import { Button, CalendarInput } from '@dhis2/ui' -import React, { useState } from 'react' -import { useField } from 'react-final-form' +import React, { useEffect, useState } from 'react' import { convertFromIso8601ToString, convertToIso8601ToString, @@ -12,15 +11,20 @@ import { import styles from './inputs.module.css' import { InputPropTypes } from './utils.js' +const convertToDateWithTime = ({ date, time }) => + `${date}${time === '' ? '' : 'T' + time}` + export const DateTimeInput = ({ cocId, deId, disabled, fieldname, - form, locked, onFocus, + onBlur, onKeyDown, + initialValue, + setValueSynced, }) => { const { data: userInfo } = useUserInfo() const keyUiLocale = userInfo?.settings?.keyUiLocale @@ -28,45 +32,45 @@ export const DateTimeInput = ({ const { systemInfo = {} } = useConfig() const { calendar = 'gregory' } = systemInfo - const { input, meta } = useField(fieldname, { - subscription: { - value: true, - dirty: true, - valid: true, - data: true, - }, - }) + const [lastSyncedValue, setLastSyncedValue] = useState(initialValue) + const [date, setDate] = useState(() => initialValue?.substring(0, 10) ?? '') + const [time, setTime] = useState( + () => initialValue?.substring(11, 23) ?? '' + ) + const [syncTouched, setSyncTouched] = useState(false) - const [date, setDate] = useState(input?.value?.substring(0, 10) ?? '') - const [time, setTime] = useState(input?.value?.substring(11, 23) ?? '') + useEffect(() => { + if (syncTouched) { + const dateWithTime = convertToDateWithTime({ date, time }) + setValueSynced(dateWithTime === lastSyncedValue) + } + }, [date, time, lastSyncedValue, syncTouched]) const { mutate } = useSetDataValueMutation({ deId, cocId }) - const syncData = (value) => { + const syncData = (dateWithTime) => { + setSyncTouched(true) mutate( // Empty values need an empty string - { value: value || '' }, + { value: dateWithTime ?? '' }, { onSuccess: () => { - form.mutators.setFieldData(fieldname, { - lastSyncedValue: value, - }) - input.onBlur() + setLastSyncedValue(dateWithTime) + onBlur() }, } ) } const handleChange = ({ date, time }) => { - const dateWithTime = `${date}${time === '' ? '' : 'T' + time}` - input.onChange(dateWithTime) + const dateWithTime = convertToDateWithTime({ date, time }) if (date === '' || time === '') { return } // If this value has changed, sync it to server if valid - if (meta.valid && dateWithTime !== meta.data.lastSyncedValue) { + if (dateWithTime !== lastSyncedValue) { syncData(dateWithTime) } } @@ -74,10 +78,9 @@ export const DateTimeInput = ({ const clearValue = () => { setDate('') setTime('') - input.onChange('') // If this value has changed, sync it to server if valid - if (meta.valid && '' !== meta.data.lastSyncedValue) { + if ('' !== lastSyncedValue) { syncData('') } } @@ -87,12 +90,10 @@ export const DateTimeInput = ({ className={styles.dateTimeInput} onClick={() => { onFocus() - input.onFocus() }} >
{ locked: false, onFocus: jest.fn(), onKeyDown: jest.fn(), + onBlur: jest.fn(), + setValueSynced: jest.fn(), } const mutate = jest.fn() diff --git a/src/data-workspace/inputs/file-inputs.js b/src/data-workspace/inputs/file-inputs.js index e7a3d1e7d..140f6e18c 100644 --- a/src/data-workspace/inputs/file-inputs.js +++ b/src/data-workspace/inputs/file-inputs.js @@ -2,8 +2,7 @@ import i18n from '@dhis2/d2-i18n' import { colors, Button, FileInput, IconAttachment16 } from '@dhis2/ui' import { useQuery } from '@tanstack/react-query' import PropTypes from 'prop-types' -import React from 'react' -import { useField } from 'react-final-form' +import React, { useEffect, useState } from 'react' import { useDataValueParams, useDeleteDataValueMutation, @@ -18,8 +17,6 @@ const formatFileSize = (size) => { } export const FileResourceInput = ({ - fieldname, - form, image, deId, cocId, @@ -27,17 +24,28 @@ export const FileResourceInput = ({ locked, onKeyDown, onFocus, + onBlur, + initialValue, + setValueSynced, }) => { - const { input, meta } = useField(fieldname, { - // todo: validate - subscription: { - value: true, - pristine: true, - onFocus: true, - onBlur: true, - onChange: true, - }, - }) + const [value, setValue] = useState(initialValue) + const [lastSyncedValue, setLastSyncedValue] = useState(initialValue) + const [syncTouched, setSyncTouched] = useState(false) + + useEffect(() => { + if (syncTouched) { + if (typeof value === 'string') { + setValueSynced(value === lastSyncedValue) + } else { + setValueSynced( + value.name && + value.name === lastSyncedValue?.name && + value.size && + value.size === lastSyncedValue?.size + ) + } + } + }, [value, lastSyncedValue, syncTouched]) const dataValueParams = useDataValueParams({ deId, cocId }) const fileLink = useFileInputUrl(dataValueParams) @@ -48,15 +56,12 @@ export const FileResourceInput = ({ // about the file to show in the entry form: const { data } = useQuery( // This endpoint doesn't support field filtering - ['fileResources', { id: input.value }], + ['fileResources', { id: value }], { - enabled: - !!input.value && - meta.pristine && - typeof input.value === 'string', + enabled: !!value && !syncTouched && typeof value === 'string', } ) - // const [deId, cocId] = [dataValueParams.de, dataValueParams.co] + const { mutate: uploadFile } = useUploadFileDataValueMutation({ deId, cocId, @@ -66,16 +71,16 @@ export const FileResourceInput = ({ const handleChange = ({ files }) => { const newFile = files[0] const newFileValue = { name: newFile.name, size: newFile.size } - input.onChange(newFileValue) - input.onBlur() + setSyncTouched(true) + setValue(newFileValue) + if (newFile instanceof File) { uploadFile( { file: newFile }, { onSuccess: () => { - form.mutators.setFieldData(fieldname, { - lastSyncedValue: newFileValue, - }) + setLastSyncedValue(newFileValue) + onBlur() }, } ) @@ -83,21 +88,21 @@ export const FileResourceInput = ({ } const handleDelete = () => { - input.onChange('') - input.onBlur() + setValue('') + setSyncTouched(true) deleteFile(null, { onSuccess: () => { - form.mutators.setFieldData(fieldname, { - lastSyncedValue: null, - }) + setLastSyncedValue('') + onBlur() }, }) } - const inputValueHasFileMeta = !!input.value.name && !!input.value.size + const inputValueHasFileMeta = + typeof value !== 'string' && value?.name && value?.size const file = inputValueHasFileMeta - ? input.value - : data && input.value // i.e. if value is a resource UID, use fetched metadata + ? value + : data && value // i.e. if value is a resource UID, use fetched metadata ? { name: data.name, size: data.contentLength, @@ -126,10 +131,9 @@ export const FileResourceInput = ({ className={styles.deleteFileButton} onClick={handleDelete} onFocus={(...args) => { - input.onFocus(...args) onFocus?.(...args) }} - onBlur={input.onBlur} + onBlur={onBlur} onKeyDown={onKeyDown} disabled={disabled || locked} > @@ -139,13 +143,11 @@ export const FileResourceInput = ({ ) : ( { - input.onFocus(...args) onFocus?.(...args) }} - onBlur={input.onBlur} + onBlur={onBlur} onKeyDown={onKeyDown} small buttonLabel={ @@ -164,7 +166,3 @@ FileResourceInput.propTypes = { ...InputPropTypes, image: PropTypes.bool, } - -export const ImageInput = (props) => { - return -} diff --git a/src/data-workspace/inputs/generic-input.js b/src/data-workspace/inputs/generic-input.js index bd4f1b4a6..aaf0c4b89 100644 --- a/src/data-workspace/inputs/generic-input.js +++ b/src/data-workspace/inputs/generic-input.js @@ -1,7 +1,6 @@ import cx from 'classnames' import PropTypes from 'prop-types' -import React from 'react' -import { useField } from 'react-final-form' +import React, { useEffect, useState } from 'react' import { NUMBER_TYPES, VALUE_TYPES, @@ -24,97 +23,103 @@ const htmlTypeAttrsByValueType = { [VALUE_TYPES.URL]: 'url', } +const formatValue = ({ value, valueType }) => { + if (value === undefined) { + return undefined + } + const trimmedValue = value.trim() + if (trimmedValue === '') { + return '' + } + if ( + trimmedValue && + NUMBER_TYPES.includes(valueType) && + Number.isFinite(Number(value)) + ) { + return Number(value).toString() + } else { + return trimmedValue + } +} + export const GenericInput = ({ fieldname, - form, deId, cocId, valueType, onKeyDown, onFocus, + onBlur, disabled, locked, + setValueSynced, + initialValue, }) => { + const [value, setValue] = useState(initialValue) + const [lastSyncedValue, setLastSyncedValue] = useState(initialValue) + const limits = useMinMaxLimits(deId, cocId) - const formatValue = (value) => { - if (value === undefined) { - return undefined - } else if ( - value.trim() && - NUMBER_TYPES.includes(valueType) && - Number.isFinite(Number(value)) - ) { - return Number(value).toString() - } else { - return value.trim() - } - } + const setWarning = useEntryFormStore((state) => state.setWarning) + const setIndividualError = useEntryFormStore( + (state) => state.setIndividualError + ) - const warningValidate = (value) => { - const warningValidator = warningValidateByValueType(valueType, limits) + const validatorForValueType = validateByValueType(valueType, limits) + const warningValidator = warningValidateByValueType(valueType, limits) + + // check if the initial value has any associated warnings + useEffect(() => { const warningValidationResult = warningValidator(value) setWarning(fieldname, warningValidationResult) - } - - const { input, meta } = useField(fieldname, { - validate: (value) => { - warningValidate(value) - return validateByValueType(valueType, limits)(value) - }, - subscription: { - value: true, - dirty: true, - valid: true, - data: true, - }, - format: formatValue, - formatOnBlur: true, - // This is required to ensure form is validated on first page load - // this is because the validate prop doesn't rerender when limits change - data: limits, - }) + }, [initialValue, warningValidator, setWarning, fieldname]) const { mutate } = useSetDataValueMutation({ deId, cocId }) - const syncData = (value) => { + + const syncData = (newValue) => { // todo: Here's where an error state could be set: ('onError') mutate( // Empty values need an empty string - { value: value || '' }, + { value: newValue || '' }, { onSuccess: () => { - form.mutators.setFieldData(fieldname, { - lastSyncedValue: value, - }) + setLastSyncedValue(newValue) + setValueSynced(value === newValue) }, } ) } - const handleBlur = () => { - const { value } = input - const formattedValue = formatValue(value) - const { valid } = meta - if (valid && formattedValue !== meta.data.lastSyncedValue) { + const handleBlur = (value) => { + const formattedValue = formatValue({ value, valueType }) + + const invalid = validatorForValueType(value) + setIndividualError(fieldname, invalid) + + const warningValidationResult = warningValidator(value) + setWarning(fieldname, warningValidationResult) + + if (formattedValue !== lastSyncedValue && !invalid) { syncData(formattedValue) } } return ( { - input.onFocus(...args) onFocus?.(...args) }} - onBlur={(e) => { - handleBlur() - input.onBlur(e) + onBlur={() => { + onBlur() + handleBlur(value) + }} + onChange={(e) => { + setValue(e.target.value) }} autoComplete="off" onKeyDown={onKeyDown} @@ -126,5 +131,5 @@ export const GenericInput = ({ GenericInput.propTypes = { ...InputPropTypes, - inputType: PropTypes.string, + valueType: PropTypes.string, } diff --git a/src/data-workspace/inputs/long-text.js b/src/data-workspace/inputs/long-text.js index 15f0baf39..882984b76 100644 --- a/src/data-workspace/inputs/long-text.js +++ b/src/data-workspace/inputs/long-text.js @@ -1,45 +1,39 @@ -import React from 'react' -import { useField } from 'react-final-form' +import React, { useState } from 'react' import { useSetDataValueMutation } from '../../shared/index.js' import styles from './inputs.module.css' import { InputPropTypes } from './utils.js' export const LongText = ({ - fieldname, - form, deId, cocId, onKeyDown, onFocus, + onBlur, disabled, locked, + initialValue, + setValueSynced, }) => { - const { - input, - meta: { valid, data }, - } = useField(fieldname, { - subscription: { value: true, dirty: true, valid: true, data: true }, - }) + const [value, setValue] = useState(initialValue) + const [lastSyncedValue, setLastSyncedValue] = useState(initialValue) const { mutate } = useSetDataValueMutation({ deId, cocId }) - const syncData = (value) => { + const syncData = (newValue) => { // todo: Here's where an error state could be set: ('onError') mutate( // Empty values need an empty string - { value: value || '' }, + { value: newValue || '' }, { onSuccess: () => { - form.mutators.setFieldData(fieldname, { - lastSyncedValue: value, - }) + setLastSyncedValue(newValue) + setValueSynced(value === newValue) }, } ) } const handleBlur = () => { - const { value } = input - if (valid && value !== data.lastSyncedValue) { + if (value !== lastSyncedValue) { syncData(value) } } @@ -48,14 +42,16 @@ export const LongText = ({