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