1- import { memo , useCallback , useMemo } from 'react'
2- import { BLOCK_DIMENSIONS , getNoteBlockHeight , NoteBlockView } from '@sim/workflow-renderer'
3- import type { NodeProps } from 'reactflow'
1+ import { memo , useCallback , useEffect , useMemo , useRef , useState } from 'react'
2+ import {
3+ BLOCK_DIMENSIONS ,
4+ DEFAULT_NOTE_COLOR ,
5+ estimateNoteBlockHeight ,
6+ isNoteColor ,
7+ NoteBlockView ,
8+ type NoteColor ,
9+ type NoteContentEditorProps ,
10+ } from '@sim/workflow-renderer'
11+ import { type NodeProps , useReactFlow } from 'reactflow'
412import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
513import { ActionBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar'
14+ import { NoteMarkdownEditor } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-markdown-editor'
15+ import type { WorkflowBlockProps } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/types'
616import { useBlockVisual } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
717import { useBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions'
18+ import { isBlockProtected } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
19+ import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
20+ import { usePanelEditorStore } from '@/stores/panel'
821import { useSubBlockStore } from '@/stores/workflows/subblock/store'
9- import type { WorkflowBlockProps } from '../ workflow-block/types '
22+ import { useWorkflowStore } from '@/stores/workflows/ workflow/store '
1023
1124interface NoteBlockNodeData extends WorkflowBlockProps { }
1225
13- /** Extracts the string content from a raw subblock value (string or `{ value }`). */
14- function extractFieldValue ( rawValue : unknown ) : string | undefined {
26+ const NOTE_EXPAND_FOCUS_DURATION_MS = 300
27+
28+ /** Resolves a string stored directly or inside a serialized subblock value. */
29+ function getNoteStringValue ( rawValue : unknown ) : string | undefined {
1530 if ( typeof rawValue === 'string' ) return rawValue
1631 if ( rawValue && typeof rawValue === 'object' && 'value' in rawValue ) {
1732 const candidate = ( rawValue as { value ?: unknown } ) . value
@@ -20,6 +35,10 @@ function extractFieldValue(rawValue: unknown): string | undefined {
2035 return undefined
2136}
2237
38+ function renderNoteContentEditor ( props : NoteContentEditorProps ) {
39+ return < NoteMarkdownEditor { ...props } />
40+ }
41+
2342/**
2443 * Editor container for {@link NoteBlockView}.
2544 *
@@ -34,12 +53,15 @@ export const NoteBlock = memo(function NoteBlock({
3453 selected,
3554} : NodeProps < NoteBlockNodeData > ) {
3655 const { type, name } = data
56+ const focusFrameRef = useRef < number | null > ( null )
57+ const { getNode, getViewport, setCenter } = useReactFlow ( )
3758
38- const { activeWorkflowId, isEnabled, handleClick , hasRing, ringStyles } = useBlockVisual ( {
59+ const { activeWorkflowId, isEnabled, hasRing, ringStyles } = useBlockVisual ( {
3960 blockId : id ,
4061 data,
4162 isSelected : selected ,
4263 } )
64+ const { collaborativeSetSubblockValue, collaborativeUpdateBlockName } = useCollaborativeWorkflow ( )
4365 const storedValues = useSubBlockStore (
4466 useCallback (
4567 ( state ) => {
@@ -52,16 +74,91 @@ export const NoteBlock = memo(function NoteBlock({
5274
5375 const content = useMemo ( ( ) => {
5476 if ( data . isPreview && data . subBlockValues ) {
55- const extractedContent = extractFieldValue ( data . subBlockValues . content )
77+ const extractedContent = getNoteStringValue ( data . subBlockValues . content )
5678 return typeof extractedContent === 'string' ? extractedContent : ''
5779 }
58- const storedContent = extractFieldValue ( storedValues ?. content )
80+ const storedContent = getNoteStringValue ( storedValues ?. content )
5981 return typeof storedContent === 'string' ? storedContent : ''
6082 } , [ data . isPreview , data . subBlockValues , storedValues ] )
6183
84+ const rawColor = data . isPreview
85+ ? getNoteStringValue ( data . subBlockValues ?. color )
86+ : getNoteStringValue ( storedValues ?. color )
87+ const noteColor = isNoteColor ( rawColor ) ? rawColor : DEFAULT_NOTE_COLOR
88+
6289 const userPermissions = useUserPermissionsContext ( )
6390 const canEditWorkflow = userPermissions . canEdit && ! data . isWorkflowLocked
64- const isEmpty = content . trim ( ) . length === 0
91+ const isProtected = useWorkflowStore (
92+ useCallback ( ( state ) => isBlockProtected ( id , state . blocks ) , [ id ] )
93+ )
94+ const clearCurrentBlock = usePanelEditorStore ( ( state ) => state . clearCurrentBlock )
95+ const canEditNote = canEditWorkflow && ! data . isPreview && ! isProtected
96+ const [ blockHeight , setBlockHeight ] = useState ( ( ) => estimateNoteBlockHeight ( content ) )
97+ const [ isExpanded , setIsExpanded ] = useState ( false )
98+ const isFocused = selected
99+
100+ useEffect ( ( ) => {
101+ if ( ! canEditNote ) setIsExpanded ( false )
102+ } , [ canEditNote ] )
103+
104+ useEffect (
105+ ( ) => ( ) => {
106+ if ( focusFrameRef . current !== null ) cancelAnimationFrame ( focusFrameRef . current )
107+ } ,
108+ [ ]
109+ )
110+
111+ const handleNameChange = ( nextName : string ) => {
112+ if ( ! canEditNote ) return false
113+ return collaborativeUpdateBlockName ( id , nextName ) . success
114+ }
115+
116+ const handleContentChange = ( nextContent : string ) => {
117+ if ( ! canEditNote ) return
118+ collaborativeSetSubblockValue ( id , 'content' , nextContent )
119+ }
120+
121+ const handleColorChange = useCallback (
122+ ( color : NoteColor ) => {
123+ if ( ! canEditNote ) return
124+ collaborativeSetSubblockValue ( id , 'color' , color )
125+ } ,
126+ [ canEditNote , collaborativeSetSubblockValue , id ]
127+ )
128+
129+ const handleNoteSelect = useCallback ( ( ) => {
130+ if ( data . isPreview || data . isEmbedded ) return
131+ clearCurrentBlock ( )
132+ } , [ clearCurrentBlock , data . isEmbedded , data . isPreview ] )
133+
134+ const handleExpandedChange = useCallback (
135+ ( expanded : boolean ) => {
136+ if ( expanded && ! canEditNote ) return
137+ if ( expanded ) handleNoteSelect ( )
138+ setIsExpanded ( expanded )
139+
140+ if ( focusFrameRef . current !== null ) cancelAnimationFrame ( focusFrameRef . current )
141+ if ( ! expanded ) return
142+
143+ focusFrameRef . current = requestAnimationFrame ( ( ) => {
144+ focusFrameRef . current = requestAnimationFrame ( ( ) => {
145+ focusFrameRef . current = null
146+ const node = getNode ( id )
147+ const position = node ?. positionAbsolute ?? node ?. position
148+ if ( ! position ) return
149+ void setCenter (
150+ position . x + BLOCK_DIMENSIONS . NOTE_WIDTH / 2 ,
151+ position . y + blockHeight / 2 ,
152+ {
153+ zoom : getViewport ( ) . zoom ,
154+ duration : NOTE_EXPAND_FOCUS_DURATION_MS ,
155+ }
156+ )
157+ } )
158+ } )
159+ } ,
160+ [ blockHeight , canEditNote , getNode , getViewport , handleNoteSelect , id , setCenter ]
161+ )
65162
66163 /**
67164 * Calculate deterministic dimensions based on content structure. Uses fixed
@@ -70,21 +167,41 @@ export const NoteBlock = memo(function NoteBlock({
70167 useBlockDimensions ( {
71168 blockId : id ,
72169 calculateDimensions : ( ) => {
73- return { width : BLOCK_DIMENSIONS . FIXED_WIDTH , height : getNoteBlockHeight ( isEmpty ) }
170+ return {
171+ width : BLOCK_DIMENSIONS . NOTE_WIDTH ,
172+ height : blockHeight ,
173+ }
74174 } ,
75- dependencies : [ isEmpty ] ,
175+ dependencies : [ blockHeight ] ,
76176 } )
77177
78178 return (
79179 < NoteBlockView
80180 name = { name }
81181 content = { content }
182+ noteColor = { noteColor }
82183 isEnabled = { isEnabled }
184+ isFocused = { isFocused }
185+ isExpanded = { isExpanded }
186+ canEdit = { canEditNote }
83187 hasRing = { hasRing }
84188 ringStyles = { ringStyles }
85- onSelect = { handleClick }
189+ onSelect = { handleNoteSelect }
190+ onNameChange = { handleNameChange }
191+ onContentChange = { handleContentChange }
192+ onHeightChange = { setBlockHeight }
193+ onExpandedChange = { handleExpandedChange }
194+ renderContentEditor = { renderNoteContentEditor }
86195 actionBar = {
87- < ActionBar blockId = { id } blockType = { type } disabled = { ! canEditWorkflow } variant = 'swell' />
196+ < ActionBar
197+ blockId = { id }
198+ blockType = { type }
199+ disabled = { ! canEditWorkflow }
200+ variant = 'swell'
201+ noteColor = { noteColor }
202+ onNoteColorChange = { handleColorChange }
203+ onNoteColorMenuOpen = { handleNoteSelect }
204+ />
88205 }
89206 />
90207 )
0 commit comments