From 5f819cd344ea602c4c06f93ff3da11a685f283c2 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 12 Aug 2026 08:41:30 +0200 Subject: [PATCH] fix(frontend): skip reserved ids when auto-assigning flow module ids (#10651) * fix(frontend): skip reserved ids when auto-assigning flow module ids * test: state the reserved-id invariant only beside the implementation --- .../components/flows/flowModuleNextId.test.ts | 5 +++++ .../src/lib/components/flows/flowModuleNextId.ts | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/components/flows/flowModuleNextId.test.ts b/frontend/src/lib/components/flows/flowModuleNextId.test.ts index dcf007e925..e8e89bfc48 100644 --- a/frontend/src/lib/components/flows/flowModuleNextId.test.ts +++ b/frontend/src/lib/components/flows/flowModuleNextId.test.ts @@ -50,4 +50,9 @@ describe('nextId', () => { const state = stateWith([...ids, 'process', 'my_step', 'failure']) expect(nextId(state, flowWith(ids))).toBe('c') }) + + // "as", the successor of "ar", is the first reserved id the sequence reaches. + it('skips a reserved id instead of assigning it', () => { + expect(nextId(stateWith(['failure']), flowWith(['ar']))).toBe('at') + }) }) diff --git a/frontend/src/lib/components/flows/flowModuleNextId.ts b/frontend/src/lib/components/flows/flowModuleNextId.ts index 48b2eb5ac2..461f0677d9 100644 --- a/frontend/src/lib/components/flows/flowModuleNextId.ts +++ b/frontend/src/lib/components/flows/flowModuleNextId.ts @@ -25,13 +25,23 @@ function autoIdNumber(key: string): number | undefined { // Computes the next available id export function nextId(flowState: FlowState, fullFlow: OpenFlow): string { - const allIds = dfs(fullFlow.value.modules, (fm) => fm.id) + const allKeys = dfs(fullFlow.value.modules, (fm) => fm.id).concat(Object.keys(flowState)) + const takenIds = new Set(allKeys) - const max = allIds.concat(Object.keys(flowState)).reduce((acc, key) => { + const max = allKeys.reduce((acc, key) => { const num = autoIdNumber(key) return num === undefined ? acc : Math.max(acc, num + 1) }, 0) - return numberToChars(max) + + // A reserved id (e.g. "as", after "ar") never raises `max`, so assigning + // numberToChars(max) when it is reserved would make every later call return + // it again; skip forward to a free, allowed id instead. + let candidate = max + let id = numberToChars(candidate) + while (reservedIds.has(id) || takenIds.has(id)) { + id = numberToChars(++candidate) + } + return id } // Computes a copy id like "a2", "a3", etc. based on the original id