Skip to content

Commit b469950

Browse files
xuyushun441-sysos-zhuangclaude
authored
feat(spec+showcase): add 'tree' view type + business_unit org-chart example (#2184)
* feat(showcase): business_unit org-chart + tree view; spec gains 'tree' view type Adds the 'tree' visualization type to the spec ListView schema (enum + a TreeConfigSchema for parentField/labelField/fields/defaultExpandedDepth) so a self-referencing object can be served as a tree-grid, and exercises it in the showcase with a new showcase_business_unit object (self-referencing parent), a 3-level org-chart seed, an 'Organization Chart' tree view, and sidebar nav. Pairs with objectui plugin-tree (renderer). Refs objectui#1885 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(changeset): @objectstack/spec tree view type * chore(spec): record TreeConfigSchema in public API surface snapshot --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 48a307a commit b469950

10 files changed

Lines changed: 140 additions & 4 deletions

File tree

.changeset/spec-tree-view-type.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): add a `tree` view type to the ListView schema
6+
7+
`'tree'` is now a valid `ListView.type` (and `VisualizationType`), backed by a
8+
new `TreeConfigSchema` (`parentField` / `labelField` / `fields` /
9+
`defaultExpandedDepth`, passthrough). This lets a self-referencing object be
10+
served as a tree-grid; without it the runtime Zod-validates view metadata and
11+
silently drops `type:'tree'`. Renderer ships in objectui `@object-ui/plugin-tree`.

examples/app-showcase/objectstack.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { ShowcaseExternalDatasource } from './src/datasources/showcase-external.
1616
import { ExternalCustomer, ExternalOrder } from './src/objects/external/index.js';
1717
import { setupShowcaseExternalDatasource } from './src/datasources/external-fixture.js';
1818
import { registerRecalcEndpoint } from './src/server/recalc-endpoint.js';
19-
import { TaskViews, ProjectViews, InquiryViews } from './src/views/index.js';
19+
import { TaskViews, ProjectViews, InquiryViews, BusinessUnitViews } from './src/views/index.js';
2020
import { ShowcaseApp } from './src/apps/index.js';
2121
import { ChartGalleryDashboard, OpsDashboard } from './src/dashboards/index.js';
2222
import { ShowcaseTaskDataset, ShowcaseProjectDataset } from './src/datasets/index.js';
@@ -153,7 +153,7 @@ export default defineStack({
153153
// UI
154154
apps: [ShowcaseApp],
155155
portals: allPortals,
156-
views: [TaskViews, ProjectViews, InquiryViews],
156+
views: [TaskViews, ProjectViews, InquiryViews, BusinessUnitViews],
157157
pages: [ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage],
158158
dashboards: [ChartGalleryDashboard, OpsDashboard],
159159
books: allBooks,

examples/app-showcase/src/apps/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const ShowcaseApp = App.create({
3939
{ id: 'nav_products', type: 'object', objectName: 'showcase_product', label: 'Products', icon: 'package' },
4040
{ id: 'nav_teams', type: 'object', objectName: 'showcase_team', label: 'Teams', icon: 'users' },
4141
{ id: 'nav_categories', type: 'object', objectName: 'showcase_category', label: 'Categories', icon: 'list-tree' },
42+
{ id: 'nav_business_units', type: 'object', objectName: 'showcase_business_unit', label: 'Business Units', icon: 'network' },
4243
{ id: 'nav_field_zoo', type: 'object', objectName: 'showcase_field_zoo', label: 'Field Zoo', icon: 'shapes' },
4344
],
4445
},

examples/app-showcase/src/data/index.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Preference } from '../objects/preference.object.js';
77
import { Project } from '../objects/project.object.js';
88
import { Task } from '../objects/task.object.js';
99
import { Category } from '../objects/category.object.js';
10+
import { BusinessUnit } from '../objects/business-unit.object.js';
1011
import { Team, ProjectMembership } from '../objects/team.object.js';
1112
import { Product, Invoice, InvoiceLine } from '../objects/invoice.object.js';
1213
import { FieldZoo } from '../objects/field-zoo.object.js';
@@ -83,6 +84,24 @@ const categories = defineSeed(Category, {
8384
],
8485
});
8586

87+
// Org-chart hierarchy seeded by `name` external id; `parent` references another
88+
// record's name, building a 3-level tree the `tree` view renders.
89+
const businessUnits = defineSeed(BusinessUnit, {
90+
mode: 'upsert',
91+
externalId: 'name',
92+
records: [
93+
{ name: 'Acme Corporation', kind: 'company', manager: 'Dana Wong', headcount: 0 },
94+
{ name: 'Product & Engineering', parent: 'Acme Corporation', kind: 'division', manager: 'Lena Ortiz', headcount: 0 },
95+
{ name: 'Platform', parent: 'Product & Engineering', kind: 'department', manager: 'Sam Patel', headcount: 12 },
96+
{ name: 'Frontend Guild', parent: 'Product & Engineering', kind: 'department', manager: 'Yuki Tan', headcount: 9 },
97+
{ name: 'Design Systems', parent: 'Frontend Guild', kind: 'team', manager: 'Priya Rao', headcount: 4 },
98+
{ name: 'Go-To-Market', parent: 'Acme Corporation', kind: 'division', manager: 'Marcus Bell', headcount: 0 },
99+
{ name: 'Sales', parent: 'Go-To-Market', kind: 'department', manager: 'Erin Cole', headcount: 18 },
100+
{ name: 'Enterprise Sales', parent: 'Sales', kind: 'team', manager: 'Tom Nyx', headcount: 7 },
101+
{ name: 'Marketing', parent: 'Go-To-Market', kind: 'department', manager: 'Aria Kim', headcount: 6 },
102+
],
103+
});
104+
86105
const teams = defineSeed(Team, {
87106
mode: 'upsert',
88107
externalId: 'name',
@@ -195,4 +214,4 @@ const preferences = defineSeed(Preference, {
195214
],
196215
});
197216

198-
export const ShowcaseSeedData = [accounts, products, projects, tasks, categories, teams, memberships, fieldZoo, invoices, invoiceLines, preferences];
217+
export const ShowcaseSeedData = [accounts, products, projects, tasks, categories, businessUnits, teams, memberships, fieldZoo, invoices, invoiceLines, preferences];
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { ObjectSchema, Field } from '@objectstack/spec/data';
4+
5+
/**
6+
* Business Unit — a self-referencing org-chart hierarchy. `parent` points back
7+
* at the same object, so the tree has arbitrary depth (company → division →
8+
* department → team). This is the canonical case for the `tree` view type:
9+
* fixed-depth grouping can't express an unbounded self-referencing hierarchy.
10+
*/
11+
export const BusinessUnit = ObjectSchema.create({
12+
name: 'showcase_business_unit',
13+
label: 'Business Unit',
14+
pluralLabel: 'Business Units',
15+
icon: 'network',
16+
description: 'Org-chart hierarchy — demonstrates the tree / tree-grid view over a self-referencing object.',
17+
18+
fields: {
19+
name: Field.text({ label: 'Name', required: true, searchable: true, maxLength: 120 }),
20+
parent: Field.lookup('showcase_business_unit', { label: 'Parent Unit', allowCreate: true }),
21+
kind: Field.select({
22+
label: 'Type',
23+
options: [
24+
{ label: 'Company', value: 'company' },
25+
{ label: 'Division', value: 'division' },
26+
{ label: 'Department', value: 'department' },
27+
{ label: 'Team', value: 'team' },
28+
],
29+
defaultValue: 'department',
30+
}),
31+
manager: Field.text({ label: 'Manager', maxLength: 120 }),
32+
headcount: Field.number({ label: 'Headcount', min: 0, defaultValue: 0 }),
33+
},
34+
});

examples/app-showcase/src/objects/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export { Account } from './account.object.js';
44
export { Project } from './project.object.js';
55
export { Task } from './task.object.js';
66
export { Category } from './category.object.js';
7+
export { BusinessUnit } from './business-unit.object.js';
78
export { Team, ProjectMembership } from './team.object.js';
89
export { Product, Invoice, InvoiceLine } from './invoice.object.js';
910
export { FieldZoo } from './field-zoo.object.js';
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { defineView } from '@objectstack/spec';
4+
5+
const data = { provider: 'object' as const, object: 'showcase_business_unit' };
6+
7+
/**
8+
* Business Unit views — a flat grid plus an Organization Chart that uses the
9+
* `tree` view type to nest records by their self-referencing `parent` field.
10+
*/
11+
export const BusinessUnitViews = defineView({
12+
list: {
13+
label: 'All Units',
14+
type: 'grid',
15+
data,
16+
columns: [
17+
{ field: 'name' },
18+
{ field: 'kind' },
19+
{ field: 'manager' },
20+
{ field: 'headcount' },
21+
{ field: 'parent' },
22+
],
23+
},
24+
listViews: {
25+
org_chart: {
26+
label: 'Organization Chart',
27+
type: 'tree',
28+
data,
29+
columns: ['name', 'kind', 'manager', 'headcount'],
30+
tree: {
31+
parentField: 'parent',
32+
labelField: 'name',
33+
fields: ['kind', 'manager', 'headcount'],
34+
// Roots + one level expanded by default; deeper teams expand on click.
35+
defaultExpandedDepth: 1,
36+
},
37+
},
38+
},
39+
formViews: {
40+
default: {
41+
type: 'simple',
42+
data,
43+
sections: [
44+
{ label: 'Unit', columns: 2, fields: ['name', 'parent', 'kind', 'manager', 'headcount'] },
45+
],
46+
},
47+
},
48+
});

examples/app-showcase/src/views/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
export { TaskViews } from './task.view.js';
44
export { ProjectViews } from './project.view.js';
55
export { InquiryViews } from './inquiry.view.js';
6+
export { BusinessUnitViews } from './business-unit.view.js';

packages/spec/api-surface.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3282,6 +3282,7 @@
32823282
"TransitionConfigSchema (const)",
32833283
"TransitionPreset (type)",
32843284
"TransitionPresetSchema (const)",
3285+
"TreeConfigSchema (const)",
32853286
"Typography (type)",
32863287
"TypographySchema (const)",
32873288
"UrlNavItem (type)",

packages/spec/src/ui/view.zod.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ export const VisualizationTypeSchema = lazySchema(() => z.enum([
225225
'gantt',
226226
'map',
227227
'chart',
228+
'tree',
228229
]).describe('Visualization type that users can switch to'));
229230

230231
/**
@@ -433,6 +434,23 @@ export const GanttConfigSchema = lazySchema(() => z.object({
433434
// stripped here, so a renderer release no longer has to wait on a spec release.
434435
}).passthrough());
435436

437+
/**
438+
* Tree (tree-grid) Settings
439+
*
440+
* Renders a self-referencing object as an indented, expand/collapse tree-grid.
441+
* Flat records are nested via a single-parent pointer field (`parentField`).
442+
* Unlike a fixed-depth `grouping`, a tree handles arbitrary depth (org charts,
443+
* category trees, BOMs, nested comments). When `parentField` is omitted the
444+
* renderer auto-detects the object's `tree`/self-reference field.
445+
*/
446+
export const TreeConfigSchema = lazySchema(() => z.object({
447+
parentField: z.string().optional().describe('Single-parent pointer field (auto-detected from the object schema when omitted)'),
448+
labelField: z.string().optional().describe('Field rendered indented in the first column (defaults to "name")'),
449+
fields: z.array(z.string()).optional().describe('Additional fields rendered as flat columns alongside the label'),
450+
defaultExpandedDepth: z.number().int().min(0).optional().describe('Initial expansion depth (0 = roots only; omit = expand all)'),
451+
// Forward-compatible: let renderer-ahead config knobs reach plugin-tree.
452+
}).passthrough());
453+
436454
/**
437455
* Navigation Mode Enum
438456
* Defines how to navigate to the detail view from a list item.
@@ -502,7 +520,8 @@ export const ListViewSchema = lazySchema(() => z.object({
502520
'timeline', // Chronological Stream (Feed)
503521
'gantt', // Project Timeline
504522
'map', // Geospatial
505-
'chart' // Aggregate visualisation
523+
'chart', // Aggregate visualisation
524+
'tree' // Self-referencing hierarchy (tree-grid)
506525
]).default('grid'),
507526

508527
/** Data Source Configuration */
@@ -552,6 +571,7 @@ export const ListViewSchema = lazySchema(() => z.object({
552571
gallery: GalleryConfigSchema.optional(),
553572
timeline: TimelineConfigSchema.optional(),
554573
chart: ListChartConfigSchema.optional(),
574+
tree: TreeConfigSchema.optional(),
555575

556576
/** View Metadata (Airtable-style view management) */
557577
description: I18nLabelSchema.optional().describe('View description for documentation/tooltips'),

0 commit comments

Comments
 (0)