From 6c9b052ff145fe5d44f803c68005e01323dceccd Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:34:15 +0800 Subject: [PATCH] feat(showcase): demonstrate action-param widgets + related-list pagination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v16 sweep (#3358 §4) couldn't exercise three shipped features because the showcase never demonstrated them out of the box — a gap `coverage.ts` doesn't catch (it tracks metadata KINDS, not sub-features like action-param widget types or list pagination). Fill them: - **Action-param widget gallery** (`ActionParamGalleryAction` on Field Zoo): one inline param of every non-trivial type so the ADR-0059 `ActionParamDialog` renders each real field widget — richtext editor, color picker, date picker, select, number, the AutoNumber widget for an `autonumber` param, and the ⚠️ `image`/`file` uploads (multiple/accept/maxSize + the upload guard). No showcase action declared `params` before, so these dialogs were undemonstrated. - **Related-list pagination**: 24 bulk prospects under one account (Northwind) so its Account → Contacts related list exceeds a page and demonstrates server-side `$top`/`$skip` windowing (objectui#2711) on a fresh boot. - Record the sub-feature demonstration in the `action` coverage entry. Verified in the running app: the ParamDialog renders richtext/select/date/color/ autonumber widgets + image/file upload zones (not text boxes); the Northwind Contacts related list holds 26 rows but fetches `?top=5` (windowed, not load-all). `os validate` (9 Actions) + `tsc --noEmit` pass. Follow-up to #3364, from the #3358 sweep §4. Co-Authored-By: Claude Opus 4.8 --- examples/app-showcase/src/coverage.ts | 7 ++- examples/app-showcase/src/data/seed/index.ts | 12 ++++ examples/app-showcase/src/ui/actions/index.ts | 56 +++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/examples/app-showcase/src/coverage.ts b/examples/app-showcase/src/coverage.ts index 6afef38fe..bcabc8cfc 100644 --- a/examples/app-showcase/src/coverage.ts +++ b/examples/app-showcase/src/coverage.ts @@ -91,7 +91,12 @@ export const KIND_COVERAGE: Record = { 'revenue-pulse also demonstrates dashboard-level filters (framework#2501): dateRange + globalFilters driving two objects via per-widget filterBindings.', }, app: { status: 'demonstrated', files: ['src/ui/apps/index.ts'] }, - action: { status: 'demonstrated', files: ['src/ui/actions/index.ts'] }, + action: { + status: 'demonstrated', + files: ['src/ui/actions/index.ts'], + notes: + 'Every ActionType (script/url/flow/modal/api/form). `ActionParamGalleryAction` additionally exercises the ADR-0059 param-dialog widgets: one inline param per non-trivial type (richtext/color/date/select/number/autonumber) plus image/file uploads with multiple/accept/maxSize and the upload guard.', + }, report: { status: 'demonstrated', files: ['src/ui/reports/index.ts'] }, dataset: { status: 'demonstrated', files: ['src/ui/datasets/index.ts'] }, diff --git a/examples/app-showcase/src/data/seed/index.ts b/examples/app-showcase/src/data/seed/index.ts index 6118551ff..84e7c06fb 100644 --- a/examples/app-showcase/src/data/seed/index.ts +++ b/examples/app-showcase/src/data/seed/index.ts @@ -85,6 +85,18 @@ const contacts = defineSeed(Contact, { { name: '张伟', email: 'zhangwei@huaning.example', phone: '+86 21 5550 1111', company: '华宁科技', title: 'Engineering Manager', account: '华宁科技', stage: 'qualified' }, { name: '王芳', email: 'wangfang@huaning.example', phone: '+86 21 5550 2222', company: '华宁科技', title: 'Procurement Director', account: '华宁科技', stage: 'working' }, { name: '李雷', email: 'lilei@huaning.example', phone: '+86 21 5550 3333', company: '华宁科技', title: 'IT Specialist', account: '华宁科技', stage: 'new' }, + // Bulk prospects under ONE account (Northwind) so its Account → Contacts + // related list exceeds a page and demonstrates server-side related-list + // pagination ($top/$skip, objectui#2711) on a fresh boot — every other + // account keeps a handful for a clean people picker. + ...Array.from({ length: 24 }, (_, i) => ({ + name: `Prospect ${String(i + 1).padStart(2, '0')}`, + email: `prospect${i + 1}@northwind.example`, + company: 'Northwind', + title: 'Sales Prospect', + account: 'Northwind', + stage: 'new', + })), ], }); diff --git a/examples/app-showcase/src/ui/actions/index.ts b/examples/app-showcase/src/ui/actions/index.ts index 950762730..4e7f8c0d0 100644 --- a/examples/app-showcase/src/ui/actions/index.ts +++ b/examples/app-showcase/src/ui/actions/index.ts @@ -4,6 +4,7 @@ import { defineAction } from '@objectstack/spec/ui'; const task = 'showcase_task'; const invoice = 'showcase_invoice'; +const fieldZoo = 'showcase_field_zoo'; /** * Action matrix — covers every `ActionType` (script / url / flow / modal / @@ -172,6 +173,60 @@ export const SubmitForSignoffAction = defineAction({ refreshAfter: true, }); +/** + * script — the **action-param widget gallery** (ADR-0059). One inline param of + * every non-trivial widget type, so the `ActionParamDialog` renders each real + * field widget (not a text box): richtext editor, color picker, date picker, + * select, number, the AutoNumber widget for an `autonumber` param, and — the + * ⚠️ ones — `image`/`file` uploads through the ambient UploadProvider with + * `multiple` / `accept` / `maxSize` honored, and the **upload guard** (Confirm + * stays disabled while a file is still uploading). Lives on Field Zoo, the + * "one specimen of everything" object, next to its every-field-type record. + * + * The body just echoes the received keys — the point is the dialog, not a side + * effect — so it needs no capabilities. + */ +export const ActionParamGalleryAction = defineAction({ + name: 'showcase_action_param_gallery', + label: 'Action Param Gallery', + icon: 'sparkles', + objectName: fieldZoo, + type: 'script', + params: [ + { name: 'p_text', type: 'text', label: 'Title', required: true, placeholder: 'A short title' }, + { name: 'p_richtext', type: 'richtext', label: 'Rich note', helpText: 'Renders the rich-text editor, not a plain textarea.' }, + { + name: 'p_priority', type: 'select', label: 'Priority', defaultValue: 'normal', + options: [ + { label: 'Low', value: 'low' }, + { label: 'Normal', value: 'normal' }, + { label: 'High', value: 'high' }, + ], + }, + { name: 'p_date', type: 'date', label: 'Effective date' }, + { name: 'p_color', type: 'color', label: 'Accent color', defaultValue: '#7C3AED' }, + // Spec `autonumber` param → the AutoNumber widget (read-only, auto-assigned). + { name: 'p_reference', type: 'autonumber', label: 'Reference #' }, + // ⚠️ image/file uploads: real widget + upload guard + multiple/accept/maxSize. + { name: 'p_cover', type: 'image', label: 'Cover image', accept: ['image/*'], maxSize: 5 * 1024 * 1024 }, + { + name: 'p_attachments', type: 'file', label: 'Attachments', multiple: true, + accept: ['application/pdf', 'image/*'], maxSize: 10 * 1024 * 1024, + helpText: 'Confirm stays disabled while a file is still uploading (ADR-0059 upload guard).', + }, + ], + body: { + language: 'js', + // No side effect — the value of this action is the dialog's widgets. Echo + // the keys the dialog collected so the result dialog shows something. + source: 'return { ok: true, received: Object.keys(input || {}) };', + capabilities: [], + }, + successMessage: 'Params received — every widget type rendered through the shared field-widget map.', + locations: ['record_header', 'list_item'], + refreshAfter: false, +}); + export const allActions = [ MarkDoneAction, OpenDocsAction, @@ -181,4 +236,5 @@ export const allActions = [ LogTimeAction, NewTaskAction, SubmitForSignoffAction, + ActionParamGalleryAction, ];