import * as React from 'react'; import { useQueryParams, useNotification, useAPIErrorHandler, getYupValidationErrors, useForm } from '@strapi/admin/strapi-admin'; import { useIntl } from 'react-intl'; import { useParams } from 'react-router-dom'; import { ValidationError } from 'yup'; import { SINGLE_TYPES } from '../constants/collections.mjs'; import { transformDocument } from '../pages/EditView/utils/data.mjs'; import { createDefaultForm } from '../pages/EditView/utils/forms.mjs'; import { useGetDocumentQuery } from '../services/documents.mjs'; import { buildValidParams } from '../utils/api.mjs'; import { createYupSchema } from '../utils/validation.mjs'; import { useContentTypeSchema } from './useContentTypeSchema.mjs'; import { useDocumentLayout } from './useDocumentLayout.mjs'; /** * Mirrors the server's `isLocalizedAttribute` in * `packages/plugins/i18n/server/src/services/content-types.ts`: an attribute * counts as localized only when `pluginOptions.i18n.localized` is explicitly * `true`. Both `false` and `undefined` mean "non-localized" — important here * because attributes created without the i18n plugin omit the field entirely, * and we still want them inherited when creating a new locale draft. */ const isLocalizedAttribute = (attribute)=>{ // `pluginOptions` on the base attribute is typed as `object`, so we have to // narrow it to the i18n-specific shape here. const i18nOptions = attribute.pluginOptions; return i18nOptions?.i18n?.localized === true; }; /* ------------------------------------------------------------------------------------------------- * useDocument * -----------------------------------------------------------------------------------------------*/ /** * @alpha * @public * @description Returns a document based on the model, collection type & id passed as arguments. * Also extracts its schema from the redux cache to be used for creating a validation schema. * @example * ```tsx * const { id, model, collectionType } = useParams<{ id: string; model: string; collectionType: string }>(); * * if(!model || !collectionType) return null; * * const { document, isLoading, validate } = useDocument({ documentId: id, model, collectionType, params: { locale: 'en-GB' } }) * const { update } = useDocumentActions() * * const onSubmit = async (document: Document) => { * const errors = validate(document); * * if(errors) { * // handle errors * } * * await update({ collectionType, model, id }, document) * } * ``` * */ const useDocument = (args, opts)=>{ const { toggleNotification } = useNotification(); const { _unstableFormatAPIError: formatAPIError } = useAPIErrorHandler(); const { formatMessage } = useIntl(); const { currentData: data, isLoading: isLoadingDocument, isFetching: isFetchingDocument, error, refetch } = useGetDocumentQuery(args, { ...opts, skip: !args.documentId && args.collectionType !== SINGLE_TYPES || opts?.skip }); const document = data?.data; const meta = data?.meta; const { components, schema, schemas, isLoading: isLoadingSchema } = useContentTypeSchema(args.model); const isSingleType = schema?.kind === 'singleType'; const getTitle = React.useCallback((mainField)=>{ // Always use mainField if it's not an id if (mainField !== 'id' && document?.[mainField]) { return document[mainField]; } // When it's a singleType without a mainField, use the contentType displayName if (schema?.kind === 'singleType' && schema.info.displayName) { return formatMessage({ id: schema.info.displayName, defaultMessage: schema.info.displayName }); } // Otherwise, use a fallback return formatMessage({ id: 'content-manager.containers.untitled', defaultMessage: 'Untitled' }); }, [ document, formatMessage, schema ]); React.useEffect(()=>{ if (error) { toggleNotification({ type: 'danger', message: formatAPIError(error) }); } }, [ toggleNotification, error, formatAPIError, args.collectionType ]); const validationSchema = React.useMemo(()=>{ if (!schema) { return null; } return createYupSchema(schema.attributes, components); }, [ schema, components ]); const validate = React.useCallback((document)=>{ if (!validationSchema) { throw new Error('There is no validation schema generated, this is likely due to the schema not being loaded yet.'); } try { validationSchema.validateSync(document, { abortEarly: false, strict: true }); return null; } catch (error) { if (error instanceof ValidationError) { return getYupValidationErrors(error); } throw error; } }, [ validationSchema ]); /** * Schema attributes that should be inherited from a sibling locale when * creating a new locale draft. * * Scope intentionally matches what the server populates into * `meta.availableLocales` (see `packages/core/content-manager/server/src/services/document-metadata.ts`): * - non-localized — `isLocalizedAttribute` semantics, so attributes whose * `pluginOptions.i18n.localized` is `undefined` count as non-localized * (an attribute created without the i18n plugin has no i18n options at * all but is still inherited at save by `copyNonLocalizedFields`). * - scalar or media — `component` / `dynamiczone` / `relation` are excluded * because the server doesn't populate them in `availableLocales` either, * so there'd be nothing to copy from. */ const nonLocalizedScalarAndMediaFields = React.useMemo(()=>{ if (!schema?.attributes) { return []; } return Object.keys(schema.attributes).filter((name)=>{ const attribute = schema.attributes[name]; if (isLocalizedAttribute(attribute)) { return false; } return attribute.type !== 'component' && attribute.type !== 'dynamiczone' && attribute.type !== 'relation'; }); }, [ schema ]); /** * Here we prepare the form for editing, we need to: * - remove prohibited fields from the document (passwords | ADD YOURS WHEN THERES A NEW ONE) * - swap out count objects on relations for empty arrays * - set __temp_key__ on array objects for drag & drop * * We also prepare the form for new documents, so we need to: * - set default values on fields * - inherit non-localized scalar/media values from a sibling locale. * Scope is intentionally limited to scalars and media — that is exactly * what the server populates into `meta.availableLocales` (see * `packages/core/content-manager/server/src/services/document-metadata.ts`). * Components, dynamic zones, and relations are not in that payload; the * server fills them at save time via `copyNonLocalizedFields` (in * `packages/core/core/src/services/document-service/internationalization.ts`) * when the new locale row is first created. * Baking the inheritance into `initialValues` here is what lets it survive * `