'use client'; import ObjectIdImport from 'bson-objectid'; import { dequal } from 'dequal/lite'; // lite: no need for Map and Set support import { deepCopyObjectSimpleWithoutReactComponents } from 'payload/shared'; import { mergeServerFormState } from './mergeServerFormState.js'; import { flattenRows, separateRows } from './rows.js'; const ObjectId = 'default' in ObjectIdImport ? ObjectIdImport.default : ObjectIdImport; /** * Reducer which modifies the form field state (all the current data of the fields in the form). When called using dispatch, it will return a new state object. */ export function fieldReducer(state, action) { switch (action.type) { case 'ADD_ROW': { const { blockType, path, rowIndex: rowIndexFromArgs, subFieldState = {} } = action; const rowIndex = typeof rowIndexFromArgs === 'number' ? rowIndexFromArgs : state[path]?.rows?.length || 0; const withNewRow = [...(state[path]?.rows || [])]; const newRow = { id: subFieldState?.id?.value || new ObjectId().toHexString(), isLoading: true }; if (blockType) { newRow.blockType = blockType; } withNewRow.splice(rowIndex, 0, newRow); if (blockType) { subFieldState.blockType = { initialValue: blockType, valid: true, value: blockType }; } // add new row to array _field state_ const { remainingFields, rows: siblingRows } = separateRows(path, state); siblingRows.splice(rowIndex, 0, subFieldState); const newState = { ...remainingFields, ...flattenRows(path, siblingRows), [`${path}.${rowIndex}.id`]: { initialValue: newRow.id, passesCondition: true, valid: true, value: newRow.id }, [path]: { ...state[path], disableFormData: true, rows: withNewRow, value: siblingRows.length } }; return newState; } case 'ADD_SERVER_ERRORS': { let newState = { ...state }; const errorPaths = []; action.errors.forEach(({ message, path: fieldPath }) => { newState[fieldPath] = { ...(newState[fieldPath] || { initialValue: null, value: null }), errorMessage: message, valid: false }; const segments = fieldPath.split('.'); if (segments.length > 1) { errorPaths.push({ fieldErrorPath: fieldPath, parentPath: segments.slice(0, segments.length - 1).join('.') }); } }); newState = Object.entries(newState).reduce((acc, [path, fieldState]) => { const fieldErrorPaths = errorPaths.reduce((errorACC, { fieldErrorPath, parentPath }) => { if (parentPath.startsWith(path)) { errorACC.push(fieldErrorPath); } return errorACC; }, []); let changed = false; if (fieldErrorPaths.length > 0) { const newErrorPaths = Array.isArray(fieldState.errorPaths) ? fieldState.errorPaths : []; fieldErrorPaths.forEach(fieldErrorPath => { if (!newErrorPaths.includes(fieldErrorPath)) { newErrorPaths.push(fieldErrorPath); changed = true; } }); if (changed) { acc[path] = { ...fieldState, errorPaths: newErrorPaths }; } } if (!changed) { acc[path] = fieldState; } return acc; }, {}); return newState; } /** * Duplicates a row in an array or blocks field. * It needs to manipulate two distinct parts of the form state: * - The `rows` property of the parent field, e.g. `array.rows`, `blocks.rows`, etc. * - The row's state, e.g. `array.0.id`, `array.0.text`, etc. */ case 'DUPLICATE_ROW': { const { path, rowIndex } = action; const { remainingFields, rows } = separateRows(path, state); // 1. Duplicate the `rows` property of the parent field, e.g. `array.rows`, `blocks.rows`, etc. const newRows = [...(state[path].rows || [])]; const newRow = deepCopyObjectSimpleWithoutReactComponents(newRows[rowIndex]); const newRowID = new ObjectId().toHexString(); if (newRow.id) { newRow.id = newRowID; } if (newRows[rowIndex]?.customComponents?.RowLabel) { newRow.customComponents = { RowLabel: newRows[rowIndex].customComponents.RowLabel }; } // 2. Duplicate the row's state, e.g. `array.0.id`, `array.0.text`, etc. const newRowState = deepCopyObjectSimpleWithoutReactComponents(rows[rowIndex]); // Ensure that `id` in form state exactly matches the row id on the parent field if (newRowState.id) { newRowState.id.value = newRowID; newRowState.id.initialValue = newRowID; } // Generate new ids for all nested id fields, e.g. `array.0.nestedArray.0.id` for (const key of Object.keys(newRowState).filter(key => key.endsWith('.id'))) { const idState = newRowState[key]; const newNestedFieldID = new ObjectId().toHexString(); if (idState && typeof idState.value === 'string' && ObjectId.isValid(idState.value)) { newRowState[key].value = newNestedFieldID; newRowState[key].initialValue = newNestedFieldID; // Apply the ID to its corresponding parent field's rows, e.g. `array.0.nestedArray.rows[0].id` const segments = key.split('.'); const rowIndex = parseInt(segments[segments.length - 2], 10); const parentFieldPath = segments.slice(0, segments.length - 2).join('.'); const parentFieldRows = newRowState?.[parentFieldPath]?.rows; if (newRowState[parentFieldPath] && Array.isArray(parentFieldRows)) { if (!parentFieldRows[rowIndex]) { parentFieldRows[rowIndex] = { id: newNestedFieldID }; } else { parentFieldRows[rowIndex].id = newNestedFieldID; } } } } // If there are subfields if (Object.keys(newRowState).length > 0) { // Add new object containing subfield names to unflattenedRows array rows.splice(rowIndex + 1, 0, newRowState); newRows.splice(rowIndex + 1, 0, newRow); } const newState = { ...remainingFields, ...flattenRows(path, rows), [path]: { ...state[path], disableFormData: true, rows: newRows, value: rows.length } }; return newState; } case 'MERGE_SERVER_STATE': { const { acceptValues, prevStateRef, serverState } = action; const newState = mergeServerFormState({ acceptValues, currentState: state || {}, incomingState: serverState }); prevStateRef.current = newState; return newState; } case 'MOVE_ROW': { const { moveFromIndex, moveToIndex, path } = action; // Handle moving rows on the top-level, i.e. `array.0.text` -> `array.1.text` const { remainingFields, rows: topLevelRows } = separateRows(path, state); const copyOfMovingRow = topLevelRows[moveFromIndex]; topLevelRows.splice(moveFromIndex, 1); topLevelRows.splice(moveToIndex, 0, copyOfMovingRow); // modify array/block internal row state (i.e. collapsed, blockType) const rowsWithinField = [...(state[path]?.rows || [])]; const copyOfMovingRow2 = { ...rowsWithinField[moveFromIndex] }; rowsWithinField.splice(moveFromIndex, 1); rowsWithinField.splice(moveToIndex, 0, copyOfMovingRow2); const newState = { ...remainingFields, ...flattenRows(path, topLevelRows), [path]: { ...state[path], rows: rowsWithinField } }; return newState; } case 'REMOVE': { const newState = { ...state }; if (newState[action.path]) { delete newState[action.path]; } return newState; } case 'REMOVE_ROW': { const { path, rowIndex } = action; const { remainingFields, rows } = separateRows(path, state); const rowsMetadata = [...(state[path]?.rows || [])]; rows.splice(rowIndex, 1); rowsMetadata.splice(rowIndex, 1); const newState = { ...remainingFields, [path]: { ...state[path], disableFormData: rows.length > 0, rows: rowsMetadata, value: rows.length }, ...flattenRows(path, rows) }; return newState; } case 'REPLACE_ROW': { const { blockType, path, rowIndex: rowIndexArg, subFieldState = {} } = action; const { remainingFields, rows: siblingRows } = separateRows(path, state); const rowIndex = Math.max(0, Math.min(rowIndexArg, siblingRows?.length - 1 || 0)); const rowsMetadata = [...(state[path]?.rows || [])]; rowsMetadata[rowIndex] = { id: new ObjectId().toHexString(), blockType: blockType || undefined, collapsed: false }; if (blockType) { subFieldState.blockType = { initialValue: blockType, valid: true, value: blockType }; } // replace form _field state_ siblingRows[rowIndex] = subFieldState; const newState = { ...remainingFields, ...flattenRows(path, siblingRows), [path]: { ...state[path], disableFormData: true, rows: rowsMetadata, value: siblingRows.length } }; return newState; } case 'REPLACE_STATE': { if (action.optimize !== false) { // Only update fields that have changed // by comparing old value / initialValue to new // .. // This is a performance enhancement for saving // large documents with hundreds of fields const newState = {}; for (const [path, newField] of Object.entries(action.state)) { const oldField = state[path]; 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; } case 'SET_ALL_ROWS_COLLAPSED': { const { path, updatedRows } = action; return { ...state, [path]: { ...state[path], rows: updatedRows } }; } case 'SET_ROW_COLLAPSED': { const { path, updatedRows } = action; const newState = { ...state, [path]: { ...state[path], rows: updatedRows } }; return newState; } case 'UPDATE': { const newField = Object.entries(action).reduce((field, [key, value]) => { if (['disableFormData', 'errorMessage', 'initialValue', 'rows', 'valid', 'validate', 'value'].includes(key)) { return { ...field, [key]: value, ...(key === 'value' ? { isModified: true } : {}) }; } return field; }, state?.[action.path] || {}); const newState = { ...state, [action.path]: newField }; // reset `isModified` in all other fields if ('value' in action) { for (const [path, field] of Object.entries(newState)) { if (path !== action.path && 'isModified' in field) { delete newState[path].isModified; } } } return newState; } case 'UPDATE_MANY': { const newState = { ...state }; Object.entries(action.formState).forEach(([path, field]) => { newState[path] = field; }); return newState; } default: { return state; } } } //# sourceMappingURL=fieldReducer.js.map