Skip to content

Commit 2e65af2

Browse files
Copilothotlong
andcommitted
refactor: unify composeStacks to @objectstack/spec, remove @object-ui/core implementation
- Delete packages/core/src/utils/compose-stacks.ts and its test file - Remove compose-stacks export from packages/core/src/index.ts - Switch objectstack.config.ts to import composeStacks from @objectstack/spec - Switch apps/console/objectstack.shared.ts to import composeStacks from @objectstack/spec - Add mergeViewsIntoObjects adapter at call sites (runtime bridge) - Aggregate manifest.data from all stacks before composing - Update ROADMAP.md to reflect the migration Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 0e0cb89 commit 2e65af2

6 files changed

Lines changed: 79 additions & 477 deletions

File tree

ROADMAP.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,7 +1166,7 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
11661166

11671167
### P2.6 Plugin Modularization & Dynamic Management
11681168

1169-
> **Status:** Phase 1 complete — Plugin class standard, install/uninstall API, example plugin classes. Phase 1.5 complete — composeStacks, plugin isolation, duplicate merge removal.
1169+
> **Status:** Phase 1 complete — Plugin class standard, install/uninstall API, example plugin classes. Phase 1.5 complete — composeStacks unified to `@objectstack/spec`, plugin isolation, duplicate merge removal.
11701170
11711171
Plugin architecture refactoring to support true modular development, plugin isolation, and dynamic plugin install/uninstall at runtime.
11721172

@@ -1192,7 +1192,7 @@ Plugin architecture refactoring to support true modular development, plugin isol
11921192
- [x] Eliminate `defineStack()` double-pass hack — single `composeStacks()` call produces final config with runtime properties (listViews, actions) preserved. `defineStack()` Zod validation stripped these fields, requiring a second `composeStacks` pass to restore them.
11931193
- [x] Use `composed.apps` unified flow in console shared config — replaced manual `[...crmApps, ...(todoConfig.apps || []), ...]` spreading with CRM navigation patch applied to composed output
11941194
- [x] Use `composed.reports` in console shared config — replaced `...(crmConfig.reports || [])` with `...(composed.reports || [])` to include reports from all stacks
1195-
- [x] **composeStacks responsibility split:** `@object-ui/core` composeStacks handles runtime mapping (merging views→objects listViews, actions→objects) while `@objectstack/spec` composeStacks handles protocol-level composition (broader field concatenation, manifest selection, i18n). ObjectUI apps use the core version for single-pass config with runtime properties preserved.
1195+
- [x] **composeStacks unified to `@objectstack/spec`:** Removed `@object-ui/core` composeStacks implementation. All config composition now uses `composeStacks` from `@objectstack/spec` (protocol-level: object dedup, array concatenation, actions→objects mapping, manifest selection, i18n). Runtime-specific `mergeViewsIntoObjects` adapter applied inline at call sites until the runtime/provider layer handles it natively.
11961196

11971197
**Phase 2 — Dynamic Plugin Loading (Planned)**
11981198
- [ ] Hot-reload / lazy loading of plugins for development
@@ -1242,7 +1242,7 @@ Plugin architecture refactoring to support true modular development, plugin isol
12421242
- [x] **P1: Chart Widget Server-Side Aggregation** — Fixed chart widgets (bar/line/area/pie/donut/scatter) downloading all raw data and aggregating client-side. Added optional `aggregate()` method to `DataSource` interface (`AggregateParams`, `AggregateResult` types) enabling server-side grouping/aggregation via analytics API (e.g. `GET /api/v1/analytics/{resource}?category=…&metric=…&agg=…`). `ObjectChart` now prefers `dataSource.aggregate()` when available, falling back to `dataSource.find()` + client-side aggregation for backward compatibility. Implemented `aggregate()` in `ValueDataSource` (in-memory), `ApiDataSource` (HTTP), and `ObjectStackAdapter` (analytics API with client-side fallback). Only detail widgets (grid/table/list) continue to fetch full data. 9 new tests.
12431243
- [x] **P1: Spec-Aligned CRM I18n** — Fixed CRM internationalization not taking effect on the console. Root cause: CRM metadata used plain string labels instead of spec-aligned `I18nLabel` objects. Fix: (1) Updated CRM app/dashboard/navigation metadata to use `I18nLabel` objects (`{ key, defaultValue }`) per spec. (2) Updated `NavigationItem` and `NavigationArea` types to support I18nLabel. (3) Added `resolveLabel()` helper in NavigationRenderer. (4) Updated `resolveI18nLabel()` to accept `t()` function for translation. (5) Added `loadLanguage` callback in I18nProvider for API-based translation loading. (6) Added `/api/v1/i18n/:lang` endpoint to mock server. Console contains zero CRM-specific code.
12441244
- [x] **P0: Opportunity List View & ObjectDef Column Enrichment** — Fixed ObjectGrid not using objectDef field metadata for type-aware rendering when columns are `string[]` or `ListColumn[]` without full options. (1) Schema resolution always fetches full schema from DataSource for field type metadata. (2) String[] column path enriched with objectDef types, options (with colors), currency, precision for proper CurrencyCellRenderer, SelectCellRenderer (colored badges), PercentCellRenderer, DateCellRenderer. (3) ListColumn[] fieldMeta deep-merged with objectDef field properties (select options with colors, currency code, precision). (4) Opportunity view columns upgraded from bare `string[]` to `ListColumn[]` with explicit types, alignment, and summary aggregation. 9 new tests.
1245-
- [x] **P1: Actions Merge into Object Definitions** — Fixed action buttons never showing in Console/Studio because example object definitions lacked `actions` field. Initially added `mergeActionsIntoObjects()` helper with longest-prefix name matching. Later refactored: all actions now declare explicit `objectName`, and merging is handled by `composeStacks()` in `@object-ui/core`. Created todo task actions (6: complete, start, clone, defer, set_reminder, assign) and kitchen-sink showcase actions (3: change_status, assign_owner, archive). All CRM/Todo/Kitchen Sink objects now serve `actions` in metadata. Fixes #840.
1245+
- [x] **P1: Actions Merge into Object Definitions** — Fixed action buttons never showing in Console/Studio because example object definitions lacked `actions` field. Initially added `mergeActionsIntoObjects()` helper with longest-prefix name matching. Later refactored: all actions now declare explicit `objectName`, and merging is handled by `composeStacks()` from `@objectstack/spec`. Created todo task actions (6: complete, start, clone, defer, set_reminder, assign) and kitchen-sink showcase actions (3: change_status, assign_owner, archive). All CRM/Todo/Kitchen Sink objects now serve `actions` in metadata. Fixes #840.
12461246
- [x] **P1: Unified Debug/Metadata Entry — Remove Redundant Metadata Button** — Removed the visible `<MetadataToggle>` button from RecordDetailView, DashboardView, PageView, and ReportView headers. End users no longer see a "</> Metadata" button that had no practical purpose. The MetadataInspector panel is now only accessible via `?__debug` URL parameter (auto-opens when debug mode is active). ObjectView retains its admin-only Design Tools menu entry for metadata inspection. This unifies the debug entry point and improves end-user UX by removing redundant UI elements.
12471247

12481248
### Ecosystem & Marketplace

apps/console/objectstack.shared.ts

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ObjectStackDefinition } from '@objectstack/spec';
2-
import { composeStacks } from '@object-ui/core';
2+
import { composeStacks } from '@objectstack/spec';
33
import crmConfigImport from '@object-ui/example-crm/objectstack.config';
44
import todoConfigImport from '@object-ui/example-todo/objectstack.config';
55
import kitchenSinkConfigImport from '@object-ui/example-kitchen-sink/objectstack.config';
@@ -13,18 +13,47 @@ function resolveDefault<T>(mod: MaybeDefault<T>): T {
1313
return mod as T;
1414
}
1515

16+
// ---------------------------------------------------------------------------
17+
// Adapter: merge stack-level views (views[].listViews) into object definitions.
18+
// The runtime reads listViews from each object; this bridges the gap until
19+
// the runtime/provider layer handles it natively.
20+
// ---------------------------------------------------------------------------
21+
function mergeViewsIntoObjects(objects: any[], views: any[]): any[] {
22+
const viewsByObject: Record<string, Record<string, any>> = {};
23+
for (const view of views) {
24+
if (!view.listViews) continue;
25+
for (const [viewName, listView] of Object.entries(view.listViews as Record<string, any>)) {
26+
const objectName = listView?.data?.object;
27+
if (!objectName) continue;
28+
if (!viewsByObject[objectName]) viewsByObject[objectName] = {};
29+
viewsByObject[objectName][viewName] = listView;
30+
}
31+
}
32+
return objects.map((obj: any) => {
33+
const v = viewsByObject[obj.name];
34+
if (!v) return obj;
35+
return { ...obj, listViews: { ...(obj.listViews || {}), ...v } };
36+
});
37+
}
38+
1639
const crmConfig = resolveDefault<ObjectStackDefinition>(crmConfigImport);
1740
const todoConfig = resolveDefault<ObjectStackDefinition>(todoConfigImport);
1841
const kitchenSinkConfig = resolveDefault<ObjectStackDefinition>(kitchenSinkConfigImport);
1942

20-
// Single-pass composition: composeStacks handles object deduplication (override),
21-
// views→objects mapping, and actions→objects assignment via objectName.
22-
// No defineStack() validation pass — it would strip runtime properties (listViews,
23-
// actions) from objects, requiring a double-pass hack to restore them.
24-
const composed = composeStacks(
25-
[crmConfig, todoConfig, kitchenSinkConfig] as Record<string, any>[],
26-
{ objectConflict: 'override' },
27-
);
43+
const allConfigs = [crmConfig, todoConfig, kitchenSinkConfig];
44+
45+
// Aggregate seed data from all manifest.data arrays (spec selects one manifest,
46+
// so we collect data from all stacks before composing).
47+
const allData = allConfigs.flatMap((c: any) => c.manifest?.data || c.data || []);
48+
49+
// Protocol-level composition via @objectstack/spec: handles object dedup,
50+
// array concatenation, actions→objects mapping, and manifest selection.
51+
const composed = composeStacks(allConfigs as any[], { objectConflict: 'override' }) as any;
52+
53+
// Adapter: merge views[].listViews into object definitions for the runtime.
54+
if (composed.objects && composed.views) {
55+
composed.objects = mergeViewsIntoObjects(composed.objects, composed.views);
56+
}
2857

2958
// Patch CRM App Navigation to include Report using a supported navigation type
3059
// (type: 'url' passes schema validation while still routing correctly via React Router)
@@ -80,7 +109,7 @@ export const sharedConfig = {
80109
version: '0.1.0',
81110
type: 'app',
82111
name: '@object-ui/console',
83-
data: composed.manifest.data,
112+
data: allData,
84113
},
85114
plugins: [],
86115
datasources: [

objectstack.config.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,34 @@ import { ObjectQLPlugin } from '@objectstack/objectql';
1919
import { InMemoryDriver } from '@objectstack/driver-memory';
2020
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
2121
import { ConsolePlugin } from '@object-ui/console';
22-
import { composeStacks } from '@object-ui/core';
22+
import { composeStacks } from '@objectstack/spec';
2323
import { CRMPlugin } from './examples/crm/plugin';
2424
import { TodoPlugin } from './examples/todo/plugin';
2525
import { KitchenSinkPlugin } from './examples/kitchen-sink/plugin';
2626

27+
// ---------------------------------------------------------------------------
28+
// Adapter: merge stack-level views (views[].listViews) into object definitions.
29+
// The runtime reads listViews from each object; this bridges the gap until
30+
// the runtime/provider layer handles it natively.
31+
// ---------------------------------------------------------------------------
32+
function mergeViewsIntoObjects(objects: any[], views: any[]): any[] {
33+
const viewsByObject: Record<string, Record<string, any>> = {};
34+
for (const view of views) {
35+
if (!view.listViews) continue;
36+
for (const [viewName, listView] of Object.entries(view.listViews as Record<string, any>)) {
37+
const objectName = listView?.data?.object;
38+
if (!objectName) continue;
39+
if (!viewsByObject[objectName]) viewsByObject[objectName] = {};
40+
viewsByObject[objectName][viewName] = listView;
41+
}
42+
}
43+
return objects.map((obj: any) => {
44+
const v = viewsByObject[obj.name];
45+
if (!v) return obj;
46+
return { ...obj, listViews: { ...(obj.listViews || {}), ...v } };
47+
});
48+
}
49+
2750
// Instantiate example plugins
2851
const plugins = [new CRMPlugin(), new TodoPlugin(), new KitchenSinkPlugin()];
2952

@@ -33,11 +56,18 @@ const allConfigs = plugins.map((p) => {
3356
return (raw as any).default || raw;
3457
});
3558

36-
// Single-pass composition: composeStacks handles object deduplication,
37-
// views→objects mapping, and actions→objects assignment via objectName.
38-
// No defineStack() validation pass needed — it would strip runtime properties
39-
// (listViews, actions) from objects, requiring a second merge pass to restore them.
40-
const composed = composeStacks(allConfigs, { objectConflict: 'override' });
59+
// Aggregate seed data from all manifest.data arrays (spec selects one manifest,
60+
// so we collect data from all stacks before composing).
61+
const allData = allConfigs.flatMap((c: any) => c.manifest?.data || c.data || []);
62+
63+
// Protocol-level composition via @objectstack/spec: handles object dedup,
64+
// array concatenation, actions→objects mapping, and manifest selection.
65+
const composed = composeStacks(allConfigs as any[], { objectConflict: 'override' }) as any;
66+
67+
// Adapter: merge views[].listViews into object definitions for the runtime.
68+
if (composed.objects && composed.views) {
69+
composed.objects = mergeViewsIntoObjects(composed.objects, composed.views);
70+
}
4171

4272
const mergedApp = {
4373
...composed,
@@ -47,7 +77,7 @@ const mergedApp = {
4777
version: '0.0.0',
4878
description: 'ObjectUI monorepo development workspace',
4979
type: 'app',
50-
data: composed.manifest.data,
80+
data: allData,
5181
},
5282
};
5383

packages/core/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,4 @@ export * from './data-scope/index.js';
2626
export * from './errors/index.js';
2727
export * from './utils/debug.js';
2828
export * from './utils/debug-collector.js';
29-
export * from './utils/compose-stacks.js';
3029
export * from './protocols/index.js';

0 commit comments

Comments
 (0)