import { pluralize } from "@prismicio/editor-support/String"; import { Alert, AnimatedSuspense, Badge, Box, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, Icon, IconButton, Skeleton, Text, TextOverflow, Tooltip, TreeView, TreeViewCheckbox, TreeViewSection, } from "@prismicio/editor-ui"; import { CustomType, DynamicWidget, Group, Link, LinkConfig, NestableWidget, } from "@prismicio/types-internal/lib/customtypes"; import { useEffect } from "react"; import { revalidateGetCustomTypes, useCustomTypes as useCustomTypesRequest, } from "@/features/customTypes/useCustomTypes"; import { DefaultErrorBoundary } from "@/features/errorBoundaries"; import { isValidObject } from "@/utils/isValidObject"; type NonReadonly = { -readonly [P in keyof T]: T[P] }; /** * Picker fields check map types. Used internally to keep track of the checked * fields in the TreeView, as it's easier to handle objects than arrays and * also ensures field uniqueness. * * @example * { * author: { * fullName: { * type: "checkbox", * value: true, * }, * awards: { * type: "group", * value: { * date: { * type: "checkbox", * value: true, * }, * awardsCr: { * type: "contentRelationship", * value: { * award: { * title: { * type: "checkbox", * value: true, * }, * issuer: { * type: "group", * value: { * name: { * type: "checkbox", * value: true, * }, * }, * }, * }, * }, * }, * }, * }, * professionCr: { * type: "contentRelationship", * value: { * profession: { * name: { * type: "checkbox", * value: true, * }, * areas: { * type: "group", * value: { * name: { * type: "checkbox", * value: true, * }, * }, * }, * }, * }, * }, * }, * } **/ interface PickerCustomTypes { [customTypeId: string]: PickerCustomType; } interface PickerCustomType { [fieldId: string]: PickerCustomTypeValue; } type PickerCustomTypeValue = | PickerCheckboxField | PickerFirstLevelGroupField | PickerContentRelationshipField; interface PickerCheckboxField { type: "checkbox"; value: boolean; } interface PickerFirstLevelGroupField { type: "group"; value: PickerFirstLevelGroupFieldValue; } interface PickerLeafGroupField { type: "group"; value: PickerLeafGroupFieldValue; } interface PickerLeafGroupFieldValue { [fieldId: string]: PickerCheckboxField; } interface PickerFirstLevelGroupFieldValue { [fieldId: string]: PickerCheckboxField | PickerContentRelationshipField; } interface PickerContentRelationshipField { type: "contentRelationship"; value: PickerContentRelationshipFieldValue; } interface PickerContentRelationshipFieldValue { [customTypeId: string]: PickerNestedCustomTypeValue; } interface PickerNestedCustomTypeValue { [fieldId: string]: PickerCheckboxField | PickerLeafGroupField; } /** * Content relationship Link customtypes property structure. * * @example * [ * { * id: "author", * fields: [ * "fullName", * { * id: "awards", * fields: [ * "date", * { * id: "awardsCr", * customtypes: [ * { * id: "award", * fields: [ * "title", * { * id: "issuer", * fields: ["name"], * }, * ], * }, * ], * }, * ], * }, * { * id: "professionCr", * customtypes: [ * { * id: "profession", * fields: [ * "name", * { * id: "areas", * fields: ["name"], * }, * ], * }, * ], * }, * ], * }, * ] */ type LinkCustomtypes = NonNullable; type LinkCustomtypesFields = Exclude< LinkCustomtypes[number], string >["fields"][number]; type LinkCustomtypesContentRelationshipFieldValue = Exclude< LinkCustomtypesFields, string | { fields: unknown } >; type LinkCustomtypesGroupFieldValue = Exclude< LinkCustomtypesFields, string | { customtypes: unknown } >; interface ContentRelationshipFieldPickerProps { value: LinkCustomtypes | undefined; onChange: (fields: LinkCustomtypes) => void; } export function ContentRelationshipFieldPicker( props: ContentRelationshipFieldPickerProps, ) { return ( ( Error loading your types )} > Loading your types... } > ); } function ContentRelationshipFieldPickerContent( props: ContentRelationshipFieldPickerProps, ) { const { value: linkCustomtypes, onChange } = props; const { allCustomTypes, pickedCustomTypes } = useCustomTypes(linkCustomtypes); const fieldCheckMap = linkCustomtypes ? convertLinkCustomtypesToFieldCheckMap({ linkCustomtypes, allCustomTypes }) : {}; function onCustomTypesChange(id: string, newCustomType: PickerCustomType) { // The picker does not handle strings (custom type ids), as it's only meant // to pick fields from custom types (objects). So we need to merge it with // the existing value, which can have strings in the first level that // represent new types added without any picked fields. onChange( mergeAndConvertCheckMapToLinkCustomtypes({ fieldCheckMap, newCustomType, linkCustomtypes, customTypeId: id, }), ); } function addCustomType(id: string) { const newFields = linkCustomtypes ? [...linkCustomtypes, id] : [id]; onChange(newFields); } function removeCustomType(id: string) { if (linkCustomtypes) { onChange( linkCustomtypes.filter((existingCt) => getId(existingCt) !== id), ); } } return ( {pickedCustomTypes.length > 0 ? ( <> Allowed type Select a single type that editors can link to in the Page Builder.
For the selected type, choose which fields to include in the API response.
{pickedCustomTypes.length > 1 && ( Legacy mode. Keep only one type to enable the improved Content Relationship feature.
See documentation } />
)}
{pickedCustomTypes.map((customType) => ( {pickedCustomTypes.length > 1 ? ( {customType.id} ) : ( onCustomTypesChange(customType.id, value) } fieldCheckMap={fieldCheckMap[customType.id] ?? {}} allCustomTypes={allCustomTypes} /> )} removeCustomType(customType.id)} sx={{ height: 24, width: 24 }} hiddenLabel="Remove type" /> ))} ) : ( )}
Have ideas for improving this field?{" "} Please provide your feedback here .
); } type EmptyViewProps = { onSelect: (customTypeId: string) => void; allCustomTypes: CustomType[]; }; function EmptyView(props: EmptyViewProps) { const { allCustomTypes, onSelect } = props; return ( No type selected Select the type editors can link to.
Then, choose which fields to return in the API.
); } type AddTypeButtonProps = { onSelect: (customTypeId: string) => void; allCustomTypes: CustomType[]; }; function AddTypeButton(props: AddTypeButtonProps) { const { allCustomTypes, onSelect } = props; const triggerButton = ( ); if (allCustomTypes.length === 0) { return ( {triggerButton} ); } return ( {triggerButton} Types {allCustomTypes.map((customType) => ( onSelect(customType.id)} > {customType.id} {getTypeFormatLabel(customType.format)} } color="purple" size="small" /> ))} ); } interface TreeViewCustomTypeProps { customType: CustomType; fieldCheckMap: PickerCustomType; onChange: (newValue: PickerCustomType) => void; allCustomTypes: CustomType[]; } function TreeViewCustomType(props: TreeViewCustomTypeProps) { const { customType, fieldCheckMap: customTypeFieldsCheckMap, onChange: onCustomTypeChange, allCustomTypes, } = props; const renderedFields = getCustomTypeStaticFields(customType).map( ([fieldId, field]) => { // Group field if (field.type === "Group") { const onGroupFieldChange = ( newGroupFields: PickerFirstLevelGroupFieldValue, ) => { onCustomTypeChange({ ...customTypeFieldsCheckMap, [fieldId]: { type: "group", value: newGroupFields }, }); }; const groupFieldCheckMap = customTypeFieldsCheckMap[fieldId] ?? {}; return ( ); } // Content relationship field with custom types if ( isContentRelationshipFieldWithSingleCustomtype(field, allCustomTypes) ) { const onContentRelationshipFieldChange = ( newCrFields: PickerContentRelationshipFieldValue, ) => { onCustomTypeChange({ ...customTypeFieldsCheckMap, [fieldId]: { type: "contentRelationship", value: newCrFields, }, }); }; const crFieldCheckMap = customTypeFieldsCheckMap[fieldId] ?? {}; return ( ); } // Regular field const onCheckedChange = (newValue: boolean) => { onCustomTypeChange({ ...customTypeFieldsCheckMap, [fieldId]: { type: "checkbox", value: newValue }, }); }; return ( ); }, ); const exposedFieldsCount = countPickedFields(customTypeFieldsCheckMap); return ( 0 ? getPickedFieldsLabel( exposedFieldsCount.pickedFields, "returned in the API", ) : "(No fields returned in the API)" } badge={getTypeFormatLabel(customType.format)} defaultOpen > {renderedFields.length > 0 ? renderedFields : } ); } interface TreeViewContentRelationshipFieldProps { fieldId: string; field: Link; fieldCheckMap: PickerContentRelationshipFieldValue; onChange: (newValue: PickerContentRelationshipFieldValue) => void; allCustomTypes: CustomType[]; } function TreeViewContentRelationshipField( props: TreeViewContentRelationshipFieldProps, ) { const { field, fieldId, fieldCheckMap: crFieldsCheckMap, onChange: onCrFieldChange, allCustomTypes, } = props; const resolvedCustomTypes = resolveContentRelationshipCustomTypes( field.config?.customtypes, allCustomTypes, ); const [customType] = resolvedCustomTypes; const onNestedCustomTypeChange = ( newNestedCustomTypeFields: PickerNestedCustomTypeValue, ) => { onCrFieldChange({ ...crFieldsCheckMap, [customType.id]: newNestedCustomTypeFields, }); }; const nestedCtFieldsCheckMap = crFieldsCheckMap[customType.id] ?? {}; const renderedFields = getCustomTypeStaticFields(customType).map( ([fieldId, field]) => { // Group field if (field.type === "Group") { const onGroupFieldsChange = ( newGroupFields: PickerLeafGroupFieldValue, ) => { onNestedCustomTypeChange({ ...nestedCtFieldsCheckMap, [fieldId]: { type: "group", value: newGroupFields }, }); }; const groupFieldCheckMap = nestedCtFieldsCheckMap[fieldId] ?? {}; return ( ); } // Regular field const onCheckedChange = (newChecked: boolean) => { onNestedCustomTypeChange({ ...nestedCtFieldsCheckMap, [fieldId]: { type: "checkbox", value: newChecked }, }); }; return ( ); }, ); return ( {fieldId} → {customType.id} } subtitle={getPickedFieldsLabel( countPickedFields(nestedCtFieldsCheckMap).pickedFields, )} badge={getTypeFormatLabel(customType.format)} > {renderedFields.length > 0 ? renderedFields : } ); } function NoFieldsAvailable() { return No available fields to select; } interface TreeViewLeafGroupFieldProps { group: Group; groupId: string; fieldCheckMap: PickerLeafGroupFieldValue; onChange: (newValue: PickerLeafGroupFieldValue) => void; } function TreeViewLeafGroupField(props: TreeViewLeafGroupFieldProps) { const { group, groupId, fieldCheckMap: groupFieldsCheckMap, onChange: onGroupFieldChange, } = props; const renderedFields = getGroupFields(group).map(({ fieldId }) => { const onCheckedChange = (newChecked: boolean) => { onGroupFieldChange({ ...groupFieldsCheckMap, [fieldId]: { type: "checkbox", value: newChecked }, }); }; return ( ); }); return ( {renderedFields.length > 0 ? renderedFields : } ); } interface TreeViewFirstLevelGroupFieldProps { group: Group; groupId: string; fieldCheckMap: PickerFirstLevelGroupFieldValue; onChange: (newValue: PickerFirstLevelGroupFieldValue) => void; allCustomTypes: CustomType[]; } function TreeViewFirstLevelGroupField( props: TreeViewFirstLevelGroupFieldProps, ) { const { group, groupId, fieldCheckMap: groupFieldsCheckMap, onChange: onGroupFieldChange, allCustomTypes, } = props; const renderedFields = getGroupFields(group).map(({ fieldId, field }) => { // Content relationship field with custom types if (isContentRelationshipFieldWithSingleCustomtype(field, allCustomTypes)) { const onContentRelationshipFieldChange = ( newCrFields: PickerContentRelationshipFieldValue, ) => { onGroupFieldChange({ ...groupFieldsCheckMap, [fieldId]: { type: "contentRelationship", value: newCrFields, }, }); }; const crFieldCheckMap = groupFieldsCheckMap[fieldId] ?? {}; return ( ); } // Regular field const onCheckedChange = (newChecked: boolean) => { onGroupFieldChange({ ...groupFieldsCheckMap, [fieldId]: { type: "checkbox", value: newChecked }, }); }; return ( ); }); return ( {renderedFields.length > 0 ? renderedFields : } ); } function getPickedFieldsLabel(count: number, suffix = "selected") { if (count === 0) return undefined; return `(${count} ${pluralize(count, "field", "fields")} ${suffix})`; } function getTypeFormatLabel(format: CustomType["format"]) { return format === "page" ? "Page type" : "Custom type"; } /** Retrieves all existing page & custom types. */ function useCustomTypes(linkCustomtypes: LinkCustomtypes | undefined): { /** Every existing custom type, used to discover nested custom types down the tree and the add type dropdown. */ allCustomTypes: CustomType[]; /** The custom types that are already picked. */ pickedCustomTypes: CustomType[]; } { const { customTypes: allCustomTypes } = useCustomTypesRequest(); useEffect(() => { void revalidateGetCustomTypes(); }, []); if (!linkCustomtypes) { return { allCustomTypes, pickedCustomTypes: [], }; } const pickedCustomTypes = linkCustomtypes.flatMap( (pickedCt) => allCustomTypes.find((ct) => ct.id === getId(pickedCt)) ?? [], ); return { allCustomTypes, pickedCustomTypes, }; } function resolveContentRelationshipCustomTypes( linkCustomtypes: LinkCustomtypes | undefined, allCustomTypes: CustomType[], ): CustomType[] { if (!linkCustomtypes) return []; return linkCustomtypes.flatMap((linkCustomtype) => { return allCustomTypes.find((ct) => ct.id === getId(linkCustomtype)) ?? []; }); } /** * Converts a Link config `customtypes` ({@link LinkCustomtypes}) structure into * picker fields check map ({@link PickerCustomTypes}). */ export function convertLinkCustomtypesToFieldCheckMap(args: { linkCustomtypes: LinkCustomtypes; allCustomTypes?: CustomType[]; }): PickerCustomTypes { const { linkCustomtypes, allCustomTypes } = args; // If allCustomTypes is undefined, avoid checking if the fields exist. const shouldValidate = allCustomTypes !== undefined; const checkMap = linkCustomtypes.reduce( (customTypes, customType) => { if (typeof customType === "string") return customTypes; let ctFlatFieldMap: Record = {}; if (shouldValidate) { const existingCt = allCustomTypes.find((c) => c.id === customType.id); // Exit early if the custom type doesn't exist if (!existingCt) return customTypes; ctFlatFieldMap = getCustomTypeStaticFieldsMap(existingCt); } const customTypeFields = customType.fields.reduce( (fields, field) => { // Check if the field exists (only if validating) const existingField = ctFlatFieldMap[getId(field)]; if (shouldValidate && existingField === undefined) return fields; // Regular field if (typeof field === "string") { // Check if the field matched the existing one in the custom type (only if validating) if ( shouldValidate && existingField !== undefined && existingField.type === "Group" ) { return fields; } fields[field] = { type: "checkbox", value: true }; return fields; } // Group field if ("fields" in field && field.fields !== undefined) { // Check if the field matched the existing one in the custom type (only if validating) if ( shouldValidate && existingField !== undefined && existingField.type !== "Group" ) { return fields; } const groupFieldCheckMap = createGroupFieldCheckMap({ group: field, allCustomTypes, ctFlatFieldMap, }); if (groupFieldCheckMap) { fields[field.id] = groupFieldCheckMap; } return fields; } // Content relationship field if ("customtypes" in field && field.customtypes !== undefined) { // Check if the field matched the existing one in the custom type (only if validating) if ( shouldValidate && existingField !== undefined && !isContentRelationshipField(existingField) ) { return fields; } const crFieldCheckMap = createContentRelationshipFieldCheckMap({ field, allCustomTypes, }); if (crFieldCheckMap) { fields[field.id] = crFieldCheckMap; } return fields; } return fields; }, {}, ); if (Object.keys(customTypeFields).length > 0) { customTypes[customType.id] = customTypeFields; } return customTypes; }, {}, ); return checkMap; } function createGroupFieldCheckMap(args: { group: LinkCustomtypesGroupFieldValue; allCustomTypes?: CustomType[]; ctFlatFieldMap: Record; }): PickerFirstLevelGroupField | undefined { const { group, ctFlatFieldMap, allCustomTypes } = args; // If allCustomTypes is undefined, avoid checking if the fields exist. const shouldValidate = allCustomTypes !== undefined; const fieldEntries = group.fields.reduce( (fields, field) => { // Check if the field exists (only if validating) const existingField = getGroupFieldFromMap( ctFlatFieldMap, group.id, getId(field), ); if (shouldValidate && !existingField) return fields; // Regular field if (typeof field === "string") { // Check if the field matched the existing one in the custom type (only if validating) if ( shouldValidate && existingField !== undefined && existingField.type === "Group" ) { return fields; } fields[field] = { type: "checkbox", value: true }; return fields; } // Content relationship field if ("customtypes" in field && field.customtypes !== undefined) { // Check if the field matched the existing one in the custom type (only if validating) if ( shouldValidate && existingField !== undefined && !isContentRelationshipField(existingField) ) { return fields; } const crFieldCheckMap = createContentRelationshipFieldCheckMap({ field, allCustomTypes, }); if (crFieldCheckMap) { fields[field.id] = crFieldCheckMap; } return fields; } return fields; }, {}, ); if (Object.keys(fieldEntries).length === 0) return undefined; return { type: "group", value: fieldEntries, }; } function createContentRelationshipFieldCheckMap(args: { field: LinkCustomtypesContentRelationshipFieldValue; allCustomTypes?: CustomType[]; }): PickerContentRelationshipField | undefined { const { field, allCustomTypes } = args; // If allCustomTypes is undefined, avoid checking if the fields exists. const shouldValidate = allCustomTypes !== undefined; const fieldEntries = field.customtypes.reduce( (customTypes, customType) => { if (typeof customType === "string") return customTypes; let ctFlatFieldMap: Record = {}; if (shouldValidate) { const existingCt = allCustomTypes.find((c) => c.id === customType.id); // Exit early if the custom type doesn't exist if (!existingCt) return customTypes; ctFlatFieldMap = getCustomTypeStaticFieldsMap(existingCt); } const ctFields = customType.fields.reduce( (nestedFields, nestedField) => { // Regular field if (typeof nestedField === "string") { const existingField = ctFlatFieldMap[nestedField]; // Check if the field matched the existing one in the custom type (only if validating) if ( shouldValidate && (existingField === undefined || existingField.type === "Group") ) { return nestedFields; } nestedFields[nestedField] = { type: "checkbox", value: true }; return nestedFields; } if ("fields" in nestedField && nestedField.fields !== undefined) { // Group field const groupFields = nestedField.fields.reduce( (groupFields, groupField) => { const existingField = getGroupFieldFromMap( ctFlatFieldMap, nestedField.id, groupField, ); // Check if the field matched the existing one in the custom type (only if validating) if ( shouldValidate && (existingField === undefined || existingField.type === "Group") ) { return groupFields; } groupFields[groupField] = { type: "checkbox", value: true }; return groupFields; }, {}, ); if (Object.keys(groupFields).length > 0) { nestedFields[nestedField.id] = { type: "group", value: groupFields, }; } } return nestedFields; }, {}, ); if (Object.keys(ctFields).length > 0) { customTypes[customType.id] = ctFields; } return customTypes; }, {}, ); if (Object.keys(fieldEntries).length === 0) return undefined; return { type: "contentRelationship", value: fieldEntries, }; } /** * Merges the existing Link `customtypes` array with the picker state, ensuring * that conversions from to string (custom type id) to object and vice versa are * made correctly and that the order is preserved. */ function mergeAndConvertCheckMapToLinkCustomtypes(args: { linkCustomtypes: LinkCustomtypes | undefined; fieldCheckMap: PickerCustomTypes; newCustomType: PickerCustomType; customTypeId: string; }): LinkCustomtypes { const { linkCustomtypes, fieldCheckMap, newCustomType, customTypeId } = args; const result: NonReadonly = []; const pickerLinkCustomtypes = convertFieldCheckMapToLinkCustomtypes({ ...fieldCheckMap, [customTypeId]: newCustomType, }); if (!linkCustomtypes) return pickerLinkCustomtypes; for (const existingLinkCt of linkCustomtypes) { const existingPickerLinkCt = pickerLinkCustomtypes.find((ct) => { return getId(ct) === getId(existingLinkCt); }); if (existingPickerLinkCt !== undefined) { // Custom type with exposed fields, keep the customtypes object result.push(existingPickerLinkCt); } else if (getId(existingLinkCt) === customTypeId) { // Custom type that had exposed fields, but now has none, change to string result.push(getId(existingLinkCt)); } else { // Custom type without exposed fields, keep the string result.push(existingLinkCt); } } return result; } /** * Converts a picker fields check map structure ({@link PickerCustomTypes}) into * Link config `customtypes` ({@link LinkCustomtypes}) and filter out empty Custom * types. */ function convertFieldCheckMapToLinkCustomtypes( checkMap: PickerCustomTypes, ): LinkCustomtypes { return Object.entries(checkMap).flatMap( ([ctId, ctFields]) => { const fields = Object.entries(ctFields).flatMap< | string | LinkCustomtypesContentRelationshipFieldValue | LinkCustomtypesGroupFieldValue >(([fieldId, fieldValue]) => { // First level group field if (fieldValue.type === "group") { const fields = Object.entries(fieldValue.value).flatMap< string | LinkCustomtypesContentRelationshipFieldValue >(([fieldId, fieldValue]) => { if (fieldValue.type === "checkbox") { return fieldValue.value ? fieldId : []; } const customTypes = createContentRelationshipLinkCustomtypes( fieldValue.value, ); return customTypes.length > 0 ? { id: fieldId, customtypes: customTypes } : []; }); return fields.length > 0 ? { id: fieldId, fields } : []; } // Content relationship field if (fieldValue.type === "contentRelationship") { const customTypes = createContentRelationshipLinkCustomtypes( fieldValue.value, ); return customTypes.length > 0 ? { id: fieldId, customtypes: customTypes } : []; } // Regular field return fieldValue.value ? fieldId : []; }); return fields.length > 0 ? { id: ctId, fields } : []; }, ); } function createContentRelationshipLinkCustomtypes( value: PickerContentRelationshipFieldValue, ): LinkCustomtypesContentRelationshipFieldValue["customtypes"] { return Object.entries(value).flatMap( ([nestedCustomTypeId, nestedCustomTypeFields]) => { const fields = Object.entries(nestedCustomTypeFields).flatMap( ([nestedFieldId, nestedFieldValue]) => { // Leaf group field if (nestedFieldValue.type === "group") { const nestedGroupFields = Object.entries( nestedFieldValue.value, ).flatMap(([fieldId, fieldValue]) => { // Regular field return fieldValue.type === "checkbox" && fieldValue.value ? fieldId : []; }); return nestedGroupFields.length > 0 ? { id: nestedFieldId, fields: nestedGroupFields } : []; } return nestedFieldValue.value ? nestedFieldId : []; }, ); return fields.length > 0 ? { id: nestedCustomTypeId, fields } : []; }, ); } type CountPickedFieldsResult = { pickedFields: number; nestedPickedFields: number; }; /** * Generic recursive function that goes down the fields check map and counts all * the properties that are set to true, which correspond to selected fields. * * Distinguishes between all picked fields and nested picked fields within a * content relationship field. * * It's not type safe, but checks the type of the values at runtime so that * it only recurses into valid objects, and only counts checkbox fields. */ export function countPickedFields( fields: Record | undefined, isNested = false, ): CountPickedFieldsResult { if (!fields) return { pickedFields: 0, nestedPickedFields: 0 }; return Object.values(fields).reduce( (result, value) => { if (!isValidObject(value)) return result; if ("type" in value && value.type === "checkbox") { const isChecked = Boolean(value.value); if (!isChecked) return result; return { pickedFields: result.pickedFields + 1, nestedPickedFields: result.nestedPickedFields + (isNested ? 1 : 0), }; } if ("type" in value && value.type === "contentRelationship") { const { pickedFields, nestedPickedFields } = countPickedFields( value, true, ); return { pickedFields: result.pickedFields + pickedFields, nestedPickedFields: result.nestedPickedFields + nestedPickedFields, }; } const { pickedFields, nestedPickedFields } = countPickedFields( value, isNested, ); return { pickedFields: result.pickedFields + pickedFields, nestedPickedFields: result.nestedPickedFields + nestedPickedFields, }; }, { pickedFields: 0, nestedPickedFields: 0, }, ); } function isContentRelationshipField(field: DynamicWidget): field is Link { return field.type === "Link" && field.config?.select === "document"; } /** * Check if the field is a Content Relationship Link with a **single** custom * type. CRs with multiple custom types are not currently supported (legacy). */ function isContentRelationshipFieldWithSingleCustomtype( field: NestableWidget | Group, allCustomTypes: CustomType[], ): field is Link { return ( isContentRelationshipField(field) && resolveContentRelationshipCustomTypes( field.config?.customtypes, allCustomTypes, ).length === 1 ); } /** * Flattens all custom type tabs and fields into an array of [fieldId, field] tuples. * Also filters out invalid fields. */ function getCustomTypeStaticFields( customType: CustomType, ): [fieldId: string, field: NestableWidget | Group][] { return Object.values(customType.json).flatMap((tabFields) => { return Object.entries(tabFields).flatMap<[string, NestableWidget | Group]>( ([fieldId, field]) => { return isValidField(fieldId, field) ? [[fieldId, field]] : []; }, ); }); } /** * Flattens all custom type tabs and fields into a map of field ids to fields. * Also filters out invalid fields. */ function getCustomTypeStaticFieldsMap( customType: CustomType, ): Record { return Object.fromEntries(getCustomTypeStaticFields(customType)); } function getGroupFieldFromMap( flattenFields: Record, groupId: string, fieldId: string, ) { const group = flattenFields[groupId]; if (group === undefined || group.type !== "Group") return undefined; return group.config?.fields?.[fieldId]; } function isValidField( fieldId: string, field: DynamicWidget, ): field is NestableWidget | Group { return ( field.type !== "Slices" && field.type !== "Choice" && // We don't display uid fields because they're a special field returned by // the API and they're not included in the document data object. // We also filter by key "uid", because (as of the time of writing this) // creating any field with that API id will result in it being used for // metadata, regardless of its type. field.type !== "UID" && fieldId !== "uid" ); } function getGroupFields(group: Group) { if (!group.config?.fields) return []; return Object.entries(group.config.fields).map(([fieldId, field]) => { return { fieldId, field: field as NestableWidget }; }); } /** If it's a string, return it, otherwise return the `id` property. */ function getId(customType: T): string { if (typeof customType === "string") return customType; return customType.id; }