mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
* refactor: extract flow delete helpers Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: unify flow delete planning Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: stabilize flow delete execution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: simplify flow delete plan execution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
213 lines
6.4 KiB
TypeScript
213 lines
6.4 KiB
TypeScript
import { untrack } from 'svelte'
|
|
import type { FlowModule } from '$lib/gen'
|
|
import { type FlowGroup, type GraphGroup, groupKey } from './groupEditor.svelte'
|
|
import type { StateStore } from '$lib/utils'
|
|
import { getAllModules } from '../flows/flowExplorer'
|
|
import { computeGroupModuleIds } from './groupDetectionUtils'
|
|
import { stateSnapshot } from '$lib/svelte5Utils.svelte'
|
|
import {
|
|
buildStructureTree,
|
|
deriveGroupsFromStructure,
|
|
applyStructureToModules,
|
|
removeEmptyGroups,
|
|
findDuplicateGroups,
|
|
removeDuplicateGroups,
|
|
flattenStructureIds,
|
|
findInStructure,
|
|
type FlowStructureNode
|
|
} from './flowStructure'
|
|
|
|
export type ExtendedOpenFlow = {
|
|
value: {
|
|
modules: FlowModule[]
|
|
groups?: FlowGroup[]
|
|
[key: string]: any
|
|
}
|
|
[key: string]: any
|
|
}
|
|
|
|
export type PreparedStructureDelete = {
|
|
affectedGroups: FlowGroup[]
|
|
duplicateGroups: FlowGroup[]
|
|
commit: (commitOpts?: { removeDuplicates?: boolean }) => void
|
|
}
|
|
|
|
/**
|
|
* Reactive read-only view of the flow structure tree.
|
|
* The tree is always derived from flowStore (single source of truth).
|
|
* Mutations go through prepareMutation: snapshot → mutate → clean empty groups → commit.
|
|
*/
|
|
export class GroupedModulesProxy {
|
|
#items = $state<FlowStructureNode[]>([])
|
|
#error = $state<unknown>(undefined)
|
|
#flowStore: StateStore<ExtendedOpenFlow>
|
|
|
|
constructor(flowStore: StateStore<ExtendedOpenFlow>) {
|
|
this.#flowStore = flowStore
|
|
this.rebuild()
|
|
|
|
// Rebuild tree whenever store changes (undo/load/mutation)
|
|
$effect(() => {
|
|
void flowStore.val.value.modules
|
|
void flowStore.val.value.groups
|
|
untrack(() => this.rebuild())
|
|
})
|
|
}
|
|
|
|
/** Reactive access to the structure tree (read-only view) */
|
|
get items(): FlowStructureNode[] {
|
|
return this.#items
|
|
}
|
|
|
|
/** Reactive access to build errors */
|
|
get error(): unknown {
|
|
return this.#error
|
|
}
|
|
|
|
/**
|
|
* Prepare a structural mutation without writing to the store yet.
|
|
* Returns the list of groups that became empty (already removed from the snapshot)
|
|
* and a `commit` function that writes the result to the store.
|
|
*
|
|
* If no groups were emptied, the caller can commit immediately.
|
|
* If groups were emptied, the caller should show a confirmation modal
|
|
* and call commit() only on user confirmation.
|
|
*/
|
|
prepareMutation(
|
|
mutate: (tree: FlowStructureNode[]) => void,
|
|
opts?: {
|
|
extraModules?: FlowModule[]
|
|
displayState?: import('./groupEditor.svelte').GroupDisplayState
|
|
}
|
|
): {
|
|
emptiedGroups: FlowGroup[]
|
|
duplicateGroups: FlowGroup[]
|
|
commit: (commitOpts?: { removeDuplicates?: boolean }) => void
|
|
} {
|
|
const snapshot = $state.snapshot(this.#items) as FlowStructureNode[]
|
|
mutate(snapshot)
|
|
|
|
// Clean up empty groups and collect which ones were removed
|
|
const emptiedGroups = removeEmptyGroups(snapshot)
|
|
// Detect groups that became duplicates after the mutation
|
|
const duplicateGroups = findDuplicateGroups(snapshot)
|
|
|
|
const commit = (commitOpts?: { removeDuplicates?: boolean }) => {
|
|
if (commitOpts?.removeDuplicates && duplicateGroups.length > 0) {
|
|
removeDuplicateGroups(snapshot)
|
|
}
|
|
|
|
// Remap runtime state for groups whose boundaries shifted
|
|
if (opts?.displayState) {
|
|
this.#remapChangedGroupKeys(snapshot, opts.displayState)
|
|
}
|
|
|
|
// Build moduleMap lazily at commit time so it reflects the latest store state
|
|
const moduleMap = new Map<string, FlowModule>()
|
|
for (const m of getAllModules(this.#flowStore.val.value.modules)) {
|
|
moduleMap.set(m.id, m)
|
|
}
|
|
if (opts?.extraModules) {
|
|
for (const m of opts.extraModules) {
|
|
moduleMap.set(m.id, m)
|
|
}
|
|
}
|
|
this.#flowStore.val.value.modules = applyStructureToModules(snapshot, moduleMap)
|
|
this.#flowStore.val.value.groups = deriveGroupsFromStructure(snapshot)
|
|
}
|
|
|
|
return { emptiedGroups, duplicateGroups, commit }
|
|
}
|
|
|
|
prepareDelete(
|
|
ids: string[],
|
|
opts?: {
|
|
displayState?: import('./groupEditor.svelte').GroupDisplayState
|
|
}
|
|
): PreparedStructureDelete {
|
|
const { emptiedGroups, duplicateGroups, commit } = this.prepareMutation((tree) => {
|
|
for (const id of ids) {
|
|
const found = findInStructure(tree, id)
|
|
if (!found) {
|
|
continue
|
|
}
|
|
|
|
found.parentChildren.splice(found.index, 1)
|
|
}
|
|
}, opts)
|
|
|
|
return {
|
|
affectedGroups: [...emptiedGroups, ...duplicateGroups],
|
|
duplicateGroups,
|
|
commit
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convenience: prepare + auto-commit. Only use for mutations that cannot
|
|
* empty groups (e.g. inserts). Throws if groups are unexpectedly emptied.
|
|
* For mutations that may empty groups, use prepareMutation() directly.
|
|
*/
|
|
applyTreeMutation(
|
|
mutate: (tree: FlowStructureNode[]) => void,
|
|
opts?: {
|
|
extraModules?: FlowModule[]
|
|
displayState?: import('./groupEditor.svelte').GroupDisplayState
|
|
}
|
|
): void {
|
|
const { emptiedGroups, duplicateGroups, commit } = this.prepareMutation(mutate, opts)
|
|
if (emptiedGroups.length > 0) {
|
|
console.error('applyTreeMutation: unexpected empty groups', emptiedGroups)
|
|
}
|
|
if (duplicateGroups.length > 0) {
|
|
console.error('applyTreeMutation: unexpected duplicate groups', duplicateGroups)
|
|
}
|
|
commit()
|
|
}
|
|
|
|
/** Remap runtime state for group nodes whose boundaries shifted after a mutation. */
|
|
#remapChangedGroupKeys(
|
|
snapshot: FlowStructureNode[],
|
|
displayState: import('./groupEditor.svelte').GroupDisplayState
|
|
): void {
|
|
const walk = (nodes: FlowStructureNode[]) => {
|
|
for (const node of nodes) {
|
|
if (node.kind === 'group') {
|
|
const oldKey = node.id
|
|
const flatIds = flattenStructureIds(node.branches[0].children)
|
|
const newKey = flatIds.length > 0 ? `${flatIds[0]}:${flatIds[flatIds.length - 1]}` : null
|
|
if (newKey && oldKey !== newKey) {
|
|
displayState.remapGroupKey(oldKey, newKey)
|
|
}
|
|
walk(node.branches[0].children)
|
|
} else {
|
|
for (const branch of node.branches) {
|
|
walk(branch.children)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
walk(snapshot)
|
|
}
|
|
|
|
/** Rebuild from flowStore */
|
|
private rebuild(): void {
|
|
const modules = stateSnapshot(this.#flowStore.val.value.modules) as FlowModule[]
|
|
const allGroups = this.#flowStore.val.value.groups ?? []
|
|
const allModules = getAllModules(modules)
|
|
const graphGroups: GraphGroup[] = allGroups.map((g) => ({
|
|
...g,
|
|
id: groupKey(g),
|
|
moduleIds: computeGroupModuleIds(g.start_id, g.end_id, allModules)
|
|
}))
|
|
try {
|
|
this.#items = buildStructureTree(modules, graphGroups)
|
|
this.#error = undefined
|
|
} catch (e) {
|
|
// Intentionally preserve last-known-good #items so the graph
|
|
// can still render while the error is surfaced to the user.
|
|
this.#error = e
|
|
}
|
|
}
|
|
}
|