@rippling/rippling-sdk 0.2.0-alpha.49 → 0.2.0-alpha.51

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 (72) hide show
  1. package/examples/manifest/existing-manifest-mutations/existing-manifest-mutations.ts +5 -0
  2. package/lib/manifest/custom-object-page-layout.d.mts +167 -48
  3. package/lib/manifest/custom-object-page-layout.d.mts.map +1 -1
  4. package/lib/manifest/custom-object-page-layout.d.ts +167 -48
  5. package/lib/manifest/custom-object-page-layout.d.ts.map +1 -1
  6. package/lib/manifest/custom-object-page-layout.js +202 -88
  7. package/lib/manifest/custom-object-page-layout.js.map +1 -1
  8. package/lib/manifest/custom-object-page-layout.mjs +202 -88
  9. package/lib/manifest/custom-object-page-layout.mjs.map +1 -1
  10. package/lib/manifest/custom-object.d.mts +1 -1
  11. package/lib/manifest/custom-object.d.ts +1 -1
  12. package/lib/manifest/custom-object.js +1 -1
  13. package/lib/manifest/custom-object.mjs +1 -1
  14. package/lib/manifest/existing-manifest-context.d.mts +1 -0
  15. package/lib/manifest/existing-manifest-context.d.mts.map +1 -1
  16. package/lib/manifest/existing-manifest-context.d.ts +1 -0
  17. package/lib/manifest/existing-manifest-context.d.ts.map +1 -1
  18. package/lib/manifest/existing-manifest-context.js +7 -6
  19. package/lib/manifest/existing-manifest-context.js.map +1 -1
  20. package/lib/manifest/existing-manifest-context.mjs +7 -6
  21. package/lib/manifest/existing-manifest-context.mjs.map +1 -1
  22. package/lib/manifest/field.d.mts +3 -3
  23. package/lib/manifest/field.d.ts +3 -3
  24. package/lib/manifest/field.js +2 -2
  25. package/lib/manifest/field.mjs +2 -2
  26. package/lib/manifest/index.d.mts +1 -1
  27. package/lib/manifest/index.d.mts.map +1 -1
  28. package/lib/manifest/index.d.ts +1 -1
  29. package/lib/manifest/index.d.ts.map +1 -1
  30. package/lib/manifest/index.js.map +1 -1
  31. package/lib/manifest/index.mjs.map +1 -1
  32. package/lib/sdui/components.d.mts +100 -211
  33. package/lib/sdui/components.d.mts.map +1 -1
  34. package/lib/sdui/components.d.ts +100 -211
  35. package/lib/sdui/components.d.ts.map +1 -1
  36. package/lib/sdui/components.js +96 -182
  37. package/lib/sdui/components.js.map +1 -1
  38. package/lib/sdui/components.mjs +93 -178
  39. package/lib/sdui/components.mjs.map +1 -1
  40. package/lib/sdui/index.d.mts +12 -52
  41. package/lib/sdui/index.d.mts.map +1 -1
  42. package/lib/sdui/index.d.ts +12 -52
  43. package/lib/sdui/index.d.ts.map +1 -1
  44. package/lib/sdui/index.js +10 -50
  45. package/lib/sdui/index.js.map +1 -1
  46. package/lib/sdui/index.mjs +5 -25
  47. package/lib/sdui/index.mjs.map +1 -1
  48. package/lib/sdui/propTypes.d.mts +3156 -3515
  49. package/lib/sdui/propTypes.d.mts.map +1 -1
  50. package/lib/sdui/propTypes.d.ts +3156 -3515
  51. package/lib/sdui/propTypes.d.ts.map +1 -1
  52. package/lib/sdui/propTypes.js +1 -1
  53. package/lib/sdui/propTypes.mjs +1 -1
  54. package/package.json +1 -1
  55. package/resources/leave-balances.d.mts +10 -0
  56. package/resources/leave-balances.d.mts.map +1 -1
  57. package/resources/leave-balances.d.ts +10 -0
  58. package/resources/leave-balances.d.ts.map +1 -1
  59. package/src/lib/manifest/custom-object-page-layout.ts +497 -180
  60. package/src/lib/manifest/custom-object.ts +1 -1
  61. package/src/lib/manifest/existing-manifest-context.ts +11 -6
  62. package/src/lib/manifest/field.ts +3 -3
  63. package/src/lib/manifest/index.ts +27 -0
  64. package/src/lib/sdui/components.ts +181 -332
  65. package/src/lib/sdui/index.ts +13 -93
  66. package/src/lib/sdui/propTypes.ts +3509 -3892
  67. package/src/resources/leave-balances.ts +12 -0
  68. package/src/version.ts +1 -1
  69. package/version.d.mts +1 -1
  70. package/version.d.ts +1 -1
  71. package/version.js +1 -1
  72. package/version.mjs +1 -1
@@ -14,32 +14,34 @@ function isFieldRef(value) {
14
14
  const candidate = value;
15
15
  return candidate != null && typeof candidate === 'object' && isField(candidate.field);
16
16
  }
17
- const FIELD_SECTION_ID = Symbol('CustomObjectPageLayout.fieldSectionId');
17
+ const LAYOUT_SECTION_KEY = Symbol('CustomObjectPageLayout.layoutSectionKey');
18
18
  function isCustomObjectFieldSection(value) {
19
19
  return value instanceof CustomObjectFieldSection;
20
20
  }
21
- function attachFieldSectionId(section, sectionId) {
22
- if (section.key != null && section.key !== sectionId) {
23
- throw new Error(`CustomObjectPageLayout fields_section key "${section.key}" does not match field section "${sectionId}".`);
24
- }
25
- section.key = sectionId;
26
- Object.defineProperty(section, FIELD_SECTION_ID, {
27
- value: sectionId,
21
+ function explicitLayoutSectionKey(section) {
22
+ const record = section;
23
+ const key = section.key ?? record['layout_section_key'] ?? record['layoutSectionKey'];
24
+ return typeof key === 'string' ? key : undefined;
25
+ }
26
+ function attachLayoutSectionKey(section, sectionKey) {
27
+ if (explicitLayoutSectionKey(section) == null)
28
+ section.key = sectionKey;
29
+ Object.defineProperty(section, LAYOUT_SECTION_KEY, {
30
+ value: sectionKey,
28
31
  enumerable: false,
29
32
  configurable: true,
30
33
  });
31
34
  return section;
32
35
  }
33
36
  function layoutSectionKey(section) {
34
- return (section.key ??
35
- section[FIELD_SECTION_ID]);
37
+ return (explicitLayoutSectionKey(section) ??
38
+ section[LAYOUT_SECTION_KEY]);
36
39
  }
37
40
  function layoutSectionMatches(section, sectionKey) {
38
41
  return layoutSectionKey(section) === sectionKey;
39
42
  }
40
- function inputFieldsSectionHasKey(input) {
41
- const record = input;
42
- return Object.prototype.hasOwnProperty.call(record, 'key') && record['key'] != null;
43
+ function isSerializedFieldsSection(section) {
44
+ return section.type === 'fields_section';
43
45
  }
44
46
  function assertSameObject(customObject, field, sectionKey) {
45
47
  if (field.getCustomObjectApiName() !== customObject.getApiName()) {
@@ -74,10 +76,29 @@ function toSerializedSectionField(fieldRqlName, options = {}) {
74
76
  normalized.hidden = options.hidden;
75
77
  if (options.editable != null)
76
78
  normalized.editable = options.editable;
79
+ if (options.readOnly != null)
80
+ normalized.readOnly = options.readOnly;
81
+ if (options.defaultValue != null)
82
+ normalized.defaultValue = options.defaultValue;
83
+ if (options.height != null)
84
+ normalized.height = options.height;
85
+ if (options.canBeDeleted != null)
86
+ normalized.canBeDeleted = options.canBeDeleted;
77
87
  if (options.movementOption != null)
78
88
  normalized.movementOption = options.movementOption;
79
89
  return normalized;
80
90
  }
91
+ function copySectionOptions(target, input) {
92
+ if (input.canBeDeleted != null)
93
+ target.canBeDeleted = input.canBeDeleted;
94
+ if (input.canBeRenamed != null)
95
+ target.canBeRenamed = input.canBeRenamed;
96
+ if (input.movementOption != null)
97
+ target.movementOption = input.movementOption;
98
+ if (input.hideName != null)
99
+ target.hideName = input.hideName;
100
+ return target;
101
+ }
81
102
  function normalizeSectionField(customObject, section, sectionKey, entry) {
82
103
  if (isField(entry)) {
83
104
  assertSameObject(customObject, entry, sectionKey);
@@ -94,26 +115,28 @@ function normalizeSectionField(customObject, section, sectionKey, entry) {
94
115
  }
95
116
  function normalizeFieldsSection(customObject, input) {
96
117
  assertSectionBelongsTo(customObject, input.section);
97
- const sectionId = input.section.getSectionId();
98
- if (inputFieldsSectionHasKey(input)) {
99
- throw new Error('CustomObjectPageLayout fields_section key is not supported. Omit key.');
100
- }
118
+ const sectionKey = input.key ?? input.section.getSectionId();
101
119
  const name = input.name ?? input.section.getName();
102
- const normalized = {
120
+ const normalized = copySectionOptions({
103
121
  name,
104
122
  type: 'fields_section',
105
- fields: input.fields.map((field) => normalizeSectionField(customObject, input.section, sectionId, field)),
106
- };
123
+ fields: input.fields.map((field) => normalizeSectionField(customObject, input.section, sectionKey, field)),
124
+ }, input);
107
125
  if (input.layout != null)
108
126
  normalized.layout = input.layout;
109
- return attachFieldSectionId(normalized, sectionId);
127
+ return attachLayoutSectionKey(normalized, sectionKey);
128
+ }
129
+ function normalizeSection(customObject, section) {
130
+ if (section.type === 'fields_section')
131
+ return normalizeFieldsSection(customObject, section);
132
+ return { ...section };
110
133
  }
111
134
  function normalizeTab(customObject, tab) {
112
135
  if (tab.type === 'custom_tab')
113
136
  return tab;
114
137
  return {
115
138
  ...tab,
116
- sections: tab.sections.map((section) => normalizeFieldsSection(customObject, section)),
139
+ sections: tab.sections.map((section) => normalizeSection(customObject, section)),
117
140
  };
118
141
  }
119
142
  function normalizeSystemSectionEdit(customObject, tabKey, sectionKey, edit) {
@@ -128,7 +151,7 @@ function normalizeSystemTabEdit(customObject, tabKey, edit) {
128
151
  const { newSections: _newSections, systemSectionEdits: _systemSectionEdits, ...rest } = edit;
129
152
  const normalized = { ...rest };
130
153
  if (edit.newSections != null) {
131
- normalized.newSections = edit.newSections.map((section) => normalizeFieldsSection(customObject, section));
154
+ normalized.newSections = edit.newSections.map((section) => normalizeSection(customObject, section));
132
155
  }
133
156
  if (edit.systemSectionEdits != null) {
134
157
  const systemSectionEdits = {};
@@ -186,6 +209,18 @@ function normalizeHeaderEdits(headerEdits) {
186
209
  for (const [index, field] of (headerEdits.newFields ?? []).entries()) {
187
210
  assertKnownKeys(field, NEW_HEADER_FIELD_KEYS, `CustomObjectPageLayout headerEdits.newFields[${index}]`, 'Use { rqlName, key } entries, or call addHeaderField(field).');
188
211
  }
212
+ if (headerEdits.newTitleField === '') {
213
+ throw new Error('CustomObjectPageLayout headerEdits.newTitleField cannot be empty. Use titleFieldDeleted.');
214
+ }
215
+ if (headerEdits.newDescriptionField === '') {
216
+ throw new Error('CustomObjectPageLayout headerEdits.newDescriptionField cannot be empty. Use descriptionFieldDeleted.');
217
+ }
218
+ if (headerEdits.newTitleField != null && headerEdits.titleFieldDeleted === true) {
219
+ throw new Error('CustomObjectPageLayout headerEdits cannot both set newTitleField and titleFieldDeleted.');
220
+ }
221
+ if (headerEdits.newDescriptionField != null && headerEdits.descriptionFieldDeleted === true) {
222
+ throw new Error('CustomObjectPageLayout headerEdits cannot both set newDescriptionField and descriptionFieldDeleted.');
223
+ }
189
224
  if (headerEdits.headerFieldsOrder == null)
190
225
  return headerEdits;
191
226
  return {
@@ -248,52 +283,90 @@ function fieldRqlNameOf(entry) {
248
283
  if (entry == null || typeof entry !== 'object')
249
284
  return undefined;
250
285
  const record = entry;
251
- const fieldRqlName = record['fieldRqlName'] ?? record['field_rql_name'];
286
+ const fieldRqlName = record['fieldRqlName'] ?? record['field_rql_name'] ?? record['apiName'] ?? record['api_name'];
252
287
  return typeof fieldRqlName === 'string' ? fieldRqlName : undefined;
253
288
  }
254
- function explicitFieldSectionId(entry) {
255
- if (entry == null || typeof entry !== 'object')
256
- return undefined;
257
- const record = entry;
258
- const fieldSectionId = record['fieldSection'] ?? record['field_section'];
259
- return typeof fieldSectionId === 'string' ? fieldSectionId : undefined;
289
+ function addFieldSectionLayoutSection(index, fieldSectionId, entry) {
290
+ const entries = index.get(fieldSectionId) ?? [];
291
+ if (!entries.some((existing) => existing.tabKey === entry.tabKey && existing.sectionKey === entry.sectionKey)) {
292
+ entries.push(entry);
293
+ }
294
+ index.set(fieldSectionId, entries);
260
295
  }
261
- function inferredSectionIdFromFields(section, resolveFieldSectionId) {
262
- const sectionIds = new Set();
296
+ function indexFieldsSectionInput(index, tabKey, section) {
297
+ addFieldSectionLayoutSection(index, section.section.getSectionId(), {
298
+ tabKey,
299
+ sectionKey: section.key ?? section.section.getSectionId(),
300
+ });
301
+ }
302
+ function indexPageLayoutSectionInput(index, tabKey, section) {
303
+ if (section.type === 'fields_section')
304
+ indexFieldsSectionInput(index, tabKey, section);
305
+ }
306
+ function buildFieldSectionLayoutIndexFromProps(tabEdits) {
307
+ const index = new Map();
308
+ if (tabEdits == null)
309
+ return index;
310
+ for (const tab of tabEdits.newTabs ?? []) {
311
+ if (tab.type !== 'tab_with_sections')
312
+ continue;
313
+ for (const section of tab.sections)
314
+ indexPageLayoutSectionInput(index, tab.key, section);
315
+ }
316
+ for (const [tabKey, tabEdit] of Object.entries(tabEdits.systemTabEdits ?? {})) {
317
+ for (const section of tabEdit.newSections ?? []) {
318
+ indexPageLayoutSectionInput(index, tabKey, section);
319
+ }
320
+ }
321
+ return index;
322
+ }
323
+ function indexLoadedFieldsSection(index, tabKey, section, resolveFieldSectionId) {
324
+ const sectionKey = layoutSectionKey(section);
325
+ if (sectionKey == null || resolveFieldSectionId == null)
326
+ return;
263
327
  for (const field of section.fields ?? []) {
264
- const explicitSectionId = explicitFieldSectionId(field);
265
- if (explicitSectionId != null) {
266
- sectionIds.add(explicitSectionId);
328
+ const fieldRqlName = fieldRqlNameOf(field);
329
+ if (fieldRqlName == null)
267
330
  continue;
331
+ const fieldSectionId = resolveFieldSectionId(fieldRqlName);
332
+ if (fieldSectionId != null) {
333
+ addFieldSectionLayoutSection(index, fieldSectionId, { tabKey, sectionKey });
268
334
  }
269
- const fieldApiName = fieldRqlNameOf(field);
270
- if (fieldApiName == null || resolveFieldSectionId == null)
335
+ }
336
+ }
337
+ function buildFieldSectionLayoutIndexFromLoadedTabEdits(tabEdits, resolveFieldSectionId) {
338
+ const index = new Map();
339
+ for (const tab of tabEdits.newTabs ?? []) {
340
+ if (!isTabWithSections(tab))
271
341
  continue;
272
- const resolvedSectionId = resolveFieldSectionId(fieldApiName);
273
- if (resolvedSectionId != null)
274
- sectionIds.add(resolvedSectionId);
342
+ for (const section of tab.sections) {
343
+ if (isSerializedFieldsSection(section)) {
344
+ indexLoadedFieldsSection(index, tab.key, section, resolveFieldSectionId);
345
+ }
346
+ }
275
347
  }
276
- return sectionIds.size === 1 ? [...sectionIds][0] : undefined;
348
+ for (const [tabKey, tabEdit] of Object.entries(tabEdits.systemTabEdits ?? {})) {
349
+ for (const section of tabEdit.newSections ?? []) {
350
+ if (isSerializedFieldsSection(section)) {
351
+ indexLoadedFieldsSection(index, tabKey, section, resolveFieldSectionId);
352
+ }
353
+ }
354
+ }
355
+ return index;
277
356
  }
278
- function annotateLoadedFieldsSection(section, resolveFieldSectionId) {
279
- const explicitSectionId = layoutSectionKey(section);
280
- const inferredSectionId = inferredSectionIdFromFields(section, resolveFieldSectionId);
281
- if (explicitSectionId != null && inferredSectionId != null && explicitSectionId !== inferredSectionId) {
282
- throw new Error(`CustomObjectPageLayout fields_section key "${explicitSectionId}" does not match ` +
283
- `field section "${inferredSectionId}".`);
284
- }
285
- const sectionId = explicitSectionId ?? inferredSectionId;
286
- return sectionId == null ? section : attachFieldSectionId(section, sectionId);
357
+ function annotateLoadedFieldsSection(section) {
358
+ const sectionKey = layoutSectionKey(section);
359
+ return sectionKey == null ? section : attachLayoutSectionKey(section, sectionKey);
287
360
  }
288
- function annotateLoadedTabEdits(tabEdits, resolveFieldSectionId) {
361
+ function annotateLoadedTabEdits(tabEdits) {
289
362
  for (const tab of tabEdits.newTabs ?? []) {
290
363
  if (!isTabWithSections(tab))
291
364
  continue;
292
- tab.sections = tab.sections.map((section) => annotateLoadedFieldsSection(section, resolveFieldSectionId));
365
+ tab.sections = tab.sections.map((section) => isSerializedFieldsSection(section) ? annotateLoadedFieldsSection(section) : section);
293
366
  }
294
367
  for (const tabEdit of Object.values(tabEdits.systemTabEdits ?? {})) {
295
368
  if (tabEdit.newSections != null) {
296
- tabEdit.newSections = tabEdit.newSections.map((section) => annotateLoadedFieldsSection(section, resolveFieldSectionId));
369
+ tabEdit.newSections = tabEdit.newSections.map((section) => isSerializedFieldsSection(section) ? annotateLoadedFieldsSection(section) : section);
297
370
  }
298
371
  }
299
372
  return tabEdits;
@@ -409,6 +482,7 @@ export class CustomObjectPageLayout {
409
482
  this._customObjectApiName = customObject.getApiName();
410
483
  this._name = props.name;
411
484
  this._tabEdits = normalizeTabEdits(customObject, props.tabEdits ?? {});
485
+ this._fieldSectionLayoutSections = buildFieldSectionLayoutIndexFromProps(props.tabEdits);
412
486
  this._headerEdits = normalizeHeaderEdits(props.headerEdits ?? {});
413
487
  this._visibilityConditions = props.visibilityConditions ?? {};
414
488
  this._blueprintKey = props.blueprintKey;
@@ -427,15 +501,17 @@ export class CustomObjectPageLayout {
427
501
  /** @internal Hydrates a page layout from existing manifest wire JSON. */
428
502
  static _fromExistingComponent(customObject, component, resolveFieldSectionId) {
429
503
  const layout = Object.create(CustomObjectPageLayout.prototype);
504
+ const tabEdits = annotateLoadedTabEdits(cloneJson(component['tab_edits'] ?? {}));
430
505
  Object.assign(layout, {
431
506
  _layoutId: requireStringComponentField(component, 'layout_id', CustomObjectPageLayout.componentType),
432
507
  _apiName: requireStringComponentField(component, 'api_name', CustomObjectPageLayout.componentType),
433
508
  _customObjectApiName: requireStringComponentField(component, 'object_rql_name', CustomObjectPageLayout.componentType),
434
509
  _name: requireStringComponentField(component, 'name', CustomObjectPageLayout.componentType),
435
- _tabEdits: annotateLoadedTabEdits(cloneJson(component['tab_edits'] ?? {}), resolveFieldSectionId),
510
+ _tabEdits: tabEdits,
436
511
  _headerEdits: cloneJson(component['header_edits'] ?? {}),
437
512
  _visibilityConditions: cloneJson(component['visibility_conditions'] ?? {}),
438
513
  _blueprintKey: component['blueprint_key'],
514
+ _fieldSectionLayoutSections: buildFieldSectionLayoutIndexFromLoadedTabEdits(tabEdits, resolveFieldSectionId),
439
515
  });
440
516
  customObject._register(layout);
441
517
  return layout;
@@ -471,8 +547,8 @@ export class CustomObjectPageLayout {
471
547
  * Adds multiple fields to an existing layout section.
472
548
  *
473
549
  * Fields are inserted in the order provided. For custom layout sections,
474
- * pass `options.section` so TypeScript and runtime validation agree on the
475
- * target `CUSTOM_OBJECT_FIELD_SECTION`.
550
+ * pass `section` to choose the typed field section. Use `sectionKey` only
551
+ * when a field section maps to multiple layout sections.
476
552
  */
477
553
  addFields(fields, options) {
478
554
  if (fields.length === 0)
@@ -486,14 +562,19 @@ export class CustomObjectPageLayout {
486
562
  return this.ensureSystemSectionFieldContainer(options.tabKey, options.sectionKey);
487
563
  })()
488
564
  : (() => {
489
- assertOptionAbsent(options, 'sectionKey', 'CustomObjectPageLayout addField sectionKey is only supported with systemSection: true. ' +
490
- 'Pass section: CustomObjectFieldSection for custom layout sections.');
491
565
  assertOptionAbsent(options, 'fieldSection', 'CustomObjectPageLayout addField fieldSection is no longer supported. ' +
492
566
  'Pass section: CustomObjectFieldSection instead.');
493
- const section = requireFieldSectionOption(options.section, 'CustomObjectPageLayout addField section');
494
- this.assertFieldSectionBelongsToLayout(section);
495
- fieldSectionForMutation = section;
496
- return this.requireOneFieldContainer(options.tabKey, section.getSectionId(), section);
567
+ const section = options.section == null ?
568
+ undefined
569
+ : requireFieldSectionOption(options.section, 'CustomObjectPageLayout addField section');
570
+ if (section != null) {
571
+ this.assertFieldSectionBelongsToLayout(section);
572
+ fieldSectionForMutation = section;
573
+ }
574
+ const sectionKey = options.sectionKey ??
575
+ (section == null ? undefined : this.layoutSectionKeyForFieldSection(options.tabKey, section)) ??
576
+ section?.getSectionId();
577
+ return this.requireOneFieldContainer(options.tabKey, sectionKey, section);
497
578
  })();
498
579
  const entries = fields.map((field) => this.serializeFieldForMutation(field, container.sectionKey ?? container.label, options, fieldSectionForMutation));
499
580
  if (options.allowDuplicate !== true) {
@@ -527,12 +608,12 @@ export class CustomObjectPageLayout {
527
608
  sectionKey = options.sectionKey;
528
609
  }
529
610
  else {
530
- assertOptionAbsent(options, 'sectionKey', 'CustomObjectPageLayout removeField sectionKey is only supported with systemSection: true. ' +
531
- 'Pass section: CustomObjectFieldSection for custom layout sections.');
611
+ sectionKey = options.sectionKey;
532
612
  if (options.section != null) {
533
613
  const section = requireFieldSectionOption(options.section, 'CustomObjectPageLayout removeField section');
534
614
  this.assertFieldSectionBelongsToLayout(section);
535
- sectionKey = section.getSectionId();
615
+ sectionKey ?? (sectionKey = this.layoutSectionKeyForFieldSection(options.tabKey, section));
616
+ sectionKey ?? (sectionKey = section.getSectionId());
536
617
  }
537
618
  }
538
619
  const containers = this.findFieldContainers({
@@ -590,6 +671,9 @@ export class CustomObjectPageLayout {
590
671
  const sectionsOrder = this.ensureSystemSectionsOrder(tabEdit);
591
672
  insertAt(sections, normalized, options.position);
592
673
  insertUniqueAt(sectionsOrder, layoutSectionKey(normalized), options.position);
674
+ if (section.type === 'fields_section') {
675
+ this.rememberFieldSectionLayoutSection(section.section, options.tabKey, layoutSectionKey(normalized));
676
+ }
593
677
  return this;
594
678
  }
595
679
  const tab = this.findNewTabWithSections(options.tabKey);
@@ -599,6 +683,9 @@ export class CustomObjectPageLayout {
599
683
  }
600
684
  this.assertSectionKeyAvailable(tab.sections, layoutSectionKey(normalized), options.tabKey, options.allowDuplicate);
601
685
  insertAt(tab.sections, normalized, options.position);
686
+ if (section.type === 'fields_section') {
687
+ this.rememberFieldSectionLayoutSection(section.section, tab.key, layoutSectionKey(normalized));
688
+ }
602
689
  return this;
603
690
  }
604
691
  removeSection(sectionOrKey, options = {}) {
@@ -615,9 +702,15 @@ export class CustomObjectPageLayout {
615
702
  this.removeSectionVisibility(sectionOrKey);
616
703
  return this;
617
704
  }
618
- const section = requireFieldSectionOption(sectionOrKey, 'CustomObjectPageLayout removeSection section');
619
- this.assertFieldSectionBelongsToLayout(section);
620
- const sectionKey = section.getSectionId();
705
+ let sectionKey;
706
+ if (typeof sectionOrKey === 'string') {
707
+ sectionKey = sectionOrKey;
708
+ }
709
+ else {
710
+ const section = requireFieldSectionOption(sectionOrKey, 'CustomObjectPageLayout removeSection section');
711
+ this.assertFieldSectionBelongsToLayout(section);
712
+ sectionKey = this.layoutSectionKeyForFieldSection(options.tabKey, section) ?? section.getSectionId();
713
+ }
621
714
  const matches = [];
622
715
  for (const tab of this._tabEdits.newTabs ?? []) {
623
716
  if (!isTabWithSections(tab))
@@ -625,7 +718,7 @@ export class CustomObjectPageLayout {
625
718
  if (options.tabKey != null && tab.key !== options.tabKey)
626
719
  continue;
627
720
  tab.sections.forEach((section, index) => {
628
- if (layoutSectionMatches(section, sectionKey))
721
+ if (isSerializedFieldsSection(section) && layoutSectionMatches(section, sectionKey))
629
722
  matches.push({ sections: tab.sections, section, index, tabKey: tab.key });
630
723
  });
631
724
  }
@@ -633,7 +726,7 @@ export class CustomObjectPageLayout {
633
726
  if (options.tabKey != null && tabKey !== options.tabKey)
634
727
  continue;
635
728
  (tabEdit.newSections ?? []).forEach((section, index) => {
636
- if (layoutSectionMatches(section, sectionKey))
729
+ if (isSerializedFieldsSection(section) && layoutSectionMatches(section, sectionKey))
637
730
  matches.push({ sections: tabEdit.newSections, section, index, tabEdit, tabKey });
638
731
  });
639
732
  }
@@ -671,6 +764,10 @@ export class CustomObjectPageLayout {
671
764
  const tabsOrder = this.ensureTabsOrder();
672
765
  insertAt(newTabs, normalized, options.position);
673
766
  insertUniqueAt(tabsOrder, normalized.key, options.position);
767
+ if (tab.type === 'tab_with_sections') {
768
+ for (const section of tab.sections)
769
+ indexPageLayoutSectionInput(this._fieldSectionLayoutSections, tab.key, section);
770
+ }
674
771
  return this;
675
772
  }
676
773
  /**
@@ -729,13 +826,11 @@ export class CustomObjectPageLayout {
729
826
  this.removeHeaderOrderFieldIfUnused(removedField);
730
827
  return this;
731
828
  }
732
- /** Sets the record title field in `header_edits`. */
733
829
  setTitleField(field) {
734
830
  const previousTitleField = this._headerEdits.newTitleField;
735
831
  const nextTitleField = this.headerFieldApiName(field);
736
832
  this._headerEdits.newTitleField = nextTitleField;
737
- if (field != null)
738
- this._headerEdits.titleFieldDeleted = false;
833
+ this._headerEdits.titleFieldDeleted = false;
739
834
  insertUniqueAt(this.ensureHeaderFieldsOrder(), nextTitleField, 'end');
740
835
  this.removeHeaderOrderFieldIfUnused(previousTitleField);
741
836
  return this;
@@ -748,13 +843,11 @@ export class CustomObjectPageLayout {
748
843
  this.removeHeaderOrderFieldIfUnused(previousTitleField);
749
844
  return this;
750
845
  }
751
- /** Sets the record description field in `header_edits`. */
752
846
  setDescriptionField(field) {
753
847
  const previousDescriptionField = this._headerEdits.newDescriptionField;
754
848
  const nextDescriptionField = this.headerFieldApiName(field);
755
849
  this._headerEdits.newDescriptionField = nextDescriptionField;
756
- if (field != null)
757
- this._headerEdits.descriptionFieldDeleted = false;
850
+ this._headerEdits.descriptionFieldDeleted = false;
758
851
  insertUniqueAt(this.ensureHeaderFieldsOrder(), nextDescriptionField, 'end');
759
852
  this.removeHeaderOrderFieldIfUnused(previousDescriptionField);
760
853
  return this;
@@ -830,20 +923,19 @@ export class CustomObjectPageLayout {
830
923
  'pass a Field instance or { field }.');
831
924
  }
832
925
  normalizeSectionForMutation(input) {
926
+ if (input.type !== 'fields_section')
927
+ return { ...input };
833
928
  this.assertFieldSectionBelongsToLayout(input.section);
834
- const sectionId = input.section.getSectionId();
835
- if (inputFieldsSectionHasKey(input)) {
836
- throw new Error('CustomObjectPageLayout fields_section key is not supported. Omit key.');
837
- }
929
+ const sectionKey = input.key ?? input.section.getSectionId();
838
930
  const name = input.name ?? input.section.getName();
839
- const normalized = {
931
+ const normalized = copySectionOptions({
840
932
  name,
841
933
  type: 'fields_section',
842
- fields: input.fields.map((field) => this.normalizeSectionFieldForMutation(input.section, sectionId, field)),
843
- };
934
+ fields: input.fields.map((field) => this.normalizeSectionFieldForMutation(input.section, sectionKey, field)),
935
+ }, input);
844
936
  if (input.layout != null)
845
937
  normalized.layout = input.layout;
846
- return attachFieldSectionId(normalized, sectionId);
938
+ return attachLayoutSectionKey(normalized, sectionKey);
847
939
  }
848
940
  normalizeTabForMutation(tab) {
849
941
  if (tab.type === 'custom_tab')
@@ -885,6 +977,26 @@ export class CustomObjectPageLayout {
885
977
  `"${section.getModelApiName()}", but the layout belongs to "${this._customObjectApiName}".`);
886
978
  }
887
979
  }
980
+ rememberFieldSectionLayoutSection(section, tabKey, sectionKey) {
981
+ if (sectionKey == null)
982
+ return;
983
+ addFieldSectionLayoutSection(this._fieldSectionLayoutSections, section.getSectionId(), {
984
+ tabKey,
985
+ sectionKey,
986
+ });
987
+ }
988
+ layoutSectionKeyForFieldSection(tabKey, section) {
989
+ const entries = this._fieldSectionLayoutSections.get(section.getSectionId()) ?? [];
990
+ const matches = tabKey == null ? entries : entries.filter((entry) => entry.tabKey === tabKey);
991
+ const sectionKeys = [...new Set(matches.map((entry) => entry.sectionKey))];
992
+ if (sectionKeys.length === 0)
993
+ return undefined;
994
+ if (sectionKeys.length === 1)
995
+ return sectionKeys[0];
996
+ const scope = tabKey == null ? '' : ` in tab "${tabKey}"`;
997
+ throw new Error(`CustomObjectPageLayout field section "${section.getSectionId()}" maps to multiple layout sections${scope}: ` +
998
+ `${sectionKeys.join(', ')}. Pass sectionKey to choose one.`);
999
+ }
888
1000
  requireOneFieldContainer(tabKey, sectionKey, fallbackFieldSection) {
889
1001
  const containers = this.findFieldContainers({
890
1002
  tabKey,
@@ -904,8 +1016,8 @@ export class CustomObjectPageLayout {
904
1016
  `"${fallbackFieldSection.getModelApiName()}", but the layout belongs to "${this._customObjectApiName}".`);
905
1017
  }
906
1018
  const container = unkeyedContainers[0];
907
- attachFieldSectionId(container.section, fallbackFieldSection.getSectionId());
908
- container.sectionKey = fallbackFieldSection.getSectionId();
1019
+ attachLayoutSectionKey(container.section, sectionKey);
1020
+ container.sectionKey = sectionKey;
909
1021
  container.label = `${container.tabKey}.${container.sectionKey}`;
910
1022
  return container;
911
1023
  }
@@ -932,6 +1044,8 @@ export class CustomObjectPageLayout {
932
1044
  if (filter.tabKey != null && tab.key !== filter.tabKey)
933
1045
  continue;
934
1046
  for (const [index, section] of tab.sections.entries()) {
1047
+ if (!isSerializedFieldsSection(section))
1048
+ continue;
935
1049
  const sectionKey = layoutSectionKey(section);
936
1050
  if (filter.sectionKey != null && !layoutSectionMatches(section, filter.sectionKey))
937
1051
  continue;
@@ -949,6 +1063,8 @@ export class CustomObjectPageLayout {
949
1063
  if (filter.tabKey != null && tabKey !== filter.tabKey)
950
1064
  continue;
951
1065
  for (const [index, section] of (tabEdit.newSections ?? []).entries()) {
1066
+ if (!isSerializedFieldsSection(section))
1067
+ continue;
952
1068
  const sectionKey = layoutSectionKey(section);
953
1069
  if (filter.sectionKey != null && !layoutSectionMatches(section, filter.sectionKey))
954
1070
  continue;
@@ -1057,8 +1173,6 @@ export class CustomObjectPageLayout {
1057
1173
  return normalized;
1058
1174
  }
1059
1175
  headerFieldApiName(field) {
1060
- if (field == null)
1061
- return null;
1062
1176
  if (typeof field === 'string')
1063
1177
  return field;
1064
1178
  this.assertFieldBelongsToLayout(field);