@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
@@ -14,6 +14,35 @@ 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');
18
+ function isCustomObjectFieldSection(value) {
19
+ return value instanceof CustomObjectFieldSection;
20
+ }
21
+ function attachFieldSectionId(section, sectionId) {
22
+ Object.defineProperty(section, FIELD_SECTION_ID, {
23
+ value: sectionId,
24
+ enumerable: false,
25
+ configurable: true,
26
+ });
27
+ return section;
28
+ }
29
+ function layoutSectionKey(section) {
30
+ return section[FIELD_SECTION_ID];
31
+ }
32
+ function layoutSectionMatches(section, sectionKey) {
33
+ return layoutSectionKey(section) === sectionKey;
34
+ }
35
+ function inputFieldsSectionHasKey(input) {
36
+ const record = input;
37
+ return Object.prototype.hasOwnProperty.call(record, 'key') && record['key'] != null;
38
+ }
39
+ function assertNoSerializedSectionKey(section) {
40
+ const record = section;
41
+ if (Object.prototype.hasOwnProperty.call(record, 'key') && record['key'] != null) {
42
+ throw new Error('CustomObjectPageLayout loaded fields_section key is not supported. ' +
43
+ 'Remove key and target the section with CustomObjectFieldSection metadata.');
44
+ }
45
+ }
17
46
  function assertSameObject(customObject, field, sectionKey) {
18
47
  if (field.getCustomObjectApiName() !== customObject.getApiName()) {
19
48
  throw new Error(`CustomObjectPageLayout fields_section "${sectionKey}" references field "${field.getApiName()}" ` +
@@ -26,6 +55,21 @@ function assertSectionBelongsTo(customObject, section) {
26
55
  `but the layout belongs to "${customObject.getApiName()}".`);
27
56
  }
28
57
  }
58
+ function requireFieldSectionOption(value, label) {
59
+ if (isCustomObjectFieldSection(value))
60
+ return value;
61
+ if (typeof value === 'string') {
62
+ throw new TypeError(`${label} must be a CustomObjectFieldSection object; received raw section id "${value}". ` +
63
+ 'Load/include the existing field section first.');
64
+ }
65
+ throw new TypeError(`${label} must be a CustomObjectFieldSection object.`);
66
+ }
67
+ function assertOptionAbsent(options, key, message) {
68
+ if (options == null || typeof options !== 'object')
69
+ return;
70
+ if (Object.prototype.hasOwnProperty.call(options, key))
71
+ throw new TypeError(message);
72
+ }
29
73
  function toSerializedSectionField(fieldRqlName, options = {}) {
30
74
  const normalized = { fieldRqlName };
31
75
  if (options.hidden != null)
@@ -52,17 +96,19 @@ function normalizeSectionField(customObject, section, sectionKey, entry) {
52
96
  }
53
97
  function normalizeFieldsSection(customObject, input) {
54
98
  assertSectionBelongsTo(customObject, input.section);
55
- const key = input.key ?? input.section.getSectionId();
99
+ const sectionId = input.section.getSectionId();
100
+ if (inputFieldsSectionHasKey(input)) {
101
+ throw new Error('CustomObjectPageLayout fields_section key is not supported. Omit key.');
102
+ }
56
103
  const name = input.name ?? input.section.getName();
57
104
  const normalized = {
58
- key,
59
105
  name,
60
106
  type: 'fields_section',
61
- fields: input.fields.map((field) => normalizeSectionField(customObject, input.section, key, field)),
107
+ fields: input.fields.map((field) => normalizeSectionField(customObject, input.section, sectionId, field)),
62
108
  };
63
109
  if (input.layout != null)
64
110
  normalized.layout = input.layout;
65
- return normalized;
111
+ return attachFieldSectionId(normalized, sectionId);
66
112
  }
67
113
  function normalizeTab(customObject, tab) {
68
114
  if (tab.type === 'custom_tab')
@@ -115,6 +161,35 @@ function cloneJson(value) {
115
161
  return value;
116
162
  return JSON.parse(JSON.stringify(value));
117
163
  }
164
+ const HEADER_EDIT_KEYS = new Set([
165
+ 'newTitleField',
166
+ 'titleFieldDeleted',
167
+ 'newDescriptionField',
168
+ 'descriptionFieldDeleted',
169
+ 'systemFieldEdits',
170
+ 'newFields',
171
+ 'headerFieldsOrder',
172
+ 'buttons',
173
+ ]);
174
+ const SYSTEM_HEADER_FIELD_EDIT_KEYS = new Set(['newRqlField', 'deleted']);
175
+ const NEW_HEADER_FIELD_KEYS = new Set(['rqlName', 'key', 'canBeDeleted', 'canBeChanged', 'canBeMoved']);
176
+ function assertKnownKeys(record, allowedKeys, label, hint) {
177
+ for (const key of Object.keys(record)) {
178
+ if (!allowedKeys.has(key)) {
179
+ throw new Error(`${label} contains unsupported key "${key}". ${hint}`);
180
+ }
181
+ }
182
+ }
183
+ function normalizeHeaderEdits(headerEdits) {
184
+ assertKnownKeys(headerEdits, HEADER_EDIT_KEYS, 'CustomObjectPageLayout headerEdits', 'Use addHeaderField(), setTitleField(), setDescriptionField(), or the HeaderEdits keys documented by the SDK.');
185
+ for (const [key, edit] of Object.entries(headerEdits.systemFieldEdits ?? {})) {
186
+ assertKnownKeys(edit, SYSTEM_HEADER_FIELD_EDIT_KEYS, `CustomObjectPageLayout headerEdits.systemFieldEdits.${key}`, 'Use newRqlField and deleted for system header field edits.');
187
+ }
188
+ for (const [index, field] of (headerEdits.newFields ?? []).entries()) {
189
+ assertKnownKeys(field, NEW_HEADER_FIELD_KEYS, `CustomObjectPageLayout headerEdits.newFields[${index}]`, 'Use { rqlName, key } entries, or call addHeaderField(field).');
190
+ }
191
+ return headerEdits;
192
+ }
118
193
  function isTabWithSections(tab) {
119
194
  return tab.type === 'tab_with_sections';
120
195
  }
@@ -140,6 +215,48 @@ function fieldRqlNameOf(entry) {
140
215
  const fieldRqlName = record['fieldRqlName'] ?? record['field_rql_name'];
141
216
  return typeof fieldRqlName === 'string' ? fieldRqlName : undefined;
142
217
  }
218
+ function explicitFieldSectionId(entry) {
219
+ if (entry == null || typeof entry !== 'object')
220
+ return undefined;
221
+ const record = entry;
222
+ const fieldSectionId = record['fieldSection'] ?? record['field_section'];
223
+ return typeof fieldSectionId === 'string' ? fieldSectionId : undefined;
224
+ }
225
+ function inferredSectionIdFromFields(section, resolveFieldSectionId) {
226
+ const sectionIds = new Set();
227
+ for (const field of section.fields ?? []) {
228
+ const explicitSectionId = explicitFieldSectionId(field);
229
+ if (explicitSectionId != null) {
230
+ sectionIds.add(explicitSectionId);
231
+ continue;
232
+ }
233
+ const fieldApiName = fieldRqlNameOf(field);
234
+ if (fieldApiName == null || resolveFieldSectionId == null)
235
+ continue;
236
+ const resolvedSectionId = resolveFieldSectionId(fieldApiName);
237
+ if (resolvedSectionId != null)
238
+ sectionIds.add(resolvedSectionId);
239
+ }
240
+ return sectionIds.size === 1 ? [...sectionIds][0] : undefined;
241
+ }
242
+ function annotateLoadedFieldsSection(section, resolveFieldSectionId) {
243
+ assertNoSerializedSectionKey(section);
244
+ const sectionId = layoutSectionKey(section) ?? inferredSectionIdFromFields(section, resolveFieldSectionId);
245
+ return sectionId == null ? section : attachFieldSectionId(section, sectionId);
246
+ }
247
+ function annotateLoadedTabEdits(tabEdits, resolveFieldSectionId) {
248
+ for (const tab of tabEdits.newTabs ?? []) {
249
+ if (!isTabWithSections(tab))
250
+ continue;
251
+ tab.sections = tab.sections.map((section) => annotateLoadedFieldsSection(section, resolveFieldSectionId));
252
+ }
253
+ for (const tabEdit of Object.values(tabEdits.systemTabEdits ?? {})) {
254
+ if (tabEdit.newSections != null) {
255
+ tabEdit.newSections = tabEdit.newSections.map((section) => annotateLoadedFieldsSection(section, resolveFieldSectionId));
256
+ }
257
+ }
258
+ return tabEdits;
259
+ }
143
260
  function removeIndexes(items, indexes) {
144
261
  for (const index of [...indexes].sort((a, b) => b - a)) {
145
262
  items.splice(index, 1);
@@ -213,7 +330,6 @@ function ensureArrayProperty(record, key, label) {
213
330
  * newTabs: [{
214
331
  * key: 'member', name: 'Member', type: 'tab_with_sections',
215
332
  * sections: [{
216
- * key: 'profile',
217
333
  * name: 'Profile',
218
334
  * type: 'fields_section',
219
335
  * section: profileSection,
@@ -252,7 +368,7 @@ export class CustomObjectPageLayout {
252
368
  this._customObjectApiName = customObject.getApiName();
253
369
  this._name = props.name;
254
370
  this._tabEdits = normalizeTabEdits(customObject, props.tabEdits ?? {});
255
- this._headerEdits = props.headerEdits ?? {};
371
+ this._headerEdits = normalizeHeaderEdits(props.headerEdits ?? {});
256
372
  this._visibilityConditions = props.visibilityConditions ?? {};
257
373
  this._blueprintKey = props.blueprintKey;
258
374
  customObject._register(this);
@@ -268,14 +384,14 @@ export class CustomObjectPageLayout {
268
384
  return getExistingManifestContext(options).loadPageLayout(layoutIdOrApiName, options);
269
385
  }
270
386
  /** @internal Hydrates a page layout from existing manifest wire JSON. */
271
- static _fromExistingComponent(customObject, component) {
387
+ static _fromExistingComponent(customObject, component, resolveFieldSectionId) {
272
388
  const layout = Object.create(CustomObjectPageLayout.prototype);
273
389
  Object.assign(layout, {
274
390
  _layoutId: requireStringComponentField(component, 'layout_id', CustomObjectPageLayout.componentType),
275
391
  _apiName: requireStringComponentField(component, 'api_name', CustomObjectPageLayout.componentType),
276
392
  _customObjectApiName: requireStringComponentField(component, 'object_rql_name', CustomObjectPageLayout.componentType),
277
393
  _name: requireStringComponentField(component, 'name', CustomObjectPageLayout.componentType),
278
- _tabEdits: cloneJson(component['tab_edits'] ?? {}),
394
+ _tabEdits: annotateLoadedTabEdits(cloneJson(component['tab_edits'] ?? {}), resolveFieldSectionId),
279
395
  _headerEdits: cloneJson(component['header_edits'] ?? {}),
280
396
  _visibilityConditions: cloneJson(component['visibility_conditions'] ?? {}),
281
397
  _blueprintKey: component['blueprint_key'],
@@ -313,17 +429,32 @@ export class CustomObjectPageLayout {
313
429
  /**
314
430
  * Adds multiple fields to an existing layout section.
315
431
  *
316
- * Fields are inserted in the order provided. If a field does not already have
317
- * field-section metadata, pass `options.fieldSection` so the corresponding
318
- * `CUSTOM_OBJECT_FIELD_SECTION` component is registered in the manifest.
432
+ * Fields are inserted in the order provided. For custom layout sections,
433
+ * pass `options.section` so TypeScript and runtime validation agree on the
434
+ * target `CUSTOM_OBJECT_FIELD_SECTION`.
319
435
  */
320
436
  addFields(fields, options) {
321
437
  if (fields.length === 0)
322
438
  return this;
439
+ let fieldSectionForMutation;
323
440
  const container = options.systemSection === true ?
324
- this.ensureSystemSectionFieldContainer(options.tabKey, options.sectionKey)
325
- : this.requireOneFieldContainer(options.tabKey, options.sectionKey);
326
- const entries = fields.map((field) => this.serializeFieldForMutation(field, options.sectionKey, options));
441
+ (() => {
442
+ assertOptionAbsent(options, 'section', 'CustomObjectPageLayout addField section is only supported for custom layout sections.');
443
+ assertOptionAbsent(options, 'fieldSection', 'CustomObjectPageLayout addField fieldSection is no longer supported. ' +
444
+ 'Use sectionKey only for systemSection: true.');
445
+ return this.ensureSystemSectionFieldContainer(options.tabKey, options.sectionKey);
446
+ })()
447
+ : (() => {
448
+ assertOptionAbsent(options, 'sectionKey', 'CustomObjectPageLayout addField sectionKey is only supported with systemSection: true. ' +
449
+ 'Pass section: CustomObjectFieldSection for custom layout sections.');
450
+ assertOptionAbsent(options, 'fieldSection', 'CustomObjectPageLayout addField fieldSection is no longer supported. ' +
451
+ 'Pass section: CustomObjectFieldSection instead.');
452
+ const section = requireFieldSectionOption(options.section, 'CustomObjectPageLayout addField section');
453
+ this.assertFieldSectionBelongsToLayout(section);
454
+ fieldSectionForMutation = section;
455
+ return this.requireOneFieldContainer(options.tabKey, section.getSectionId(), section);
456
+ })();
457
+ const entries = fields.map((field) => this.serializeFieldForMutation(field, container.sectionKey ?? container.label, options, fieldSectionForMutation));
327
458
  if (options.allowDuplicate !== true) {
328
459
  const existing = new Set(container.fields.map((entry) => fieldRqlNameOf(entry)).filter(Boolean));
329
460
  const seen = new Set();
@@ -349,9 +480,23 @@ export class CustomObjectPageLayout {
349
480
  const fieldApiName = typeof field === 'string' ? field : field.getApiName();
350
481
  if (isField(field))
351
482
  this.assertFieldBelongsToLayout(field);
483
+ let sectionKey;
484
+ if (options.systemSection === true) {
485
+ assertOptionAbsent(options, 'section', 'CustomObjectPageLayout removeField section is only supported for custom layout sections.');
486
+ sectionKey = options.sectionKey;
487
+ }
488
+ else {
489
+ assertOptionAbsent(options, 'sectionKey', 'CustomObjectPageLayout removeField sectionKey is only supported with systemSection: true. ' +
490
+ 'Pass section: CustomObjectFieldSection for custom layout sections.');
491
+ if (options.section != null) {
492
+ const section = requireFieldSectionOption(options.section, 'CustomObjectPageLayout removeField section');
493
+ this.assertFieldSectionBelongsToLayout(section);
494
+ sectionKey = section.getSectionId();
495
+ }
496
+ }
352
497
  const containers = this.findFieldContainers({
353
498
  tabKey: options.tabKey,
354
- sectionKey: options.sectionKey,
499
+ sectionKey,
355
500
  systemSectionOnly: options.systemSection === true,
356
501
  });
357
502
  const matches = [];
@@ -400,10 +545,8 @@ export class CustomObjectPageLayout {
400
545
  if (options.systemTab === true) {
401
546
  const tabEdit = this.ensureSystemTabEdit(options.tabKey);
402
547
  const sections = ensureArrayProperty(tabEdit, 'newSections', `systemTabEdits.${options.tabKey}.newSections`);
403
- this.assertSectionKeyAvailable(sections, normalized.key, options.tabKey, options.allowDuplicate);
548
+ this.assertSectionKeyAvailable(sections, layoutSectionKey(normalized), options.tabKey, options.allowDuplicate);
404
549
  insertAt(sections, normalized, options.position);
405
- if (tabEdit.sectionsOrder != null)
406
- insertAt(tabEdit.sectionsOrder, normalized.key, options.position);
407
550
  return this;
408
551
  }
409
552
  const tab = this.findNewTabWithSections(options.tabKey);
@@ -411,26 +554,26 @@ export class CustomObjectPageLayout {
411
554
  throw new Error(`CustomObjectPageLayout could not find new tab "${options.tabKey}". ` +
412
555
  'Pass systemTab: true to add a section to a built-in system tab.');
413
556
  }
414
- this.assertSectionKeyAvailable(tab.sections, normalized.key, options.tabKey, options.allowDuplicate);
557
+ this.assertSectionKeyAvailable(tab.sections, layoutSectionKey(normalized), options.tabKey, options.allowDuplicate);
415
558
  insertAt(tab.sections, normalized, options.position);
416
559
  return this;
417
560
  }
418
- /**
419
- * Removes a section from `newTabs` / `systemTabEdits.newSections`.
420
- *
421
- * Pass `systemSection: true` with `tabKey` to delete a built-in section via
422
- * `systemSectionEdits[sectionKey].deleted`.
423
- */
424
- removeSection(sectionKey, options = {}) {
561
+ removeSection(sectionOrKey, options = {}) {
425
562
  if (options.systemSection === true) {
563
+ if (typeof sectionOrKey !== 'string') {
564
+ throw new TypeError('CustomObjectPageLayout removeSection with systemSection: true requires a system section key.');
565
+ }
426
566
  if (options.tabKey == null) {
427
567
  throw new Error('CustomObjectPageLayout removeSection with systemSection: true requires tabKey.');
428
568
  }
429
- const sectionEdit = this.ensureSystemSectionEdit(options.tabKey, sectionKey);
569
+ const sectionEdit = this.ensureSystemSectionEdit(options.tabKey, sectionOrKey);
430
570
  sectionEdit.deleted = true;
431
- this.removeSectionVisibility(sectionKey);
571
+ this.removeSectionVisibility(sectionOrKey);
432
572
  return this;
433
573
  }
574
+ const section = requireFieldSectionOption(sectionOrKey, 'CustomObjectPageLayout removeSection section');
575
+ this.assertFieldSectionBelongsToLayout(section);
576
+ const sectionKey = section.getSectionId();
434
577
  const matches = [];
435
578
  for (const tab of this._tabEdits.newTabs ?? []) {
436
579
  if (!isTabWithSections(tab))
@@ -438,16 +581,16 @@ export class CustomObjectPageLayout {
438
581
  if (options.tabKey != null && tab.key !== options.tabKey)
439
582
  continue;
440
583
  tab.sections.forEach((section, index) => {
441
- if (section.key === sectionKey)
442
- matches.push({ sections: tab.sections, index, tabKey: tab.key });
584
+ if (layoutSectionMatches(section, sectionKey))
585
+ matches.push({ sections: tab.sections, section, index, tabKey: tab.key });
443
586
  });
444
587
  }
445
588
  for (const [tabKey, tabEdit] of Object.entries(this._tabEdits.systemTabEdits ?? {})) {
446
589
  if (options.tabKey != null && tabKey !== options.tabKey)
447
590
  continue;
448
591
  (tabEdit.newSections ?? []).forEach((section, index) => {
449
- if (section.key === sectionKey)
450
- matches.push({ sections: tabEdit.newSections, index, tabEdit, tabKey });
592
+ if (layoutSectionMatches(section, sectionKey))
593
+ matches.push({ sections: tabEdit.newSections, section, index, tabEdit, tabKey });
451
594
  });
452
595
  }
453
596
  if (matches.length === 0) {
@@ -466,11 +609,11 @@ export class CustomObjectPageLayout {
466
609
  byArray.set(match.sections, indexes);
467
610
  if (match.tabEdit != null)
468
611
  removeKeyFromOrder(match.tabEdit.sectionsOrder, sectionKey);
612
+ this.removeSectionVisibility(sectionKey);
469
613
  }
470
614
  for (const [sections, indexes] of byArray.entries()) {
471
615
  removeIndexes(sections, indexes);
472
616
  }
473
- this.removeSectionVisibility(sectionKey);
474
617
  return this;
475
618
  }
476
619
  /** Adds a new tab to `tab_edits.newTabs`. */
@@ -571,9 +714,9 @@ export class CustomObjectPageLayout {
571
714
  this._headerEdits.descriptionFieldDeleted = true;
572
715
  return this;
573
716
  }
574
- serializeFieldForMutation(field, fallbackSectionKey, options) {
717
+ serializeFieldForMutation(field, fallbackSectionKey, options, fieldSection) {
575
718
  this.assertFieldBelongsToLayout(field);
576
- this.assignFieldSectionForMutation(field, fallbackSectionKey, options.fieldSection);
719
+ this.assignFieldSectionForMutation(field, fallbackSectionKey, fieldSection);
577
720
  return toSerializedSectionField(field.getApiName(), options);
578
721
  }
579
722
  normalizeSectionFieldForMutation(section, sectionKey, entry) {
@@ -591,21 +734,20 @@ export class CustomObjectPageLayout {
591
734
  'pass a Field instance or { field }.');
592
735
  }
593
736
  normalizeSectionForMutation(input) {
594
- if (input.section.getModelApiName() !== this._customObjectApiName) {
595
- throw new Error(`CustomObjectPageLayout section "${input.section.getSectionId()}" belongs to ` +
596
- `"${input.section.getModelApiName()}", but the layout belongs to "${this._customObjectApiName}".`);
737
+ this.assertFieldSectionBelongsToLayout(input.section);
738
+ const sectionId = input.section.getSectionId();
739
+ if (inputFieldsSectionHasKey(input)) {
740
+ throw new Error('CustomObjectPageLayout fields_section key is not supported. Omit key.');
597
741
  }
598
- const key = input.key ?? input.section.getSectionId();
599
742
  const name = input.name ?? input.section.getName();
600
743
  const normalized = {
601
- key,
602
744
  name,
603
745
  type: 'fields_section',
604
- fields: input.fields.map((field) => this.normalizeSectionFieldForMutation(input.section, key, field)),
746
+ fields: input.fields.map((field) => this.normalizeSectionFieldForMutation(input.section, sectionId, field)),
605
747
  };
606
748
  if (input.layout != null)
607
749
  normalized.layout = input.layout;
608
- return normalized;
750
+ return attachFieldSectionId(normalized, sectionId);
609
751
  }
610
752
  normalizeTabForMutation(tab) {
611
753
  if (tab.type === 'custom_tab')
@@ -619,7 +761,7 @@ export class CustomObjectPageLayout {
619
761
  const rawFieldSection = fieldSection;
620
762
  if (rawFieldSection != null) {
621
763
  if (typeof rawFieldSection === 'string') {
622
- throw new TypeError(`CustomObjectPageLayout fieldSection must be a CustomObjectFieldSection object; ` +
764
+ throw new TypeError(`CustomObjectPageLayout section must be a CustomObjectFieldSection object; ` +
623
765
  `received raw section id "${rawFieldSection}". Load/include the existing field section first.`);
624
766
  }
625
767
  if (rawFieldSection.getModelApiName() !== this._customObjectApiName) {
@@ -631,7 +773,7 @@ export class CustomObjectPageLayout {
631
773
  }
632
774
  if (field.getSectionId() == null) {
633
775
  throw new Error(`CustomObjectPageLayout field "${field.getApiName()}" is missing field-section metadata. ` +
634
- `Construct the field with a CustomObjectFieldSection or pass fieldSection when adding it to ` +
776
+ `Construct the field with a CustomObjectFieldSection or pass section when adding it to ` +
635
777
  `section "${fallbackSectionKey}".`);
636
778
  }
637
779
  }
@@ -641,17 +783,45 @@ export class CustomObjectPageLayout {
641
783
  `"${field.getCustomObjectApiName()}", but the layout belongs to "${this._customObjectApiName}".`);
642
784
  }
643
785
  }
644
- requireOneFieldContainer(tabKey, sectionKey) {
786
+ assertFieldSectionBelongsToLayout(section) {
787
+ if (section.getModelApiName() !== this._customObjectApiName) {
788
+ throw new Error(`CustomObjectPageLayout field section "${section.getSectionId()}" belongs to ` +
789
+ `"${section.getModelApiName()}", but the layout belongs to "${this._customObjectApiName}".`);
790
+ }
791
+ }
792
+ requireOneFieldContainer(tabKey, sectionKey, fallbackFieldSection) {
645
793
  const containers = this.findFieldContainers({
646
794
  tabKey,
647
795
  sectionKey,
648
796
  systemSectionOnly: false,
649
797
  });
650
798
  if (containers.length === 0) {
651
- const prefix = tabKey != null ? `${tabKey}.` : '';
652
- throw new Error(`CustomObjectPageLayout could not find section "${prefix}${sectionKey}".`);
799
+ if (sectionKey != null && fallbackFieldSection != null) {
800
+ const unkeyedContainers = this.findFieldContainers({
801
+ tabKey,
802
+ sectionKey: undefined,
803
+ systemSectionOnly: false,
804
+ }).filter((container) => container.section != null && container.sectionKey == null);
805
+ if (unkeyedContainers.length === 1) {
806
+ if (fallbackFieldSection.getModelApiName() !== this._customObjectApiName) {
807
+ throw new Error(`CustomObjectPageLayout field section "${fallbackFieldSection.getSectionId()}" belongs to ` +
808
+ `"${fallbackFieldSection.getModelApiName()}", but the layout belongs to "${this._customObjectApiName}".`);
809
+ }
810
+ const container = unkeyedContainers[0];
811
+ attachFieldSectionId(container.section, fallbackFieldSection.getSectionId());
812
+ container.sectionKey = fallbackFieldSection.getSectionId();
813
+ container.label = `${container.tabKey}.${container.sectionKey}`;
814
+ return container;
815
+ }
816
+ }
817
+ const scope = sectionKey == null ? 'a field section' : (`section "${tabKey != null ? `${tabKey}.` : ''}${sectionKey}"`);
818
+ throw new Error(`CustomObjectPageLayout could not find ${scope}.`);
653
819
  }
654
820
  if (containers.length > 1) {
821
+ if (sectionKey == null) {
822
+ throw new Error(`CustomObjectPageLayout matched multiple sections: ` +
823
+ `${containers.map((container) => container.label).join(', ')}. Pass section to choose one.`);
824
+ }
655
825
  throw new Error(`CustomObjectPageLayout section "${sectionKey}" matched multiple sections: ` +
656
826
  `${containers.map((container) => container.label).join(', ')}. Pass tabKey to choose one.`);
657
827
  }
@@ -665,28 +835,34 @@ export class CustomObjectPageLayout {
665
835
  continue;
666
836
  if (filter.tabKey != null && tab.key !== filter.tabKey)
667
837
  continue;
668
- for (const section of tab.sections) {
669
- if (filter.sectionKey != null && section.key !== filter.sectionKey)
838
+ for (const [index, section] of tab.sections.entries()) {
839
+ const sectionKey = layoutSectionKey(section);
840
+ if (filter.sectionKey != null && !layoutSectionMatches(section, filter.sectionKey))
670
841
  continue;
842
+ const label = sectionKey == null ? `${tab.key}.sections[${index}]` : `${tab.key}.${sectionKey}`;
671
843
  containers.push({
672
844
  tabKey: tab.key,
673
- sectionKey: section.key,
674
- fields: ensureArrayProperty(section, 'fields', `newTabs.${tab.key}.sections.${section.key}.fields`),
675
- label: `${tab.key}.${section.key}`,
845
+ sectionKey,
846
+ section,
847
+ fields: ensureArrayProperty(section, 'fields', `newTabs.${label}.fields`),
848
+ label,
676
849
  });
677
850
  }
678
851
  }
679
852
  for (const [tabKey, tabEdit] of Object.entries(this._tabEdits.systemTabEdits ?? {})) {
680
853
  if (filter.tabKey != null && tabKey !== filter.tabKey)
681
854
  continue;
682
- for (const section of tabEdit.newSections ?? []) {
683
- if (filter.sectionKey != null && section.key !== filter.sectionKey)
855
+ for (const [index, section] of (tabEdit.newSections ?? []).entries()) {
856
+ const sectionKey = layoutSectionKey(section);
857
+ if (filter.sectionKey != null && !layoutSectionMatches(section, filter.sectionKey))
684
858
  continue;
859
+ const label = sectionKey == null ? `${tabKey}.newSections[${index}]` : `${tabKey}.${sectionKey}`;
685
860
  containers.push({
686
861
  tabKey,
687
- sectionKey: section.key,
688
- fields: ensureArrayProperty(section, 'fields', `systemTabEdits.${tabKey}.newSections.${section.key}.fields`),
689
- label: `${tabKey}.${section.key}`,
862
+ sectionKey,
863
+ section,
864
+ fields: ensureArrayProperty(section, 'fields', `systemTabEdits.${label}.fields`),
865
+ label,
690
866
  });
691
867
  }
692
868
  }
@@ -717,6 +893,9 @@ export class CustomObjectPageLayout {
717
893
  if (tabKey == null) {
718
894
  throw new Error('CustomObjectPageLayout addField with systemSection: true requires tabKey.');
719
895
  }
896
+ if (sectionKey == null) {
897
+ throw new Error('CustomObjectPageLayout addField with systemSection: true requires sectionKey.');
898
+ }
720
899
  const sectionEdit = this.ensureSystemSectionEdit(tabKey, sectionKey);
721
900
  if (sectionEdit.deleted === true) {
722
901
  throw new Error(`CustomObjectPageLayout system section "${tabKey}.${sectionKey}" is marked deleted.`);
@@ -755,7 +934,7 @@ export class CustomObjectPageLayout {
755
934
  assertSectionKeyAvailable(sections, sectionKey, tabKey, allowDuplicate) {
756
935
  if (allowDuplicate === true)
757
936
  return;
758
- if (sections.some((section) => section.key === sectionKey)) {
937
+ if (sectionKey != null && sections.some((section) => layoutSectionMatches(section, sectionKey))) {
759
938
  throw new Error(`CustomObjectPageLayout tab "${tabKey}" already contains section "${sectionKey}".`);
760
939
  }
761
940
  }
@@ -827,12 +1006,13 @@ export class CustomObjectPageLayout {
827
1006
  * tabs or sections. Always uses `apiName: 'default'`.
828
1007
  *
829
1008
  * @param customObject - The custom object to create the layout for.
830
- * @param props - Optional. Pass `fields` to populate the General section.
1009
+ * @param props - Optional. Pass `section` and `fields` to populate the General section.
831
1010
  * @returns A new `CustomObjectPageLayout` registered with the manifest.
832
1011
  *
833
1012
  * @example
834
1013
  * ```ts
835
1014
  * CustomObjectPageLayout.basic(memberObj, {
1015
+ * section: profileSection,
836
1016
  * fields: [memberFirstName, memberEmail, memberStatusField],
837
1017
  * });
838
1018
  * ```
@@ -840,11 +1020,29 @@ export class CustomObjectPageLayout {
840
1020
  static basic(customObject, props = {}) {
841
1021
  const coApiName = customObject.getApiName();
842
1022
  const coName = customObject.getName();
843
- const sectionPrefix = coApiName.replace(/__c$/, '').replace(/[^a-zA-Z0-9]+/g, '_');
844
- const generalSection = new CustomObjectFieldSection(customObject, {
845
- sectionId: `sec_${sectionPrefix}_general`,
846
- name: 'General',
847
- });
1023
+ const fields = props.fields ?? [];
1024
+ const generalSection = props.section ??
1025
+ (() => {
1026
+ const alreadySectionedField = fields.find((field) => field.getSectionId() != null);
1027
+ if (alreadySectionedField != null) {
1028
+ throw new Error(`CustomObjectPageLayout.basic field "${alreadySectionedField.getApiName()}" already belongs to ` +
1029
+ `field section "${alreadySectionedField.getSectionId()}". Pass that CustomObjectFieldSection ` +
1030
+ 'as section so basic() does not reassign the field.');
1031
+ }
1032
+ const sectionPrefix = coApiName.replace(/__c$/, '').replace(/[^a-zA-Z0-9]+/g, '_');
1033
+ return new CustomObjectFieldSection(customObject, {
1034
+ sectionId: `sec_${sectionPrefix}_general`,
1035
+ name: 'General',
1036
+ });
1037
+ })();
1038
+ assertSectionBelongsTo(customObject, generalSection);
1039
+ for (const field of fields) {
1040
+ const fieldSectionId = field.getSectionId();
1041
+ if (fieldSectionId != null && fieldSectionId !== generalSection.getSectionId()) {
1042
+ throw new Error(`CustomObjectPageLayout.basic field "${field.getApiName()}" belongs to field section ` +
1043
+ `"${fieldSectionId}", but basic() was given section "${generalSection.getSectionId()}".`);
1044
+ }
1045
+ }
848
1046
  return new CustomObjectPageLayout(customObject, {
849
1047
  apiName: 'default',
850
1048
  name: `${coName} Layout`,
@@ -856,11 +1054,10 @@ export class CustomObjectPageLayout {
856
1054
  type: 'tab_with_sections',
857
1055
  sections: [
858
1056
  {
859
- key: 'general',
860
1057
  name: 'General',
861
1058
  type: 'fields_section',
862
1059
  section: generalSection,
863
- fields: props.fields ?? [],
1060
+ fields,
864
1061
  },
865
1062
  ],
866
1063
  },