5.9 KiB
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/Fieldsetpair but a distinct field name — the modern field is a lazy loader,FieldsetLoader(() => Promise<{ default: ComponentType }>), sitting beside the legacy lowercasefieldsetdata. The switch is now field-name-based, not case-based. Everything else below (relocation to client-v2,import type { ISchema }legality, theuseVariablescore adapter and its coverage) still holds; readFieldsetbelow as "the modern config UI extension point, now spelledFieldsetLoader".
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
Instructionclass to the modern client; legacy canvas reaches it viav1 → v2import. The case-sensitivefieldset(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 carriesfieldset?: Record<string, ISchema>as a type-onlyimport type { ISchema }— erased at build time, zero runtime, no Formily in the modern runtime. (Precedent:@nocobase/client-v2'sCollectionFieldInterface.tsandVariableFilterItem.tsxalready doimport 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 likeuseAvailableUpstreams) move to the modern client. The legacy Formily rendering (Node,NodeDefaultView, theSchemaComponentconfig drawer innodes/index.tsx) stays insrc/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 areFieldset?: React.ComponentType<…>anduseVariablesreturningMetaTreeNode(not the legacyVariableOption). - Downstream pro plugins must repoint their
extends Instructionimport to the modern base class. A node migrates by gaining aFieldset; itsfieldsetmay 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/forfrom '@formily/'and requires zero matches, which would flag the legalimport type { ISchema }. The skill should be amended to allowimport typefrom@formily/*(type-only, zero runtime), matching what@nocobase/client-v2core 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.