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..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,12 +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,
- })
const { locked } = useLockedContext()
return (
@@ -29,7 +23,6 @@ export const DataEntryField = React.memo(function DataEntryField({
fieldname={fieldname}
deId={de.id}
cocId={coc.id}
- syncStatus={syncStatus}
disabled={disabled}
locked={locked}
>
@@ -37,7 +30,6 @@ export const DataEntryField = React.memo(function DataEntryField({
fieldname={fieldname}
dataElement={de}
categoryOptionCombo={coc}
- setSyncStatus={setSyncStatus}
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..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,7 +60,6 @@ export function EntryFieldInput({
fieldname,
dataElement: de,
categoryOptionCombo: coc,
- setSyncStatus,
disabled,
locked,
}) {
@@ -68,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(
@@ -97,24 +97,15 @@ export function EntryFieldInput({
const sharedProps = useMemo(
() => ({
fieldname,
+ form,
deId: de.id,
cocId: coc.id,
disabled,
locked,
- setSyncStatus,
onFocus,
onKeyDown,
}),
- [
- fieldname,
- de,
- coc,
- disabled,
- locked,
- setSyncStatus,
- onFocus,
- onKeyDown,
- ]
+ [fieldname, form, de, coc, disabled, locked, onFocus, onKeyDown]
)
return
@@ -135,5 +126,4 @@ EntryFieldInput.propTypes = {
disabled: PropTypes.bool,
fieldname: PropTypes.string,
locked: PropTypes.bool,
- setSyncStatus: 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 ab97d4f0d..ad8d9e876 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,
@@ -50,7 +50,6 @@ export function InnerWrapper({
fieldname,
deId,
cocId,
- syncStatus,
}) {
const hasComment = useValueStore((state) =>
state.hasComment({
@@ -61,9 +60,34 @@ 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, 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,
+ active: true,
+ data: true,
+ dirty: true,
+ },
+ })
+ 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({
@@ -74,7 +98,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 +114,7 @@ export function InnerWrapper({
{children}
0}
- isSynced={syncStatus.synced}
+ isSynced={synced}
/>
@@ -103,8 +127,4 @@ InnerWrapper.propTypes = {
disabled: PropTypes.bool,
fieldname: PropTypes.string,
locked: PropTypes.bool,
- syncStatus: PropTypes.shape({
- synced: PropTypes.bool,
- syncing: PropTypes.bool,
- }),
}
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..1d264b0ba 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'
@@ -15,11 +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,
onKeyDown,
onFocus,
}) => {
@@ -57,9 +57,10 @@ export const BooleanRadios = ({
const {
input: { value: fieldvalue },
- meta,
- } = useField(fieldname)
- const [lastSyncedValue, setLastSyncedValue] = useState(fieldvalue)
+ meta: { valid, data },
+ } = useField(fieldname, {
+ subscription: { value: true, valid: true, data: true },
+ })
const { mutate } = useSetDataValueMutation({ deId, cocId })
const syncData = (value) => {
@@ -69,8 +70,9 @@ export const BooleanRadios = ({
{ value: value || '' },
{
onSuccess: () => {
- setLastSyncedValue(value)
- setSyncStatus({ syncing: false, synced: true })
+ form.mutators.setFieldData(fieldname, {
+ lastSyncedValue: value,
+ })
},
}
)
@@ -80,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 !== lastSyncedValue) {
+ if (valid && value !== data.lastSyncedValue) {
syncData(value)
}
}
diff --git a/src/data-workspace/inputs/file-inputs.js b/src/data-workspace/inputs/file-inputs.js
index dd215358e..e7a3d1e7d 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,
}) => {
@@ -65,15 +65,17 @@ 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) {
- setSyncStatus({ syncing: true, synced: false })
uploadFile(
{ file: newFile },
{
onSuccess: () => {
- setSyncStatus({ syncing: false, synced: true })
+ form.mutators.setFieldData(fieldname, {
+ lastSyncedValue: newFileValue,
+ })
},
}
)
@@ -83,10 +85,11 @@ export const FileResourceInput = ({
const handleDelete = () => {
input.onChange('')
input.onBlur()
- setSyncStatus({ syncing: true, synced: false })
deleteFile(null, {
onSuccess: () => {
- setSyncStatus({ syncing: false, synced: true })
+ form.mutators.setFieldData(fieldname, {
+ lastSyncedValue: null,
+ })
},
})
}
diff --git a/src/data-workspace/inputs/generic-input.js b/src/data-workspace/inputs/generic-input.js
index 20ecced82..65be412a7 100644
--- a/src/data-workspace/inputs/generic-input.js
+++ b/src/data-workspace/inputs/generic-input.js
@@ -1,6 +1,6 @@
import cx from 'classnames'
import PropTypes from 'prop-types'
-import React, { useState } from 'react'
+import React from 'react'
import { useField } from 'react-final-form'
import {
NUMBER_TYPES,
@@ -23,9 +23,9 @@ const htmlTypeAttrsByValueType = {
export const GenericInput = ({
fieldname,
+ form,
deId,
cocId,
- setSyncStatus,
valueType,
onKeyDown,
onFocus,
@@ -48,14 +48,14 @@ export const GenericInput = ({
}
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 required 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/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..210895b45 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,9 @@ export const OptionSet = ({
{ value: value || '' },
{
onSuccess: () => {
- setLastSyncedValue(value)
- setSyncStatus({ syncing: false, synced: true })
+ form.mutators.setFieldData(fieldname, {
+ lastSyncedValue: value,
+ })
},
}
)
@@ -43,7 +48,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..bf844c877 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,11 @@ export const TrueOnlyCheckbox = ({
{ value: value || '' },
{
onSuccess: () => {
- setLastSyncedValue(value)
- setSyncStatus({ syncing: false, synced: true })
+ form.mutators.setFieldData(fieldname, {
+ // value will be formatted to boolean, so keep same format
+ // '' becomes false
+ lastSyncedValue: !!value,
+ })
},
}
)
@@ -40,8 +45,9 @@ 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) {
+ const lastVal = data.lastSyncedValue ? 'true' : ''
+
+ if (valid && value !== lastVal) {
syncData(value)
}
}
diff --git a/src/data-workspace/inputs/utils.js b/src/data-workspace/inputs/utils.js
index 90921dd70..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,12 +10,16 @@ export const convertCallbackSignatures = (props) => ({
})
export const InputPropTypes = {
+ 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,
}
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"