@rippling/rippling-sdk 0.2.0-alpha.44 → 0.2.0-alpha.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/manifest/dental-practice-management/dental-practice-management.ts +0 -7
- package/examples/manifest/dental-practice-management/manifest.json +0 -7
- package/examples/manifest/existing-manifest-mutations/existing-manifest-mutations.ts +6 -7
- package/examples/manifest/gym-membership-management/gym-membership-management.ts +0 -7
- package/examples/manifest/gym-membership-management/manifest.json +0 -7
- package/lib/manifest/app.d.mts +2 -2
- package/lib/manifest/app.d.mts.map +1 -1
- package/lib/manifest/app.d.ts +2 -2
- package/lib/manifest/app.d.ts.map +1 -1
- package/lib/manifest/app.js +15 -4
- package/lib/manifest/app.js.map +1 -1
- package/lib/manifest/app.mjs +15 -4
- package/lib/manifest/app.mjs.map +1 -1
- package/lib/manifest/custom-object-field-section.d.mts +2 -2
- package/lib/manifest/custom-object-field-section.d.ts +2 -2
- package/lib/manifest/custom-object-field-section.js +2 -2
- package/lib/manifest/custom-object-field-section.mjs +2 -2
- package/lib/manifest/custom-object-page-layout.d.mts +86 -40
- package/lib/manifest/custom-object-page-layout.d.mts.map +1 -1
- package/lib/manifest/custom-object-page-layout.d.ts +86 -40
- package/lib/manifest/custom-object-page-layout.d.ts.map +1 -1
- package/lib/manifest/custom-object-page-layout.js +369 -79
- package/lib/manifest/custom-object-page-layout.js.map +1 -1
- package/lib/manifest/custom-object-page-layout.mjs +369 -79
- package/lib/manifest/custom-object-page-layout.mjs.map +1 -1
- package/lib/manifest/custom-object.d.mts +6 -0
- package/lib/manifest/custom-object.d.mts.map +1 -1
- package/lib/manifest/custom-object.d.ts +6 -0
- package/lib/manifest/custom-object.d.ts.map +1 -1
- package/lib/manifest/custom-object.js +6 -0
- package/lib/manifest/custom-object.js.map +1 -1
- package/lib/manifest/custom-object.mjs +6 -0
- package/lib/manifest/custom-object.mjs.map +1 -1
- package/lib/manifest/existing-manifest-context.d.mts +0 -1
- package/lib/manifest/existing-manifest-context.d.mts.map +1 -1
- package/lib/manifest/existing-manifest-context.d.ts +0 -1
- package/lib/manifest/existing-manifest-context.d.ts.map +1 -1
- package/lib/manifest/existing-manifest-context.js +5 -10
- package/lib/manifest/existing-manifest-context.js.map +1 -1
- package/lib/manifest/existing-manifest-context.mjs +5 -10
- package/lib/manifest/existing-manifest-context.mjs.map +1 -1
- package/lib/manifest/field.d.mts +6 -3
- package/lib/manifest/field.d.mts.map +1 -1
- package/lib/manifest/field.d.ts +6 -3
- package/lib/manifest/field.d.ts.map +1 -1
- package/lib/manifest/field.js +26 -6
- package/lib/manifest/field.js.map +1 -1
- package/lib/manifest/field.mjs +26 -6
- package/lib/manifest/field.mjs.map +1 -1
- package/package.json +1 -1
- package/src/lib/manifest/app.ts +19 -5
- package/src/lib/manifest/custom-object-field-section.ts +2 -2
- package/src/lib/manifest/custom-object-page-layout.ts +599 -109
- package/src/lib/manifest/custom-object.ts +6 -0
- package/src/lib/manifest/existing-manifest-context.ts +5 -12
- package/src/lib/manifest/field.ts +45 -9
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
|
@@ -27,7 +27,7 @@ export interface SystemHeaderFieldEdit {
|
|
|
27
27
|
* Custom header field added via `newFields`.
|
|
28
28
|
*
|
|
29
29
|
* `key` is required and must be non-empty — it is used as the stable identity
|
|
30
|
-
* for this field in
|
|
30
|
+
* for this field in visibility conditions.
|
|
31
31
|
*/
|
|
32
32
|
export interface NewHeaderField {
|
|
33
33
|
/** RQL field name to display. Required. */
|
|
@@ -64,7 +64,7 @@ export interface HeaderEdits {
|
|
|
64
64
|
systemFieldEdits?: Record<string, SystemHeaderFieldEdit>;
|
|
65
65
|
/** Additional custom fields to add to the header. Optional. */
|
|
66
66
|
newFields?: NewHeaderField[];
|
|
67
|
-
/** Ordered list of header field
|
|
67
|
+
/** Ordered list of header field RQL names. Custom header keys are accepted and converted. Optional. */
|
|
68
68
|
headerFieldsOrder?: string[];
|
|
69
69
|
/** Action button keys to show in the header. Optional. */
|
|
70
70
|
buttons?: string[];
|
|
@@ -98,8 +98,6 @@ export interface FieldsSection {
|
|
|
98
98
|
type: 'fields_section';
|
|
99
99
|
/** Field section metadata for the fields displayed in this layout section. Required. */
|
|
100
100
|
section: CustomObjectFieldSection;
|
|
101
|
-
/** Stable layout key for this section. Optional. Defaults to the field section id. */
|
|
102
|
-
key?: string;
|
|
103
101
|
/** Display name of the layout section. Optional. Defaults to the field section name. */
|
|
104
102
|
name?: string;
|
|
105
103
|
/** Fields to display in this section. Required. */
|
|
@@ -227,53 +225,93 @@ export interface VisibilityConditions {
|
|
|
227
225
|
/** Where to insert a new layout item. Defaults to `'end'`. */
|
|
228
226
|
export type PageLayoutMutationPosition = 'start' | 'end' | number;
|
|
229
227
|
|
|
230
|
-
|
|
231
|
-
export interface AddPageLayoutFieldOptions extends SectionFieldOptions {
|
|
232
|
-
/**
|
|
233
|
-
* Tab key containing the target section. Optional when `sectionKey` is unique
|
|
234
|
-
* across the layout.
|
|
235
|
-
*/
|
|
236
|
-
tabKey?: string;
|
|
237
|
-
/** Section key to add the field to. Required. */
|
|
238
|
-
sectionKey: string;
|
|
239
|
-
/**
|
|
240
|
-
* Field-section metadata to assign to the field component.
|
|
241
|
-
*
|
|
242
|
-
* Existing layouts often use layout section keys that differ from field section
|
|
243
|
-
* ids. Pass this when the field was not constructed with a `section`.
|
|
244
|
-
*/
|
|
245
|
-
fieldSection?: CustomObjectFieldSection;
|
|
228
|
+
interface AddPageLayoutFieldOptionsBase extends SectionFieldOptions {
|
|
246
229
|
/** Insert position inside the section field list. Optional. @default `'end'` */
|
|
247
230
|
position?: PageLayoutMutationPosition;
|
|
248
231
|
/** Allow the same field to appear twice in the same section. Optional. @default false */
|
|
249
232
|
allowDuplicate?: boolean;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Options for adding a field to a typed custom-object field section. */
|
|
236
|
+
export interface AddCustomPageLayoutFieldOptions extends AddPageLayoutFieldOptionsBase {
|
|
237
|
+
/**
|
|
238
|
+
* Tab key containing the target section. Optional when `section` is unique
|
|
239
|
+
* across the layout.
|
|
240
|
+
*/
|
|
241
|
+
tabKey?: string;
|
|
242
|
+
/** Field section to add the field to. Required for custom layout sections. */
|
|
243
|
+
section: CustomObjectFieldSection;
|
|
244
|
+
/** Raw section keys are only accepted for built-in system sections. */
|
|
245
|
+
sectionKey?: never;
|
|
246
|
+
/** Deprecated. Use `section`. */
|
|
247
|
+
fieldSection?: never;
|
|
248
|
+
/** Custom layout sections are selected by `section`; `sectionKey` is only for system sections. */
|
|
249
|
+
systemSection?: false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Options for adding a field to a built-in system section. */
|
|
253
|
+
export interface AddSystemPageLayoutFieldOptions extends AddPageLayoutFieldOptionsBase {
|
|
250
254
|
/**
|
|
251
255
|
* Add to a built-in system section via `systemTabEdits`.
|
|
252
256
|
*
|
|
253
|
-
* When true, `tabKey`
|
|
254
|
-
* already exist.
|
|
257
|
+
* When true, `tabKey` and `sectionKey` are required and the edit is created
|
|
258
|
+
* if it does not already exist.
|
|
255
259
|
*/
|
|
256
|
-
systemSection
|
|
260
|
+
systemSection: true;
|
|
261
|
+
/** System tab key containing the target section. Required. */
|
|
262
|
+
tabKey: string;
|
|
263
|
+
/** Built-in system section key. Required. */
|
|
264
|
+
sectionKey: string;
|
|
265
|
+
/** Custom field-section objects are only accepted for custom layout sections. */
|
|
266
|
+
section?: never;
|
|
267
|
+
/** Deprecated. Use `section` for custom sections, or `sectionKey` for system sections. */
|
|
268
|
+
fieldSection?: never;
|
|
257
269
|
}
|
|
258
270
|
|
|
271
|
+
/** Options for adding a field to an existing layout section. */
|
|
272
|
+
export type AddPageLayoutFieldOptions = AddCustomPageLayoutFieldOptions | AddSystemPageLayoutFieldOptions;
|
|
273
|
+
|
|
259
274
|
/** Options for adding multiple fields to an existing layout section. */
|
|
260
275
|
export type AddPageLayoutFieldsOptions = AddPageLayoutFieldOptions;
|
|
261
276
|
|
|
262
277
|
/** Options for removing fields from a layout. */
|
|
263
|
-
|
|
264
|
-
/** Limit removal to one tab. Optional. */
|
|
265
|
-
tabKey?: string;
|
|
266
|
-
/** Limit removal to one section. Optional. */
|
|
267
|
-
sectionKey?: string;
|
|
278
|
+
interface RemovePageLayoutFieldOptionsBase {
|
|
268
279
|
/**
|
|
269
280
|
* Remove all matching occurrences. When false, removing a field that appears
|
|
270
281
|
* more than once throws. Optional. @default true
|
|
271
282
|
*/
|
|
272
283
|
removeAll?: boolean;
|
|
273
|
-
/** Remove from a built-in system section edit. Optional. */
|
|
274
|
-
systemSection?: boolean;
|
|
275
284
|
}
|
|
276
285
|
|
|
286
|
+
/** Options for removing fields from typed custom-object field sections. */
|
|
287
|
+
export interface RemoveCustomPageLayoutFieldOptions extends RemovePageLayoutFieldOptionsBase {
|
|
288
|
+
/** Limit removal to one tab. Optional. */
|
|
289
|
+
tabKey?: string;
|
|
290
|
+
/** Limit removal to one field section. Optional. */
|
|
291
|
+
section?: CustomObjectFieldSection;
|
|
292
|
+
/** Raw section keys are only accepted for built-in system sections. */
|
|
293
|
+
sectionKey?: never;
|
|
294
|
+
/** Custom layout sections are selected by `section`; `sectionKey` is only for system sections. */
|
|
295
|
+
systemSection?: false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Options for removing fields from built-in system section edits. */
|
|
299
|
+
export interface RemoveSystemPageLayoutFieldOptions extends RemovePageLayoutFieldOptionsBase {
|
|
300
|
+
/** Remove from a built-in system section edit. Required. */
|
|
301
|
+
systemSection: true;
|
|
302
|
+
/** System tab key containing the target section. Required. */
|
|
303
|
+
tabKey: string;
|
|
304
|
+
/** Built-in system section key. Required. */
|
|
305
|
+
sectionKey: string;
|
|
306
|
+
/** Custom field-section objects are only accepted for custom layout sections. */
|
|
307
|
+
section?: never;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Options for removing fields from a layout. */
|
|
311
|
+
export type RemovePageLayoutFieldOptions =
|
|
312
|
+
| RemoveCustomPageLayoutFieldOptions
|
|
313
|
+
| RemoveSystemPageLayoutFieldOptions;
|
|
314
|
+
|
|
277
315
|
/** Options for adding a section to an existing tab. */
|
|
278
316
|
export interface AddPageLayoutSectionOptions {
|
|
279
317
|
/** Tab key to add the section to. Required. */
|
|
@@ -299,13 +337,21 @@ export interface RemovePageLayoutSectionOptions {
|
|
|
299
337
|
* more than once throws. Optional. @default true
|
|
300
338
|
*/
|
|
301
339
|
removeAll?: boolean;
|
|
302
|
-
/**
|
|
303
|
-
systemSection?:
|
|
340
|
+
/** Custom layout sections are selected by `CustomObjectFieldSection`; raw keys are only for system sections. */
|
|
341
|
+
systemSection?: false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Options for removing a built-in system section from a layout. */
|
|
345
|
+
export interface RemoveSystemPageLayoutSectionOptions {
|
|
346
|
+
/** System tab key containing the target section. Required. */
|
|
347
|
+
tabKey: string;
|
|
348
|
+
/** Delete a built-in system section via `systemSectionEdits`. Required. */
|
|
349
|
+
systemSection: true;
|
|
304
350
|
}
|
|
305
351
|
|
|
306
352
|
/** Options for adding a tab to a layout. */
|
|
307
353
|
export interface AddPageLayoutTabOptions {
|
|
308
|
-
/** Insert position inside `newTabs`. Optional. @default `'end'` */
|
|
354
|
+
/** Insert position inside `newTabs` and `tabsOrder`. Optional. @default `'end'` */
|
|
309
355
|
position?: PageLayoutMutationPosition;
|
|
310
356
|
/** Allow another tab with the same key. Optional. @default false */
|
|
311
357
|
allowDuplicate?: boolean;
|
|
@@ -409,7 +455,6 @@ interface SerializedSectionField extends SectionFieldOptions {
|
|
|
409
455
|
|
|
410
456
|
interface SerializedFieldsSection {
|
|
411
457
|
type: 'fields_section';
|
|
412
|
-
key: string;
|
|
413
458
|
name: string;
|
|
414
459
|
fields: SerializedSectionField[];
|
|
415
460
|
layout?: SectionFieldsLayout;
|
|
@@ -459,6 +504,44 @@ function isFieldRef(value: SectionField): value is SectionFieldRef {
|
|
|
459
504
|
return candidate != null && typeof candidate === 'object' && isField(candidate.field);
|
|
460
505
|
}
|
|
461
506
|
|
|
507
|
+
const FIELD_SECTION_ID = Symbol('CustomObjectPageLayout.fieldSectionId');
|
|
508
|
+
|
|
509
|
+
function isCustomObjectFieldSection(value: unknown): value is CustomObjectFieldSection {
|
|
510
|
+
return value instanceof CustomObjectFieldSection;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function attachFieldSectionId(section: SerializedFieldsSection, sectionId: string): SerializedFieldsSection {
|
|
514
|
+
Object.defineProperty(section, FIELD_SECTION_ID, {
|
|
515
|
+
value: sectionId,
|
|
516
|
+
enumerable: false,
|
|
517
|
+
configurable: true,
|
|
518
|
+
});
|
|
519
|
+
return section;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function layoutSectionKey(section: SerializedFieldsSection): string | undefined {
|
|
523
|
+
return (section as unknown as Record<typeof FIELD_SECTION_ID, string | undefined>)[FIELD_SECTION_ID];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function layoutSectionMatches(section: SerializedFieldsSection, sectionKey: string): boolean {
|
|
527
|
+
return layoutSectionKey(section) === sectionKey;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function inputFieldsSectionHasKey(input: PageLayoutSection): boolean {
|
|
531
|
+
const record = input as unknown as Record<string, unknown>;
|
|
532
|
+
return Object.prototype.hasOwnProperty.call(record, 'key') && record['key'] != null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function assertNoSerializedSectionKey(section: SerializedFieldsSection): void {
|
|
536
|
+
const record = section as unknown as Record<string, unknown>;
|
|
537
|
+
if (Object.prototype.hasOwnProperty.call(record, 'key') && record['key'] != null) {
|
|
538
|
+
throw new Error(
|
|
539
|
+
'CustomObjectPageLayout loaded fields_section key is not supported. ' +
|
|
540
|
+
'Remove key and target the section with CustomObjectFieldSection metadata.',
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
462
545
|
function assertSameObject(customObject: CustomObject, field: Field, sectionKey: string): void {
|
|
463
546
|
if (field.getCustomObjectApiName() !== customObject.getApiName()) {
|
|
464
547
|
throw new Error(
|
|
@@ -477,6 +560,22 @@ function assertSectionBelongsTo(customObject: CustomObject, section: CustomObjec
|
|
|
477
560
|
}
|
|
478
561
|
}
|
|
479
562
|
|
|
563
|
+
function requireFieldSectionOption(value: unknown, label: string): CustomObjectFieldSection {
|
|
564
|
+
if (isCustomObjectFieldSection(value)) return value;
|
|
565
|
+
if (typeof value === 'string') {
|
|
566
|
+
throw new TypeError(
|
|
567
|
+
`${label} must be a CustomObjectFieldSection object; received raw section id "${value}". ` +
|
|
568
|
+
'Load/include the existing field section first.',
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
throw new TypeError(`${label} must be a CustomObjectFieldSection object.`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function assertOptionAbsent(options: unknown, key: string, message: string): void {
|
|
575
|
+
if (options == null || typeof options !== 'object') return;
|
|
576
|
+
if (Object.prototype.hasOwnProperty.call(options, key)) throw new TypeError(message);
|
|
577
|
+
}
|
|
578
|
+
|
|
480
579
|
function toSerializedSectionField(
|
|
481
580
|
fieldRqlName: string,
|
|
482
581
|
options: SectionFieldOptions = {},
|
|
@@ -517,16 +616,18 @@ function normalizeFieldsSection(
|
|
|
517
616
|
input: PageLayoutSection,
|
|
518
617
|
): SerializedFieldsSection {
|
|
519
618
|
assertSectionBelongsTo(customObject, input.section);
|
|
520
|
-
const
|
|
619
|
+
const sectionId = input.section.getSectionId();
|
|
620
|
+
if (inputFieldsSectionHasKey(input)) {
|
|
621
|
+
throw new Error('CustomObjectPageLayout fields_section key is not supported. Omit key.');
|
|
622
|
+
}
|
|
521
623
|
const name = input.name ?? input.section.getName();
|
|
522
624
|
const normalized: SerializedFieldsSection = {
|
|
523
|
-
key,
|
|
524
625
|
name,
|
|
525
626
|
type: 'fields_section',
|
|
526
|
-
fields: input.fields.map((field) => normalizeSectionField(customObject, input.section,
|
|
627
|
+
fields: input.fields.map((field) => normalizeSectionField(customObject, input.section, sectionId, field)),
|
|
527
628
|
};
|
|
528
629
|
if (input.layout != null) normalized.layout = input.layout;
|
|
529
|
-
return normalized;
|
|
630
|
+
return attachFieldSectionId(normalized, sectionId);
|
|
530
631
|
}
|
|
531
632
|
|
|
532
633
|
function normalizeTab(customObject: CustomObject, tab: PageLayoutTab): SerializedPageLayoutTab {
|
|
@@ -599,11 +700,111 @@ function cloneJson<T>(value: T): T {
|
|
|
599
700
|
return JSON.parse(JSON.stringify(value)) as T;
|
|
600
701
|
}
|
|
601
702
|
|
|
703
|
+
const HEADER_EDIT_KEYS = new Set([
|
|
704
|
+
'newTitleField',
|
|
705
|
+
'titleFieldDeleted',
|
|
706
|
+
'newDescriptionField',
|
|
707
|
+
'descriptionFieldDeleted',
|
|
708
|
+
'systemFieldEdits',
|
|
709
|
+
'newFields',
|
|
710
|
+
'headerFieldsOrder',
|
|
711
|
+
'buttons',
|
|
712
|
+
]);
|
|
713
|
+
|
|
714
|
+
const SYSTEM_HEADER_FIELD_EDIT_KEYS = new Set(['newRqlField', 'deleted']);
|
|
715
|
+
|
|
716
|
+
const NEW_HEADER_FIELD_KEYS = new Set(['rqlName', 'key', 'canBeDeleted', 'canBeChanged', 'canBeMoved']);
|
|
717
|
+
|
|
718
|
+
function assertKnownKeys(
|
|
719
|
+
record: Record<string, unknown>,
|
|
720
|
+
allowedKeys: ReadonlySet<string>,
|
|
721
|
+
label: string,
|
|
722
|
+
hint: string,
|
|
723
|
+
): void {
|
|
724
|
+
for (const key of Object.keys(record)) {
|
|
725
|
+
if (!allowedKeys.has(key)) {
|
|
726
|
+
throw new Error(`${label} contains unsupported key "${key}". ${hint}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function normalizeHeaderEdits(headerEdits: HeaderEdits): HeaderEdits {
|
|
732
|
+
assertKnownKeys(
|
|
733
|
+
headerEdits as unknown as Record<string, unknown>,
|
|
734
|
+
HEADER_EDIT_KEYS,
|
|
735
|
+
'CustomObjectPageLayout headerEdits',
|
|
736
|
+
'Use addHeaderField(), setTitleField(), setDescriptionField(), or the HeaderEdits keys documented by the SDK.',
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
for (const [key, edit] of Object.entries(headerEdits.systemFieldEdits ?? {})) {
|
|
740
|
+
assertKnownKeys(
|
|
741
|
+
edit as Record<string, unknown>,
|
|
742
|
+
SYSTEM_HEADER_FIELD_EDIT_KEYS,
|
|
743
|
+
`CustomObjectPageLayout headerEdits.systemFieldEdits.${key}`,
|
|
744
|
+
'Use newRqlField and deleted for system header field edits.',
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
for (const [index, field] of (headerEdits.newFields ?? []).entries()) {
|
|
749
|
+
assertKnownKeys(
|
|
750
|
+
field as unknown as Record<string, unknown>,
|
|
751
|
+
NEW_HEADER_FIELD_KEYS,
|
|
752
|
+
`CustomObjectPageLayout headerEdits.newFields[${index}]`,
|
|
753
|
+
'Use { rqlName, key } entries, or call addHeaderField(field).',
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (headerEdits.headerFieldsOrder == null) return headerEdits;
|
|
758
|
+
return {
|
|
759
|
+
...headerEdits,
|
|
760
|
+
headerFieldsOrder: normalizeHeaderFieldsOrder(headerEdits),
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function addUniqueHeaderOrderField(order: string[], field: unknown): void {
|
|
765
|
+
if (typeof field !== 'string' || field.length === 0) return;
|
|
766
|
+
if (!order.includes(field)) order.push(field);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function normalizeHeaderFieldsOrder(headerEdits: HeaderEdits): string[] {
|
|
770
|
+
const customFieldRqlByKey = new Map(
|
|
771
|
+
(headerEdits.newFields ?? []).map((field) => [field.key, field.rqlName]),
|
|
772
|
+
);
|
|
773
|
+
const systemFieldRqlByKey = new Map(
|
|
774
|
+
Object.entries(headerEdits.systemFieldEdits ?? {})
|
|
775
|
+
.filter((entry): entry is [string, SystemHeaderFieldEdit & { newRqlField: string }] => {
|
|
776
|
+
const [, edit] = entry;
|
|
777
|
+
return typeof edit.newRqlField === 'string' && edit.newRqlField.length > 0;
|
|
778
|
+
})
|
|
779
|
+
.map(([key, edit]) => [key, edit.newRqlField]),
|
|
780
|
+
);
|
|
781
|
+
const order: string[] = [];
|
|
782
|
+
|
|
783
|
+
for (const entry of headerEdits.headerFieldsOrder ?? []) {
|
|
784
|
+
addUniqueHeaderOrderField(
|
|
785
|
+
order,
|
|
786
|
+
customFieldRqlByKey.get(entry) ?? systemFieldRqlByKey.get(entry) ?? entry,
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
addUniqueHeaderOrderField(order, headerEdits.newTitleField);
|
|
791
|
+
addUniqueHeaderOrderField(order, headerEdits.newDescriptionField);
|
|
792
|
+
for (const field of headerEdits.newFields ?? []) {
|
|
793
|
+
addUniqueHeaderOrderField(order, field.rqlName);
|
|
794
|
+
}
|
|
795
|
+
for (const systemEdit of Object.values(headerEdits.systemFieldEdits ?? {})) {
|
|
796
|
+
addUniqueHeaderOrderField(order, systemEdit.newRqlField);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return order;
|
|
800
|
+
}
|
|
801
|
+
|
|
602
802
|
type MutableSectionField = SerializedSectionField | Record<string, any> | string;
|
|
603
803
|
|
|
604
804
|
interface MutableFieldContainer {
|
|
605
805
|
tabKey: string;
|
|
606
|
-
sectionKey: string;
|
|
806
|
+
sectionKey: string | undefined;
|
|
807
|
+
section?: SerializedFieldsSection;
|
|
607
808
|
fields: MutableSectionField[];
|
|
608
809
|
label: string;
|
|
609
810
|
}
|
|
@@ -625,6 +826,15 @@ function insertAt<T>(items: T[], item: T, position: PageLayoutMutationPosition |
|
|
|
625
826
|
items.splice(insertionIndex(items.length, position), 0, item);
|
|
626
827
|
}
|
|
627
828
|
|
|
829
|
+
function insertUniqueAt(
|
|
830
|
+
items: string[],
|
|
831
|
+
item: string | null | undefined,
|
|
832
|
+
position: PageLayoutMutationPosition | undefined,
|
|
833
|
+
): void {
|
|
834
|
+
if (item == null || item.length === 0 || items.includes(item)) return;
|
|
835
|
+
insertAt(items, item, position);
|
|
836
|
+
}
|
|
837
|
+
|
|
628
838
|
function fieldRqlNameOf(entry: unknown): string | undefined {
|
|
629
839
|
if (typeof entry === 'string') return entry;
|
|
630
840
|
if (entry == null || typeof entry !== 'object') return undefined;
|
|
@@ -633,6 +843,61 @@ function fieldRqlNameOf(entry: unknown): string | undefined {
|
|
|
633
843
|
return typeof fieldRqlName === 'string' ? fieldRqlName : undefined;
|
|
634
844
|
}
|
|
635
845
|
|
|
846
|
+
function explicitFieldSectionId(entry: unknown): string | undefined {
|
|
847
|
+
if (entry == null || typeof entry !== 'object') return undefined;
|
|
848
|
+
const record = entry as Record<string, any>;
|
|
849
|
+
const fieldSectionId = record['fieldSection'] ?? record['field_section'];
|
|
850
|
+
return typeof fieldSectionId === 'string' ? fieldSectionId : undefined;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function inferredSectionIdFromFields(
|
|
854
|
+
section: SerializedFieldsSection,
|
|
855
|
+
resolveFieldSectionId?: (fieldApiName: string) => string | undefined,
|
|
856
|
+
): string | undefined {
|
|
857
|
+
const sectionIds = new Set<string>();
|
|
858
|
+
for (const field of section.fields ?? []) {
|
|
859
|
+
const explicitSectionId = explicitFieldSectionId(field);
|
|
860
|
+
if (explicitSectionId != null) {
|
|
861
|
+
sectionIds.add(explicitSectionId);
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const fieldApiName = fieldRqlNameOf(field);
|
|
866
|
+
if (fieldApiName == null || resolveFieldSectionId == null) continue;
|
|
867
|
+
const resolvedSectionId = resolveFieldSectionId(fieldApiName);
|
|
868
|
+
if (resolvedSectionId != null) sectionIds.add(resolvedSectionId);
|
|
869
|
+
}
|
|
870
|
+
return sectionIds.size === 1 ? [...sectionIds][0] : undefined;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function annotateLoadedFieldsSection(
|
|
874
|
+
section: SerializedFieldsSection,
|
|
875
|
+
resolveFieldSectionId?: (fieldApiName: string) => string | undefined,
|
|
876
|
+
): SerializedFieldsSection {
|
|
877
|
+
assertNoSerializedSectionKey(section);
|
|
878
|
+
const sectionId = layoutSectionKey(section) ?? inferredSectionIdFromFields(section, resolveFieldSectionId);
|
|
879
|
+
return sectionId == null ? section : attachFieldSectionId(section, sectionId);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function annotateLoadedTabEdits(
|
|
883
|
+
tabEdits: SerializedTabEdits,
|
|
884
|
+
resolveFieldSectionId?: (fieldApiName: string) => string | undefined,
|
|
885
|
+
): SerializedTabEdits {
|
|
886
|
+
for (const tab of tabEdits.newTabs ?? []) {
|
|
887
|
+
if (!isTabWithSections(tab)) continue;
|
|
888
|
+
tab.sections = tab.sections.map((section) => annotateLoadedFieldsSection(section, resolveFieldSectionId));
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
for (const tabEdit of Object.values(tabEdits.systemTabEdits ?? {})) {
|
|
892
|
+
if (tabEdit.newSections != null) {
|
|
893
|
+
tabEdit.newSections = tabEdit.newSections.map((section) =>
|
|
894
|
+
annotateLoadedFieldsSection(section, resolveFieldSectionId),
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return tabEdits;
|
|
899
|
+
}
|
|
900
|
+
|
|
636
901
|
function removeIndexes<T>(items: T[], indexes: number[]): void {
|
|
637
902
|
for (const index of [...indexes].sort((a, b) => b - a)) {
|
|
638
903
|
items.splice(index, 1);
|
|
@@ -715,7 +980,6 @@ function ensureArrayProperty<T>(record: Record<string, any>, key: string, label:
|
|
|
715
980
|
* newTabs: [{
|
|
716
981
|
* key: 'member', name: 'Member', type: 'tab_with_sections',
|
|
717
982
|
* sections: [{
|
|
718
|
-
* key: 'profile',
|
|
719
983
|
* name: 'Profile',
|
|
720
984
|
* type: 'fields_section',
|
|
721
985
|
* section: profileSection,
|
|
@@ -764,7 +1028,7 @@ export class CustomObjectPageLayout {
|
|
|
764
1028
|
this._customObjectApiName = customObject.getApiName();
|
|
765
1029
|
this._name = props.name;
|
|
766
1030
|
this._tabEdits = normalizeTabEdits(customObject, props.tabEdits ?? {});
|
|
767
|
-
this._headerEdits = props.headerEdits ?? {};
|
|
1031
|
+
this._headerEdits = normalizeHeaderEdits(props.headerEdits ?? {});
|
|
768
1032
|
this._visibilityConditions = props.visibilityConditions ?? {};
|
|
769
1033
|
this._blueprintKey = props.blueprintKey;
|
|
770
1034
|
customObject._register(this);
|
|
@@ -788,6 +1052,7 @@ export class CustomObjectPageLayout {
|
|
|
788
1052
|
static _fromExistingComponent(
|
|
789
1053
|
customObject: CustomObject,
|
|
790
1054
|
component: Record<string, any>,
|
|
1055
|
+
resolveFieldSectionId?: (fieldApiName: string) => string | undefined,
|
|
791
1056
|
): CustomObjectPageLayout {
|
|
792
1057
|
const layout = Object.create(CustomObjectPageLayout.prototype) as CustomObjectPageLayout;
|
|
793
1058
|
Object.assign(layout as Record<string, any>, {
|
|
@@ -799,7 +1064,7 @@ export class CustomObjectPageLayout {
|
|
|
799
1064
|
CustomObjectPageLayout.componentType,
|
|
800
1065
|
),
|
|
801
1066
|
_name: requireStringComponentField(component, 'name', CustomObjectPageLayout.componentType),
|
|
802
|
-
_tabEdits: cloneJson(component['tab_edits'] ?? {}),
|
|
1067
|
+
_tabEdits: annotateLoadedTabEdits(cloneJson(component['tab_edits'] ?? {}), resolveFieldSectionId),
|
|
803
1068
|
_headerEdits: cloneJson(component['header_edits'] ?? {}),
|
|
804
1069
|
_visibilityConditions: cloneJson(component['visibility_conditions'] ?? {}),
|
|
805
1070
|
_blueprintKey: component['blueprint_key'],
|
|
@@ -842,17 +1107,59 @@ export class CustomObjectPageLayout {
|
|
|
842
1107
|
/**
|
|
843
1108
|
* Adds multiple fields to an existing layout section.
|
|
844
1109
|
*
|
|
845
|
-
* Fields are inserted in the order provided.
|
|
846
|
-
*
|
|
847
|
-
* `CUSTOM_OBJECT_FIELD_SECTION
|
|
1110
|
+
* Fields are inserted in the order provided. For custom layout sections,
|
|
1111
|
+
* pass `options.section` so TypeScript and runtime validation agree on the
|
|
1112
|
+
* target `CUSTOM_OBJECT_FIELD_SECTION`.
|
|
848
1113
|
*/
|
|
849
1114
|
addFields(fields: Field[], options: AddPageLayoutFieldsOptions): this {
|
|
850
1115
|
if (fields.length === 0) return this;
|
|
1116
|
+
|
|
1117
|
+
let fieldSectionForMutation: CustomObjectFieldSection | undefined;
|
|
851
1118
|
const container =
|
|
852
1119
|
options.systemSection === true ?
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1120
|
+
(() => {
|
|
1121
|
+
assertOptionAbsent(
|
|
1122
|
+
options,
|
|
1123
|
+
'section',
|
|
1124
|
+
'CustomObjectPageLayout addField section is only supported for custom layout sections.',
|
|
1125
|
+
);
|
|
1126
|
+
assertOptionAbsent(
|
|
1127
|
+
options,
|
|
1128
|
+
'fieldSection',
|
|
1129
|
+
'CustomObjectPageLayout addField fieldSection is no longer supported. ' +
|
|
1130
|
+
'Use sectionKey only for systemSection: true.',
|
|
1131
|
+
);
|
|
1132
|
+
return this.ensureSystemSectionFieldContainer(options.tabKey, options.sectionKey);
|
|
1133
|
+
})()
|
|
1134
|
+
: (() => {
|
|
1135
|
+
assertOptionAbsent(
|
|
1136
|
+
options,
|
|
1137
|
+
'sectionKey',
|
|
1138
|
+
'CustomObjectPageLayout addField sectionKey is only supported with systemSection: true. ' +
|
|
1139
|
+
'Pass section: CustomObjectFieldSection for custom layout sections.',
|
|
1140
|
+
);
|
|
1141
|
+
assertOptionAbsent(
|
|
1142
|
+
options,
|
|
1143
|
+
'fieldSection',
|
|
1144
|
+
'CustomObjectPageLayout addField fieldSection is no longer supported. ' +
|
|
1145
|
+
'Pass section: CustomObjectFieldSection instead.',
|
|
1146
|
+
);
|
|
1147
|
+
const section = requireFieldSectionOption(
|
|
1148
|
+
options.section,
|
|
1149
|
+
'CustomObjectPageLayout addField section',
|
|
1150
|
+
);
|
|
1151
|
+
this.assertFieldSectionBelongsToLayout(section);
|
|
1152
|
+
fieldSectionForMutation = section;
|
|
1153
|
+
return this.requireOneFieldContainer(options.tabKey, section.getSectionId(), section);
|
|
1154
|
+
})();
|
|
1155
|
+
const entries = fields.map((field) =>
|
|
1156
|
+
this.serializeFieldForMutation(
|
|
1157
|
+
field,
|
|
1158
|
+
container.sectionKey ?? container.label,
|
|
1159
|
+
options,
|
|
1160
|
+
fieldSectionForMutation,
|
|
1161
|
+
),
|
|
1162
|
+
);
|
|
856
1163
|
|
|
857
1164
|
if (options.allowDuplicate !== true) {
|
|
858
1165
|
const existing = new Set(container.fields.map((entry) => fieldRqlNameOf(entry)).filter(Boolean));
|
|
@@ -883,9 +1190,34 @@ export class CustomObjectPageLayout {
|
|
|
883
1190
|
const fieldApiName = typeof field === 'string' ? field : field.getApiName();
|
|
884
1191
|
if (isField(field)) this.assertFieldBelongsToLayout(field);
|
|
885
1192
|
|
|
1193
|
+
let sectionKey: string | undefined;
|
|
1194
|
+
if (options.systemSection === true) {
|
|
1195
|
+
assertOptionAbsent(
|
|
1196
|
+
options,
|
|
1197
|
+
'section',
|
|
1198
|
+
'CustomObjectPageLayout removeField section is only supported for custom layout sections.',
|
|
1199
|
+
);
|
|
1200
|
+
sectionKey = options.sectionKey;
|
|
1201
|
+
} else {
|
|
1202
|
+
assertOptionAbsent(
|
|
1203
|
+
options,
|
|
1204
|
+
'sectionKey',
|
|
1205
|
+
'CustomObjectPageLayout removeField sectionKey is only supported with systemSection: true. ' +
|
|
1206
|
+
'Pass section: CustomObjectFieldSection for custom layout sections.',
|
|
1207
|
+
);
|
|
1208
|
+
if (options.section != null) {
|
|
1209
|
+
const section = requireFieldSectionOption(
|
|
1210
|
+
options.section,
|
|
1211
|
+
'CustomObjectPageLayout removeField section',
|
|
1212
|
+
);
|
|
1213
|
+
this.assertFieldSectionBelongsToLayout(section);
|
|
1214
|
+
sectionKey = section.getSectionId();
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
886
1218
|
const containers = this.findFieldContainers({
|
|
887
1219
|
tabKey: options.tabKey,
|
|
888
|
-
sectionKey
|
|
1220
|
+
sectionKey,
|
|
889
1221
|
systemSectionOnly: options.systemSection === true,
|
|
890
1222
|
});
|
|
891
1223
|
const matches: Array<{ container: MutableFieldContainer; index: number }> = [];
|
|
@@ -944,9 +1276,15 @@ export class CustomObjectPageLayout {
|
|
|
944
1276
|
'newSections',
|
|
945
1277
|
`systemTabEdits.${options.tabKey}.newSections`,
|
|
946
1278
|
);
|
|
947
|
-
this.assertSectionKeyAvailable(
|
|
1279
|
+
this.assertSectionKeyAvailable(
|
|
1280
|
+
sections,
|
|
1281
|
+
layoutSectionKey(normalized),
|
|
1282
|
+
options.tabKey,
|
|
1283
|
+
options.allowDuplicate,
|
|
1284
|
+
);
|
|
1285
|
+
const sectionsOrder = this.ensureSystemSectionsOrder(tabEdit);
|
|
948
1286
|
insertAt(sections, normalized, options.position);
|
|
949
|
-
|
|
1287
|
+
insertUniqueAt(sectionsOrder, layoutSectionKey(normalized), options.position);
|
|
950
1288
|
return this;
|
|
951
1289
|
}
|
|
952
1290
|
|
|
@@ -957,7 +1295,12 @@ export class CustomObjectPageLayout {
|
|
|
957
1295
|
'Pass systemTab: true to add a section to a built-in system tab.',
|
|
958
1296
|
);
|
|
959
1297
|
}
|
|
960
|
-
this.assertSectionKeyAvailable(
|
|
1298
|
+
this.assertSectionKeyAvailable(
|
|
1299
|
+
tab.sections,
|
|
1300
|
+
layoutSectionKey(normalized),
|
|
1301
|
+
options.tabKey,
|
|
1302
|
+
options.allowDuplicate,
|
|
1303
|
+
);
|
|
961
1304
|
insertAt(tab.sections, normalized, options.position);
|
|
962
1305
|
return this;
|
|
963
1306
|
}
|
|
@@ -968,19 +1311,34 @@ export class CustomObjectPageLayout {
|
|
|
968
1311
|
* Pass `systemSection: true` with `tabKey` to delete a built-in section via
|
|
969
1312
|
* `systemSectionEdits[sectionKey].deleted`.
|
|
970
1313
|
*/
|
|
971
|
-
removeSection(
|
|
1314
|
+
removeSection(section: CustomObjectFieldSection, options?: RemovePageLayoutSectionOptions): this;
|
|
1315
|
+
removeSection(sectionKey: string, options: RemoveSystemPageLayoutSectionOptions): this;
|
|
1316
|
+
removeSection(
|
|
1317
|
+
sectionOrKey: CustomObjectFieldSection | string,
|
|
1318
|
+
options: RemovePageLayoutSectionOptions | RemoveSystemPageLayoutSectionOptions = {},
|
|
1319
|
+
): this {
|
|
972
1320
|
if (options.systemSection === true) {
|
|
1321
|
+
if (typeof sectionOrKey !== 'string') {
|
|
1322
|
+
throw new TypeError(
|
|
1323
|
+
'CustomObjectPageLayout removeSection with systemSection: true requires a system section key.',
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
973
1326
|
if (options.tabKey == null) {
|
|
974
1327
|
throw new Error('CustomObjectPageLayout removeSection with systemSection: true requires tabKey.');
|
|
975
1328
|
}
|
|
976
|
-
const sectionEdit = this.ensureSystemSectionEdit(options.tabKey,
|
|
1329
|
+
const sectionEdit = this.ensureSystemSectionEdit(options.tabKey, sectionOrKey);
|
|
977
1330
|
sectionEdit.deleted = true;
|
|
978
|
-
this.
|
|
1331
|
+
removeKeyFromOrder(this._tabEdits.systemTabEdits?.[options.tabKey]?.sectionsOrder, sectionOrKey);
|
|
1332
|
+
this.removeSectionVisibility(sectionOrKey);
|
|
979
1333
|
return this;
|
|
980
1334
|
}
|
|
981
1335
|
|
|
1336
|
+
const section = requireFieldSectionOption(sectionOrKey, 'CustomObjectPageLayout removeSection section');
|
|
1337
|
+
this.assertFieldSectionBelongsToLayout(section);
|
|
1338
|
+
const sectionKey = section.getSectionId();
|
|
982
1339
|
const matches: Array<{
|
|
983
1340
|
sections: SerializedFieldsSection[];
|
|
1341
|
+
section: SerializedFieldsSection;
|
|
984
1342
|
index: number;
|
|
985
1343
|
tabEdit?: SerializedSystemTabEdit;
|
|
986
1344
|
tabKey: string;
|
|
@@ -989,14 +1347,15 @@ export class CustomObjectPageLayout {
|
|
|
989
1347
|
if (!isTabWithSections(tab)) continue;
|
|
990
1348
|
if (options.tabKey != null && tab.key !== options.tabKey) continue;
|
|
991
1349
|
tab.sections.forEach((section, index) => {
|
|
992
|
-
if (section
|
|
1350
|
+
if (layoutSectionMatches(section, sectionKey))
|
|
1351
|
+
matches.push({ sections: tab.sections, section, index, tabKey: tab.key });
|
|
993
1352
|
});
|
|
994
1353
|
}
|
|
995
1354
|
for (const [tabKey, tabEdit] of Object.entries(this._tabEdits.systemTabEdits ?? {})) {
|
|
996
1355
|
if (options.tabKey != null && tabKey !== options.tabKey) continue;
|
|
997
1356
|
(tabEdit.newSections ?? []).forEach((section, index) => {
|
|
998
|
-
if (section
|
|
999
|
-
matches.push({ sections: tabEdit.newSections!, index, tabEdit, tabKey });
|
|
1357
|
+
if (layoutSectionMatches(section, sectionKey))
|
|
1358
|
+
matches.push({ sections: tabEdit.newSections!, section, index, tabEdit, tabKey });
|
|
1000
1359
|
});
|
|
1001
1360
|
}
|
|
1002
1361
|
|
|
@@ -1018,11 +1377,11 @@ export class CustomObjectPageLayout {
|
|
|
1018
1377
|
indexes.push(match.index);
|
|
1019
1378
|
byArray.set(match.sections, indexes);
|
|
1020
1379
|
if (match.tabEdit != null) removeKeyFromOrder(match.tabEdit.sectionsOrder, sectionKey);
|
|
1380
|
+
this.removeSectionVisibility(sectionKey);
|
|
1021
1381
|
}
|
|
1022
1382
|
for (const [sections, indexes] of byArray.entries()) {
|
|
1023
1383
|
removeIndexes(sections, indexes);
|
|
1024
1384
|
}
|
|
1025
|
-
this.removeSectionVisibility(sectionKey);
|
|
1026
1385
|
return this;
|
|
1027
1386
|
}
|
|
1028
1387
|
|
|
@@ -1033,10 +1392,9 @@ export class CustomObjectPageLayout {
|
|
|
1033
1392
|
if (options.allowDuplicate !== true && newTabs.some((existing) => existing.key === normalized.key)) {
|
|
1034
1393
|
throw new Error(`CustomObjectPageLayout already contains tab "${normalized.key}".`);
|
|
1035
1394
|
}
|
|
1395
|
+
const tabsOrder = this.ensureTabsOrder();
|
|
1036
1396
|
insertAt(newTabs, normalized, options.position);
|
|
1037
|
-
|
|
1038
|
-
insertAt(this._tabEdits.tabsOrder, normalized.key, options.position);
|
|
1039
|
-
}
|
|
1397
|
+
insertUniqueAt(tabsOrder, normalized.key, options.position);
|
|
1040
1398
|
return this;
|
|
1041
1399
|
}
|
|
1042
1400
|
|
|
@@ -1076,10 +1434,9 @@ export class CustomObjectPageLayout {
|
|
|
1076
1434
|
if (options.allowDuplicate !== true && newFields.some((existing) => existing.key === headerField.key)) {
|
|
1077
1435
|
throw new Error(`CustomObjectPageLayout header already contains field key "${headerField.key}".`);
|
|
1078
1436
|
}
|
|
1437
|
+
const headerFieldsOrder = this.ensureHeaderFieldsOrder();
|
|
1079
1438
|
insertAt(newFields, headerField, options.position);
|
|
1080
|
-
|
|
1081
|
-
insertAt(this._headerEdits.headerFieldsOrder, headerField.key, options.position);
|
|
1082
|
-
}
|
|
1439
|
+
insertUniqueAt(headerFieldsOrder, headerField.rqlName, options.position);
|
|
1083
1440
|
return this;
|
|
1084
1441
|
}
|
|
1085
1442
|
|
|
@@ -1089,12 +1446,12 @@ export class CustomObjectPageLayout {
|
|
|
1089
1446
|
if (isField(fieldOrKey)) this.assertFieldBelongsToLayout(fieldOrKey);
|
|
1090
1447
|
const key = typeof fieldOrKey === 'string' ? fieldOrKey : fieldApiName;
|
|
1091
1448
|
const newFields = this._headerEdits.newFields ?? [];
|
|
1092
|
-
const
|
|
1449
|
+
const removedFields: NewHeaderField[] = [];
|
|
1093
1450
|
const removed = removeFromArray(
|
|
1094
1451
|
newFields,
|
|
1095
1452
|
(field) => {
|
|
1096
1453
|
const matched = field.key === key || (fieldApiName != null && field.rqlName === fieldApiName);
|
|
1097
|
-
if (matched)
|
|
1454
|
+
if (matched) removedFields.push(field);
|
|
1098
1455
|
return matched;
|
|
1099
1456
|
},
|
|
1100
1457
|
`CustomObjectPageLayout header field "${key}"`,
|
|
@@ -1103,47 +1460,102 @@ export class CustomObjectPageLayout {
|
|
|
1103
1460
|
if (removed === 0) {
|
|
1104
1461
|
throw new Error(`CustomObjectPageLayout header does not contain field "${key}".`);
|
|
1105
1462
|
}
|
|
1106
|
-
for (const
|
|
1107
|
-
removeKeyFromOrder(this._headerEdits.headerFieldsOrder, removedKey);
|
|
1108
|
-
}
|
|
1463
|
+
for (const removedField of removedFields) this.removeHeaderOrderFieldIfUnused(removedField);
|
|
1109
1464
|
return this;
|
|
1110
1465
|
}
|
|
1111
1466
|
|
|
1112
1467
|
/** Sets the record title field in `header_edits`. */
|
|
1113
1468
|
setTitleField(field: Field | string | null): this {
|
|
1114
|
-
this._headerEdits.newTitleField
|
|
1469
|
+
const previousTitleField = this._headerEdits.newTitleField;
|
|
1470
|
+
const nextTitleField = this.headerFieldApiName(field);
|
|
1471
|
+
this._headerEdits.newTitleField = nextTitleField;
|
|
1115
1472
|
if (field != null) this._headerEdits.titleFieldDeleted = false;
|
|
1473
|
+
insertUniqueAt(this.ensureHeaderFieldsOrder(), nextTitleField, 'end');
|
|
1474
|
+
this.removeHeaderOrderFieldIfUnused(previousTitleField);
|
|
1116
1475
|
return this;
|
|
1117
1476
|
}
|
|
1118
1477
|
|
|
1119
1478
|
/** Removes the record title field from the header. */
|
|
1120
1479
|
removeTitleField(): this {
|
|
1480
|
+
const previousTitleField = this._headerEdits.newTitleField;
|
|
1121
1481
|
this._headerEdits.newTitleField = null;
|
|
1122
1482
|
this._headerEdits.titleFieldDeleted = true;
|
|
1483
|
+
this.removeHeaderOrderFieldIfUnused(previousTitleField);
|
|
1123
1484
|
return this;
|
|
1124
1485
|
}
|
|
1125
1486
|
|
|
1126
1487
|
/** Sets the record description field in `header_edits`. */
|
|
1127
1488
|
setDescriptionField(field: Field | string | null): this {
|
|
1128
|
-
this._headerEdits.newDescriptionField
|
|
1489
|
+
const previousDescriptionField = this._headerEdits.newDescriptionField;
|
|
1490
|
+
const nextDescriptionField = this.headerFieldApiName(field);
|
|
1491
|
+
this._headerEdits.newDescriptionField = nextDescriptionField;
|
|
1129
1492
|
if (field != null) this._headerEdits.descriptionFieldDeleted = false;
|
|
1493
|
+
insertUniqueAt(this.ensureHeaderFieldsOrder(), nextDescriptionField, 'end');
|
|
1494
|
+
this.removeHeaderOrderFieldIfUnused(previousDescriptionField);
|
|
1130
1495
|
return this;
|
|
1131
1496
|
}
|
|
1132
1497
|
|
|
1133
1498
|
/** Removes the record description field from the header. */
|
|
1134
1499
|
removeDescriptionField(): this {
|
|
1500
|
+
const previousDescriptionField = this._headerEdits.newDescriptionField;
|
|
1135
1501
|
this._headerEdits.newDescriptionField = null;
|
|
1136
1502
|
this._headerEdits.descriptionFieldDeleted = true;
|
|
1503
|
+
this.removeHeaderOrderFieldIfUnused(previousDescriptionField);
|
|
1137
1504
|
return this;
|
|
1138
1505
|
}
|
|
1139
1506
|
|
|
1507
|
+
private ensureTabsOrder(): string[] {
|
|
1508
|
+
const order = (this._tabEdits.tabsOrder ??= []);
|
|
1509
|
+
for (const tab of this._tabEdits.newTabs ?? []) insertUniqueAt(order, tab.key, 'end');
|
|
1510
|
+
for (const [tabKey, tabEdit] of Object.entries(this._tabEdits.systemTabEdits ?? {})) {
|
|
1511
|
+
if (tabEdit.deleted !== true) insertUniqueAt(order, tabKey, 'end');
|
|
1512
|
+
}
|
|
1513
|
+
return order;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
private ensureSystemSectionsOrder(tabEdit: SerializedSystemTabEdit): string[] {
|
|
1517
|
+
const order = (tabEdit.sectionsOrder ??= []);
|
|
1518
|
+
for (const section of tabEdit.newSections ?? []) insertUniqueAt(order, layoutSectionKey(section), 'end');
|
|
1519
|
+
for (const [sectionKey, sectionEdit] of Object.entries(tabEdit.systemSectionEdits ?? {})) {
|
|
1520
|
+
if (sectionEdit.deleted !== true) insertUniqueAt(order, sectionKey, 'end');
|
|
1521
|
+
}
|
|
1522
|
+
return order;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
private ensureHeaderFieldsOrder(): string[] {
|
|
1526
|
+
this._headerEdits.headerFieldsOrder = normalizeHeaderFieldsOrder({
|
|
1527
|
+
...this._headerEdits,
|
|
1528
|
+
headerFieldsOrder: this._headerEdits.headerFieldsOrder ?? [],
|
|
1529
|
+
});
|
|
1530
|
+
return this._headerEdits.headerFieldsOrder;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
private headerOrderFieldIsUsed(rqlName: string): boolean {
|
|
1534
|
+
return (
|
|
1535
|
+
this._headerEdits.newTitleField === rqlName ||
|
|
1536
|
+
this._headerEdits.newDescriptionField === rqlName ||
|
|
1537
|
+
(this._headerEdits.newFields ?? []).some((field) => field.rqlName === rqlName) ||
|
|
1538
|
+
Object.values(this._headerEdits.systemFieldEdits ?? {}).some((edit) => edit.newRqlField === rqlName)
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
private removeHeaderOrderFieldIfUnused(field: NewHeaderField | string | null | undefined): void {
|
|
1543
|
+
const rqlName = typeof field === 'string' ? field : field?.rqlName;
|
|
1544
|
+
if (field != null && typeof field !== 'string' && field.key !== rqlName) {
|
|
1545
|
+
removeKeyFromOrder(this._headerEdits.headerFieldsOrder, field.key);
|
|
1546
|
+
}
|
|
1547
|
+
if (rqlName == null || this.headerOrderFieldIsUsed(rqlName)) return;
|
|
1548
|
+
removeKeyFromOrder(this._headerEdits.headerFieldsOrder, rqlName);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1140
1551
|
private serializeFieldForMutation(
|
|
1141
1552
|
field: Field,
|
|
1142
1553
|
fallbackSectionKey: string,
|
|
1143
1554
|
options: AddPageLayoutFieldOptions,
|
|
1555
|
+
fieldSection: CustomObjectFieldSection | undefined,
|
|
1144
1556
|
): SerializedSectionField {
|
|
1145
1557
|
this.assertFieldBelongsToLayout(field);
|
|
1146
|
-
this.assignFieldSectionForMutation(field, fallbackSectionKey,
|
|
1558
|
+
this.assignFieldSectionForMutation(field, fallbackSectionKey, fieldSection);
|
|
1147
1559
|
return toSerializedSectionField(field.getApiName(), options);
|
|
1148
1560
|
}
|
|
1149
1561
|
|
|
@@ -1171,23 +1583,22 @@ export class CustomObjectPageLayout {
|
|
|
1171
1583
|
}
|
|
1172
1584
|
|
|
1173
1585
|
private normalizeSectionForMutation(input: PageLayoutSection): SerializedFieldsSection {
|
|
1174
|
-
|
|
1175
|
-
throw new Error(
|
|
1176
|
-
`CustomObjectPageLayout section "${input.section.getSectionId()}" belongs to ` +
|
|
1177
|
-
`"${input.section.getModelApiName()}", but the layout belongs to "${this._customObjectApiName}".`,
|
|
1178
|
-
);
|
|
1179
|
-
}
|
|
1586
|
+
this.assertFieldSectionBelongsToLayout(input.section);
|
|
1180
1587
|
|
|
1181
|
-
const
|
|
1588
|
+
const sectionId = input.section.getSectionId();
|
|
1589
|
+
if (inputFieldsSectionHasKey(input)) {
|
|
1590
|
+
throw new Error('CustomObjectPageLayout fields_section key is not supported. Omit key.');
|
|
1591
|
+
}
|
|
1182
1592
|
const name = input.name ?? input.section.getName();
|
|
1183
1593
|
const normalized: SerializedFieldsSection = {
|
|
1184
|
-
key,
|
|
1185
1594
|
name,
|
|
1186
1595
|
type: 'fields_section',
|
|
1187
|
-
fields: input.fields.map((field) =>
|
|
1596
|
+
fields: input.fields.map((field) =>
|
|
1597
|
+
this.normalizeSectionFieldForMutation(input.section, sectionId, field),
|
|
1598
|
+
),
|
|
1188
1599
|
};
|
|
1189
1600
|
if (input.layout != null) normalized.layout = input.layout;
|
|
1190
|
-
return normalized;
|
|
1601
|
+
return attachFieldSectionId(normalized, sectionId);
|
|
1191
1602
|
}
|
|
1192
1603
|
|
|
1193
1604
|
private normalizeTabForMutation(tab: PageLayoutTab): SerializedPageLayoutTab {
|
|
@@ -1207,7 +1618,7 @@ export class CustomObjectPageLayout {
|
|
|
1207
1618
|
if (rawFieldSection != null) {
|
|
1208
1619
|
if (typeof rawFieldSection === 'string') {
|
|
1209
1620
|
throw new TypeError(
|
|
1210
|
-
`CustomObjectPageLayout
|
|
1621
|
+
`CustomObjectPageLayout section must be a CustomObjectFieldSection object; ` +
|
|
1211
1622
|
`received raw section id "${rawFieldSection}". Load/include the existing field section first.`,
|
|
1212
1623
|
);
|
|
1213
1624
|
}
|
|
@@ -1226,7 +1637,7 @@ export class CustomObjectPageLayout {
|
|
|
1226
1637
|
if (field.getSectionId() == null) {
|
|
1227
1638
|
throw new Error(
|
|
1228
1639
|
`CustomObjectPageLayout field "${field.getApiName()}" is missing field-section metadata. ` +
|
|
1229
|
-
`Construct the field with a CustomObjectFieldSection or pass
|
|
1640
|
+
`Construct the field with a CustomObjectFieldSection or pass section when adding it to ` +
|
|
1230
1641
|
`section "${fallbackSectionKey}".`,
|
|
1231
1642
|
);
|
|
1232
1643
|
}
|
|
@@ -1241,17 +1652,63 @@ export class CustomObjectPageLayout {
|
|
|
1241
1652
|
}
|
|
1242
1653
|
}
|
|
1243
1654
|
|
|
1244
|
-
private
|
|
1655
|
+
private assertFieldSectionBelongsToLayout(section: CustomObjectFieldSection): void {
|
|
1656
|
+
if (section.getModelApiName() !== this._customObjectApiName) {
|
|
1657
|
+
throw new Error(
|
|
1658
|
+
`CustomObjectPageLayout field section "${section.getSectionId()}" belongs to ` +
|
|
1659
|
+
`"${section.getModelApiName()}", but the layout belongs to "${this._customObjectApiName}".`,
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
private requireOneFieldContainer(
|
|
1665
|
+
tabKey: string | undefined,
|
|
1666
|
+
sectionKey: string | undefined,
|
|
1667
|
+
fallbackFieldSection?: CustomObjectFieldSection,
|
|
1668
|
+
): MutableFieldContainer {
|
|
1245
1669
|
const containers = this.findFieldContainers({
|
|
1246
1670
|
tabKey,
|
|
1247
1671
|
sectionKey,
|
|
1248
1672
|
systemSectionOnly: false,
|
|
1249
1673
|
});
|
|
1250
1674
|
if (containers.length === 0) {
|
|
1251
|
-
|
|
1252
|
-
|
|
1675
|
+
if (sectionKey != null && fallbackFieldSection != null) {
|
|
1676
|
+
const unkeyedContainers = this.findFieldContainers({
|
|
1677
|
+
tabKey,
|
|
1678
|
+
sectionKey: undefined,
|
|
1679
|
+
systemSectionOnly: false,
|
|
1680
|
+
}).filter((container) => container.section != null && container.sectionKey == null);
|
|
1681
|
+
if (unkeyedContainers.length === 1) {
|
|
1682
|
+
if (fallbackFieldSection.getModelApiName() !== this._customObjectApiName) {
|
|
1683
|
+
throw new Error(
|
|
1684
|
+
`CustomObjectPageLayout field section "${fallbackFieldSection.getSectionId()}" belongs to ` +
|
|
1685
|
+
`"${fallbackFieldSection.getModelApiName()}", but the layout belongs to "${
|
|
1686
|
+
this._customObjectApiName
|
|
1687
|
+
}".`,
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
const container = unkeyedContainers[0] as MutableFieldContainer & {
|
|
1691
|
+
section: SerializedFieldsSection;
|
|
1692
|
+
};
|
|
1693
|
+
attachFieldSectionId(container.section, fallbackFieldSection.getSectionId());
|
|
1694
|
+
container.sectionKey = fallbackFieldSection.getSectionId();
|
|
1695
|
+
container.label = `${container.tabKey}.${container.sectionKey}`;
|
|
1696
|
+
return container;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
const scope =
|
|
1700
|
+
sectionKey == null ? 'a field section' : (
|
|
1701
|
+
`section "${tabKey != null ? `${tabKey}.` : ''}${sectionKey}"`
|
|
1702
|
+
);
|
|
1703
|
+
throw new Error(`CustomObjectPageLayout could not find ${scope}.`);
|
|
1253
1704
|
}
|
|
1254
1705
|
if (containers.length > 1) {
|
|
1706
|
+
if (sectionKey == null) {
|
|
1707
|
+
throw new Error(
|
|
1708
|
+
`CustomObjectPageLayout matched multiple sections: ` +
|
|
1709
|
+
`${containers.map((container) => container.label).join(', ')}. Pass section to choose one.`,
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1255
1712
|
throw new Error(
|
|
1256
1713
|
`CustomObjectPageLayout section "${sectionKey}" matched multiple sections: ` +
|
|
1257
1714
|
`${containers.map((container) => container.label).join(', ')}. Pass tabKey to choose one.`,
|
|
@@ -1271,34 +1728,40 @@ export class CustomObjectPageLayout {
|
|
|
1271
1728
|
for (const tab of this._tabEdits.newTabs ?? []) {
|
|
1272
1729
|
if (!isTabWithSections(tab)) continue;
|
|
1273
1730
|
if (filter.tabKey != null && tab.key !== filter.tabKey) continue;
|
|
1274
|
-
for (const section of tab.sections) {
|
|
1275
|
-
|
|
1731
|
+
for (const [index, section] of tab.sections.entries()) {
|
|
1732
|
+
const sectionKey = layoutSectionKey(section);
|
|
1733
|
+
if (filter.sectionKey != null && !layoutSectionMatches(section, filter.sectionKey)) continue;
|
|
1734
|
+
const label = sectionKey == null ? `${tab.key}.sections[${index}]` : `${tab.key}.${sectionKey}`;
|
|
1276
1735
|
containers.push({
|
|
1277
1736
|
tabKey: tab.key,
|
|
1278
|
-
sectionKey
|
|
1737
|
+
sectionKey,
|
|
1738
|
+
section,
|
|
1279
1739
|
fields: ensureArrayProperty<MutableSectionField>(
|
|
1280
1740
|
section as unknown as Record<string, any>,
|
|
1281
1741
|
'fields',
|
|
1282
|
-
`newTabs.${
|
|
1742
|
+
`newTabs.${label}.fields`,
|
|
1283
1743
|
),
|
|
1284
|
-
label
|
|
1744
|
+
label,
|
|
1285
1745
|
});
|
|
1286
1746
|
}
|
|
1287
1747
|
}
|
|
1288
1748
|
|
|
1289
1749
|
for (const [tabKey, tabEdit] of Object.entries(this._tabEdits.systemTabEdits ?? {})) {
|
|
1290
1750
|
if (filter.tabKey != null && tabKey !== filter.tabKey) continue;
|
|
1291
|
-
for (const section of tabEdit.newSections ?? []) {
|
|
1292
|
-
|
|
1751
|
+
for (const [index, section] of (tabEdit.newSections ?? []).entries()) {
|
|
1752
|
+
const sectionKey = layoutSectionKey(section);
|
|
1753
|
+
if (filter.sectionKey != null && !layoutSectionMatches(section, filter.sectionKey)) continue;
|
|
1754
|
+
const label = sectionKey == null ? `${tabKey}.newSections[${index}]` : `${tabKey}.${sectionKey}`;
|
|
1293
1755
|
containers.push({
|
|
1294
1756
|
tabKey,
|
|
1295
|
-
sectionKey
|
|
1757
|
+
sectionKey,
|
|
1758
|
+
section,
|
|
1296
1759
|
fields: ensureArrayProperty<MutableSectionField>(
|
|
1297
1760
|
section as unknown as Record<string, any>,
|
|
1298
1761
|
'fields',
|
|
1299
|
-
`systemTabEdits.${
|
|
1762
|
+
`systemTabEdits.${label}.fields`,
|
|
1300
1763
|
),
|
|
1301
|
-
label
|
|
1764
|
+
label,
|
|
1302
1765
|
});
|
|
1303
1766
|
}
|
|
1304
1767
|
}
|
|
@@ -1329,11 +1792,14 @@ export class CustomObjectPageLayout {
|
|
|
1329
1792
|
|
|
1330
1793
|
private ensureSystemSectionFieldContainer(
|
|
1331
1794
|
tabKey: string | undefined,
|
|
1332
|
-
sectionKey: string,
|
|
1795
|
+
sectionKey: string | undefined,
|
|
1333
1796
|
): MutableFieldContainer {
|
|
1334
1797
|
if (tabKey == null) {
|
|
1335
1798
|
throw new Error('CustomObjectPageLayout addField with systemSection: true requires tabKey.');
|
|
1336
1799
|
}
|
|
1800
|
+
if (sectionKey == null) {
|
|
1801
|
+
throw new Error('CustomObjectPageLayout addField with systemSection: true requires sectionKey.');
|
|
1802
|
+
}
|
|
1337
1803
|
const sectionEdit = this.ensureSystemSectionEdit(tabKey, sectionKey);
|
|
1338
1804
|
if (sectionEdit.deleted === true) {
|
|
1339
1805
|
throw new Error(`CustomObjectPageLayout system section "${tabKey}.${sectionKey}" is marked deleted.`);
|
|
@@ -1396,12 +1862,12 @@ export class CustomObjectPageLayout {
|
|
|
1396
1862
|
|
|
1397
1863
|
private assertSectionKeyAvailable(
|
|
1398
1864
|
sections: SerializedFieldsSection[],
|
|
1399
|
-
sectionKey: string,
|
|
1865
|
+
sectionKey: string | undefined,
|
|
1400
1866
|
tabKey: string,
|
|
1401
1867
|
allowDuplicate: boolean | undefined,
|
|
1402
1868
|
): void {
|
|
1403
1869
|
if (allowDuplicate === true) return;
|
|
1404
|
-
if (sections.some((section) => section
|
|
1870
|
+
if (sectionKey != null && sections.some((section) => layoutSectionMatches(section, sectionKey))) {
|
|
1405
1871
|
throw new Error(`CustomObjectPageLayout tab "${tabKey}" already contains section "${sectionKey}".`);
|
|
1406
1872
|
}
|
|
1407
1873
|
}
|
|
@@ -1476,12 +1942,13 @@ export class CustomObjectPageLayout {
|
|
|
1476
1942
|
* tabs or sections. Always uses `apiName: 'default'`.
|
|
1477
1943
|
*
|
|
1478
1944
|
* @param customObject - The custom object to create the layout for.
|
|
1479
|
-
* @param props - Optional. Pass `fields` to populate the General section.
|
|
1945
|
+
* @param props - Optional. Pass `section` and `fields` to populate the General section.
|
|
1480
1946
|
* @returns A new `CustomObjectPageLayout` registered with the manifest.
|
|
1481
1947
|
*
|
|
1482
1948
|
* @example
|
|
1483
1949
|
* ```ts
|
|
1484
1950
|
* CustomObjectPageLayout.basic(memberObj, {
|
|
1951
|
+
* section: profileSection,
|
|
1485
1952
|
* fields: [memberFirstName, memberEmail, memberStatusField],
|
|
1486
1953
|
* });
|
|
1487
1954
|
* ```
|
|
@@ -1489,16 +1956,40 @@ export class CustomObjectPageLayout {
|
|
|
1489
1956
|
static basic(
|
|
1490
1957
|
customObject: CustomObject,
|
|
1491
1958
|
props: {
|
|
1959
|
+
section?: CustomObjectFieldSection;
|
|
1492
1960
|
fields?: Field[];
|
|
1493
1961
|
} = {},
|
|
1494
1962
|
): CustomObjectPageLayout {
|
|
1495
1963
|
const coApiName = customObject.getApiName();
|
|
1496
1964
|
const coName = customObject.getName();
|
|
1497
|
-
const
|
|
1498
|
-
const generalSection =
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1965
|
+
const fields = props.fields ?? [];
|
|
1966
|
+
const generalSection =
|
|
1967
|
+
props.section ??
|
|
1968
|
+
(() => {
|
|
1969
|
+
const alreadySectionedField = fields.find((field) => field.getSectionId() != null);
|
|
1970
|
+
if (alreadySectionedField != null) {
|
|
1971
|
+
throw new Error(
|
|
1972
|
+
`CustomObjectPageLayout.basic field "${alreadySectionedField.getApiName()}" already belongs to ` +
|
|
1973
|
+
`field section "${alreadySectionedField.getSectionId()}". Pass that CustomObjectFieldSection ` +
|
|
1974
|
+
'as section so basic() does not reassign the field.',
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
const sectionPrefix = coApiName.replace(/__c$/, '').replace(/[^a-zA-Z0-9]+/g, '_');
|
|
1978
|
+
return new CustomObjectFieldSection(customObject, {
|
|
1979
|
+
sectionId: `sec_${sectionPrefix}_general`,
|
|
1980
|
+
name: 'General',
|
|
1981
|
+
});
|
|
1982
|
+
})();
|
|
1983
|
+
assertSectionBelongsTo(customObject, generalSection);
|
|
1984
|
+
for (const field of fields) {
|
|
1985
|
+
const fieldSectionId = field.getSectionId();
|
|
1986
|
+
if (fieldSectionId != null && fieldSectionId !== generalSection.getSectionId()) {
|
|
1987
|
+
throw new Error(
|
|
1988
|
+
`CustomObjectPageLayout.basic field "${field.getApiName()}" belongs to field section ` +
|
|
1989
|
+
`"${fieldSectionId}", but basic() was given section "${generalSection.getSectionId()}".`,
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1502
1993
|
return new CustomObjectPageLayout(customObject, {
|
|
1503
1994
|
apiName: 'default',
|
|
1504
1995
|
name: `${coName} Layout`,
|
|
@@ -1510,11 +2001,10 @@ export class CustomObjectPageLayout {
|
|
|
1510
2001
|
type: 'tab_with_sections',
|
|
1511
2002
|
sections: [
|
|
1512
2003
|
{
|
|
1513
|
-
key: 'general',
|
|
1514
2004
|
name: 'General',
|
|
1515
2005
|
type: 'fields_section',
|
|
1516
2006
|
section: generalSection,
|
|
1517
|
-
fields
|
|
2007
|
+
fields,
|
|
1518
2008
|
},
|
|
1519
2009
|
],
|
|
1520
2010
|
},
|