@rippling/rippling-sdk 0.2.0-alpha.50 → 0.2.0-alpha.52

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 (39) hide show
  1. package/examples/functions/durable.ts +96 -0
  2. package/examples/manifest/existing-manifest-mutations/existing-manifest-mutations.ts +5 -0
  3. package/functions.d.mts +35 -0
  4. package/functions.d.mts.map +1 -1
  5. package/functions.d.ts +35 -0
  6. package/functions.d.ts.map +1 -1
  7. package/functions.js.map +1 -1
  8. package/functions.mjs.map +1 -1
  9. package/lib/manifest/custom-object-page-layout.d.mts +19 -14
  10. package/lib/manifest/custom-object-page-layout.d.mts.map +1 -1
  11. package/lib/manifest/custom-object-page-layout.d.ts +19 -14
  12. package/lib/manifest/custom-object-page-layout.d.ts.map +1 -1
  13. package/lib/manifest/custom-object-page-layout.js +148 -77
  14. package/lib/manifest/custom-object-page-layout.js.map +1 -1
  15. package/lib/manifest/custom-object-page-layout.mjs +148 -77
  16. package/lib/manifest/custom-object-page-layout.mjs.map +1 -1
  17. package/lib/manifest/existing-manifest-context.d.mts +1 -0
  18. package/lib/manifest/existing-manifest-context.d.mts.map +1 -1
  19. package/lib/manifest/existing-manifest-context.d.ts +1 -0
  20. package/lib/manifest/existing-manifest-context.d.ts.map +1 -1
  21. package/lib/manifest/existing-manifest-context.js +7 -6
  22. package/lib/manifest/existing-manifest-context.js.map +1 -1
  23. package/lib/manifest/existing-manifest-context.mjs +7 -6
  24. package/lib/manifest/existing-manifest-context.mjs.map +1 -1
  25. package/package.json +1 -1
  26. package/resources/leave-balances.d.mts +10 -0
  27. package/resources/leave-balances.d.mts.map +1 -1
  28. package/resources/leave-balances.d.ts +10 -0
  29. package/resources/leave-balances.d.ts.map +1 -1
  30. package/src/functions.md +238 -0
  31. package/src/functions.ts +42 -0
  32. package/src/lib/manifest/custom-object-page-layout.ts +214 -112
  33. package/src/lib/manifest/existing-manifest-context.ts +11 -6
  34. package/src/resources/leave-balances.ts +12 -0
  35. package/src/version.ts +1 -1
  36. package/version.d.mts +1 -1
  37. package/version.d.ts +1 -1
  38. package/version.js +1 -1
  39. package/version.mjs +1 -1
package/src/functions.ts CHANGED
@@ -23,6 +23,36 @@ export class FunctionResponse {
23
23
  }
24
24
 
25
25
  export type FunctionEvent = Record<string, any>;
26
+ export type DurableDuration = number | string;
27
+ export type DurableTimestamp = number | Date | string;
28
+ export interface DurableWaitForEventOptions {
29
+ type: string;
30
+ timeout?: DurableDuration;
31
+ }
32
+ export interface DurableFailureOptions {
33
+ code?: string;
34
+ details?: unknown;
35
+ }
36
+ export interface DurableWaitForEventEventResult<T = unknown> {
37
+ kind: 'event';
38
+ waitName: string;
39
+ eventType: string;
40
+ payload: T;
41
+ }
42
+ export interface DurableWaitForEventTimeoutResult {
43
+ kind: 'timeout';
44
+ waitName: string;
45
+ eventType: string;
46
+ }
47
+ export interface DurableWaitForEventResultHelpers<T = unknown> {
48
+ isEvent(): this is DurableWaitForEventEventResult<T> & DurableWaitForEventResultHelpers<T>;
49
+ isTimeout(): this is DurableWaitForEventTimeoutResult & DurableWaitForEventResultHelpers<T>;
50
+ throwTimeout(this: DurableWaitForEventTimeoutResult & DurableWaitForEventResultHelpers<T>): never;
51
+ }
52
+ export type DurableWaitForEventResult<T = unknown> =
53
+ | (DurableWaitForEventEventResult<T> & DurableWaitForEventResultHelpers<T>)
54
+ | (DurableWaitForEventTimeoutResult & DurableWaitForEventResultHelpers<T>);
55
+
26
56
  export interface FunctionContext {
27
57
  env: {
28
58
  // Set to the Rippling user's bearer token when the function is triggered by a user.
@@ -35,10 +65,22 @@ export interface FunctionContext {
35
65
  function_id: string;
36
66
  function_version_id: string;
37
67
  run_id: string;
68
+ role_id?: string;
38
69
  };
39
70
  settings: Record<string, string | boolean | number>;
40
71
  }
41
72
 
73
+ export interface DurableFunctionContext extends FunctionContext {
74
+ step<T>(name: string, fn: () => Promise<T> | T, opts?: unknown): Promise<T>;
75
+ sleep(name: string, duration: DurableDuration): Promise<void>;
76
+ sleepUntil(name: string, timestamp: DurableTimestamp): Promise<void>;
77
+ waitForEvent<T = unknown>(
78
+ name: string,
79
+ opts: DurableWaitForEventOptions,
80
+ ): Promise<DurableWaitForEventResult<T>>;
81
+ fail(message: string, opts?: DurableFailureOptions): never;
82
+ }
83
+
42
84
  export class DataBridgeRecord {
43
85
  id: string;
44
86
  version: string;
@@ -160,8 +160,8 @@ export interface FieldsSection extends PageLayoutSectionOptions {
160
160
  type: 'fields_section';
161
161
  /** Field section metadata for the fields displayed in this layout section. Required. */
162
162
  section: CustomObjectFieldSection;
163
- /** Stable layout section key. Derived from `section.getSectionId()` on the wire. */
164
- key?: never;
163
+ /** Stable layout section key. Optional. Defaults to `section.getSectionId()`. */
164
+ key?: SectionKey;
165
165
  /** Display name of the layout section. Optional. Defaults to the field section name. */
166
166
  name?: string;
167
167
  /** Fields to display in this section. Required. */
@@ -382,13 +382,13 @@ export interface AddCustomPageLayoutFieldOptions extends AddPageLayoutFieldOptio
382
382
  * across the layout.
383
383
  */
384
384
  tabKey?: string;
385
- /** Field section to add the field to. Required for custom layout sections. */
386
- section: CustomObjectFieldSection;
387
- /** Raw section keys are only accepted for built-in system sections. */
388
- sectionKey?: never;
385
+ /** Field-section metadata used to find typed custom layout sections and assign field metadata. */
386
+ section?: CustomObjectFieldSection;
387
+ /** Layout section key. Optional escape hatch for ambiguous custom sections. */
388
+ sectionKey?: SectionKey;
389
389
  /** Deprecated. Use `section`. */
390
390
  fieldSection?: never;
391
- /** Custom layout sections are selected by `section`; `sectionKey` is only for system sections. */
391
+ /** Custom layout sections are selected by `section`; `sectionKey` can disambiguate. */
392
392
  systemSection?: false;
393
393
  }
394
394
 
@@ -432,9 +432,9 @@ export interface RemoveCustomPageLayoutFieldOptions extends RemovePageLayoutFiel
432
432
  tabKey?: string;
433
433
  /** Limit removal to one field section. Optional. */
434
434
  section?: CustomObjectFieldSection;
435
- /** Raw section keys are only accepted for built-in system sections. */
436
- sectionKey?: never;
437
- /** Custom layout sections are selected by `section`; `sectionKey` is only for system sections. */
435
+ /** Limit removal to one layout section key. Optional. */
436
+ sectionKey?: SectionKey;
437
+ /** Custom layout sections are selected by `section`; `sectionKey` can disambiguate. */
438
438
  systemSection?: false;
439
439
  }
440
440
 
@@ -480,7 +480,7 @@ export interface RemovePageLayoutSectionOptions {
480
480
  * more than once throws. Optional. @default true
481
481
  */
482
482
  removeAll?: boolean;
483
- /** Custom layout sections are selected by `CustomObjectFieldSection`; raw keys are only for system sections. */
483
+ /** Custom layout sections are selected by layout section key or `CustomObjectFieldSection`. */
484
484
  systemSection?: false;
485
485
  }
486
486
 
@@ -592,6 +592,8 @@ export interface CustomObjectPageLayoutLoadFromExistingOptions extends ExistingM
592
592
  customObjectApiName?: string;
593
593
  }
594
594
 
595
+ type ResolveFieldSectionId = (fieldRqlName: string) => string | undefined;
596
+
595
597
  interface SerializedSectionField extends SectionFieldOptions {
596
598
  fieldRqlName: string;
597
599
  }
@@ -656,21 +658,25 @@ function isFieldRef(value: SectionFieldInput): value is SectionField {
656
658
  return candidate != null && typeof candidate === 'object' && isField(candidate.field);
657
659
  }
658
660
 
659
- const FIELD_SECTION_ID = Symbol('CustomObjectPageLayout.fieldSectionId');
661
+ const LAYOUT_SECTION_KEY = Symbol('CustomObjectPageLayout.layoutSectionKey');
660
662
 
661
663
  function isCustomObjectFieldSection(value: unknown): value is CustomObjectFieldSection {
662
664
  return value instanceof CustomObjectFieldSection;
663
665
  }
664
666
 
665
- function attachFieldSectionId(section: SerializedFieldsSection, sectionId: string): SerializedFieldsSection {
666
- if (section.key != null && section.key !== sectionId) {
667
- throw new Error(
668
- `CustomObjectPageLayout fields_section key "${section.key}" does not match field section "${sectionId}".`,
669
- );
670
- }
671
- section.key = sectionId;
672
- Object.defineProperty(section, FIELD_SECTION_ID, {
673
- value: sectionId,
667
+ function explicitLayoutSectionKey(section: { key?: string }): string | undefined {
668
+ const record = section as unknown as Record<string, unknown>;
669
+ const key = section.key ?? record['layout_section_key'] ?? record['layoutSectionKey'];
670
+ return typeof key === 'string' ? key : undefined;
671
+ }
672
+
673
+ function attachLayoutSectionKey(
674
+ section: SerializedFieldsSection,
675
+ sectionKey: string,
676
+ ): SerializedFieldsSection {
677
+ if (explicitLayoutSectionKey(section) == null) section.key = sectionKey;
678
+ Object.defineProperty(section, LAYOUT_SECTION_KEY, {
679
+ value: sectionKey,
674
680
  enumerable: false,
675
681
  configurable: true,
676
682
  });
@@ -679,8 +685,8 @@ function attachFieldSectionId(section: SerializedFieldsSection, sectionId: strin
679
685
 
680
686
  function layoutSectionKey(section: { key?: string }): string | undefined {
681
687
  return (
682
- section.key ??
683
- (section as unknown as Record<typeof FIELD_SECTION_ID, string | undefined>)[FIELD_SECTION_ID]
688
+ explicitLayoutSectionKey(section) ??
689
+ (section as unknown as Record<typeof LAYOUT_SECTION_KEY, string | undefined>)[LAYOUT_SECTION_KEY]
684
690
  );
685
691
  }
686
692
 
@@ -688,11 +694,6 @@ function layoutSectionMatches(section: { key?: string }, sectionKey: string): bo
688
694
  return layoutSectionKey(section) === sectionKey;
689
695
  }
690
696
 
691
- function inputFieldsSectionHasKey(input: FieldsSection): boolean {
692
- const record = input as unknown as Record<string, unknown>;
693
- return Object.prototype.hasOwnProperty.call(record, 'key') && record['key'] != null;
694
- }
695
-
696
697
  function isSerializedFieldsSection(section: SerializedPageLayoutSection): section is SerializedFieldsSection {
697
698
  return section.type === 'fields_section';
698
699
  }
@@ -783,23 +784,20 @@ function normalizeSectionField(
783
784
 
784
785
  function normalizeFieldsSection(customObject: CustomObject, input: FieldsSection): SerializedFieldsSection {
785
786
  assertSectionBelongsTo(customObject, input.section);
786
- const sectionId = input.section.getSectionId();
787
- if (inputFieldsSectionHasKey(input)) {
788
- throw new Error('CustomObjectPageLayout fields_section key is not supported. Omit key.');
789
- }
787
+ const sectionKey = input.key ?? input.section.getSectionId();
790
788
  const name = input.name ?? input.section.getName();
791
789
  const normalized = copySectionOptions<SerializedFieldsSection>(
792
790
  {
793
791
  name,
794
792
  type: 'fields_section',
795
793
  fields: input.fields.map((field) =>
796
- normalizeSectionField(customObject, input.section, sectionId, field),
794
+ normalizeSectionField(customObject, input.section, sectionKey, field),
797
795
  ),
798
796
  },
799
797
  input,
800
798
  );
801
799
  if (input.layout != null) normalized.layout = input.layout;
802
- return attachFieldSectionId(normalized, sectionId);
800
+ return attachLayoutSectionKey(normalized, sectionKey);
803
801
  }
804
802
 
805
803
  function normalizeSection(
@@ -1010,6 +1008,11 @@ interface MutableFieldContainer {
1010
1008
  label: string;
1011
1009
  }
1012
1010
 
1011
+ interface FieldSectionLayoutSection {
1012
+ tabKey: string;
1013
+ sectionKey: string;
1014
+ }
1015
+
1013
1016
  function isTabWithSections(tab: SerializedPageLayoutTab): tab is SerializedTabWithSections {
1014
1017
  return tab.type === 'tab_with_sections';
1015
1018
  }
@@ -1040,72 +1043,126 @@ function fieldRqlNameOf(entry: unknown): string | undefined {
1040
1043
  if (typeof entry === 'string') return entry;
1041
1044
  if (entry == null || typeof entry !== 'object') return undefined;
1042
1045
  const record = entry as Record<string, any>;
1043
- const fieldRqlName = record['fieldRqlName'] ?? record['field_rql_name'];
1046
+ const fieldRqlName =
1047
+ record['fieldRqlName'] ?? record['field_rql_name'] ?? record['apiName'] ?? record['api_name'];
1044
1048
  return typeof fieldRqlName === 'string' ? fieldRqlName : undefined;
1045
1049
  }
1046
1050
 
1047
- function explicitFieldSectionId(entry: unknown): string | undefined {
1048
- if (entry == null || typeof entry !== 'object') return undefined;
1049
- const record = entry as Record<string, any>;
1050
- const fieldSectionId = record['fieldSection'] ?? record['field_section'];
1051
- return typeof fieldSectionId === 'string' ? fieldSectionId : undefined;
1051
+ function addFieldSectionLayoutSection(
1052
+ index: Map<string, FieldSectionLayoutSection[]>,
1053
+ fieldSectionId: string,
1054
+ entry: FieldSectionLayoutSection,
1055
+ ): void {
1056
+ const entries = index.get(fieldSectionId) ?? [];
1057
+ if (
1058
+ !entries.some((existing) => existing.tabKey === entry.tabKey && existing.sectionKey === entry.sectionKey)
1059
+ ) {
1060
+ entries.push(entry);
1061
+ }
1062
+ index.set(fieldSectionId, entries);
1052
1063
  }
1053
1064
 
1054
- function inferredSectionIdFromFields(
1055
- section: SerializedFieldsSection,
1056
- resolveFieldSectionId?: (fieldApiName: string) => string | undefined,
1057
- ): string | undefined {
1058
- const sectionIds = new Set<string>();
1059
- for (const field of section.fields ?? []) {
1060
- const explicitSectionId = explicitFieldSectionId(field);
1061
- if (explicitSectionId != null) {
1062
- sectionIds.add(explicitSectionId);
1063
- continue;
1064
- }
1065
+ function indexFieldsSectionInput(
1066
+ index: Map<string, FieldSectionLayoutSection[]>,
1067
+ tabKey: string,
1068
+ section: FieldsSection,
1069
+ ): void {
1070
+ addFieldSectionLayoutSection(index, section.section.getSectionId(), {
1071
+ tabKey,
1072
+ sectionKey: section.key ?? section.section.getSectionId(),
1073
+ });
1074
+ }
1075
+
1076
+ function indexPageLayoutSectionInput(
1077
+ index: Map<string, FieldSectionLayoutSection[]>,
1078
+ tabKey: string,
1079
+ section: PageLayoutSection,
1080
+ ): void {
1081
+ if (section.type === 'fields_section') indexFieldsSectionInput(index, tabKey, section);
1082
+ }
1083
+
1084
+ function buildFieldSectionLayoutIndexFromProps(
1085
+ tabEdits: TabEdits | undefined,
1086
+ ): Map<string, FieldSectionLayoutSection[]> {
1087
+ const index = new Map<string, FieldSectionLayoutSection[]>();
1088
+ if (tabEdits == null) return index;
1065
1089
 
1066
- const fieldApiName = fieldRqlNameOf(field);
1067
- if (fieldApiName == null || resolveFieldSectionId == null) continue;
1068
- const resolvedSectionId = resolveFieldSectionId(fieldApiName);
1069
- if (resolvedSectionId != null) sectionIds.add(resolvedSectionId);
1090
+ for (const tab of tabEdits.newTabs ?? []) {
1091
+ if (tab.type !== 'tab_with_sections') continue;
1092
+ for (const section of tab.sections) indexPageLayoutSectionInput(index, tab.key, section);
1093
+ }
1094
+
1095
+ for (const [tabKey, tabEdit] of Object.entries(tabEdits.systemTabEdits ?? {})) {
1096
+ for (const section of tabEdit.newSections ?? []) {
1097
+ indexPageLayoutSectionInput(index, tabKey, section);
1098
+ }
1070
1099
  }
1071
- return sectionIds.size === 1 ? [...sectionIds][0] : undefined;
1100
+
1101
+ return index;
1072
1102
  }
1073
1103
 
1074
- function annotateLoadedFieldsSection(
1104
+ function indexLoadedFieldsSection(
1105
+ index: Map<string, FieldSectionLayoutSection[]>,
1106
+ tabKey: string,
1075
1107
  section: SerializedFieldsSection,
1076
- resolveFieldSectionId?: (fieldApiName: string) => string | undefined,
1077
- ): SerializedFieldsSection {
1078
- const explicitSectionId = layoutSectionKey(section);
1079
- const inferredSectionId = inferredSectionIdFromFields(section, resolveFieldSectionId);
1080
- if (explicitSectionId != null && inferredSectionId != null && explicitSectionId !== inferredSectionId) {
1081
- throw new Error(
1082
- `CustomObjectPageLayout fields_section key "${explicitSectionId}" does not match ` +
1083
- `field section "${inferredSectionId}".`,
1084
- );
1108
+ resolveFieldSectionId: ResolveFieldSectionId | undefined,
1109
+ ): void {
1110
+ const sectionKey = layoutSectionKey(section);
1111
+ if (sectionKey == null || resolveFieldSectionId == null) return;
1112
+
1113
+ for (const field of section.fields ?? []) {
1114
+ const fieldRqlName = fieldRqlNameOf(field);
1115
+ if (fieldRqlName == null) continue;
1116
+ const fieldSectionId = resolveFieldSectionId(fieldRqlName);
1117
+ if (fieldSectionId != null) {
1118
+ addFieldSectionLayoutSection(index, fieldSectionId, { tabKey, sectionKey });
1119
+ }
1085
1120
  }
1086
- const sectionId = explicitSectionId ?? inferredSectionId;
1087
- return sectionId == null ? section : attachFieldSectionId(section, sectionId);
1088
1121
  }
1089
1122
 
1090
- function annotateLoadedTabEdits(
1123
+ function buildFieldSectionLayoutIndexFromLoadedTabEdits(
1091
1124
  tabEdits: SerializedTabEdits,
1092
- resolveFieldSectionId?: (fieldApiName: string) => string | undefined,
1093
- ): SerializedTabEdits {
1125
+ resolveFieldSectionId: ResolveFieldSectionId | undefined,
1126
+ ): Map<string, FieldSectionLayoutSection[]> {
1127
+ const index = new Map<string, FieldSectionLayoutSection[]>();
1128
+
1129
+ for (const tab of tabEdits.newTabs ?? []) {
1130
+ if (!isTabWithSections(tab)) continue;
1131
+ for (const section of tab.sections) {
1132
+ if (isSerializedFieldsSection(section)) {
1133
+ indexLoadedFieldsSection(index, tab.key, section, resolveFieldSectionId);
1134
+ }
1135
+ }
1136
+ }
1137
+
1138
+ for (const [tabKey, tabEdit] of Object.entries(tabEdits.systemTabEdits ?? {})) {
1139
+ for (const section of tabEdit.newSections ?? []) {
1140
+ if (isSerializedFieldsSection(section)) {
1141
+ indexLoadedFieldsSection(index, tabKey, section, resolveFieldSectionId);
1142
+ }
1143
+ }
1144
+ }
1145
+
1146
+ return index;
1147
+ }
1148
+
1149
+ function annotateLoadedFieldsSection(section: SerializedFieldsSection): SerializedFieldsSection {
1150
+ const sectionKey = layoutSectionKey(section);
1151
+ return sectionKey == null ? section : attachLayoutSectionKey(section, sectionKey);
1152
+ }
1153
+
1154
+ function annotateLoadedTabEdits(tabEdits: SerializedTabEdits): SerializedTabEdits {
1094
1155
  for (const tab of tabEdits.newTabs ?? []) {
1095
1156
  if (!isTabWithSections(tab)) continue;
1096
1157
  tab.sections = tab.sections.map((section) =>
1097
- isSerializedFieldsSection(section) ?
1098
- annotateLoadedFieldsSection(section, resolveFieldSectionId)
1099
- : section,
1158
+ isSerializedFieldsSection(section) ? annotateLoadedFieldsSection(section) : section,
1100
1159
  );
1101
1160
  }
1102
1161
 
1103
1162
  for (const tabEdit of Object.values(tabEdits.systemTabEdits ?? {})) {
1104
1163
  if (tabEdit.newSections != null) {
1105
1164
  tabEdit.newSections = tabEdit.newSections.map((section) =>
1106
- isSerializedFieldsSection(section) ?
1107
- annotateLoadedFieldsSection(section, resolveFieldSectionId)
1108
- : section,
1165
+ isSerializedFieldsSection(section) ? annotateLoadedFieldsSection(section) : section,
1109
1166
  );
1110
1167
  }
1111
1168
  }
@@ -1228,6 +1285,7 @@ export class CustomObjectPageLayout {
1228
1285
  private readonly _headerEdits: SerializedHeaderEdits;
1229
1286
  private readonly _visibilityConditions: VisibilityConditions;
1230
1287
  private readonly _blueprintKey: string | undefined;
1288
+ private readonly _fieldSectionLayoutSections: Map<string, FieldSectionLayoutSection[]>;
1231
1289
 
1232
1290
  /**
1233
1291
  * @param customObject - The custom object this layout belongs to.
@@ -1242,6 +1300,7 @@ export class CustomObjectPageLayout {
1242
1300
  this._customObjectApiName = customObject.getApiName();
1243
1301
  this._name = props.name;
1244
1302
  this._tabEdits = normalizeTabEdits(customObject, props.tabEdits ?? {});
1303
+ this._fieldSectionLayoutSections = buildFieldSectionLayoutIndexFromProps(props.tabEdits);
1245
1304
  this._headerEdits = normalizeHeaderEdits(props.headerEdits ?? {});
1246
1305
  this._visibilityConditions = props.visibilityConditions ?? {};
1247
1306
  this._blueprintKey = props.blueprintKey;
@@ -1266,9 +1325,10 @@ export class CustomObjectPageLayout {
1266
1325
  static _fromExistingComponent(
1267
1326
  customObject: CustomObject,
1268
1327
  component: Record<string, any>,
1269
- resolveFieldSectionId?: (fieldApiName: string) => string | undefined,
1328
+ resolveFieldSectionId?: ResolveFieldSectionId,
1270
1329
  ): CustomObjectPageLayout {
1271
1330
  const layout = Object.create(CustomObjectPageLayout.prototype) as CustomObjectPageLayout;
1331
+ const tabEdits = annotateLoadedTabEdits(cloneJson(component['tab_edits'] ?? {}));
1272
1332
  Object.assign(layout as Record<string, any>, {
1273
1333
  _layoutId: requireStringComponentField(component, 'layout_id', CustomObjectPageLayout.componentType),
1274
1334
  _apiName: requireStringComponentField(component, 'api_name', CustomObjectPageLayout.componentType),
@@ -1278,10 +1338,14 @@ export class CustomObjectPageLayout {
1278
1338
  CustomObjectPageLayout.componentType,
1279
1339
  ),
1280
1340
  _name: requireStringComponentField(component, 'name', CustomObjectPageLayout.componentType),
1281
- _tabEdits: annotateLoadedTabEdits(cloneJson(component['tab_edits'] ?? {}), resolveFieldSectionId),
1341
+ _tabEdits: tabEdits,
1282
1342
  _headerEdits: cloneJson(component['header_edits'] ?? {}),
1283
1343
  _visibilityConditions: cloneJson(component['visibility_conditions'] ?? {}),
1284
1344
  _blueprintKey: component['blueprint_key'],
1345
+ _fieldSectionLayoutSections: buildFieldSectionLayoutIndexFromLoadedTabEdits(
1346
+ tabEdits,
1347
+ resolveFieldSectionId,
1348
+ ),
1285
1349
  });
1286
1350
  customObject._register(layout);
1287
1351
  return layout;
@@ -1322,8 +1386,8 @@ export class CustomObjectPageLayout {
1322
1386
  * Adds multiple fields to an existing layout section.
1323
1387
  *
1324
1388
  * Fields are inserted in the order provided. For custom layout sections,
1325
- * pass `options.section` so TypeScript and runtime validation agree on the
1326
- * target `CUSTOM_OBJECT_FIELD_SECTION`.
1389
+ * pass `section` to choose the typed field section. Use `sectionKey` only
1390
+ * when a field section maps to multiple layout sections.
1327
1391
  */
1328
1392
  addFields(fields: Field[], options: AddPageLayoutFieldsOptions): this {
1329
1393
  if (fields.length === 0) return this;
@@ -1346,25 +1410,25 @@ export class CustomObjectPageLayout {
1346
1410
  return this.ensureSystemSectionFieldContainer(options.tabKey, options.sectionKey);
1347
1411
  })()
1348
1412
  : (() => {
1349
- assertOptionAbsent(
1350
- options,
1351
- 'sectionKey',
1352
- 'CustomObjectPageLayout addField sectionKey is only supported with systemSection: true. ' +
1353
- 'Pass section: CustomObjectFieldSection for custom layout sections.',
1354
- );
1355
1413
  assertOptionAbsent(
1356
1414
  options,
1357
1415
  'fieldSection',
1358
1416
  'CustomObjectPageLayout addField fieldSection is no longer supported. ' +
1359
1417
  'Pass section: CustomObjectFieldSection instead.',
1360
1418
  );
1361
- const section = requireFieldSectionOption(
1362
- options.section,
1363
- 'CustomObjectPageLayout addField section',
1364
- );
1365
- this.assertFieldSectionBelongsToLayout(section);
1366
- fieldSectionForMutation = section;
1367
- return this.requireOneFieldContainer(options.tabKey, section.getSectionId(), section);
1419
+ const section =
1420
+ options.section == null ?
1421
+ undefined
1422
+ : requireFieldSectionOption(options.section, 'CustomObjectPageLayout addField section');
1423
+ if (section != null) {
1424
+ this.assertFieldSectionBelongsToLayout(section);
1425
+ fieldSectionForMutation = section;
1426
+ }
1427
+ const sectionKey =
1428
+ options.sectionKey ??
1429
+ (section == null ? undefined : this.layoutSectionKeyForFieldSection(options.tabKey, section)) ??
1430
+ section?.getSectionId();
1431
+ return this.requireOneFieldContainer(options.tabKey, sectionKey, section);
1368
1432
  })();
1369
1433
  const entries = fields.map((field) =>
1370
1434
  this.serializeFieldForMutation(
@@ -1413,19 +1477,15 @@ export class CustomObjectPageLayout {
1413
1477
  );
1414
1478
  sectionKey = options.sectionKey;
1415
1479
  } else {
1416
- assertOptionAbsent(
1417
- options,
1418
- 'sectionKey',
1419
- 'CustomObjectPageLayout removeField sectionKey is only supported with systemSection: true. ' +
1420
- 'Pass section: CustomObjectFieldSection for custom layout sections.',
1421
- );
1480
+ sectionKey = options.sectionKey;
1422
1481
  if (options.section != null) {
1423
1482
  const section = requireFieldSectionOption(
1424
1483
  options.section,
1425
1484
  'CustomObjectPageLayout removeField section',
1426
1485
  );
1427
1486
  this.assertFieldSectionBelongsToLayout(section);
1428
- sectionKey = section.getSectionId();
1487
+ sectionKey ??= this.layoutSectionKeyForFieldSection(options.tabKey, section);
1488
+ sectionKey ??= section.getSectionId();
1429
1489
  }
1430
1490
  }
1431
1491
 
@@ -1499,6 +1559,9 @@ export class CustomObjectPageLayout {
1499
1559
  const sectionsOrder = this.ensureSystemSectionsOrder(tabEdit);
1500
1560
  insertAt(sections, normalized, options.position);
1501
1561
  insertUniqueAt(sectionsOrder, layoutSectionKey(normalized), options.position);
1562
+ if (section.type === 'fields_section') {
1563
+ this.rememberFieldSectionLayoutSection(section.section, options.tabKey, layoutSectionKey(normalized));
1564
+ }
1502
1565
  return this;
1503
1566
  }
1504
1567
 
@@ -1516,6 +1579,9 @@ export class CustomObjectPageLayout {
1516
1579
  options.allowDuplicate,
1517
1580
  );
1518
1581
  insertAt(tab.sections, normalized, options.position);
1582
+ if (section.type === 'fields_section') {
1583
+ this.rememberFieldSectionLayoutSection(section.section, tab.key, layoutSectionKey(normalized));
1584
+ }
1519
1585
  return this;
1520
1586
  }
1521
1587
 
@@ -1526,6 +1592,7 @@ export class CustomObjectPageLayout {
1526
1592
  * `systemSectionEdits[sectionKey].deleted`.
1527
1593
  */
1528
1594
  removeSection(section: CustomObjectFieldSection, options?: RemovePageLayoutSectionOptions): this;
1595
+ removeSection(sectionKey: string, options?: RemovePageLayoutSectionOptions): this;
1529
1596
  removeSection(sectionKey: string, options: RemoveSystemPageLayoutSectionOptions): this;
1530
1597
  removeSection(
1531
1598
  sectionOrKey: CustomObjectFieldSection | string,
@@ -1547,9 +1614,14 @@ export class CustomObjectPageLayout {
1547
1614
  return this;
1548
1615
  }
1549
1616
 
1550
- const section = requireFieldSectionOption(sectionOrKey, 'CustomObjectPageLayout removeSection section');
1551
- this.assertFieldSectionBelongsToLayout(section);
1552
- const sectionKey = section.getSectionId();
1617
+ let sectionKey: string;
1618
+ if (typeof sectionOrKey === 'string') {
1619
+ sectionKey = sectionOrKey;
1620
+ } else {
1621
+ const section = requireFieldSectionOption(sectionOrKey, 'CustomObjectPageLayout removeSection section');
1622
+ this.assertFieldSectionBelongsToLayout(section);
1623
+ sectionKey = this.layoutSectionKeyForFieldSection(options.tabKey, section) ?? section.getSectionId();
1624
+ }
1553
1625
  const matches: Array<{
1554
1626
  sections: SerializedPageLayoutSection[];
1555
1627
  section: SerializedFieldsSection;
@@ -1609,6 +1681,10 @@ export class CustomObjectPageLayout {
1609
1681
  const tabsOrder = this.ensureTabsOrder();
1610
1682
  insertAt(newTabs, normalized, options.position);
1611
1683
  insertUniqueAt(tabsOrder, normalized.key, options.position);
1684
+ if (tab.type === 'tab_with_sections') {
1685
+ for (const section of tab.sections)
1686
+ indexPageLayoutSectionInput(this._fieldSectionLayoutSections, tab.key, section);
1687
+ }
1612
1688
  return this;
1613
1689
  }
1614
1690
 
@@ -1805,23 +1881,20 @@ export class CustomObjectPageLayout {
1805
1881
 
1806
1882
  this.assertFieldSectionBelongsToLayout(input.section);
1807
1883
 
1808
- const sectionId = input.section.getSectionId();
1809
- if (inputFieldsSectionHasKey(input)) {
1810
- throw new Error('CustomObjectPageLayout fields_section key is not supported. Omit key.');
1811
- }
1884
+ const sectionKey = input.key ?? input.section.getSectionId();
1812
1885
  const name = input.name ?? input.section.getName();
1813
1886
  const normalized = copySectionOptions<SerializedFieldsSection>(
1814
1887
  {
1815
1888
  name,
1816
1889
  type: 'fields_section',
1817
1890
  fields: input.fields.map((field) =>
1818
- this.normalizeSectionFieldForMutation(input.section, sectionId, field),
1891
+ this.normalizeSectionFieldForMutation(input.section, sectionKey, field),
1819
1892
  ),
1820
1893
  },
1821
1894
  input,
1822
1895
  );
1823
1896
  if (input.layout != null) normalized.layout = input.layout;
1824
- return attachFieldSectionId(normalized, sectionId);
1897
+ return attachLayoutSectionKey(normalized, sectionKey);
1825
1898
  }
1826
1899
 
1827
1900
  private normalizeTabForMutation(tab: PageLayoutTab): SerializedPageLayoutTab {
@@ -1884,6 +1957,35 @@ export class CustomObjectPageLayout {
1884
1957
  }
1885
1958
  }
1886
1959
 
1960
+ private rememberFieldSectionLayoutSection(
1961
+ section: CustomObjectFieldSection,
1962
+ tabKey: string,
1963
+ sectionKey: string | undefined,
1964
+ ): void {
1965
+ if (sectionKey == null) return;
1966
+ addFieldSectionLayoutSection(this._fieldSectionLayoutSections, section.getSectionId(), {
1967
+ tabKey,
1968
+ sectionKey,
1969
+ });
1970
+ }
1971
+
1972
+ private layoutSectionKeyForFieldSection(
1973
+ tabKey: string | undefined,
1974
+ section: CustomObjectFieldSection,
1975
+ ): string | undefined {
1976
+ const entries = this._fieldSectionLayoutSections.get(section.getSectionId()) ?? [];
1977
+ const matches = tabKey == null ? entries : entries.filter((entry) => entry.tabKey === tabKey);
1978
+ const sectionKeys = [...new Set(matches.map((entry) => entry.sectionKey))];
1979
+ if (sectionKeys.length === 0) return undefined;
1980
+ if (sectionKeys.length === 1) return sectionKeys[0];
1981
+
1982
+ const scope = tabKey == null ? '' : ` in tab "${tabKey}"`;
1983
+ throw new Error(
1984
+ `CustomObjectPageLayout field section "${section.getSectionId()}" maps to multiple layout sections${scope}: ` +
1985
+ `${sectionKeys.join(', ')}. Pass sectionKey to choose one.`,
1986
+ );
1987
+ }
1988
+
1887
1989
  private requireOneFieldContainer(
1888
1990
  tabKey: string | undefined,
1889
1991
  sectionKey: string | undefined,
@@ -1913,8 +2015,8 @@ export class CustomObjectPageLayout {
1913
2015
  const container = unkeyedContainers[0] as MutableFieldContainer & {
1914
2016
  section: SerializedFieldsSection;
1915
2017
  };
1916
- attachFieldSectionId(container.section, fallbackFieldSection.getSectionId());
1917
- container.sectionKey = fallbackFieldSection.getSectionId();
2018
+ attachLayoutSectionKey(container.section, sectionKey);
2019
+ container.sectionKey = sectionKey;
1918
2020
  container.label = `${container.tabKey}.${container.sectionKey}`;
1919
2021
  return container;
1920
2022
  }
@@ -417,11 +417,9 @@ export class ExistingManifestContext {
417
417
  return this.getOrLoad(cacheKey, () => {
418
418
  const customObject = this.loadCustomObject(customObjectApiName);
419
419
  this.loadPageLayoutDependencies(customObjectApiName, component);
420
- return CustomObjectPageLayout._fromExistingComponent(customObject, component, (fieldApiName) => {
421
- const fieldComponent = this.findFieldComponent(customObjectApiName, fieldApiName);
422
- const sectionId = fieldComponent?.['section'];
423
- return typeof sectionId === 'string' ? sectionId : undefined;
424
- });
420
+ return CustomObjectPageLayout._fromExistingComponent(customObject, component, (fieldApiName) =>
421
+ this.fieldSectionIdForField(customObjectApiName, fieldApiName),
422
+ );
425
423
  });
426
424
  }
427
425
 
@@ -522,6 +520,12 @@ export class ExistingManifestContext {
522
520
  return matches[0] as Record<string, any>;
523
521
  }
524
522
 
523
+ private fieldSectionIdForField(customObjectApiName: string, fieldApiName: string): string | undefined {
524
+ const field = this.findFieldComponent(customObjectApiName, fieldApiName);
525
+ const section = field?.['section'];
526
+ return typeof section === 'string' && section.length > 0 ? section : undefined;
527
+ }
528
+
525
529
  private loadFieldReferenceComponent(component: Record<string, any>): Field {
526
530
  const cacheKey = this.cacheKey(
527
531
  'CUSTOM_OBJECT_FIELD_REF',
@@ -670,7 +674,8 @@ export class ExistingManifestContext {
670
674
  if (field == null || typeof field !== 'object') return;
671
675
  const record = field as Record<string, any>;
672
676
  const fieldSection = record['fieldSection'] ?? record['field_section'];
673
- const fieldApiName = record['fieldRqlName'] ?? record['field_rql_name'];
677
+ const fieldApiName =
678
+ record['fieldRqlName'] ?? record['field_rql_name'] ?? record['apiName'] ?? record['api_name'];
674
679
  if (typeof fieldSection === 'string') {
675
680
  const fieldLabel = typeof fieldApiName === 'string' ? ` ${fieldApiName}` : '';
676
681
  this.loadRequiredFieldSection(