@rippling/rippling-sdk 0.2.0-alpha.37 → 0.2.0-alpha.39

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 (202) hide show
  1. package/examples/sdui/sdui-crud-todo/sdui-crud-todo.ts +3 -3
  2. package/examples/sdui/sdui-showcase/sdui-showcase.ts +6 -11
  3. package/examples/sdui/sdui-tab-multi-view/sdui-tab-multi-view.ts +14 -24
  4. package/examples/sdui/vote/vote.ts +7 -11
  5. package/lib/manifest/app.d.mts +11 -1
  6. package/lib/manifest/app.d.mts.map +1 -1
  7. package/lib/manifest/app.d.ts +11 -1
  8. package/lib/manifest/app.d.ts.map +1 -1
  9. package/lib/manifest/app.js +42 -1
  10. package/lib/manifest/app.js.map +1 -1
  11. package/lib/manifest/app.mjs +42 -1
  12. package/lib/manifest/app.mjs.map +1 -1
  13. package/lib/manifest/category.d.mts +14 -3
  14. package/lib/manifest/category.d.mts.map +1 -1
  15. package/lib/manifest/category.d.ts +14 -3
  16. package/lib/manifest/category.d.ts.map +1 -1
  17. package/lib/manifest/category.js +25 -3
  18. package/lib/manifest/category.js.map +1 -1
  19. package/lib/manifest/category.mjs +25 -3
  20. package/lib/manifest/category.mjs.map +1 -1
  21. package/lib/manifest/custom-object-field-section.d.mts +13 -0
  22. package/lib/manifest/custom-object-field-section.d.mts.map +1 -1
  23. package/lib/manifest/custom-object-field-section.d.ts +13 -0
  24. package/lib/manifest/custom-object-field-section.d.ts.map +1 -1
  25. package/lib/manifest/custom-object-field-section.js +22 -1
  26. package/lib/manifest/custom-object-field-section.js.map +1 -1
  27. package/lib/manifest/custom-object-field-section.mjs +22 -1
  28. package/lib/manifest/custom-object-field-section.mjs.map +1 -1
  29. package/lib/manifest/custom-object-page-layout.d.mts +23 -0
  30. package/lib/manifest/custom-object-page-layout.d.mts.map +1 -1
  31. package/lib/manifest/custom-object-page-layout.d.ts +23 -0
  32. package/lib/manifest/custom-object-page-layout.d.ts.map +1 -1
  33. package/lib/manifest/custom-object-page-layout.js +45 -2
  34. package/lib/manifest/custom-object-page-layout.js.map +1 -1
  35. package/lib/manifest/custom-object-page-layout.mjs +45 -2
  36. package/lib/manifest/custom-object-page-layout.mjs.map +1 -1
  37. package/lib/manifest/custom-object.d.mts +41 -2
  38. package/lib/manifest/custom-object.d.mts.map +1 -1
  39. package/lib/manifest/custom-object.d.ts +41 -2
  40. package/lib/manifest/custom-object.d.ts.map +1 -1
  41. package/lib/manifest/custom-object.js +70 -3
  42. package/lib/manifest/custom-object.js.map +1 -1
  43. package/lib/manifest/custom-object.mjs +70 -3
  44. package/lib/manifest/custom-object.mjs.map +1 -1
  45. package/lib/manifest/existing-manifest-context.d.mts +87 -0
  46. package/lib/manifest/existing-manifest-context.d.mts.map +1 -0
  47. package/lib/manifest/existing-manifest-context.d.ts +87 -0
  48. package/lib/manifest/existing-manifest-context.d.ts.map +1 -0
  49. package/lib/manifest/existing-manifest-context.js +511 -0
  50. package/lib/manifest/existing-manifest-context.js.map +1 -0
  51. package/lib/manifest/existing-manifest-context.mjs +506 -0
  52. package/lib/manifest/existing-manifest-context.mjs.map +1 -0
  53. package/lib/manifest/field.d.mts +44 -2
  54. package/lib/manifest/field.d.mts.map +1 -1
  55. package/lib/manifest/field.d.ts +44 -2
  56. package/lib/manifest/field.d.ts.map +1 -1
  57. package/lib/manifest/field.js +134 -3
  58. package/lib/manifest/field.js.map +1 -1
  59. package/lib/manifest/field.mjs +134 -3
  60. package/lib/manifest/field.mjs.map +1 -1
  61. package/lib/manifest/index.d.mts +8 -7
  62. package/lib/manifest/index.d.mts.map +1 -1
  63. package/lib/manifest/index.d.ts +8 -7
  64. package/lib/manifest/index.d.ts.map +1 -1
  65. package/lib/manifest/index.js +3 -1
  66. package/lib/manifest/index.js.map +1 -1
  67. package/lib/manifest/index.mjs +5 -4
  68. package/lib/manifest/index.mjs.map +1 -1
  69. package/lib/manifest/list-view.d.mts +17 -0
  70. package/lib/manifest/list-view.d.mts.map +1 -1
  71. package/lib/manifest/list-view.d.ts +17 -0
  72. package/lib/manifest/list-view.d.ts.map +1 -1
  73. package/lib/manifest/list-view.js +38 -1
  74. package/lib/manifest/list-view.js.map +1 -1
  75. package/lib/manifest/list-view.mjs +38 -1
  76. package/lib/manifest/list-view.mjs.map +1 -1
  77. package/lib/manifest/manifest-builder.d.mts +60 -7
  78. package/lib/manifest/manifest-builder.d.mts.map +1 -1
  79. package/lib/manifest/manifest-builder.d.ts +60 -7
  80. package/lib/manifest/manifest-builder.d.ts.map +1 -1
  81. package/lib/manifest/manifest-builder.js +62 -4
  82. package/lib/manifest/manifest-builder.js.map +1 -1
  83. package/lib/manifest/manifest-builder.mjs +62 -4
  84. package/lib/manifest/manifest-builder.mjs.map +1 -1
  85. package/lib/manifest/manifest-function.d.mts +18 -3
  86. package/lib/manifest/manifest-function.d.mts.map +1 -1
  87. package/lib/manifest/manifest-function.d.ts +18 -3
  88. package/lib/manifest/manifest-function.d.ts.map +1 -1
  89. package/lib/manifest/manifest-function.js +44 -3
  90. package/lib/manifest/manifest-function.js.map +1 -1
  91. package/lib/manifest/manifest-function.mjs +44 -3
  92. package/lib/manifest/manifest-function.mjs.map +1 -1
  93. package/lib/manifest/rule.d.mts +17 -0
  94. package/lib/manifest/rule.d.mts.map +1 -1
  95. package/lib/manifest/rule.d.ts +17 -0
  96. package/lib/manifest/rule.d.ts.map +1 -1
  97. package/lib/manifest/rule.js +45 -1
  98. package/lib/manifest/rule.js.map +1 -1
  99. package/lib/manifest/rule.mjs +45 -1
  100. package/lib/manifest/rule.mjs.map +1 -1
  101. package/lib/manifest/sdui-page.d.mts +10 -1
  102. package/lib/manifest/sdui-page.d.mts.map +1 -1
  103. package/lib/manifest/sdui-page.d.ts +10 -1
  104. package/lib/manifest/sdui-page.d.ts.map +1 -1
  105. package/lib/manifest/sdui-page.js +28 -1
  106. package/lib/manifest/sdui-page.js.map +1 -1
  107. package/lib/manifest/sdui-page.mjs +28 -1
  108. package/lib/manifest/sdui-page.mjs.map +1 -1
  109. package/lib/manifest/validation.d.mts +17 -0
  110. package/lib/manifest/validation.d.mts.map +1 -1
  111. package/lib/manifest/validation.d.ts +17 -0
  112. package/lib/manifest/validation.d.ts.map +1 -1
  113. package/lib/manifest/validation.js +31 -1
  114. package/lib/manifest/validation.js.map +1 -1
  115. package/lib/manifest/validation.mjs +31 -1
  116. package/lib/manifest/validation.mjs.map +1 -1
  117. package/lib/sdui/actions.d.mts.map +1 -1
  118. package/lib/sdui/actions.d.ts.map +1 -1
  119. package/lib/sdui/actions.js +23 -13
  120. package/lib/sdui/actions.js.map +1 -1
  121. package/lib/sdui/actions.mjs +23 -13
  122. package/lib/sdui/actions.mjs.map +1 -1
  123. package/lib/sdui/component.d.mts +28 -11
  124. package/lib/sdui/component.d.mts.map +1 -1
  125. package/lib/sdui/component.d.ts +28 -11
  126. package/lib/sdui/component.d.ts.map +1 -1
  127. package/lib/sdui/component.js +69 -36
  128. package/lib/sdui/component.js.map +1 -1
  129. package/lib/sdui/component.mjs +69 -36
  130. package/lib/sdui/component.mjs.map +1 -1
  131. package/lib/sdui/components.d.mts +1141 -132
  132. package/lib/sdui/components.d.mts.map +1 -1
  133. package/lib/sdui/components.d.ts +1141 -132
  134. package/lib/sdui/components.d.ts.map +1 -1
  135. package/lib/sdui/components.js +834 -206
  136. package/lib/sdui/components.js.map +1 -1
  137. package/lib/sdui/components.mjs +830 -203
  138. package/lib/sdui/components.mjs.map +1 -1
  139. package/lib/sdui/directives.d.mts.map +1 -1
  140. package/lib/sdui/directives.d.ts.map +1 -1
  141. package/lib/sdui/directives.js +4 -1
  142. package/lib/sdui/directives.js.map +1 -1
  143. package/lib/sdui/directives.mjs +4 -1
  144. package/lib/sdui/directives.mjs.map +1 -1
  145. package/lib/sdui/index.d.mts +314 -261
  146. package/lib/sdui/index.d.mts.map +1 -1
  147. package/lib/sdui/index.d.ts +314 -261
  148. package/lib/sdui/index.d.ts.map +1 -1
  149. package/lib/sdui/index.js +255 -204
  150. package/lib/sdui/index.js.map +1 -1
  151. package/lib/sdui/index.mjs +128 -103
  152. package/lib/sdui/index.mjs.map +1 -1
  153. package/lib/sdui/propTypes.d.mts +6330 -0
  154. package/lib/sdui/propTypes.d.mts.map +1 -0
  155. package/lib/sdui/propTypes.d.ts +6330 -0
  156. package/lib/sdui/propTypes.d.ts.map +1 -0
  157. package/lib/sdui/propTypes.js +14 -0
  158. package/lib/sdui/propTypes.js.map +1 -0
  159. package/lib/sdui/propTypes.mjs +13 -0
  160. package/lib/sdui/propTypes.mjs.map +1 -0
  161. package/lib/sdui/render.d.mts +1 -1
  162. package/lib/sdui/render.d.mts.map +1 -1
  163. package/lib/sdui/render.d.ts +1 -1
  164. package/lib/sdui/render.d.ts.map +1 -1
  165. package/lib/sdui/render.js +11 -3
  166. package/lib/sdui/render.js.map +1 -1
  167. package/lib/sdui/render.mjs +11 -3
  168. package/lib/sdui/render.mjs.map +1 -1
  169. package/lib/sdui/state.d.mts.map +1 -1
  170. package/lib/sdui/state.d.ts.map +1 -1
  171. package/lib/sdui/state.js +3 -1
  172. package/lib/sdui/state.js.map +1 -1
  173. package/lib/sdui/state.mjs +3 -1
  174. package/lib/sdui/state.mjs.map +1 -1
  175. package/package.json +1 -1
  176. package/src/lib/manifest/app.ts +46 -2
  177. package/src/lib/manifest/category.ts +35 -7
  178. package/src/lib/manifest/custom-object-field-section.ts +37 -1
  179. package/src/lib/manifest/custom-object-page-layout.ts +67 -2
  180. package/src/lib/manifest/custom-object.ts +88 -4
  181. package/src/lib/manifest/existing-manifest-context.ts +673 -0
  182. package/src/lib/manifest/field.ts +160 -3
  183. package/src/lib/manifest/index.ts +39 -4
  184. package/src/lib/manifest/list-view.ts +50 -1
  185. package/src/lib/manifest/manifest-builder.ts +115 -5
  186. package/src/lib/manifest/manifest-function.ts +44 -4
  187. package/src/lib/manifest/rule.ts +60 -1
  188. package/src/lib/manifest/sdui-page.ts +37 -2
  189. package/src/lib/manifest/validation.ts +48 -1
  190. package/src/lib/sdui/actions.ts +23 -10
  191. package/src/lib/sdui/component.ts +84 -44
  192. package/src/lib/sdui/components.ts +1627 -212
  193. package/src/lib/sdui/directives.ts +4 -1
  194. package/src/lib/sdui/index.ts +565 -200
  195. package/src/lib/sdui/propTypes.ts +8470 -0
  196. package/src/lib/sdui/render.ts +13 -5
  197. package/src/lib/sdui/state.ts +8 -2
  198. package/src/version.ts +1 -1
  199. package/version.d.mts +1 -1
  200. package/version.d.ts +1 -1
  201. package/version.js +1 -1
  202. package/version.mjs +1 -1
@@ -1,5 +1,6 @@
1
1
  import { CustomObject } from './custom-object';
2
- import type { ManifestScope } from './manifest-builder';
2
+ import { getExistingManifestContext, type ExistingManifestContextOptions } from './existing-manifest-context';
3
+ import type { ManifestComponentDeletion, ManifestScope } from './manifest-builder';
3
4
  import { SduiPage } from './sdui-page';
4
5
 
5
6
  /**
@@ -82,6 +83,11 @@ function resolvePage(entry: AppPage): Record<string, any> {
82
83
  }
83
84
  }
84
85
 
86
+ function cloneJson<T>(value: T): T {
87
+ if (value == null) return value;
88
+ return JSON.parse(JSON.stringify(value)) as T;
89
+ }
90
+
85
91
  /**
86
92
  * Defines a Rippling app and registers it with the manifest.
87
93
  *
@@ -105,6 +111,15 @@ function resolvePage(entry: AppPage): Record<string, any> {
105
111
  * @see {@link SduiPage} — SDUI surface tab source.
106
112
  */
107
113
  export class App {
114
+ static readonly componentType = 'CUSTOM_APP' as const;
115
+
116
+ static toDeletionIdentifier(apiName: string): ManifestComponentDeletion {
117
+ return {
118
+ type: App.componentType,
119
+ api_name: apiName,
120
+ };
121
+ }
122
+
108
123
  private readonly _apiName: string;
109
124
  private readonly _name: string;
110
125
  private readonly _description: string | undefined;
@@ -127,6 +142,35 @@ export class App {
127
142
  scope._register(this);
128
143
  }
129
144
 
145
+ /**
146
+ * Loads this app from the active existing-manifest JSON context.
147
+ */
148
+ static loadFromExisting(apiName: string, options: ExistingManifestContextOptions = {}): App {
149
+ return getExistingManifestContext(options).loadApp(apiName);
150
+ }
151
+
152
+ /** @internal Hydrates an app from existing manifest wire JSON. */
153
+ static _fromExistingComponent(scope: ManifestScope, component: Record<string, any>): App {
154
+ const app = Object.create(App.prototype) as App;
155
+ Object.assign(app as Record<string, any>, {
156
+ _apiName: component['api_name'],
157
+ _name: component['name'] ?? component['api_name'],
158
+ _description: component['description'],
159
+ _appType: component['app_type'],
160
+ _icon: App.appIconFromExistingComponent(component['icon']),
161
+ _pages: cloneJson(component['pages'] ?? []),
162
+ });
163
+ scope._register(app);
164
+ return app;
165
+ }
166
+
167
+ private static appIconFromExistingComponent(value: unknown): AppIcon | undefined {
168
+ if (value == null || typeof value !== 'object') return undefined;
169
+ const wire = value as Record<string, any>;
170
+ if (typeof wire['s3_bucket'] !== 'string' || typeof wire['s3_key'] !== 'string') return undefined;
171
+ return { s3Bucket: wire['s3_bucket'], s3Key: wire['s3_key'] };
172
+ }
173
+
130
174
  /**
131
175
  * Returns the api_name of this app (e.g. `'gym_membership_management_app'`).
132
176
  */
@@ -148,7 +192,7 @@ export class App {
148
192
  */
149
193
  toDict(): Record<string, any> {
150
194
  const component: Record<string, any> = {
151
- type: 'CUSTOM_APP',
195
+ type: App.componentType,
152
196
  api_name: this._apiName,
153
197
  name: this._name,
154
198
  };
@@ -1,4 +1,5 @@
1
- import type { ManifestScope } from './manifest-builder';
1
+ import { getExistingManifestContext, type ExistingManifestContextOptions } from './existing-manifest-context';
2
+ import type { ManifestComponentDeletion, ManifestScope } from './manifest-builder';
2
3
 
3
4
  /**
4
5
  * Initialization properties for {@link Category}.
@@ -19,9 +20,11 @@ export interface CategoryProps {
19
20
  /**
20
21
  * Human-readable description of what this category groups.
21
22
  *
22
- * Optional. @default null
23
+ * Required. The backend (`CustomCategoryConfig.description: str`) requires the
24
+ * key to be present and rejects `null`. Pass an empty string only if you truly
25
+ * have no meaningful copy.
23
26
  */
24
- description?: string | null;
27
+ description: string;
25
28
  }
26
29
 
27
30
  /**
@@ -55,9 +58,18 @@ export interface CategoryProps {
55
58
  * @see {@link CustomObject} — references this category via instance or api_name string.
56
59
  */
57
60
  export class Category {
61
+ static readonly componentType = 'CUSTOM_CATEGORY' as const;
62
+
63
+ static toDeletionIdentifier(apiName: string): ManifestComponentDeletion {
64
+ return {
65
+ type: Category.componentType,
66
+ api_name: apiName,
67
+ };
68
+ }
69
+
58
70
  private readonly _apiName: string;
59
71
  private readonly _name: string;
60
- private readonly _description: string | null;
72
+ private readonly _description: string;
61
73
 
62
74
  /**
63
75
  * @param scope - The manifest to register this category with.
@@ -66,10 +78,26 @@ export class Category {
66
78
  constructor(scope: ManifestScope, props: CategoryProps) {
67
79
  this._apiName = props.apiName;
68
80
  this._name = props.name;
69
- this._description = props.description ?? null;
81
+ this._description = props.description;
70
82
  scope._register(this);
71
83
  }
72
84
 
85
+ /**
86
+ * Loads this category from the active existing-manifest JSON context.
87
+ */
88
+ static loadFromExisting(apiName: string, options: ExistingManifestContextOptions = {}): Category {
89
+ return getExistingManifestContext(options).loadCategory(apiName);
90
+ }
91
+
92
+ /** @internal Hydrates a category from existing manifest wire JSON. */
93
+ static _fromExistingComponent(scope: ManifestScope, component: Record<string, any>): Category {
94
+ return new Category(scope, {
95
+ apiName: component['api_name'],
96
+ name: component['name'] ?? component['api_name'],
97
+ description: component['description'] ?? '',
98
+ });
99
+ }
100
+
73
101
  /**
74
102
  * Returns the api_name of this category (e.g. `'gym__c'`).
75
103
  */
@@ -91,10 +119,10 @@ export class Category {
91
119
  */
92
120
  toDict(): Record<string, any> {
93
121
  return {
94
- type: 'CUSTOM_CATEGORY',
122
+ type: Category.componentType,
95
123
  api_name: this._apiName,
96
124
  name: this._name,
97
- description: this._description ?? null,
125
+ description: this._description,
98
126
  };
99
127
  }
100
128
  }
@@ -1,5 +1,11 @@
1
1
  import { generateId } from './_helpers';
2
2
  import type { CustomObject } from './custom-object';
3
+ import { getExistingManifestContext, type ExistingManifestContextOptions } from './existing-manifest-context';
4
+ import type { ManifestComponentDeletion } from './manifest-builder';
5
+
6
+ export interface CustomObjectFieldSectionLoadFromExistingOptions extends ExistingManifestContextOptions {
7
+ customObjectApiName?: string;
8
+ }
3
9
 
4
10
  /**
5
11
  * Initialization properties for {@link CustomObjectFieldSection}.
@@ -45,6 +51,15 @@ export interface CustomObjectFieldSectionProps {
45
51
  * @see {@link CustomObjectPageLayout} — controls which sections appear on which tab.
46
52
  */
47
53
  export class CustomObjectFieldSection {
54
+ static readonly componentType = 'CUSTOM_OBJECT_FIELD_SECTION' as const;
55
+
56
+ static toDeletionIdentifier(sectionId: string): ManifestComponentDeletion {
57
+ return {
58
+ type: CustomObjectFieldSection.componentType,
59
+ section_id: sectionId,
60
+ };
61
+ }
62
+
48
63
  private readonly _sectionId: string;
49
64
  private readonly _name: string;
50
65
  private readonly _model: CustomObject;
@@ -60,6 +75,27 @@ export class CustomObjectFieldSection {
60
75
  customObject._register(this);
61
76
  }
62
77
 
78
+ /**
79
+ * Loads this field section from the active existing-manifest JSON context.
80
+ */
81
+ static loadFromExisting(
82
+ sectionId: string,
83
+ options: CustomObjectFieldSectionLoadFromExistingOptions = {},
84
+ ): CustomObjectFieldSection {
85
+ return getExistingManifestContext(options).loadFieldSection(sectionId, options);
86
+ }
87
+
88
+ /** @internal Hydrates a field section from existing manifest wire JSON. */
89
+ static _fromExistingComponent(
90
+ customObject: CustomObject,
91
+ component: Record<string, any>,
92
+ ): CustomObjectFieldSection {
93
+ return new CustomObjectFieldSection(customObject, {
94
+ sectionId: component['section_id'],
95
+ name: component['name'] ?? component['section_id'],
96
+ });
97
+ }
98
+
63
99
  /**
64
100
  * Returns the stable identifier for this section (e.g. `'sec_gm_profile'`).
65
101
  *
@@ -91,7 +127,7 @@ export class CustomObjectFieldSection {
91
127
  */
92
128
  toDict(): Record<string, any> {
93
129
  return {
94
- type: 'CUSTOM_OBJECT_FIELD_SECTION',
130
+ type: CustomObjectFieldSection.componentType,
95
131
  section_id: this._sectionId,
96
132
  model_name: this._model.getApiName(),
97
133
  name: this._name,
@@ -1,7 +1,14 @@
1
1
  import { generateId } from './_helpers';
2
2
  import { CustomObjectFieldSection } from './custom-object-field-section';
3
3
  import type { CustomObject } from './custom-object';
4
+ import { getExistingManifestContext, type ExistingManifestContextOptions } from './existing-manifest-context';
4
5
  import type { Field } from './field';
6
+ import type { ManifestComponentDeletion } from './manifest-builder';
7
+
8
+ export interface PageLayoutDeletionIdentifier {
9
+ layoutId: string;
10
+ objectRqlName: string;
11
+ }
5
12
 
6
13
  // ---------------------------------------------------------------------------
7
14
  // header_edits — HeaderEditsDTO
@@ -96,6 +103,8 @@ export interface FieldsSection {
96
103
  name?: string;
97
104
  /** Fields to display in this section. Required. */
98
105
  fields: SectionField[];
106
+ /** Column layout for the fields. Optional. @default `'responsive'` (backend default). */
107
+ layout?: SectionFieldsLayout;
99
108
  }
100
109
 
101
110
  /**
@@ -265,6 +274,10 @@ export interface CustomObjectPageLayoutProps {
265
274
  blueprintKey?: string;
266
275
  }
267
276
 
277
+ export interface CustomObjectPageLayoutLoadFromExistingOptions extends ExistingManifestContextOptions {
278
+ customObjectApiName?: string;
279
+ }
280
+
268
281
  interface SerializedSectionField extends SectionFieldOptions {
269
282
  fieldRqlName: string;
270
283
  }
@@ -274,6 +287,7 @@ interface SerializedFieldsSection {
274
287
  key: string;
275
288
  name: string;
276
289
  fields: SerializedSectionField[];
290
+ layout?: SectionFieldsLayout;
277
291
  }
278
292
 
279
293
  interface SerializedTabWithSections {
@@ -380,12 +394,14 @@ function normalizeFieldsSection(
380
394
  assertSectionBelongsTo(customObject, input.section);
381
395
  const key = input.key ?? input.section.getSectionId();
382
396
  const name = input.name ?? input.section.getName();
383
- return {
397
+ const normalized: SerializedFieldsSection = {
384
398
  key,
385
399
  name,
386
400
  type: 'fields_section',
387
401
  fields: input.fields.map((field) => normalizeSectionField(customObject, input.section, key, field)),
388
402
  };
403
+ if (input.layout != null) normalized.layout = input.layout;
404
+ return normalized;
389
405
  }
390
406
 
391
407
  function normalizeTab(customObject: CustomObject, tab: PageLayoutTab): SerializedPageLayoutTab {
@@ -453,6 +469,11 @@ function normalizeTabEdits(customObject: CustomObject, tabEdits: TabEdits): Seri
453
469
  return normalized;
454
470
  }
455
471
 
472
+ function cloneJson<T>(value: T): T {
473
+ if (value == null) return value;
474
+ return JSON.parse(JSON.stringify(value)) as T;
475
+ }
476
+
456
477
  // ---------------------------------------------------------------------------
457
478
  // Class
458
479
  // ---------------------------------------------------------------------------
@@ -498,6 +519,16 @@ function normalizeTabEdits(customObject: CustomObject, tabEdits: TabEdits): Seri
498
519
  * @see {@link CustomObjectPageLayout.basic} — factory for simple single-tab layouts.
499
520
  */
500
521
  export class CustomObjectPageLayout {
522
+ static readonly componentType = 'CUSTOM_OBJECT_PAGE_LAYOUT' as const;
523
+
524
+ static toDeletionIdentifier(identifier: PageLayoutDeletionIdentifier): ManifestComponentDeletion {
525
+ return {
526
+ type: CustomObjectPageLayout.componentType,
527
+ layout_id: identifier.layoutId,
528
+ object_rql_name: identifier.objectRqlName,
529
+ };
530
+ }
531
+
501
532
  private readonly _layoutId: string;
502
533
  private readonly _apiName: string;
503
534
  private readonly _customObjectApiName: string;
@@ -526,6 +557,40 @@ export class CustomObjectPageLayout {
526
557
  customObject._register(this);
527
558
  }
528
559
 
560
+ /**
561
+ * Loads this page layout from the active existing-manifest JSON context.
562
+ *
563
+ * `id` resolves against `layout_id` first, then `api_name`. When loading by
564
+ * api_name such as `'default'`, pass `customObjectApiName` if multiple objects
565
+ * have a layout with the same api_name.
566
+ */
567
+ static loadFromExisting(
568
+ layoutIdOrApiName: string,
569
+ options: CustomObjectPageLayoutLoadFromExistingOptions = {},
570
+ ): CustomObjectPageLayout {
571
+ return getExistingManifestContext(options).loadPageLayout(layoutIdOrApiName, options);
572
+ }
573
+
574
+ /** @internal Hydrates a page layout from existing manifest wire JSON. */
575
+ static _fromExistingComponent(
576
+ customObject: CustomObject,
577
+ component: Record<string, any>,
578
+ ): CustomObjectPageLayout {
579
+ const layout = Object.create(CustomObjectPageLayout.prototype) as CustomObjectPageLayout;
580
+ Object.assign(layout as Record<string, any>, {
581
+ _layoutId: component['layout_id'],
582
+ _apiName: component['api_name'],
583
+ _customObjectApiName: component['object_rql_name'],
584
+ _name: component['name'] ?? component['api_name'],
585
+ _tabEdits: cloneJson(component['tab_edits'] ?? {}),
586
+ _headerEdits: cloneJson(component['header_edits'] ?? {}),
587
+ _visibilityConditions: cloneJson(component['visibility_conditions'] ?? {}),
588
+ _blueprintKey: component['blueprint_key'],
589
+ });
590
+ customObject._register(layout);
591
+ return layout;
592
+ }
593
+
529
594
  /**
530
595
  * Returns the stable identifier for this layout record (e.g. `'layout_gym_member'`).
531
596
  */
@@ -554,7 +619,7 @@ export class CustomObjectPageLayout {
554
619
  */
555
620
  toDict(): Record<string, any> {
556
621
  const d: Record<string, any> = {
557
- type: 'CUSTOM_OBJECT_PAGE_LAYOUT',
622
+ type: CustomObjectPageLayout.componentType,
558
623
  layout_id: this._layoutId,
559
624
  api_name: this._apiName,
560
625
  object_rql_name: this._customObjectApiName,
@@ -1,5 +1,6 @@
1
1
  import { Category } from './category';
2
- import type { ManifestComponent, ManifestScope } from './manifest-builder';
2
+ import { getExistingManifestContext, type ExistingManifestContextOptions } from './existing-manifest-context';
3
+ import type { ManifestComponent, ManifestComponentDeletion, ManifestScope } from './manifest-builder';
3
4
 
4
5
  // ────────────────────────────────────────────────────────────────────────
5
6
  // Platform-managed standard fields — mirrors
@@ -113,6 +114,8 @@ const STANDARD_FIELD_SPECS: ReadonlyArray<{
113
114
  * (`id`, `created_at`, …) don't carry the custom-field suffix.
114
115
  */
115
116
  class _StandardField implements ManifestComponent {
117
+ static readonly componentType = 'CUSTOM_OBJECT_FIELD' as const;
118
+
116
119
  constructor(
117
120
  private readonly customObjectApiName: string,
118
121
  private readonly spec: (typeof STANDARD_FIELD_SPECS)[number],
@@ -120,7 +123,7 @@ class _StandardField implements ManifestComponent {
120
123
 
121
124
  toDict(): Record<string, any> {
122
125
  return {
123
- type: 'CUSTOM_OBJECT_FIELD',
126
+ type: _StandardField.componentType,
124
127
  custom_object_api_name: this.customObjectApiName,
125
128
  field_api_name: this.spec.apiName,
126
129
  field_display_name: this.spec.displayName,
@@ -215,6 +218,21 @@ export interface CustomObjectProps {
215
218
  * Overrides for the auto-number name field — controls the format string and starting number.
216
219
  * Leave `null` to use the default name field behavior.
217
220
  *
221
+ * Example:
222
+ * ```ts
223
+ * nameFieldDetails: {
224
+ * is_autonumber_field: true,
225
+ * starting_number: 1,
226
+ * autonumber_format: 'EXP-{0000}',
227
+ * }
228
+ * ```
229
+ *
230
+ * @remarks
231
+ * The backend auto-appends `{0000}` to `autonumber_format` if the format string
232
+ * doesn't already contain a sequence token — so `'EXP-'` becomes `'EXP-{0000}'`
233
+ * on the wire. Include the token explicitly when you want a different width
234
+ * (e.g. `'EXP-{00000}'`).
235
+ *
218
236
  * Optional. @default null
219
237
  */
220
238
  nameFieldDetails?: Record<string, any> | null;
@@ -241,7 +259,7 @@ export interface CustomObjectProps {
241
259
  * name: 'Gym Membership Management',
242
260
  * });
243
261
  *
244
- * const gymCategory = new Category(manifest, { apiName: 'gym__c', name: 'Gym' });
262
+ * const gymCategory = new Category(manifest, { apiName: 'gym__c', name: 'Gym', description: 'Gym domain' });
245
263
  *
246
264
  * const memberObj = new CustomObject(manifest, {
247
265
  * apiName: 'gym_member__c',
@@ -266,8 +284,31 @@ export interface CustomObjectProps {
266
284
  * with `DEPENDENCY_ERROR: Page layout config for CO (api_name=...) not found in package
267
285
  * configs. CO requires at least one page layout in the package.`
268
286
  * Use {@link CustomObjectPageLayout.basic} for a minimal single-tab layout.
287
+ *
288
+ * **Consider defining at least one {@link ListViewDef}.** Without a list view,
289
+ * the records page has nothing to display by default — users land on an empty grid.
290
+ * Consider shipping a "default" view with the columns most users want to see
291
+ * (typically `name`, the status / category field, owner, and `created_at`), sorted
292
+ * however the records are most commonly browsed.
293
+ *
294
+ * **Consider system fields before authoring new ones.** Every CO ships with
295
+ * `owner_role`, `created_by`, and `last_modified_by` (all `Employee` references),
296
+ * plus `name`, `created_at`, `updated_at`, `external_id`, and `id`. For any
297
+ * "the employee responsible for this record" semantics, consider using
298
+ * `owner_role` rather than adding a new {@link LookupField} targeting `'role'`.
299
+ * `owner_role` drives Rippling's record-level permissions, so a parallel field
300
+ * can fragment ownership and complicate the permission story.
269
301
  */
270
302
  export class CustomObject implements ManifestScope {
303
+ static readonly componentType = 'CUSTOM_OBJECT' as const;
304
+
305
+ static toDeletionIdentifier(apiName: string): ManifestComponentDeletion {
306
+ return {
307
+ type: CustomObject.componentType,
308
+ api_name: apiName,
309
+ };
310
+ }
311
+
271
312
  private readonly _scope: ManifestScope;
272
313
  private readonly _apiName: string;
273
314
  private readonly _name: string;
@@ -304,6 +345,49 @@ export class CustomObject implements ManifestScope {
304
345
  }
305
346
  }
306
347
 
348
+ /**
349
+ * Loads this custom object from the active existing-manifest JSON context.
350
+ */
351
+ static loadFromExisting(apiName: string, options: ExistingManifestContextOptions = {}): CustomObject {
352
+ return getExistingManifestContext(options).loadCustomObject(apiName);
353
+ }
354
+
355
+ /** @internal Hydrates a custom object from existing manifest wire JSON. */
356
+ static _fromExistingComponent(
357
+ scope: ManifestScope,
358
+ component: Record<string, any>,
359
+ resolveCategory: (apiName: string) => Category | string,
360
+ ): CustomObject {
361
+ const categoryApiName = component['category_api_name'];
362
+ const props: CustomObjectProps = {
363
+ apiName: component['api_name'],
364
+ name: component['name'] ?? component['api_name'],
365
+ category: typeof categoryApiName === 'string' ? resolveCategory(categoryApiName) : categoryApiName,
366
+ };
367
+ if (component['description'] !== undefined) props.description = component['description'];
368
+ if (component['plural_label'] !== undefined) props.pluralLabel = component['plural_label'];
369
+ if (component['name_field_details'] !== undefined) {
370
+ props.nameFieldDetails = component['name_field_details'];
371
+ }
372
+ if (component['default_field_section_name'] !== undefined) {
373
+ props.defaultFieldSectionName = component['default_field_section_name'];
374
+ }
375
+ const icon = CustomObject.iconFromExistingComponent(component['icon']);
376
+ if (icon != null) props.icon = icon;
377
+ return new CustomObject(scope, props);
378
+ }
379
+
380
+ private static iconFromExistingComponent(value: unknown): IconConfig | undefined {
381
+ if (value == null || typeof value !== 'object') return undefined;
382
+ const wire = value as Record<string, any>;
383
+ const icon: IconConfig = {};
384
+ if (wire['emoji'] !== undefined) icon.emoji = wire['emoji'];
385
+ if (wire['rippling_icon'] !== undefined) icon.ripplingIcon = wire['rippling_icon'];
386
+ if (wire['s3_bucket'] !== undefined) icon.s3Bucket = wire['s3_bucket'];
387
+ if (wire['s3_key'] !== undefined) icon.s3Key = wire['s3_key'];
388
+ return Object.keys(icon).length > 0 ? icon : undefined;
389
+ }
390
+
307
391
  /** @internal Scope hook — forwards child registration up to the root builder. */
308
392
  _register(component: ManifestComponent): void {
309
393
  this._scope._register(component);
@@ -333,7 +417,7 @@ export class CustomObject implements ManifestScope {
333
417
  */
334
418
  toDict(): Record<string, any> {
335
419
  const component: Record<string, any> = {
336
- type: 'CUSTOM_OBJECT',
420
+ type: CustomObject.componentType,
337
421
  api_name: this._apiName,
338
422
  name: this._name,
339
423
  description: this._description ?? null,