diff --git a/packages/app-shell/src/views/metadata-admin/anchors.ts b/packages/app-shell/src/views/metadata-admin/anchors.ts index fabc480ff..db5e1ade9 100644 --- a/packages/app-shell/src/views/metadata-admin/anchors.ts +++ b/packages/app-shell/src/views/metadata-admin/anchors.ts @@ -186,7 +186,7 @@ export function registerBuiltinAnchors(): void { }, }); - // flow / workflow may reference an object at the root or under `on` + // flow may reference an object at the root or under `on` registerMetadataResource({ type: 'flow', anchors: [{ @@ -201,16 +201,8 @@ export function registerBuiltinAnchors(): void { ], createDefaults: { nodes: [], edges: [] }, }); - registerMetadataResource({ - type: 'workflow', - anchors: [{ - anchorType: 'object', - match: anchorByField(['object', 'on.object']), - groupLabel: 'Workflow Rules', - order: 51, - }], - createFields: ['name', 'objectName', 'triggerType', 'description'], - }); + // ADR-0020: `workflow` retired as a metadata type — record state machines + // are a `state_machine` validation rule on the object (no separate anchor). // trigger.object → object (low-level DB-style triggers) registerMetadataResource({ diff --git a/packages/app-shell/src/views/metadata-admin/clientValidation.ts b/packages/app-shell/src/views/metadata-admin/clientValidation.ts index 58dd9d503..79e079759 100644 --- a/packages/app-shell/src/views/metadata-admin/clientValidation.ts +++ b/packages/app-shell/src/views/metadata-admin/clientValidation.ts @@ -64,7 +64,9 @@ const LOADERS: Record = { // automation flow: async () => (await import('@objectstack/spec/automation')).FlowSchema as unknown as ZodLikeSchema, - workflow: async () => (await import('@objectstack/spec/automation')).StateMachineSchema as unknown as ZodLikeSchema, + // `workflow` is no longer a standalone metadata type (ADR-0020) — record + // state machines are a `state_machine` validation rule on the object, + // validated as part of ObjectSchema; there is no top-level workflow schema. // `approval` is no longer a standalone metadata type — it's a flow node // (`type: 'approval'`, ADR-0019). Its config (ApprovalNodeConfigSchema) is // validated as part of the enclosing flow; there is no top-level schema, so diff --git a/packages/app-shell/src/views/metadata-admin/i18n.ts b/packages/app-shell/src/views/metadata-admin/i18n.ts index 5b4f0371e..1e5ec868d 100644 --- a/packages/app-shell/src/views/metadata-admin/i18n.ts +++ b/packages/app-shell/src/views/metadata-admin/i18n.ts @@ -40,7 +40,7 @@ const TYPE_LABELS_EN: Record = { report: 'Report', // Automation flow: 'Flow', - workflow: 'Workflow', + // ADR-0020: `workflow` retired as a metadata type. approval: 'Approval Process', // System datasource: 'Datasource', @@ -86,7 +86,7 @@ const TYPE_LABELS_ZH: Record = { action: '操作', report: '报表', flow: '流程', - workflow: '工作流', + // ADR-0020: `workflow` 已不再是独立元数据类型。 approval: '审批流程', datasource: '数据源', translation: '翻译', diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/WorkflowActionInspector.tsx b/packages/app-shell/src/views/metadata-admin/inspectors/WorkflowActionInspector.tsx deleted file mode 100644 index 891074466..000000000 --- a/packages/app-shell/src/views/metadata-admin/inspectors/WorkflowActionInspector.tsx +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * WorkflowActionInspector — scoped editor for the selected workflow - * action (immediate or time-dependent). - * - * Selection shapes: - * { kind: 'action', id: 'actions[i]' } — immediate - * { kind: 'action', id: 'timeTriggers[i].actions[j]' } — scheduled - * - * Patches: rewrite the appropriate slot in the draft, then onPatch(). - */ - -import * as React from 'react'; -import type { MetadataInspectorProps } from '../inspector-registry'; -import { t } from '../i18n'; -import { - InspectorShell, - InspectorReorderButtons, - InspectorTextField, - InspectorSelectField, - InspectorRemoveButton, - InspectorEmptyState, - spliceArray, - moveArray, -} from './_shared'; - -interface WorkflowAction { type?: string; name?: string; [k: string]: unknown } -interface TimeTrigger { offset?: number; unit?: string; actions?: WorkflowAction[]; [k: string]: unknown } - -const ACTION_TYPES = [ - 'field_update', 'email_alert', 'task_create', 'outbound_message', - 'webhook', 'apex', 'flow_invoke', 'notification', -]; - -interface ParsedSel { - scope: 'immediate' | 'timed'; - i: number; - j?: number; -} - -function parseSelectionId(id: string): ParsedSel | null { - const a = /^actions\[(\d+)\]$/.exec(id); - if (a) return { scope: 'immediate', i: Number(a[1]) }; - const b = /^timeTriggers\[(\d+)\]\.actions\[(\d+)\]$/.exec(id); - if (b) return { scope: 'timed', i: Number(b[1]), j: Number(b[2]) }; - return null; -} - -export function WorkflowActionInspector({ selection, draft, onPatch, onClearSelection, onSelectionChange, locale, readOnly }: MetadataInspectorProps) { - const parsed = parseSelectionId(selection.id); - const immediate = Array.isArray((draft as any).actions) ? (draft as any).actions as WorkflowAction[] : []; - const timed = Array.isArray((draft as any).timeTriggers) ? (draft as any).timeTriggers as TimeTrigger[] : []; - - const action: WorkflowAction | null = (() => { - if (!parsed) return null; - if (parsed.scope === 'immediate') return immediate[parsed.i] ?? null; - const t = timed[parsed.i]; - if (!t || !Array.isArray(t.actions)) return null; - return t.actions[parsed.j!] ?? null; - })(); - - if (!action || !parsed) { - return ( - - - - ); - } - - const patch = (updates: Partial) => { - const next = { ...action, ...updates }; - if (parsed.scope === 'immediate') { - onPatch({ actions: spliceArray(immediate, parsed.i, next) }); - } else { - const trig = timed[parsed.i]; - const newActions = spliceArray(trig.actions ?? [], parsed.j!, next); - const newTrigs = spliceArray(timed, parsed.i, { ...trig, actions: newActions }); - onPatch({ timeTriggers: newTrigs }); - } - }; - - const remove = () => { - if (parsed.scope === 'immediate') { - onPatch({ actions: spliceArray(immediate, parsed.i, null) }); - } else { - const trig = timed[parsed.i]; - const newActions = spliceArray(trig.actions ?? [], parsed.j!, null); - onPatch({ timeTriggers: spliceArray(timed, parsed.i, { ...trig, actions: newActions }) }); - } - onClearSelection(); - }; - - const otherConfig = React.useMemo(() => { - const { type, name, ...rest } = action; - return JSON.stringify(rest, null, 2); - }, [action]); - const [configText, setConfigText] = React.useState(otherConfig); - const [configError, setConfigError] = React.useState(null); - React.useEffect(() => { setConfigText(otherConfig); setConfigError(null); }, [otherConfig]); - - const commitConfig = () => { - try { - const parsedObj = configText.trim() === '' ? {} : JSON.parse(configText); - if (!parsedObj || typeof parsedObj !== 'object' || Array.isArray(parsedObj)) throw new Error('not object'); - setConfigError(null); - patch(parsedObj as Partial); - } catch (e) { - setConfigError(String((e as Error).message)); - } - }; - - const { currentIndex, total } = (() => { - if (parsed.scope === 'immediate') return { currentIndex: parsed.i, total: immediate.length }; - const trig = timed[parsed.i]; - const arr = Array.isArray(trig?.actions) ? trig.actions : []; - return { currentIndex: parsed.j!, total: arr.length }; - })(); - - const move = (to: number) => { - if (parsed.scope === 'immediate') { - onPatch({ actions: moveArray(immediate, parsed.i, to) }); - onSelectionChange?.({ kind: 'action', id: `actions[${to}]`, label: action.name || action.type }); - } else { - const trig = timed[parsed.i]; - const newActions = moveArray(trig.actions ?? [], parsed.j!, to); - onPatch({ timeTriggers: spliceArray(timed, parsed.i, { ...trig, actions: newActions }) }); - onSelectionChange?.({ kind: 'action', id: `timeTriggers[${parsed.i}].actions[${to}]`, label: action.name || action.type }); - } - }; - - return ( - - } - footer={} - > - ({ value: v, label: v }))} onCommit={(v) => patch({ type: v })} disabled={readOnly} /> - patch({ name: v })} disabled={readOnly} /> -
- -