@rippling/rippling-sdk 0.2.0-alpha.44 → 0.2.0-alpha.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/examples/manifest/dental-practice-management/dental-practice-management.ts +0 -7
  2. package/examples/manifest/dental-practice-management/manifest.json +0 -7
  3. package/examples/manifest/existing-manifest-mutations/existing-manifest-mutations.ts +6 -7
  4. package/examples/manifest/gym-membership-management/gym-membership-management.ts +0 -7
  5. package/examples/manifest/gym-membership-management/manifest.json +0 -7
  6. package/lib/manifest/custom-object-field-section.d.mts +2 -2
  7. package/lib/manifest/custom-object-field-section.d.ts +2 -2
  8. package/lib/manifest/custom-object-field-section.js +2 -2
  9. package/lib/manifest/custom-object-field-section.mjs +2 -2
  10. package/lib/manifest/custom-object-page-layout.d.mts +78 -37
  11. package/lib/manifest/custom-object-page-layout.d.mts.map +1 -1
  12. package/lib/manifest/custom-object-page-layout.d.ts +78 -37
  13. package/lib/manifest/custom-object-page-layout.d.ts.map +1 -1
  14. package/lib/manifest/custom-object-page-layout.js +263 -66
  15. package/lib/manifest/custom-object-page-layout.js.map +1 -1
  16. package/lib/manifest/custom-object-page-layout.mjs +263 -66
  17. package/lib/manifest/custom-object-page-layout.mjs.map +1 -1
  18. package/lib/manifest/custom-object.d.mts +6 -0
  19. package/lib/manifest/custom-object.d.mts.map +1 -1
  20. package/lib/manifest/custom-object.d.ts +6 -0
  21. package/lib/manifest/custom-object.d.ts.map +1 -1
  22. package/lib/manifest/custom-object.js +6 -0
  23. package/lib/manifest/custom-object.js.map +1 -1
  24. package/lib/manifest/custom-object.mjs +6 -0
  25. package/lib/manifest/custom-object.mjs.map +1 -1
  26. package/lib/manifest/existing-manifest-context.d.mts +0 -1
  27. package/lib/manifest/existing-manifest-context.d.mts.map +1 -1
  28. package/lib/manifest/existing-manifest-context.d.ts +0 -1
  29. package/lib/manifest/existing-manifest-context.d.ts.map +1 -1
  30. package/lib/manifest/existing-manifest-context.js +5 -10
  31. package/lib/manifest/existing-manifest-context.js.map +1 -1
  32. package/lib/manifest/existing-manifest-context.mjs +5 -10
  33. package/lib/manifest/existing-manifest-context.mjs.map +1 -1
  34. package/lib/manifest/field.d.mts +6 -3
  35. package/lib/manifest/field.d.mts.map +1 -1
  36. package/lib/manifest/field.d.ts +6 -3
  37. package/lib/manifest/field.d.ts.map +1 -1
  38. package/lib/manifest/field.js +26 -6
  39. package/lib/manifest/field.js.map +1 -1
  40. package/lib/manifest/field.mjs +26 -6
  41. package/lib/manifest/field.mjs.map +1 -1
  42. package/package.json +1 -1
  43. package/src/lib/manifest/custom-object-field-section.ts +2 -2
  44. package/src/lib/manifest/custom-object-page-layout.ts +477 -93
  45. package/src/lib/manifest/custom-object.ts +6 -0
  46. package/src/lib/manifest/existing-manifest-context.ts +5 -12
  47. package/src/lib/manifest/field.ts +45 -9
  48. package/src/version.ts +1 -1
  49. package/version.d.mts +1 -1
  50. package/version.d.ts +1 -1
  51. package/version.js +1 -1
  52. package/version.mjs +1 -1
@@ -98,8 +98,6 @@ export interface FieldsSection {
98
98
  type: 'fields_section';
99
99
  /** Field section metadata for the fields displayed in this layout section. Required. */
100
100
  section: CustomObjectFieldSection;
101
- /** Stable layout key for this section. Optional. Defaults to the field section id. */
102
- key?: string;
103
101
  /** Display name of the layout section. Optional. Defaults to the field section name. */
104
102
  name?: string;
105
103
  /** Fields to display in this section. Required. */
@@ -227,53 +225,93 @@ export interface VisibilityConditions {
227
225
  /** Where to insert a new layout item. Defaults to `'end'`. */
228
226
  export type PageLayoutMutationPosition = 'start' | 'end' | number;
229
227
 
230
- /** Options for adding a field to an existing layout section. */
231
- export interface AddPageLayoutFieldOptions extends SectionFieldOptions {
232
- /**
233
- * Tab key containing the target section. Optional when `sectionKey` is unique
234
- * across the layout.
235
- */
236
- tabKey?: string;
237
- /** Section key to add the field to. Required. */
238
- sectionKey: string;
239
- /**
240
- * Field-section metadata to assign to the field component.
241
- *
242
- * Existing layouts often use layout section keys that differ from field section
243
- * ids. Pass this when the field was not constructed with a `section`.
244
- */
245
- fieldSection?: CustomObjectFieldSection;
228
+ interface AddPageLayoutFieldOptionsBase extends SectionFieldOptions {
246
229
  /** Insert position inside the section field list. Optional. @default `'end'` */
247
230
  position?: PageLayoutMutationPosition;
248
231
  /** Allow the same field to appear twice in the same section. Optional. @default false */
249
232
  allowDuplicate?: boolean;
233
+ }
234
+
235
+ /** Options for adding a field to a typed custom-object field section. */
236
+ export interface AddCustomPageLayoutFieldOptions extends AddPageLayoutFieldOptionsBase {
237
+ /**
238
+ * Tab key containing the target section. Optional when `section` is unique
239
+ * across the layout.
240
+ */
241
+ tabKey?: string;
242
+ /** Field section to add the field to. Required for custom layout sections. */
243
+ section: CustomObjectFieldSection;
244
+ /** Raw section keys are only accepted for built-in system sections. */
245
+ sectionKey?: never;
246
+ /** Deprecated. Use `section`. */
247
+ fieldSection?: never;
248
+ /** Custom layout sections are selected by `section`; `sectionKey` is only for system sections. */
249
+ systemSection?: false;
250
+ }
251
+
252
+ /** Options for adding a field to a built-in system section. */
253
+ export interface AddSystemPageLayoutFieldOptions extends AddPageLayoutFieldOptionsBase {
250
254
  /**
251
255
  * Add to a built-in system section via `systemTabEdits`.
252
256
  *
253
- * When true, `tabKey` is required and the edit is created if it does not
254
- * already exist.
257
+ * When true, `tabKey` and `sectionKey` are required and the edit is created
258
+ * if it does not already exist.
255
259
  */
256
- systemSection?: boolean;
260
+ systemSection: true;
261
+ /** System tab key containing the target section. Required. */
262
+ tabKey: string;
263
+ /** Built-in system section key. Required. */
264
+ sectionKey: string;
265
+ /** Custom field-section objects are only accepted for custom layout sections. */
266
+ section?: never;
267
+ /** Deprecated. Use `section` for custom sections, or `sectionKey` for system sections. */
268
+ fieldSection?: never;
257
269
  }
258
270
 
271
+ /** Options for adding a field to an existing layout section. */
272
+ export type AddPageLayoutFieldOptions = AddCustomPageLayoutFieldOptions | AddSystemPageLayoutFieldOptions;
273
+
259
274
  /** Options for adding multiple fields to an existing layout section. */
260
275
  export type AddPageLayoutFieldsOptions = AddPageLayoutFieldOptions;
261
276
 
262
277
  /** Options for removing fields from a layout. */
263
- export interface RemovePageLayoutFieldOptions {
264
- /** Limit removal to one tab. Optional. */
265
- tabKey?: string;
266
- /** Limit removal to one section. Optional. */
267
- sectionKey?: string;
278
+ interface RemovePageLayoutFieldOptionsBase {
268
279
  /**
269
280
  * Remove all matching occurrences. When false, removing a field that appears
270
281
  * more than once throws. Optional. @default true
271
282
  */
272
283
  removeAll?: boolean;
273
- /** Remove from a built-in system section edit. Optional. */
274
- systemSection?: boolean;
275
284
  }
276
285
 
286
+ /** Options for removing fields from typed custom-object field sections. */
287
+ export interface RemoveCustomPageLayoutFieldOptions extends RemovePageLayoutFieldOptionsBase {
288
+ /** Limit removal to one tab. Optional. */
289
+ tabKey?: string;
290
+ /** Limit removal to one field section. Optional. */
291
+ section?: CustomObjectFieldSection;
292
+ /** Raw section keys are only accepted for built-in system sections. */
293
+ sectionKey?: never;
294
+ /** Custom layout sections are selected by `section`; `sectionKey` is only for system sections. */
295
+ systemSection?: false;
296
+ }
297
+
298
+ /** Options for removing fields from built-in system section edits. */
299
+ export interface RemoveSystemPageLayoutFieldOptions extends RemovePageLayoutFieldOptionsBase {
300
+ /** Remove from a built-in system section edit. Required. */
301
+ systemSection: true;
302
+ /** System tab key containing the target section. Required. */
303
+ tabKey: string;
304
+ /** Built-in system section key. Required. */
305
+ sectionKey: string;
306
+ /** Custom field-section objects are only accepted for custom layout sections. */
307
+ section?: never;
308
+ }
309
+
310
+ /** Options for removing fields from a layout. */
311
+ export type RemovePageLayoutFieldOptions =
312
+ | RemoveCustomPageLayoutFieldOptions
313
+ | RemoveSystemPageLayoutFieldOptions;
314
+
277
315
  /** Options for adding a section to an existing tab. */
278
316
  export interface AddPageLayoutSectionOptions {
279
317
  /** Tab key to add the section to. Required. */
@@ -299,8 +337,16 @@ export interface RemovePageLayoutSectionOptions {
299
337
  * more than once throws. Optional. @default true
300
338
  */
301
339
  removeAll?: boolean;
302
- /** Delete a built-in system section via `systemSectionEdits`. Requires `tabKey`. */
303
- systemSection?: boolean;
340
+ /** Custom layout sections are selected by `CustomObjectFieldSection`; raw keys are only for system sections. */
341
+ systemSection?: false;
342
+ }
343
+
344
+ /** Options for removing a built-in system section from a layout. */
345
+ export interface RemoveSystemPageLayoutSectionOptions {
346
+ /** System tab key containing the target section. Required. */
347
+ tabKey: string;
348
+ /** Delete a built-in system section via `systemSectionEdits`. Required. */
349
+ systemSection: true;
304
350
  }
305
351
 
306
352
  /** Options for adding a tab to a layout. */
@@ -409,7 +455,6 @@ interface SerializedSectionField extends SectionFieldOptions {
409
455
 
410
456
  interface SerializedFieldsSection {
411
457
  type: 'fields_section';
412
- key: string;
413
458
  name: string;
414
459
  fields: SerializedSectionField[];
415
460
  layout?: SectionFieldsLayout;
@@ -459,6 +504,44 @@ function isFieldRef(value: SectionField): value is SectionFieldRef {
459
504
  return candidate != null && typeof candidate === 'object' && isField(candidate.field);
460
505
  }
461
506
 
507
+ const FIELD_SECTION_ID = Symbol('CustomObjectPageLayout.fieldSectionId');
508
+
509
+ function isCustomObjectFieldSection(value: unknown): value is CustomObjectFieldSection {
510
+ return value instanceof CustomObjectFieldSection;
511
+ }
512
+
513
+ function attachFieldSectionId(section: SerializedFieldsSection, sectionId: string): SerializedFieldsSection {
514
+ Object.defineProperty(section, FIELD_SECTION_ID, {
515
+ value: sectionId,
516
+ enumerable: false,
517
+ configurable: true,
518
+ });
519
+ return section;
520
+ }
521
+
522
+ function layoutSectionKey(section: SerializedFieldsSection): string | undefined {
523
+ return (section as unknown as Record<typeof FIELD_SECTION_ID, string | undefined>)[FIELD_SECTION_ID];
524
+ }
525
+
526
+ function layoutSectionMatches(section: SerializedFieldsSection, sectionKey: string): boolean {
527
+ return layoutSectionKey(section) === sectionKey;
528
+ }
529
+
530
+ function inputFieldsSectionHasKey(input: PageLayoutSection): boolean {
531
+ const record = input as unknown as Record<string, unknown>;
532
+ return Object.prototype.hasOwnProperty.call(record, 'key') && record['key'] != null;
533
+ }
534
+
535
+ function assertNoSerializedSectionKey(section: SerializedFieldsSection): void {
536
+ const record = section as unknown as Record<string, unknown>;
537
+ if (Object.prototype.hasOwnProperty.call(record, 'key') && record['key'] != null) {
538
+ throw new Error(
539
+ 'CustomObjectPageLayout loaded fields_section key is not supported. ' +
540
+ 'Remove key and target the section with CustomObjectFieldSection metadata.',
541
+ );
542
+ }
543
+ }
544
+
462
545
  function assertSameObject(customObject: CustomObject, field: Field, sectionKey: string): void {
463
546
  if (field.getCustomObjectApiName() !== customObject.getApiName()) {
464
547
  throw new Error(
@@ -477,6 +560,22 @@ function assertSectionBelongsTo(customObject: CustomObject, section: CustomObjec
477
560
  }
478
561
  }
479
562
 
563
+ function requireFieldSectionOption(value: unknown, label: string): CustomObjectFieldSection {
564
+ if (isCustomObjectFieldSection(value)) return value;
565
+ if (typeof value === 'string') {
566
+ throw new TypeError(
567
+ `${label} must be a CustomObjectFieldSection object; received raw section id "${value}". ` +
568
+ 'Load/include the existing field section first.',
569
+ );
570
+ }
571
+ throw new TypeError(`${label} must be a CustomObjectFieldSection object.`);
572
+ }
573
+
574
+ function assertOptionAbsent(options: unknown, key: string, message: string): void {
575
+ if (options == null || typeof options !== 'object') return;
576
+ if (Object.prototype.hasOwnProperty.call(options, key)) throw new TypeError(message);
577
+ }
578
+
480
579
  function toSerializedSectionField(
481
580
  fieldRqlName: string,
482
581
  options: SectionFieldOptions = {},
@@ -517,16 +616,18 @@ function normalizeFieldsSection(
517
616
  input: PageLayoutSection,
518
617
  ): SerializedFieldsSection {
519
618
  assertSectionBelongsTo(customObject, input.section);
520
- const key = input.key ?? input.section.getSectionId();
619
+ const sectionId = input.section.getSectionId();
620
+ if (inputFieldsSectionHasKey(input)) {
621
+ throw new Error('CustomObjectPageLayout fields_section key is not supported. Omit key.');
622
+ }
521
623
  const name = input.name ?? input.section.getName();
522
624
  const normalized: SerializedFieldsSection = {
523
- key,
524
625
  name,
525
626
  type: 'fields_section',
526
- fields: input.fields.map((field) => normalizeSectionField(customObject, input.section, key, field)),
627
+ fields: input.fields.map((field) => normalizeSectionField(customObject, input.section, sectionId, field)),
527
628
  };
528
629
  if (input.layout != null) normalized.layout = input.layout;
529
- return normalized;
630
+ return attachFieldSectionId(normalized, sectionId);
530
631
  }
531
632
 
532
633
  function normalizeTab(customObject: CustomObject, tab: PageLayoutTab): SerializedPageLayoutTab {
@@ -599,11 +700,69 @@ function cloneJson<T>(value: T): T {
599
700
  return JSON.parse(JSON.stringify(value)) as T;
600
701
  }
601
702
 
703
+ const HEADER_EDIT_KEYS = new Set([
704
+ 'newTitleField',
705
+ 'titleFieldDeleted',
706
+ 'newDescriptionField',
707
+ 'descriptionFieldDeleted',
708
+ 'systemFieldEdits',
709
+ 'newFields',
710
+ 'headerFieldsOrder',
711
+ 'buttons',
712
+ ]);
713
+
714
+ const SYSTEM_HEADER_FIELD_EDIT_KEYS = new Set(['newRqlField', 'deleted']);
715
+
716
+ const NEW_HEADER_FIELD_KEYS = new Set(['rqlName', 'key', 'canBeDeleted', 'canBeChanged', 'canBeMoved']);
717
+
718
+ function assertKnownKeys(
719
+ record: Record<string, unknown>,
720
+ allowedKeys: ReadonlySet<string>,
721
+ label: string,
722
+ hint: string,
723
+ ): void {
724
+ for (const key of Object.keys(record)) {
725
+ if (!allowedKeys.has(key)) {
726
+ throw new Error(`${label} contains unsupported key "${key}". ${hint}`);
727
+ }
728
+ }
729
+ }
730
+
731
+ function normalizeHeaderEdits(headerEdits: HeaderEdits): HeaderEdits {
732
+ assertKnownKeys(
733
+ headerEdits as unknown as Record<string, unknown>,
734
+ HEADER_EDIT_KEYS,
735
+ 'CustomObjectPageLayout headerEdits',
736
+ 'Use addHeaderField(), setTitleField(), setDescriptionField(), or the HeaderEdits keys documented by the SDK.',
737
+ );
738
+
739
+ for (const [key, edit] of Object.entries(headerEdits.systemFieldEdits ?? {})) {
740
+ assertKnownKeys(
741
+ edit as Record<string, unknown>,
742
+ SYSTEM_HEADER_FIELD_EDIT_KEYS,
743
+ `CustomObjectPageLayout headerEdits.systemFieldEdits.${key}`,
744
+ 'Use newRqlField and deleted for system header field edits.',
745
+ );
746
+ }
747
+
748
+ for (const [index, field] of (headerEdits.newFields ?? []).entries()) {
749
+ assertKnownKeys(
750
+ field as unknown as Record<string, unknown>,
751
+ NEW_HEADER_FIELD_KEYS,
752
+ `CustomObjectPageLayout headerEdits.newFields[${index}]`,
753
+ 'Use { rqlName, key } entries, or call addHeaderField(field).',
754
+ );
755
+ }
756
+
757
+ return headerEdits;
758
+ }
759
+
602
760
  type MutableSectionField = SerializedSectionField | Record<string, any> | string;
603
761
 
604
762
  interface MutableFieldContainer {
605
763
  tabKey: string;
606
- sectionKey: string;
764
+ sectionKey: string | undefined;
765
+ section?: SerializedFieldsSection;
607
766
  fields: MutableSectionField[];
608
767
  label: string;
609
768
  }
@@ -633,6 +792,61 @@ function fieldRqlNameOf(entry: unknown): string | undefined {
633
792
  return typeof fieldRqlName === 'string' ? fieldRqlName : undefined;
634
793
  }
635
794
 
795
+ function explicitFieldSectionId(entry: unknown): string | undefined {
796
+ if (entry == null || typeof entry !== 'object') return undefined;
797
+ const record = entry as Record<string, any>;
798
+ const fieldSectionId = record['fieldSection'] ?? record['field_section'];
799
+ return typeof fieldSectionId === 'string' ? fieldSectionId : undefined;
800
+ }
801
+
802
+ function inferredSectionIdFromFields(
803
+ section: SerializedFieldsSection,
804
+ resolveFieldSectionId?: (fieldApiName: string) => string | undefined,
805
+ ): string | undefined {
806
+ const sectionIds = new Set<string>();
807
+ for (const field of section.fields ?? []) {
808
+ const explicitSectionId = explicitFieldSectionId(field);
809
+ if (explicitSectionId != null) {
810
+ sectionIds.add(explicitSectionId);
811
+ continue;
812
+ }
813
+
814
+ const fieldApiName = fieldRqlNameOf(field);
815
+ if (fieldApiName == null || resolveFieldSectionId == null) continue;
816
+ const resolvedSectionId = resolveFieldSectionId(fieldApiName);
817
+ if (resolvedSectionId != null) sectionIds.add(resolvedSectionId);
818
+ }
819
+ return sectionIds.size === 1 ? [...sectionIds][0] : undefined;
820
+ }
821
+
822
+ function annotateLoadedFieldsSection(
823
+ section: SerializedFieldsSection,
824
+ resolveFieldSectionId?: (fieldApiName: string) => string | undefined,
825
+ ): SerializedFieldsSection {
826
+ assertNoSerializedSectionKey(section);
827
+ const sectionId = layoutSectionKey(section) ?? inferredSectionIdFromFields(section, resolveFieldSectionId);
828
+ return sectionId == null ? section : attachFieldSectionId(section, sectionId);
829
+ }
830
+
831
+ function annotateLoadedTabEdits(
832
+ tabEdits: SerializedTabEdits,
833
+ resolveFieldSectionId?: (fieldApiName: string) => string | undefined,
834
+ ): SerializedTabEdits {
835
+ for (const tab of tabEdits.newTabs ?? []) {
836
+ if (!isTabWithSections(tab)) continue;
837
+ tab.sections = tab.sections.map((section) => annotateLoadedFieldsSection(section, resolveFieldSectionId));
838
+ }
839
+
840
+ for (const tabEdit of Object.values(tabEdits.systemTabEdits ?? {})) {
841
+ if (tabEdit.newSections != null) {
842
+ tabEdit.newSections = tabEdit.newSections.map((section) =>
843
+ annotateLoadedFieldsSection(section, resolveFieldSectionId),
844
+ );
845
+ }
846
+ }
847
+ return tabEdits;
848
+ }
849
+
636
850
  function removeIndexes<T>(items: T[], indexes: number[]): void {
637
851
  for (const index of [...indexes].sort((a, b) => b - a)) {
638
852
  items.splice(index, 1);
@@ -715,7 +929,6 @@ function ensureArrayProperty<T>(record: Record<string, any>, key: string, label:
715
929
  * newTabs: [{
716
930
  * key: 'member', name: 'Member', type: 'tab_with_sections',
717
931
  * sections: [{
718
- * key: 'profile',
719
932
  * name: 'Profile',
720
933
  * type: 'fields_section',
721
934
  * section: profileSection,
@@ -764,7 +977,7 @@ export class CustomObjectPageLayout {
764
977
  this._customObjectApiName = customObject.getApiName();
765
978
  this._name = props.name;
766
979
  this._tabEdits = normalizeTabEdits(customObject, props.tabEdits ?? {});
767
- this._headerEdits = props.headerEdits ?? {};
980
+ this._headerEdits = normalizeHeaderEdits(props.headerEdits ?? {});
768
981
  this._visibilityConditions = props.visibilityConditions ?? {};
769
982
  this._blueprintKey = props.blueprintKey;
770
983
  customObject._register(this);
@@ -788,6 +1001,7 @@ export class CustomObjectPageLayout {
788
1001
  static _fromExistingComponent(
789
1002
  customObject: CustomObject,
790
1003
  component: Record<string, any>,
1004
+ resolveFieldSectionId?: (fieldApiName: string) => string | undefined,
791
1005
  ): CustomObjectPageLayout {
792
1006
  const layout = Object.create(CustomObjectPageLayout.prototype) as CustomObjectPageLayout;
793
1007
  Object.assign(layout as Record<string, any>, {
@@ -799,7 +1013,7 @@ export class CustomObjectPageLayout {
799
1013
  CustomObjectPageLayout.componentType,
800
1014
  ),
801
1015
  _name: requireStringComponentField(component, 'name', CustomObjectPageLayout.componentType),
802
- _tabEdits: cloneJson(component['tab_edits'] ?? {}),
1016
+ _tabEdits: annotateLoadedTabEdits(cloneJson(component['tab_edits'] ?? {}), resolveFieldSectionId),
803
1017
  _headerEdits: cloneJson(component['header_edits'] ?? {}),
804
1018
  _visibilityConditions: cloneJson(component['visibility_conditions'] ?? {}),
805
1019
  _blueprintKey: component['blueprint_key'],
@@ -842,17 +1056,59 @@ export class CustomObjectPageLayout {
842
1056
  /**
843
1057
  * Adds multiple fields to an existing layout section.
844
1058
  *
845
- * Fields are inserted in the order provided. If a field does not already have
846
- * field-section metadata, pass `options.fieldSection` so the corresponding
847
- * `CUSTOM_OBJECT_FIELD_SECTION` component is registered in the manifest.
1059
+ * Fields are inserted in the order provided. For custom layout sections,
1060
+ * pass `options.section` so TypeScript and runtime validation agree on the
1061
+ * target `CUSTOM_OBJECT_FIELD_SECTION`.
848
1062
  */
849
1063
  addFields(fields: Field[], options: AddPageLayoutFieldsOptions): this {
850
1064
  if (fields.length === 0) return this;
1065
+
1066
+ let fieldSectionForMutation: CustomObjectFieldSection | undefined;
851
1067
  const container =
852
1068
  options.systemSection === true ?
853
- this.ensureSystemSectionFieldContainer(options.tabKey, options.sectionKey)
854
- : this.requireOneFieldContainer(options.tabKey, options.sectionKey);
855
- const entries = fields.map((field) => this.serializeFieldForMutation(field, options.sectionKey, options));
1069
+ (() => {
1070
+ assertOptionAbsent(
1071
+ options,
1072
+ 'section',
1073
+ 'CustomObjectPageLayout addField section is only supported for custom layout sections.',
1074
+ );
1075
+ assertOptionAbsent(
1076
+ options,
1077
+ 'fieldSection',
1078
+ 'CustomObjectPageLayout addField fieldSection is no longer supported. ' +
1079
+ 'Use sectionKey only for systemSection: true.',
1080
+ );
1081
+ return this.ensureSystemSectionFieldContainer(options.tabKey, options.sectionKey);
1082
+ })()
1083
+ : (() => {
1084
+ assertOptionAbsent(
1085
+ options,
1086
+ 'sectionKey',
1087
+ 'CustomObjectPageLayout addField sectionKey is only supported with systemSection: true. ' +
1088
+ 'Pass section: CustomObjectFieldSection for custom layout sections.',
1089
+ );
1090
+ assertOptionAbsent(
1091
+ options,
1092
+ 'fieldSection',
1093
+ 'CustomObjectPageLayout addField fieldSection is no longer supported. ' +
1094
+ 'Pass section: CustomObjectFieldSection instead.',
1095
+ );
1096
+ const section = requireFieldSectionOption(
1097
+ options.section,
1098
+ 'CustomObjectPageLayout addField section',
1099
+ );
1100
+ this.assertFieldSectionBelongsToLayout(section);
1101
+ fieldSectionForMutation = section;
1102
+ return this.requireOneFieldContainer(options.tabKey, section.getSectionId(), section);
1103
+ })();
1104
+ const entries = fields.map((field) =>
1105
+ this.serializeFieldForMutation(
1106
+ field,
1107
+ container.sectionKey ?? container.label,
1108
+ options,
1109
+ fieldSectionForMutation,
1110
+ ),
1111
+ );
856
1112
 
857
1113
  if (options.allowDuplicate !== true) {
858
1114
  const existing = new Set(container.fields.map((entry) => fieldRqlNameOf(entry)).filter(Boolean));
@@ -883,9 +1139,34 @@ export class CustomObjectPageLayout {
883
1139
  const fieldApiName = typeof field === 'string' ? field : field.getApiName();
884
1140
  if (isField(field)) this.assertFieldBelongsToLayout(field);
885
1141
 
1142
+ let sectionKey: string | undefined;
1143
+ if (options.systemSection === true) {
1144
+ assertOptionAbsent(
1145
+ options,
1146
+ 'section',
1147
+ 'CustomObjectPageLayout removeField section is only supported for custom layout sections.',
1148
+ );
1149
+ sectionKey = options.sectionKey;
1150
+ } else {
1151
+ assertOptionAbsent(
1152
+ options,
1153
+ 'sectionKey',
1154
+ 'CustomObjectPageLayout removeField sectionKey is only supported with systemSection: true. ' +
1155
+ 'Pass section: CustomObjectFieldSection for custom layout sections.',
1156
+ );
1157
+ if (options.section != null) {
1158
+ const section = requireFieldSectionOption(
1159
+ options.section,
1160
+ 'CustomObjectPageLayout removeField section',
1161
+ );
1162
+ this.assertFieldSectionBelongsToLayout(section);
1163
+ sectionKey = section.getSectionId();
1164
+ }
1165
+ }
1166
+
886
1167
  const containers = this.findFieldContainers({
887
1168
  tabKey: options.tabKey,
888
- sectionKey: options.sectionKey,
1169
+ sectionKey,
889
1170
  systemSectionOnly: options.systemSection === true,
890
1171
  });
891
1172
  const matches: Array<{ container: MutableFieldContainer; index: number }> = [];
@@ -944,9 +1225,13 @@ export class CustomObjectPageLayout {
944
1225
  'newSections',
945
1226
  `systemTabEdits.${options.tabKey}.newSections`,
946
1227
  );
947
- this.assertSectionKeyAvailable(sections, normalized.key, options.tabKey, options.allowDuplicate);
1228
+ this.assertSectionKeyAvailable(
1229
+ sections,
1230
+ layoutSectionKey(normalized),
1231
+ options.tabKey,
1232
+ options.allowDuplicate,
1233
+ );
948
1234
  insertAt(sections, normalized, options.position);
949
- if (tabEdit.sectionsOrder != null) insertAt(tabEdit.sectionsOrder, normalized.key, options.position);
950
1235
  return this;
951
1236
  }
952
1237
 
@@ -957,7 +1242,12 @@ export class CustomObjectPageLayout {
957
1242
  'Pass systemTab: true to add a section to a built-in system tab.',
958
1243
  );
959
1244
  }
960
- this.assertSectionKeyAvailable(tab.sections, normalized.key, options.tabKey, options.allowDuplicate);
1245
+ this.assertSectionKeyAvailable(
1246
+ tab.sections,
1247
+ layoutSectionKey(normalized),
1248
+ options.tabKey,
1249
+ options.allowDuplicate,
1250
+ );
961
1251
  insertAt(tab.sections, normalized, options.position);
962
1252
  return this;
963
1253
  }
@@ -968,19 +1258,33 @@ export class CustomObjectPageLayout {
968
1258
  * Pass `systemSection: true` with `tabKey` to delete a built-in section via
969
1259
  * `systemSectionEdits[sectionKey].deleted`.
970
1260
  */
971
- removeSection(sectionKey: string, options: RemovePageLayoutSectionOptions = {}): this {
1261
+ removeSection(section: CustomObjectFieldSection, options?: RemovePageLayoutSectionOptions): this;
1262
+ removeSection(sectionKey: string, options: RemoveSystemPageLayoutSectionOptions): this;
1263
+ removeSection(
1264
+ sectionOrKey: CustomObjectFieldSection | string,
1265
+ options: RemovePageLayoutSectionOptions | RemoveSystemPageLayoutSectionOptions = {},
1266
+ ): this {
972
1267
  if (options.systemSection === true) {
1268
+ if (typeof sectionOrKey !== 'string') {
1269
+ throw new TypeError(
1270
+ 'CustomObjectPageLayout removeSection with systemSection: true requires a system section key.',
1271
+ );
1272
+ }
973
1273
  if (options.tabKey == null) {
974
1274
  throw new Error('CustomObjectPageLayout removeSection with systemSection: true requires tabKey.');
975
1275
  }
976
- const sectionEdit = this.ensureSystemSectionEdit(options.tabKey, sectionKey);
1276
+ const sectionEdit = this.ensureSystemSectionEdit(options.tabKey, sectionOrKey);
977
1277
  sectionEdit.deleted = true;
978
- this.removeSectionVisibility(sectionKey);
1278
+ this.removeSectionVisibility(sectionOrKey);
979
1279
  return this;
980
1280
  }
981
1281
 
1282
+ const section = requireFieldSectionOption(sectionOrKey, 'CustomObjectPageLayout removeSection section');
1283
+ this.assertFieldSectionBelongsToLayout(section);
1284
+ const sectionKey = section.getSectionId();
982
1285
  const matches: Array<{
983
1286
  sections: SerializedFieldsSection[];
1287
+ section: SerializedFieldsSection;
984
1288
  index: number;
985
1289
  tabEdit?: SerializedSystemTabEdit;
986
1290
  tabKey: string;
@@ -989,14 +1293,15 @@ export class CustomObjectPageLayout {
989
1293
  if (!isTabWithSections(tab)) continue;
990
1294
  if (options.tabKey != null && tab.key !== options.tabKey) continue;
991
1295
  tab.sections.forEach((section, index) => {
992
- if (section.key === sectionKey) matches.push({ sections: tab.sections, index, tabKey: tab.key });
1296
+ if (layoutSectionMatches(section, sectionKey))
1297
+ matches.push({ sections: tab.sections, section, index, tabKey: tab.key });
993
1298
  });
994
1299
  }
995
1300
  for (const [tabKey, tabEdit] of Object.entries(this._tabEdits.systemTabEdits ?? {})) {
996
1301
  if (options.tabKey != null && tabKey !== options.tabKey) continue;
997
1302
  (tabEdit.newSections ?? []).forEach((section, index) => {
998
- if (section.key === sectionKey)
999
- matches.push({ sections: tabEdit.newSections!, index, tabEdit, tabKey });
1303
+ if (layoutSectionMatches(section, sectionKey))
1304
+ matches.push({ sections: tabEdit.newSections!, section, index, tabEdit, tabKey });
1000
1305
  });
1001
1306
  }
1002
1307
 
@@ -1018,11 +1323,11 @@ export class CustomObjectPageLayout {
1018
1323
  indexes.push(match.index);
1019
1324
  byArray.set(match.sections, indexes);
1020
1325
  if (match.tabEdit != null) removeKeyFromOrder(match.tabEdit.sectionsOrder, sectionKey);
1326
+ this.removeSectionVisibility(sectionKey);
1021
1327
  }
1022
1328
  for (const [sections, indexes] of byArray.entries()) {
1023
1329
  removeIndexes(sections, indexes);
1024
1330
  }
1025
- this.removeSectionVisibility(sectionKey);
1026
1331
  return this;
1027
1332
  }
1028
1333
 
@@ -1141,9 +1446,10 @@ export class CustomObjectPageLayout {
1141
1446
  field: Field,
1142
1447
  fallbackSectionKey: string,
1143
1448
  options: AddPageLayoutFieldOptions,
1449
+ fieldSection: CustomObjectFieldSection | undefined,
1144
1450
  ): SerializedSectionField {
1145
1451
  this.assertFieldBelongsToLayout(field);
1146
- this.assignFieldSectionForMutation(field, fallbackSectionKey, options.fieldSection);
1452
+ this.assignFieldSectionForMutation(field, fallbackSectionKey, fieldSection);
1147
1453
  return toSerializedSectionField(field.getApiName(), options);
1148
1454
  }
1149
1455
 
@@ -1171,23 +1477,22 @@ export class CustomObjectPageLayout {
1171
1477
  }
1172
1478
 
1173
1479
  private normalizeSectionForMutation(input: PageLayoutSection): SerializedFieldsSection {
1174
- if (input.section.getModelApiName() !== this._customObjectApiName) {
1175
- throw new Error(
1176
- `CustomObjectPageLayout section "${input.section.getSectionId()}" belongs to ` +
1177
- `"${input.section.getModelApiName()}", but the layout belongs to "${this._customObjectApiName}".`,
1178
- );
1179
- }
1480
+ this.assertFieldSectionBelongsToLayout(input.section);
1180
1481
 
1181
- const key = input.key ?? input.section.getSectionId();
1482
+ const sectionId = input.section.getSectionId();
1483
+ if (inputFieldsSectionHasKey(input)) {
1484
+ throw new Error('CustomObjectPageLayout fields_section key is not supported. Omit key.');
1485
+ }
1182
1486
  const name = input.name ?? input.section.getName();
1183
1487
  const normalized: SerializedFieldsSection = {
1184
- key,
1185
1488
  name,
1186
1489
  type: 'fields_section',
1187
- fields: input.fields.map((field) => this.normalizeSectionFieldForMutation(input.section, key, field)),
1490
+ fields: input.fields.map((field) =>
1491
+ this.normalizeSectionFieldForMutation(input.section, sectionId, field),
1492
+ ),
1188
1493
  };
1189
1494
  if (input.layout != null) normalized.layout = input.layout;
1190
- return normalized;
1495
+ return attachFieldSectionId(normalized, sectionId);
1191
1496
  }
1192
1497
 
1193
1498
  private normalizeTabForMutation(tab: PageLayoutTab): SerializedPageLayoutTab {
@@ -1207,7 +1512,7 @@ export class CustomObjectPageLayout {
1207
1512
  if (rawFieldSection != null) {
1208
1513
  if (typeof rawFieldSection === 'string') {
1209
1514
  throw new TypeError(
1210
- `CustomObjectPageLayout fieldSection must be a CustomObjectFieldSection object; ` +
1515
+ `CustomObjectPageLayout section must be a CustomObjectFieldSection object; ` +
1211
1516
  `received raw section id "${rawFieldSection}". Load/include the existing field section first.`,
1212
1517
  );
1213
1518
  }
@@ -1226,7 +1531,7 @@ export class CustomObjectPageLayout {
1226
1531
  if (field.getSectionId() == null) {
1227
1532
  throw new Error(
1228
1533
  `CustomObjectPageLayout field "${field.getApiName()}" is missing field-section metadata. ` +
1229
- `Construct the field with a CustomObjectFieldSection or pass fieldSection when adding it to ` +
1534
+ `Construct the field with a CustomObjectFieldSection or pass section when adding it to ` +
1230
1535
  `section "${fallbackSectionKey}".`,
1231
1536
  );
1232
1537
  }
@@ -1241,17 +1546,63 @@ export class CustomObjectPageLayout {
1241
1546
  }
1242
1547
  }
1243
1548
 
1244
- private requireOneFieldContainer(tabKey: string | undefined, sectionKey: string): MutableFieldContainer {
1549
+ private assertFieldSectionBelongsToLayout(section: CustomObjectFieldSection): void {
1550
+ if (section.getModelApiName() !== this._customObjectApiName) {
1551
+ throw new Error(
1552
+ `CustomObjectPageLayout field section "${section.getSectionId()}" belongs to ` +
1553
+ `"${section.getModelApiName()}", but the layout belongs to "${this._customObjectApiName}".`,
1554
+ );
1555
+ }
1556
+ }
1557
+
1558
+ private requireOneFieldContainer(
1559
+ tabKey: string | undefined,
1560
+ sectionKey: string | undefined,
1561
+ fallbackFieldSection?: CustomObjectFieldSection,
1562
+ ): MutableFieldContainer {
1245
1563
  const containers = this.findFieldContainers({
1246
1564
  tabKey,
1247
1565
  sectionKey,
1248
1566
  systemSectionOnly: false,
1249
1567
  });
1250
1568
  if (containers.length === 0) {
1251
- const prefix = tabKey != null ? `${tabKey}.` : '';
1252
- throw new Error(`CustomObjectPageLayout could not find section "${prefix}${sectionKey}".`);
1569
+ if (sectionKey != null && fallbackFieldSection != null) {
1570
+ const unkeyedContainers = this.findFieldContainers({
1571
+ tabKey,
1572
+ sectionKey: undefined,
1573
+ systemSectionOnly: false,
1574
+ }).filter((container) => container.section != null && container.sectionKey == null);
1575
+ if (unkeyedContainers.length === 1) {
1576
+ if (fallbackFieldSection.getModelApiName() !== this._customObjectApiName) {
1577
+ throw new Error(
1578
+ `CustomObjectPageLayout field section "${fallbackFieldSection.getSectionId()}" belongs to ` +
1579
+ `"${fallbackFieldSection.getModelApiName()}", but the layout belongs to "${
1580
+ this._customObjectApiName
1581
+ }".`,
1582
+ );
1583
+ }
1584
+ const container = unkeyedContainers[0] as MutableFieldContainer & {
1585
+ section: SerializedFieldsSection;
1586
+ };
1587
+ attachFieldSectionId(container.section, fallbackFieldSection.getSectionId());
1588
+ container.sectionKey = fallbackFieldSection.getSectionId();
1589
+ container.label = `${container.tabKey}.${container.sectionKey}`;
1590
+ return container;
1591
+ }
1592
+ }
1593
+ const scope =
1594
+ sectionKey == null ? 'a field section' : (
1595
+ `section "${tabKey != null ? `${tabKey}.` : ''}${sectionKey}"`
1596
+ );
1597
+ throw new Error(`CustomObjectPageLayout could not find ${scope}.`);
1253
1598
  }
1254
1599
  if (containers.length > 1) {
1600
+ if (sectionKey == null) {
1601
+ throw new Error(
1602
+ `CustomObjectPageLayout matched multiple sections: ` +
1603
+ `${containers.map((container) => container.label).join(', ')}. Pass section to choose one.`,
1604
+ );
1605
+ }
1255
1606
  throw new Error(
1256
1607
  `CustomObjectPageLayout section "${sectionKey}" matched multiple sections: ` +
1257
1608
  `${containers.map((container) => container.label).join(', ')}. Pass tabKey to choose one.`,
@@ -1271,34 +1622,40 @@ export class CustomObjectPageLayout {
1271
1622
  for (const tab of this._tabEdits.newTabs ?? []) {
1272
1623
  if (!isTabWithSections(tab)) continue;
1273
1624
  if (filter.tabKey != null && tab.key !== filter.tabKey) continue;
1274
- for (const section of tab.sections) {
1275
- if (filter.sectionKey != null && section.key !== filter.sectionKey) continue;
1625
+ for (const [index, section] of tab.sections.entries()) {
1626
+ const sectionKey = layoutSectionKey(section);
1627
+ if (filter.sectionKey != null && !layoutSectionMatches(section, filter.sectionKey)) continue;
1628
+ const label = sectionKey == null ? `${tab.key}.sections[${index}]` : `${tab.key}.${sectionKey}`;
1276
1629
  containers.push({
1277
1630
  tabKey: tab.key,
1278
- sectionKey: section.key,
1631
+ sectionKey,
1632
+ section,
1279
1633
  fields: ensureArrayProperty<MutableSectionField>(
1280
1634
  section as unknown as Record<string, any>,
1281
1635
  'fields',
1282
- `newTabs.${tab.key}.sections.${section.key}.fields`,
1636
+ `newTabs.${label}.fields`,
1283
1637
  ),
1284
- label: `${tab.key}.${section.key}`,
1638
+ label,
1285
1639
  });
1286
1640
  }
1287
1641
  }
1288
1642
 
1289
1643
  for (const [tabKey, tabEdit] of Object.entries(this._tabEdits.systemTabEdits ?? {})) {
1290
1644
  if (filter.tabKey != null && tabKey !== filter.tabKey) continue;
1291
- for (const section of tabEdit.newSections ?? []) {
1292
- if (filter.sectionKey != null && section.key !== filter.sectionKey) continue;
1645
+ for (const [index, section] of (tabEdit.newSections ?? []).entries()) {
1646
+ const sectionKey = layoutSectionKey(section);
1647
+ if (filter.sectionKey != null && !layoutSectionMatches(section, filter.sectionKey)) continue;
1648
+ const label = sectionKey == null ? `${tabKey}.newSections[${index}]` : `${tabKey}.${sectionKey}`;
1293
1649
  containers.push({
1294
1650
  tabKey,
1295
- sectionKey: section.key,
1651
+ sectionKey,
1652
+ section,
1296
1653
  fields: ensureArrayProperty<MutableSectionField>(
1297
1654
  section as unknown as Record<string, any>,
1298
1655
  'fields',
1299
- `systemTabEdits.${tabKey}.newSections.${section.key}.fields`,
1656
+ `systemTabEdits.${label}.fields`,
1300
1657
  ),
1301
- label: `${tabKey}.${section.key}`,
1658
+ label,
1302
1659
  });
1303
1660
  }
1304
1661
  }
@@ -1329,11 +1686,14 @@ export class CustomObjectPageLayout {
1329
1686
 
1330
1687
  private ensureSystemSectionFieldContainer(
1331
1688
  tabKey: string | undefined,
1332
- sectionKey: string,
1689
+ sectionKey: string | undefined,
1333
1690
  ): MutableFieldContainer {
1334
1691
  if (tabKey == null) {
1335
1692
  throw new Error('CustomObjectPageLayout addField with systemSection: true requires tabKey.');
1336
1693
  }
1694
+ if (sectionKey == null) {
1695
+ throw new Error('CustomObjectPageLayout addField with systemSection: true requires sectionKey.');
1696
+ }
1337
1697
  const sectionEdit = this.ensureSystemSectionEdit(tabKey, sectionKey);
1338
1698
  if (sectionEdit.deleted === true) {
1339
1699
  throw new Error(`CustomObjectPageLayout system section "${tabKey}.${sectionKey}" is marked deleted.`);
@@ -1396,12 +1756,12 @@ export class CustomObjectPageLayout {
1396
1756
 
1397
1757
  private assertSectionKeyAvailable(
1398
1758
  sections: SerializedFieldsSection[],
1399
- sectionKey: string,
1759
+ sectionKey: string | undefined,
1400
1760
  tabKey: string,
1401
1761
  allowDuplicate: boolean | undefined,
1402
1762
  ): void {
1403
1763
  if (allowDuplicate === true) return;
1404
- if (sections.some((section) => section.key === sectionKey)) {
1764
+ if (sectionKey != null && sections.some((section) => layoutSectionMatches(section, sectionKey))) {
1405
1765
  throw new Error(`CustomObjectPageLayout tab "${tabKey}" already contains section "${sectionKey}".`);
1406
1766
  }
1407
1767
  }
@@ -1476,12 +1836,13 @@ export class CustomObjectPageLayout {
1476
1836
  * tabs or sections. Always uses `apiName: 'default'`.
1477
1837
  *
1478
1838
  * @param customObject - The custom object to create the layout for.
1479
- * @param props - Optional. Pass `fields` to populate the General section.
1839
+ * @param props - Optional. Pass `section` and `fields` to populate the General section.
1480
1840
  * @returns A new `CustomObjectPageLayout` registered with the manifest.
1481
1841
  *
1482
1842
  * @example
1483
1843
  * ```ts
1484
1844
  * CustomObjectPageLayout.basic(memberObj, {
1845
+ * section: profileSection,
1485
1846
  * fields: [memberFirstName, memberEmail, memberStatusField],
1486
1847
  * });
1487
1848
  * ```
@@ -1489,16 +1850,40 @@ export class CustomObjectPageLayout {
1489
1850
  static basic(
1490
1851
  customObject: CustomObject,
1491
1852
  props: {
1853
+ section?: CustomObjectFieldSection;
1492
1854
  fields?: Field[];
1493
1855
  } = {},
1494
1856
  ): CustomObjectPageLayout {
1495
1857
  const coApiName = customObject.getApiName();
1496
1858
  const coName = customObject.getName();
1497
- const sectionPrefix = coApiName.replace(/__c$/, '').replace(/[^a-zA-Z0-9]+/g, '_');
1498
- const generalSection = new CustomObjectFieldSection(customObject, {
1499
- sectionId: `sec_${sectionPrefix}_general`,
1500
- name: 'General',
1501
- });
1859
+ const fields = props.fields ?? [];
1860
+ const generalSection =
1861
+ props.section ??
1862
+ (() => {
1863
+ const alreadySectionedField = fields.find((field) => field.getSectionId() != null);
1864
+ if (alreadySectionedField != null) {
1865
+ throw new Error(
1866
+ `CustomObjectPageLayout.basic field "${alreadySectionedField.getApiName()}" already belongs to ` +
1867
+ `field section "${alreadySectionedField.getSectionId()}". Pass that CustomObjectFieldSection ` +
1868
+ 'as section so basic() does not reassign the field.',
1869
+ );
1870
+ }
1871
+ const sectionPrefix = coApiName.replace(/__c$/, '').replace(/[^a-zA-Z0-9]+/g, '_');
1872
+ return new CustomObjectFieldSection(customObject, {
1873
+ sectionId: `sec_${sectionPrefix}_general`,
1874
+ name: 'General',
1875
+ });
1876
+ })();
1877
+ assertSectionBelongsTo(customObject, generalSection);
1878
+ for (const field of fields) {
1879
+ const fieldSectionId = field.getSectionId();
1880
+ if (fieldSectionId != null && fieldSectionId !== generalSection.getSectionId()) {
1881
+ throw new Error(
1882
+ `CustomObjectPageLayout.basic field "${field.getApiName()}" belongs to field section ` +
1883
+ `"${fieldSectionId}", but basic() was given section "${generalSection.getSectionId()}".`,
1884
+ );
1885
+ }
1886
+ }
1502
1887
  return new CustomObjectPageLayout(customObject, {
1503
1888
  apiName: 'default',
1504
1889
  name: `${coName} Layout`,
@@ -1510,11 +1895,10 @@ export class CustomObjectPageLayout {
1510
1895
  type: 'tab_with_sections',
1511
1896
  sections: [
1512
1897
  {
1513
- key: 'general',
1514
1898
  name: 'General',
1515
1899
  type: 'fields_section',
1516
1900
  section: generalSection,
1517
- fields: props.fields ?? [],
1901
+ fields,
1518
1902
  },
1519
1903
  ],
1520
1904
  },