Back to Nocobase

Workflow Instruction definition lives in the modern client, with case-sensitive `fieldset`/`Fieldset` as the per-node migration switch

docs/adr/0002-workflow-instruction-progressive-migration.md

2.2.05.9 KB
Original Source

Workflow Instruction definition lives in the modern client, with case-sensitive fieldset/Fieldset as the per-node migration switch

Amended by ADR-0003: the per-node config-UI switch is no longer the case-sensitive fieldset/Fieldset pair but a distinct field name — the modern field is a lazy loader, FieldsetLoader (() => Promise<{ default: ComponentType }>), sitting beside the legacy lowercase fieldset data. The switch is now field-name-based, not case-based. Everything else below (relocation to client-v2, import type { ISchema } legality, the useVariables core adapter and its coverage) still holds; read Fieldset below as "the modern config UI extension point, now spelled FieldsetLoader".

The workflow node extension contract (the Instruction class) is relocated into the modern client (src/client-v2/), so node plugins extend a single definition that serves both canvases. A node's config UI is migrated incrementally by adding an uppercase Fieldset (a plain React + antd component the modern canvas renders) alongside — or eventually replacing — the legacy lowercase fieldset (a Formily schema the legacy canvas renders through SchemaComponent). The modern canvas prefers Fieldset; the legacy canvas keeps using fieldset. This lets the ~10 core nodes and 6+ pro-plugin nodes migrate one node at a time rather than in a single cutover.

Considered Options

  • (A, chosen) Relocate the Instruction class to the modern client; legacy canvas reaches it via v1 → v2 import. The case-sensitive fieldset (legacy Formily) / Fieldset (modern React) pair on one shared definition is the per-node migration switch. Legal because the repo's import rule is one-way: v1 may import v2, never the reverse. The base class carries fieldset?: Record<string, ISchema> as a type-only import type { ISchema } — erased at build time, zero runtime, no Formily in the modern runtime. (Precedent: @nocobase/client-v2's CollectionFieldInterface.ts and VariableFilterItem.tsx already do import type { ISchema }.)
  • (B) Two independent instruction registries (v1 and v2); downstream double-registers via a v1-imports-v2 shim. Rejected: two sources of truth long-term, and every downstream node needs a bridge file — more ceremony than (A) while delivering the same progressive migration.
  • (C) Fully independent v2 registration; legacy untouched, no shared definition. Rejected: cleanest re-architecture but abandons the "share one definition, migrate one field at a time" goal — every node would be re-registered from scratch for v2.

Consequences

  • Only the data/type parts of Instruction (the class + pure hooks like useAvailableUpstreams) move to the modern client. The legacy Formily rendering (Node, NodeDefaultView, the SchemaComponent config drawer in nodes/index.tsx) stays in src/client/ — moving it would drag Formily runtime into v2 and break the rule.
  • The base class keeps legacy-only data fields (fieldset, view, scope, components) as pass-through data the modern canvas does not interpret; only the legacy canvas consumes them. New modern fields are Fieldset?: React.ComponentType<…> and useVariables returning MetaTreeNode (not the legacy VariableOption).
  • Downstream pro plugins must repoint their extends Instruction import to the modern base class. A node migrates by gaining a Fieldset; its fieldset may stay until the legacy canvas is retired for that node.
  • Doc/code conflict to resolve: the migration skill's verify step greps src/client-v2/ for from '@formily/' and requires zero matches, which would flag the legal import type { ISchema }. The skill should be amended to allow import type from @formily/* (type-only, zero runtime), matching what @nocobase/client-v2 core already does.

Output variables: a core adapter, not per-node rewrites

During migration, a node's useVariables (which returns the legacy VariableOption tree) is left untouched; the modern canvas converts its aggregated upstream variables to MetaTreeNode via a single core adapter (VariableOption → MetaTreeNode). A node author migrates by adding a Fieldset only — they never touch useVariables. This deliberately borrows the mature legacy field-tree logic (getCollectionFieldOptions: relation lazy-loading, type filtering, foreign-key handling — ~250 lines, the bug-prone heart of the variable system) rather than rewriting it concurrently with the dual-canvas migration. Rewriting that logic into a native modern field-tree builder + per-node useVariablesV2 is deferred to a separate cleanup once the legacy canvas retires and the dual-canvas complexity is gone.

Coverage is provable, not assumed. The modern variable consumers (FlowContextSelector cascader, VariableHybridInput.walk, VariableTag) read exactly 7 MetaTreeNode fields: title (←label), name (←value), children (←children/loadChildren() => Promise), disabled (←disabled), disabledReason (nullable), type/interface (only for custom render, derivable from field), and paths. Of these, only paths has no VariableOption counterpart and must be constructed by the adapter — it accumulates the parent path down the recursion (and through the loadChildren closure for lazy children). Everything else is a direct map or nullable. The v1-only keys (field/types/appends/depth) are captured in the adapter's loadChildren closure and never surface on the produced MetaTreeNode.

The adapter ships with tests pinning: basic field mapping, paths accumulation across nesting + lazy loadChildren, the "v1-only keys never leak onto MetaTreeNode" assertion, and a formatPathToValue/parseValueToPath round-trip. The adapter is a pure, context-free function so the whole suite is deletable in one move when the legacy logic is finally rewritten.