fix(ui): client should add back default values for valid and passesCondition form field properties (#10709)
As a result of #9388, the `valid` and `passesCondition` properties no longer appear in form state. This leads to breaking logic if you were previously relying on these properties to have explicit values. To fix this, we simply perform the inverse on these properties before accepting them into client side form state. In the next major release, we can accept form state as it is received and instruct users to modify their logic as needed. Also comes with a small perf optimization, by keeping the old object reference of fields if they did not change when server form state comes back
This commit is contained in:
@@ -284,20 +284,39 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
|
||||
// ..
|
||||
// This is a performance enhancement for saving
|
||||
// large documents with hundreds of fields
|
||||
const newState = {}
|
||||
const newState: FormState = {}
|
||||
|
||||
Object.entries(action.state).forEach(([path, field]) => {
|
||||
for (const [path, newField] of Object.entries(action.state)) {
|
||||
const oldField = state[path]
|
||||
const newField = field
|
||||
|
||||
if (newField.valid !== false) {
|
||||
newField.valid = true
|
||||
}
|
||||
if (newField.passesCondition !== false) {
|
||||
newField.passesCondition = true
|
||||
}
|
||||
|
||||
if (!dequal(oldField, newField)) {
|
||||
newState[path] = newField
|
||||
} else if (oldField) {
|
||||
newState[path] = oldField
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return newState
|
||||
}
|
||||
|
||||
//TODO: Remove this in 4.0 - this is a temporary fix to prevent a breaking change
|
||||
if (action.sanitize) {
|
||||
for (const field of Object.values(action.state)) {
|
||||
if (field.valid !== false) {
|
||||
field.valid = true
|
||||
}
|
||||
if (field.passesCondition !== false) {
|
||||
field.passesCondition = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// If we're not optimizing, just set the state to the new state
|
||||
return action.state
|
||||
}
|
||||
|
||||
@@ -637,7 +637,12 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
useEffect(() => {
|
||||
if (initialState) {
|
||||
contextRef.current = { ...initContextState } as FormContextType
|
||||
dispatchFields({ type: 'REPLACE_STATE', optimize: false, state: initialState })
|
||||
dispatchFields({
|
||||
type: 'REPLACE_STATE',
|
||||
optimize: false,
|
||||
sanitize: true,
|
||||
state: initialState,
|
||||
})
|
||||
}
|
||||
}, [initialState, dispatchFields])
|
||||
|
||||
|
||||
@@ -22,28 +22,29 @@ export const mergeServerFormState = ({
|
||||
existingState,
|
||||
incomingState,
|
||||
}: Args): { changed: boolean; newState: FormState } => {
|
||||
const serverPropsToAccept = [
|
||||
'passesCondition',
|
||||
'valid',
|
||||
'errorMessage',
|
||||
'rows',
|
||||
'customComponents',
|
||||
'requiresRender',
|
||||
]
|
||||
|
||||
if (acceptValues) {
|
||||
serverPropsToAccept.push('value')
|
||||
}
|
||||
|
||||
let changed = false
|
||||
|
||||
const newState = {}
|
||||
|
||||
if (existingState) {
|
||||
Object.entries(existingState).forEach(([path, newFieldState]) => {
|
||||
const serverPropsToAccept = [
|
||||
'passesCondition',
|
||||
'valid',
|
||||
'errorMessage',
|
||||
'rows',
|
||||
'customComponents',
|
||||
'requiresRender',
|
||||
]
|
||||
|
||||
if (acceptValues) {
|
||||
serverPropsToAccept.push('value')
|
||||
}
|
||||
|
||||
for (const [path, newFieldState] of Object.entries(existingState)) {
|
||||
if (!incomingState[path]) {
|
||||
return
|
||||
continue
|
||||
}
|
||||
let fieldChanged = false
|
||||
|
||||
/**
|
||||
* Handle error paths
|
||||
@@ -65,6 +66,7 @@ export const mergeServerFormState = ({
|
||||
if (incomingState[path]?.filterOptions || newFieldState.filterOptions) {
|
||||
if (!dequal(incomingState[path]?.filterOptions, newFieldState.filterOptions)) {
|
||||
changed = true
|
||||
fieldChanged = true
|
||||
newFieldState.filterOptions = incomingState[path].filterOptions
|
||||
}
|
||||
}
|
||||
@@ -75,6 +77,7 @@ export const mergeServerFormState = ({
|
||||
serverPropsToAccept.forEach((prop) => {
|
||||
if (!dequal(incomingState[path]?.[prop], newFieldState[prop])) {
|
||||
changed = true
|
||||
fieldChanged = true
|
||||
if (!(prop in incomingState[path])) {
|
||||
// Regarding excluding the customComponents prop from being deleted: the incoming state might not have been rendered, as rendering components for every form onchange is expensive.
|
||||
// Thus, we simply re-use the initial render state
|
||||
@@ -87,18 +90,25 @@ export const mergeServerFormState = ({
|
||||
}
|
||||
})
|
||||
|
||||
if (newFieldState.valid !== false) {
|
||||
newFieldState.valid = true
|
||||
}
|
||||
if (newFieldState.passesCondition !== false) {
|
||||
newFieldState.passesCondition = true
|
||||
}
|
||||
|
||||
// Conditions don't work if we don't memcopy the new state, as the object references would otherwise be the same
|
||||
newState[path] = { ...newFieldState }
|
||||
})
|
||||
newState[path] = fieldChanged ? { ...newFieldState } : newFieldState
|
||||
}
|
||||
|
||||
// Now loop over values that are part of incoming state but not part of existing state, and add them to the new state.
|
||||
// This can happen if a new array row was added. In our local state, we simply add out stubbed `array` and `array.[index].id` entries to the local form state.
|
||||
// However, all other array sub-fields are not added to the local state - those will be added by the server and may be incoming here.
|
||||
|
||||
for (const [path, newFieldState] of Object.entries(incomingState)) {
|
||||
for (const [path, field] of Object.entries(incomingState)) {
|
||||
if (!existingState[path]) {
|
||||
changed = true
|
||||
newState[path] = newFieldState
|
||||
newState[path] = field
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,11 @@ export type Reset = (data: unknown) => Promise<void>
|
||||
|
||||
export type REPLACE_STATE = {
|
||||
optimize?: boolean
|
||||
/**
|
||||
* If `sanitize` is true, default values will be set for form field properties that are not present in the incoming state.
|
||||
* For example, `valid` will be set to true if it is not present in the incoming state.
|
||||
*/
|
||||
sanitize?: boolean
|
||||
state: FormState
|
||||
type: 'REPLACE_STATE'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user