feat(error-groups): sort execution error cards by node execution ID (#9334)

## Summary

Sort execution error cards within each error group by their node
execution ID in ascending numeric order, ensuring consistent and
predictable display order.

## Changes

- **What**: Added `compareExecutionId` utility to
`src/types/nodeIdentification.ts` that splits node IDs on `:` and
compares segments numerically left-to-right; applied it as a sort
comparator when building `ErrorGroup.cards` in `useErrorGroups.ts`

## Review Focus

- The comparison treats missing segments as `0`, so `"1"` sorts before
`"1:20"` (subgraph nodes follow their parent); confirm this ordering
matches user expectations
- All comparisons are purely numeric — non-numeric segment values would
sort as `NaN` (treated as `0`)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9334-feat-error-groups-sort-execution-error-cards-by-node-execution-ID-3176d73d365081e1b3e4e4fa8831fe16)
by [Unito](https://www.unito.io)
This commit is contained in:
jaeone94
2026-03-05 06:59:58 +09:00
committed by Johnpaul
parent e59bb247b8
commit c0368ca557
4 changed files with 158 additions and 2 deletions

View File

@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import {
compareExecutionId,
createNodeExecutionId,
createNodeLocatorId,
getAncestorExecutionIds,
@@ -232,4 +233,45 @@ describe('nodeIdentification', () => {
expect(getParentExecutionIds('65:70:63')).toEqual(['65', '65:70'])
})
})
describe('compareExecutionId', () => {
it('sorts simple numeric IDs in ascending order', () => {
expect(compareExecutionId('1', '2')).toBeLessThan(0)
expect(compareExecutionId('2', '1')).toBeGreaterThan(0)
expect(compareExecutionId('5', '5')).toBe(0)
})
it('compares nested IDs left-to-right by segment', () => {
// "1" < "1:20" < "2" < "10:11:12" as documented
expect(compareExecutionId('1', '1:20')).toBeLessThan(0)
expect(compareExecutionId('1:20', '2')).toBeLessThan(0)
expect(compareExecutionId('2', '10:11:12')).toBeLessThan(0)
})
it('treats a shorter ID as having trailing segment 0 when comparing', () => {
// "5" vs "5:0" → first segments equal, second: 0 vs 0 → equal
expect(compareExecutionId('5', '5:0')).toBe(0)
// "5" vs "5:1" → second segment 0 < 1
expect(compareExecutionId('5', '5:1')).toBeLessThan(0)
})
it('handles undefined inputs by treating them as empty string (segment 0)', () => {
expect(compareExecutionId(undefined, '1')).toBeLessThan(0)
expect(compareExecutionId('1', undefined)).toBeGreaterThan(0)
expect(compareExecutionId(undefined, undefined)).toBe(0)
})
it('handles empty string inputs', () => {
expect(compareExecutionId('', '1')).toBeLessThan(0)
expect(compareExecutionId('1', '')).toBeGreaterThan(0)
expect(compareExecutionId('', '')).toBe(0)
})
it('treats non-numeric segments as 0 via NaN guard', () => {
// Number('foo') → NaN → treated as 0
expect(compareExecutionId('foo', '1')).toBeLessThan(0)
expect(compareExecutionId('foo', '0')).toBe(0)
expect(compareExecutionId('1:foo', '1:0')).toBe(0)
})
})
})

View File

@@ -146,3 +146,26 @@ export function getParentExecutionIds(
): NodeExecutionId[] {
return getAncestorExecutionIds(executionId).slice(0, -1)
}
/**
* Compare two NodeExecutionIds for ascending numeric sort order.
* Splits each ID by ":" and compares segments numerically left to right.
*
* Example order: "1" < "1:20" < "2" < "10:11:12"
*/
export function compareExecutionId(
a: string | undefined,
b: string | undefined
): number {
const parse = (id: string | undefined) => (id ?? '').split(':').map(Number)
const idA = parse(a)
const idB = parse(b)
for (let i = 0; i < Math.max(idA.length, idB.length); i++) {
const segA = idA[i] ?? 0
const segB = idB[i] ?? 0
const diff =
(Number.isNaN(segA) ? 0 : segA) - (Number.isNaN(segB) ? 0 : segB)
if (diff !== 0) return diff
}
return 0
}