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

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 (61) 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/app.d.mts +2 -2
  7. package/lib/manifest/app.d.mts.map +1 -1
  8. package/lib/manifest/app.d.ts +2 -2
  9. package/lib/manifest/app.d.ts.map +1 -1
  10. package/lib/manifest/app.js +15 -4
  11. package/lib/manifest/app.js.map +1 -1
  12. package/lib/manifest/app.mjs +15 -4
  13. package/lib/manifest/app.mjs.map +1 -1
  14. package/lib/manifest/custom-object-field-section.d.mts +2 -2
  15. package/lib/manifest/custom-object-field-section.d.ts +2 -2
  16. package/lib/manifest/custom-object-field-section.js +2 -2
  17. package/lib/manifest/custom-object-field-section.mjs +2 -2
  18. package/lib/manifest/custom-object-page-layout.d.mts +86 -40
  19. package/lib/manifest/custom-object-page-layout.d.mts.map +1 -1
  20. package/lib/manifest/custom-object-page-layout.d.ts +86 -40
  21. package/lib/manifest/custom-object-page-layout.d.ts.map +1 -1
  22. package/lib/manifest/custom-object-page-layout.js +369 -79
  23. package/lib/manifest/custom-object-page-layout.js.map +1 -1
  24. package/lib/manifest/custom-object-page-layout.mjs +369 -79
  25. package/lib/manifest/custom-object-page-layout.mjs.map +1 -1
  26. package/lib/manifest/custom-object.d.mts +6 -0
  27. package/lib/manifest/custom-object.d.mts.map +1 -1
  28. package/lib/manifest/custom-object.d.ts +6 -0
  29. package/lib/manifest/custom-object.d.ts.map +1 -1
  30. package/lib/manifest/custom-object.js +6 -0
  31. package/lib/manifest/custom-object.js.map +1 -1
  32. package/lib/manifest/custom-object.mjs +6 -0
  33. package/lib/manifest/custom-object.mjs.map +1 -1
  34. package/lib/manifest/existing-manifest-context.d.mts +0 -1
  35. package/lib/manifest/existing-manifest-context.d.mts.map +1 -1
  36. package/lib/manifest/existing-manifest-context.d.ts +0 -1
  37. package/lib/manifest/existing-manifest-context.d.ts.map +1 -1
  38. package/lib/manifest/existing-manifest-context.js +5 -10
  39. package/lib/manifest/existing-manifest-context.js.map +1 -1
  40. package/lib/manifest/existing-manifest-context.mjs +5 -10
  41. package/lib/manifest/existing-manifest-context.mjs.map +1 -1
  42. package/lib/manifest/field.d.mts +6 -3
  43. package/lib/manifest/field.d.mts.map +1 -1
  44. package/lib/manifest/field.d.ts +6 -3
  45. package/lib/manifest/field.d.ts.map +1 -1
  46. package/lib/manifest/field.js +26 -6
  47. package/lib/manifest/field.js.map +1 -1
  48. package/lib/manifest/field.mjs +26 -6
  49. package/lib/manifest/field.mjs.map +1 -1
  50. package/package.json +1 -1
  51. package/src/lib/manifest/app.ts +19 -5
  52. package/src/lib/manifest/custom-object-field-section.ts +2 -2
  53. package/src/lib/manifest/custom-object-page-layout.ts +599 -109
  54. package/src/lib/manifest/custom-object.ts +6 -0
  55. package/src/lib/manifest/existing-manifest-context.ts +5 -12
  56. package/src/lib/manifest/field.ts +45 -9
  57. package/src/version.ts +1 -1
  58. package/version.d.mts +1 -1
  59. package/version.d.ts +1 -1
  60. package/version.js +1 -1
  61. 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,68 @@ 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
+ if (headerEdits.headerFieldsOrder == null)
192
+ return headerEdits;
193
+ return {
194
+ ...headerEdits,
195
+ headerFieldsOrder: normalizeHeaderFieldsOrder(headerEdits),
196
+ };
197
+ }
198
+ function addUniqueHeaderOrderField(order, field) {
199
+ if (typeof field !== 'string' || field.length === 0)
200
+ return;
201
+ if (!order.includes(field))
202
+ order.push(field);
203
+ }
204
+ function normalizeHeaderFieldsOrder(headerEdits) {
205
+ const customFieldRqlByKey = new Map((headerEdits.newFields ?? []).map((field) => [field.key, field.rqlName]));
206
+ const systemFieldRqlByKey = new Map(Object.entries(headerEdits.systemFieldEdits ?? {})
207
+ .filter((entry) => {
208
+ const [, edit] = entry;
209
+ return typeof edit.newRqlField === 'string' && edit.newRqlField.length > 0;
210
+ })
211
+ .map(([key, edit]) => [key, edit.newRqlField]));
212
+ const order = [];
213
+ for (const entry of headerEdits.headerFieldsOrder ?? []) {
214
+ addUniqueHeaderOrderField(order, customFieldRqlByKey.get(entry) ?? systemFieldRqlByKey.get(entry) ?? entry);
215
+ }
216
+ addUniqueHeaderOrderField(order, headerEdits.newTitleField);
217
+ addUniqueHeaderOrderField(order, headerEdits.newDescriptionField);
218
+ for (const field of headerEdits.newFields ?? []) {
219
+ addUniqueHeaderOrderField(order, field.rqlName);
220
+ }
221
+ for (const systemEdit of Object.values(headerEdits.systemFieldEdits ?? {})) {
222
+ addUniqueHeaderOrderField(order, systemEdit.newRqlField);
223
+ }
224
+ return order;
225
+ }
118
226
  function isTabWithSections(tab) {
119
227
  return tab.type === 'tab_with_sections';
120
228
  }
@@ -131,6 +239,11 @@ function insertionIndex(length, position) {
131
239
  function insertAt(items, item, position) {
132
240
  items.splice(insertionIndex(items.length, position), 0, item);
133
241
  }
242
+ function insertUniqueAt(items, item, position) {
243
+ if (item == null || item.length === 0 || items.includes(item))
244
+ return;
245
+ insertAt(items, item, position);
246
+ }
134
247
  function fieldRqlNameOf(entry) {
135
248
  if (typeof entry === 'string')
136
249
  return entry;
@@ -140,6 +253,48 @@ function fieldRqlNameOf(entry) {
140
253
  const fieldRqlName = record['fieldRqlName'] ?? record['field_rql_name'];
141
254
  return typeof fieldRqlName === 'string' ? fieldRqlName : undefined;
142
255
  }
256
+ function explicitFieldSectionId(entry) {
257
+ if (entry == null || typeof entry !== 'object')
258
+ return undefined;
259
+ const record = entry;
260
+ const fieldSectionId = record['fieldSection'] ?? record['field_section'];
261
+ return typeof fieldSectionId === 'string' ? fieldSectionId : undefined;
262
+ }
263
+ function inferredSectionIdFromFields(section, resolveFieldSectionId) {
264
+ const sectionIds = new Set();
265
+ for (const field of section.fields ?? []) {
266
+ const explicitSectionId = explicitFieldSectionId(field);
267
+ if (explicitSectionId != null) {
268
+ sectionIds.add(explicitSectionId);
269
+ continue;
270
+ }
271
+ const fieldApiName = fieldRqlNameOf(field);
272
+ if (fieldApiName == null || resolveFieldSectionId == null)
273
+ continue;
274
+ const resolvedSectionId = resolveFieldSectionId(fieldApiName);
275
+ if (resolvedSectionId != null)
276
+ sectionIds.add(resolvedSectionId);
277
+ }
278
+ return sectionIds.size === 1 ? [...sectionIds][0] : undefined;
279
+ }
280
+ function annotateLoadedFieldsSection(section, resolveFieldSectionId) {
281
+ assertNoSerializedSectionKey(section);
282
+ const sectionId = layoutSectionKey(section) ?? inferredSectionIdFromFields(section, resolveFieldSectionId);
283
+ return sectionId == null ? section : attachFieldSectionId(section, sectionId);
284
+ }
285
+ function annotateLoadedTabEdits(tabEdits, resolveFieldSectionId) {
286
+ for (const tab of tabEdits.newTabs ?? []) {
287
+ if (!isTabWithSections(tab))
288
+ continue;
289
+ tab.sections = tab.sections.map((section) => annotateLoadedFieldsSection(section, resolveFieldSectionId));
290
+ }
291
+ for (const tabEdit of Object.values(tabEdits.systemTabEdits ?? {})) {
292
+ if (tabEdit.newSections != null) {
293
+ tabEdit.newSections = tabEdit.newSections.map((section) => annotateLoadedFieldsSection(section, resolveFieldSectionId));
294
+ }
295
+ }
296
+ return tabEdits;
297
+ }
143
298
  function removeIndexes(items, indexes) {
144
299
  for (const index of [...indexes].sort((a, b) => b - a)) {
145
300
  items.splice(index, 1);
@@ -213,7 +368,6 @@ function ensureArrayProperty(record, key, label) {
213
368
  * newTabs: [{
214
369
  * key: 'member', name: 'Member', type: 'tab_with_sections',
215
370
  * sections: [{
216
- * key: 'profile',
217
371
  * name: 'Profile',
218
372
  * type: 'fields_section',
219
373
  * section: profileSection,
@@ -252,7 +406,7 @@ export class CustomObjectPageLayout {
252
406
  this._customObjectApiName = customObject.getApiName();
253
407
  this._name = props.name;
254
408
  this._tabEdits = normalizeTabEdits(customObject, props.tabEdits ?? {});
255
- this._headerEdits = props.headerEdits ?? {};
409
+ this._headerEdits = normalizeHeaderEdits(props.headerEdits ?? {});
256
410
  this._visibilityConditions = props.visibilityConditions ?? {};
257
411
  this._blueprintKey = props.blueprintKey;
258
412
  customObject._register(this);
@@ -268,14 +422,14 @@ export class CustomObjectPageLayout {
268
422
  return getExistingManifestContext(options).loadPageLayout(layoutIdOrApiName, options);
269
423
  }
270
424
  /** @internal Hydrates a page layout from existing manifest wire JSON. */
271
- static _fromExistingComponent(customObject, component) {
425
+ static _fromExistingComponent(customObject, component, resolveFieldSectionId) {
272
426
  const layout = Object.create(CustomObjectPageLayout.prototype);
273
427
  Object.assign(layout, {
274
428
  _layoutId: requireStringComponentField(component, 'layout_id', CustomObjectPageLayout.componentType),
275
429
  _apiName: requireStringComponentField(component, 'api_name', CustomObjectPageLayout.componentType),
276
430
  _customObjectApiName: requireStringComponentField(component, 'object_rql_name', CustomObjectPageLayout.componentType),
277
431
  _name: requireStringComponentField(component, 'name', CustomObjectPageLayout.componentType),
278
- _tabEdits: cloneJson(component['tab_edits'] ?? {}),
432
+ _tabEdits: annotateLoadedTabEdits(cloneJson(component['tab_edits'] ?? {}), resolveFieldSectionId),
279
433
  _headerEdits: cloneJson(component['header_edits'] ?? {}),
280
434
  _visibilityConditions: cloneJson(component['visibility_conditions'] ?? {}),
281
435
  _blueprintKey: component['blueprint_key'],
@@ -313,17 +467,32 @@ export class CustomObjectPageLayout {
313
467
  /**
314
468
  * Adds multiple fields to an existing layout section.
315
469
  *
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.
470
+ * Fields are inserted in the order provided. For custom layout sections,
471
+ * pass `options.section` so TypeScript and runtime validation agree on the
472
+ * target `CUSTOM_OBJECT_FIELD_SECTION`.
319
473
  */
320
474
  addFields(fields, options) {
321
475
  if (fields.length === 0)
322
476
  return this;
477
+ let fieldSectionForMutation;
323
478
  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));
479
+ (() => {
480
+ assertOptionAbsent(options, 'section', 'CustomObjectPageLayout addField section is only supported for custom layout sections.');
481
+ assertOptionAbsent(options, 'fieldSection', 'CustomObjectPageLayout addField fieldSection is no longer supported. ' +
482
+ 'Use sectionKey only for systemSection: true.');
483
+ return this.ensureSystemSectionFieldContainer(options.tabKey, options.sectionKey);
484
+ })()
485
+ : (() => {
486
+ assertOptionAbsent(options, 'sectionKey', 'CustomObjectPageLayout addField sectionKey is only supported with systemSection: true. ' +
487
+ 'Pass section: CustomObjectFieldSection for custom layout sections.');
488
+ assertOptionAbsent(options, 'fieldSection', 'CustomObjectPageLayout addField fieldSection is no longer supported. ' +
489
+ 'Pass section: CustomObjectFieldSection instead.');
490
+ const section = requireFieldSectionOption(options.section, 'CustomObjectPageLayout addField section');
491
+ this.assertFieldSectionBelongsToLayout(section);
492
+ fieldSectionForMutation = section;
493
+ return this.requireOneFieldContainer(options.tabKey, section.getSectionId(), section);
494
+ })();
495
+ const entries = fields.map((field) => this.serializeFieldForMutation(field, container.sectionKey ?? container.label, options, fieldSectionForMutation));
327
496
  if (options.allowDuplicate !== true) {
328
497
  const existing = new Set(container.fields.map((entry) => fieldRqlNameOf(entry)).filter(Boolean));
329
498
  const seen = new Set();
@@ -349,9 +518,23 @@ export class CustomObjectPageLayout {
349
518
  const fieldApiName = typeof field === 'string' ? field : field.getApiName();
350
519
  if (isField(field))
351
520
  this.assertFieldBelongsToLayout(field);
521
+ let sectionKey;
522
+ if (options.systemSection === true) {
523
+ assertOptionAbsent(options, 'section', 'CustomObjectPageLayout removeField section is only supported for custom layout sections.');
524
+ sectionKey = options.sectionKey;
525
+ }
526
+ else {
527
+ assertOptionAbsent(options, 'sectionKey', 'CustomObjectPageLayout removeField sectionKey is only supported with systemSection: true. ' +
528
+ 'Pass section: CustomObjectFieldSection for custom layout sections.');
529
+ if (options.section != null) {
530
+ const section = requireFieldSectionOption(options.section, 'CustomObjectPageLayout removeField section');
531
+ this.assertFieldSectionBelongsToLayout(section);
532
+ sectionKey = section.getSectionId();
533
+ }
534
+ }
352
535
  const containers = this.findFieldContainers({
353
536
  tabKey: options.tabKey,
354
- sectionKey: options.sectionKey,
537
+ sectionKey,
355
538
  systemSectionOnly: options.systemSection === true,
356
539
  });
357
540
  const matches = [];
@@ -400,10 +583,10 @@ export class CustomObjectPageLayout {
400
583
  if (options.systemTab === true) {
401
584
  const tabEdit = this.ensureSystemTabEdit(options.tabKey);
402
585
  const sections = ensureArrayProperty(tabEdit, 'newSections', `systemTabEdits.${options.tabKey}.newSections`);
403
- this.assertSectionKeyAvailable(sections, normalized.key, options.tabKey, options.allowDuplicate);
586
+ this.assertSectionKeyAvailable(sections, layoutSectionKey(normalized), options.tabKey, options.allowDuplicate);
587
+ const sectionsOrder = this.ensureSystemSectionsOrder(tabEdit);
404
588
  insertAt(sections, normalized, options.position);
405
- if (tabEdit.sectionsOrder != null)
406
- insertAt(tabEdit.sectionsOrder, normalized.key, options.position);
589
+ insertUniqueAt(sectionsOrder, layoutSectionKey(normalized), options.position);
407
590
  return this;
408
591
  }
409
592
  const tab = this.findNewTabWithSections(options.tabKey);
@@ -411,26 +594,27 @@ export class CustomObjectPageLayout {
411
594
  throw new Error(`CustomObjectPageLayout could not find new tab "${options.tabKey}". ` +
412
595
  'Pass systemTab: true to add a section to a built-in system tab.');
413
596
  }
414
- this.assertSectionKeyAvailable(tab.sections, normalized.key, options.tabKey, options.allowDuplicate);
597
+ this.assertSectionKeyAvailable(tab.sections, layoutSectionKey(normalized), options.tabKey, options.allowDuplicate);
415
598
  insertAt(tab.sections, normalized, options.position);
416
599
  return this;
417
600
  }
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 = {}) {
601
+ removeSection(sectionOrKey, options = {}) {
425
602
  if (options.systemSection === true) {
603
+ if (typeof sectionOrKey !== 'string') {
604
+ throw new TypeError('CustomObjectPageLayout removeSection with systemSection: true requires a system section key.');
605
+ }
426
606
  if (options.tabKey == null) {
427
607
  throw new Error('CustomObjectPageLayout removeSection with systemSection: true requires tabKey.');
428
608
  }
429
- const sectionEdit = this.ensureSystemSectionEdit(options.tabKey, sectionKey);
609
+ const sectionEdit = this.ensureSystemSectionEdit(options.tabKey, sectionOrKey);
430
610
  sectionEdit.deleted = true;
431
- this.removeSectionVisibility(sectionKey);
611
+ removeKeyFromOrder(this._tabEdits.systemTabEdits?.[options.tabKey]?.sectionsOrder, sectionOrKey);
612
+ this.removeSectionVisibility(sectionOrKey);
432
613
  return this;
433
614
  }
615
+ const section = requireFieldSectionOption(sectionOrKey, 'CustomObjectPageLayout removeSection section');
616
+ this.assertFieldSectionBelongsToLayout(section);
617
+ const sectionKey = section.getSectionId();
434
618
  const matches = [];
435
619
  for (const tab of this._tabEdits.newTabs ?? []) {
436
620
  if (!isTabWithSections(tab))
@@ -438,16 +622,16 @@ export class CustomObjectPageLayout {
438
622
  if (options.tabKey != null && tab.key !== options.tabKey)
439
623
  continue;
440
624
  tab.sections.forEach((section, index) => {
441
- if (section.key === sectionKey)
442
- matches.push({ sections: tab.sections, index, tabKey: tab.key });
625
+ if (layoutSectionMatches(section, sectionKey))
626
+ matches.push({ sections: tab.sections, section, index, tabKey: tab.key });
443
627
  });
444
628
  }
445
629
  for (const [tabKey, tabEdit] of Object.entries(this._tabEdits.systemTabEdits ?? {})) {
446
630
  if (options.tabKey != null && tabKey !== options.tabKey)
447
631
  continue;
448
632
  (tabEdit.newSections ?? []).forEach((section, index) => {
449
- if (section.key === sectionKey)
450
- matches.push({ sections: tabEdit.newSections, index, tabEdit, tabKey });
633
+ if (layoutSectionMatches(section, sectionKey))
634
+ matches.push({ sections: tabEdit.newSections, section, index, tabEdit, tabKey });
451
635
  });
452
636
  }
453
637
  if (matches.length === 0) {
@@ -466,11 +650,11 @@ export class CustomObjectPageLayout {
466
650
  byArray.set(match.sections, indexes);
467
651
  if (match.tabEdit != null)
468
652
  removeKeyFromOrder(match.tabEdit.sectionsOrder, sectionKey);
653
+ this.removeSectionVisibility(sectionKey);
469
654
  }
470
655
  for (const [sections, indexes] of byArray.entries()) {
471
656
  removeIndexes(sections, indexes);
472
657
  }
473
- this.removeSectionVisibility(sectionKey);
474
658
  return this;
475
659
  }
476
660
  /** Adds a new tab to `tab_edits.newTabs`. */
@@ -481,10 +665,9 @@ export class CustomObjectPageLayout {
481
665
  if (options.allowDuplicate !== true && newTabs.some((existing) => existing.key === normalized.key)) {
482
666
  throw new Error(`CustomObjectPageLayout already contains tab "${normalized.key}".`);
483
667
  }
668
+ const tabsOrder = this.ensureTabsOrder();
484
669
  insertAt(newTabs, normalized, options.position);
485
- if (this._tabEdits.tabsOrder != null) {
486
- insertAt(this._tabEdits.tabsOrder, normalized.key, options.position);
487
- }
670
+ insertUniqueAt(tabsOrder, normalized.key, options.position);
488
671
  return this;
489
672
  }
490
673
  /**
@@ -517,10 +700,9 @@ export class CustomObjectPageLayout {
517
700
  if (options.allowDuplicate !== true && newFields.some((existing) => existing.key === headerField.key)) {
518
701
  throw new Error(`CustomObjectPageLayout header already contains field key "${headerField.key}".`);
519
702
  }
703
+ const headerFieldsOrder = this.ensureHeaderFieldsOrder();
520
704
  insertAt(newFields, headerField, options.position);
521
- if (this._headerEdits.headerFieldsOrder != null) {
522
- insertAt(this._headerEdits.headerFieldsOrder, headerField.key, options.position);
523
- }
705
+ insertUniqueAt(headerFieldsOrder, headerField.rqlName, options.position);
524
706
  return this;
525
707
  }
526
708
  /** Removes custom header fields from `header_edits.newFields`. */
@@ -530,50 +712,104 @@ export class CustomObjectPageLayout {
530
712
  this.assertFieldBelongsToLayout(fieldOrKey);
531
713
  const key = typeof fieldOrKey === 'string' ? fieldOrKey : fieldApiName;
532
714
  const newFields = this._headerEdits.newFields ?? [];
533
- const removedKeys = [];
715
+ const removedFields = [];
534
716
  const removed = removeFromArray(newFields, (field) => {
535
717
  const matched = field.key === key || (fieldApiName != null && field.rqlName === fieldApiName);
536
718
  if (matched)
537
- removedKeys.push(field.key);
719
+ removedFields.push(field);
538
720
  return matched;
539
721
  }, `CustomObjectPageLayout header field "${key}"`, options.removeAll !== false);
540
722
  if (removed === 0) {
541
723
  throw new Error(`CustomObjectPageLayout header does not contain field "${key}".`);
542
724
  }
543
- for (const removedKey of removedKeys) {
544
- removeKeyFromOrder(this._headerEdits.headerFieldsOrder, removedKey);
545
- }
725
+ for (const removedField of removedFields)
726
+ this.removeHeaderOrderFieldIfUnused(removedField);
546
727
  return this;
547
728
  }
548
729
  /** Sets the record title field in `header_edits`. */
549
730
  setTitleField(field) {
550
- this._headerEdits.newTitleField = this.headerFieldApiName(field);
731
+ const previousTitleField = this._headerEdits.newTitleField;
732
+ const nextTitleField = this.headerFieldApiName(field);
733
+ this._headerEdits.newTitleField = nextTitleField;
551
734
  if (field != null)
552
735
  this._headerEdits.titleFieldDeleted = false;
736
+ insertUniqueAt(this.ensureHeaderFieldsOrder(), nextTitleField, 'end');
737
+ this.removeHeaderOrderFieldIfUnused(previousTitleField);
553
738
  return this;
554
739
  }
555
740
  /** Removes the record title field from the header. */
556
741
  removeTitleField() {
742
+ const previousTitleField = this._headerEdits.newTitleField;
557
743
  this._headerEdits.newTitleField = null;
558
744
  this._headerEdits.titleFieldDeleted = true;
745
+ this.removeHeaderOrderFieldIfUnused(previousTitleField);
559
746
  return this;
560
747
  }
561
748
  /** Sets the record description field in `header_edits`. */
562
749
  setDescriptionField(field) {
563
- this._headerEdits.newDescriptionField = this.headerFieldApiName(field);
750
+ const previousDescriptionField = this._headerEdits.newDescriptionField;
751
+ const nextDescriptionField = this.headerFieldApiName(field);
752
+ this._headerEdits.newDescriptionField = nextDescriptionField;
564
753
  if (field != null)
565
754
  this._headerEdits.descriptionFieldDeleted = false;
755
+ insertUniqueAt(this.ensureHeaderFieldsOrder(), nextDescriptionField, 'end');
756
+ this.removeHeaderOrderFieldIfUnused(previousDescriptionField);
566
757
  return this;
567
758
  }
568
759
  /** Removes the record description field from the header. */
569
760
  removeDescriptionField() {
761
+ const previousDescriptionField = this._headerEdits.newDescriptionField;
570
762
  this._headerEdits.newDescriptionField = null;
571
763
  this._headerEdits.descriptionFieldDeleted = true;
764
+ this.removeHeaderOrderFieldIfUnused(previousDescriptionField);
572
765
  return this;
573
766
  }
574
- serializeFieldForMutation(field, fallbackSectionKey, options) {
767
+ ensureTabsOrder() {
768
+ var _a;
769
+ const order = ((_a = this._tabEdits).tabsOrder ?? (_a.tabsOrder = []));
770
+ for (const tab of this._tabEdits.newTabs ?? [])
771
+ insertUniqueAt(order, tab.key, 'end');
772
+ for (const [tabKey, tabEdit] of Object.entries(this._tabEdits.systemTabEdits ?? {})) {
773
+ if (tabEdit.deleted !== true)
774
+ insertUniqueAt(order, tabKey, 'end');
775
+ }
776
+ return order;
777
+ }
778
+ ensureSystemSectionsOrder(tabEdit) {
779
+ const order = (tabEdit.sectionsOrder ?? (tabEdit.sectionsOrder = []));
780
+ for (const section of tabEdit.newSections ?? [])
781
+ insertUniqueAt(order, layoutSectionKey(section), 'end');
782
+ for (const [sectionKey, sectionEdit] of Object.entries(tabEdit.systemSectionEdits ?? {})) {
783
+ if (sectionEdit.deleted !== true)
784
+ insertUniqueAt(order, sectionKey, 'end');
785
+ }
786
+ return order;
787
+ }
788
+ ensureHeaderFieldsOrder() {
789
+ this._headerEdits.headerFieldsOrder = normalizeHeaderFieldsOrder({
790
+ ...this._headerEdits,
791
+ headerFieldsOrder: this._headerEdits.headerFieldsOrder ?? [],
792
+ });
793
+ return this._headerEdits.headerFieldsOrder;
794
+ }
795
+ headerOrderFieldIsUsed(rqlName) {
796
+ return (this._headerEdits.newTitleField === rqlName ||
797
+ this._headerEdits.newDescriptionField === rqlName ||
798
+ (this._headerEdits.newFields ?? []).some((field) => field.rqlName === rqlName) ||
799
+ Object.values(this._headerEdits.systemFieldEdits ?? {}).some((edit) => edit.newRqlField === rqlName));
800
+ }
801
+ removeHeaderOrderFieldIfUnused(field) {
802
+ const rqlName = typeof field === 'string' ? field : field?.rqlName;
803
+ if (field != null && typeof field !== 'string' && field.key !== rqlName) {
804
+ removeKeyFromOrder(this._headerEdits.headerFieldsOrder, field.key);
805
+ }
806
+ if (rqlName == null || this.headerOrderFieldIsUsed(rqlName))
807
+ return;
808
+ removeKeyFromOrder(this._headerEdits.headerFieldsOrder, rqlName);
809
+ }
810
+ serializeFieldForMutation(field, fallbackSectionKey, options, fieldSection) {
575
811
  this.assertFieldBelongsToLayout(field);
576
- this.assignFieldSectionForMutation(field, fallbackSectionKey, options.fieldSection);
812
+ this.assignFieldSectionForMutation(field, fallbackSectionKey, fieldSection);
577
813
  return toSerializedSectionField(field.getApiName(), options);
578
814
  }
579
815
  normalizeSectionFieldForMutation(section, sectionKey, entry) {
@@ -591,21 +827,20 @@ export class CustomObjectPageLayout {
591
827
  'pass a Field instance or { field }.');
592
828
  }
593
829
  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}".`);
830
+ this.assertFieldSectionBelongsToLayout(input.section);
831
+ const sectionId = input.section.getSectionId();
832
+ if (inputFieldsSectionHasKey(input)) {
833
+ throw new Error('CustomObjectPageLayout fields_section key is not supported. Omit key.');
597
834
  }
598
- const key = input.key ?? input.section.getSectionId();
599
835
  const name = input.name ?? input.section.getName();
600
836
  const normalized = {
601
- key,
602
837
  name,
603
838
  type: 'fields_section',
604
- fields: input.fields.map((field) => this.normalizeSectionFieldForMutation(input.section, key, field)),
839
+ fields: input.fields.map((field) => this.normalizeSectionFieldForMutation(input.section, sectionId, field)),
605
840
  };
606
841
  if (input.layout != null)
607
842
  normalized.layout = input.layout;
608
- return normalized;
843
+ return attachFieldSectionId(normalized, sectionId);
609
844
  }
610
845
  normalizeTabForMutation(tab) {
611
846
  if (tab.type === 'custom_tab')
@@ -619,7 +854,7 @@ export class CustomObjectPageLayout {
619
854
  const rawFieldSection = fieldSection;
620
855
  if (rawFieldSection != null) {
621
856
  if (typeof rawFieldSection === 'string') {
622
- throw new TypeError(`CustomObjectPageLayout fieldSection must be a CustomObjectFieldSection object; ` +
857
+ throw new TypeError(`CustomObjectPageLayout section must be a CustomObjectFieldSection object; ` +
623
858
  `received raw section id "${rawFieldSection}". Load/include the existing field section first.`);
624
859
  }
625
860
  if (rawFieldSection.getModelApiName() !== this._customObjectApiName) {
@@ -631,7 +866,7 @@ export class CustomObjectPageLayout {
631
866
  }
632
867
  if (field.getSectionId() == null) {
633
868
  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 ` +
869
+ `Construct the field with a CustomObjectFieldSection or pass section when adding it to ` +
635
870
  `section "${fallbackSectionKey}".`);
636
871
  }
637
872
  }
@@ -641,17 +876,45 @@ export class CustomObjectPageLayout {
641
876
  `"${field.getCustomObjectApiName()}", but the layout belongs to "${this._customObjectApiName}".`);
642
877
  }
643
878
  }
644
- requireOneFieldContainer(tabKey, sectionKey) {
879
+ assertFieldSectionBelongsToLayout(section) {
880
+ if (section.getModelApiName() !== this._customObjectApiName) {
881
+ throw new Error(`CustomObjectPageLayout field section "${section.getSectionId()}" belongs to ` +
882
+ `"${section.getModelApiName()}", but the layout belongs to "${this._customObjectApiName}".`);
883
+ }
884
+ }
885
+ requireOneFieldContainer(tabKey, sectionKey, fallbackFieldSection) {
645
886
  const containers = this.findFieldContainers({
646
887
  tabKey,
647
888
  sectionKey,
648
889
  systemSectionOnly: false,
649
890
  });
650
891
  if (containers.length === 0) {
651
- const prefix = tabKey != null ? `${tabKey}.` : '';
652
- throw new Error(`CustomObjectPageLayout could not find section "${prefix}${sectionKey}".`);
892
+ if (sectionKey != null && fallbackFieldSection != null) {
893
+ const unkeyedContainers = this.findFieldContainers({
894
+ tabKey,
895
+ sectionKey: undefined,
896
+ systemSectionOnly: false,
897
+ }).filter((container) => container.section != null && container.sectionKey == null);
898
+ if (unkeyedContainers.length === 1) {
899
+ if (fallbackFieldSection.getModelApiName() !== this._customObjectApiName) {
900
+ throw new Error(`CustomObjectPageLayout field section "${fallbackFieldSection.getSectionId()}" belongs to ` +
901
+ `"${fallbackFieldSection.getModelApiName()}", but the layout belongs to "${this._customObjectApiName}".`);
902
+ }
903
+ const container = unkeyedContainers[0];
904
+ attachFieldSectionId(container.section, fallbackFieldSection.getSectionId());
905
+ container.sectionKey = fallbackFieldSection.getSectionId();
906
+ container.label = `${container.tabKey}.${container.sectionKey}`;
907
+ return container;
908
+ }
909
+ }
910
+ const scope = sectionKey == null ? 'a field section' : (`section "${tabKey != null ? `${tabKey}.` : ''}${sectionKey}"`);
911
+ throw new Error(`CustomObjectPageLayout could not find ${scope}.`);
653
912
  }
654
913
  if (containers.length > 1) {
914
+ if (sectionKey == null) {
915
+ throw new Error(`CustomObjectPageLayout matched multiple sections: ` +
916
+ `${containers.map((container) => container.label).join(', ')}. Pass section to choose one.`);
917
+ }
655
918
  throw new Error(`CustomObjectPageLayout section "${sectionKey}" matched multiple sections: ` +
656
919
  `${containers.map((container) => container.label).join(', ')}. Pass tabKey to choose one.`);
657
920
  }
@@ -665,28 +928,34 @@ export class CustomObjectPageLayout {
665
928
  continue;
666
929
  if (filter.tabKey != null && tab.key !== filter.tabKey)
667
930
  continue;
668
- for (const section of tab.sections) {
669
- if (filter.sectionKey != null && section.key !== filter.sectionKey)
931
+ for (const [index, section] of tab.sections.entries()) {
932
+ const sectionKey = layoutSectionKey(section);
933
+ if (filter.sectionKey != null && !layoutSectionMatches(section, filter.sectionKey))
670
934
  continue;
935
+ const label = sectionKey == null ? `${tab.key}.sections[${index}]` : `${tab.key}.${sectionKey}`;
671
936
  containers.push({
672
937
  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}`,
938
+ sectionKey,
939
+ section,
940
+ fields: ensureArrayProperty(section, 'fields', `newTabs.${label}.fields`),
941
+ label,
676
942
  });
677
943
  }
678
944
  }
679
945
  for (const [tabKey, tabEdit] of Object.entries(this._tabEdits.systemTabEdits ?? {})) {
680
946
  if (filter.tabKey != null && tabKey !== filter.tabKey)
681
947
  continue;
682
- for (const section of tabEdit.newSections ?? []) {
683
- if (filter.sectionKey != null && section.key !== filter.sectionKey)
948
+ for (const [index, section] of (tabEdit.newSections ?? []).entries()) {
949
+ const sectionKey = layoutSectionKey(section);
950
+ if (filter.sectionKey != null && !layoutSectionMatches(section, filter.sectionKey))
684
951
  continue;
952
+ const label = sectionKey == null ? `${tabKey}.newSections[${index}]` : `${tabKey}.${sectionKey}`;
685
953
  containers.push({
686
954
  tabKey,
687
- sectionKey: section.key,
688
- fields: ensureArrayProperty(section, 'fields', `systemTabEdits.${tabKey}.newSections.${section.key}.fields`),
689
- label: `${tabKey}.${section.key}`,
955
+ sectionKey,
956
+ section,
957
+ fields: ensureArrayProperty(section, 'fields', `systemTabEdits.${label}.fields`),
958
+ label,
690
959
  });
691
960
  }
692
961
  }
@@ -717,6 +986,9 @@ export class CustomObjectPageLayout {
717
986
  if (tabKey == null) {
718
987
  throw new Error('CustomObjectPageLayout addField with systemSection: true requires tabKey.');
719
988
  }
989
+ if (sectionKey == null) {
990
+ throw new Error('CustomObjectPageLayout addField with systemSection: true requires sectionKey.');
991
+ }
720
992
  const sectionEdit = this.ensureSystemSectionEdit(tabKey, sectionKey);
721
993
  if (sectionEdit.deleted === true) {
722
994
  throw new Error(`CustomObjectPageLayout system section "${tabKey}.${sectionKey}" is marked deleted.`);
@@ -755,7 +1027,7 @@ export class CustomObjectPageLayout {
755
1027
  assertSectionKeyAvailable(sections, sectionKey, tabKey, allowDuplicate) {
756
1028
  if (allowDuplicate === true)
757
1029
  return;
758
- if (sections.some((section) => section.key === sectionKey)) {
1030
+ if (sectionKey != null && sections.some((section) => layoutSectionMatches(section, sectionKey))) {
759
1031
  throw new Error(`CustomObjectPageLayout tab "${tabKey}" already contains section "${sectionKey}".`);
760
1032
  }
761
1033
  }
@@ -827,12 +1099,13 @@ export class CustomObjectPageLayout {
827
1099
  * tabs or sections. Always uses `apiName: 'default'`.
828
1100
  *
829
1101
  * @param customObject - The custom object to create the layout for.
830
- * @param props - Optional. Pass `fields` to populate the General section.
1102
+ * @param props - Optional. Pass `section` and `fields` to populate the General section.
831
1103
  * @returns A new `CustomObjectPageLayout` registered with the manifest.
832
1104
  *
833
1105
  * @example
834
1106
  * ```ts
835
1107
  * CustomObjectPageLayout.basic(memberObj, {
1108
+ * section: profileSection,
836
1109
  * fields: [memberFirstName, memberEmail, memberStatusField],
837
1110
  * });
838
1111
  * ```
@@ -840,11 +1113,29 @@ export class CustomObjectPageLayout {
840
1113
  static basic(customObject, props = {}) {
841
1114
  const coApiName = customObject.getApiName();
842
1115
  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
- });
1116
+ const fields = props.fields ?? [];
1117
+ const generalSection = props.section ??
1118
+ (() => {
1119
+ const alreadySectionedField = fields.find((field) => field.getSectionId() != null);
1120
+ if (alreadySectionedField != null) {
1121
+ throw new Error(`CustomObjectPageLayout.basic field "${alreadySectionedField.getApiName()}" already belongs to ` +
1122
+ `field section "${alreadySectionedField.getSectionId()}". Pass that CustomObjectFieldSection ` +
1123
+ 'as section so basic() does not reassign the field.');
1124
+ }
1125
+ const sectionPrefix = coApiName.replace(/__c$/, '').replace(/[^a-zA-Z0-9]+/g, '_');
1126
+ return new CustomObjectFieldSection(customObject, {
1127
+ sectionId: `sec_${sectionPrefix}_general`,
1128
+ name: 'General',
1129
+ });
1130
+ })();
1131
+ assertSectionBelongsTo(customObject, generalSection);
1132
+ for (const field of fields) {
1133
+ const fieldSectionId = field.getSectionId();
1134
+ if (fieldSectionId != null && fieldSectionId !== generalSection.getSectionId()) {
1135
+ throw new Error(`CustomObjectPageLayout.basic field "${field.getApiName()}" belongs to field section ` +
1136
+ `"${fieldSectionId}", but basic() was given section "${generalSection.getSectionId()}".`);
1137
+ }
1138
+ }
848
1139
  return new CustomObjectPageLayout(customObject, {
849
1140
  apiName: 'default',
850
1141
  name: `${coName} Layout`,
@@ -856,11 +1147,10 @@ export class CustomObjectPageLayout {
856
1147
  type: 'tab_with_sections',
857
1148
  sections: [
858
1149
  {
859
- key: 'general',
860
1150
  name: 'General',
861
1151
  type: 'fields_section',
862
1152
  section: generalSection,
863
- fields: props.fields ?? [],
1153
+ fields,
864
1154
  },
865
1155
  ],
866
1156
  },