Files
windmill/frontend/src/lib/components/flows/flowModuleNextId.ts
T
Ruben Fiszel 4dbf873723 fix(frontend): stop flow step id generation from being poisoned by non-canonical keys (#9766)
* fix(frontend): stop flow step id generation from being poisoned by non-canonical keys

nextId computed the next step id from the max of charsToNumber over every
module id and flowState key. Only canonical auto-ids (a, b, ... aa, ab) have a
meaningful charsToNumber value, but flowState also holds copy ids ("z2"),
subflow result keys ("subflow:..."), reserved keys ("failure"/"preprocessor")
and user-renamed ids. The old `length >= 4` guard filtered long junk but let
short junk through, so e.g. duplicating step "z" (key "z2", charsToNumber 629)
made the next new step jump to "xg" and escalate from there.

nextId now only counts a key if it round-trips through numberToChars and is not
reserved, and the broken length cap is removed so large flows still get correct
ids.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): keep length cap in nextId to avoid regressing long renames

Address CI review: removing the length cap made all-lowercase renamed step
ids (e.g. "process", which round-trips through numberToChars) feed into the
max and poison id generation again — a regression versus the prior behavior,
since step ids can be renamed to ^[a-zA-Z][a-zA-Z0-9_]*$.

Restore the length>=4 skip and pair it with the round-trip canonical check,
so short non-canonical keys (copy ids "z2"/"c10", reserved/renamed short ids)
no longer poison the max while long renames stay out of the sequence. Update
the tests to reflect the actual coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:40:07 +00:00

48 lines
1.8 KiB
TypeScript

import type { OpenFlow } from '$lib/gen'
import { dfs } from './dfs'
import type { FlowState } from './flowState'
import { charsToNumber, forbiddenIds, numberToChars } from './idUtils'
const reservedIds = new Set(forbiddenIds)
// Returns the base-26 value of a key only if it is a short, auto-generated step id
// (a, b, ..., z, aa, ...). flowState/module-id keys also include copy ids ("a2"), subflow
// result keys ("subflow:..."), reserved keys and user-renamed ids; feeding those through
// charsToNumber yields meaningless (often huge) numbers that would poison id generation and
// make new steps jump to ids like "bzw". Short non-canonical keys are rejected via a
// round-trip check; longer keys are skipped entirely, which also leaves user renames to long
// lowercase words (e.g. "process") out of the sequence.
function autoIdNumber(key: string): number | undefined {
if (key.length >= 4 || reservedIds.has(key)) {
return undefined
}
const num = charsToNumber(key)
if (num < 0 || numberToChars(num) !== key) {
return undefined
}
return num
}
// Computes the next available id
export function nextId(flowState: FlowState, fullFlow: OpenFlow): string {
const allIds = dfs(fullFlow.value.modules, (fm) => fm.id)
const max = allIds.concat(Object.keys(flowState)).reduce((acc, key) => {
const num = autoIdNumber(key)
return num === undefined ? acc : Math.max(acc, num + 1)
}, 0)
return numberToChars(max)
}
// Computes a copy id like "a2", "a3", etc. based on the original id
export function copyId(originalId: string, flowState: FlowState, fullFlow: OpenFlow): string {
const allIds = new Set(dfs(fullFlow.value.modules, (fm) => fm.id).concat(Object.keys(flowState)))
for (let n = 2; n < 10000; n++) {
const candidate = `${originalId}${n}`
if (!allIds.has(candidate)) {
return candidate
}
}
return `${originalId}10000`
}