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
This commit is contained in:
5
.changeset/FN-6519-custom-workflow-trait-validator.md
Normal file
5
.changeset/FN-6519-custom-workflow-trait-validator.md
Normal file
@@ -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.
|
||||
@@ -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({
|
||||
|
||||
@@ -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<string, TraitCatalogEntry>,
|
||||
names: Array<keyof CatalogFlags>,
|
||||
): 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`,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user