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