Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/spec-tree-view-type.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
---
"@objectstack/spec": minor
---

feat(spec): add a `tree` view type to the ListView schema

`'tree'` is now a valid `ListView.type` (and `VisualizationType`), backed by a
new `TreeConfigSchema` (`parentField` / `labelField` / `fields` /
`defaultExpandedDepth`, passthrough). This lets a self-referencing object be
served as a tree-grid; without it the runtime Zod-validates view metadata and
silently drops `type:'tree'`. Renderer ships in objectui `@object-ui/plugin-tree`.
4 changes: 2 additions & 2 deletions examples/app-showcase/objectstack.config.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import { ShowcaseExternalDatasource } from './src/datasources/showcase-external.
import { ExternalCustomer, ExternalOrder } from './src/objects/external/index.js';
import { setupShowcaseExternalDatasource } from './src/datasources/external-fixture.js';
import { registerRecalcEndpoint } from './src/server/recalc-endpoint.js';
import { TaskViews, ProjectViews, InquiryViews } from './src/views/index.js';
import { TaskViews, ProjectViews, InquiryViews, BusinessUnitViews } from './src/views/index.js';
import { ShowcaseApp } from './src/apps/index.js';
import { ChartGalleryDashboard, OpsDashboard } from './src/dashboards/index.js';
import { ShowcaseTaskDataset, ShowcaseProjectDataset } from './src/datasets/index.js';
Expand DownExpand Up@@ -153,7 +153,7 @@ export default defineStack({
// UI
apps: [ShowcaseApp],
portals: allPortals,
views: [TaskViews, ProjectViews, InquiryViews],
views: [TaskViews, ProjectViews, InquiryViews, BusinessUnitViews],
pages: [ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage],
dashboards: [ChartGalleryDashboard, OpsDashboard],
books: allBooks,
Expand Down
1 change: 1 addition & 0 deletions examples/app-showcase/src/apps/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,6 +39,7 @@ export const ShowcaseApp = App.create({
{ id: 'nav_products', type: 'object', objectName: 'showcase_product', label: 'Products', icon: 'package' },
{ id: 'nav_teams', type: 'object', objectName: 'showcase_team', label: 'Teams', icon: 'users' },
{ id: 'nav_categories', type: 'object', objectName: 'showcase_category', label: 'Categories', icon: 'list-tree' },
{ id: 'nav_business_units', type: 'object', objectName: 'showcase_business_unit', label: 'Business Units', icon: 'network' },
{ id: 'nav_field_zoo', type: 'object', objectName: 'showcase_field_zoo', label: 'Field Zoo', icon: 'shapes' },
],
},
Expand Down
21 changes: 20 additions & 1 deletion examples/app-showcase/src/data/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { Preference } from '../objects/preference.object.js';
import { Project } from '../objects/project.object.js';
import { Task } from '../objects/task.object.js';
import { Category } from '../objects/category.object.js';
import { BusinessUnit } from '../objects/business-unit.object.js';
import { Team, ProjectMembership } from '../objects/team.object.js';
import { Product, Invoice, InvoiceLine } from '../objects/invoice.object.js';
import { FieldZoo } from '../objects/field-zoo.object.js';
Expand DownExpand Up@@ -83,6 +84,24 @@ const categories = defineSeed(Category, {
],
});

// Org-chart hierarchy seeded by `name` external id; `parent` references another
// record's name, building a 3-level tree the `tree` view renders.
const businessUnits = defineSeed(BusinessUnit, {
mode: 'upsert',
externalId: 'name',
records: [
{ name: 'Acme Corporation', kind: 'company', manager: 'Dana Wong', headcount: 0 },
{ name: 'Product & Engineering', parent: 'Acme Corporation', kind: 'division', manager: 'Lena Ortiz', headcount: 0 },
{ name: 'Platform', parent: 'Product & Engineering', kind: 'department', manager: 'Sam Patel', headcount: 12 },
{ name: 'Frontend Guild', parent: 'Product & Engineering', kind: 'department', manager: 'Yuki Tan', headcount: 9 },
{ name: 'Design Systems', parent: 'Frontend Guild', kind: 'team', manager: 'Priya Rao', headcount: 4 },
{ name: 'Go-To-Market', parent: 'Acme Corporation', kind: 'division', manager: 'Marcus Bell', headcount: 0 },
{ name: 'Sales', parent: 'Go-To-Market', kind: 'department', manager: 'Erin Cole', headcount: 18 },
{ name: 'Enterprise Sales', parent: 'Sales', kind: 'team', manager: 'Tom Nyx', headcount: 7 },
{ name: 'Marketing', parent: 'Go-To-Market', kind: 'department', manager: 'Aria Kim', headcount: 6 },
],
});

const teams = defineSeed(Team, {
mode: 'upsert',
externalId: 'name',
Expand DownExpand Up@@ -195,4 +214,4 @@ const preferences = defineSeed(Preference, {
],
});

export const ShowcaseSeedData = [accounts, products, projects, tasks, categories, teams, memberships, fieldZoo, invoices, invoiceLines, preferences];
export const ShowcaseSeedData = [accounts, products, projects, tasks, categories, businessUnits, teams, memberships, fieldZoo, invoices, invoiceLines, preferences];
34 changes: 34 additions & 0 deletions examples/app-showcase/src/objects/business-unit.object.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { ObjectSchema, Field } from '@objectstack/spec/data';

/**
* Business Unit — a self-referencing org-chart hierarchy. `parent` points back
* at the same object, so the tree has arbitrary depth (company → division →
* department → team). This is the canonical case for the `tree` view type:
* fixed-depth grouping can't express an unbounded self-referencing hierarchy.
*/
export const BusinessUnit = ObjectSchema.create({
name: 'showcase_business_unit',
label: 'Business Unit',
pluralLabel: 'Business Units',
icon: 'network',
description: 'Org-chart hierarchy — demonstrates the tree / tree-grid view over a self-referencing object.',

fields: {
name: Field.text({ label: 'Name', required: true, searchable: true, maxLength: 120 }),
parent: Field.lookup('showcase_business_unit', { label: 'Parent Unit', allowCreate: true }),
kind: Field.select({
label: 'Type',
options: [
{ label: 'Company', value: 'company' },
{ label: 'Division', value: 'division' },
{ label: 'Department', value: 'department' },
{ label: 'Team', value: 'team' },
],
defaultValue: 'department',
}),
manager: Field.text({ label: 'Manager', maxLength: 120 }),
headcount: Field.number({ label: 'Headcount', min: 0, defaultValue: 0 }),
},
});
1 change: 1 addition & 0 deletions examples/app-showcase/src/objects/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ export { Account } from './account.object.js';
export { Project } from './project.object.js';
export { Task } from './task.object.js';
export { Category } from './category.object.js';
export { BusinessUnit } from './business-unit.object.js';
export { Team, ProjectMembership } from './team.object.js';
export { Product, Invoice, InvoiceLine } from './invoice.object.js';
export { FieldZoo } from './field-zoo.object.js';
Expand Down
48 changes: 48 additions & 0 deletions examples/app-showcase/src/views/business-unit.view.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { defineView } from '@objectstack/spec';

const data = { provider: 'object' as const, object: 'showcase_business_unit' };

/**
* Business Unit views — a flat grid plus an Organization Chart that uses the
* `tree` view type to nest records by their self-referencing `parent` field.
*/
export const BusinessUnitViews = defineView({
list: {
label: 'All Units',
type: 'grid',
data,
columns: [
{ field: 'name' },
{ field: 'kind' },
{ field: 'manager' },
{ field: 'headcount' },
{ field: 'parent' },
],
},
listViews: {
org_chart: {
label: 'Organization Chart',
type: 'tree',
data,
columns: ['name', 'kind', 'manager', 'headcount'],
tree: {
parentField: 'parent',
labelField: 'name',
fields: ['kind', 'manager', 'headcount'],
// Roots + one level expanded by default; deeper teams expand on click.
defaultExpandedDepth: 1,
},
},
},
formViews: {
default: {
type: 'simple',
data,
sections: [
{ label: 'Unit', columns: 2, fields: ['name', 'parent', 'kind', 'manager', 'headcount'] },
],
},
},
});
1 change: 1 addition & 0 deletions examples/app-showcase/src/views/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,3 +3,4 @@
export { TaskViews } from './task.view.js';
export { ProjectViews } from './project.view.js';
export { InquiryViews } from './inquiry.view.js';
export { BusinessUnitViews } from './business-unit.view.js';
1 change: 1 addition & 0 deletions packages/spec/api-surface.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -3282,6 +3282,7 @@
"TransitionConfigSchema (const)",
"TransitionPreset (type)",
"TransitionPresetSchema (const)",
"TreeConfigSchema (const)",
"Typography (type)",
"TypographySchema (const)",
"UrlNavItem (type)",
Expand Down
22 changes: 21 additions & 1 deletion packages/spec/src/ui/view.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,7 @@ export const VisualizationTypeSchema = lazySchema(() => z.enum([
'gantt',
'map',
'chart',
'tree',
]).describe('Visualization type that users can switch to'));

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

/**
* Tree (tree-grid) Settings
*
* Renders a self-referencing object as an indented, expand/collapse tree-grid.
* Flat records are nested via a single-parent pointer field (`parentField`).
* Unlike a fixed-depth `grouping`, a tree handles arbitrary depth (org charts,
* category trees, BOMs, nested comments). When `parentField` is omitted the
* renderer auto-detects the object's `tree`/self-reference field.
*/
export const TreeConfigSchema = lazySchema(() => z.object({
parentField: z.string().optional().describe('Single-parent pointer field (auto-detected from the object schema when omitted)'),
labelField: z.string().optional().describe('Field rendered indented in the first column (defaults to "name")'),
fields: z.array(z.string()).optional().describe('Additional fields rendered as flat columns alongside the label'),
defaultExpandedDepth: z.number().int().min(0).optional().describe('Initial expansion depth (0 = roots only; omit = expand all)'),
// Forward-compatible: let renderer-ahead config knobs reach plugin-tree.
}).passthrough());

/**
* Navigation Mode Enum
* Defines how to navigate to the detail view from a list item.
Expand DownExpand Up@@ -502,7 +520,8 @@ export const ListViewSchema = lazySchema(() => z.object({
'timeline', // Chronological Stream (Feed)
'gantt', // Project Timeline
'map', // Geospatial
'chart' // Aggregate visualisation
'chart', // Aggregate visualisation
'tree' // Self-referencing hierarchy (tree-grid)
]).default('grid'),

/** Data Source Configuration */
Expand DownExpand Up@@ -552,6 +571,7 @@ export const ListViewSchema = lazySchema(() => z.object({
gallery: GalleryConfigSchema.optional(),
timeline: TimelineConfigSchema.optional(),
chart: ListChartConfigSchema.optional(),
tree: TreeConfigSchema.optional(),

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