Mark the node inside a container that carries the error
A container's errors are reported against the container, with the node that actually carries one in relatedNodeIds - so opening the subflow to look for it found a canvas with nothing marked, and an error naming a container the view no longer showed. Entering a subflow also cleared the marks outright, and the set was only ever computed at the root, where the errors grouped under a container are not among the flow-level ones. The marks now follow whatever the current view reports: the container at the root, the block that carries the error once inside. Focusing one error from the panel still narrows them, and dropping that focus goes back to all of them rather than to none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3af6f14bb7
commit
b4302bf7db
|
|
@ -235,6 +235,76 @@ describe('EditorStateHolder', () => {
|
|||
expect(service.structureNavigationRequest()).toBe(previousRequest + 1);
|
||||
});
|
||||
|
||||
it('marks the node inside a container that carries the error, once the subflow is open', async () => {
|
||||
// The error is reported against the container, with the node that actually carries it in
|
||||
// relatedNodeIds: at the root that marks the container, and inside the subflow it has to mark
|
||||
// the block - opening it to a canvas with nothing marked leaves nowhere to look.
|
||||
const validation: GroupedFlowValidation = {
|
||||
flowLevel: [],
|
||||
byContainer: {
|
||||
'container-1': {
|
||||
body: [{
|
||||
message: "Global input 'document' is referenced but not declared in flow.globalInputs",
|
||||
code: 'GLOBAL_INPUT_NOT_DECLARED',
|
||||
entity: 'container',
|
||||
id: 'container-1',
|
||||
field: 'specificConfiguration.subFlow',
|
||||
relatedNodeIds: ['inner-block-1']
|
||||
}],
|
||||
guard: []
|
||||
}
|
||||
}
|
||||
};
|
||||
flowsServiceSpy.getGroupedFlowValidation.mockReturnValue(of(validation));
|
||||
const flow = makeFlow({
|
||||
data: {
|
||||
blocks: [],
|
||||
containers: [{
|
||||
id: 'container-1',
|
||||
name: 'Loop',
|
||||
typeName: 'LoopContainer',
|
||||
nodeFamily: 'container',
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
specificConfiguration: {
|
||||
subFlow: { blocks: [], containers: [], connections: [], dependencies: [] }
|
||||
}
|
||||
}],
|
||||
connections: [],
|
||||
dependencies: []
|
||||
}
|
||||
});
|
||||
|
||||
await service.openDocument(flow);
|
||||
// At the root the container is what carries a subflow error, and nothing else would mark it.
|
||||
expect(service.isValidationNodeHighlighted('container-1')).toBe(true);
|
||||
|
||||
expect(service.openSubflow([{ containerId: 'container-1', configurationPath: 'subFlow' }])).toBe(true);
|
||||
expect(service.isValidationNodeHighlighted('inner-block-1')).toBe(true);
|
||||
});
|
||||
|
||||
it('focuses one error on request, and goes back to all of them when the focus is dropped', async () => {
|
||||
const validation: GroupedFlowValidation = {
|
||||
flowLevel: [
|
||||
{ message: 'First', code: 'A', relatedNodeIds: ['block-1'] },
|
||||
{ message: 'Second', code: 'B', relatedNodeIds: ['block-2'] }
|
||||
],
|
||||
byContainer: {}
|
||||
};
|
||||
flowsServiceSpy.getGroupedFlowValidation.mockReturnValue(of(validation));
|
||||
await service.openDocument(makeFlow());
|
||||
|
||||
expect(service.isValidationNodeHighlighted('block-1')).toBe(true);
|
||||
expect(service.isValidationNodeHighlighted('block-2')).toBe(true);
|
||||
|
||||
service.setHighlightedValidationNodes(['block-2']);
|
||||
expect(service.isValidationNodeHighlighted('block-1')).toBe(false);
|
||||
expect(service.isValidationNodeHighlighted('block-2')).toBe(true);
|
||||
|
||||
service.setHighlightedValidationNodes([]);
|
||||
expect(service.isValidationNodeHighlighted('block-1')).toBe(true);
|
||||
});
|
||||
|
||||
it('shows only the active subflow validation errors', async () => {
|
||||
const validation: GroupedFlowValidation = {
|
||||
flowLevel: [{ message: 'Root flow error', code: 'ROOT' }],
|
||||
|
|
|
|||
|
|
@ -48,7 +48,33 @@ export class EditorStateHolder {
|
|||
? containerErrors.guard
|
||||
: containerErrors.body;
|
||||
});
|
||||
readonly highlightedValidationNodeIds = signal<string[]>([]);
|
||||
/**
|
||||
* What the panel has focused, or null to mark whatever the current view's errors point at. The
|
||||
* default matters most inside a container: an error there is reported against the container, with
|
||||
* the node that actually carries it in relatedNodeIds, so without this the subflow opened to a
|
||||
* canvas where nothing was marked and the error named a container the view no longer showed.
|
||||
*/
|
||||
private readonly focusedValidationNodeIds = signal<string[] | null>(null);
|
||||
|
||||
readonly highlightedValidationNodeIds = computed(() => {
|
||||
const focused = this.focusedValidationNodeIds();
|
||||
if (focused) return focused;
|
||||
return uniqueNodeIds([
|
||||
...this.flowValidationErrors().flatMap((error) => error.relatedNodeIds ?? []),
|
||||
...this.containerIdsWithSubflowErrors()
|
||||
]);
|
||||
});
|
||||
|
||||
/**
|
||||
* Containers whose errors all live inside their subflow. Those are grouped away from the flow
|
||||
* level, so at the root nothing else would mark the container that holds them.
|
||||
*/
|
||||
private readonly containerIdsWithSubflowErrors = computed(() => {
|
||||
if (this.activeSubflow()) return [];
|
||||
return Object.entries(this.groupedFlowValidation().byContainer)
|
||||
.filter(([, errors]) => errors.body.length > 0 || errors.guard.length > 0)
|
||||
.map(([containerId]) => containerId);
|
||||
});
|
||||
readonly validationRequiresSave = signal(false);
|
||||
readonly activeSubflow = signal<FlowSubflowEntry | null>(null);
|
||||
readonly structureNavigationRequest = signal(0);
|
||||
|
|
@ -306,9 +332,10 @@ export class EditorStateHolder {
|
|||
)
|
||||
}
|
||||
|
||||
/** Focuses one error's nodes; an empty list goes back to marking everything the view reports. */
|
||||
setHighlightedValidationNodes(nodeIds: string[]) {
|
||||
const unique = Array.from(new Set((nodeIds ?? []).filter((id) => typeof id === 'string' && id.length > 0)));
|
||||
this.highlightedValidationNodeIds.set(unique);
|
||||
const unique = uniqueNodeIds(nodeIds ?? []);
|
||||
this.focusedValidationNodeIds.set(unique.length > 0 ? unique : null);
|
||||
}
|
||||
|
||||
isValidationNodeHighlighted(blockId: string | null | undefined): boolean {
|
||||
|
|
@ -327,10 +354,9 @@ export class EditorStateHolder {
|
|||
|
||||
private refreshValidationPresentation(flow?: Flow | null) {
|
||||
this.localFlowValidationErrors.set(flow ? this.deriveGlobalInputReferenceErrors(flow) : []);
|
||||
const errors = this.flowValidationErrors();
|
||||
this.highlightedValidationNodeIds.set(Array.from(new Set(
|
||||
errors.flatMap((error) => Array.isArray(error.relatedNodeIds) ? error.relatedNodeIds : [])
|
||||
)));
|
||||
// A fresh result is about the whole flow again, so whatever one error the panel had focused is
|
||||
// no longer what the person is looking at.
|
||||
this.focusedValidationNodeIds.set(null);
|
||||
}
|
||||
|
||||
private extractValidationErrors(error: unknown): FlowValidationError[] {
|
||||
|
|
@ -464,3 +490,9 @@ function emptyGroupedFlowValidation(): GroupedFlowValidation {
|
|||
function isGuardSubflow(configurationPath: string): boolean {
|
||||
return configurationPath.split('.').at(-1)?.toLowerCase().includes('guard') ?? false;
|
||||
}
|
||||
|
||||
function uniqueNodeIds(nodeIds: (string | null | undefined)[]): string[] {
|
||||
return Array.from(new Set(
|
||||
nodeIds.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
||||
));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue