From c8788d85c5bae971611df5646cd25d419e84dc31 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 16 Jun 2026 22:59:08 -0700 Subject: [PATCH] FN-6519: align workflow trait validation details Align client workflow column validation with server trait conflict reporting. - Add a helper that maps conflicting composed traits back to their catalog trait IDs. - Populate client validation traitIds for complete/intake/archive/WIP conflicts. - Cover client/server traitId parity for save-blocking workflow validation errors. - Add a patch changeset for the published Fusion package. Files changed: .../FN-6519-custom-workflow-trait-validator.md | 5 +++++ .../__tests__/workflow-flow-mapping.test.ts | 19 ++++++++++++++++++- .../app/components/workflow-flow-mapping.ts | 21 ++++++++++++++++++--- 3 files changed, 41 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-6519 Fusion-Task-Lineage: c6f3197f-5474-482b-a63b-9d20d3af3aa9 --- ...FN-6519-custom-workflow-trait-validator.md | 5 +++++ .../__tests__/workflow-flow-mapping.test.ts | 19 ++++++++++++++++- .../app/components/workflow-flow-mapping.ts | 21 ++++++++++++++++--- 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 .changeset/FN-6519-custom-workflow-trait-validator.md diff --git a/.changeset/FN-6519-custom-workflow-trait-validator.md b/.changeset/FN-6519-custom-workflow-trait-validator.md new file mode 100644 index 0000000000..bc1ef98171 --- /dev/null +++ b/.changeset/FN-6519-custom-workflow-trait-validator.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Align the workflow editor's client-side column trait validation details with the server validator so conflicting trait compositions identify the same source traits before save. diff --git a/packages/dashboard/app/components/__tests__/workflow-flow-mapping.test.ts b/packages/dashboard/app/components/__tests__/workflow-flow-mapping.test.ts index 77501e8cf0..9fad390614 100644 --- a/packages/dashboard/app/components/__tests__/workflow-flow-mapping.test.ts +++ b/packages/dashboard/app/components/__tests__/workflow-flow-mapping.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import type { WorkflowDefinition, WorkflowIrNodeKind } from "@fusion/core"; -import { parseWorkflowIr } from "@fusion/core"; +import { parseWorkflowIr, validateColumnTraits } from "@fusion/core"; import type { Node as FlowNode } from "@xyflow/react"; import { irToFlow, @@ -352,6 +352,23 @@ describe("workflow-flow-mapping validation helpers", () => { expect(v?.columnId).toBeNull(); }); + it("mirrors server trait ids for save-blocking composition conflicts", () => { + const columns = [ + { id: "complete-wip", name: "Complete WIP", traits: [{ trait: "complete" }, { trait: "wip" }] }, + { id: "two-wip", name: "Two WIP", traits: [{ trait: "wip" }, { trait: "wip" }] }, + { id: "done", name: "Done", traits: [{ trait: "complete" }, { trait: "intake" }] }, + { id: "archive", name: "Archive", traits: [{ trait: "archived" }, { trait: "wip" }] }, + ]; + const clientViolations = validateColumnsClient(columns, CATALOG); + const serverViolations = validateColumnTraits(columns); + + for (const code of ["complete-with-wip", "two-capacity-traits", "complete-with-intake", "archived-with-wip"] as const) { + expect(clientViolations.find((v) => v.code === code)?.traitIds.sort()).toEqual( + serverViolations.find((v) => v.code === code)?.traitIds.sort(), + ); + } + }); + it("reports unplaced step nodes (not start/end, not bands)", () => { const columns = columnsOf( v2Def({ diff --git a/packages/dashboard/app/components/workflow-flow-mapping.ts b/packages/dashboard/app/components/workflow-flow-mapping.ts index 06c6c929ad..b57e7ef650 100644 --- a/packages/dashboard/app/components/workflow-flow-mapping.ts +++ b/packages/dashboard/app/components/workflow-flow-mapping.ts @@ -790,6 +790,21 @@ function mergedFlags( return { flags, capacityTraitIds, unknown }; } +/* +FNXC:CustomWorkflows 2026-06-16-22:30: +The workflow editor's client-side trait validator mirrors the server validator, including traitIds used to identify the exact composed traits behind blocking save errors. +*/ +function traitIdsWithFlags( + traits: WorkflowIrColumn["traits"], + catalog: Map, + names: Array, +): string[] { + return traits + .map((ct) => catalog.get(ct.trait)) + .filter((def): def is TraitCatalogEntry => !!def && names.some((name) => !!def.flags[name])) + .map((def) => def.id); +} + /** Client mirror of core's validateColumnTraits, driven by the trait catalog. */ export function validateColumnsClient( columns: WorkflowIrColumn[], @@ -815,7 +830,7 @@ export function validateColumnsClient( code: "complete-with-wip", severity: "error", columnId: col.id, - traitIds: capacityTraitIds, + traitIds: traitIdsWithFlags(col.traits, byId, ["complete", "countsTowardWip"]), message: `Column '${col.name || col.id}' is both a completion column and counts toward WIP`, }); } @@ -833,7 +848,7 @@ export function validateColumnsClient( code: "complete-with-intake", severity: "error", columnId: col.id, - traitIds: [], + traitIds: traitIdsWithFlags(col.traits, byId, ["complete", "intake"]), message: `Column '${col.name || col.id}' is both a completion column and an intake column`, }); } @@ -842,7 +857,7 @@ export function validateColumnsClient( code: "archived-with-wip", severity: "error", columnId: col.id, - traitIds: [], + traitIds: traitIdsWithFlags(col.traits, byId, ["archived", "countsTowardWip"]), message: `Column '${col.name || col.id}' is archived but counts toward WIP`, }); }