From 62e5a7015fe63b54ea39b0c566546a14bd891dba Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 13:10:55 +0000 Subject: [PATCH 1/2] feat(app-shell): metadata-driven Validations/Hooks/Actions config panels in Studio Data tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The object Validations, Hooks and Actions sub-tabs of the Studio Data pillar are now no-code config panels driven by the corresponding metadata, each able to create new entries: - Validations: author every spec rule type — not just `script` but also `cross_field`, `state_machine`, `format`, `json_schema` and `conditional` (previously read-only "maintain in code"). The New menu adds any type seeded with a valid never-firing skeleton, and a rule's type is switchable in place (replacing stale type-specific keys). CEL predicates reuse the shared ConditionBuilder fed the object's draft fields. - Hooks: feed the live `hook` JSONSchema from /meta/types to SchemaForm instead of synthesising a form from the value shape. - Actions: pass the live `action` JSONSchema to ActionDefaultInspector as serverSchema so its "More fields" section can edit any uncurated property. DataPillar resolves the per-type schemas once (useMetadataTypes) and passes them down. Updates the Validations panel tests and console guide docs. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LnU2XsAYisR96vUMCqfWVm --- ...config-panels-validations-hooks-actions.md | 13 + content/docs/guide/console.md | 18 + .../src/views/metadata-admin/i18n.ts | 60 ++ .../studio-design/ObjectActionsPanel.tsx | 9 + .../views/studio-design/ObjectHooksPanel.tsx | 9 +- .../ObjectValidationsPanel.test.tsx | 53 +- .../studio-design/ObjectValidationsPanel.tsx | 582 +++++++++++++++--- .../studio-design/StudioDesignSurface.tsx | 25 +- 8 files changed, 686 insertions(+), 83 deletions(-) create mode 100644 .changeset/object-config-panels-validations-hooks-actions.md diff --git a/.changeset/object-config-panels-validations-hooks-actions.md b/.changeset/object-config-panels-validations-hooks-actions.md new file mode 100644 index 000000000..75a288f3d --- /dev/null +++ b/.changeset/object-config-panels-validations-hooks-actions.md @@ -0,0 +1,13 @@ +--- +'@object-ui/app-shell': minor +--- + +Studio Data tab: metadata-driven config panels for Validations, Hooks and Actions (with add). + +The object **Validations**, **Hooks** and **Actions** sub-tabs are now no-code config panels driven by the corresponding metadata, each able to **create** new entries: + +- **Validations** — the panel covers every spec rule type, not just `script`: `cross_field`, `state_machine`, `format`, `json_schema` and `conditional` are all authorable (previously they were read-only "maintain in code"). The **New** menu adds any type seeded with a valid, never-firing skeleton, and a rule's type can be switched in place; CEL predicates reuse the shared `ConditionBuilder`. +- **Hooks** — the `SchemaForm` is now fed the live `hook` JSONSchema from `/meta/types` instead of synthesising a form from the value shape, so its fields and enums match the running server's contract. +- **Actions** — the `ActionDefaultInspector` now receives the live `action` JSONSchema as `serverSchema`, so its "More fields" section can edit any spec property not curated above (nothing is un-editable). + +`DataPillar` resolves the per-type schemas once (via `useMetadataTypes`) and passes them down. diff --git a/content/docs/guide/console.md b/content/docs/guide/console.md index dd11505f1..97924d7e9 100644 --- a/content/docs/guide/console.md +++ b/content/docs/guide/console.md @@ -33,6 +33,24 @@ The console opens at **http://localhost:5175** with MSW (Mock Service Worker) pr | **App Creation Wizard** | 4-step wizard (Basic Info → Objects → Navigation → Branding) to create or edit apps. | | **Error Boundary** | Graceful error handling with a retry button. | +### Object design (Studio Data tab) + +Selecting an object in Studio's **Data** pillar (`/studio/:packageId/data`) opens a +tab strip over that object — **Records · Form · Validations · Hooks · Actions · +API · Settings**. Each of Validations, Hooks and Actions is a no-code **config +panel driven by the corresponding metadata**, and each supports **adding** new +entries — no code round-trip required: + +| Tab | Edits | Panel | +|-----|-------|-------| +| **Validations** | the object's inline `validations[]` (spec `ValidationRuleSchema`) | Master-detail covering **every** rule type — `script`, `cross_field`, `state_machine`, `format`, `json_schema`, `conditional`. The **New** menu adds any type (seeded with a valid, never-firing skeleton); a rule's type can be switched in place. CEL predicates reuse the shared `ConditionBuilder`, fed the object's draft fields. | +| **Hooks** | the separate `hook` metadata type targeting this object | Master-detail whose editor is the platform `SchemaForm` **driven by the live `hook` JSONSchema from `/meta/types`**, so its fields and enums always match the running server's contract. | +| **Actions** | the object's inline `actions[]` (spec `ActionSchema`) | Master-detail using the type-aware `ActionDefaultInspector`; anything not curated falls through to a **"More fields"** form fed the live `action` JSONSchema, so no spec property is un-editable. | + +Validations and Actions persist with the object's own **Save draft**; Hooks (a +distinct metadata type) save per-hook. Nothing goes live until the package is +published from the top-bar **Publish** flow. + ## Configuration The console reads its configuration from `objectstack.config.ts`: diff --git a/packages/app-shell/src/views/metadata-admin/i18n.ts b/packages/app-shell/src/views/metadata-admin/i18n.ts index bb2c9fcf1..8283417d9 100644 --- a/packages/app-shell/src/views/metadata-admin/i18n.ts +++ b/packages/app-shell/src/views/metadata-admin/i18n.ts @@ -984,6 +984,36 @@ const ENGINE_STRINGS_EN: Record = { 'engine.studio.rules.severityError': 'error (rejects the save)', 'engine.studio.rules.enabled': 'Enabled', 'engine.studio.rules.delete': 'Delete', + 'engine.studio.rules.type': 'Type', + 'engine.studio.rules.newType': 'New validation type', + 'engine.studio.rules.typeScript': 'Script — CEL fail condition', + 'engine.studio.rules.typeCrossField': 'Cross-field — CEL over multiple fields', + 'engine.studio.rules.typeStateMachine': 'State machine — allowed transitions', + 'engine.studio.rules.typeFormat': 'Format — regex / built-in format', + 'engine.studio.rules.typeJsonSchema': 'JSON schema — validate a JSON field', + 'engine.studio.rules.typeConditional': 'Conditional — apply a rule when a guard holds', + 'engine.studio.rules.label': 'Label (optional)', + 'engine.studio.rules.field': 'Field', + 'engine.studio.rules.statusField': 'Status field', + 'engine.studio.rules.fields': 'Participating fields', + 'engine.studio.rules.noFields': 'This object has no fields yet.', + 'engine.studio.rules.pickField': '— pick a field —', + 'engine.studio.rules.format': 'Built-in format', + 'engine.studio.rules.formatNone': '— none —', + 'engine.studio.rules.regex': 'Regex pattern', + 'engine.studio.rules.transitions': 'Allowed transitions (from → next states)', + 'engine.studio.rules.transitionFrom': 'From state', + 'engine.studio.rules.transitionTo': 'Next states (comma-separated)', + 'engine.studio.rules.addTransition': 'Add transition', + 'engine.studio.rules.jsonSchema': 'JSON Schema', + 'engine.studio.rules.invalidJson': 'Invalid JSON', + 'engine.studio.rules.then': 'Then — rule applied when the condition holds (JSON)', + 'engine.studio.rules.otherwise': 'Otherwise — rule applied when it does not (JSON, optional)', + 'engine.studio.rules.events': 'Runs on', + 'engine.studio.rules.event.insert': 'Create', + 'engine.studio.rules.event.update': 'Update', + 'engine.studio.rules.event.delete': 'Delete', + 'engine.studio.rules.priority': 'Priority', 'engine.studio.settings.basics': 'Basics', 'engine.studio.settings.noInspector': 'No default object inspector registered.', 'engine.studio.settings.semanticRoles': 'Semantic roles', @@ -1988,6 +2018,36 @@ const ENGINE_STRINGS_ZH: Record = { 'engine.studio.rules.severityError': 'error(拒绝保存)', 'engine.studio.rules.enabled': '启用', 'engine.studio.rules.delete': '删除', + 'engine.studio.rules.type': '类型', + 'engine.studio.rules.newType': '新建验证类型', + 'engine.studio.rules.typeScript': 'Script —— CEL 失败条件', + 'engine.studio.rules.typeCrossField': 'Cross-field —— 跨字段 CEL', + 'engine.studio.rules.typeStateMachine': 'State machine —— 允许的状态流转', + 'engine.studio.rules.typeFormat': 'Format —— 正则 / 内置格式', + 'engine.studio.rules.typeJsonSchema': 'JSON schema —— 校验 JSON 字段', + 'engine.studio.rules.typeConditional': 'Conditional —— 条件成立时应用规则', + 'engine.studio.rules.label': '标签(可选)', + 'engine.studio.rules.field': '字段', + 'engine.studio.rules.statusField': '状态字段', + 'engine.studio.rules.fields': '参与字段', + 'engine.studio.rules.noFields': '该对象暂无字段。', + 'engine.studio.rules.pickField': '—— 选择字段 ——', + 'engine.studio.rules.format': '内置格式', + 'engine.studio.rules.formatNone': '—— 无 ——', + 'engine.studio.rules.regex': '正则表达式', + 'engine.studio.rules.transitions': '允许的状态流转(从 → 下一状态)', + 'engine.studio.rules.transitionFrom': '起始状态', + 'engine.studio.rules.transitionTo': '下一状态(逗号分隔)', + 'engine.studio.rules.addTransition': '新增流转', + 'engine.studio.rules.jsonSchema': 'JSON Schema', + 'engine.studio.rules.invalidJson': 'JSON 格式无效', + 'engine.studio.rules.then': 'Then —— 条件成立时应用的规则(JSON)', + 'engine.studio.rules.otherwise': 'Otherwise —— 条件不成立时应用的规则(JSON,可选)', + 'engine.studio.rules.events': '触发时机', + 'engine.studio.rules.event.insert': '新建', + 'engine.studio.rules.event.update': '更新', + 'engine.studio.rules.event.delete': '删除', + 'engine.studio.rules.priority': '优先级', 'engine.studio.settings.basics': '基础信息', 'engine.studio.settings.noInspector': '未注册对象默认检查器。', 'engine.studio.settings.semanticRoles': '语义角色', diff --git a/packages/app-shell/src/views/studio-design/ObjectActionsPanel.tsx b/packages/app-shell/src/views/studio-design/ObjectActionsPanel.tsx index a57f30f83..4bdaba40f 100644 --- a/packages/app-shell/src/views/studio-design/ObjectActionsPanel.tsx +++ b/packages/app-shell/src/views/studio-design/ObjectActionsPanel.tsx @@ -69,10 +69,18 @@ export function ObjectActionsPanel({ draft, onPatch, disabled, + actionSchema, }: { draft: Record; onPatch: (patch: Record) => void; disabled?: boolean; + /** + * The live server JSONSchema for the `action` type (`/meta/types`). Handed to + * ActionDefaultInspector as `serverSchema` so its "More fields" section can + * edit any spec property not curated above — keeping this a faithful, + * forward-compatible config panel for the action metadata. + */ + actionSchema?: Record; }) { const locale = useMetadataLocale(); const actions = React.useMemo(() => readActions(draft.actions), [draft.actions]); @@ -203,6 +211,7 @@ export function ObjectActionsPanel({ onPatch={patchSelected} readOnly={!!disabled} locale={locale} + serverSchema={actionSchema} /> ) : ( diff --git a/packages/app-shell/src/views/studio-design/ObjectHooksPanel.tsx b/packages/app-shell/src/views/studio-design/ObjectHooksPanel.tsx index 22f4a7f83..b8c6cda6a 100644 --- a/packages/app-shell/src/views/studio-design/ObjectHooksPanel.tsx +++ b/packages/app-shell/src/views/studio-design/ObjectHooksPanel.tsx @@ -68,10 +68,17 @@ export function ObjectHooksPanel({ objectName, packageId, disabled, + hookSchema, }: { objectName: string; packageId: string; disabled?: boolean; + /** + * The live server JSONSchema for the `hook` type (`/meta/types`). Drives the + * SchemaForm so the fields, enums and grouping come from the real hook + * metadata contract rather than being synthesised from the value shape. + */ + hookSchema?: Record; }) { const locale = useMetadataLocale(); const client = useMetadataClient(); @@ -249,7 +256,7 @@ export function ObjectHooksPanel({
} onChange={(next) => { setDraft(next as HookItem); diff --git a/packages/app-shell/src/views/studio-design/ObjectValidationsPanel.test.tsx b/packages/app-shell/src/views/studio-design/ObjectValidationsPanel.test.tsx index 1d81607e2..637cab4df 100644 --- a/packages/app-shell/src/views/studio-design/ObjectValidationsPanel.test.tsx +++ b/packages/app-shell/src/views/studio-design/ObjectValidationsPanel.test.tsx @@ -14,6 +14,7 @@ const draft = { fields: { name: { type: 'text', label: '名称' }, amount: { type: 'number', label: '金额' }, + status: { type: 'select', label: '状态' }, }, validations: [ { type: 'script', name: 'no_negative', message: '金额不能为负', condition: 'record.amount < 0', severity: 'error' }, @@ -29,17 +30,20 @@ describe('ObjectValidationsPanel', () => { expect(screen.getByDisplayValue('金额不能为负')).toBeTruthy(); }); - it('lists every rule with its type badge (non-script rules stay visible)', () => { + it('lists every rule with its type badge', () => { render( {}} />); expect(screen.getByText('no_negative')).toBeTruthy(); expect(screen.getByText('status_flow')).toBeTruthy(); expect(screen.getByText('state_machine')).toBeTruthy(); }); - it('adds a script rule with a VALID never-failing default condition', () => { + it('adds a script rule with a VALID never-failing default condition from the New menu', () => { const onPatch = vi.fn(); render(); fireEvent.click(screen.getByText('New')); + // Target the menu