@rippling/rippling-sdk 0.2.0-alpha.45 → 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.
@@ -58,9 +58,9 @@ export interface AppProps {
58
58
  /**
59
59
  * SDUI pages shown as tabs in the app. Tab order matches the array order.
60
60
  *
61
- * Optional. @default `[]`
61
+ * Required. Must include at least one page.
62
62
  */
63
- pages?: AppPage[];
63
+ pages: AppPage[];
64
64
  }
65
65
 
66
66
  function resolvePage(entry: AppPage): Record<string, any> {
@@ -77,6 +77,20 @@ function resolvePage(entry: AppPage): Record<string, any> {
77
77
  };
78
78
  }
79
79
 
80
+ function resolvePages(pages: AppPage[] | undefined): Record<string, any>[] {
81
+ if (pages == null || pages.length === 0) {
82
+ throw new Error('App pages must include at least one page.');
83
+ }
84
+ return pages.map(resolvePage);
85
+ }
86
+
87
+ function clonePages(value: unknown): Record<string, any>[] {
88
+ if (!Array.isArray(value) || value.length === 0) {
89
+ throw new Error('App pages must include at least one page.');
90
+ }
91
+ return cloneJson(value) as Record<string, any>[];
92
+ }
93
+
80
94
  function cloneJson<T>(value: T): T {
81
95
  if (value == null) return value;
82
96
  return JSON.parse(JSON.stringify(value)) as T;
@@ -130,7 +144,7 @@ export class App {
130
144
  this._description = props.description;
131
145
  this._appType = props.appType;
132
146
  this._icon = props.icon;
133
- this._pages = (props.pages ?? []).map(resolvePage);
147
+ this._pages = resolvePages(props.pages);
134
148
  scope._register(this);
135
149
  }
136
150
 
@@ -150,7 +164,7 @@ export class App {
150
164
  _description: component['description'],
151
165
  _appType: component['app_type'],
152
166
  _icon: App.appIconFromExistingComponent(component['icon']),
153
- _pages: cloneJson(component['pages'] ?? []),
167
+ _pages: clonePages(component['pages']),
154
168
  });
155
169
  scope._register(app);
156
170
  return app;
@@ -193,7 +207,7 @@ export class App {
193
207
  component['icon'] = { s3_bucket: this._icon.s3Bucket, s3_key: this._icon.s3Key };
194
208
  }
195
209
  if (this._appType != null) component['app_type'] = this._appType;
196
- if (this._pages.length > 0) component['pages'] = this._pages.map((p) => ({ ...p }));
210
+ component['pages'] = this._pages.map((p) => ({ ...p }));
197
211
  return component;
198
212
  }
199
213
  }
@@ -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 `headerFieldsOrder` and visibility conditions.
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 `key` values (custom + system). Optional. */
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[];
@@ -351,7 +351,7 @@ export interface RemoveSystemPageLayoutSectionOptions {
351
351
 
352
352
  /** Options for adding a tab to a layout. */
353
353
  export interface AddPageLayoutTabOptions {
354
- /** Insert position inside `newTabs`. Optional. @default `'end'` */
354
+ /** Insert position inside `newTabs` and `tabsOrder`. Optional. @default `'end'` */
355
355
  position?: PageLayoutMutationPosition;
356
356
  /** Allow another tab with the same key. Optional. @default false */
357
357
  allowDuplicate?: boolean;
@@ -754,7 +754,49 @@ function normalizeHeaderEdits(headerEdits: HeaderEdits): HeaderEdits {
754
754
  );
755
755
  }
756
756
 
757
- return headerEdits;
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;
758
800
  }
759
801
 
760
802
  type MutableSectionField = SerializedSectionField | Record<string, any> | string;
@@ -784,6 +826,15 @@ function insertAt<T>(items: T[], item: T, position: PageLayoutMutationPosition |
784
826
  items.splice(insertionIndex(items.length, position), 0, item);
785
827
  }
786
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
+
787
838
  function fieldRqlNameOf(entry: unknown): string | undefined {
788
839
  if (typeof entry === 'string') return entry;
789
840
  if (entry == null || typeof entry !== 'object') return undefined;
@@ -1231,7 +1282,9 @@ export class CustomObjectPageLayout {
1231
1282
  options.tabKey,
1232
1283
  options.allowDuplicate,
1233
1284
  );
1285
+ const sectionsOrder = this.ensureSystemSectionsOrder(tabEdit);
1234
1286
  insertAt(sections, normalized, options.position);
1287
+ insertUniqueAt(sectionsOrder, layoutSectionKey(normalized), options.position);
1235
1288
  return this;
1236
1289
  }
1237
1290
 
@@ -1275,6 +1328,7 @@ export class CustomObjectPageLayout {
1275
1328
  }
1276
1329
  const sectionEdit = this.ensureSystemSectionEdit(options.tabKey, sectionOrKey);
1277
1330
  sectionEdit.deleted = true;
1331
+ removeKeyFromOrder(this._tabEdits.systemTabEdits?.[options.tabKey]?.sectionsOrder, sectionOrKey);
1278
1332
  this.removeSectionVisibility(sectionOrKey);
1279
1333
  return this;
1280
1334
  }
@@ -1338,10 +1392,9 @@ export class CustomObjectPageLayout {
1338
1392
  if (options.allowDuplicate !== true && newTabs.some((existing) => existing.key === normalized.key)) {
1339
1393
  throw new Error(`CustomObjectPageLayout already contains tab "${normalized.key}".`);
1340
1394
  }
1395
+ const tabsOrder = this.ensureTabsOrder();
1341
1396
  insertAt(newTabs, normalized, options.position);
1342
- if (this._tabEdits.tabsOrder != null) {
1343
- insertAt(this._tabEdits.tabsOrder, normalized.key, options.position);
1344
- }
1397
+ insertUniqueAt(tabsOrder, normalized.key, options.position);
1345
1398
  return this;
1346
1399
  }
1347
1400
 
@@ -1381,10 +1434,9 @@ export class CustomObjectPageLayout {
1381
1434
  if (options.allowDuplicate !== true && newFields.some((existing) => existing.key === headerField.key)) {
1382
1435
  throw new Error(`CustomObjectPageLayout header already contains field key "${headerField.key}".`);
1383
1436
  }
1437
+ const headerFieldsOrder = this.ensureHeaderFieldsOrder();
1384
1438
  insertAt(newFields, headerField, options.position);
1385
- if (this._headerEdits.headerFieldsOrder != null) {
1386
- insertAt(this._headerEdits.headerFieldsOrder, headerField.key, options.position);
1387
- }
1439
+ insertUniqueAt(headerFieldsOrder, headerField.rqlName, options.position);
1388
1440
  return this;
1389
1441
  }
1390
1442
 
@@ -1394,12 +1446,12 @@ export class CustomObjectPageLayout {
1394
1446
  if (isField(fieldOrKey)) this.assertFieldBelongsToLayout(fieldOrKey);
1395
1447
  const key = typeof fieldOrKey === 'string' ? fieldOrKey : fieldApiName;
1396
1448
  const newFields = this._headerEdits.newFields ?? [];
1397
- const removedKeys: string[] = [];
1449
+ const removedFields: NewHeaderField[] = [];
1398
1450
  const removed = removeFromArray(
1399
1451
  newFields,
1400
1452
  (field) => {
1401
1453
  const matched = field.key === key || (fieldApiName != null && field.rqlName === fieldApiName);
1402
- if (matched) removedKeys.push(field.key);
1454
+ if (matched) removedFields.push(field);
1403
1455
  return matched;
1404
1456
  },
1405
1457
  `CustomObjectPageLayout header field "${key}"`,
@@ -1408,40 +1460,94 @@ export class CustomObjectPageLayout {
1408
1460
  if (removed === 0) {
1409
1461
  throw new Error(`CustomObjectPageLayout header does not contain field "${key}".`);
1410
1462
  }
1411
- for (const removedKey of removedKeys) {
1412
- removeKeyFromOrder(this._headerEdits.headerFieldsOrder, removedKey);
1413
- }
1463
+ for (const removedField of removedFields) this.removeHeaderOrderFieldIfUnused(removedField);
1414
1464
  return this;
1415
1465
  }
1416
1466
 
1417
1467
  /** Sets the record title field in `header_edits`. */
1418
1468
  setTitleField(field: Field | string | null): this {
1419
- this._headerEdits.newTitleField = this.headerFieldApiName(field);
1469
+ const previousTitleField = this._headerEdits.newTitleField;
1470
+ const nextTitleField = this.headerFieldApiName(field);
1471
+ this._headerEdits.newTitleField = nextTitleField;
1420
1472
  if (field != null) this._headerEdits.titleFieldDeleted = false;
1473
+ insertUniqueAt(this.ensureHeaderFieldsOrder(), nextTitleField, 'end');
1474
+ this.removeHeaderOrderFieldIfUnused(previousTitleField);
1421
1475
  return this;
1422
1476
  }
1423
1477
 
1424
1478
  /** Removes the record title field from the header. */
1425
1479
  removeTitleField(): this {
1480
+ const previousTitleField = this._headerEdits.newTitleField;
1426
1481
  this._headerEdits.newTitleField = null;
1427
1482
  this._headerEdits.titleFieldDeleted = true;
1483
+ this.removeHeaderOrderFieldIfUnused(previousTitleField);
1428
1484
  return this;
1429
1485
  }
1430
1486
 
1431
1487
  /** Sets the record description field in `header_edits`. */
1432
1488
  setDescriptionField(field: Field | string | null): this {
1433
- this._headerEdits.newDescriptionField = this.headerFieldApiName(field);
1489
+ const previousDescriptionField = this._headerEdits.newDescriptionField;
1490
+ const nextDescriptionField = this.headerFieldApiName(field);
1491
+ this._headerEdits.newDescriptionField = nextDescriptionField;
1434
1492
  if (field != null) this._headerEdits.descriptionFieldDeleted = false;
1493
+ insertUniqueAt(this.ensureHeaderFieldsOrder(), nextDescriptionField, 'end');
1494
+ this.removeHeaderOrderFieldIfUnused(previousDescriptionField);
1435
1495
  return this;
1436
1496
  }
1437
1497
 
1438
1498
  /** Removes the record description field from the header. */
1439
1499
  removeDescriptionField(): this {
1500
+ const previousDescriptionField = this._headerEdits.newDescriptionField;
1440
1501
  this._headerEdits.newDescriptionField = null;
1441
1502
  this._headerEdits.descriptionFieldDeleted = true;
1503
+ this.removeHeaderOrderFieldIfUnused(previousDescriptionField);
1442
1504
  return this;
1443
1505
  }
1444
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
+
1445
1551
  private serializeFieldForMutation(
1446
1552
  field: Field,
1447
1553
  fallbackSectionKey: string,
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.0-alpha.45'; // x-release-please-version
1
+ export const VERSION = '0.2.0-alpha.46'; // x-release-please-version
package/version.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.2.0-alpha.45";
1
+ export declare const VERSION = "0.2.0-alpha.46";
2
2
  //# sourceMappingURL=version.d.mts.map
package/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.2.0-alpha.45";
1
+ export declare const VERSION = "0.2.0-alpha.46";
2
2
  //# sourceMappingURL=version.d.ts.map
package/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
- exports.VERSION = '0.2.0-alpha.45'; // x-release-please-version
4
+ exports.VERSION = '0.2.0-alpha.46'; // x-release-please-version
5
5
  //# sourceMappingURL=version.js.map
package/version.mjs CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = '0.2.0-alpha.45'; // x-release-please-version
1
+ export const VERSION = '0.2.0-alpha.46'; // x-release-please-version
2
2
  //# sourceMappingURL=version.mjs.map