Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
432 lines
14 KiB
Plaintext
432 lines
14 KiB
Plaintext
'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 |