@object-ui/core 3.1.1 → 3.1.3

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.
@@ -1,4 +1,4 @@
1
1
 
2
- > @object-ui/core@3.1.1 build /home/runner/work/objectui/objectui/packages/core
2
+ > @object-ui/core@3.1.3 build /home/runner/work/objectui/objectui/packages/core
3
3
  > tsc
4
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @object-ui/core
2
2
 
3
+ ## 3.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - @object-ui/types@3.1.3
8
+
9
+ ## 3.1.2
10
+
11
+ ### Patch Changes
12
+
13
+ - @object-ui/types@3.1.2
14
+
3
15
  ## 3.1.1
4
16
 
5
17
  ### Patch Changes
@@ -12,7 +12,7 @@ import type { DataSource, QueryParams, QueryResult, AggregateParams, AggregateRe
12
12
  export interface ValueDataSourceConfig<T = any> {
13
13
  /** The static data array */
14
14
  items: T[];
15
- /** Optional ID field name for findOne/update/delete (defaults to '_id' then 'id') */
15
+ /** Optional ID field name for findOne/update/delete (defaults to 'id' then '_id') */
16
16
  idField?: string;
17
17
  }
18
18
  /**
@@ -15,7 +15,7 @@
15
15
  function getRecordId(record, idField) {
16
16
  if (idField)
17
17
  return record[idField];
18
- return record._id ?? record.id;
18
+ return record.id ?? record._id;
19
19
  }
20
20
  /**
21
21
  * Evaluate an AST-format filter node against a record.
@@ -273,7 +273,7 @@ export class ValueDataSource {
273
273
  const record = { ...data };
274
274
  // Auto-generate an ID if missing
275
275
  if (!getRecordId(record, this.idField)) {
276
- const field = this.idField ?? '_id';
276
+ const field = this.idField ?? 'id';
277
277
  record[field] = `auto_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
278
278
  }
279
279
  this.items.push(record);
package/dist/index.d.ts CHANGED
@@ -25,4 +25,12 @@ export * from './data-scope/index.js';
25
25
  export * from './errors/index.js';
26
26
  export * from './utils/debug.js';
27
27
  export * from './utils/debug-collector.js';
28
+ export * from './utils/merge-views-into-objects.js';
28
29
  export * from './protocols/index.js';
30
+ /**
31
+ * @deprecated Import `composeStacks` from `@objectstack/spec` instead.
32
+ *
33
+ * This re-export is kept only for backward compatibility and will be removed
34
+ * in the next major version of `@object-ui/core`.
35
+ */
36
+ export { composeStacks } from '@objectstack/spec';
package/dist/index.js CHANGED
@@ -24,4 +24,12 @@ export * from './data-scope/index.js';
24
24
  export * from './errors/index.js';
25
25
  export * from './utils/debug.js';
26
26
  export * from './utils/debug-collector.js';
27
+ export * from './utils/merge-views-into-objects.js';
27
28
  export * from './protocols/index.js';
29
+ /**
30
+ * @deprecated Import `composeStacks` from `@objectstack/spec` instead.
31
+ *
32
+ * This re-export is kept only for backward compatibility and will be removed
33
+ * in the next major version of `@object-ui/core`.
34
+ */
35
+ export { composeStacks } from '@objectstack/spec';
@@ -6,7 +6,7 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  import type { Registry } from './Registry.js';
9
- import type { PluginScope, PluginScopeConfig } from '@object-ui/types';
9
+ import type { PluginScope, PluginScopeConfig, AppMetadataPlugin, AppPluginContext } from '@object-ui/types';
10
10
  export interface PluginDefinition {
11
11
  name: string;
12
12
  version: string;
@@ -63,4 +63,24 @@ export declare class PluginSystem {
63
63
  * @returns Array of all plugin definitions
64
64
  */
65
65
  getAllPlugins(): PluginDefinition[];
66
+ /**
67
+ * Install an AppMetadataPlugin at runtime.
68
+ *
69
+ * Wraps the plugin as a PluginDefinition, calls its `init()` and `start()`
70
+ * lifecycle hooks, and loads it into the system.
71
+ *
72
+ * @param plugin - An AppMetadataPlugin instance
73
+ * @param registry - The component registry
74
+ * @param ctx - Optional context passed to the plugin's start() hook
75
+ */
76
+ install(plugin: AppMetadataPlugin, registry: Registry, ctx?: AppPluginContext): Promise<void>;
77
+ /**
78
+ * Uninstall an AppMetadataPlugin at runtime.
79
+ *
80
+ * Calls the plugin's `stop()` lifecycle hook (via onUnload) and
81
+ * removes it from the system.
82
+ *
83
+ * @param pluginName - Name of the plugin to uninstall
84
+ */
85
+ uninstall(pluginName: string): Promise<void>;
66
86
  }
@@ -139,4 +139,44 @@ export class PluginSystem {
139
139
  getAllPlugins() {
140
140
  return Array.from(this.plugins.values());
141
141
  }
142
+ /**
143
+ * Install an AppMetadataPlugin at runtime.
144
+ *
145
+ * Wraps the plugin as a PluginDefinition, calls its `init()` and `start()`
146
+ * lifecycle hooks, and loads it into the system.
147
+ *
148
+ * @param plugin - An AppMetadataPlugin instance
149
+ * @param registry - The component registry
150
+ * @param ctx - Optional context passed to the plugin's start() hook
151
+ */
152
+ async install(plugin, registry, ctx) {
153
+ if (this.loaded.has(plugin.name)) {
154
+ console.warn(`Plugin "${plugin.name}" is already installed. Skipping.`);
155
+ return;
156
+ }
157
+ await plugin.init();
158
+ const definition = {
159
+ name: plugin.name,
160
+ version: plugin.version,
161
+ register: () => { },
162
+ onLoad: async () => {
163
+ await plugin.start(ctx ?? { logger: console });
164
+ },
165
+ onUnload: async () => {
166
+ await plugin.stop();
167
+ },
168
+ };
169
+ await this.loadPlugin(definition, registry);
170
+ }
171
+ /**
172
+ * Uninstall an AppMetadataPlugin at runtime.
173
+ *
174
+ * Calls the plugin's `stop()` lifecycle hook (via onUnload) and
175
+ * removes it from the system.
176
+ *
177
+ * @param pluginName - Name of the plugin to uninstall
178
+ */
179
+ async uninstall(pluginName) {
180
+ await this.unloadPlugin(pluginName);
181
+ }
142
182
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Adapter: merge stack-level views into object definitions.
10
+ *
11
+ * Views are defined at the stack level (views[].listViews) but the runtime
12
+ * expects listViews on each object definition. This bridges the gap until
13
+ * the runtime/provider layer handles it natively.
14
+ *
15
+ * @param objects - Object definitions from composed stack
16
+ * @param views - View definitions containing listViews keyed by object name
17
+ * @returns Objects with listViews merged in from matching views
18
+ */
19
+ export declare function mergeViewsIntoObjects(objects: any[], views: any[]): any[];
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Adapter: merge stack-level views into object definitions.
10
+ *
11
+ * Views are defined at the stack level (views[].listViews) but the runtime
12
+ * expects listViews on each object definition. This bridges the gap until
13
+ * the runtime/provider layer handles it natively.
14
+ *
15
+ * @param objects - Object definitions from composed stack
16
+ * @param views - View definitions containing listViews keyed by object name
17
+ * @returns Objects with listViews merged in from matching views
18
+ */
19
+ export function mergeViewsIntoObjects(objects, views) {
20
+ const viewsByObject = {};
21
+ for (const view of views) {
22
+ if (!view.listViews)
23
+ continue;
24
+ for (const [viewName, listView] of Object.entries(view.listViews)) {
25
+ const objectName = listView?.data?.object;
26
+ if (!objectName)
27
+ continue;
28
+ if (!viewsByObject[objectName])
29
+ viewsByObject[objectName] = {};
30
+ viewsByObject[objectName][viewName] = listView;
31
+ }
32
+ }
33
+ return objects.map((obj) => {
34
+ const v = viewsByObject[obj.name];
35
+ if (!v)
36
+ return obj;
37
+ return { ...obj, listViews: { ...(obj.listViews || {}), ...v } };
38
+ });
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/core",
3
- "version": "3.1.1",
3
+ "version": "3.1.3",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "license": "MIT",
@@ -25,10 +25,10 @@
25
25
  }
26
26
  },
27
27
  "dependencies": {
28
- "@objectstack/spec": "^3.2.1",
28
+ "@objectstack/spec": "^3.2.6",
29
29
  "lodash": "^4.17.23",
30
30
  "zod": "^4.3.6",
31
- "@object-ui/types": "3.1.1"
31
+ "@object-ui/types": "3.1.3"
32
32
  },
33
33
  "devDependencies": {
34
34
  "typescript": "^5.9.3",
@@ -24,7 +24,7 @@ import type {
24
24
  export interface ValueDataSourceConfig<T = any> {
25
25
  /** The static data array */
26
26
  items: T[];
27
- /** Optional ID field name for findOne/update/delete (defaults to '_id' then 'id') */
27
+ /** Optional ID field name for findOne/update/delete (defaults to 'id' then '_id') */
28
28
  idField?: string;
29
29
  }
30
30
 
@@ -35,7 +35,7 @@ export interface ValueDataSourceConfig<T = any> {
35
35
  /** Resolve the ID of a record given possible field names */
36
36
  function getRecordId(record: any, idField?: string): string | number | undefined {
37
37
  if (idField) return record[idField];
38
- return record._id ?? record.id;
38
+ return record.id ?? record._id;
39
39
  }
40
40
 
41
41
  /**
@@ -304,7 +304,7 @@ export class ValueDataSource<T = any> implements DataSource<T> {
304
304
  const record = { ...data } as T;
305
305
  // Auto-generate an ID if missing
306
306
  if (!getRecordId(record, this.idField)) {
307
- const field = this.idField ?? '_id';
307
+ const field = this.idField ?? 'id';
308
308
  (record as any)[field] = `auto_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
309
309
  }
310
310
  this.items.push(record);
@@ -10,11 +10,11 @@ import { describe, it, expect } from 'vitest';
10
10
  import { ValueDataSource } from '../ValueDataSource';
11
11
 
12
12
  const sampleData = [
13
- { _id: '1', name: 'Alice', age: 30, role: 'admin' },
14
- { _id: '2', name: 'Bob', age: 25, role: 'user' },
15
- { _id: '3', name: 'Charlie', age: 35, role: 'admin' },
16
- { _id: '4', name: 'Diana', age: 28, role: 'user' },
17
- { _id: '5', name: 'Eve', age: 22, role: 'guest' },
13
+ { id: '1', name: 'Alice', age: 30, role: 'admin' },
14
+ { id: '2', name: 'Bob', age: 25, role: 'user' },
15
+ { id: '3', name: 'Charlie', age: 35, role: 'admin' },
16
+ { id: '4', name: 'Diana', age: 28, role: 'user' },
17
+ { id: '5', name: 'Eve', age: 22, role: 'guest' },
18
18
  ];
19
19
 
20
20
  function createDS() {
@@ -132,7 +132,7 @@ describe('ValueDataSource — find', () => {
132
132
  const first = result.data[0] as any;
133
133
  expect(first.name).toBeDefined();
134
134
  expect(first.age).toBeDefined();
135
- expect(first._id).toBeUndefined();
135
+ expect(first.id).toBeUndefined();
136
136
  expect(first.role).toBeUndefined();
137
137
  });
138
138
 
@@ -252,7 +252,7 @@ describe('ValueDataSource — AST filter', () => {
252
252
  // ---------------------------------------------------------------------------
253
253
 
254
254
  describe('ValueDataSource — findOne', () => {
255
- it('should find a record by _id', async () => {
255
+ it('should find a record by id', async () => {
256
256
  const ds = createDS();
257
257
  const record = await ds.findOne('users', '3');
258
258
  expect(record).not.toBeNull();
@@ -299,14 +299,14 @@ describe('ValueDataSource — create', () => {
299
299
  it('should auto-generate an ID if missing', async () => {
300
300
  const ds = createDS();
301
301
  const created = await ds.create('users', { name: 'Grace' });
302
- expect((created as any)._id).toBeDefined();
303
- expect(String((created as any)._id).startsWith('auto_')).toBe(true);
302
+ expect((created as any).id).toBeDefined();
303
+ expect(String((created as any).id).startsWith('auto_')).toBe(true);
304
304
  });
305
305
 
306
306
  it('should preserve existing ID', async () => {
307
307
  const ds = createDS();
308
- const created = await ds.create('users', { _id: 'custom-id', name: 'Heidi' });
309
- expect((created as any)._id).toBe('custom-id');
308
+ const created = await ds.create('users', { id: 'custom-id', name: 'Heidi' });
309
+ expect((created as any).id).toBe('custom-id');
310
310
  });
311
311
  });
312
312
 
@@ -374,7 +374,7 @@ describe('ValueDataSource — bulk', () => {
374
374
 
375
375
  it('should bulk delete records', async () => {
376
376
  const ds = createDS();
377
- await ds.bulk!('users', 'delete', [{ _id: '1' }, { _id: '2' }]);
377
+ await ds.bulk!('users', 'delete', [{ id: '1' }, { id: '2' }]);
378
378
  expect(ds.count).toBe(3);
379
379
  });
380
380
  });
@@ -388,7 +388,7 @@ describe('ValueDataSource — getObjectSchema', () => {
388
388
  const ds = createDS();
389
389
  const schema = await ds.getObjectSchema('users');
390
390
  expect(schema.name).toBe('users');
391
- expect(schema.fields._id).toBeDefined();
391
+ expect(schema.fields.id).toBeDefined();
392
392
  expect(schema.fields.name.type).toBe('string');
393
393
  expect(schema.fields.age.type).toBe('number');
394
394
  });
@@ -406,7 +406,7 @@ describe('ValueDataSource — getObjectSchema', () => {
406
406
 
407
407
  describe('ValueDataSource — isolation', () => {
408
408
  it('should not mutate the original items array', async () => {
409
- const original = [{ _id: '1', name: 'Test' }];
409
+ const original = [{ id: '1', name: 'Test' }];
410
410
  const ds = new ValueDataSource({ items: original });
411
411
  await ds.create('x', { name: 'New' });
412
412
  expect(original).toHaveLength(1); // original untouched
@@ -420,11 +420,11 @@ describe('ValueDataSource — isolation', () => {
420
420
 
421
421
  describe('ValueDataSource — aggregate', () => {
422
422
  const aggData = [
423
- { _id: '1', category: 'A', amount: 10 },
424
- { _id: '2', category: 'A', amount: 20 },
425
- { _id: '3', category: 'B', amount: 30 },
426
- { _id: '4', category: 'B', amount: 40 },
427
- { _id: '5', category: 'B', amount: 50 },
423
+ { id: '1', category: 'A', amount: 10 },
424
+ { id: '2', category: 'A', amount: 20 },
425
+ { id: '3', category: 'B', amount: 30 },
426
+ { id: '4', category: 'B', amount: 40 },
427
+ { id: '5', category: 'B', amount: 50 },
428
428
  ];
429
429
 
430
430
  function createAggDS() {
package/src/index.ts CHANGED
@@ -26,4 +26,13 @@ export * from './data-scope/index.js';
26
26
  export * from './errors/index.js';
27
27
  export * from './utils/debug.js';
28
28
  export * from './utils/debug-collector.js';
29
+ export * from './utils/merge-views-into-objects.js';
29
30
  export * from './protocols/index.js';
31
+
32
+ /**
33
+ * @deprecated Import `composeStacks` from `@objectstack/spec` instead.
34
+ *
35
+ * This re-export is kept only for backward compatibility and will be removed
36
+ * in the next major version of `@object-ui/core`.
37
+ */
38
+ export { composeStacks } from '@objectstack/spec';
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { Registry } from './Registry.js';
10
- import type { PluginScope, PluginScopeConfig } from '@object-ui/types';
10
+ import type { PluginScope, PluginScopeConfig, AppMetadataPlugin, AppPluginContext } from '@object-ui/types';
11
11
  import { PluginScopeImpl } from './PluginScopeImpl.js';
12
12
 
13
13
  export interface PluginDefinition {
@@ -158,4 +158,49 @@ export class PluginSystem {
158
158
  getAllPlugins(): PluginDefinition[] {
159
159
  return Array.from(this.plugins.values());
160
160
  }
161
+
162
+ /**
163
+ * Install an AppMetadataPlugin at runtime.
164
+ *
165
+ * Wraps the plugin as a PluginDefinition, calls its `init()` and `start()`
166
+ * lifecycle hooks, and loads it into the system.
167
+ *
168
+ * @param plugin - An AppMetadataPlugin instance
169
+ * @param registry - The component registry
170
+ * @param ctx - Optional context passed to the plugin's start() hook
171
+ */
172
+ async install(plugin: AppMetadataPlugin, registry: Registry, ctx?: AppPluginContext): Promise<void> {
173
+ if (this.loaded.has(plugin.name)) {
174
+ console.warn(`Plugin "${plugin.name}" is already installed. Skipping.`);
175
+ return;
176
+ }
177
+
178
+ await plugin.init();
179
+
180
+ const definition: PluginDefinition = {
181
+ name: plugin.name,
182
+ version: plugin.version,
183
+ register: () => {},
184
+ onLoad: async () => {
185
+ await plugin.start(ctx ?? { logger: console });
186
+ },
187
+ onUnload: async () => {
188
+ await plugin.stop();
189
+ },
190
+ };
191
+
192
+ await this.loadPlugin(definition, registry);
193
+ }
194
+
195
+ /**
196
+ * Uninstall an AppMetadataPlugin at runtime.
197
+ *
198
+ * Calls the plugin's `stop()` lifecycle hook (via onUnload) and
199
+ * removes it from the system.
200
+ *
201
+ * @param pluginName - Name of the plugin to uninstall
202
+ */
203
+ async uninstall(pluginName: string): Promise<void> {
204
+ await this.unloadPlugin(pluginName);
205
+ }
161
206
  }
@@ -223,4 +223,87 @@ describe('PluginSystem', () => {
223
223
  expect(pluginSystem.isLoaded('failing-plugin')).toBe(false);
224
224
  expect(pluginSystem.getPlugin('failing-plugin')).toBeUndefined();
225
225
  });
226
+
227
+ describe('install / uninstall (AppMetadataPlugin)', () => {
228
+ it('should install an AppMetadataPlugin and call init + start', async () => {
229
+ const initFn = vi.fn();
230
+ const startFn = vi.fn();
231
+ const stopFn = vi.fn();
232
+
233
+ const appPlugin = {
234
+ name: '@test/my-plugin',
235
+ version: '1.0.0',
236
+ type: 'app-metadata' as const,
237
+ description: 'Test metadata plugin',
238
+ init: initFn,
239
+ start: startFn,
240
+ stop: stopFn,
241
+ getConfig: () => ({ objects: [] }),
242
+ };
243
+
244
+ await pluginSystem.install(appPlugin, registry, { logger: console });
245
+
246
+ expect(initFn).toHaveBeenCalledTimes(1);
247
+ expect(startFn).toHaveBeenCalledTimes(1);
248
+ expect(startFn).toHaveBeenCalledWith({ logger: console });
249
+ expect(pluginSystem.isLoaded('@test/my-plugin')).toBe(true);
250
+ });
251
+
252
+ it('should not install the same plugin twice', async () => {
253
+ const appPlugin = {
254
+ name: '@test/dup-plugin',
255
+ version: '1.0.0',
256
+ type: 'app-metadata' as const,
257
+ init: vi.fn(),
258
+ start: vi.fn(),
259
+ stop: vi.fn(),
260
+ };
261
+
262
+ await pluginSystem.install(appPlugin, registry);
263
+ await pluginSystem.install(appPlugin, registry);
264
+
265
+ expect(appPlugin.init).toHaveBeenCalledTimes(1);
266
+ });
267
+
268
+ it('should uninstall a plugin and call stop', async () => {
269
+ const stopFn = vi.fn();
270
+ const appPlugin = {
271
+ name: '@test/removable',
272
+ version: '1.0.0',
273
+ type: 'app-metadata' as const,
274
+ init: vi.fn(),
275
+ start: vi.fn(),
276
+ stop: stopFn,
277
+ };
278
+
279
+ await pluginSystem.install(appPlugin, registry);
280
+ expect(pluginSystem.isLoaded('@test/removable')).toBe(true);
281
+
282
+ await pluginSystem.uninstall('@test/removable');
283
+ expect(pluginSystem.isLoaded('@test/removable')).toBe(false);
284
+ expect(stopFn).toHaveBeenCalledTimes(1);
285
+ });
286
+
287
+ it('should throw when uninstalling a plugin that is not installed', async () => {
288
+ await expect(pluginSystem.uninstall('@test/nonexistent')).rejects.toThrow(
289
+ 'Plugin "@test/nonexistent" is not loaded'
290
+ );
291
+ });
292
+
293
+ it('should provide default context when none is given', async () => {
294
+ const startFn = vi.fn();
295
+ const appPlugin = {
296
+ name: '@test/default-ctx',
297
+ version: '1.0.0',
298
+ type: 'app-metadata' as const,
299
+ init: vi.fn(),
300
+ start: startFn,
301
+ stop: vi.fn(),
302
+ };
303
+
304
+ await pluginSystem.install(appPlugin, registry);
305
+
306
+ expect(startFn).toHaveBeenCalledWith({ logger: console });
307
+ });
308
+ });
226
309
  });
@@ -0,0 +1,110 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import { describe, it, expect } from 'vitest';
10
+ import { mergeViewsIntoObjects } from '../merge-views-into-objects';
11
+
12
+ describe('mergeViewsIntoObjects', () => {
13
+ it('should merge listViews from views into corresponding objects', () => {
14
+ const objects = [{ name: 'account', label: 'Account', fields: {} }];
15
+ const views = [
16
+ {
17
+ listViews: {
18
+ all_accounts: {
19
+ name: 'all_accounts',
20
+ label: 'All Accounts',
21
+ type: 'grid',
22
+ data: { provider: 'object', object: 'account' },
23
+ },
24
+ },
25
+ },
26
+ ];
27
+
28
+ const result = mergeViewsIntoObjects(objects, views);
29
+ expect(result[0].listViews.all_accounts).toBeDefined();
30
+ expect(result[0].listViews.all_accounts.label).toBe('All Accounts');
31
+ });
32
+
33
+ it('should preserve existing listViews on objects', () => {
34
+ const objects = [
35
+ {
36
+ name: 'todo_task',
37
+ label: 'Task',
38
+ fields: {},
39
+ listViews: {
40
+ existing: { name: 'existing', label: 'Existing View', type: 'grid', data: { provider: 'object', object: 'todo_task' } },
41
+ },
42
+ },
43
+ ];
44
+ const views = [
45
+ {
46
+ listViews: {
47
+ new_view: {
48
+ name: 'new_view',
49
+ label: 'New View',
50
+ type: 'grid',
51
+ data: { provider: 'object', object: 'todo_task' },
52
+ },
53
+ },
54
+ },
55
+ ];
56
+
57
+ const result = mergeViewsIntoObjects(objects, views);
58
+ const task = result[0];
59
+ expect(task.listViews.existing).toBeDefined();
60
+ expect(task.listViews.new_view).toBeDefined();
61
+ });
62
+
63
+ it('should ignore listViews without data.object', () => {
64
+ const objects = [{ name: 'account', label: 'Account', fields: {} }];
65
+ const views = [
66
+ {
67
+ listViews: {
68
+ orphan: { name: 'orphan', label: 'Orphan View', type: 'grid' },
69
+ },
70
+ },
71
+ ];
72
+
73
+ const result = mergeViewsIntoObjects(objects, views);
74
+ expect(result[0].listViews).toBeUndefined();
75
+ });
76
+
77
+ it('should return objects unchanged when views is empty', () => {
78
+ const objects = [{ name: 'account', label: 'Account', fields: {} }];
79
+
80
+ const result = mergeViewsIntoObjects(objects, []);
81
+ expect(result).toEqual(objects);
82
+ });
83
+
84
+ it('should ignore views without listViews property', () => {
85
+ const objects = [{ name: 'account', label: 'Account', fields: {} }];
86
+ const views = [{ someOtherProp: true }];
87
+
88
+ const result = mergeViewsIntoObjects(objects, views);
89
+ expect(result).toEqual(objects);
90
+ });
91
+
92
+ it('should merge views from multiple view entries into different objects', () => {
93
+ const objects = [
94
+ { name: 'account', label: 'Account', fields: {} },
95
+ { name: 'contact', label: 'Contact', fields: {} },
96
+ ];
97
+ const views = [
98
+ {
99
+ listViews: {
100
+ all_accounts: { name: 'all_accounts', label: 'All Accounts', type: 'grid', data: { provider: 'object', object: 'account' } },
101
+ all_contacts: { name: 'all_contacts', label: 'All Contacts', type: 'grid', data: { provider: 'object', object: 'contact' } },
102
+ },
103
+ },
104
+ ];
105
+
106
+ const result = mergeViewsIntoObjects(objects, views);
107
+ expect(result[0].listViews.all_accounts).toBeDefined();
108
+ expect(result[1].listViews.all_contacts).toBeDefined();
109
+ });
110
+ });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * Adapter: merge stack-level views into object definitions.
11
+ *
12
+ * Views are defined at the stack level (views[].listViews) but the runtime
13
+ * expects listViews on each object definition. This bridges the gap until
14
+ * the runtime/provider layer handles it natively.
15
+ *
16
+ * @param objects - Object definitions from composed stack
17
+ * @param views - View definitions containing listViews keyed by object name
18
+ * @returns Objects with listViews merged in from matching views
19
+ */
20
+ export function mergeViewsIntoObjects(objects: any[], views: any[]): any[] {
21
+ const viewsByObject: Record<string, Record<string, any>> = {};
22
+ for (const view of views) {
23
+ if (!view.listViews) continue;
24
+ for (const [viewName, listView] of Object.entries(view.listViews as Record<string, any>)) {
25
+ const objectName = listView?.data?.object;
26
+ if (!objectName) continue;
27
+ if (!viewsByObject[objectName]) viewsByObject[objectName] = {};
28
+ viewsByObject[objectName][viewName] = listView;
29
+ }
30
+ }
31
+ return objects.map((obj: any) => {
32
+ const v = viewsByObject[obj.name];
33
+ if (!v) return obj;
34
+ return { ...obj, listViews: { ...(obj.listViews || {}), ...v } };
35
+ });
36
+ }