import * as React from 'react'; import { useQueryParams, useStrapiApp, useNotification, useAPIErrorHandler } from '@strapi/admin/strapi-admin'; import { HOOKS } from '../constants/hooks.mjs'; import { useGetContentTypeConfigurationQuery } from '../services/contentTypes.mjs'; import { getMainField } from '../utils/attributes.mjs'; import { normalizeContentManagerLayout } from '../utils/layouts/normalizeContentManagerLayout.mjs'; import { useContentTypeSchema } from './useContentTypeSchema.mjs'; import { useDocument, useDoc } from './useDocument.mjs'; const RESOLVED_LAYOUT_CACHE_LIMIT = 25; // Module-level cache keeps resolved layout object identities stable across hook instances // when RTK Query returns the same schema/configuration references. const resolvedLayoutCache = []; const getCachedResolvedLayouts = ({ components, data, model, schema, schemas })=>{ return resolvedLayoutCache.find((entry)=>entry.components === components && entry.data === data && entry.model === model && entry.schema === schema && entry.schemas === schemas)?.value; }; const setCachedResolvedLayouts = (entry)=>{ resolvedLayoutCache.unshift(entry); if (resolvedLayoutCache.length > RESOLVED_LAYOUT_CACHE_LIMIT) { resolvedLayoutCache.pop(); } }; /* ------------------------------------------------------------------------------------------------- * useDocumentLayout * -----------------------------------------------------------------------------------------------*/ const DEFAULT_SETTINGS = { bulkable: false, filterable: false, searchable: false, pagination: false, defaultSortBy: '', defaultSortOrder: 'asc', mainField: 'id', pageSize: 10, relationOpenMode: 'modal' }; /** * @alpha * @description This hook is used to get the layouts for either the edit view or list view of a specific content-type * including the layouts for the components used in the content-type. It also runs the mutation hook waterfall so the data * is consistent wherever it is used. It's a light wrapper around the `useDocument` hook, but provides the `skip` option a document * is not fetched, however, it does fetch the schemas & components if they do not already exist in the cache. * * If the fetch fails, it will display a notification to the user. * * @example * ```tsx * const { model } = useParams<{ model: string }>(); * const { edit: { schema: layout } } = useDocumentLayout(model); * * return layout.map(panel => panel.map(row => row.map(field => ))) * ``` * */ const useDocumentLayout = (model)=>{ const { schema, components } = useDocument({ model, collectionType: '' }, { skip: true }); const [{ query }] = useQueryParams(); const runHookWaterfall = useStrapiApp('useDocumentLayout', (state)=>state.runHookWaterfall); const { toggleNotification } = useNotification(); const { _unstableFormatAPIError: formatAPIError } = useAPIErrorHandler(); const { isLoading: isLoadingSchemas, schemas } = useContentTypeSchema(); const { currentData: data, isLoading: isLoadingConfigs, error } = useGetContentTypeConfigurationQuery(model); const isConfigResolvedForModel = data?.contentType?.uid === model; const isLoading = isLoadingSchemas || isLoadingConfigs || !error && !isConfigResolvedForModel; const stableLayoutsByModelRef = React.useRef(new Map()); React.useEffect(()=>{ if (error) { toggleNotification({ type: 'danger', message: formatAPIError(error) }); } }, [ error, formatAPIError, toggleNotification ]); const resolvedLayouts = React.useMemo(()=>{ if (!data || isLoading) { return null; } const cachedLayouts = getCachedResolvedLayouts({ components, data, model, schema, schemas }); if (cachedLayouts) { return cachedLayouts; } const normalizedData = normalizeContentManagerLayout(data, { schemas, schema, components }); const edit = formatEditLayout(normalizedData, { schemas, schema, components }); const list = formatListLayout(normalizedData, { schemas, schema, components }); const listViewConversionContext = { componentConfigurations: normalizedData.components, componentSchemas: components, contentTypeSchemas: schemas }; const layouts = { edit, list, listViewConversionContext }; setCachedResolvedLayouts({ components, data, model, schema, schemas, value: layouts }); return layouts; }, [ data, isLoading, model, schemas, schema, components ]); React.useEffect(()=>{ if (resolvedLayouts) { stableLayoutsByModelRef.current.set(model, resolvedLayouts); } }, [ model, resolvedLayouts ]); const stableLayouts = resolvedLayouts ?? stableLayoutsByModelRef.current.get(model) ?? null; const editLayout = React.useMemo(()=>stableLayouts?.edit ?? { layout: [], components: {}, metadatas: {}, options: {}, settings: DEFAULT_SETTINGS }, [ stableLayouts ]); const listLayout = React.useMemo(()=>stableLayouts?.list ?? { layout: [], metadatas: {}, options: {}, settings: DEFAULT_SETTINGS }, [ stableLayouts ]); const { layout: edit } = React.useMemo(()=>runHookWaterfall(HOOKS.MUTATE_EDIT_VIEW_LAYOUT, { layout: editLayout, query }), [ editLayout, query, runHookWaterfall ]); const listViewConversionContext = stableLayouts?.listViewConversionContext ?? null; return { error, isLoading, edit, list: listLayout, listViewConversionContext }; }; /* ------------------------------------------------------------------------------------------------- * useDocLayout * -----------------------------------------------------------------------------------------------*/ /** * @internal this hook uses the internal useDoc hook, as such it shouldn't be used outside of the * content-manager because it won't work as intended. */ const useDocLayout = ()=>{ const { model } = useDoc(); return useDocumentLayout(model); }; /** * @internal * @description takes the configuration data, the schema & the components used in the schema and formats the edit view * versions of the schema & components. This is then used to render the edit view of the content-type. */ const formatEditLayout = (data, { schemas, schema, components })=>{ let currentPanelIndex = 0; /** * The fields arranged by the panels, new panels are made for dynamic zones only. */ const panelledEditAttributes = convertEditLayoutToFieldLayouts(data.contentType.layouts.edit, schema?.attributes, data.contentType.metadatas, { configurations: data.components, schemas: components }, schemas).reduce((panels, row)=>{ if (row.some((field)=>field.type === 'dynamiczone')) { panels.push([ row ]); currentPanelIndex += 2; } else { if (!panels[currentPanelIndex]) { panels.push([ row ]); } else { panels[currentPanelIndex].push(row); } } return panels; }, []); const componentEditAttributes = Object.entries(data.components).reduce((acc, [uid, configuration])=>{ const componentSchema = components[uid]; // Persisted configuration can reference component UIDs absent from `/init`. if (!componentSchema) { return acc; } acc[uid] = { layout: convertEditLayoutToFieldLayouts(configuration.layouts.edit, componentSchema.attributes, configuration.metadatas, { configurations: data.components, schemas: components }), settings: { ...configuration.settings, icon: componentSchema.info.icon, displayName: componentSchema.info.displayName } }; return acc; }, {}); const editMetadatas = Object.entries(data.contentType.metadatas).reduce((acc, [attribute, metadata])=>{ return { ...acc, [attribute]: metadata.edit }; }, {}); return { layout: panelledEditAttributes, components: componentEditAttributes, metadatas: editMetadatas, settings: { ...data.contentType.settings, displayName: schema?.info?.displayName }, options: { ...schema?.options, ...schema?.pluginOptions, ...data.contentType.options } }; }; /* ------------------------------------------------------------------------------------------------- * convertEditLayoutToFieldLayouts * -----------------------------------------------------------------------------------------------*/ /** * @internal * @description takes the edit layout from either a content-type or a component * and formats it into a generic object that can be used to correctly render * the form fields. */ const convertEditLayoutToFieldLayouts = (rows, attributes = {}, metadatas, components, schemas = [])=>{ return rows.map((row)=>row.map((field)=>{ const attribute = attributes[field.name]; if (!attribute) { return null; } const fieldMetadata = metadatas[field.name]; if (!fieldMetadata) { return null; } const { edit: metadata } = fieldMetadata; const settings = attribute.type === 'component' && components ? components.configurations[attribute.component]?.settings ?? {} : {}; return { attribute, disabled: !metadata.editable, hint: metadata.description, label: metadata.label ?? '', name: field.name, // @ts-expect-error – mainField does exist on the metadata for a relation. mainField: getMainField(attribute, metadata.mainField || settings.mainField, { schemas, components: components?.schemas ?? {} }), placeholder: metadata.placeholder ?? '', required: attribute.required ?? false, size: field.size, unique: 'unique' in attribute ? attribute.unique : false, visible: metadata.visible ?? true, type: attribute.type }; }).filter((field)=>field !== null)); }; /* ------------------------------------------------------------------------------------------------- * formatListLayout * -----------------------------------------------------------------------------------------------*/ /** * @internal * @description takes the complete configuration data, the schema & the components used in the schema and * formats a list view layout for the content-type. This is much simpler than the edit view layout as there * are less options to consider. */ const formatListLayout = (data, { schemas, schema, components })=>{ const listMetadatas = Object.entries(data.contentType.metadatas).reduce((acc, [attribute, metadata])=>{ return { ...acc, [attribute]: metadata.list }; }, {}); /** * The fields arranged by the panels, new panels are made for dynamic zones only. */ const listAttributes = convertListLayoutToFieldLayouts(data.contentType.layouts.list, schema?.attributes, listMetadatas, { configurations: data.components, schemas: components }, schemas); return { layout: listAttributes, settings: { ...data.contentType.settings, displayName: schema?.info?.displayName }, metadatas: listMetadatas, options: { ...schema?.options, ...schema?.pluginOptions, ...data.contentType.options } }; }; /* ------------------------------------------------------------------------------------------------- * convertListLayoutToFieldLayouts * -----------------------------------------------------------------------------------------------*/ /** * @internal * @description takes the columns from the list view configuration and formats them into a generic object * combinining metadata and attribute data. * * @note We do use this to reformat the list of strings when updating the displayed headers for the list view. */ const convertListLayoutToFieldLayouts = (columns, attributes = {}, metadatas, components, schemas = [])=>{ return columns.map((name)=>{ const attribute = attributes[name]; if (!attribute) { return null; } const metadata = metadatas[name]; if (!metadata) { return null; } const settings = attribute.type === 'component' && components ? components.configurations[attribute.component]?.settings ?? {} : {}; return { attribute, label: metadata.label ?? '', mainField: getMainField(attribute, metadata.mainField || settings.mainField, { schemas, components: components?.schemas ?? {} }), name: name, searchable: metadata.searchable ?? true, sortable: metadata.sortable ?? true }; }).filter((field)=>field !== null); }; export { DEFAULT_SETTINGS, convertEditLayoutToFieldLayouts, convertListLayoutToFieldLayouts, useDocLayout, useDocumentLayout }; //# sourceMappingURL=useDocumentLayout.mjs.map