mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 08:07:03 +00:00
create a note manager class
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { FlowService, type FlowModule, type FlowNote, type Job } from '../../gen'
|
||||
import { NODE, type GraphModuleState } from '.'
|
||||
import { DEFAULT_NOTE_COLOR, type NoteColor } from './noteColors'
|
||||
import { DEFAULT_NOTE_COLOR } from './noteColors'
|
||||
import { getContext, onDestroy, tick, untrack, type Snippet } from 'svelte'
|
||||
|
||||
import { get, writable, type Writable } from 'svelte/store'
|
||||
@@ -65,12 +65,7 @@
|
||||
import NodeContextMenu from './NodeContextMenu.svelte'
|
||||
import { SelectionManager } from './selectionUtils.svelte'
|
||||
import { ChangeTracker } from '$lib/svelte5Utils.svelte'
|
||||
import {
|
||||
createGroupNote,
|
||||
isGroupNote,
|
||||
calculateGroupNoteBounds,
|
||||
convertToExtendedNote
|
||||
} from './groupNoteUtils'
|
||||
import { NoteManager } from './noteManager.svelte'
|
||||
import type { ModulesTestStates } from '../modulesTest.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import type { AssetWithAltAccessType } from '../assets/lib'
|
||||
@@ -225,6 +220,16 @@
|
||||
multiSelectEnabled = false
|
||||
}: Props = $props()
|
||||
|
||||
// Initialize note manager
|
||||
const noteManager = new NoteManager(notes)
|
||||
noteManager.setOnNotesChangeCallback(onNotesChange || (() => {}))
|
||||
noteManager.setUpdateStoresCallback(updateStores)
|
||||
|
||||
// Update note manager when notes prop changes
|
||||
$effect(() => {
|
||||
noteManager.setNotes(notes)
|
||||
})
|
||||
|
||||
// Selection manager - create one if not provided
|
||||
let actualSelectionManager = selectionManager || new SelectionManager()
|
||||
|
||||
@@ -340,7 +345,7 @@
|
||||
// First, collect all nodes that need spacing above them
|
||||
const spacingMap = new Map<string, number>()
|
||||
for (const node of initialNodes) {
|
||||
const groupNoteHeight = getGroupNoteHeightForNode(node.id, initialNodes)
|
||||
const groupNoteHeight = noteManager.getGroupNoteHeightForNode(node.id, initialNodes)
|
||||
if (groupNoteHeight > 0) {
|
||||
spacingMap.set(node.id, groupNoteHeight)
|
||||
}
|
||||
@@ -478,169 +483,23 @@
|
||||
size: newNoteFromTool.size || { width: 200, height: 100 },
|
||||
color: DEFAULT_NOTE_COLOR
|
||||
}
|
||||
onNotesChange([...notes, newNote])
|
||||
noteManager.addNote(newNote)
|
||||
nextNoteId += 1
|
||||
}
|
||||
exitNoteMode?.()
|
||||
updateStores()
|
||||
}
|
||||
|
||||
function updateNoteText(noteId: string, text: string) {
|
||||
if (onNotesChange) {
|
||||
onNotesChange(notes.map((note) => (note.id === noteId ? { ...note, text } : note)))
|
||||
}
|
||||
updateStores()
|
||||
}
|
||||
|
||||
function deleteNote(noteId: string) {
|
||||
if (onNotesChange) {
|
||||
onNotesChange(notes.filter((note) => note.id !== noteId))
|
||||
}
|
||||
updateStores()
|
||||
}
|
||||
|
||||
function updateNotePosition(noteId: string, position: { x: number; y: number }) {
|
||||
if (onNotesChange) {
|
||||
onNotesChange(notes.map((note) => (note.id === noteId ? { ...note, position } : note)))
|
||||
}
|
||||
}
|
||||
|
||||
function updateNoteSize(noteId: string, size: { width: number; height: number }) {
|
||||
if (onNotesChange) {
|
||||
onNotesChange(notes.map((note) => (note.id === noteId ? { ...note, size } : note)))
|
||||
}
|
||||
}
|
||||
|
||||
function updateNoteColor(noteId: string, color: NoteColor) {
|
||||
if (onNotesChange) {
|
||||
onNotesChange(notes.map((note) => (note.id === noteId ? { ...note, color } : note)))
|
||||
}
|
||||
updateStores()
|
||||
}
|
||||
|
||||
function updateNoteLock(noteId: string, locked: boolean) {
|
||||
if (onNotesChange) {
|
||||
onNotesChange(notes.map((note) => (note.id === noteId ? { ...note, locked } : note)))
|
||||
}
|
||||
updateStores()
|
||||
}
|
||||
|
||||
function handleCreateGroupNote(selectedNodeIds: string[]) {
|
||||
if (selectedNodeIds.length === 0 || !onNotesChange) return
|
||||
|
||||
try {
|
||||
const groupNote = createGroupNote(selectedNodeIds)
|
||||
// For now, we need to store group notes as FlowNote format with additional properties
|
||||
// We'll add dummy position/size that will be calculated dynamically in convertNotesToNodes
|
||||
const lockedGroupNote = {
|
||||
...groupNote,
|
||||
position: { x: 0, y: 0 }, // Dummy values, will be calculated dynamically
|
||||
size: { width: 300, height: 100 }, // Dummy values, will be calculated dynamically
|
||||
locked: true,
|
||||
isGroupNote: true,
|
||||
containedNodeIds: groupNote.containedNodeIds,
|
||||
type: 'group'
|
||||
} as FlowNote & {
|
||||
locked: boolean;
|
||||
isGroupNote: boolean;
|
||||
containedNodeIds: string[];
|
||||
type: string;
|
||||
}
|
||||
onNotesChange([...notes, lockedGroupNote])
|
||||
nextNoteId += 1
|
||||
} catch (error) {
|
||||
console.error('Failed to create group note:', error)
|
||||
}
|
||||
noteManager.createGroupNote(selectedNodeIds)
|
||||
updateStores()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper function to determine if a node needs additional spacing above it for group notes.
|
||||
* Returns the height needed above the node.
|
||||
*/
|
||||
function getGroupNoteHeightForNode(nodeId: string, layoutedNodes: (NodeDep & NodePos)[]): number {
|
||||
for (const note of notes) {
|
||||
const extendedNote = convertToExtendedNote(note as any)
|
||||
if (isGroupNote(extendedNote) && extendedNote.containedNodeIds.includes(nodeId)) {
|
||||
// Find the topmost node in this group by Y position
|
||||
const containedNodes = layoutedNodes.filter(node =>
|
||||
extendedNote.containedNodeIds.includes(node.id)
|
||||
)
|
||||
|
||||
if (containedNodes.length > 0) {
|
||||
const topmostNode = containedNodes.reduce((topMost, node) =>
|
||||
node.position.y < topMost.position.y ? node : topMost
|
||||
)
|
||||
|
||||
// If this is the topmost node in the group, return the needed height
|
||||
if (topmostNode.id === nodeId) {
|
||||
return 60 // Height for group note text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function convertNotesToNodes(currentNodes: Node[]): Node[] {
|
||||
return notes.map((note) => {
|
||||
const extendedNote = convertToExtendedNote(note as any)
|
||||
|
||||
if (isGroupNote(extendedNote)) {
|
||||
// Calculate dynamic bounds for group notes
|
||||
const bounds = calculateGroupNoteBounds(extendedNote, currentNodes, 60)
|
||||
|
||||
return {
|
||||
id: extendedNote.id,
|
||||
type: 'note',
|
||||
position: bounds.position,
|
||||
data: {
|
||||
text: extendedNote.text,
|
||||
color: extendedNote.color,
|
||||
locked: (note as any).locked || true, // Group notes are locked by default
|
||||
isGroupNote: true,
|
||||
containedNodeIds: extendedNote.containedNodeIds,
|
||||
onUpdate: (text: string) => updateNoteText(extendedNote.id, text),
|
||||
onDelete: () => deleteNote(extendedNote.id),
|
||||
onColorChange: (color: NoteColor) => updateNoteColor(extendedNote.id, color),
|
||||
onSizeChange: (size: { width: number; height: number }) => updateNoteSize(extendedNote.id, size),
|
||||
onLockToggle: (locked: boolean) => updateNoteLock(extendedNote.id, locked)
|
||||
},
|
||||
style: `width: ${bounds.size.width}px; height: ${bounds.size.height}px;`,
|
||||
width: bounds.size.width,
|
||||
height: bounds.size.height,
|
||||
zIndex: -2000,
|
||||
draggable: !(note as any).locked, // Don't allow dragging locked notes
|
||||
selectable: true
|
||||
}
|
||||
} else {
|
||||
// Handle regular notes
|
||||
return {
|
||||
id: extendedNote.id,
|
||||
type: 'note',
|
||||
position: extendedNote.position,
|
||||
data: {
|
||||
text: extendedNote.text,
|
||||
color: extendedNote.color,
|
||||
locked: (note as any).locked || false,
|
||||
isGroupNote: false,
|
||||
onUpdate: (text: string) => updateNoteText(extendedNote.id, text),
|
||||
onDelete: () => deleteNote(extendedNote.id),
|
||||
onColorChange: (color: NoteColor) => updateNoteColor(extendedNote.id, color),
|
||||
onSizeChange: (size: { width: number; height: number }) => updateNoteSize(extendedNote.id, size),
|
||||
onLockToggle: (locked: boolean) => updateNoteLock(extendedNote.id, locked)
|
||||
},
|
||||
style: `width: ${extendedNote.size.width}px; height: ${extendedNote.size.height}px;`,
|
||||
width: extendedNote.size.width,
|
||||
height: extendedNote.size.height,
|
||||
zIndex: -2000,
|
||||
draggable: !(note as any).locked, // Don't allow dragging locked notes
|
||||
selectable: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function updateStores() {
|
||||
if (graph.error) {
|
||||
@@ -681,7 +540,7 @@
|
||||
|
||||
nodes = [
|
||||
...finalNodes,
|
||||
...convertNotesToNodes(finalNodes)
|
||||
...noteManager.convertToNodes(finalNodes)
|
||||
]
|
||||
edges = [
|
||||
...(assetNodesResult?.newAssetEdges ?? []),
|
||||
@@ -903,7 +762,7 @@
|
||||
onnodedragstop={(event) => {
|
||||
const node = event.targetNode
|
||||
if (node && node.type === 'note') {
|
||||
updateNotePosition(node.id, node.position)
|
||||
noteManager.updatePosition(node.id, node.position)
|
||||
}
|
||||
}}
|
||||
onmove={(event, viewport) => {
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
import type { FlowNote } from '$lib/gen'
|
||||
import type { Node } from '@xyflow/svelte'
|
||||
import type { NoteColor } from './noteColors'
|
||||
import {
|
||||
createGroupNote,
|
||||
isGroupNote,
|
||||
calculateGroupNoteBounds,
|
||||
convertToExtendedNote
|
||||
} from './groupNoteUtils'
|
||||
|
||||
type NodeDep = { id: string; parentIds?: string[]; offset?: number }
|
||||
type NodePos = { position: { x: number; y: number } }
|
||||
|
||||
/**
|
||||
* Utility class for managing flow notes including regular and group notes
|
||||
*/
|
||||
export class NoteManager {
|
||||
private notes = $state<FlowNote[]>([])
|
||||
private onNotesChangeCallback?: (notes: FlowNote[]) => void
|
||||
private updateStoresCallback?: () => void
|
||||
|
||||
constructor(initialNotes: FlowNote[] = []) {
|
||||
this.notes = initialNotes
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback function to be called when notes change
|
||||
*/
|
||||
setOnNotesChangeCallback(callback: (notes: FlowNote[]) => void) {
|
||||
this.onNotesChangeCallback = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback function to be called when stores need updating
|
||||
*/
|
||||
setUpdateStoresCallback(callback: () => void) {
|
||||
this.updateStoresCallback = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current notes array
|
||||
*/
|
||||
getNotes(): FlowNote[] {
|
||||
return this.notes
|
||||
}
|
||||
|
||||
/**
|
||||
* Update notes array
|
||||
*/
|
||||
setNotes(newNotes: FlowNote[]) {
|
||||
this.notes = newNotes
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new note from the note tool
|
||||
*/
|
||||
addNote(newNoteFromTool: any) {
|
||||
if (!this.onNotesChangeCallback) return
|
||||
|
||||
// Add the note to our separate notes array if a note was created
|
||||
if (newNoteFromTool) {
|
||||
const newNote: FlowNote = {
|
||||
id: newNoteFromTool.id,
|
||||
text: newNoteFromTool.data?.text || '',
|
||||
position: newNoteFromTool.position,
|
||||
size: { width: newNoteFromTool.width || 300, height: newNoteFromTool.height || 100 },
|
||||
color: newNoteFromTool.data?.color || 'yellow'
|
||||
}
|
||||
this.onNotesChangeCallback([...this.notes, newNote])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update note text
|
||||
*/
|
||||
updateText(noteId: string, text: string) {
|
||||
if (!this.onNotesChangeCallback) return
|
||||
this.onNotesChangeCallback(
|
||||
this.notes.map((note) => (note.id === noteId ? { ...note, text } : note))
|
||||
)
|
||||
this.updateStoresCallback?.()
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a note
|
||||
*/
|
||||
delete(noteId: string) {
|
||||
if (!this.onNotesChangeCallback) return
|
||||
this.onNotesChangeCallback(this.notes.filter((note) => note.id !== noteId))
|
||||
this.updateStoresCallback?.()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update note position
|
||||
*/
|
||||
updatePosition(noteId: string, position: { x: number; y: number }) {
|
||||
if (!this.onNotesChangeCallback) return
|
||||
this.onNotesChangeCallback(
|
||||
this.notes.map((note) => (note.id === noteId ? { ...note, position } : note))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update note size
|
||||
*/
|
||||
updateSize(noteId: string, size: { width: number; height: number }) {
|
||||
if (!this.onNotesChangeCallback) return
|
||||
this.onNotesChangeCallback(
|
||||
this.notes.map((note) => (note.id === noteId ? { ...note, size } : note))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update note color
|
||||
*/
|
||||
updateColor(noteId: string, color: NoteColor) {
|
||||
if (!this.onNotesChangeCallback) return
|
||||
this.onNotesChangeCallback(
|
||||
this.notes.map((note) => (note.id === noteId ? { ...note, color } : note))
|
||||
)
|
||||
this.updateStoresCallback?.()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update note lock state
|
||||
*/
|
||||
updateLock(noteId: string, locked: boolean) {
|
||||
if (!this.onNotesChangeCallback) return
|
||||
this.onNotesChangeCallback(
|
||||
this.notes.map((note) => (note.id === noteId ? { ...note, locked } as any : note))
|
||||
)
|
||||
this.updateStoresCallback?.()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a group note from selected node IDs
|
||||
*/
|
||||
createGroupNote(selectedNodeIds: string[]) {
|
||||
if (selectedNodeIds.length === 0 || !this.onNotesChangeCallback) return
|
||||
|
||||
try {
|
||||
const groupNote = createGroupNote(selectedNodeIds)
|
||||
// For now, we need to store group notes as FlowNote format with additional properties
|
||||
// We'll add dummy position/size that will be calculated dynamically in convertNotesToNodes
|
||||
const lockedGroupNote = {
|
||||
...groupNote,
|
||||
position: { x: 0, y: 0 }, // Dummy values, will be calculated dynamically
|
||||
size: { width: 300, height: 100 }, // Dummy values, will be calculated dynamically
|
||||
locked: true,
|
||||
isGroupNote: true,
|
||||
containedNodeIds: groupNote.containedNodeIds,
|
||||
type: 'group'
|
||||
} as FlowNote & {
|
||||
locked: boolean;
|
||||
isGroupNote: boolean;
|
||||
containedNodeIds: string[];
|
||||
type: string;
|
||||
}
|
||||
this.onNotesChangeCallback([...this.notes, lockedGroupNote])
|
||||
} catch (error) {
|
||||
console.error('Failed to create group note:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to determine if a node needs additional spacing above it for group notes.
|
||||
* Returns the height needed above the node.
|
||||
*/
|
||||
getGroupNoteHeightForNode(nodeId: string, layoutedNodes: (NodeDep & NodePos)[]): number {
|
||||
for (const note of this.notes) {
|
||||
const extendedNote = convertToExtendedNote(note as any)
|
||||
if (isGroupNote(extendedNote) && extendedNote.containedNodeIds.includes(nodeId)) {
|
||||
// Find the topmost node in this group by Y position
|
||||
const containedNodes = layoutedNodes.filter(node =>
|
||||
extendedNote.containedNodeIds.includes(node.id)
|
||||
)
|
||||
|
||||
if (containedNodes.length > 0) {
|
||||
const topmostNode = containedNodes.reduce((topMost, node) =>
|
||||
node.position.y < topMost.position.y ? node : topMost
|
||||
)
|
||||
|
||||
// If this is the topmost node in the group, return the needed height
|
||||
if (topmostNode.id === nodeId) {
|
||||
return 60 // Height for group note text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert notes to SvelteFlow nodes
|
||||
*/
|
||||
convertToNodes(currentNodes: Node[]): Node[] {
|
||||
return this.notes.map((note) => {
|
||||
const extendedNote = convertToExtendedNote(note as any)
|
||||
|
||||
if (isGroupNote(extendedNote)) {
|
||||
// Calculate dynamic bounds for group notes
|
||||
const bounds = calculateGroupNoteBounds(extendedNote, currentNodes, 60)
|
||||
|
||||
return {
|
||||
id: extendedNote.id,
|
||||
type: 'note',
|
||||
position: bounds.position,
|
||||
data: {
|
||||
text: extendedNote.text,
|
||||
color: extendedNote.color,
|
||||
locked: (note as any).locked || true, // Group notes are locked by default
|
||||
isGroupNote: true,
|
||||
containedNodeIds: extendedNote.containedNodeIds,
|
||||
onUpdate: (text: string) => this.updateText(extendedNote.id, text),
|
||||
onDelete: () => this.delete(extendedNote.id),
|
||||
onColorChange: (color: NoteColor) => this.updateColor(extendedNote.id, color),
|
||||
onSizeChange: (size: { width: number; height: number }) => this.updateSize(extendedNote.id, size),
|
||||
onLockToggle: (locked: boolean) => this.updateLock(extendedNote.id, locked)
|
||||
},
|
||||
style: `width: ${bounds.size.width}px; height: ${bounds.size.height}px;`,
|
||||
width: bounds.size.width,
|
||||
height: bounds.size.height,
|
||||
zIndex: -2000,
|
||||
draggable: !(note as any).locked, // Don't allow dragging locked notes
|
||||
selectable: true
|
||||
}
|
||||
} else {
|
||||
// Handle regular notes
|
||||
return {
|
||||
id: extendedNote.id,
|
||||
type: 'note',
|
||||
position: extendedNote.position,
|
||||
data: {
|
||||
text: extendedNote.text,
|
||||
color: extendedNote.color,
|
||||
locked: (note as any).locked || false,
|
||||
isGroupNote: false,
|
||||
onUpdate: (text: string) => this.updateText(extendedNote.id, text),
|
||||
onDelete: () => this.delete(extendedNote.id),
|
||||
onColorChange: (color: NoteColor) => this.updateColor(extendedNote.id, color),
|
||||
onSizeChange: (size: { width: number; height: number }) => this.updateSize(extendedNote.id, size),
|
||||
onLockToggle: (locked: boolean) => this.updateLock(extendedNote.id, locked)
|
||||
},
|
||||
style: `width: ${extendedNote.size.width}px; height: ${extendedNote.size.height}px;`,
|
||||
width: extendedNote.size.width,
|
||||
height: extendedNote.size.height,
|
||||
zIndex: -2000,
|
||||
draggable: !(note as any).locked, // Don't allow dragging locked notes
|
||||
selectable: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user