Skip to content

Commit 8c1e17d

Browse files
os-zhuangclaude
andauthored
feat(app-showcase): Account 360 record page + Delivery Operations dashboard (#2059)
Two flagship composed enterprise surfaces — moving the showcase beyond atomic "one-of-everything" fixtures toward realistic, multi-capability pages a developer would actually build. Account 360 (showcase_account_detail, the account object's first record page) — exercises the related-data + collaboration blocks nothing in the showcase used before: • record:related_list — Projects and Invoices as live child lists • record:history — the audit-trail tab • the synthesized discussion slot — activity + comment feed (@mentions) • record:highlights + record:details (Company / Billing sections) Highlights and details deliberately use *disjoint* fields: record:details dedupes highlight fields and hide-empties the rest, so reusing the same fields (as a first draft did) collapses every section to empty. Account seed enriched with tax_id + billing_email so the Billing section renders. Delivery Operations (showcase_ops_dashboard) — a believable ops landing page vs the one-of-every-chart Chart Gallery: • KPI hero row of metric tiles, each scoped by a per-widget filter (active projects / at-risk / awaiting-review) — one dataset sliced different ways • health column + status bar + priority donut + throughput line + spend table • global dateRange (created_at) + global status filter showcase_project_metrics gains status + health dimensions to power it. Both wired into nav (Analytics → Delivery Operations) and registered in objectstack.config. Verified in the browser (:5181): KPI filters resolve, charts render, the Account tabs (Details / Projects / Invoices) and discussion feed all populate. typecheck + 20 showcase tests pass. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f8e25ca commit 8c1e17d

8 files changed

Lines changed: 187 additions & 7 deletions

File tree

examples/app-showcase/objectstack.config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ import {
1414
import * as objects from './src/objects/index.js';
1515
import { TaskViews, ProjectViews } from './src/views/index.js';
1616
import { ShowcaseApp } from './src/apps/index.js';
17-
import { ChartGalleryDashboard } from './src/dashboards/index.js';
17+
import { ChartGalleryDashboard, OpsDashboard } from './src/dashboards/index.js';
1818
import { ShowcaseTaskDataset, ShowcaseProjectDataset } from './src/datasets/index.js';
1919
import { allReports } from './src/reports/index.js';
2020
import { allActions } from './src/actions/index.js';
21-
import { ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage } from './src/pages/index.js';
21+
import { ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage } from './src/pages/index.js';
2222
import { allFlows } from './src/flows/index.js';
2323
import { allWebhooks } from './src/webhooks/index.js';
2424
import { allHooks } from './src/hooks/index.js';
@@ -143,8 +143,8 @@ export default defineStack({
143143
apps: [ShowcaseApp],
144144
portals: allPortals,
145145
views: [TaskViews, ProjectViews],
146-
pages: [ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage],
147-
dashboards: [ChartGalleryDashboard],
146+
pages: [ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage],
147+
dashboards: [ChartGalleryDashboard, OpsDashboard],
148148
books: allBooks,
149149
datasets: [ShowcaseTaskDataset, ShowcaseProjectDataset],
150150
reports: allReports,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const ShowcaseApp = App.create({
3636
label: 'Analytics',
3737
icon: 'chart-bar',
3838
children: [
39+
{ id: 'nav_ops', type: 'dashboard', dashboardName: 'showcase_ops_dashboard', label: 'Delivery Operations', icon: 'gauge' },
3940
{ id: 'nav_charts', type: 'dashboard', dashboardName: 'showcase_chart_gallery', label: 'Chart Gallery', icon: 'layout-dashboard' },
4041
{ id: 'nav_report_tabular', type: 'object', objectName: 'showcase_task', viewName: 'tabular', label: 'Task List', icon: 'table' },
4142
{ id: 'nav_report_summary', type: 'report', reportName: 'showcase_hours_by_status', label: 'Hours by Status', icon: 'sigma' },
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
export { ChartGalleryDashboard } from './chart-gallery.dashboard.js';
4+
export { OpsDashboard } from './ops-dashboard.dashboard.js';
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { ChartConfig, ChartType, Dashboard } from '@objectstack/spec/ui';
4+
5+
const taskDs = 'showcase_task_metrics';
6+
const projectDs = 'showcase_project_metrics';
7+
8+
const cfg = (type: ChartType, dimension: string, measure: string): ChartConfig => ({
9+
type,
10+
xAxis: { field: dimension, showGridLines: true, logarithmic: false },
11+
yAxis: [{ field: measure, showGridLines: true, logarithmic: false }],
12+
showLegend: true,
13+
showDataLabels: false,
14+
});
15+
16+
/**
17+
* Delivery Operations — a *believable business* dashboard (vs the Chart Gallery,
18+
* which is one-of-every-chart). It composes the patterns a real ops landing page
19+
* needs:
20+
* • a KPI hero row of `metric` tiles, each scoped by a per-widget `filter`
21+
* (active projects, at-risk projects, awaiting-review tasks) — the same
22+
* dataset, sliced different ways;
23+
* • comparison / distribution / trend charts underneath;
24+
* • a global `dateRange` (created_at) and a global status filter so the whole
25+
* board re-scopes from the header.
26+
*
27+
* Everything binds the semantic datasets by name (ADR-0021), so a metric is
28+
* defined once and reused.
29+
*/
30+
export const OpsDashboard: Dashboard = {
31+
name: 'showcase_ops_dashboard',
32+
label: 'Delivery Operations',
33+
description: 'Operations landing page — KPI hero row, project health, and task throughput.',
34+
columns: 12,
35+
dateRange: { field: 'created_at', defaultRange: 'last_90_days', allowCustomRange: true },
36+
globalFilters: [
37+
{
38+
field: 'status',
39+
label: 'Task Status',
40+
type: 'select',
41+
options: [
42+
{ value: 'backlog', label: 'Backlog' },
43+
{ value: 'todo', label: 'To Do' },
44+
{ value: 'in_progress', label: 'In Progress' },
45+
{ value: 'in_review', label: 'In Review' },
46+
{ value: 'done', label: 'Done' },
47+
],
48+
scope: 'dashboard',
49+
},
50+
],
51+
widgets: [
52+
// ── KPI hero row — same project dataset, sliced by per-widget filter ──
53+
{ id: 'kpi_active_projects', type: 'metric', title: 'Active Projects', dataset: projectDs, values: ['project_count'], filter: { status: 'active' }, colorVariant: 'blue', layout: { x: 0, y: 0, w: 3, h: 2 } },
54+
{ id: 'kpi_at_risk', type: 'metric', title: 'At-Risk (Red)', dataset: projectDs, values: ['project_count'], filter: { health: 'red' }, colorVariant: 'danger', layout: { x: 3, y: 0, w: 3, h: 2 } },
55+
{ id: 'kpi_awaiting_review', type: 'metric', title: 'Awaiting Review', dataset: taskDs, values: ['task_count'], filter: { status: 'in_review' }, colorVariant: 'warning', layout: { x: 6, y: 0, w: 3, h: 2 } },
56+
{ id: 'kpi_total_budget', type: 'metric', title: 'Total Budget', dataset: projectDs, values: ['budget_sum'], colorVariant: 'success', layout: { x: 9, y: 0, w: 3, h: 2 } },
57+
58+
// ── Health + throughput ──────────────────────────────────────────────
59+
{ id: 'col_health', type: 'column', title: 'Projects by Health', dataset: projectDs, dimensions: ['health'], values: ['project_count'], chartConfig: cfg('column', 'health', 'project_count'), layout: { x: 0, y: 2, w: 4, h: 4 } },
60+
{ id: 'bar_status', type: 'bar', title: 'Tasks by Status', dataset: taskDs, dimensions: ['status'], values: ['task_count'], chartConfig: cfg('bar', 'status', 'task_count'), layout: { x: 4, y: 2, w: 4, h: 4 } },
61+
{ id: 'donut_priority', type: 'donut', title: 'Priority Mix', dataset: taskDs, dimensions: ['priority'], values: ['task_count'], chartConfig: cfg('donut', 'priority', 'task_count'), layout: { x: 8, y: 2, w: 4, h: 4 } },
62+
63+
// ── Trend + account spend ────────────────────────────────────────────
64+
{ id: 'line_created', type: 'line', title: 'Task Throughput (monthly)', dataset: taskDs, dimensions: ['created_at'], values: ['task_count'], chartConfig: cfg('line', 'created_at', 'task_count'), layout: { x: 0, y: 6, w: 6, h: 4 } },
65+
{ id: 'table_spend', type: 'table', title: 'Budget vs Spent by Account', dataset: projectDs, dimensions: ['account'], values: ['project_count', 'budget_sum', 'spent_sum'], layout: { x: 6, y: 6, w: 6, h: 4 } },
66+
],
67+
};

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ const accounts = defineSeed(Account, {
2323
// `status` is required and no default is applied at seed-insert time, so it
2424
// must be set explicitly or the row is rejected (this is why Accounts was
2525
// empty). `hq` also exercises the location field.
26-
{ name: 'Northwind', industry: 'retail', annual_revenue: 8_000_000, website: 'https://northwind.example', status: 'active', hq: { lat: 47.6062, lng: -122.3321 } },
27-
{ name: 'Contoso', industry: 'technology', annual_revenue: 25_000_000, website: 'https://contoso.example', status: 'active', hq: { lat: 37.7749, lng: -122.4194 } },
28-
{ name: 'Fabrikam', industry: 'healthcare', annual_revenue: 12_000_000, website: 'https://fabrikam.example', status: 'prospect', hq: { lat: 40.7128, lng: -74.0060 } },
26+
{ name: 'Northwind', industry: 'retail', annual_revenue: 8_000_000, website: 'https://northwind.example', status: 'active', hq: { lat: 47.6062, lng: -122.3321 }, tax_id: '91-1144442', billing_email: 'ap@northwind.example' },
27+
{ name: 'Contoso', industry: 'technology', annual_revenue: 25_000_000, website: 'https://contoso.example', status: 'active', hq: { lat: 37.7749, lng: -122.4194 }, tax_id: '20-3399881', billing_email: 'billing@contoso.example' },
28+
{ name: 'Fabrikam', industry: 'healthcare', annual_revenue: 12_000_000, website: 'https://fabrikam.example', status: 'prospect', hq: { lat: 40.7128, lng: -74.0060 }, tax_id: '46-7782013', billing_email: 'accounts@fabrikam.example' },
2929
],
3030
});
3131

examples/app-showcase/src/datasets/chart-gallery.dataset.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export const ShowcaseProjectDataset = defineDataset({
3434
object: 'showcase_project',
3535
dimensions: [
3636
{ name: 'account', label: 'Account', field: 'account', type: 'lookup' },
37+
// status / health added for the Operations dashboard — power the
38+
// "by health" chart and the filtered KPI tiles (active / at-risk).
39+
{ name: 'status', label: 'Status', field: 'status', type: 'string' },
40+
{ name: 'health', label: 'Health', field: 'health', type: 'string' },
3741
],
3842
measures: [
3943
{ name: 'project_count', label: 'Projects', aggregate: 'count' },
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { Page } from '@objectstack/spec/ui';
4+
5+
/**
6+
* Account 360 — the flagship "object 360" record page every enterprise app has,
7+
* and the first showcase page to exercise the related-data + collaboration
8+
* blocks that nothing else used before:
9+
* • `record:related_list` — the account's Projects and Invoices, each a live
10+
* child list (relationshipField = the `account`
11+
* lookup on the child object).
12+
* • `record:history` — the field-level audit trail.
13+
* • the synthesized `discussion` slot — a unified activity + comment feed
14+
* (@mentions, reactions): the human collaboration surface, for free.
15+
* • `record:highlights` + `record:details` — the summary strip + sections.
16+
*
17+
* `kind: 'slotted'` — overrides only `highlights` + `tabs`; the synthesizer
18+
* fills the header and the discussion feed. (Tabs-with-children render under the
19+
* slotted path; the same shape inside a full-page region does not — mirror the
20+
* working Project Detail page.)
21+
*/
22+
export const AccountDetailPage: Page = {
23+
name: 'showcase_account_detail',
24+
label: 'Account',
25+
type: 'record',
26+
object: 'showcase_account',
27+
kind: 'slotted',
28+
template: 'default',
29+
isDefault: true,
30+
regions: [],
31+
slots: {
32+
highlights: {
33+
type: 'record:highlights',
34+
properties: { fields: ['status', 'industry', 'annual_revenue'] },
35+
},
36+
tabs: {
37+
type: 'page:tabs',
38+
properties: {
39+
type: 'line',
40+
items: [
41+
{
42+
key: 'details',
43+
label: 'Details',
44+
children: [
45+
{
46+
type: 'record:details',
47+
properties: {
48+
sections: [
49+
{ label: 'Company', columns: 2, fields: ['website', 'hq'] },
50+
{ label: 'Billing', columns: 2, fields: ['tax_id', 'billing_email'] },
51+
],
52+
},
53+
},
54+
],
55+
},
56+
{
57+
key: 'projects',
58+
label: 'Projects',
59+
children: [
60+
{
61+
type: 'record:related_list',
62+
properties: {
63+
objectName: 'showcase_project',
64+
relationshipField: 'account',
65+
title: 'Projects',
66+
columns: ['name', 'status', 'health', 'budget', 'end_date'],
67+
sort: [{ field: 'budget', order: 'desc' }],
68+
limit: 10,
69+
showViewAll: true,
70+
},
71+
},
72+
],
73+
},
74+
{
75+
key: 'invoices',
76+
label: 'Invoices',
77+
children: [
78+
{
79+
type: 'record:related_list',
80+
properties: {
81+
objectName: 'showcase_invoice',
82+
relationshipField: 'account',
83+
title: 'Invoices',
84+
columns: ['name', 'status', 'total', 'issued_on'],
85+
sort: [{ field: 'issued_on', order: 'desc' }],
86+
limit: 10,
87+
showViewAll: true,
88+
},
89+
},
90+
],
91+
},
92+
{
93+
key: 'history',
94+
label: 'History',
95+
children: [
96+
{
97+
type: 'record:history',
98+
properties: { limit: 50, showUser: true, showTimestamp: true },
99+
},
100+
],
101+
},
102+
],
103+
},
104+
},
105+
},
106+
};

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export { TaskWorkbenchPage } from './task-workbench.page.js';
88
export { TaskTriagePage } from './task-triage.page.js';
99
export { ActiveProjectsPage } from './active-projects.page.js';
1010
export { TaskDetailPage } from './task-detail.page.js';
11+
export { AccountDetailPage } from './account-detail.page.js';
1112
export {
1213
TaskBoardPage,
1314
TaskCalendarPage,

0 commit comments

Comments
 (0)