From 477059a5b1fddd906cbcd3ef6af5b754944c8265 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Sat, 20 Jun 2026 16:10:17 +0800 Subject: [PATCH] feat(app-showcase): re-instate grid KPIs, role-gated note, History tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that the SDUI renderer fixes landed in objectui (#1814), restore the showcase features that were dropped as workarounds: My Work — KPI hero row moves from `flex` back to an equal-width `grid` (layout grid no longer shadowed by ObjectGrid); sidebar gains a per-user `visible`-gated note (`user.email == 'admin@objectos.ai'` shows for the admin, a different-email note stays hidden) demonstrating component-level role gating. Account 360 — the History tab returns: `record:history` now self-fetches from sys_activity field_change events (trackHistory on status/industry), so the tab lists real audit entries instead of "No history yet". Browser-verified on :5181: grid-cols KPI row, role-gated visibility, and the History tab populating after an industry change. typecheck + 20 tests pass. Co-Authored-By: Claude Opus 4.8 --- .../src/pages/account-detail.page.ts | 13 ++++++-- .../app-showcase/src/pages/my-work.page.ts | 30 +++++++++---------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/examples/app-showcase/src/pages/account-detail.page.ts b/examples/app-showcase/src/pages/account-detail.page.ts index 09b62a655f..01b6943fc6 100644 --- a/examples/app-showcase/src/pages/account-detail.page.ts +++ b/examples/app-showcase/src/pages/account-detail.page.ts @@ -9,8 +9,8 @@ import type { Page } from '@objectstack/spec/ui'; * • `record:related_list` — the account's Projects and Invoices, each a live * child list (relationshipField = the `account` * lookup on the child object). - * • field changes surface in the synthesized discussion feed (trackHistory - * on status/industry) — verified: 'Industry: Retail → Finance'. + * • `record:history` — a self-fetching audit tab (sys_activity field_change), + * and the same changes also surface in the discussion feed. * • the synthesized `discussion` slot — a unified activity + comment feed * (@mentions, reactions): the human collaboration surface, for free. * • `record:highlights` + `record:details` — the summary strip + sections. @@ -90,6 +90,15 @@ export const AccountDetailPage: Page = { }, ], }, + { + key: 'history', + label: 'History', + children: [ + // Self-fetches from sys_activity (field_change events) via record + // context — trackHistory on status/industry feeds the entries. + { type: 'record:history', properties: { limit: 50 } }, + ], + }, ], }, }, diff --git a/examples/app-showcase/src/pages/my-work.page.ts b/examples/app-showcase/src/pages/my-work.page.ts index 76c2dc4e8e..8d8eeaa3ea 100644 --- a/examples/app-showcase/src/pages/my-work.page.ts +++ b/examples/app-showcase/src/pages/my-work.page.ts @@ -5,14 +5,10 @@ import type { Page } from '@objectstack/spec/ui'; /** * My Work — a role-aware workspace home that *composes* live data the way a * real landing page does, instead of listing one component per type: - * • a KPI hero row of live `object-metric` tiles (team throughput); + * • a KPI hero row of live `object-metric` tiles in an equal-width `grid`; * • a personal work queue — `object-grid` filtered to the signed-in user * via the `{current_user_id}` token (records I own); - * • a sidebar Shortcuts card pointing at the key surfaces. - * - * (Note: page-component `visible` expressions don't receive a `current_user` - * context in the renderer, so true per-role gating belongs in object/field - * permissions + sharing rules, not a card-level expression.) + * • sidebar shortcuts + a per-user `visible`-gated note on `user.email`. */ export const MyWorkPage: Page = { name: 'showcase_my_work', @@ -33,14 +29,12 @@ export const MyWorkPage: Page = { name: 'main', width: 'large', components: [ - // KPI hero ROW — three live tiles laid out horizontally via a grid. + // KPI hero ROW — equal-width tiles in a 3-col grid (layout grid, not ObjectGrid). { - type: 'flex', + type: 'grid', properties: { - direction: 'row', - wrap: true, + columns: 3, gap: 4, - align: 'stretch', children: [ { type: 'object-metric', properties: { objectName: 'showcase_task', label: 'Open Tasks', icon: 'list-checks', aggregate: { field: 'id', function: 'count' }, filter: { status: { $ne: 'done' } } } }, { type: 'object-metric', properties: { objectName: 'showcase_task', label: 'In Review', icon: 'eye', aggregate: { field: 'id', function: 'count' }, filter: { status: 'in_review' } } }, @@ -64,12 +58,16 @@ export const MyWorkPage: Page = { name: 'sidebar', width: 'small', components: [ - // Sidebar shortcuts — plain text lines (region containers don't render - // nested page:card bodies; direct components are the reliable path). { type: 'element:text', properties: { content: 'Shortcuts' } }, - { type: 'element:text', properties: { content: '• Delivery Operations — the org-wide KPI dashboard.' } }, - { type: 'element:text', properties: { content: '• Approvals — items in review awaiting a decision.' } }, - { type: 'element:text', properties: { content: '• New Project (Wizard) — create a project step by step.' } }, + { type: 'element:text', properties: { content: '• Delivery Operations — org-wide KPI dashboard.' } }, + { type: 'element:text', properties: { content: '• Approvals — items in review.' } }, + { type: 'element:text', properties: { content: '• New Project (Wizard).' } }, + // Per-user rendering via `visible` on the signed-in user (now that the + // renderer feeds `user` into the expression context). The first line + // shows for the admin; the second is gated to a different email and + // stays hidden — proving the gate, not just always-on rendering. + { type: 'element:text', properties: { visible: "user.email == 'admin@objectos.ai'", content: '✓ Admin-only note (visible: user.email == admin@objectos.ai)' } }, + { type: 'element:text', properties: { visible: "user.email == 'nobody@example.com'", content: 'This must NOT appear (gated to a different user).' } }, ], }, ],