import { defineComponent, withApiName, registeredComponent, type ComponentDefinition } from './component'; import { CustomObjectPageLayout, type CustomObjectPageLayoutProps } from '../custom-object-page-layout'; import type { CustomObjectFieldSection } from '../custom-object-field-section'; import type { Field } from '../field'; import { manifestObject, manifestField, manifestSection, type ObjectReference, type DefinitionFieldReference, type SectionReference, scopedDefinition, type ObjectScoped, } from './custom-object'; export type PageLayoutOptions = LayoutInput< O, Omit >; export type DefinedPageLayout = ObjectScoped< O, Omit & { setTitleField(field: DefinitionFieldReference): DefinedPageLayout; setDescriptionField(field: DefinitionFieldReference): DefinedPageLayout; removeSection( section: SectionReference | string, options?: | import('../custom-object-page-layout').RemovePageLayoutSectionOptions | import('../custom-object-page-layout').RemoveSystemPageLayoutSectionOptions, ): DefinedPageLayout; } & { [K in Exclude< Mutations, 'setTitleField' | 'setDescriptionField' | 'removeSection' >]: CustomObjectPageLayout[K] extends (...args: infer A) => unknown ? (...args: LayoutInput) => DefinedPageLayout : never; } >; export function definePageLayout( object: O, props: PageLayoutOptions>, ): DefinedPageLayout { const candidates = new Map }>(); // Adapt explicit references and delegate every layout operation to the existing implementation. const adapt = (value: unknown, key?: string): any => { if (key != null && opaque.has(key)) return value; if (isSectionReference(value)) { const sections = candidates.get(value)?.sections ?? new Set(); sections.add(value.getSectionId()); candidates.set(value, { sections }); return manifestSection(object, value as SectionReference); } if ( isFieldReference(value) || (value != null && typeof value === 'object' && 'objectApiName' in value && 'apiName' in value) ) { const field = manifestField(object, value as DefinitionFieldReference); if (registeredComponent(value) != null) candidates.set(value, { field: field.getApiName() }); return key != null && headerFields.has(key) ? field.getApiName() : field; } if (Array.isArray(value)) return value.map((item) => adapt(item)); if (value != null && typeof value === 'object') { const input = value as Record; const result = Object.fromEntries( Object.entries(input).map(([name, item]) => [ name, input['type'] === 'fields_section' && name === 'section' ? manifestSection(object, item as SectionReference) : adapt(item, name), ]), ); if (input['type'] === 'fields_section' && typeof input['section'] === 'object') { const section = input['section'] as CustomObjectFieldSection; const sections = candidates.get(section)?.sections ?? new Set(); sections.add(input['key'] ?? section.getSectionId()); candidates.set(section, { sections }); } return result; } return value; }; const adapted = adapt(props) as CustomObjectPageLayoutProps; if (adapted.tabEdits != null && adapted.tabEdits.tabsOrder === undefined) { adapted.tabEdits.tabsOrder = [ ...new Set([ ...(adapted.tabEdits.newTabs ?? []).map((tab) => tab.key), ...Object.entries(adapted.tabEdits.systemTabEdits ?? {}) .filter(([, tab]) => !tab.deleted) .map(([key]) => key), ]), ]; } const layout = withApiName( new CustomObjectPageLayout(manifestObject(object), { ...adapted, layoutId: props.apiName, }), props.apiName, ); for (const name of Object.getOwnPropertyNames(CustomObjectPageLayout.prototype)) { if (!/^(add|remove|set)[A-Z]/.test(name)) continue; const method = (layout as any)[name].bind(layout); (layout as any)[name] = (...args: unknown[]) => { if (/^(add|remove)Fields?$/.test(name)) { const options = args[1]; if ( options != null && typeof options === 'object' && 'section' in options && typeof options.section === 'string' ) { args[1] = { ...options, section: manifestSection(object, options.section as SectionReference), }; } } method(...args.map((arg) => adapt(arg))); return layout; }; } // Only currently serialized structural references count; removed fields/sections do not become roots. dependencies.set(layout, () => { const config = layout.toDict(); const fields = new Set(); const sections = new Set(); const header = config['header_edits'] ?? {}; for (const name of [ header.newTitleField, header.newDescriptionField, ...(header.newFields ?? []).map((f: any) => f.rqlName), ...Object.values(header.systemFieldEdits ?? {}).map((f: any) => f.newRqlField), ]) if (typeof name === 'string') fields.add(name); const collectSection = (section: any) => { if (section.type !== 'fields_section') return; sections.add(section.key); for (const field of section.fields ?? []) fields.add(field.fieldRqlName); }; const tabs = config['tab_edits'] ?? {}; for (const tab of tabs.newTabs ?? []) for (const section of tab.sections ?? []) collectSection(section); for (const tab of Object.values(tabs.systemTabEdits ?? {}) as any[]) { if (tab.deleted) continue; for (const section of tab.newSections ?? []) collectSection(section); for (const [key, section] of Object.entries(tab.systemSectionEdits ?? {}) as [string, any][]) { if (section.deleted) continue; sections.add(key); for (const field of section.newFields ?? []) fields.add(field.fieldRqlName); } } return [...candidates] .filter(([, ref]) => ref.field != null ? fields.has(ref.field) : [...(ref.sections ?? [])].some((key) => sections.has(key)), ) .map(([definition]) => definition); }); return pageLayoutComponent.register( scopedDefinition(object, layout) as unknown as DefinedPageLayout, object, props, ); } function isFieldReference(value: unknown): value is Field { return ( value != null && typeof value === 'object' && 'getApiName' in value && typeof value.getApiName === 'function' && 'getCustomObjectApiName' in value && typeof value.getCustomObjectApiName === 'function' ); } function isSectionReference(value: unknown): value is CustomObjectFieldSection { return ( value != null && typeof value === 'object' && 'getSectionId' in value && typeof value.getSectionId === 'function' && 'getModelApiName' in value && typeof value.getModelApiName === 'function' ); } type LayoutInput = T extends CustomObjectFieldSection ? SectionReference : T extends Field ? DefinitionFieldReference : T extends readonly unknown[] ? { [K in keyof T]: LayoutInput } : T extends object ? { [K in keyof T]: K extends 'defaultValue' | 'visibilityConditions' | 'inputMapping' ? T[K] : K extends 'newRqlField' | 'newTitleField' | 'newDescriptionField' | 'rqlName' ? T[K] | DefinitionFieldReference : LayoutInput; } : T; type Mutations = Extract; const dependencies = new WeakMap object[]>(); const opaque = new Set(['defaultValue', 'visibilityConditions', 'inputMapping']); const headerFields = new Set(['newRqlField', 'newTitleField', 'newDescriptionField', 'rqlName']); const pageLayoutComponent: ComponentDefinition = defineComponent({ type: 'CUSTOM_OBJECT_PAGE_LAYOUT', define: definePageLayout, references: (definition, object) => () => [ object, ...(dependencies.get(definition)?.() ?? []).filter((reference) => registeredComponent(reference) != null), ], });