From 9859522d2cd7bbe560784980cecf8e5a3ef4dd39 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Sun, 25 Sep 2022 23:50:58 +0200 Subject: [PATCH 1/8] refactor: move lastSyncedValue to mutation --- package.json | 1 + .../data-entry-cell/data-entry-field.js | 2 ++ .../data-entry-cell/entry-field-input.js | 4 ++++ .../data-entry-cell/inner-wrapper.js | 24 +++++++++++++++---- src/data-workspace/final-form-wrapper.js | 2 ++ src/data-workspace/inputs/boolean-radios.js | 9 ++++--- src/data-workspace/inputs/file-inputs.js | 6 ++--- src/data-workspace/inputs/generic-input.js | 17 ++++++------- yarn.lock | 5 ++++ 9 files changed, 48 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 4bf878afe..10a2d9e87 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "classnames": "^2.3.1", "expr-eval": "^2.0.2", "final-form": "^4.20.6", + "final-form-set-field-data": "^1.0.2", "history": "^5.1.0", "html-react-parser": "^1.4.8", "idb-keyval": "^6.1.0", 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 7ed6bc5cf..a51073606 100644 --- a/src/data-workspace/data-entry-cell/data-entry-field.js +++ b/src/data-workspace/data-entry-cell/data-entry-field.js @@ -20,6 +20,7 @@ export const DataEntryField = React.memo(function DataEntryField({ const [syncStatus, setSyncStatus] = useState({ syncing: false, synced: false, + lastSyncedValue: undefined, }) const { locked } = useLockedContext() @@ -38,6 +39,7 @@ export const DataEntryField = React.memo(function DataEntryField({ dataElement={de} categoryOptionCombo={coc} setSyncStatus={setSyncStatus} + syncStatus={syncStatus} disabled={disabled} locked={locked} /> 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 9169813c4..cca43baec 100644 --- a/src/data-workspace/data-entry-cell/entry-field-input.js +++ b/src/data-workspace/data-entry-cell/entry-field-input.js @@ -60,6 +60,7 @@ export function EntryFieldInput({ dataElement: de, categoryOptionCombo: coc, setSyncStatus, + syncStatus, disabled, locked, }) { @@ -102,6 +103,7 @@ export function EntryFieldInput({ disabled, locked, setSyncStatus, + syncStatus, onFocus, onKeyDown, }), @@ -112,6 +114,7 @@ export function EntryFieldInput({ disabled, locked, setSyncStatus, + syncStatus, onFocus, onKeyDown, ] @@ -136,4 +139,5 @@ EntryFieldInput.propTypes = { fieldname: PropTypes.string, locked: PropTypes.bool, setSyncStatus: PropTypes.func, + syncStatus: PropTypes.object, } diff --git a/src/data-workspace/data-entry-cell/inner-wrapper.js b/src/data-workspace/data-entry-cell/inner-wrapper.js index ab97d4f0d..a88dfbd94 100644 --- a/src/data-workspace/data-entry-cell/inner-wrapper.js +++ b/src/data-workspace/data-entry-cell/inner-wrapper.js @@ -50,7 +50,10 @@ export function InnerWrapper({ fieldname, deId, cocId, +<<<<<<< HEAD syncStatus, +======= +>>>>>>> refactor: move lastSyncedValue to mutatior }) { const hasComment = useValueStore((state) => state.hasComment({ @@ -61,9 +64,17 @@ export function InnerWrapper({ const { item } = useHighlightedFieldIdContext() const highlighted = item && deId === item.de.id && cocId === item.coc.id const { - meta: { invalid, active }, - } = useField(fieldname, { subscription: { invalid: true, active: true } }) - + input: { value }, + meta: { invalid, active, data }, + } = useField(fieldname, { + subscription: { + value: true, + invalid: true, + active: true, + data: true, + }, + }) + const synced = data.lastSyncedValue === value // Detect if this field is sending data const dataValueParams = useDataValueParams({ deId, cocId }) const activeMutations = useIsMutating({ @@ -74,7 +85,7 @@ export function InnerWrapper({ // see https://dhis2.atlassian.net/browse/TECH-1316 const cellStateClassName = invalid ? styles.invalid - : activeMutations === 0 && syncStatus.synced + : activeMutations === 0 && synced ? styles.synced : null @@ -90,7 +101,7 @@ export function InnerWrapper({ {children} 0} - isSynced={syncStatus.synced} + isSynced={synced} /> @@ -103,8 +114,11 @@ InnerWrapper.propTypes = { disabled: PropTypes.bool, fieldname: PropTypes.string, locked: PropTypes.bool, +<<<<<<< HEAD syncStatus: PropTypes.shape({ synced: PropTypes.bool, syncing: PropTypes.bool, }), +======= +>>>>>>> refactor: move lastSyncedValue to mutatior } diff --git a/src/data-workspace/final-form-wrapper.js b/src/data-workspace/final-form-wrapper.js index c078ffb44..7d3c549ab 100644 --- a/src/data-workspace/final-form-wrapper.js +++ b/src/data-workspace/final-form-wrapper.js @@ -1,3 +1,4 @@ +import setFieldData from 'final-form-set-field-data' import PropTypes from 'prop-types' import React, { useState } from 'react' import { Form } from 'react-final-form' @@ -37,6 +38,7 @@ export function FinalFormWrapper({ children, dataValueSet }) { initialValues={initialValues} subscriptions={subscriptions} keepDirtyOnReinitialize + mutators={{ setFieldData }} > {() => children} diff --git a/src/data-workspace/inputs/boolean-radios.js b/src/data-workspace/inputs/boolean-radios.js index 8b7e767d1..5e9a75f7b 100644 --- a/src/data-workspace/inputs/boolean-radios.js +++ b/src/data-workspace/inputs/boolean-radios.js @@ -1,7 +1,7 @@ import i18n from '@dhis2/d2-i18n' import { Button, Radio } from '@dhis2/ui' import cx from 'classnames' -import React, { useState } from 'react' +import React from 'react' import { useField } from 'react-final-form' import { useSetDataValueMutation } from '../../shared/index.js' import styles from './inputs.module.css' @@ -20,6 +20,7 @@ export const BooleanRadios = ({ disabled, locked, setSyncStatus, + syncStatus, onKeyDown, onFocus, }) => { @@ -59,7 +60,6 @@ export const BooleanRadios = ({ input: { value: fieldvalue }, meta, } = useField(fieldname) - const [lastSyncedValue, setLastSyncedValue] = useState(fieldvalue) const { mutate } = useSetDataValueMutation({ deId, cocId }) const syncData = (value) => { @@ -69,8 +69,7 @@ export const BooleanRadios = ({ { value: value || '' }, { onSuccess: () => { - setLastSyncedValue(value) - setSyncStatus({ syncing: false, synced: true }) + setSyncStatus({ synced: true, lastSyncedValue: value }) }, } ) @@ -82,7 +81,7 @@ export const BooleanRadios = ({ const handleChange = (value) => { const { valid } = meta // If this value has changed, sync it to server if valid - if (valid && value !== lastSyncedValue) { + if (valid && value !== syncStatus.lastSyncedValue) { syncData(value) } } diff --git a/src/data-workspace/inputs/file-inputs.js b/src/data-workspace/inputs/file-inputs.js index dd215358e..bfb9d11fc 100644 --- a/src/data-workspace/inputs/file-inputs.js +++ b/src/data-workspace/inputs/file-inputs.js @@ -68,12 +68,11 @@ export const FileResourceInput = ({ input.onChange({ name: newFile.name, size: newFile.size }) input.onBlur() if (newFile instanceof File) { - setSyncStatus({ syncing: true, synced: false }) uploadFile( { file: newFile }, { onSuccess: () => { - setSyncStatus({ syncing: false, synced: true }) + setSyncStatus({ synced: true }) }, } ) @@ -83,10 +82,9 @@ export const FileResourceInput = ({ const handleDelete = () => { input.onChange('') input.onBlur() - setSyncStatus({ syncing: true, synced: false }) deleteFile(null, { onSuccess: () => { - setSyncStatus({ syncing: false, synced: true }) + setSyncStatus({ synced: true }) }, }) } diff --git a/src/data-workspace/inputs/generic-input.js b/src/data-workspace/inputs/generic-input.js index 20ecced82..af9c847e2 100644 --- a/src/data-workspace/inputs/generic-input.js +++ b/src/data-workspace/inputs/generic-input.js @@ -1,7 +1,7 @@ import cx from 'classnames' import PropTypes from 'prop-types' -import React, { useState } from 'react' -import { useField } from 'react-final-form' +import React from 'react' +import { useField, useForm } from 'react-final-form' import { NUMBER_TYPES, VALUE_TYPES, @@ -25,7 +25,6 @@ export const GenericInput = ({ fieldname, deId, cocId, - setSyncStatus, valueType, onKeyDown, onFocus, @@ -46,16 +45,17 @@ export const GenericInput = ({ return value.trim() } } + const form = useForm() const { input, meta } = useField(fieldname, { validate: validateByValueTypeWithLimits(valueType, limits), - subscription: { value: true, dirty: true, valid: true }, + subscription: { value: true, dirty: true, valid: true, data: true }, format: formatValue, formatOnBlur: true, // This is require to ensure form is validated on first page load // this is because the validate prop doesn't rerender when limits change data: limits, }) - const [lastSyncedValue, setLastSyncedValue] = useState(input.value) + const { mutate } = useSetDataValueMutation({ deId, cocId }) const syncData = (value) => { // todo: Here's where an error state could be set: ('onError') @@ -64,8 +64,9 @@ export const GenericInput = ({ { value: value || '' }, { onSuccess: () => { - setLastSyncedValue(value) - setSyncStatus({ syncing: false, synced: true }) + form.mutators.setFieldData(fieldname, { + lastSyncedValue: value, + }) }, } ) @@ -75,7 +76,7 @@ export const GenericInput = ({ const { value } = input const formattedValue = formatValue(value) const { valid } = meta - if (valid && formattedValue !== lastSyncedValue) { + if (valid && formattedValue !== meta.data.lastSyncedValue) { syncData(formattedValue) } } diff --git a/yarn.lock b/yarn.lock index 2e79c8093..e96ae3363 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8446,6 +8446,11 @@ filter-obj@^1.1.0: resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== +final-form-set-field-data@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/final-form-set-field-data/-/final-form-set-field-data-1.0.2.tgz#ce19095af5d607148c1e6ce3403d75d40223d848" + integrity sha512-gAnENimyQ5GW3OEGca5pbwm4lYshW2orzfBlPUYqzcm7ZxkQrVO8FqCAgEcCM+Rq9U1OU0q+D+UkqETvvDY6jw== + final-form@^4.20.2, final-form@^4.20.6: version "4.20.7" resolved "https://registry.yarnpkg.com/final-form/-/final-form-4.20.7.tgz#e7e2eb5fd952951d4fe6153d46043da2d68b207e" From d8c0b72decdb1e31ed71daa8adb38bb0e98224bb Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Mon, 26 Sep 2022 00:05:20 +0200 Subject: [PATCH 2/8] fix: cleanup --- .../data-entry-cell/data-entry-field.js | 12 +--------- .../data-entry-cell/entry-field-input.js | 22 ++++--------------- .../data-entry-cell/inner-wrapper.js | 11 ---------- src/data-workspace/inputs/file-inputs.js | 8 ++++--- src/data-workspace/inputs/generic-input.js | 4 ++-- src/data-workspace/inputs/utils.js | 1 + 6 files changed, 13 insertions(+), 45 deletions(-) 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 a51073606..5bb1e46bd 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, { useState } from 'react' +import React from 'react' import { useLockedContext } from '../../shared/index.js' import { getFieldId } from '../get-field-id.js' import { EntryFieldInput } from './entry-field-input.js' @@ -15,13 +15,6 @@ export const DataEntryField = React.memo(function DataEntryField({ // { [deId]: { [cocId]: value } } const fieldname = getFieldId(de.id, coc.id) - // todo: perhaps this could be refactored to use DV mutation state - // See https://dhis2.atlassian.net/browse/TECH-1316 - const [syncStatus, setSyncStatus] = useState({ - syncing: false, - synced: false, - lastSyncedValue: undefined, - }) const { locked } = useLockedContext() return ( @@ -30,7 +23,6 @@ export const DataEntryField = React.memo(function DataEntryField({ fieldname={fieldname} deId={de.id} cocId={coc.id} - syncStatus={syncStatus} disabled={disabled} locked={locked} > @@ -38,8 +30,6 @@ export const DataEntryField = React.memo(function DataEntryField({ fieldname={fieldname} dataElement={de} categoryOptionCombo={coc} - setSyncStatus={setSyncStatus} - syncStatus={syncStatus} disabled={disabled} locked={locked} /> 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 cca43baec..ba30b0695 100644 --- a/src/data-workspace/data-entry-cell/entry-field-input.js +++ b/src/data-workspace/data-entry-cell/entry-field-input.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types' import React, { useCallback, useMemo } from 'react' +import { useForm } from 'react-final-form' import { useSetRightHandPanel } from '../../right-hand-panel/index.js' import { VALUE_TYPES, @@ -59,8 +60,6 @@ export function EntryFieldInput({ fieldname, dataElement: de, categoryOptionCombo: coc, - setSyncStatus, - syncStatus, disabled, locked, }) { @@ -69,7 +68,7 @@ export function EntryFieldInput({ // used so we don't consume the "id" which // would cause this component to rerender const setRightHandPanel = useSetRightHandPanel() - + const form = useForm() // todo: maybe move to InnerWrapper? // See https://dhis2.atlassian.net/browse/TECH-1296 const onKeyDown = useCallback( @@ -98,26 +97,15 @@ export function EntryFieldInput({ const sharedProps = useMemo( () => ({ fieldname, + form, deId: de.id, cocId: coc.id, disabled, locked, - setSyncStatus, - syncStatus, onFocus, onKeyDown, }), - [ - fieldname, - de, - coc, - disabled, - locked, - setSyncStatus, - syncStatus, - onFocus, - onKeyDown, - ] + [fieldname, form, de, coc, disabled, locked, onFocus, onKeyDown] ) return @@ -138,6 +126,4 @@ EntryFieldInput.propTypes = { disabled: PropTypes.bool, fieldname: PropTypes.string, locked: PropTypes.bool, - setSyncStatus: PropTypes.func, - syncStatus: PropTypes.object, } diff --git a/src/data-workspace/data-entry-cell/inner-wrapper.js b/src/data-workspace/data-entry-cell/inner-wrapper.js index a88dfbd94..1541c9277 100644 --- a/src/data-workspace/data-entry-cell/inner-wrapper.js +++ b/src/data-workspace/data-entry-cell/inner-wrapper.js @@ -50,10 +50,6 @@ export function InnerWrapper({ fieldname, deId, cocId, -<<<<<<< HEAD - syncStatus, -======= ->>>>>>> refactor: move lastSyncedValue to mutatior }) { const hasComment = useValueStore((state) => state.hasComment({ @@ -114,11 +110,4 @@ InnerWrapper.propTypes = { disabled: PropTypes.bool, fieldname: PropTypes.string, locked: PropTypes.bool, -<<<<<<< HEAD - syncStatus: PropTypes.shape({ - synced: PropTypes.bool, - syncing: PropTypes.bool, - }), -======= ->>>>>>> refactor: move lastSyncedValue to mutatior } diff --git a/src/data-workspace/inputs/file-inputs.js b/src/data-workspace/inputs/file-inputs.js index bfb9d11fc..729806dcb 100644 --- a/src/data-workspace/inputs/file-inputs.js +++ b/src/data-workspace/inputs/file-inputs.js @@ -19,12 +19,12 @@ const formatFileSize = (size) => { export const FileResourceInput = ({ fieldname, + form, image, deId, cocId, disabled, locked, - setSyncStatus, onKeyDown, onFocus, }) => { @@ -72,7 +72,9 @@ export const FileResourceInput = ({ { file: newFile }, { onSuccess: () => { - setSyncStatus({ synced: true }) + form.mutators.setFieldData(fieldname, { + lastSyncedValue: newFile, + }) }, } ) @@ -84,7 +86,7 @@ export const FileResourceInput = ({ input.onBlur() deleteFile(null, { onSuccess: () => { - setSyncStatus({ synced: true }) + form.mutators.setFieldData(fieldname, null) }, }) } diff --git a/src/data-workspace/inputs/generic-input.js b/src/data-workspace/inputs/generic-input.js index af9c847e2..3a8f7a926 100644 --- a/src/data-workspace/inputs/generic-input.js +++ b/src/data-workspace/inputs/generic-input.js @@ -1,7 +1,7 @@ import cx from 'classnames' import PropTypes from 'prop-types' import React from 'react' -import { useField, useForm } from 'react-final-form' +import { useField } from 'react-final-form' import { NUMBER_TYPES, VALUE_TYPES, @@ -23,6 +23,7 @@ const htmlTypeAttrsByValueType = { export const GenericInput = ({ fieldname, + form, deId, cocId, valueType, @@ -45,7 +46,6 @@ export const GenericInput = ({ return value.trim() } } - const form = useForm() const { input, meta } = useField(fieldname, { validate: validateByValueTypeWithLimits(valueType, limits), subscription: { value: true, dirty: true, valid: true, data: true }, diff --git a/src/data-workspace/inputs/utils.js b/src/data-workspace/inputs/utils.js index 90921dd70..45457bafa 100644 --- a/src/data-workspace/inputs/utils.js +++ b/src/data-workspace/inputs/utils.js @@ -11,6 +11,7 @@ export const convertCallbackSignatures = (props) => ({ }) export const InputPropTypes = { + form: PropTypes.object, onKeyDown: PropTypes.func.isRequired, cocId: PropTypes.string, deId: PropTypes.string, From 7585a3de1efce9bf53e077877fdcd70b3e258545 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Mon, 26 Sep 2022 00:29:56 +0200 Subject: [PATCH 3/8] refactor: use mutator in inputs --- src/data-workspace/inputs/boolean-radios.js | 16 ++++++++------- src/data-workspace/inputs/generic-input.js | 2 +- src/data-workspace/inputs/long-text.js | 20 ++++++++++--------- src/data-workspace/inputs/option-set.js | 17 +++++++++------- .../inputs/true-only-checkbox.js | 20 ++++++++++--------- src/data-workspace/inputs/utils.js | 8 +++++--- 6 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/data-workspace/inputs/boolean-radios.js b/src/data-workspace/inputs/boolean-radios.js index 5e9a75f7b..1d264b0ba 100644 --- a/src/data-workspace/inputs/boolean-radios.js +++ b/src/data-workspace/inputs/boolean-radios.js @@ -15,12 +15,11 @@ import { convertCallbackSignatures, InputPropTypes } from './utils.js' // does `isEqual` prop help make 1/true and 0/false/'' equal? export const BooleanRadios = ({ fieldname, + form, deId, cocId, disabled, locked, - setSyncStatus, - syncStatus, onKeyDown, onFocus, }) => { @@ -58,8 +57,10 @@ export const BooleanRadios = ({ const { input: { value: fieldvalue }, - meta, - } = useField(fieldname) + meta: { valid, data }, + } = useField(fieldname, { + subscription: { value: true, valid: true, data: true }, + }) const { mutate } = useSetDataValueMutation({ deId, cocId }) const syncData = (value) => { @@ -69,7 +70,9 @@ export const BooleanRadios = ({ { value: value || '' }, { onSuccess: () => { - setSyncStatus({ synced: true, lastSyncedValue: value }) + form.mutators.setFieldData(fieldname, { + lastSyncedValue: value, + }) }, } ) @@ -79,9 +82,8 @@ export const BooleanRadios = ({ delete clearButtonProps.type const handleChange = (value) => { - const { valid } = meta // If this value has changed, sync it to server if valid - if (valid && value !== syncStatus.lastSyncedValue) { + if (valid && value !== data.lastSyncedValue) { syncData(value) } } diff --git a/src/data-workspace/inputs/generic-input.js b/src/data-workspace/inputs/generic-input.js index 3a8f7a926..65be412a7 100644 --- a/src/data-workspace/inputs/generic-input.js +++ b/src/data-workspace/inputs/generic-input.js @@ -51,7 +51,7 @@ export const GenericInput = ({ subscription: { value: true, dirty: true, valid: true, data: true }, format: formatValue, formatOnBlur: true, - // This is require to ensure form is validated on first page load + // 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, }) diff --git a/src/data-workspace/inputs/long-text.js b/src/data-workspace/inputs/long-text.js index 4b7aa38a5..15f0baf39 100644 --- a/src/data-workspace/inputs/long-text.js +++ b/src/data-workspace/inputs/long-text.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React from 'react' import { useField } from 'react-final-form' import { useSetDataValueMutation } from '../../shared/index.js' import styles from './inputs.module.css' @@ -6,19 +6,21 @@ import { InputPropTypes } from './utils.js' export const LongText = ({ fieldname, + form, deId, cocId, - setSyncStatus, onKeyDown, onFocus, disabled, locked, }) => { - const { input, meta } = useField(fieldname, { - subscription: { value: true, dirty: true, valid: true }, + const { + input, + meta: { valid, data }, + } = useField(fieldname, { + subscription: { value: true, dirty: true, valid: true, data: true }, }) - const [lastSyncedValue, setLastSyncedValue] = useState(input.value) const { mutate } = useSetDataValueMutation({ deId, cocId }) const syncData = (value) => { // todo: Here's where an error state could be set: ('onError') @@ -27,8 +29,9 @@ export const LongText = ({ { value: value || '' }, { onSuccess: () => { - setLastSyncedValue(value) - setSyncStatus({ syncing: false, synced: true }) + form.mutators.setFieldData(fieldname, { + lastSyncedValue: value, + }) }, } ) @@ -36,8 +39,7 @@ export const LongText = ({ const handleBlur = () => { const { value } = input - const { valid } = meta - if (valid && value !== lastSyncedValue) { + if (valid && value !== data.lastSyncedValue) { syncData(value) } } diff --git a/src/data-workspace/inputs/option-set.js b/src/data-workspace/inputs/option-set.js index 90bd4700e..47000a3a3 100644 --- a/src/data-workspace/inputs/option-set.js +++ b/src/data-workspace/inputs/option-set.js @@ -1,7 +1,7 @@ import i18n from '@dhis2/d2-i18n' import { Button, SingleSelect, SingleSelectOption } from '@dhis2/ui' import PropTypes from 'prop-types' -import React, { useState } from 'react' +import React from 'react' import { useField } from 'react-final-form' import { useMetadata, @@ -13,19 +13,23 @@ import { InputPropTypes } from './utils.js' export const OptionSet = ({ fieldname, + form, optionSetId, deId, cocId, - setSyncStatus, onKeyDown, onFocus, disabled, locked, }) => { - const { input } = useField(fieldname, { subscription: { value: true } }) + const { + input, + meta: { data }, + } = useField(fieldname, { + subscription: { value: true, data: true }, + }) const { data: metadata } = useMetadata() - const [lastSyncedValue, setLastSyncedValue] = useState(input.value) const { mutate } = useSetDataValueMutation({ deId, cocId }) const syncData = (value) => { // todo: Here's where an error state could be set: ('onError') @@ -34,8 +38,7 @@ export const OptionSet = ({ { value: value || '' }, { onSuccess: () => { - setLastSyncedValue(value) - setSyncStatus({ syncing: false, synced: true }) + form.mutators.setFieldData({ lastSyncedValue: value }) }, } ) @@ -43,7 +46,7 @@ export const OptionSet = ({ const handleChange = (value) => { // For a select using onChange, don't need to check valid or dirty, respectively - if (value !== lastSyncedValue) { + if (value !== data.lastSyncedValue) { syncData(value) } } diff --git a/src/data-workspace/inputs/true-only-checkbox.js b/src/data-workspace/inputs/true-only-checkbox.js index 94dccb51d..0055443a9 100644 --- a/src/data-workspace/inputs/true-only-checkbox.js +++ b/src/data-workspace/inputs/true-only-checkbox.js @@ -1,5 +1,5 @@ import { Checkbox } from '@dhis2/ui' -import React, { useState } from 'react' +import React from 'react' import { useField } from 'react-final-form' import { useSetDataValueMutation } from '../../shared/index.js' import styles from './inputs.module.css' @@ -7,20 +7,22 @@ import { convertCallbackSignatures, InputPropTypes } from './utils.js' export const TrueOnlyCheckbox = ({ fieldname, + form, deId, cocId, - setSyncStatus, onKeyDown, onFocus, disabled, locked, }) => { - const { input, meta } = useField(fieldname, { + const { + input, + meta: { valid, data }, + } = useField(fieldname, { type: 'checkbox', - subscription: { value: true, dirty: true, valid: true }, + subscription: { value: true, dirty: true, valid: true, data: true }, }) - const [lastSyncedValue, setLastSyncedValue] = useState(input.value) const { mutate } = useSetDataValueMutation({ deId, cocId }) const syncData = (value) => { // todo: Here's where an error state could be set: ('onError') @@ -29,8 +31,9 @@ export const TrueOnlyCheckbox = ({ { value: value || '' }, { onSuccess: () => { - setLastSyncedValue(value) - setSyncStatus({ syncing: false, synced: true }) + form.mutators.setFieldData(fieldname, { + lastSyncedValue: value, + }) }, } ) @@ -40,8 +43,7 @@ export const TrueOnlyCheckbox = ({ const handleBlur = () => { // For 'True only', can only send 'true' (or '1') or '' const value = input.checked ? 'true' : '' - const { valid } = meta - if (valid && value !== lastSyncedValue) { + if (valid && value !== data.lastSyncedValue) { syncData(value) } } diff --git a/src/data-workspace/inputs/utils.js b/src/data-workspace/inputs/utils.js index 45457bafa..2cf460e0a 100644 --- a/src/data-workspace/inputs/utils.js +++ b/src/data-workspace/inputs/utils.js @@ -1,5 +1,4 @@ import PropTypes from 'prop-types' - /** * Adapt UI components to Final Form's callbacks */ @@ -11,13 +10,16 @@ export const convertCallbackSignatures = (props) => ({ }) export const InputPropTypes = { - form: PropTypes.object, + form: PropTypes.shape({ + mutators: PropTypes.shape({ + setFieldData: PropTypes.func, + }), + }), onKeyDown: PropTypes.func.isRequired, cocId: PropTypes.string, deId: PropTypes.string, disabled: PropTypes.bool, lastSyncedValue: PropTypes.any, locked: PropTypes.bool, - setSyncStatus: PropTypes.func, onFocus: PropTypes.func, } From c9a41363d94b9b158a4c5636daa8c6242eb9e72c Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Mon, 26 Sep 2022 01:01:59 +0200 Subject: [PATCH 4/8] fix: init lastSyncedValue, fix file-field --- .../data-entry-cell/inner-wrapper.js | 20 +++++++++++++++---- src/data-workspace/inputs/file-inputs.js | 5 +++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/data-workspace/data-entry-cell/inner-wrapper.js b/src/data-workspace/data-entry-cell/inner-wrapper.js index 1541c9277..31d9acd25 100644 --- a/src/data-workspace/data-entry-cell/inner-wrapper.js +++ b/src/data-workspace/data-entry-cell/inner-wrapper.js @@ -2,8 +2,8 @@ import { IconMore16, colors } from '@dhis2/ui' import { useIsMutating } from '@tanstack/react-query' import cx from 'classnames' import PropTypes from 'prop-types' -import React from 'react' -import { useField } from 'react-final-form' +import React, { useEffect } from 'react' +import { useField, useForm } from 'react-final-form' import { mutationKeys as dataValueMutationKeys, useDataValueParams, @@ -61,16 +61,28 @@ export function InnerWrapper({ const highlighted = item && deId === item.de.id && cocId === item.coc.id const { input: { value }, - meta: { invalid, active, data }, + meta: { invalid, active, data, dirty }, } = useField(fieldname, { subscription: { value: true, invalid: true, active: true, data: true, + dirty: true, }, }) - const synced = data.lastSyncedValue === value + const form = useForm() + + // initalize lastSyncedValue + useEffect( + () => + form.mutators.setFieldData(fieldname, { + lastSyncedValue: value, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ) + const synced = dirty && data.lastSyncedValue === value // Detect if this field is sending data const dataValueParams = useDataValueParams({ deId, cocId }) const activeMutations = useIsMutating({ diff --git a/src/data-workspace/inputs/file-inputs.js b/src/data-workspace/inputs/file-inputs.js index 729806dcb..5a63ea031 100644 --- a/src/data-workspace/inputs/file-inputs.js +++ b/src/data-workspace/inputs/file-inputs.js @@ -65,7 +65,8 @@ export const FileResourceInput = ({ const handleChange = ({ files }) => { const newFile = files[0] - input.onChange({ name: newFile.name, size: newFile.size }) + const newFileValue = { name: newFile.name, size: newFile.size } + input.onChange(newFileValue) input.onBlur() if (newFile instanceof File) { uploadFile( @@ -73,7 +74,7 @@ export const FileResourceInput = ({ { onSuccess: () => { form.mutators.setFieldData(fieldname, { - lastSyncedValue: newFile, + lastSyncedValue: newFileValue, }) }, } From 28cec0c6ca5f46cb784a68ec53b3ed169ead69a0 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Mon, 26 Sep 2022 01:08:26 +0200 Subject: [PATCH 5/8] fix: option-set input lastSyncedValue --- src/data-workspace/inputs/option-set.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/data-workspace/inputs/option-set.js b/src/data-workspace/inputs/option-set.js index 47000a3a3..210895b45 100644 --- a/src/data-workspace/inputs/option-set.js +++ b/src/data-workspace/inputs/option-set.js @@ -38,7 +38,9 @@ export const OptionSet = ({ { value: value || '' }, { onSuccess: () => { - form.mutators.setFieldData({ lastSyncedValue: value }) + form.mutators.setFieldData(fieldname, { + lastSyncedValue: value, + }) }, } ) From e7e201ef2997b6661771e5aea18b8e4eb7b69460 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Mon, 26 Sep 2022 01:29:05 +0200 Subject: [PATCH 6/8] fix: true-only checkbox lastSynced comparison --- src/data-workspace/inputs/true-only-checkbox.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/data-workspace/inputs/true-only-checkbox.js b/src/data-workspace/inputs/true-only-checkbox.js index 0055443a9..bf844c877 100644 --- a/src/data-workspace/inputs/true-only-checkbox.js +++ b/src/data-workspace/inputs/true-only-checkbox.js @@ -32,7 +32,9 @@ export const TrueOnlyCheckbox = ({ { onSuccess: () => { form.mutators.setFieldData(fieldname, { - lastSyncedValue: value, + // value will be formatted to boolean, so keep same format + // '' becomes false + lastSyncedValue: !!value, }) }, } @@ -43,7 +45,9 @@ export const TrueOnlyCheckbox = ({ const handleBlur = () => { // For 'True only', can only send 'true' (or '1') or '' const value = input.checked ? 'true' : '' - if (valid && value !== data.lastSyncedValue) { + const lastVal = data.lastSyncedValue ? 'true' : '' + + if (valid && value !== lastVal) { syncData(value) } } From baf66e04f2e6347232a1f639b7c04d85fb2c7e5b Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Mon, 26 Sep 2022 12:36:34 +0200 Subject: [PATCH 7/8] fix: preserve input-component format --- src/data-workspace/data-entry-cell/inner-wrapper.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/data-workspace/data-entry-cell/inner-wrapper.js b/src/data-workspace/data-entry-cell/inner-wrapper.js index 31d9acd25..ad8d9e876 100644 --- a/src/data-workspace/data-entry-cell/inner-wrapper.js +++ b/src/data-workspace/data-entry-cell/inner-wrapper.js @@ -63,6 +63,9 @@ export function InnerWrapper({ input: { value }, meta: { invalid, active, data, dirty }, } = 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, @@ -75,13 +78,15 @@ export function InnerWrapper({ // initalize lastSyncedValue useEffect( - () => + () => { form.mutators.setFieldData(fieldname, { lastSyncedValue: value, - }), + }) + }, // eslint-disable-next-line react-hooks/exhaustive-deps [] ) + const synced = dirty && data.lastSyncedValue === value // Detect if this field is sending data const dataValueParams = useDataValueParams({ deId, cocId }) From 1779a81c9c8de636774ae71d6b6000b173416bd0 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Tue, 27 Sep 2022 12:58:54 +0200 Subject: [PATCH 8/8] fix(file-input): reset lastSyncedValue --- src/data-workspace/inputs/file-inputs.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/data-workspace/inputs/file-inputs.js b/src/data-workspace/inputs/file-inputs.js index 5a63ea031..e7a3d1e7d 100644 --- a/src/data-workspace/inputs/file-inputs.js +++ b/src/data-workspace/inputs/file-inputs.js @@ -87,7 +87,9 @@ export const FileResourceInput = ({ input.onBlur() deleteFile(null, { onSuccess: () => { - form.mutators.setFieldData(fieldname, null) + form.mutators.setFieldData(fieldname, { + lastSyncedValue: null, + }) }, }) }