mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 16:05:58 +00:00
separate noteManager into editor and render
This commit is contained in:
@@ -44,6 +44,8 @@
|
||||
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
|
||||
import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types'
|
||||
import { SelectionManager } from './graph/selectionUtils.svelte'
|
||||
import { NoteEditor } from './graph/noteEditor.svelte'
|
||||
import { setNoteEditorContext } from './graph/noteEditor.svelte'
|
||||
import { cleanInputs } from './flows/utils'
|
||||
import {
|
||||
Calendar,
|
||||
@@ -636,6 +638,10 @@
|
||||
outputPickerOpenFns
|
||||
})
|
||||
|
||||
// Set up NoteEditor context for note editing capabilities
|
||||
const noteEditor = new NoteEditor(flowStore)
|
||||
setNoteEditorContext(noteEditor)
|
||||
|
||||
setContext(
|
||||
'FlowGraphAssetContext',
|
||||
initFlowGraphAssetsCtx({ getModules: () => flowStore.val.value.modules })
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
import { ModulesTestStates } from '$lib/components/modulesTest.svelte'
|
||||
import type { StateStore } from '$lib/utils'
|
||||
import { type AgentTool, flowModuleToAgentTool, createMcpTool } from '../agentToolUtils'
|
||||
import { getNoteEditorContext } from '$lib/components/graph/noteEditor.svelte'
|
||||
|
||||
interface Props {
|
||||
sidebarSize?: number | undefined
|
||||
@@ -111,6 +112,9 @@
|
||||
|
||||
const { flowPropPickerConfig } = getContext<PropPickerContext>('PropPickerContext')
|
||||
|
||||
// Get NoteEditor context for note position updates
|
||||
const noteEditorContext = getNoteEditorContext()
|
||||
|
||||
export async function insertNewModuleAtIndex(
|
||||
modules: FlowModule[] | AgentTool[],
|
||||
index: number,
|
||||
@@ -426,7 +430,7 @@
|
||||
maxHeight={minHeight}
|
||||
modules={flowStore.val.value.modules}
|
||||
{noteMode}
|
||||
bind:notes={flowStore.val.value.notes}
|
||||
notes={flowStore.val.value.notes}
|
||||
preprocessorModule={flowStore.val.value?.preprocessor_module}
|
||||
{selectionManager}
|
||||
{workspace}
|
||||
@@ -655,6 +659,12 @@
|
||||
{onOpenPreview}
|
||||
{onHideJobStatus}
|
||||
exitNoteMode={() => (noteMode = false)}
|
||||
onNotePositionUpdate={(noteId, position) => {
|
||||
// Update note position via NoteEditor context in edit mode
|
||||
if (noteEditorContext?.noteEditor) {
|
||||
noteEditorContext.noteEditor.updatePosition(noteId, position)
|
||||
}
|
||||
}}
|
||||
multiSelectEnabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -37,9 +37,6 @@ export type ExtendedOpenFlow = OpenFlow & {
|
||||
dedicated_worker?: boolean
|
||||
visible_to_runner_only?: boolean
|
||||
on_behalf_of_email?: string
|
||||
ui?: {
|
||||
notes?: FlowNote[]
|
||||
}
|
||||
}
|
||||
|
||||
export type FlowInputEditorState = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { FlowService, type FlowModule, type FlowNote, type Job } from '../../gen'
|
||||
import { NODE, type GraphModuleState } from '.'
|
||||
import { DEFAULT_NOTE_COLOR } from './noteColors'
|
||||
import { getContext, onDestroy, tick, untrack, type Snippet } from 'svelte'
|
||||
|
||||
import { get, writable, type Writable } from 'svelte/store'
|
||||
@@ -152,6 +151,7 @@
|
||||
onShowModuleDiff?: (moduleId: string) => void
|
||||
flowHasChanged?: boolean
|
||||
exitNoteMode?: () => void
|
||||
onNotePositionUpdate?: (noteId: string, position: { x: number; y: number }) => void
|
||||
// Viewport synchronization props (for diff viewer)
|
||||
sharedViewport?: Viewport
|
||||
onViewportChange?: (viewport: Viewport, isUserInitiated: boolean) => void
|
||||
@@ -210,8 +210,9 @@
|
||||
suspendStatus = {},
|
||||
flowHasChanged = false,
|
||||
noteMode = false,
|
||||
notes = $bindable(),
|
||||
notes = undefined,
|
||||
exitNoteMode = undefined,
|
||||
onNotePositionUpdate = undefined,
|
||||
chatInputEnabled = false,
|
||||
sharedViewport = undefined,
|
||||
onViewportChange = undefined,
|
||||
@@ -426,7 +427,6 @@
|
||||
let height = $state(0)
|
||||
|
||||
// Note feature state
|
||||
let nextNoteId = $state(1)
|
||||
|
||||
function isSimplifiable(modules: FlowModule[] | undefined): boolean {
|
||||
if (!modules || modules?.length !== 2) {
|
||||
@@ -449,26 +449,6 @@
|
||||
// Keep for potential future use
|
||||
}
|
||||
|
||||
function onNoteAdded(newNoteFromTool: any) {
|
||||
// Add the note to our separate notes array if a note was created
|
||||
if (newNoteFromTool) {
|
||||
const newNote = {
|
||||
id: `note-${nextNoteId}`,
|
||||
text: '',
|
||||
position: newNoteFromTool.position,
|
||||
size: newNoteFromTool.size || { width: 200, height: 100 },
|
||||
color: DEFAULT_NOTE_COLOR
|
||||
}
|
||||
notes = noteManager.addNote(notes ?? [], newNote)
|
||||
nextNoteId += 1
|
||||
}
|
||||
exitNoteMode?.()
|
||||
}
|
||||
|
||||
function handleCreateGroupNote(selectedNodeIds: string[]) {
|
||||
notes = noteManager.createGroupNote(notes ?? [], selectedNodeIds)
|
||||
}
|
||||
|
||||
async function updateStores() {
|
||||
if (graph.error) {
|
||||
return
|
||||
@@ -514,10 +494,7 @@
|
||||
notes ?? [],
|
||||
finalNodes,
|
||||
noteTextHeights,
|
||||
(newNotes) => {
|
||||
notes = newNotes
|
||||
},
|
||||
(noteId, height) => {
|
||||
(noteId: string, height: number) => {
|
||||
noteTextHeights[noteId] = height
|
||||
}
|
||||
)
|
||||
@@ -740,7 +717,7 @@
|
||||
onnodedragstop={(event) => {
|
||||
const node = event.targetNode
|
||||
if (node && node.type === 'note') {
|
||||
notes = noteManager.updatePosition(notes ?? [], node.id, node.position)
|
||||
onNotePositionUpdate?.(node.id, node.position)
|
||||
}
|
||||
}}
|
||||
onmove={(event, viewport) => {
|
||||
@@ -766,15 +743,13 @@
|
||||
<div class="absolute inset-0 !bg-surface-secondary h-full" id="flow-graph-v2"></div>
|
||||
|
||||
{#if noteMode}
|
||||
<NoteTool {onNoteAdded} />
|
||||
<NoteTool {exitNoteMode} />
|
||||
{/if}
|
||||
|
||||
<NodeContextMenu
|
||||
selectedNodeIds={actualSelectionManager.selectedIds.filter(
|
||||
(id) =>
|
||||
!id.startsWith('Settings') && !id.startsWith('Trigger') && !id.startsWith('Result')
|
||||
)}
|
||||
onCreateGroupNote={handleCreateGroupNote}
|
||||
>
|
||||
<SelectionBoundingBox
|
||||
selectedNodes={nodes.filter((node) =>
|
||||
|
||||
@@ -2,30 +2,35 @@
|
||||
import ContextMenu, { type ContextMenuItem } from '../common/contextmenu/ContextMenu.svelte'
|
||||
import { StickyNote } from 'lucide-svelte'
|
||||
import type { Snippet } from 'svelte'
|
||||
import { getNoteEditorContext } from './noteEditor.svelte'
|
||||
|
||||
interface Props {
|
||||
children: Snippet
|
||||
selectedNodeIds: string[]
|
||||
onCreateGroupNote?: (selectedNodeIds: string[]) => void
|
||||
}
|
||||
|
||||
let { children, selectedNodeIds, onCreateGroupNote }: Props = $props()
|
||||
let { children, selectedNodeIds }: Props = $props()
|
||||
|
||||
// Get NoteEditor context for group note creation
|
||||
const noteEditorContext = getNoteEditorContext()
|
||||
|
||||
const menuItems: ContextMenuItem[] = $derived([
|
||||
{
|
||||
id: 'create-group-note',
|
||||
label: `Create group note (${selectedNodeIds.length} nodes)`,
|
||||
icon: StickyNote,
|
||||
disabled: selectedNodeIds.length === 0,
|
||||
disabled: selectedNodeIds.length === 0 || !noteEditorContext?.noteEditor,
|
||||
onClick: () => {
|
||||
if (selectedNodeIds.length > 0) {
|
||||
onCreateGroupNote?.(selectedNodeIds)
|
||||
if (selectedNodeIds.length > 0 && noteEditorContext?.noteEditor) {
|
||||
noteEditorContext.noteEditor.createGroupNote(selectedNodeIds, 'Group Note')
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<ContextMenu items={menuItems}>
|
||||
{@render children()}
|
||||
</ContextMenu>
|
||||
{#if noteEditorContext?.noteEditor}
|
||||
<ContextMenu items={menuItems}>
|
||||
{@render children()}
|
||||
</ContextMenu>
|
||||
{/if}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { useSvelteFlow, type XYPosition } from '@xyflow/svelte'
|
||||
import { getNoteEditorContext } from './noteEditor.svelte'
|
||||
import { DEFAULT_NOTE_COLOR } from './noteColors'
|
||||
|
||||
interface Props {
|
||||
onNoteAdded?: (note: any) => void
|
||||
exitNoteMode?: () => void
|
||||
}
|
||||
|
||||
let { onNoteAdded }: Props = $props()
|
||||
let { exitNoteMode }: Props = $props()
|
||||
|
||||
// Get NoteEditor context for direct note creation
|
||||
const noteEditorContext = getNoteEditorContext()
|
||||
|
||||
const { screenToFlowPosition, getViewport } = useSvelteFlow()
|
||||
|
||||
@@ -64,11 +69,20 @@
|
||||
height: Math.abs(absoluteEndPosition.y - absoluteStartPosition.y) / zoom
|
||||
}
|
||||
|
||||
// Create the actual note with the calculated size and position
|
||||
onNoteAdded?.({
|
||||
position,
|
||||
size
|
||||
})
|
||||
// Create the actual note using NoteEditor context
|
||||
if (noteEditorContext?.noteEditor) {
|
||||
noteEditorContext.noteEditor.addNote({
|
||||
text: '',
|
||||
position,
|
||||
size,
|
||||
color: DEFAULT_NOTE_COLOR,
|
||||
type: 'free',
|
||||
locked: false
|
||||
})
|
||||
}
|
||||
|
||||
// Exit note mode after creating note
|
||||
exitNoteMode?.()
|
||||
|
||||
// Reset state
|
||||
isDrawing = false
|
||||
@@ -107,7 +121,7 @@
|
||||
startPosition = null
|
||||
} else {
|
||||
// Exit note mode
|
||||
onNoteAdded?.(null)
|
||||
exitNoteMode?.()
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import type { FlowNote } from '$lib/gen'
|
||||
import type { StateStore } from '$lib/utils'
|
||||
import type { ExtendedOpenFlow } from '../flows/types'
|
||||
import type { NoteColor } from './noteColors'
|
||||
import { generateId } from './util'
|
||||
import { getContext, setContext } from 'svelte'
|
||||
|
||||
/**
|
||||
* Utility class for editing flow notes via direct flowStore mutations
|
||||
* This class is designed to be used in editor contexts via Svelte context
|
||||
*/
|
||||
export class NoteEditor {
|
||||
private flowStore: StateStore<ExtendedOpenFlow>
|
||||
|
||||
constructor(flowStore: StateStore<ExtendedOpenFlow>) {
|
||||
this.flowStore = flowStore
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current notes array from the flow store
|
||||
*/
|
||||
private getNotes(): FlowNote[] {
|
||||
return this.flowStore.val.value?.notes || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the notes array in the flow store
|
||||
*/
|
||||
private setNotes(notes: FlowNote[]): void {
|
||||
if (this.flowStore.val.value) {
|
||||
this.flowStore.val.value.notes = notes
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new note to the flow
|
||||
*/
|
||||
addNote(note: Omit<FlowNote, 'id'>): string {
|
||||
const notes = this.getNotes()
|
||||
const newNote: FlowNote = {
|
||||
id: generateId(),
|
||||
...note
|
||||
}
|
||||
this.setNotes([...notes, newNote])
|
||||
return newNote.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the text content of a note
|
||||
*/
|
||||
updateText(noteId: string, text: string): void {
|
||||
const notes = this.getNotes()
|
||||
const updatedNotes = notes.map((note) => (note.id === noteId ? { ...note, text } : note))
|
||||
this.setNotes(updatedNotes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the color of a note
|
||||
*/
|
||||
updateColor(noteId: string, color: NoteColor): void {
|
||||
const notes = this.getNotes()
|
||||
const updatedNotes = notes.map((note) => (note.id === noteId ? { ...note, color } : note))
|
||||
this.setNotes(updatedNotes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the position of a note
|
||||
*/
|
||||
updatePosition(noteId: string, position: { x: number; y: number }): void {
|
||||
const notes = this.getNotes()
|
||||
const updatedNotes = notes.map((note) => (note.id === noteId ? { ...note, position } : note))
|
||||
this.setNotes(updatedNotes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the size of a note
|
||||
*/
|
||||
updateSize(noteId: string, size: { width: number; height: number }): void {
|
||||
const notes = this.getNotes()
|
||||
const updatedNotes = notes.map((note) => (note.id === noteId ? { ...note, size } : note))
|
||||
this.setNotes(updatedNotes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the locked state of a note
|
||||
*/
|
||||
updateLock(noteId: string, locked: boolean): void {
|
||||
const notes = this.getNotes()
|
||||
const updatedNotes = notes.map((note) => (note.id === noteId ? { ...note, locked } : note))
|
||||
this.setNotes(updatedNotes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a note from the flow
|
||||
*/
|
||||
deleteNote(noteId: string): void {
|
||||
const notes = this.getNotes()
|
||||
const updatedNotes = notes.filter((note) => note.id !== noteId)
|
||||
this.setNotes(updatedNotes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a group note containing the specified node IDs
|
||||
*/
|
||||
createGroupNote(nodeIds: string[], text: string = 'Group'): string {
|
||||
// Calculate rough position based on contained nodes (can be refined by layout)
|
||||
const defaultPosition = { x: 0, y: 0 }
|
||||
const defaultSize = { width: 200, height: 100 }
|
||||
|
||||
const groupNote: Omit<FlowNote, 'id'> = {
|
||||
text,
|
||||
color: 'blue', // Default color, can be made configurable
|
||||
position: defaultPosition,
|
||||
size: defaultSize,
|
||||
type: 'group',
|
||||
contained_node_ids: nodeIds,
|
||||
locked: false
|
||||
}
|
||||
|
||||
return this.addNote(groupNote)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if editing is available (flowStore is properly initialized)
|
||||
*/
|
||||
isAvailable(): boolean {
|
||||
return !!this.flowStore.val.value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Context type for NoteEditor
|
||||
*/
|
||||
export type NoteEditorContext = {
|
||||
noteEditor: NoteEditor
|
||||
}
|
||||
|
||||
const CONTEXT_KEY = 'NoteEditorContext'
|
||||
|
||||
/**
|
||||
* Set the NoteEditor context (used in FlowBuilder)
|
||||
*/
|
||||
export function setNoteEditorContext(noteEditor: NoteEditor): void {
|
||||
setContext<NoteEditorContext>(CONTEXT_KEY, { noteEditor })
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the NoteEditor context (used in components that need editing capabilities)
|
||||
*/
|
||||
export function getNoteEditorContext(): NoteEditorContext | undefined {
|
||||
return getContext<NoteEditorContext | undefined>(CONTEXT_KEY)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { FlowNote } from '$lib/gen'
|
||||
import type { Node } from '@xyflow/svelte'
|
||||
import type { NoteColor } from './noteColors'
|
||||
import { calculateNodesBounds } from './util'
|
||||
|
||||
export type NodePosition = {
|
||||
@@ -34,95 +33,6 @@ export class NoteManager {
|
||||
return this.#cache
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new note from the note tool
|
||||
*/
|
||||
addNote(notes: FlowNote[], newNoteFromTool: any): FlowNote[] {
|
||||
// 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',
|
||||
type: 'free',
|
||||
locked: false
|
||||
}
|
||||
return [...notes, newNote]
|
||||
}
|
||||
return notes
|
||||
}
|
||||
|
||||
/**
|
||||
* Update note text
|
||||
*/
|
||||
updateText(notes: FlowNote[], noteId: string, text: string): FlowNote[] {
|
||||
return notes.map((note) => (note.id === noteId ? { ...note, text } : note))
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a note
|
||||
*/
|
||||
delete(notes: FlowNote[], noteId: string): FlowNote[] {
|
||||
return notes.filter((note) => note.id !== noteId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update note position
|
||||
*/
|
||||
updatePosition(
|
||||
notes: FlowNote[],
|
||||
noteId: string,
|
||||
position: { x: number; y: number }
|
||||
): FlowNote[] {
|
||||
return notes.map((note) => (note.id === noteId ? { ...note, position } : note))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update note size
|
||||
*/
|
||||
updateSize(
|
||||
notes: FlowNote[],
|
||||
noteId: string,
|
||||
size: { width: number; height: number }
|
||||
): FlowNote[] {
|
||||
return notes.map((note) => (note.id === noteId ? { ...note, size } : note))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update note color
|
||||
*/
|
||||
updateColor(notes: FlowNote[], noteId: string, color: NoteColor): FlowNote[] {
|
||||
return notes.map((note) => (note.id === noteId ? { ...note, color } : note))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update note lock state
|
||||
*/
|
||||
updateLock(notes: FlowNote[], noteId: string, locked: boolean): FlowNote[] {
|
||||
return notes.map((note) => (note.id === noteId ? { ...note, locked } : note))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a group note from selected node IDs
|
||||
*/
|
||||
createGroupNote(notes: FlowNote[], selectedNodeIds: string[]): FlowNote[] {
|
||||
if (selectedNodeIds.length === 0) return notes
|
||||
|
||||
const newGroupNote: FlowNote = {
|
||||
id: `group-${Date.now()}`,
|
||||
text: '',
|
||||
position: { x: 0, y: 0 }, // Will be calculated dynamically
|
||||
size: { width: 300, height: 100 }, // Will be calculated dynamically
|
||||
color: 'gray',
|
||||
type: 'group',
|
||||
locked: false,
|
||||
contained_node_ids: selectedNodeIds
|
||||
}
|
||||
return [...notes, newGroupNote]
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate position and size for group notes based on contained nodes
|
||||
*/
|
||||
@@ -159,46 +69,21 @@ export class NoteManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create common data object for note nodes
|
||||
* Create common data object for note nodes (rendering-only version)
|
||||
*/
|
||||
private createNoteData(
|
||||
note: FlowNote,
|
||||
notes: FlowNote[],
|
||||
onNotesChange: (notes: FlowNote[]) => void,
|
||||
onTextHeightChange: (noteId: string, height: number) => void,
|
||||
isGroupNote: boolean
|
||||
) {
|
||||
return {
|
||||
noteId: note.id,
|
||||
text: note.text,
|
||||
color: note.color,
|
||||
locked: note.locked || false,
|
||||
isGroupNote,
|
||||
...(isGroupNote && { containedNodeIds: note.contained_node_ids || [] }),
|
||||
onUpdate: (text: string) => {
|
||||
const newNotes = this.updateText(notes, note.id, text)
|
||||
onNotesChange(newNotes)
|
||||
this.render()
|
||||
},
|
||||
onDelete: () => {
|
||||
const newNotes = this.delete(notes, note.id)
|
||||
onNotesChange(newNotes)
|
||||
this.render()
|
||||
},
|
||||
onColorChange: (color: NoteColor) => {
|
||||
const newNotes = this.updateColor(notes, note.id, color)
|
||||
onNotesChange(newNotes)
|
||||
this.render()
|
||||
},
|
||||
onSizeChange: (size: { width: number; height: number }) => {
|
||||
const newNotes = this.updateSize(notes, note.id, size)
|
||||
onNotesChange(newNotes)
|
||||
this.render()
|
||||
},
|
||||
onLockToggle: (locked: boolean) => {
|
||||
const newNotes = this.updateLock(notes, note.id, locked)
|
||||
onNotesChange(newNotes)
|
||||
this.render()
|
||||
},
|
||||
// Note: Edit callbacks will be added by NoteNode when NoteEditor context is available
|
||||
onTextHeightChange: (textHeight: number) => {
|
||||
onTextHeightChange(note.id, textHeight)
|
||||
// Cache the text height for improved performance
|
||||
@@ -208,13 +93,12 @@ export class NoteManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert notes to SvelteFlow nodes
|
||||
* Convert notes to SvelteFlow nodes (rendering-only)
|
||||
*/
|
||||
convertToNodes(
|
||||
notes: FlowNote[],
|
||||
currentNodes: Node[],
|
||||
textHeights: Record<string, number>,
|
||||
onNotesChange: (notes: FlowNote[]) => void,
|
||||
onTextHeightChange: (noteId: string, height: number) => void
|
||||
): Node[] {
|
||||
return notes.map((note) => {
|
||||
@@ -229,7 +113,7 @@ export class NoteManager {
|
||||
id: note.id,
|
||||
type: 'note',
|
||||
position,
|
||||
data: this.createNoteData(note, notes, onNotesChange, onTextHeightChange, isGroupNote),
|
||||
data: this.createNoteData(note, onTextHeightChange, isGroupNote),
|
||||
style: `width: ${size.width}px; height: ${size.height}px;`,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
|
||||
@@ -7,13 +7,16 @@
|
||||
import NoteColorPicker from '../../NoteColorPicker.svelte'
|
||||
import { NoteColor, NOTE_COLORS, DEFAULT_NOTE_COLOR } from '../../noteColors'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { getNoteEditorContext } from '../../noteEditor.svelte'
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
noteId: string
|
||||
text: string
|
||||
color: NoteColor
|
||||
locked?: boolean
|
||||
isGroupNote?: boolean
|
||||
// Callback props for view mode (when no NoteEditor context)
|
||||
onUpdate?: (text: string) => void
|
||||
onDelete?: () => void
|
||||
onColorChange?: (color: NoteColor) => void
|
||||
@@ -27,6 +30,10 @@
|
||||
|
||||
let { data, selected = false, dragging = false }: Props = $props()
|
||||
|
||||
// Get NoteEditor context for edit mode
|
||||
const noteEditorContext = getNoteEditorContext()
|
||||
const isEditModeAvailable = $derived(!!noteEditorContext?.noteEditor)
|
||||
|
||||
let textareaElement: HTMLTextAreaElement | undefined = $state(undefined)
|
||||
let editMode = $state(false)
|
||||
let hovering = $state(false)
|
||||
@@ -35,24 +42,47 @@
|
||||
|
||||
function handleTextSave() {
|
||||
// Only update parent when done editing
|
||||
data.onUpdate?.(textContent)
|
||||
if (isEditModeAvailable && noteEditorContext?.noteEditor) {
|
||||
// Use NoteEditor context in edit mode
|
||||
noteEditorContext.noteEditor.updateText(data.noteId, textContent)
|
||||
} else {
|
||||
// Fallback to callback in view mode
|
||||
data.onUpdate?.(textContent)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(event?: Event) {
|
||||
event?.preventDefault?.()
|
||||
event?.stopPropagation?.()
|
||||
// Call the delete callback
|
||||
data.onDelete?.()
|
||||
if (isEditModeAvailable && noteEditorContext?.noteEditor) {
|
||||
// Use NoteEditor context in edit mode
|
||||
noteEditorContext.noteEditor.deleteNote(data.noteId)
|
||||
} else {
|
||||
// Fallback to callback in view mode
|
||||
data.onDelete?.()
|
||||
}
|
||||
}
|
||||
|
||||
function handleColorChange(color: NoteColor) {
|
||||
data.onColorChange?.(color)
|
||||
if (isEditModeAvailable && noteEditorContext?.noteEditor) {
|
||||
// Use NoteEditor context in edit mode
|
||||
noteEditorContext.noteEditor.updateColor(data.noteId, color)
|
||||
} else {
|
||||
// Fallback to callback in view mode
|
||||
data.onColorChange?.(color)
|
||||
}
|
||||
}
|
||||
|
||||
function handleLockToggle(event?: Event) {
|
||||
event?.preventDefault?.()
|
||||
event?.stopPropagation?.()
|
||||
data.onLockToggle?.(!data.locked)
|
||||
if (isEditModeAvailable && noteEditorContext?.noteEditor) {
|
||||
// Use NoteEditor context in edit mode
|
||||
noteEditorContext.noteEditor.updateLock(data.noteId, !data.locked)
|
||||
} else {
|
||||
// Fallback to callback in view mode
|
||||
data.onLockToggle?.(!data.locked)
|
||||
}
|
||||
}
|
||||
|
||||
// Get color configuration for current color
|
||||
@@ -62,8 +92,8 @@
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
// Don't allow editing if note is locked
|
||||
if (data.locked) {
|
||||
// Don't allow editing if note is locked or edit mode is not available
|
||||
if (data.locked || !isEditModeAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -127,49 +157,51 @@
|
||||
onmouseleave={handleMouseLeave}
|
||||
role="note"
|
||||
>
|
||||
<!-- Action buttons -->
|
||||
<div class="absolute -top-10 -right-2.5 p-2 w-32 h-12 group flex justify-end">
|
||||
<div
|
||||
class={twMerge(
|
||||
'hidden group-hover:flex flex-row gap-2 h-fit',
|
||||
hovering || editMode || colorPickerIsOpen || selected ? 'flex' : ''
|
||||
)}
|
||||
>
|
||||
<!-- Lock/Unlock button -->
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
title={data.locked ? 'Unlock note' : 'Lock note'}
|
||||
aria-label={data.locked ? 'Unlock note' : 'Lock note'}
|
||||
startIcon={{ icon: data.locked ? Lock : Unlock }}
|
||||
onClick={handleLockToggle}
|
||||
iconOnly
|
||||
/>
|
||||
|
||||
<!-- Color picker -->
|
||||
{#if !data.locked}
|
||||
<NoteColorPicker
|
||||
selectedColor={data.color}
|
||||
onColorChange={handleColorChange}
|
||||
bind:isOpen={colorPickerIsOpen}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Delete button -->
|
||||
{#if !data.locked}
|
||||
<!-- Action buttons - only show in edit mode -->
|
||||
{#if isEditModeAvailable}
|
||||
<div class="absolute -top-10 -right-2.5 p-2 w-32 h-12 group flex justify-end">
|
||||
<div
|
||||
class={twMerge(
|
||||
'hidden group-hover:flex flex-row gap-2 h-fit',
|
||||
hovering || editMode || colorPickerIsOpen || selected ? 'flex' : ''
|
||||
)}
|
||||
>
|
||||
<!-- Lock/Unlock button -->
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
title="Delete note"
|
||||
aria-label="Delete note"
|
||||
startIcon={{ icon: X }}
|
||||
onClick={handleDelete}
|
||||
title={data.locked ? 'Unlock note' : 'Lock note'}
|
||||
aria-label={data.locked ? 'Unlock note' : 'Lock note'}
|
||||
startIcon={{ icon: data.locked ? Lock : Unlock }}
|
||||
onClick={handleLockToggle}
|
||||
iconOnly
|
||||
destructive
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Color picker -->
|
||||
{#if !data.locked}
|
||||
<NoteColorPicker
|
||||
selectedColor={data.color}
|
||||
onColorChange={handleColorChange}
|
||||
bind:isOpen={colorPickerIsOpen}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Delete button -->
|
||||
{#if !data.locked}
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
title="Delete note"
|
||||
aria-label="Delete note"
|
||||
startIcon={{ icon: X }}
|
||||
onClick={handleDelete}
|
||||
iconOnly
|
||||
destructive
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Hover help text -->
|
||||
{#if hovering || selected}
|
||||
@@ -178,9 +210,13 @@
|
||||
in:fade={{ duration: 200 }}
|
||||
class="absolute -top-5 h-5 left-0 text-2xs text-secondary rounded-md z-10 transition-opacity duration-300"
|
||||
>
|
||||
{data.locked ? 'Note is locked' : 'Double click to edit'}
|
||||
{data.locked
|
||||
? 'Note is locked'
|
||||
: isEditModeAvailable
|
||||
? 'Double click to edit'
|
||||
: 'View only mode'}
|
||||
</div>
|
||||
{:else if !data.locked}
|
||||
{:else if !data.locked && isEditModeAvailable}
|
||||
<div
|
||||
in:fade={{ duration: 200 }}
|
||||
class="absolute -top-5 h-5 left-0 text-2xs text-secondary rounded-md z-10 transition-opacity duration-300"
|
||||
@@ -238,8 +274,8 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Node resizer - only visible when selected and not locked -->
|
||||
{#if !data.locked}
|
||||
<!-- Node resizer - only visible when selected and not locked and edit mode is available -->
|
||||
{#if !data.locked && isEditModeAvailable}
|
||||
<NodeResizer
|
||||
isVisible={selected && !dragging}
|
||||
minWidth={200}
|
||||
@@ -248,8 +284,15 @@
|
||||
handleClass="!bg-transparent !w-4 !h-4 !border-none !rounded-md"
|
||||
onResizeEnd={(_, params) => {
|
||||
// Update note size when resizing ends
|
||||
if (data.onSizeChange && params.width !== undefined && params.height !== undefined) {
|
||||
data.onSizeChange({ width: params.width, height: params.height })
|
||||
if (params.width !== undefined && params.height !== undefined) {
|
||||
const size = { width: params.width, height: params.height }
|
||||
if (isEditModeAvailable && noteEditorContext?.noteEditor) {
|
||||
// Use NoteEditor context in edit mode
|
||||
noteEditorContext.noteEditor.updateSize(data.noteId, size)
|
||||
} else {
|
||||
// Fallback to callback in view mode
|
||||
data.onSizeChange?.(size)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -132,9 +132,12 @@ export function getNodeColorClasses(state: FlowNodeState, selected: boolean): Fl
|
||||
* @param nodes - Array of nodes with position.x and position.y properties
|
||||
* @returns The bounds { minX, minY, maxX, maxY }
|
||||
*/
|
||||
export function calculateNodesBounds(
|
||||
nodes: Array<{ position: { x: number; y: number } }>
|
||||
): { minX: number; minY: number; maxX: number; maxY: number } {
|
||||
export function calculateNodesBounds(nodes: Array<{ position: { x: number; y: number } }>): {
|
||||
minX: number
|
||||
minY: number
|
||||
maxX: number
|
||||
maxY: number
|
||||
} {
|
||||
return nodes.reduce(
|
||||
(acc, node) => ({
|
||||
minX: Math.min(acc.minX, node.position.x),
|
||||
@@ -150,3 +153,11 @@ export function calculateNodesBounds(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random unique ID for notes
|
||||
* @returns A random string ID
|
||||
*/
|
||||
export function generateId(): string {
|
||||
return 'note-' + Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user