Skip to main content

API Reference

Dry signature reference for @vef-framework-react/approval-flow-editor. For narrative usage, see Overview and Host Integration.

Component

ApprovalFlowEditor

FC<ApprovalFlowEditorProps>

PropTypeDescription
valueFlowDefinitionFlow definition (backend format). A new reference (not one the editor itself emitted) reloads the canvas. Empty/missing seeds a start→end flow.
onChange(definition: FlowDefinition) => voidCalled on every graph change with a detached deep copy.
onPublish(definition: FlowDefinition) => voidPublish hook, called with the current definition after a passing validation pass. Omitted ⇒ no publish button rendered.
publishTextstringPublish button label. Default "发布".
publishLoadingbooleanShows a spinner and blocks re-clicks on the publish button. Default false.
readonlybooleanDisables editing and hides the toolbar/publish button. Default false.
pluginsEditorPluginsHost integration points (pickers, form fields, global subjects).
classNamestringCustom class on the editor shell.
styleCSSPropertiesCustom style on the editor shell.

Plugins

ExportKindDescription
EditorPluginstype{ pickers?, formFields?, globalSubjects? } — see Host Integration.
PickerPropstype{ value: string[]; onChange: (ids: string[]) => void; disabled?: boolean }.
PrincipalKindtype"user" | "role" | "department".
PRINCIPAL_KINDSconstreadonly ["user", "role", "department"].
isPrincipalKindfn(kind: string) => kind is PrincipalKind.

Flow definition (wire format)

ExportShape
FlowDefinition{ nodes: NodeDefinition[]; edges: EdgeDefinition[] }
NodeDefinition{ id: string; kind: NodeKind; position: XYPosition; data?: NodeDataMap[kind] } (discriminated union on kind)
EdgeDefinition{ id?: string; source: string; target: string; sourceHandle?: string; data?: Record<string, unknown> }sourceHandle binds a condition node's outgoing edge to a branch id
NodeKind"start" | "approval" | "handle" | "condition" | "cc" | "end"
FlowNode, FlowEdgeLive (in-memory) canvas shapes, defined by this package: FlowNode is the union of the per-kind typed @xyflow/react nodes (below); FlowEdge is xyflow's Edge. Convert to/from FlowDefinition via toFlowDefinition / fromFlowDefinition.
StartNode, ApprovalNode, HandleNode, CcNodePer-kind typed live nodes — Node<StartNodeData, "start">, Node<ApprovalNodeData, "approval">, Node<HandleNodeData, "handle">, Node<CcNodeData, "cc">. On a live node React Flow's type carries the kind (it drives node rendering); only the wire-level NodeDefinition lifts it to kind, and the node's business fields sit directly on data — the exact NodeDataMap[kind] shape the wire carries. The end / condition aliases exist internally but are not exported.
Version note: the nodeloom era (v2.7.0 – v2.12.0)

From v2.7.0 through v2.12.0 the editor's state engine was @coldsmirk/nodeloom-core, and the live shapes were nodeloom's uniform ones: every node carried type: "flowNode", the kind discriminator lived at data.kind, and the business fields sat under data.config. FlowNode / FlowEdge / EditorState were nodeloom re-exports, and the per-kind node aliases were not exported. An unreleased change after v2.12.0 (81e3ade) drops the dependency and restores the hand-written engine this page describes — per-kind typed nodes with the business fields directly on data. The wire format (FlowDefinition / NodeDefinition / EdgeDefinition) and the toFlowDefinition / fromFlowDefinition signatures were untouched by both moves, so persisted definitions are unaffected. The migration history lives in the release notes.

Node data by kind

NodeDataMap maps each NodeKind to its data shape; AnyNodeData is the union of all six (defined as FlowNode["data"]). Every kind's data includes name?: string (display name) and description?: string. These shapes serve double duty: they are the wire NodeDefinition.data payloads and the live nodes' data — the serializer translates only the kind discriminator (kind ↔ React Flow's type), never the data.

KindTypeFields (beyond name/description)
startStartNodeData
endEndNodeData
approvalApprovalNodeDataextends TaskNodeData + approvalMethod, passRule, passRatio, sameApplicantAction, consecutiveApproverAction, rollbackType, rollbackDataStrategy, rollbackTargetKeys, isRollbackAllowed, isAddAssigneeAllowed, addAssigneeTypes, isRemoveAssigneeAllowed, isManualCcAllowed
handleHandleNodeDataalias of TaskNodeData (no approval-specific fields)
conditionConditionNodeDatabranches?: ConditionBranchDefinition[]
ccCcNodeDataccs?: CcDefinition[], isReadConfirmRequired?: boolean, fieldPermissions?: Record<string, CcFieldPermission>

TaskNodeData (shared by approval and handle):

FieldType
assignees?AssigneeDefinition[]
executionType?ExecutionType
emptyAssigneeAction?EmptyAssigneeAction
fallbackUserIds?string[]
adminUserIds?string[]
isTransferAllowed?boolean
isOpinionRequired?boolean
timeoutHours?number
timeoutAction?TimeoutAction
timeoutNotifyBeforeHours?number
urgeCooldownMinutes?number
ccs?CcDefinition[]
fieldPermissions?Record<string, FieldPermission>

Assignee, CC, and condition types

ExportShape
AssigneeDefinition{ kind: AssigneeKind; ids?: string[]; formField?: string; sortOrder: number }
AssigneeKindPrincipalKind | "self" | "superior" | "department_leader" | "form_field"
CcDefinition{ kind: CcKind; ids?: string[]; formField?: string; timing?: CcTiming }
CcKindPrincipalKind | "form_field"
CcTiming"always" | "on_approve" | "on_reject"
FieldPermission"visible" | "editable" | "hidden" | "required"
CcFieldPermission"visible" | "hidden" — CC nodes only observe the form, so editable/required are not offered
ConditionDefinition{ kind: ConditionKind; subject: string; aggregate?: "sum" | "count" | "avg"; column?: string; operator: ConditionOperator | ""; value: unknown; expression: string }
ConditionKind"field" | "expression"
ConditionOperatorDerived from CONDITION_OPERATORS — see Host Integration
ConditionGroup{ conditions: ConditionDefinition[] } — conditions AND-ed within a group
ConditionBranchDefinition{ id: string; label: string; conditionGroups?: ConditionGroup[]; isDefault?: boolean; priority: number }
FormFieldDefinition{ key: string; kind: FieldKind; label: string; options?: FieldOptionDefinition[]; columns?: FormFieldDefinition[]; hasConditionalVisibility?: boolean }
FieldKind"input" | "textarea" | "select" | "number" | "date" | "upload" | "table"
FieldOptionDefinition{ label: string; value: unknown }

ConditionDefinition.aggregate scopes an aggregate fold (sum / count / avg, mirroring the backend's AggregateKind) over a detail-table subject; column names the numeric column to fold (unset for count, which folds rows).

FormFieldDefinition.hasConditionalVisibility (v2.10.0) is a designer-side hint: true when the field's visibility can be toggled off by form linkage — its own hide-capable linkage, a hidden default, or a hide-capable ancestor container. The field-permission table shows a non-blocking warning when a node grants required on such a field, because the backend's required check is linkage-blind: if the field is hidden at approval time, its empty value would reject the approve forever. It is populated by approval-form-bridge's projectFormSchema and absent when the inventory came from elsewhere.

Enums

ExportValues
ApprovalMethod"sequential" | "parallel"
PassRule"all" | "any" | "ratio""all" implies veto power
EmptyAssigneeAction"auto_pass" | "transfer_admin" | "transfer_superior" | "transfer_applicant" | "transfer_specified"
ExecutionType"manual" | "auto_pass" | "auto_reject"
SameApplicantAction"auto_pass" | "self_approve" | "transfer_superior"
RollbackType"none" | "previous" | "start" | "any" | "specified"
RollbackDataStrategy"clear" | "keep"
ConsecutiveApproverAction"none" | "auto_pass"
TimeoutAction"none" | "auto_pass" | "auto_reject" | "notify" | "transfer_admin"
AddAssigneeType"before" | "after" | "parallel"

Validation

function validateFlowDefinition(
definition: FlowDefinition,
formFields?: FormFieldDefinition[]
): FlowValidationError[];

FlowValidationError: { code: FlowValidationCode; message: string; nodeId?: string; edgeId?: string; branchId?: string }. message is human-readable Chinese product copy matching the editor's UI language; program against code and the id fields.

formFields is tri-state (v2.10.0): undefined means the field inventory is unavailable (a host with no form integration) — only the key-existence check (field_permission_key_unknown) is skipped; an explicit [] means the form is known to have zero fields, making every fieldPermissions entry a dangling reference. See Host Integration → Validation.

FlowValidationCode is a closed set mirroring the backend's deploy-time checks:

Graph structure

CodeMeaning
no_nodesThe flow has no nodes.
empty_node_id / duplicate_node_idA node id is empty / reused.
invalid_node_kindA node's kind isn't one of NODE_KINDS.
start_node_countThe flow must have exactly one start node.
end_node_countThe flow must have at least one end node.
empty_edge_id / duplicate_edge_idAn edge id is empty / reused.
unknown_source_node / unknown_target_nodeAn edge references a node id that doesn't exist.
start_incomingThe start node has an incoming edge.
start_outgoingThe start node doesn't have exactly one outgoing edge.
end_outgoingAn end node has an outgoing edge.
end_incomingAn end node has no incoming edges.
node_outgoing_countA non-condition node doesn't have exactly one outgoing edge.
node_source_handleA non-condition node's outgoing edge carries a branch handle.
graph_cycleThe graph contains a cycle.
node_unreachableA node can't be reached from the start node.
node_cannot_reach_endA node can't reach any end node.

Condition nodes

CodeMeaning
condition_min_branchesFewer than 2 branches.
condition_empty_branch_id / condition_duplicate_branch_idA branch id is empty / reused.
condition_default_countNot exactly one default branch.
condition_missing_handleAn outgoing edge has no branch handle.
condition_unknown_handleAn outgoing edge's handle doesn't match any branch id.
condition_duplicate_handleTwo outgoing edges bind the same branch.
condition_branch_no_edgeA branch has no outgoing edge.
duplicate_branch_priorityTwo non-default branches share a priority.
condition_subject_requiredA field condition has no subject.
invalid_condition_operatorAn operator outside CONDITION_OPERATORS.
condition_expression_requiredAn expression condition's expression is blank.
invalid_condition_kindkind isn't "field" or "expression".
branch_conditions_requiredA non-default branch has no condition groups.
condition_group_emptyA condition group has zero conditions.
invalid_aggregateaggregate isn't "sum" | "count" | "avg".
aggregate_operatorAn aggregate condition uses a non-numeric-comparison operator.
aggregate_column_requiredA sum/avg aggregate has no column.
aggregate_column_forbiddenA count aggregate selected a column.
aggregate_on_expressionAn expression condition carries an aggregate.

Assignees, CC, and field permissions (approval / handle / cc nodes) — the field-permission codes landed in v2.10.0, mirroring the backend's deploy-time ValidateFieldPermissions, so a permission mistake surfaces in the designer instead of at deploy

CodeMeaning
invalid_assignee_kindAn assignee's kind outside AssigneeKind.
assignee_form_field_requiredA form_field assignee has no field selected.
invalid_cc_kind / invalid_cc_timingA CC's kind / timing outside its enum.
cc_form_field_requiredA form_field CC recipient has no field selected.
invalid_execution_typeexecutionType outside ExecutionType.
invalid_empty_assignee_actionemptyAssigneeAction outside its enum.
invalid_timeout_actiontimeoutAction outside TimeoutAction.
fallback_users_requiredemptyAssigneeAction: "transfer_specified" with no fallbackUserIds.
admin_users_requiredemptyAssigneeAction: "transfer_admin" with no adminUserIds.
invalid_field_permissionA fieldPermissions value outside FieldPermission.
field_permission_key_unknownA fieldPermissions key isn't in the supplied formFields. Skipped when formFields is omitted (undefined) — there is no inventory to check against.
cc_field_permission_not_allowedA CC node's fieldPermissions value outside visible/hidden.
field_permission_required_auto_passA task node pairs a "required" field permission with timeoutAction: "auto_pass" — the timeout path finishes the node without the manual-approve required check, so the required field could pass through empty. Runs regardless of the formFields tri-state (it inspects only the node's own data).

Approval-specific

CodeMeaning
invalid_approval_method / invalid_pass_ruleOutside their enums.
pass_ratio_rangepassRule: "ratio" with passRatio outside (0, 100].
invalid_same_applicant_action / invalid_consecutive_approver_actionOutside their enums.
invalid_rollback_type / invalid_rollback_data_strategyOutside their enums.
invalid_add_assignee_typeAn addAssigneeTypes entry outside AddAssigneeType.
rollback_targets_requiredrollbackType: "specified" with no rollbackTargetKeys.
rollback_target_unknownA rollback target isn't an existing approval/handle node.
rollback_target_selfA rollback target references the node itself.
sequential_parallel_add_assigneeapprovalMethod: "sequential" with addAssigneeTypes including "parallel".

Handle-specific

CodeMeaning
handle_execution_auto_rejectA handle node's executionType is "auto_reject" (unsupported).
handle_timeout_auto_rejectA handle node's timeoutAction is "auto_reject" (unsupported).

Serialization

ExportSignature
toFlowDefinition(nodes: FlowNode[], edges: FlowEdge[]) => FlowDefinition
fromFlowDefinition(definition: FlowDefinition) => { nodes: FlowNode[]; edges: FlowEdge[] }

Specifications

ExportSignature / Shape
NodeSpecification{ type: NodeKind; label: string; color: string; icon: ComponentType<LucideProps>; badgeVariant?: "soft" | "solid"; configPanel?: ComponentType<{ nodeId: string }> }
getSpecification(kind: NodeKind) => NodeSpecification — throws on an unknown kind
getAllSpecifications() => NodeSpecification[]
getAddableSpecifications() => NodeSpecification[] — specifications for kinds the toolbar can add (NODE_RULES[kind].addable)

Constants and structural rules

ExportShape
NODE_KINDSreadonly NodeKind[] — all six kinds
isNodeKind(kind: string) => kind is NodeKind
NODE_KIND_LABELSRecord<NodeKind, string> — display label per kind
NodeRule{ maxCount?: number; deletable: boolean; addable: boolean }
NODE_RULESRecord<NodeKind, NodeRule> — see the table in Overview
DEFAULT_NODE_DATARecord<NodeKind, Record<string, unknown>> — deep-frozen default data for a newly-added node of each kind; clone before mutating
CONDITION_OPERATORSreadonly ConditionOperator[] — the runtime allow-list ConditionOperator derives from; see Host Integration

Store hooks

For advanced state inspection — e.g. testing, or reading the live graph programmatically:

ExportSignature
useEditorStore<T>(selector: (state: EditorState) => T) => T
useEditorStoreApi() => UnboundStore<EditorState> — a zustand store api (with the subscribeWithSelector / immer middlewares), from core's createComponentStore
EditorStateThe full store state + actions — this package's own type since the hand-written engine returned (see the version note above; it was a @coldsmirk/nodeloom-core re-export from v2.7.0 to v2.12.0)

EditorState is the intersection of five slices (the slice types themselves are internal; only EditorState is exported):

SliceStateActions
Graphnodes: FlowNode[], edges: FlowEdge[]onNodesChange / onEdgesChange / onConnect (xyflow change handlers); addNode(type: NodeKind, position: XYPosition, data?: Partial<AnyNodeData>) => string | null — the new node's id, or null when refused (readonly, or the kind's max count reached); removeNode(nodeId) — refused for readonly / non-deletable kinds, prunes connected edges and rollback-target references; removeEdge(edgeId); updateNodeData(nodeId, data: Partial<AnyNodeData>); updateNodePositions(positions: Map<string, XYPosition>) — batch position commit (auto-layout); updateConditionBranch(nodeId, branchId, patch); addConditionBranch(nodeId) — inserts before the default branch; removeConditionBranch(nodeId, branchId) — cascades removal of the branch's edges
InteractionselectedNodeId: string | null, hoveredEdgeId: string | null, readonly: booleanselectNode(nodeId | null), setHoveredEdgeId(edgeId | null), setReadonly(readonly)
FlowisDirty: booleanloadDefinition(definition) — hydrates (empty definition seeds start→end) and resets selection, dirty flag, and the undo timeline; toDefinition() => FlowDefinition — a detached deep copy; reset()
ValidationvalidationIssues: FlowValidationError[], nodeIssueCounts: Record<string, number> — issue count per node id, so node chrome subscribes only to its own entrysetValidationIssues(issues)
Historypast: HistoryEntry[], future: HistoryEntry[]HistoryEntry = { nodes: FlowNode[]; edges: FlowEdge[] } (whole-graph snapshots; view state is never checkpointed)undo(), redo()

There are no canUndo / canRedo members (the nodeloom store had them): derive them as past.length > 0 / future.length > 0. A few members are internal plumbing (changeVersion, checkpoint, breakCoalescing) — visible on the type but not meant for hosts.

Both hooks resolve against the store <ApprovalFlowEditor> mounts internally — there is no host-facing slot to render additional components inside that provider tree (the provider itself is deliberately not exported), so these are primarily useful for reading state (e.g. via useEditorStoreApi().getState()) rather than for mounting custom UI inside the canvas.

Visual tokens

ExportShape
NODE_KIND_COLORSRecord<NodeKind, string> — accent color per kind (theme-adaptive CSS var references)
ApprovalIcon, CcIcon, ConditionIcon, EndIcon, HandleIcon, StartIconlucide-react icon components matching the six node kinds