diff --git a/frontend/src/lib/components/graph/FlowGraphV2.svelte b/frontend/src/lib/components/graph/FlowGraphV2.svelte index daafdb6452..69840f4a03 100644 --- a/frontend/src/lib/components/graph/FlowGraphV2.svelte +++ b/frontend/src/lib/components/graph/FlowGraphV2.svelte @@ -220,8 +220,8 @@ multiSelectEnabled = false }: Props = $props() - // Initialize note manager (now stateless) - const noteManager = new NoteManager() + // Initialize note manager with notes function + const noteManager = new NoteManager(() => notes ?? []) // Runtime text height tracking for notes (not stored in FlowNote) let noteTextHeights = $state>({}) diff --git a/frontend/src/lib/components/graph/noteManager.svelte.ts b/frontend/src/lib/components/graph/noteManager.svelte.ts index 0041ece766..6b63c771c7 100644 --- a/frontend/src/lib/components/graph/noteManager.svelte.ts +++ b/frontend/src/lib/components/graph/noteManager.svelte.ts @@ -20,7 +20,29 @@ export class NoteManager { #cache: Record = $state({}) renderCount = $state(0) - constructor() {} + // Track notes for layout change detection + #notes: () => FlowNote[] + #previousStructuralState: { count: number; groupMemberships: Record } = $state({ + count: 0, + groupMemberships: {} + }) + + constructor(notes: () => FlowNote[]) { + this.#notes = notes + + // Effect to monitor structural changes in notes + $effect(() => { + const currentNotes = this.#notes() + const currentState = this.#extractStructuralState(currentNotes) + const hasStructuralChanges = this.#hasStructuralChanges(currentState, this.#previousStructuralState) + + if (hasStructuralChanges) { + this.#previousStructuralState = currentState + this.render() + } + }) + } + /** * Triggers a re-render of the graph by incrementing the render count @@ -29,6 +51,72 @@ export class NoteManager { this.renderCount++ } + /** + * Extract structural state from notes for change detection + */ + #extractStructuralState(notes: FlowNote[]): { count: number; groupMemberships: Record } { + const groupMemberships: Record = {} + + // Extract group memberships for group notes + notes + .filter((note) => note.type === 'group') + .forEach((note) => { + if (note.contained_node_ids) { + groupMemberships[note.id] = [...note.contained_node_ids].sort() // Sort for consistent comparison + } + }) + + return { + count: notes.length, + groupMemberships + } + } + + /** + * Check if there are structural changes that affect layout + */ + #hasStructuralChanges( + current: { count: number; groupMemberships: Record }, + previous: { count: number; groupMemberships: Record } + ): boolean { + // Check if note count changed + if (current.count !== previous.count) { + return true + } + + // Check if group memberships changed + const currentGroups = Object.keys(current.groupMemberships) + const previousGroups = Object.keys(previous.groupMemberships) + + // Different number of group notes + if (currentGroups.length !== previousGroups.length) { + return true + } + + // Check each group's membership + for (const groupId of currentGroups) { + const currentMembers = current.groupMemberships[groupId] + const previousMembers = previous.groupMemberships[groupId] + + // Group didn't exist before or membership changed + if (!previousMembers || !this.#arraysEqual(currentMembers, previousMembers)) { + return true + } + } + + return false + } + + /** + * Helper to compare two sorted arrays for equality + */ + #arraysEqual(a: string[], b: string[]): boolean { + if (a.length !== b.length) { + return false + } + return a.every((value, index) => value === b[index]) + } + getCache(): Record { return this.#cache }