Files
klz-cables.com/.pnpm-store/v10/files/a7/87ea057ecc3cb16c66cd66c70e837e143b7beb2bd946d2005f6442f6ec1f25c05b82d1e9e67ebbd184e8b33a140afb10d3dabd8bfe2185849e04d18e25dea6
Marc Mintel 5397309103
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
fix(products): fix breadcrumbs and product filtering (backport from main)
2026-02-24 16:04:21 +01:00

742 lines
26 KiB
Plaintext

'use client';
import { jsx as _jsx } from "react/jsx-runtime";
import { dequal } from 'dequal/lite'; // lite: no need for Map and Set support
import { useRouter } from 'next/navigation.js';
import { serialize } from 'object-to-formdata';
import { deepCopyObjectSimpleWithoutReactComponents, getDataByPath as getDataByPathFunc, getSiblingData as getSiblingDataFunc, hasDraftValidationEnabled, reduceFieldsToValues, wait } from 'payload/shared';
import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react';
import { toast } from 'sonner';
import { FieldErrorsToast } from '../../elements/Toasts/fieldErrors.js';
import { useDebouncedEffect } from '../../hooks/useDebouncedEffect.js';
import { useEffectEvent } from '../../hooks/useEffectEvent.js';
import { useQueue } from '../../hooks/useQueue.js';
import { useThrottledEffect } from '../../hooks/useThrottledEffect.js';
import { useAuth } from '../../providers/Auth/index.js';
import { useConfig } from '../../providers/Config/index.js';
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js';
import { useLocale } from '../../providers/Locale/index.js';
import { useOperation } from '../../providers/Operation/index.js';
import { useRouteTransition } from '../../providers/RouteTransition/index.js';
import { useServerFunctions } from '../../providers/ServerFunctions/index.js';
import { useTranslation } from '../../providers/Translation/index.js';
import { useUploadHandlers } from '../../providers/UploadHandlers/index.js';
import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js';
import { requests } from '../../utilities/api.js';
import { BackgroundProcessingContext, DocumentFormContext, FormContext, FormFieldsContext, FormWatchContext, InitializingContext, ModifiedContext, ProcessingContext, SubmittedContext, useDocumentForm } from './context.js';
import { errorMessages } from './errorMessages.js';
import { fieldReducer } from './fieldReducer.js';
import { initContextState } from './initContextState.js';
const baseClass = 'form';
export const Form = props => {
const {
id,
collectionSlug,
docConfig,
docPermissions,
getDocPreferences,
globalSlug
} = useDocumentInfo();
const validateDrafts = hasDraftValidationEnabled(docConfig);
const {
action,
beforeSubmit,
children,
className,
disabled: disabledFromProps,
disableSuccessStatus,
disableValidationOnSubmit,
// fields: fieldsFromProps = collection?.fields || global?.fields,
el,
handleResponse,
initialState,
isDocumentForm,
isInitializing: initializingFromProps,
onChange,
onSubmit,
onSuccess,
redirect,
submitted: submittedFromProps,
uuid,
waitForAutocomplete
} = props;
const method = 'method' in props ? props?.method : undefined;
const router = useRouter();
const documentForm = useDocumentForm();
const {
code: locale
} = useLocale();
const {
i18n,
t
} = useTranslation();
const {
refreshCookie,
user
} = useAuth();
const operation = useOperation();
const {
queueTask
} = useQueue();
const {
getFormState
} = useServerFunctions();
const {
startRouteTransition
} = useRouteTransition();
const {
getUploadHandler
} = useUploadHandlers();
const {
config
} = useConfig();
const [disabled, setDisabled] = useState(disabledFromProps || false);
const [isMounted, setIsMounted] = useState(false);
const [submitted, setSubmitted] = useState(false);
/**
* Tracks wether the form state passes validation.
* For example the state could be submitted but invalid as field errors have been returned.
*/
const [isValid, setIsValid] = useState(true);
const [initializing, setInitializing] = useState(initializingFromProps);
const [processing, setProcessing] = useState(false);
/**
* Determines whether the form is processing asynchronously in the background, e.g. autosave is running.
* Useful to determine whether to disable the form or queue other processes while in flight, e.g. disable manual submits while an autosave is running.
*/
const [backgroundProcessing, _setBackgroundProcessing] = useState(false);
/**
* A ref that can be read within the `setModified` interceptor.
* Dependents of this state can read it immediately without needing to wait for a render cycle.
*/
const backgroundProcessingRef = useRef(backgroundProcessing);
/**
* Flag to track if the form was modified _during a submission_, e.g. while autosave is running.
* Useful in order to avoid resetting `modified` to false wrongfully after a submit.
* For example, if the user modifies a field while the a background process (autosave) is running,
* we need to ensure that after the submit completes, the `modified` state remains true.
*/
const modifiedWhileProcessingRef = useRef(false);
/**
* Intercept the `setBackgroundProcessing` method to keep the ref in sync.
* See the `backgroundProcessingRef` for more details.
*/
const setBackgroundProcessing = useCallback(backgroundProcessing_0 => {
backgroundProcessingRef.current = backgroundProcessing_0;
_setBackgroundProcessing(backgroundProcessing_0);
}, []);
const [modified, _setModified] = useState(false);
/**
* Intercept the `setModified` method to track whether the event happened during background processing.
* See the `modifiedWhileProcessingRef` ref for more details.
*/
const setModified = useCallback(modified_0 => {
if (backgroundProcessingRef.current) {
modifiedWhileProcessingRef.current = true;
}
_setModified(modified_0);
}, []);
const formRef = useRef(null);
const contextRef = useRef({});
const abortResetFormRef = useRef(null);
const isFirstRenderRef = useRef(true);
const fieldsReducer = useReducer(fieldReducer, {}, () => initialState);
const [formState, dispatchFields] = fieldsReducer;
contextRef.current.fields = formState;
const prevFormState = useRef(formState);
const validateForm = useCallback(async () => {
const validatedFieldState = {};
let isValid_0 = true;
const data = contextRef.current.getData();
const validationPromises = Object.entries(contextRef.current.fields).map(async ([path, field]) => {
const validatedField = field;
const pathSegments = path ? path.split('.') : [];
if (field.passesCondition !== false) {
let validationResult = validatedField.valid;
if ('validate' in field && typeof field.validate === 'function') {
let valueToValidate = field.value;
if (field?.rows && Array.isArray(field.rows)) {
valueToValidate = contextRef.current.getDataByPath(path);
}
validationResult = await field.validate(valueToValidate, {
...field,
id,
collectionSlug,
// If there is a parent document form, we can get the data from that form
blockData: undefined,
data: documentForm?.getData ? documentForm.getData() : data,
event: 'submit',
operation,
path: pathSegments,
preferences: {},
req: {
payload: {
config
},
t,
user
},
siblingData: contextRef.current.getSiblingData(path)
});
if (typeof validationResult === 'string') {
validatedField.errorMessage = validationResult;
validatedField.valid = false;
} else {
validatedField.valid = true;
validatedField.errorMessage = undefined;
}
}
if (validatedField.valid === false) {
isValid_0 = false;
}
}
validatedFieldState[path] = validatedField;
});
await Promise.all(validationPromises);
if (!dequal(contextRef.current.fields, validatedFieldState)) {
dispatchFields({
type: 'REPLACE_STATE',
state: validatedFieldState
});
}
setIsValid(isValid_0);
return isValid_0;
}, [collectionSlug, config, dispatchFields, id, operation, t, user, documentForm]);
const submit = useCallback(async (options, e) => {
const {
acceptValues = true,
action: actionArg = action,
context,
disableFormWhileProcessing = true,
disableSuccessStatus: disableSuccessStatusFromArgs,
method: methodToUse = method,
overrides: overridesFromArgs = {},
skipValidation
} = options || {};
const disableToast = disableSuccessStatusFromArgs ?? disableSuccessStatus;
if (disabled) {
if (e) {
e.preventDefault();
}
return;
}
// create new toast promise which will resolve manually later
let errorToast, successToast;
const promise = new Promise((resolve, reject) => {
successToast = resolve;
errorToast = reject;
});
const hasFormSubmitAction = actionArg || typeof action === 'string' || typeof action === 'function';
if (redirect || disableToast || !hasFormSubmitAction) {
// Do not show submitting toast, as the promise toast may never disappear under these conditions.
// Instead, make successToast() or errorToast() throw toast.success / toast.error
successToast = data_0 => toast.success(data_0);
errorToast = data_1 => toast.error(data_1);
} else {
toast.promise(promise, {
error: data_2 => {
return data_2;
},
loading: t('general:submitting'),
success: data_3 => {
return data_3;
}
});
}
if (e) {
e.stopPropagation();
e.preventDefault();
}
if (disableFormWhileProcessing) {
setProcessing(true);
setDisabled(true);
}
if (waitForAutocomplete) {
await wait(100);
}
const data_4 = reduceFieldsToValues(contextRef.current.fields, true);
const serializableFormState = deepCopyObjectSimpleWithoutReactComponents(contextRef.current.fields, {
excludeFiles: true
});
// Execute server side validations
if (Array.isArray(beforeSubmit)) {
let revalidatedFormState;
await beforeSubmit.reduce(async (priorOnChange, beforeSubmitFn) => {
await priorOnChange;
const result = await beforeSubmitFn({
formState: serializableFormState
});
revalidatedFormState = result;
}, Promise.resolve());
const isValid_1 = Object.entries(revalidatedFormState).every(([, field_0]) => field_0.valid !== false);
setIsValid(isValid_1);
if (!isValid_1) {
setProcessing(false);
setSubmitted(true);
setDisabled(false);
return dispatchFields({
type: 'REPLACE_STATE',
state: revalidatedFormState
});
}
}
const isValid_2 = skipValidation || disableValidationOnSubmit ? true : await contextRef.current.validateForm();
setIsValid(isValid_2);
// If not valid, prevent submission
if (!isValid_2) {
errorToast(t('error:correctInvalidFields'));
setProcessing(false);
setSubmitted(true);
setDisabled(false);
return;
}
let overrides = {};
if (typeof overridesFromArgs === 'function') {
overrides = overridesFromArgs(contextRef.current.fields);
} else if (typeof overridesFromArgs === 'object') {
overrides = overridesFromArgs;
}
// If submit handler comes through via props, run that
if (onSubmit) {
for (const [key, value] of Object.entries(overrides)) {
data_4[key] = value;
}
onSubmit(contextRef.current.fields, data_4);
}
if (!hasFormSubmitAction) {
// No action provided, so we should return. An example where this happens are lexical link drawers. Upon submitting the drawer, we
// want to close it without submitting the form. Stuff like validation would be handled by lexical before this, through beforeSubmit
setProcessing(false);
setSubmitted(true);
setDisabled(false);
return;
}
try {
const formData = await contextRef.current.createFormData(overrides, {
data: data_4,
mergeOverrideData: Boolean(typeof overridesFromArgs !== 'function')
});
let res;
if (typeof actionArg === 'string') {
res = await requests[methodToUse.toLowerCase()](actionArg, {
body: formData,
headers: {
'Accept-Language': i18n.language
}
});
} else if (typeof action === 'function') {
res = await action(formData);
}
if (!modifiedWhileProcessingRef.current) {
setModified(false);
} else {
modifiedWhileProcessingRef.current = false;
}
setDisabled(false);
if (typeof handleResponse === 'function') {
handleResponse(res, successToast, errorToast);
return;
}
const contentType = res.headers.get('content-type');
const isJSON = contentType && contentType.indexOf('application/json') !== -1;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let json = {};
if (isJSON) {
json = await res.json();
}
if (res.status < 400) {
if (typeof onSuccess === 'function') {
const newFormState = await onSuccess(json, {
context,
formState: serializableFormState
});
if (newFormState) {
dispatchFields({
type: 'MERGE_SERVER_STATE',
acceptValues,
prevStateRef: prevFormState,
serverState: newFormState
});
}
}
setSubmitted(false);
setProcessing(false);
if (redirect) {
startRouteTransition(() => router.push(redirect));
} else if (!disableToast) {
successToast(json.message || t('general:submissionSuccessful'));
}
} else {
setProcessing(false);
setSubmitted(true);
// When there was an error submitting a draft,
// set the form state to unsubmitted, to not trigger visible form validation on changes after the failed submit.
// Also keep the form as modified so the save button remains enabled for retry.
if (overridesFromArgs['_status'] === 'draft') {
setModified(true);
if (!validateDrafts) {
setSubmitted(false);
}
}
contextRef.current = {
...contextRef.current
}; // triggers rerender of all components that subscribe to form
if (json.message) {
errorToast(json.message);
return;
}
if (Array.isArray(json.errors)) {
const [fieldErrors, nonFieldErrors] = json.errors.reduce(([fieldErrs, nonFieldErrs], err_0) => {
const newFieldErrs = [];
const newNonFieldErrs = [];
if (err_0?.message) {
newNonFieldErrs.push(err_0);
}
if (Array.isArray(err_0?.data?.errors)) {
err_0.data?.errors.forEach(dataError => {
if (dataError?.path) {
newFieldErrs.push(dataError);
} else {
newNonFieldErrs.push(dataError);
}
});
}
return [[...fieldErrs, ...newFieldErrs], [...nonFieldErrs, ...newNonFieldErrs]];
}, [[], []]);
setIsValid(false);
dispatchFields({
type: 'ADD_SERVER_ERRORS',
errors: fieldErrors
});
nonFieldErrors.forEach(err_1 => {
errorToast(/*#__PURE__*/_jsx(FieldErrorsToast, {
errorMessage: err_1.message || t('error:unknown')
}));
});
return;
}
const message = errorMessages?.[res.status] || res?.statusText || t('error:unknown');
errorToast(message);
}
return {
formState: contextRef.current.fields,
res
};
} catch (err) {
console.error('Error submitting form', err); // eslint-disable-line no-console
setProcessing(false);
setSubmitted(true);
setDisabled(false);
errorToast(err.message);
}
}, [beforeSubmit, startRouteTransition, action, disableSuccessStatus, disableValidationOnSubmit, disabled, dispatchFields, handleResponse, method, onSubmit, onSuccess, redirect, router, t, i18n, validateDrafts, waitForAutocomplete, setModified, setSubmitted]);
const getFields = useCallback(() => contextRef.current.fields, []);
const getField = useCallback(path_0 => contextRef.current.fields[path_0], []);
const getData = useCallback(() => reduceFieldsToValues(contextRef.current.fields, true), []);
const getSiblingData = useCallback(path_1 => getSiblingDataFunc(contextRef.current.fields, path_1), []);
const getDataByPath = useCallback(path_2 => getDataByPathFunc(contextRef.current.fields, path_2), []);
const createFormData = useCallback(async (overrides_0, {
data: dataFromArgs,
mergeOverrideData = true
}) => {
let data_5 = dataFromArgs || reduceFieldsToValues(contextRef.current.fields, true);
let file = data_5?.file;
if (docConfig && 'upload' in docConfig && docConfig.upload && file) {
delete data_5.file;
const handler = getUploadHandler({
collectionSlug
});
if (typeof handler === 'function') {
let filename = file.name;
const clientUploadContext = await handler({
file,
updateFilename: value_0 => {
filename = value_0;
}
});
file = JSON.stringify({
clientUploadContext,
collectionSlug,
filename,
mimeType: file.type,
size: file.size
});
}
}
if (mergeOverrideData) {
data_5 = {
...data_5,
...overrides_0
};
} else {
data_5 = overrides_0;
}
const dataToSerialize = {
_payload: JSON.stringify(data_5)
};
if (docConfig && 'upload' in docConfig && docConfig.upload && file) {
dataToSerialize.file = file;
}
// nullAsUndefineds is important to allow uploads and relationship fields to clear themselves
const formData_0 = serialize(dataToSerialize, {
indices: true,
nullsAsUndefineds: false
});
return formData_0;
}, [collectionSlug, docConfig, getUploadHandler]);
const reset = useCallback(async data_6 => {
const controller = handleAbortRef(abortResetFormRef);
const docPreferences = await getDocPreferences();
const {
state: newState
} = await getFormState({
id,
collectionSlug,
data: data_6,
docPermissions,
docPreferences,
globalSlug,
locale,
operation,
renderAllFields: true,
schemaPath: collectionSlug ? collectionSlug : globalSlug,
signal: controller.signal,
skipValidation: true
});
contextRef.current = {
...initContextState
};
setModified(false);
dispatchFields({
type: 'REPLACE_STATE',
state: newState
});
abortResetFormRef.current = null;
}, [collectionSlug, dispatchFields, globalSlug, id, operation, getFormState, docPermissions, getDocPreferences, locale, setModified]);
const replaceState = useCallback(state => {
contextRef.current = {
...initContextState
};
setModified(false);
dispatchFields({
type: 'REPLACE_STATE',
state
});
}, [dispatchFields, setModified]);
const addFieldRow = useCallback(({
blockType,
path: path_3,
rowIndex: rowIndexArg,
subFieldState
}) => {
const newRows = getDataByPath(path_3) || [];
const rowIndex = rowIndexArg === undefined ? newRows.length : rowIndexArg;
// dispatch ADD_ROW adds a blank row to local form state.
// This performs no form state request, as the debounced onChange effect will do that for us.
dispatchFields({
type: 'ADD_ROW',
blockType,
path: path_3,
rowIndex,
subFieldState
});
setModified(true);
}, [dispatchFields, getDataByPath, setModified]);
const moveFieldRow = useCallback(({
moveFromIndex,
moveToIndex,
path: path_4
}) => {
dispatchFields({
type: 'MOVE_ROW',
moveFromIndex,
moveToIndex,
path: path_4
});
setModified(true);
}, [dispatchFields, setModified]);
const removeFieldRow = useCallback(({
path: path_5,
rowIndex: rowIndex_0
}) => {
dispatchFields({
type: 'REMOVE_ROW',
path: path_5,
rowIndex: rowIndex_0
});
setModified(true);
}, [dispatchFields, setModified]);
const replaceFieldRow = useCallback(({
blockType: blockType_0,
path: path_6,
rowIndex: rowIndexArg_0,
subFieldState: subFieldState_0
}) => {
const currentRows = getDataByPath(path_6);
const rowIndex_1 = rowIndexArg_0 === undefined ? currentRows.length : rowIndexArg_0;
dispatchFields({
type: 'REPLACE_ROW',
blockType: blockType_0,
path: path_6,
rowIndex: rowIndex_1,
subFieldState: subFieldState_0
});
setModified(true);
}, [dispatchFields, getDataByPath, setModified]);
useEffect(() => {
const abortOnChange = abortResetFormRef.current;
return () => {
abortAndIgnore(abortOnChange);
};
}, []);
useEffect(() => {
if (initializingFromProps !== undefined) {
setInitializing(initializingFromProps);
}
}, [initializingFromProps]);
contextRef.current.submit = submit;
contextRef.current.getFields = getFields;
contextRef.current.getField = getField;
contextRef.current.getData = getData;
contextRef.current.getSiblingData = getSiblingData;
contextRef.current.getDataByPath = getDataByPath;
contextRef.current.validateForm = validateForm;
contextRef.current.createFormData = createFormData;
contextRef.current.setModified = setModified;
contextRef.current.setProcessing = setProcessing;
contextRef.current.setBackgroundProcessing = setBackgroundProcessing;
contextRef.current.setSubmitted = setSubmitted;
contextRef.current.setIsValid = setIsValid;
contextRef.current.disabled = disabled;
contextRef.current.setDisabled = setDisabled;
contextRef.current.formRef = formRef;
contextRef.current.reset = reset;
contextRef.current.replaceState = replaceState;
contextRef.current.dispatchFields = dispatchFields;
contextRef.current.addFieldRow = addFieldRow;
contextRef.current.removeFieldRow = removeFieldRow;
contextRef.current.moveFieldRow = moveFieldRow;
contextRef.current.replaceFieldRow = replaceFieldRow;
contextRef.current.uuid = uuid;
contextRef.current.initializing = initializing;
contextRef.current.isValid = isValid;
useEffect(() => {
setIsMounted(true);
}, []);
useEffect(() => {
if (typeof disabledFromProps === 'boolean') {
setDisabled(disabledFromProps);
}
}, [disabledFromProps]);
useEffect(() => {
if (typeof submittedFromProps === 'boolean') {
setSubmitted(submittedFromProps);
}
}, [submittedFromProps]);
useEffect(() => {
if (initialState) {
contextRef.current = {
...initContextState
};
dispatchFields({
type: 'REPLACE_STATE',
optimize: false,
sanitize: true,
state: initialState
});
}
}, [initialState, dispatchFields]);
useThrottledEffect(() => {
refreshCookie();
}, 15000, [formState]);
const handleLocaleChange = useEffectEvent(() => {
contextRef.current = {
...contextRef.current
}; // triggers rerender of all components that subscribe to form
setModified(false);
});
useEffect(() => {
handleLocaleChange();
}, [locale]);
const classes = [className, baseClass].filter(Boolean).join(' ');
const executeOnChange = useEffectEvent(submitted_0 => {
queueTask(async () => {
if (Array.isArray(onChange)) {
let serverState;
for (const onChangeFn of onChange) {
// Edit view default onChange is in packages/ui/src/views/Edit/index.tsx. This onChange usually sends a form state request
serverState = await onChangeFn({
formState: deepCopyObjectSimpleWithoutReactComponents(formState, {
excludeFiles: true
}),
submitted: submitted_0
});
}
dispatchFields({
type: 'MERGE_SERVER_STATE',
prevStateRef: prevFormState,
serverState
});
}
});
});
useDebouncedEffect(() => {
if ((isFirstRenderRef.current || !dequal(formState, prevFormState.current)) && modified) {
executeOnChange(submitted);
}
prevFormState.current = formState;
isFirstRenderRef.current = false;
}, [modified, submitted, formState], 250);
const DocumentFormContextComponent = isDocumentForm ? DocumentFormContext : React.Fragment;
const documentFormContextProps = isDocumentForm ? {
value: contextRef.current
} : {};
const El = el || 'form';
return /*#__PURE__*/_jsx(El, {
action: typeof action === 'function' ? void action : action,
className: classes,
/**
* data-form-ready signals if the form is ready to be used. This is used by our e2e tests
* to wait for the form to be ready before interacting with it, reducing flakiness if the test is run in
* slow network conditions.
*/
"data-form-ready": !processing && isMounted && !initializing,
method: method,
noValidate: true,
onSubmit: e_0 => void contextRef.current.submit({}, e_0),
ref: formRef,
children: /*#__PURE__*/_jsx(DocumentFormContextComponent, {
...documentFormContextProps,
children: /*#__PURE__*/_jsx(FormContext, {
value: contextRef.current,
children: /*#__PURE__*/_jsx(FormWatchContext, {
value: {
fields: formState,
...contextRef.current
},
children: /*#__PURE__*/_jsx(SubmittedContext, {
value: submitted,
children: /*#__PURE__*/_jsx(InitializingContext, {
value: !isMounted || isMounted && initializing,
children: /*#__PURE__*/_jsx(ProcessingContext, {
value: processing,
children: /*#__PURE__*/_jsx(BackgroundProcessingContext, {
value: backgroundProcessing,
children: /*#__PURE__*/_jsx(ModifiedContext, {
value: modified,
children: /*#__PURE__*/_jsx(FormFieldsContext.Provider, {
value: fieldsReducer,
children: children
})
})
})
})
})
})
})
})
})
});
};
export { DocumentFormContext, FormContext, FormFieldsContext, FormWatchContext, ModifiedContext, ProcessingContext, SubmittedContext, useAllFormFields, useDocumentForm, useForm, useFormFields, useFormModified, useFormProcessing, useFormSubmitted, useWatchForm } from './context.js';
//# sourceMappingURL=index.js.map