Files
windmill/frontend/src/lib/components/graph/noteManager.svelte.ts
T
GuilhemandRuben Fiszel 24f9115dc0 feat(frontend): add notes to flow (#6628)
* Add note component

* save note size and position

* move add note button up

* nit

* Add markdown support

* wip

* fix add sticky note button

* fix text update

* Add sticky note to saved flow data

* add note color picker

* Introduce node multiselect

* Add group notes

* Adapt layout to group node

* create a note manager class

* clean reactivity

* clean

* improve adaptive layout to group note

* modify layout based on cached text height

* fined grained graph rendering for notes

* separate noteManager into editor and render

* separate noteManager into editor and render

* create a note change observer

* render note node from context

* simplify note state managment

* show note in flow viewer

* clean dirty changes

* clean selection manager

* fix layout check

* improve bg surface select

* Handle z-index for stacked group notes

* clean selection manager

* exclude notes from rect select

* Allow switch between selection modes with keyboard keys

* improve selection box styling

* prevent dragging note when editing

* nit

* Simplify selection using svelte flow built in feature

* handle note selection separately

* Add min size for notes

* improve selection toggle

* improve mode switch

* make size and position optional for group notes

* Improve initial viewport position

* Add context menu for the canevas

* nit

* Add node context menu

* improve note select

* use clickoutside for note deselect

* use pointerdown outside to close context menu

* nit

* fix selection issues

* make edges non selectable

* improve color palette

* fix backend

* fix backend check

* cargo lock restore

* Add toggle to display notes

* fix note selection

* nit

* account for css offset in for loop

* fix multiple selection pannel styling

* clear flow selection when creating note

* Improve placeholder and note default text

* Escape note edit mode when pressing Esc

* Allow note edition in local dev

* clean

* Handle subflow selection

* prevent group note resizing

* nit

* allow notes in flow expand

* Improve multi select panel

* Allow context menu in note mode

* Add event listenner to fix pane click deselect

* prevent zoom in text area in notes

* improve bounding box styling

* Use control for box selection for non mac users

* nit

* clean notes groups

* nit

* use portal for note actions

* handle assets node when computing note layout

* Simplify layout compute for notes

* use smart color choice for notes

* Switch display note when adding a new note

* clean code

* improve group note bound size calculation

* simplify AI tool nodes and asset handling

* nit

* nit

* improve flow centering

* create group note button

* Improve selection of nodes

* Revert "Improve selection of nodes"

This reverts commit d2c40d82b1.

* refert backend changes

* nit

* improve graph selection

* clean

* make backend work except job runs

* fix notSelectable

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-11-19 21:25:57 +00:00

151 lines
4.2 KiB
TypeScript

import type { FlowNote } from '$lib/gen'
import type { Node } from '@xyflow/svelte'
import { getLayoutSignature, getPropertySignature } from './noteUtils.svelte'
import { deepEqual } from 'fast-equals'
import { untrack } from 'svelte'
/**
* Utility class for managing flow note text height caching, selection, and fine-grained reactivity
* Handles both fast visual updates and structural changes
*/
export class NoteManager {
renderCount = $state(0)
// Track notes for layout change detection
#notes: () => FlowNote[]
#previousLayoutSignature: ReturnType<typeof getLayoutSignature> = $state({
notesCount: 0,
noteIds: [],
groupMemberships: []
})
#previousPropertySignature: ReturnType<typeof getPropertySignature> = $state([])
// Function to update nodes array with reactivity
#setNodes: (nodes: Node[]) => void
#getNodes: () => Node[]
// Selection state
#selectedNoteId = $state<string | undefined>(undefined)
constructor(notes: () => FlowNote[], setNodes: (nodes: Node[]) => void, getNodes: () => Node[]) {
this.#notes = notes
this.#setNodes = setNodes
this.#getNodes = getNodes
// Effect to monitor note changes with dual signature tracking
$effect(() => {
const currentNotes = this.#notes()
const currentLayoutSignature = getLayoutSignature(currentNotes)
const currentPropertySignature = getPropertySignature(currentNotes)
untrack(() => {
const hasLayoutChanges = !deepEqual(currentLayoutSignature, this.#previousLayoutSignature)
const hasPropertyChanges = !deepEqual(
currentPropertySignature,
this.#previousPropertySignature
)
if (hasLayoutChanges) {
// Structural changes require full re-render
this.#previousLayoutSignature = currentLayoutSignature
this.#previousPropertySignature = currentPropertySignature
this.render()
} else if (hasPropertyChanges) {
// Property changes can be handled with fast updates
this.#updateNodesProperties(currentNotes)
this.#previousPropertySignature = currentPropertySignature
}
})
})
}
/**
* Triggers a re-render of the graph by incrementing the render count
*/
render(): void {
this.renderCount++
}
/**
* Update node properties using setter function for proper reactivity
* Only updates visual properties that don't affect layout
*/
#updateNodesProperties(currentNotes: FlowNote[]): void {
const currentNodes = this.#getNodes()
if (currentNodes.length === 0) return
// Create a new array with updated nodes to trigger reactivity
const updatedNodes = currentNodes.map((node) => {
const note = currentNotes.find((n) => n.id === node.id)
if (!note || node.type !== 'note') return node
// Clone the node to avoid mutation
const updatedNode = { ...node, data: { ...node.data } }
// Update properties that don't affect layout
if (updatedNode.data) {
updatedNode.data.text = note.text
updatedNode.data.color = note.color
updatedNode.data.locked = note.locked || false
}
// Update draggable property based on lock state
const isGroupNote = note.type === 'group'
updatedNode.draggable = isGroupNote ? false : !note.locked
// Update free note size and position (group notes are calculated differently)
if (!isGroupNote && note.size && note.position) {
updatedNode.width = note.size.width
updatedNode.height = note.size.height
updatedNode.position = { ...note.position }
}
return updatedNode
})
// Use setter function to trigger reactivity
this.#setNodes(updatedNodes)
}
/**
* Select a note by ID (single selection only)
*/
selectNote(noteId: string): void {
if (this.#selectedNoteId === noteId) {
return
}
this.#selectedNoteId = noteId
}
/**
* Clear note selection
*/
clearNoteSelection(): void {
this.#selectedNoteId = undefined
}
/**
* Deselect a note by ID (single selection only)
*/
deselectNote(noteId?: string): void {
if (this.#selectedNoteId === noteId) {
this.#selectedNoteId = undefined
}
}
/**
* Check if a note is currently selected
*/
isNoteSelected(noteId: string): boolean {
return this.#selectedNoteId === noteId
}
// Handle keyboard shortcuts
handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
// Escape key clears selection regardless of mode
this.clearNoteSelection()
}
}
}