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 { describe, expect, it } from "vitest";
|
||||||
import type { WorkflowDefinition, WorkflowIrNodeKind } from "@fusion/core";
|
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 type { Node as FlowNode } from "@xyflow/react";
|
||||||
import {
|
import {
|
||||||
irToFlow,
|
irToFlow,
|
||||||
@@ -352,6 +352,23 @@ describe("workflow-flow-mapping validation helpers", () => {
|
|||||||
expect(v?.columnId).toBeNull();
|
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)", () => {
|
it("reports unplaced step nodes (not start/end, not bands)", () => {
|
||||||
const columns = columnsOf(
|
const columns = columnsOf(
|
||||||
v2Def({
|
v2Def({
|
||||||
|
|||||||
@@ -790,6 +790,21 @@ function mergedFlags(
|
|||||||
return { flags, capacityTraitIds, unknown };
|
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. */
|
/** Client mirror of core's validateColumnTraits, driven by the trait catalog. */
|
||||||
export function validateColumnsClient(
|
export function validateColumnsClient(
|
||||||
columns: WorkflowIrColumn[],
|
columns: WorkflowIrColumn[],
|
||||||
@@ -815,7 +830,7 @@ export function validateColumnsClient(
|
|||||||
code: "complete-with-wip",
|
code: "complete-with-wip",
|
||||||
severity: "error",
|
severity: "error",
|
||||||
columnId: col.id,
|
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`,
|
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",
|
code: "complete-with-intake",
|
||||||
severity: "error",
|
severity: "error",
|
||||||
columnId: col.id,
|
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`,
|
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",
|
code: "archived-with-wip",
|
||||||
severity: "error",
|
severity: "error",
|
||||||
columnId: col.id,
|
columnId: col.id,
|
||||||
traitIds: [],
|
traitIds: traitIdsWithFlags(col.traits, byId, ["archived", "countsTowardWip"]),
|
||||||
message: `Column '${col.name || col.id}' is archived but counts toward WIP`,
|
message: `Column '${col.name || col.id}' is archived but counts toward WIP`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user