@rippling/rippling-sdk 0.2.0-alpha.49 → 0.2.0-alpha.51

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 (72) hide show
  1. package/examples/manifest/existing-manifest-mutations/existing-manifest-mutations.ts +5 -0
  2. package/lib/manifest/custom-object-page-layout.d.mts +167 -48
  3. package/lib/manifest/custom-object-page-layout.d.mts.map +1 -1
  4. package/lib/manifest/custom-object-page-layout.d.ts +167 -48
  5. package/lib/manifest/custom-object-page-layout.d.ts.map +1 -1
  6. package/lib/manifest/custom-object-page-layout.js +202 -88
  7. package/lib/manifest/custom-object-page-layout.js.map +1 -1
  8. package/lib/manifest/custom-object-page-layout.mjs +202 -88
  9. package/lib/manifest/custom-object-page-layout.mjs.map +1 -1
  10. package/lib/manifest/custom-object.d.mts +1 -1
  11. package/lib/manifest/custom-object.d.ts +1 -1
  12. package/lib/manifest/custom-object.js +1 -1
  13. package/lib/manifest/custom-object.mjs +1 -1
  14. package/lib/manifest/existing-manifest-context.d.mts +1 -0
  15. package/lib/manifest/existing-manifest-context.d.mts.map +1 -1
  16. package/lib/manifest/existing-manifest-context.d.ts +1 -0
  17. package/lib/manifest/existing-manifest-context.d.ts.map +1 -1
  18. package/lib/manifest/existing-manifest-context.js +7 -6
  19. package/lib/manifest/existing-manifest-context.js.map +1 -1
  20. package/lib/manifest/existing-manifest-context.mjs +7 -6
  21. package/lib/manifest/existing-manifest-context.mjs.map +1 -1
  22. package/lib/manifest/field.d.mts +3 -3
  23. package/lib/manifest/field.d.ts +3 -3
  24. package/lib/manifest/field.js +2 -2
  25. package/lib/manifest/field.mjs +2 -2
  26. package/lib/manifest/index.d.mts +1 -1
  27. package/lib/manifest/index.d.mts.map +1 -1
  28. package/lib/manifest/index.d.ts +1 -1
  29. package/lib/manifest/index.d.ts.map +1 -1
  30. package/lib/manifest/index.js.map +1 -1
  31. package/lib/manifest/index.mjs.map +1 -1
  32. package/lib/sdui/components.d.mts +100 -211
  33. package/lib/sdui/components.d.mts.map +1 -1
  34. package/lib/sdui/components.d.ts +100 -211
  35. package/lib/sdui/components.d.ts.map +1 -1
  36. package/lib/sdui/components.js +96 -182
  37. package/lib/sdui/components.js.map +1 -1
  38. package/lib/sdui/components.mjs +93 -178
  39. package/lib/sdui/components.mjs.map +1 -1
  40. package/lib/sdui/index.d.mts +12 -52
  41. package/lib/sdui/index.d.mts.map +1 -1
  42. package/lib/sdui/index.d.ts +12 -52
  43. package/lib/sdui/index.d.ts.map +1 -1
  44. package/lib/sdui/index.js +10 -50
  45. package/lib/sdui/index.js.map +1 -1
  46. package/lib/sdui/index.mjs +5 -25
  47. package/lib/sdui/index.mjs.map +1 -1
  48. package/lib/sdui/propTypes.d.mts +3156 -3515
  49. package/lib/sdui/propTypes.d.mts.map +1 -1
  50. package/lib/sdui/propTypes.d.ts +3156 -3515
  51. package/lib/sdui/propTypes.d.ts.map +1 -1
  52. package/lib/sdui/propTypes.js +1 -1
  53. package/lib/sdui/propTypes.mjs +1 -1
  54. package/package.json +1 -1
  55. package/resources/leave-balances.d.mts +10 -0
  56. package/resources/leave-balances.d.mts.map +1 -1
  57. package/resources/leave-balances.d.ts +10 -0
  58. package/resources/leave-balances.d.ts.map +1 -1
  59. package/src/lib/manifest/custom-object-page-layout.ts +497 -180
  60. package/src/lib/manifest/custom-object.ts +1 -1
  61. package/src/lib/manifest/existing-manifest-context.ts +11 -6
  62. package/src/lib/manifest/field.ts +3 -3
  63. package/src/lib/manifest/index.ts +27 -0
  64. package/src/lib/sdui/components.ts +181 -332
  65. package/src/lib/sdui/index.ts +13 -93
  66. package/src/lib/sdui/propTypes.ts +3509 -3892
  67. package/src/resources/leave-balances.ts +12 -0
  68. package/src/version.ts +1 -1
  69. package/version.d.mts +1 -1
  70. package/version.d.ts +1 -1
  71. package/version.js +1 -1
  72. package/version.mjs +1 -1
@@ -11,6 +11,23 @@ export interface PageLayoutDeletionIdentifier {
11
11
  objectRqlName: string;
12
12
  }
13
13
 
14
+ export type ObjectRqlName = string;
15
+ export type BlueprintKey = string;
16
+ export type LayoutApiName = string;
17
+ export type TabKey = string;
18
+ export type SectionKey = string;
19
+ export type HeaderFieldKey = string;
20
+ export type FieldRqlName = string;
21
+ export type RelatedObjectRqlName = string;
22
+ export type CanvasCompositionId = string;
23
+ export type HeaderButtonKey = string;
24
+
25
+ export type JsonPrimitive = string | number | boolean | null;
26
+ export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
27
+ export interface JsonObject {
28
+ [key: string]: JsonValue;
29
+ }
30
+
14
31
  // ---------------------------------------------------------------------------
15
32
  // header_edits — HeaderEditsDTO
16
33
  // ---------------------------------------------------------------------------
@@ -18,7 +35,7 @@ export interface PageLayoutDeletionIdentifier {
18
35
  /** Edit to a built-in (system) header field. */
19
36
  export interface SystemHeaderFieldEdit {
20
37
  /** Replace the system field's RQL field reference. Optional. */
21
- newRqlField?: string;
38
+ newRqlField?: FieldRqlName;
22
39
  /** Remove this system field from the header. Optional. */
23
40
  deleted?: boolean;
24
41
  }
@@ -31,9 +48,9 @@ export interface SystemHeaderFieldEdit {
31
48
  */
32
49
  export interface NewHeaderField {
33
50
  /** RQL field name to display. Required. */
34
- rqlName: string;
51
+ rqlName: FieldRqlName;
35
52
  /** Stable key for this field. Required. */
36
- key: string;
53
+ key: HeaderFieldKey;
37
54
  /** Whether the user can remove this field. Optional. */
38
55
  canBeDeleted?: boolean;
39
56
  /** Whether the user can swap this field for another. Optional. */
@@ -53,74 +70,200 @@ export interface NewHeaderField {
53
70
  */
54
71
  export interface HeaderEdits {
55
72
  /** Replace the record title field (pass the field's api_name). Optional. */
56
- newTitleField?: string | null;
73
+ newTitleField?: FieldRqlName;
57
74
  /** Remove the title field from the header. Optional. */
58
75
  titleFieldDeleted?: boolean;
59
76
  /** Replace the record description field. Optional. */
60
- newDescriptionField?: string | null;
77
+ newDescriptionField?: FieldRqlName;
61
78
  /** Remove the description field from the header. Optional. */
62
79
  descriptionFieldDeleted?: boolean;
63
80
  /** Edits to built-in system header fields, keyed by system field key. Optional. */
64
- systemFieldEdits?: Record<string, SystemHeaderFieldEdit>;
81
+ systemFieldEdits?: Record<HeaderFieldKey, SystemHeaderFieldEdit>;
65
82
  /** Additional custom fields to add to the header. Optional. */
66
83
  newFields?: NewHeaderField[];
67
84
  /** Ordered list of header field RQL names. Custom header keys are accepted and converted. Optional. */
68
- headerFieldsOrder?: string[];
85
+ headerFieldsOrder?: FieldRqlName[];
69
86
  /** Action button keys to show in the header. Optional. */
70
- buttons?: string[];
87
+ buttons?: HeaderButtonKey[];
88
+ }
89
+
90
+ interface SerializedSystemHeaderFieldEdit extends Omit<SystemHeaderFieldEdit, 'newRqlField'> {
91
+ newRqlField?: FieldRqlName | null;
92
+ }
93
+
94
+ interface SerializedHeaderEdits
95
+ extends Omit<HeaderEdits, 'newTitleField' | 'newDescriptionField' | 'systemFieldEdits'> {
96
+ newTitleField?: FieldRqlName | null;
97
+ newDescriptionField?: FieldRqlName | null;
98
+ systemFieldEdits?: Record<HeaderFieldKey, SerializedSystemHeaderFieldEdit>;
71
99
  }
72
100
 
73
101
  // ---------------------------------------------------------------------------
74
102
  // tab_edits — TabEditsDTO + section/tab DTOs
75
103
  // ---------------------------------------------------------------------------
76
104
 
105
+ export type SectionFieldsLayout = 'one_column' | 'two_column' | 'three_column' | 'responsive';
106
+ export type TabLayout = 'vertical' | 'horizontal';
107
+ export type SectionMovementOption = 'static' | 'tab' | 'free';
108
+ export type FieldMovementOption = 'tab' | 'free';
109
+ export type CustomObjectCustomSectionComponentKey = 'attachments' | 'comments';
110
+
111
+ /** Default value wrapper used by `SectionField.defaultValue`. */
112
+ export interface SectionFieldDefaultValue {
113
+ value: JsonValue;
114
+ }
115
+
77
116
  /** Options for one field entry inside a `fields_section`. */
78
117
  export interface SectionFieldOptions {
79
118
  /** Hide this field from view. Optional. */
80
119
  hidden?: boolean;
81
120
  /** Allow inline editing of this field. Optional. */
82
121
  editable?: boolean;
122
+ /** Render this field read-only. Optional. */
123
+ readOnly?: boolean;
124
+ /** Default value metadata for the field. Optional. */
125
+ defaultValue?: SectionFieldDefaultValue;
126
+ /** Rich-text field height. Optional. */
127
+ height?: number;
128
+ /** Whether the layout manager can remove this field. Optional. */
129
+ canBeDeleted?: boolean;
83
130
  /** Controls how the field can be moved in the UI. Optional. */
84
- movementOption?: 'tab' | 'free';
131
+ movementOption?: FieldMovementOption;
85
132
  }
86
133
 
87
134
  /** A typed field entry inside a `fields_section`. */
88
- export interface SectionFieldRef extends SectionFieldOptions {
135
+ export interface SectionField extends SectionFieldOptions {
89
136
  /** Field to display. Required. */
90
137
  field: Field;
91
138
  }
92
139
 
140
+ /** @deprecated Use `SectionField` for object entries or `SectionFieldInput` for field arrays. */
141
+ export type SectionFieldRef = SectionField;
142
+
93
143
  /** One field entry inside a `fields_section`. */
94
- export type SectionField = Field | SectionFieldRef;
144
+ export type SectionFieldInput = Field | SectionField;
145
+
146
+ /** Common edit controls for sections. */
147
+ export interface PageLayoutSectionOptions {
148
+ /** Whether the layout manager can delete this section. Optional. */
149
+ canBeDeleted?: boolean;
150
+ /** Whether the layout manager can rename this section. Optional. */
151
+ canBeRenamed?: boolean;
152
+ /** Controls how the section can be moved. Optional. */
153
+ movementOption?: SectionMovementOption;
154
+ /** Hide the section title. Optional. */
155
+ hideName?: boolean;
156
+ }
95
157
 
96
158
  /** A standard field-grid section inside a tab. */
97
- export interface FieldsSection {
159
+ export interface FieldsSection extends PageLayoutSectionOptions {
98
160
  type: 'fields_section';
99
161
  /** Field section metadata for the fields displayed in this layout section. Required. */
100
162
  section: CustomObjectFieldSection;
101
- /** Stable layout section key. Derived from `section.getSectionId()` on the wire. */
102
- key?: never;
163
+ /** Stable layout section key. Optional. Defaults to `section.getSectionId()`. */
164
+ key?: SectionKey;
103
165
  /** Display name of the layout section. Optional. Defaults to the field section name. */
104
166
  name?: string;
105
167
  /** Fields to display in this section. Required. */
106
- fields: SectionField[];
168
+ fields: SectionFieldInput[];
107
169
  /** Column layout for the fields. Optional. @default `'responsive'` (backend default). */
108
170
  layout?: SectionFieldsLayout;
109
171
  }
110
172
 
111
- /**
112
- * A section inside a page layout tab.
113
- *
114
- * Only `fields_section` is modeled today. Add other section types
115
- * (`related_object_section`, `custom_section`, …) explicitly as needed.
116
- */
117
- export type PageLayoutSection = FieldsSection;
173
+ export interface RelatedObjectSection extends PageLayoutSectionOptions {
174
+ type: 'related_object_section';
175
+ /** Stable layout section key. Required. */
176
+ key: SectionKey;
177
+ /** Display name of the layout section. Required. */
178
+ name: string;
179
+ /** Related object whose records point back to this custom object. Required. */
180
+ relatedObjectRqlName: RelatedObjectRqlName;
181
+ /** Field on the related object that points back to this custom object. Required. */
182
+ relatedFieldRqlName: FieldRqlName;
183
+ /** Field RQL names to display for related records. Required. */
184
+ fields: FieldRqlName[];
185
+ }
186
+
187
+ export interface RelatedFieldSection extends PageLayoutSectionOptions {
188
+ type: 'related_field_section';
189
+ /** Stable layout section key. Required. */
190
+ key: SectionKey;
191
+ /** Display name of the layout section. Required. */
192
+ name: string;
193
+ /** Related object reached from this custom object. Required. */
194
+ relatedObjectRqlName: RelatedObjectRqlName;
195
+ /** Field on this custom object that points to the related object. Required. */
196
+ relatedFieldRqlName: FieldRqlName;
197
+ /** Field RQL names to display from the related object. Required. */
198
+ fields: FieldRqlName[];
199
+ /** Column layout for the fields. Optional. @default `'responsive'` (backend default). */
200
+ layout?: SectionFieldsLayout;
201
+ }
202
+
203
+ export interface CustomSection extends PageLayoutSectionOptions {
204
+ type: 'custom_section';
205
+ /** Stable layout section key. Required. */
206
+ key: SectionKey;
207
+ /** Display name of the layout section. Required. */
208
+ name: string;
209
+ /** Custom object section component key. Required. */
210
+ componentKey: CustomObjectCustomSectionComponentKey;
211
+ }
212
+
213
+ export interface ResizableLayoutPosition {
214
+ identifier: string;
215
+ width: number;
216
+ height: number;
217
+ minWidth?: number;
218
+ minHeight?: number;
219
+ maxWidth?: number;
220
+ maxHeight?: number;
221
+ }
222
+
223
+ export interface ReportsSectionItem {
224
+ key: string;
225
+ reportId: string;
226
+ position: ResizableLayoutPosition;
227
+ contextualFieldAttrName?: FieldRqlName;
228
+ visualizationType?: string;
229
+ }
230
+
231
+ export interface ReportsSection extends PageLayoutSectionOptions {
232
+ type: 'reports_section';
233
+ /** Stable layout section key. Required. */
234
+ key: SectionKey;
235
+ /** Display name of the layout section. Required. */
236
+ name: string;
237
+ /** Reports to display in this section. Required. */
238
+ reports: ReportsSectionItem[];
239
+ }
240
+
241
+ export interface CanvasSection extends PageLayoutSectionOptions {
242
+ type: 'canvas_section';
243
+ /** Stable layout section key. Required. */
244
+ key: SectionKey;
245
+ /** Display name of the layout section. Required. */
246
+ name: string;
247
+ /** Canvas composition to render. Required. */
248
+ canvasCompositionId: CanvasCompositionId;
249
+ /** Composition input name to field or state path mapping. Optional. */
250
+ inputMapping?: Record<string, string>;
251
+ }
252
+
253
+ /** A section inside a page layout tab. */
254
+ export type PageLayoutSection =
255
+ | FieldsSection
256
+ | RelatedObjectSection
257
+ | RelatedFieldSection
258
+ | CustomSection
259
+ | ReportsSection
260
+ | CanvasSection;
118
261
 
119
262
  /** A tab that contains one or more field-grid sections. */
120
263
  export interface TabWithSections {
121
264
  type: 'tab_with_sections';
122
265
  /** Stable key for this tab. Required. */
123
- key: string;
266
+ key: TabKey;
124
267
  /** Display name of the tab. Required. */
125
268
  name: string;
126
269
  /** Sections inside this tab. Required. */
@@ -131,7 +274,7 @@ export interface TabWithSections {
131
274
  export interface CustomTab {
132
275
  type: 'custom_tab';
133
276
  /** Stable key for this tab. Required. */
134
- key: string;
277
+ key: TabKey;
135
278
  /** Display name of the tab. Required. */
136
279
  name: string;
137
280
  /** Key of the custom component to render. Required. */
@@ -142,8 +285,6 @@ export interface CustomTab {
142
285
  export type PageLayoutTab = TabWithSections | CustomTab;
143
286
 
144
287
  /** Column layout for a `fields_section`. */
145
- export type SectionFieldsLayout = 'one_column' | 'two_column' | 'three_column' | 'responsive';
146
-
147
288
  /** Edits to a built-in section inside a system tab. */
148
289
  export interface SystemSectionEdit {
149
290
  /** Rename this section. Optional. */
@@ -153,7 +294,7 @@ export interface SystemSectionEdit {
153
294
  /** Change the column layout. Optional. */
154
295
  newLayout?: SectionFieldsLayout;
155
296
  /** Add fields to this section. Optional. */
156
- newFields?: SectionField[];
297
+ newFields?: SectionFieldInput[];
157
298
  /** Hide the section name header. Optional. */
158
299
  newHideName?: boolean;
159
300
  }
@@ -167,9 +308,9 @@ export interface SystemTabEdit {
167
308
  /** Add new sections to this tab. Optional. */
168
309
  newSections?: PageLayoutSection[];
169
310
  /** Edits to built-in sections inside this tab, keyed by section key. Optional. */
170
- systemSectionEdits?: Record<string, SystemSectionEdit>;
311
+ systemSectionEdits?: Record<SectionKey, SystemSectionEdit>;
171
312
  /** Reorder sections by key. Optional. */
172
- sectionsOrder?: string[];
313
+ sectionsOrder?: SectionKey[];
173
314
  }
174
315
 
175
316
  /**
@@ -184,11 +325,11 @@ export interface TabEdits {
184
325
  /** New tabs to add to this layout. Each must include a `type` discriminator. Optional. */
185
326
  newTabs?: PageLayoutTab[];
186
327
  /** Edits to existing system tabs, keyed by tab key. Optional. */
187
- systemTabEdits?: Record<string, SystemTabEdit>;
328
+ systemTabEdits?: Record<TabKey, SystemTabEdit>;
188
329
  /** Ordered list of tab `key` values (new + system). Optional. */
189
- tabsOrder?: string[];
330
+ tabsOrder?: TabKey[];
190
331
  /** Tab strip orientation. Optional. */
191
- newTabLayout?: 'vertical' | 'horizontal';
332
+ newTabLayout?: TabLayout;
192
333
  }
193
334
 
194
335
  // ---------------------------------------------------------------------------
@@ -205,7 +346,7 @@ export interface VisibilityCondition {
205
346
  /** RQL formula that controls visibility. Required. */
206
347
  rqlFormula: string;
207
348
  /** Layered conditions tree (AST). Required. */
208
- layeredConditionsTree: Record<string, any>;
349
+ layeredConditionsTree: JsonObject;
209
350
  }
210
351
 
211
352
  /**
@@ -215,9 +356,9 @@ export interface VisibilityCondition {
215
356
  */
216
357
  export interface VisibilityConditions {
217
358
  /** Per-section visibility conditions, keyed by section key. Optional. */
218
- sections?: Record<string, VisibilityCondition>;
359
+ sections?: Record<SectionKey, VisibilityCondition>;
219
360
  /** Per-tab visibility conditions, keyed by tab key. Optional. */
220
- tabs?: Record<string, VisibilityCondition>;
361
+ tabs?: Record<TabKey, VisibilityCondition>;
221
362
  }
222
363
 
223
364
  // ---------------------------------------------------------------------------
@@ -241,13 +382,13 @@ export interface AddCustomPageLayoutFieldOptions extends AddPageLayoutFieldOptio
241
382
  * across the layout.
242
383
  */
243
384
  tabKey?: string;
244
- /** Field section to add the field to. Required for custom layout sections. */
245
- section: CustomObjectFieldSection;
246
- /** Raw section keys are only accepted for built-in system sections. */
247
- 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;
248
389
  /** Deprecated. Use `section`. */
249
390
  fieldSection?: never;
250
- /** Custom layout sections are selected by `section`; `sectionKey` is only for system sections. */
391
+ /** Custom layout sections are selected by `section`; `sectionKey` can disambiguate. */
251
392
  systemSection?: false;
252
393
  }
253
394
 
@@ -291,9 +432,9 @@ export interface RemoveCustomPageLayoutFieldOptions extends RemovePageLayoutFiel
291
432
  tabKey?: string;
292
433
  /** Limit removal to one field section. Optional. */
293
434
  section?: CustomObjectFieldSection;
294
- /** Raw section keys are only accepted for built-in system sections. */
295
- sectionKey?: never;
296
- /** 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. */
297
438
  systemSection?: false;
298
439
  }
299
440
 
@@ -339,7 +480,7 @@ export interface RemovePageLayoutSectionOptions {
339
480
  * more than once throws. Optional. @default true
340
481
  */
341
482
  removeAll?: boolean;
342
- /** 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`. */
343
484
  systemSection?: false;
344
485
  }
345
486
 
@@ -451,11 +592,13 @@ export interface CustomObjectPageLayoutLoadFromExistingOptions extends ExistingM
451
592
  customObjectApiName?: string;
452
593
  }
453
594
 
595
+ type ResolveFieldSectionId = (fieldRqlName: string) => string | undefined;
596
+
454
597
  interface SerializedSectionField extends SectionFieldOptions {
455
598
  fieldRqlName: string;
456
599
  }
457
600
 
458
- interface SerializedFieldsSection {
601
+ interface SerializedFieldsSection extends PageLayoutSectionOptions {
459
602
  key?: string;
460
603
  type: 'fields_section';
461
604
  name: string;
@@ -463,11 +606,19 @@ interface SerializedFieldsSection {
463
606
  layout?: SectionFieldsLayout;
464
607
  }
465
608
 
609
+ type SerializedPageLayoutSection =
610
+ | SerializedFieldsSection
611
+ | RelatedObjectSection
612
+ | RelatedFieldSection
613
+ | CustomSection
614
+ | ReportsSection
615
+ | CanvasSection;
616
+
466
617
  interface SerializedTabWithSections {
467
618
  type: 'tab_with_sections';
468
619
  key: string;
469
620
  name: string;
470
- sections: SerializedFieldsSection[];
621
+ sections: SerializedPageLayoutSection[];
471
622
  }
472
623
 
473
624
  type SerializedPageLayoutTab = SerializedTabWithSections | CustomTab;
@@ -477,7 +628,7 @@ interface SerializedSystemSectionEdit extends Omit<SystemSectionEdit, 'newFields
477
628
  }
478
629
 
479
630
  interface SerializedSystemTabEdit extends Omit<SystemTabEdit, 'newSections' | 'systemSectionEdits'> {
480
- newSections?: SerializedFieldsSection[];
631
+ newSections?: SerializedPageLayoutSection[];
481
632
  systemSectionEdits?: Record<string, SerializedSystemSectionEdit>;
482
633
  }
483
634
 
@@ -502,46 +653,49 @@ function isField(value: unknown): value is Field {
502
653
  );
503
654
  }
504
655
 
505
- function isFieldRef(value: SectionField): value is SectionFieldRef {
656
+ function isFieldRef(value: SectionFieldInput): value is SectionField {
506
657
  const candidate = value as { field?: unknown } | null;
507
658
  return candidate != null && typeof candidate === 'object' && isField(candidate.field);
508
659
  }
509
660
 
510
- const FIELD_SECTION_ID = Symbol('CustomObjectPageLayout.fieldSectionId');
661
+ const LAYOUT_SECTION_KEY = Symbol('CustomObjectPageLayout.layoutSectionKey');
511
662
 
512
663
  function isCustomObjectFieldSection(value: unknown): value is CustomObjectFieldSection {
513
664
  return value instanceof CustomObjectFieldSection;
514
665
  }
515
666
 
516
- function attachFieldSectionId(section: SerializedFieldsSection, sectionId: string): SerializedFieldsSection {
517
- if (section.key != null && section.key !== sectionId) {
518
- throw new Error(
519
- `CustomObjectPageLayout fields_section key "${section.key}" does not match field section "${sectionId}".`,
520
- );
521
- }
522
- section.key = sectionId;
523
- Object.defineProperty(section, FIELD_SECTION_ID, {
524
- 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,
525
680
  enumerable: false,
526
681
  configurable: true,
527
682
  });
528
683
  return section;
529
684
  }
530
685
 
531
- function layoutSectionKey(section: SerializedFieldsSection): string | undefined {
686
+ function layoutSectionKey(section: { key?: string }): string | undefined {
532
687
  return (
533
- section.key ??
534
- (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]
535
690
  );
536
691
  }
537
692
 
538
- function layoutSectionMatches(section: SerializedFieldsSection, sectionKey: string): boolean {
693
+ function layoutSectionMatches(section: { key?: string }, sectionKey: string): boolean {
539
694
  return layoutSectionKey(section) === sectionKey;
540
695
  }
541
696
 
542
- function inputFieldsSectionHasKey(input: PageLayoutSection): boolean {
543
- const record = input as unknown as Record<string, unknown>;
544
- return Object.prototype.hasOwnProperty.call(record, 'key') && record['key'] != null;
697
+ function isSerializedFieldsSection(section: SerializedPageLayoutSection): section is SerializedFieldsSection {
698
+ return section.type === 'fields_section';
545
699
  }
546
700
 
547
701
  function assertSameObject(customObject: CustomObject, field: Field, sectionKey: string): void {
@@ -585,15 +739,30 @@ function toSerializedSectionField(
585
739
  const normalized: SerializedSectionField = { fieldRqlName };
586
740
  if (options.hidden != null) normalized.hidden = options.hidden;
587
741
  if (options.editable != null) normalized.editable = options.editable;
742
+ if (options.readOnly != null) normalized.readOnly = options.readOnly;
743
+ if (options.defaultValue != null) normalized.defaultValue = options.defaultValue;
744
+ if (options.height != null) normalized.height = options.height;
745
+ if (options.canBeDeleted != null) normalized.canBeDeleted = options.canBeDeleted;
588
746
  if (options.movementOption != null) normalized.movementOption = options.movementOption;
589
747
  return normalized;
590
748
  }
591
749
 
750
+ function copySectionOptions<T extends PageLayoutSectionOptions>(
751
+ target: T,
752
+ input: PageLayoutSectionOptions,
753
+ ): T {
754
+ if (input.canBeDeleted != null) target.canBeDeleted = input.canBeDeleted;
755
+ if (input.canBeRenamed != null) target.canBeRenamed = input.canBeRenamed;
756
+ if (input.movementOption != null) target.movementOption = input.movementOption;
757
+ if (input.hideName != null) target.hideName = input.hideName;
758
+ return target;
759
+ }
760
+
592
761
  function normalizeSectionField(
593
762
  customObject: CustomObject,
594
763
  section: CustomObjectFieldSection | string,
595
764
  sectionKey: string,
596
- entry: SectionField,
765
+ entry: SectionFieldInput,
597
766
  ): SerializedSectionField {
598
767
  if (isField(entry)) {
599
768
  assertSameObject(customObject, entry, sectionKey);
@@ -613,30 +782,37 @@ function normalizeSectionField(
613
782
  );
614
783
  }
615
784
 
616
- function normalizeFieldsSection(
617
- customObject: CustomObject,
618
- input: PageLayoutSection,
619
- ): SerializedFieldsSection {
785
+ function normalizeFieldsSection(customObject: CustomObject, input: FieldsSection): SerializedFieldsSection {
620
786
  assertSectionBelongsTo(customObject, input.section);
621
- const sectionId = input.section.getSectionId();
622
- if (inputFieldsSectionHasKey(input)) {
623
- throw new Error('CustomObjectPageLayout fields_section key is not supported. Omit key.');
624
- }
787
+ const sectionKey = input.key ?? input.section.getSectionId();
625
788
  const name = input.name ?? input.section.getName();
626
- const normalized: SerializedFieldsSection = {
627
- name,
628
- type: 'fields_section',
629
- fields: input.fields.map((field) => normalizeSectionField(customObject, input.section, sectionId, field)),
630
- };
789
+ const normalized = copySectionOptions<SerializedFieldsSection>(
790
+ {
791
+ name,
792
+ type: 'fields_section',
793
+ fields: input.fields.map((field) =>
794
+ normalizeSectionField(customObject, input.section, sectionKey, field),
795
+ ),
796
+ },
797
+ input,
798
+ );
631
799
  if (input.layout != null) normalized.layout = input.layout;
632
- return attachFieldSectionId(normalized, sectionId);
800
+ return attachLayoutSectionKey(normalized, sectionKey);
801
+ }
802
+
803
+ function normalizeSection(
804
+ customObject: CustomObject,
805
+ section: PageLayoutSection,
806
+ ): SerializedPageLayoutSection {
807
+ if (section.type === 'fields_section') return normalizeFieldsSection(customObject, section);
808
+ return { ...section };
633
809
  }
634
810
 
635
811
  function normalizeTab(customObject: CustomObject, tab: PageLayoutTab): SerializedPageLayoutTab {
636
812
  if (tab.type === 'custom_tab') return tab;
637
813
  return {
638
814
  ...tab,
639
- sections: tab.sections.map((section) => normalizeFieldsSection(customObject, section)),
815
+ sections: tab.sections.map((section) => normalizeSection(customObject, section)),
640
816
  };
641
817
  }
642
818
 
@@ -664,7 +840,7 @@ function normalizeSystemTabEdit(
664
840
  const { newSections: _newSections, systemSectionEdits: _systemSectionEdits, ...rest } = edit;
665
841
  const normalized: SerializedSystemTabEdit = { ...rest };
666
842
  if (edit.newSections != null) {
667
- normalized.newSections = edit.newSections.map((section) => normalizeFieldsSection(customObject, section));
843
+ normalized.newSections = edit.newSections.map((section) => normalizeSection(customObject, section));
668
844
  }
669
845
  if (edit.systemSectionEdits != null) {
670
846
  const systemSectionEdits: Record<string, SerializedSystemSectionEdit> = {};
@@ -730,7 +906,7 @@ function assertKnownKeys(
730
906
  }
731
907
  }
732
908
 
733
- function normalizeHeaderEdits(headerEdits: HeaderEdits): HeaderEdits {
909
+ function normalizeHeaderEdits(headerEdits: HeaderEdits): SerializedHeaderEdits {
734
910
  assertKnownKeys(
735
911
  headerEdits as unknown as Record<string, unknown>,
736
912
  HEADER_EDIT_KEYS,
@@ -756,6 +932,27 @@ function normalizeHeaderEdits(headerEdits: HeaderEdits): HeaderEdits {
756
932
  );
757
933
  }
758
934
 
935
+ if (headerEdits.newTitleField === '') {
936
+ throw new Error(
937
+ 'CustomObjectPageLayout headerEdits.newTitleField cannot be empty. Use titleFieldDeleted.',
938
+ );
939
+ }
940
+ if (headerEdits.newDescriptionField === '') {
941
+ throw new Error(
942
+ 'CustomObjectPageLayout headerEdits.newDescriptionField cannot be empty. Use descriptionFieldDeleted.',
943
+ );
944
+ }
945
+ if (headerEdits.newTitleField != null && headerEdits.titleFieldDeleted === true) {
946
+ throw new Error(
947
+ 'CustomObjectPageLayout headerEdits cannot both set newTitleField and titleFieldDeleted.',
948
+ );
949
+ }
950
+ if (headerEdits.newDescriptionField != null && headerEdits.descriptionFieldDeleted === true) {
951
+ throw new Error(
952
+ 'CustomObjectPageLayout headerEdits cannot both set newDescriptionField and descriptionFieldDeleted.',
953
+ );
954
+ }
955
+
759
956
  if (headerEdits.headerFieldsOrder == null) return headerEdits;
760
957
  return {
761
958
  ...headerEdits,
@@ -768,13 +965,13 @@ function addUniqueHeaderOrderField(order: string[], field: unknown): void {
768
965
  if (!order.includes(field)) order.push(field);
769
966
  }
770
967
 
771
- function normalizeHeaderFieldsOrder(headerEdits: HeaderEdits): string[] {
968
+ function normalizeHeaderFieldsOrder(headerEdits: SerializedHeaderEdits): string[] {
772
969
  const customFieldRqlByKey = new Map(
773
970
  (headerEdits.newFields ?? []).map((field) => [field.key, field.rqlName]),
774
971
  );
775
972
  const systemFieldRqlByKey = new Map(
776
973
  Object.entries(headerEdits.systemFieldEdits ?? {})
777
- .filter((entry): entry is [string, SystemHeaderFieldEdit & { newRqlField: string }] => {
974
+ .filter((entry): entry is [string, SerializedSystemHeaderFieldEdit & { newRqlField: string }] => {
778
975
  const [, edit] = entry;
779
976
  return typeof edit.newRqlField === 'string' && edit.newRqlField.length > 0;
780
977
  })
@@ -811,6 +1008,11 @@ interface MutableFieldContainer {
811
1008
  label: string;
812
1009
  }
813
1010
 
1011
+ interface FieldSectionLayoutSection {
1012
+ tabKey: string;
1013
+ sectionKey: string;
1014
+ }
1015
+
814
1016
  function isTabWithSections(tab: SerializedPageLayoutTab): tab is SerializedTabWithSections {
815
1017
  return tab.type === 'tab_with_sections';
816
1018
  }
@@ -841,66 +1043,126 @@ function fieldRqlNameOf(entry: unknown): string | undefined {
841
1043
  if (typeof entry === 'string') return entry;
842
1044
  if (entry == null || typeof entry !== 'object') return undefined;
843
1045
  const record = entry as Record<string, any>;
844
- const fieldRqlName = record['fieldRqlName'] ?? record['field_rql_name'];
1046
+ const fieldRqlName =
1047
+ record['fieldRqlName'] ?? record['field_rql_name'] ?? record['apiName'] ?? record['api_name'];
845
1048
  return typeof fieldRqlName === 'string' ? fieldRqlName : undefined;
846
1049
  }
847
1050
 
848
- function explicitFieldSectionId(entry: unknown): string | undefined {
849
- if (entry == null || typeof entry !== 'object') return undefined;
850
- const record = entry as Record<string, any>;
851
- const fieldSectionId = record['fieldSection'] ?? record['field_section'];
852
- 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);
853
1063
  }
854
1064
 
855
- function inferredSectionIdFromFields(
856
- section: SerializedFieldsSection,
857
- resolveFieldSectionId?: (fieldApiName: string) => string | undefined,
858
- ): string | undefined {
859
- const sectionIds = new Set<string>();
860
- for (const field of section.fields ?? []) {
861
- const explicitSectionId = explicitFieldSectionId(field);
862
- if (explicitSectionId != null) {
863
- sectionIds.add(explicitSectionId);
864
- continue;
865
- }
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;
1089
+
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
+ }
866
1094
 
867
- const fieldApiName = fieldRqlNameOf(field);
868
- if (fieldApiName == null || resolveFieldSectionId == null) continue;
869
- const resolvedSectionId = resolveFieldSectionId(fieldApiName);
870
- if (resolvedSectionId != null) sectionIds.add(resolvedSectionId);
1095
+ for (const [tabKey, tabEdit] of Object.entries(tabEdits.systemTabEdits ?? {})) {
1096
+ for (const section of tabEdit.newSections ?? []) {
1097
+ indexPageLayoutSectionInput(index, tabKey, section);
1098
+ }
871
1099
  }
872
- return sectionIds.size === 1 ? [...sectionIds][0] : undefined;
1100
+
1101
+ return index;
873
1102
  }
874
1103
 
875
- function annotateLoadedFieldsSection(
1104
+ function indexLoadedFieldsSection(
1105
+ index: Map<string, FieldSectionLayoutSection[]>,
1106
+ tabKey: string,
876
1107
  section: SerializedFieldsSection,
877
- resolveFieldSectionId?: (fieldApiName: string) => string | undefined,
878
- ): SerializedFieldsSection {
879
- const explicitSectionId = layoutSectionKey(section);
880
- const inferredSectionId = inferredSectionIdFromFields(section, resolveFieldSectionId);
881
- if (explicitSectionId != null && inferredSectionId != null && explicitSectionId !== inferredSectionId) {
882
- throw new Error(
883
- `CustomObjectPageLayout fields_section key "${explicitSectionId}" does not match ` +
884
- `field section "${inferredSectionId}".`,
885
- );
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
+ }
886
1120
  }
887
- const sectionId = explicitSectionId ?? inferredSectionId;
888
- return sectionId == null ? section : attachFieldSectionId(section, sectionId);
889
1121
  }
890
1122
 
891
- function annotateLoadedTabEdits(
1123
+ function buildFieldSectionLayoutIndexFromLoadedTabEdits(
892
1124
  tabEdits: SerializedTabEdits,
893
- resolveFieldSectionId?: (fieldApiName: string) => string | undefined,
894
- ): 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 {
895
1155
  for (const tab of tabEdits.newTabs ?? []) {
896
1156
  if (!isTabWithSections(tab)) continue;
897
- tab.sections = tab.sections.map((section) => annotateLoadedFieldsSection(section, resolveFieldSectionId));
1157
+ tab.sections = tab.sections.map((section) =>
1158
+ isSerializedFieldsSection(section) ? annotateLoadedFieldsSection(section) : section,
1159
+ );
898
1160
  }
899
1161
 
900
1162
  for (const tabEdit of Object.values(tabEdits.systemTabEdits ?? {})) {
901
1163
  if (tabEdit.newSections != null) {
902
1164
  tabEdit.newSections = tabEdit.newSections.map((section) =>
903
- annotateLoadedFieldsSection(section, resolveFieldSectionId),
1165
+ isSerializedFieldsSection(section) ? annotateLoadedFieldsSection(section) : section,
904
1166
  );
905
1167
  }
906
1168
  }
@@ -1020,9 +1282,10 @@ export class CustomObjectPageLayout {
1020
1282
  private readonly _customObjectApiName: string;
1021
1283
  private readonly _name: string;
1022
1284
  private readonly _tabEdits: SerializedTabEdits;
1023
- private readonly _headerEdits: HeaderEdits;
1285
+ private readonly _headerEdits: SerializedHeaderEdits;
1024
1286
  private readonly _visibilityConditions: VisibilityConditions;
1025
1287
  private readonly _blueprintKey: string | undefined;
1288
+ private readonly _fieldSectionLayoutSections: Map<string, FieldSectionLayoutSection[]>;
1026
1289
 
1027
1290
  /**
1028
1291
  * @param customObject - The custom object this layout belongs to.
@@ -1037,6 +1300,7 @@ export class CustomObjectPageLayout {
1037
1300
  this._customObjectApiName = customObject.getApiName();
1038
1301
  this._name = props.name;
1039
1302
  this._tabEdits = normalizeTabEdits(customObject, props.tabEdits ?? {});
1303
+ this._fieldSectionLayoutSections = buildFieldSectionLayoutIndexFromProps(props.tabEdits);
1040
1304
  this._headerEdits = normalizeHeaderEdits(props.headerEdits ?? {});
1041
1305
  this._visibilityConditions = props.visibilityConditions ?? {};
1042
1306
  this._blueprintKey = props.blueprintKey;
@@ -1061,9 +1325,10 @@ export class CustomObjectPageLayout {
1061
1325
  static _fromExistingComponent(
1062
1326
  customObject: CustomObject,
1063
1327
  component: Record<string, any>,
1064
- resolveFieldSectionId?: (fieldApiName: string) => string | undefined,
1328
+ resolveFieldSectionId?: ResolveFieldSectionId,
1065
1329
  ): CustomObjectPageLayout {
1066
1330
  const layout = Object.create(CustomObjectPageLayout.prototype) as CustomObjectPageLayout;
1331
+ const tabEdits = annotateLoadedTabEdits(cloneJson(component['tab_edits'] ?? {}));
1067
1332
  Object.assign(layout as Record<string, any>, {
1068
1333
  _layoutId: requireStringComponentField(component, 'layout_id', CustomObjectPageLayout.componentType),
1069
1334
  _apiName: requireStringComponentField(component, 'api_name', CustomObjectPageLayout.componentType),
@@ -1073,10 +1338,14 @@ export class CustomObjectPageLayout {
1073
1338
  CustomObjectPageLayout.componentType,
1074
1339
  ),
1075
1340
  _name: requireStringComponentField(component, 'name', CustomObjectPageLayout.componentType),
1076
- _tabEdits: annotateLoadedTabEdits(cloneJson(component['tab_edits'] ?? {}), resolveFieldSectionId),
1341
+ _tabEdits: tabEdits,
1077
1342
  _headerEdits: cloneJson(component['header_edits'] ?? {}),
1078
1343
  _visibilityConditions: cloneJson(component['visibility_conditions'] ?? {}),
1079
1344
  _blueprintKey: component['blueprint_key'],
1345
+ _fieldSectionLayoutSections: buildFieldSectionLayoutIndexFromLoadedTabEdits(
1346
+ tabEdits,
1347
+ resolveFieldSectionId,
1348
+ ),
1080
1349
  });
1081
1350
  customObject._register(layout);
1082
1351
  return layout;
@@ -1117,8 +1386,8 @@ export class CustomObjectPageLayout {
1117
1386
  * Adds multiple fields to an existing layout section.
1118
1387
  *
1119
1388
  * Fields are inserted in the order provided. For custom layout sections,
1120
- * pass `options.section` so TypeScript and runtime validation agree on the
1121
- * 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.
1122
1391
  */
1123
1392
  addFields(fields: Field[], options: AddPageLayoutFieldsOptions): this {
1124
1393
  if (fields.length === 0) return this;
@@ -1141,25 +1410,25 @@ export class CustomObjectPageLayout {
1141
1410
  return this.ensureSystemSectionFieldContainer(options.tabKey, options.sectionKey);
1142
1411
  })()
1143
1412
  : (() => {
1144
- assertOptionAbsent(
1145
- options,
1146
- 'sectionKey',
1147
- 'CustomObjectPageLayout addField sectionKey is only supported with systemSection: true. ' +
1148
- 'Pass section: CustomObjectFieldSection for custom layout sections.',
1149
- );
1150
1413
  assertOptionAbsent(
1151
1414
  options,
1152
1415
  'fieldSection',
1153
1416
  'CustomObjectPageLayout addField fieldSection is no longer supported. ' +
1154
1417
  'Pass section: CustomObjectFieldSection instead.',
1155
1418
  );
1156
- const section = requireFieldSectionOption(
1157
- options.section,
1158
- 'CustomObjectPageLayout addField section',
1159
- );
1160
- this.assertFieldSectionBelongsToLayout(section);
1161
- fieldSectionForMutation = section;
1162
- 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);
1163
1432
  })();
1164
1433
  const entries = fields.map((field) =>
1165
1434
  this.serializeFieldForMutation(
@@ -1208,19 +1477,15 @@ export class CustomObjectPageLayout {
1208
1477
  );
1209
1478
  sectionKey = options.sectionKey;
1210
1479
  } else {
1211
- assertOptionAbsent(
1212
- options,
1213
- 'sectionKey',
1214
- 'CustomObjectPageLayout removeField sectionKey is only supported with systemSection: true. ' +
1215
- 'Pass section: CustomObjectFieldSection for custom layout sections.',
1216
- );
1480
+ sectionKey = options.sectionKey;
1217
1481
  if (options.section != null) {
1218
1482
  const section = requireFieldSectionOption(
1219
1483
  options.section,
1220
1484
  'CustomObjectPageLayout removeField section',
1221
1485
  );
1222
1486
  this.assertFieldSectionBelongsToLayout(section);
1223
- sectionKey = section.getSectionId();
1487
+ sectionKey ??= this.layoutSectionKeyForFieldSection(options.tabKey, section);
1488
+ sectionKey ??= section.getSectionId();
1224
1489
  }
1225
1490
  }
1226
1491
 
@@ -1280,7 +1545,7 @@ export class CustomObjectPageLayout {
1280
1545
  const normalized = this.normalizeSectionForMutation(section);
1281
1546
  if (options.systemTab === true) {
1282
1547
  const tabEdit = this.ensureSystemTabEdit(options.tabKey);
1283
- const sections = ensureArrayProperty<SerializedFieldsSection>(
1548
+ const sections = ensureArrayProperty<SerializedPageLayoutSection>(
1284
1549
  tabEdit as Record<string, any>,
1285
1550
  'newSections',
1286
1551
  `systemTabEdits.${options.tabKey}.newSections`,
@@ -1294,6 +1559,9 @@ export class CustomObjectPageLayout {
1294
1559
  const sectionsOrder = this.ensureSystemSectionsOrder(tabEdit);
1295
1560
  insertAt(sections, normalized, options.position);
1296
1561
  insertUniqueAt(sectionsOrder, layoutSectionKey(normalized), options.position);
1562
+ if (section.type === 'fields_section') {
1563
+ this.rememberFieldSectionLayoutSection(section.section, options.tabKey, layoutSectionKey(normalized));
1564
+ }
1297
1565
  return this;
1298
1566
  }
1299
1567
 
@@ -1311,6 +1579,9 @@ export class CustomObjectPageLayout {
1311
1579
  options.allowDuplicate,
1312
1580
  );
1313
1581
  insertAt(tab.sections, normalized, options.position);
1582
+ if (section.type === 'fields_section') {
1583
+ this.rememberFieldSectionLayoutSection(section.section, tab.key, layoutSectionKey(normalized));
1584
+ }
1314
1585
  return this;
1315
1586
  }
1316
1587
 
@@ -1321,6 +1592,7 @@ export class CustomObjectPageLayout {
1321
1592
  * `systemSectionEdits[sectionKey].deleted`.
1322
1593
  */
1323
1594
  removeSection(section: CustomObjectFieldSection, options?: RemovePageLayoutSectionOptions): this;
1595
+ removeSection(sectionKey: string, options?: RemovePageLayoutSectionOptions): this;
1324
1596
  removeSection(sectionKey: string, options: RemoveSystemPageLayoutSectionOptions): this;
1325
1597
  removeSection(
1326
1598
  sectionOrKey: CustomObjectFieldSection | string,
@@ -1342,11 +1614,16 @@ export class CustomObjectPageLayout {
1342
1614
  return this;
1343
1615
  }
1344
1616
 
1345
- const section = requireFieldSectionOption(sectionOrKey, 'CustomObjectPageLayout removeSection section');
1346
- this.assertFieldSectionBelongsToLayout(section);
1347
- 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
+ }
1348
1625
  const matches: Array<{
1349
- sections: SerializedFieldsSection[];
1626
+ sections: SerializedPageLayoutSection[];
1350
1627
  section: SerializedFieldsSection;
1351
1628
  index: number;
1352
1629
  tabEdit?: SerializedSystemTabEdit;
@@ -1356,14 +1633,14 @@ export class CustomObjectPageLayout {
1356
1633
  if (!isTabWithSections(tab)) continue;
1357
1634
  if (options.tabKey != null && tab.key !== options.tabKey) continue;
1358
1635
  tab.sections.forEach((section, index) => {
1359
- if (layoutSectionMatches(section, sectionKey))
1636
+ if (isSerializedFieldsSection(section) && layoutSectionMatches(section, sectionKey))
1360
1637
  matches.push({ sections: tab.sections, section, index, tabKey: tab.key });
1361
1638
  });
1362
1639
  }
1363
1640
  for (const [tabKey, tabEdit] of Object.entries(this._tabEdits.systemTabEdits ?? {})) {
1364
1641
  if (options.tabKey != null && tabKey !== options.tabKey) continue;
1365
1642
  (tabEdit.newSections ?? []).forEach((section, index) => {
1366
- if (layoutSectionMatches(section, sectionKey))
1643
+ if (isSerializedFieldsSection(section) && layoutSectionMatches(section, sectionKey))
1367
1644
  matches.push({ sections: tabEdit.newSections!, section, index, tabEdit, tabKey });
1368
1645
  });
1369
1646
  }
@@ -1380,7 +1657,7 @@ export class CustomObjectPageLayout {
1380
1657
  }
1381
1658
 
1382
1659
  const removals = removeAll ? matches : matches.slice(0, 1);
1383
- const byArray = new Map<SerializedFieldsSection[], number[]>();
1660
+ const byArray = new Map<SerializedPageLayoutSection[], number[]>();
1384
1661
  for (const match of removals) {
1385
1662
  const indexes = byArray.get(match.sections) ?? [];
1386
1663
  indexes.push(match.index);
@@ -1404,6 +1681,10 @@ export class CustomObjectPageLayout {
1404
1681
  const tabsOrder = this.ensureTabsOrder();
1405
1682
  insertAt(newTabs, normalized, options.position);
1406
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
+ }
1407
1688
  return this;
1408
1689
  }
1409
1690
 
@@ -1474,11 +1755,13 @@ export class CustomObjectPageLayout {
1474
1755
  }
1475
1756
 
1476
1757
  /** Sets the record title field in `header_edits`. */
1477
- setTitleField(field: Field | string | null): this {
1758
+ setTitleField(field: Field): this;
1759
+ setTitleField(fieldApiName: FieldRqlName): this;
1760
+ setTitleField(field: Field | FieldRqlName): this {
1478
1761
  const previousTitleField = this._headerEdits.newTitleField;
1479
1762
  const nextTitleField = this.headerFieldApiName(field);
1480
1763
  this._headerEdits.newTitleField = nextTitleField;
1481
- if (field != null) this._headerEdits.titleFieldDeleted = false;
1764
+ this._headerEdits.titleFieldDeleted = false;
1482
1765
  insertUniqueAt(this.ensureHeaderFieldsOrder(), nextTitleField, 'end');
1483
1766
  this.removeHeaderOrderFieldIfUnused(previousTitleField);
1484
1767
  return this;
@@ -1494,11 +1777,13 @@ export class CustomObjectPageLayout {
1494
1777
  }
1495
1778
 
1496
1779
  /** Sets the record description field in `header_edits`. */
1497
- setDescriptionField(field: Field | string | null): this {
1780
+ setDescriptionField(field: Field): this;
1781
+ setDescriptionField(fieldApiName: FieldRqlName): this;
1782
+ setDescriptionField(field: Field | FieldRqlName): this {
1498
1783
  const previousDescriptionField = this._headerEdits.newDescriptionField;
1499
1784
  const nextDescriptionField = this.headerFieldApiName(field);
1500
1785
  this._headerEdits.newDescriptionField = nextDescriptionField;
1501
- if (field != null) this._headerEdits.descriptionFieldDeleted = false;
1786
+ this._headerEdits.descriptionFieldDeleted = false;
1502
1787
  insertUniqueAt(this.ensureHeaderFieldsOrder(), nextDescriptionField, 'end');
1503
1788
  this.removeHeaderOrderFieldIfUnused(previousDescriptionField);
1504
1789
  return this;
@@ -1571,7 +1856,7 @@ export class CustomObjectPageLayout {
1571
1856
  private normalizeSectionFieldForMutation(
1572
1857
  section: CustomObjectFieldSection | string,
1573
1858
  sectionKey: string,
1574
- entry: SectionField,
1859
+ entry: SectionFieldInput,
1575
1860
  ): SerializedSectionField {
1576
1861
  if (isField(entry)) {
1577
1862
  this.assertFieldBelongsToLayout(entry);
@@ -1591,23 +1876,25 @@ export class CustomObjectPageLayout {
1591
1876
  );
1592
1877
  }
1593
1878
 
1594
- private normalizeSectionForMutation(input: PageLayoutSection): SerializedFieldsSection {
1879
+ private normalizeSectionForMutation(input: PageLayoutSection): SerializedPageLayoutSection {
1880
+ if (input.type !== 'fields_section') return { ...input };
1881
+
1595
1882
  this.assertFieldSectionBelongsToLayout(input.section);
1596
1883
 
1597
- const sectionId = input.section.getSectionId();
1598
- if (inputFieldsSectionHasKey(input)) {
1599
- throw new Error('CustomObjectPageLayout fields_section key is not supported. Omit key.');
1600
- }
1884
+ const sectionKey = input.key ?? input.section.getSectionId();
1601
1885
  const name = input.name ?? input.section.getName();
1602
- const normalized: SerializedFieldsSection = {
1603
- name,
1604
- type: 'fields_section',
1605
- fields: input.fields.map((field) =>
1606
- this.normalizeSectionFieldForMutation(input.section, sectionId, field),
1607
- ),
1608
- };
1886
+ const normalized = copySectionOptions<SerializedFieldsSection>(
1887
+ {
1888
+ name,
1889
+ type: 'fields_section',
1890
+ fields: input.fields.map((field) =>
1891
+ this.normalizeSectionFieldForMutation(input.section, sectionKey, field),
1892
+ ),
1893
+ },
1894
+ input,
1895
+ );
1609
1896
  if (input.layout != null) normalized.layout = input.layout;
1610
- return attachFieldSectionId(normalized, sectionId);
1897
+ return attachLayoutSectionKey(normalized, sectionKey);
1611
1898
  }
1612
1899
 
1613
1900
  private normalizeTabForMutation(tab: PageLayoutTab): SerializedPageLayoutTab {
@@ -1670,6 +1957,35 @@ export class CustomObjectPageLayout {
1670
1957
  }
1671
1958
  }
1672
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
+
1673
1989
  private requireOneFieldContainer(
1674
1990
  tabKey: string | undefined,
1675
1991
  sectionKey: string | undefined,
@@ -1699,8 +2015,8 @@ export class CustomObjectPageLayout {
1699
2015
  const container = unkeyedContainers[0] as MutableFieldContainer & {
1700
2016
  section: SerializedFieldsSection;
1701
2017
  };
1702
- attachFieldSectionId(container.section, fallbackFieldSection.getSectionId());
1703
- container.sectionKey = fallbackFieldSection.getSectionId();
2018
+ attachLayoutSectionKey(container.section, sectionKey);
2019
+ container.sectionKey = sectionKey;
1704
2020
  container.label = `${container.tabKey}.${container.sectionKey}`;
1705
2021
  return container;
1706
2022
  }
@@ -1738,6 +2054,7 @@ export class CustomObjectPageLayout {
1738
2054
  if (!isTabWithSections(tab)) continue;
1739
2055
  if (filter.tabKey != null && tab.key !== filter.tabKey) continue;
1740
2056
  for (const [index, section] of tab.sections.entries()) {
2057
+ if (!isSerializedFieldsSection(section)) continue;
1741
2058
  const sectionKey = layoutSectionKey(section);
1742
2059
  if (filter.sectionKey != null && !layoutSectionMatches(section, filter.sectionKey)) continue;
1743
2060
  const label = sectionKey == null ? `${tab.key}.sections[${index}]` : `${tab.key}.${sectionKey}`;
@@ -1758,6 +2075,7 @@ export class CustomObjectPageLayout {
1758
2075
  for (const [tabKey, tabEdit] of Object.entries(this._tabEdits.systemTabEdits ?? {})) {
1759
2076
  if (filter.tabKey != null && tabKey !== filter.tabKey) continue;
1760
2077
  for (const [index, section] of (tabEdit.newSections ?? []).entries()) {
2078
+ if (!isSerializedFieldsSection(section)) continue;
1761
2079
  const sectionKey = layoutSectionKey(section);
1762
2080
  if (filter.sectionKey != null && !layoutSectionMatches(section, filter.sectionKey)) continue;
1763
2081
  const label = sectionKey == null ? `${tabKey}.newSections[${index}]` : `${tabKey}.${sectionKey}`;
@@ -1870,7 +2188,7 @@ export class CustomObjectPageLayout {
1870
2188
  }
1871
2189
 
1872
2190
  private assertSectionKeyAvailable(
1873
- sections: SerializedFieldsSection[],
2191
+ sections: SerializedPageLayoutSection[],
1874
2192
  sectionKey: string | undefined,
1875
2193
  tabKey: string,
1876
2194
  allowDuplicate: boolean | undefined,
@@ -1903,8 +2221,7 @@ export class CustomObjectPageLayout {
1903
2221
  return normalized;
1904
2222
  }
1905
2223
 
1906
- private headerFieldApiName(field: Field | string | null): string | null {
1907
- if (field == null) return null;
2224
+ private headerFieldApiName(field: Field | FieldRqlName): FieldRqlName {
1908
2225
  if (typeof field === 'string') return field;
1909
2226
  this.assertFieldBelongsToLayout(field);
1910
2227
  return field.getApiName();