diff --git a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx index c5ccfcc4bc6..74cf5990e2a 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx @@ -17,6 +17,7 @@ import { toast, } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' +import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context' import { useSubmitCopilotFeedback } from '@/hooks/queries/copilot-feedback' import { useForkMothershipChat } from '@/hooks/queries/mothership-chats' import { useFolderStore } from '@/stores/folders/store' @@ -49,7 +50,6 @@ const BUTTON_CLASS = interface MessageActionsProps { content: string - chatId?: string userQuery?: string requestId?: string messageId?: string @@ -57,13 +57,13 @@ interface MessageActionsProps { export const MessageActions = memo(function MessageActions({ content, - chatId, userQuery, requestId, messageId, }: MessageActionsProps) { const router = useRouter() const params = useParams<{ workspaceId: string }>() + const { chatId } = useChatSurface() const [copied, setCopied] = useState(false) const [copiedRequestId, setCopiedRequestId] = useState(false) const [pendingFeedback, setPendingFeedback] = useState<'up' | 'down' | null>(null) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/chat-surface-context/chat-surface-context.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/chat-surface-context/chat-surface-context.tsx new file mode 100644 index 00000000000..4e5ca2b901c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/chat-surface-context/chat-surface-context.tsx @@ -0,0 +1,106 @@ +'use client' + +import { + createContext, + type ReactNode, + useCallback, + useContext, + useLayoutEffect, + useMemo, + useRef, +} from 'react' +import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types' +import type { ChatContext } from '@/stores/panel' + +/** + * Identity and interaction callbacks shared across a Mothership chat surface + * (home conversation view, home initial view, copilot panel). Carried via + * context so leaf components (UserInput, MessageContent, MessageActions) can + * consume them without relaying through every intermediate component. + */ +interface ChatSurfaceContextValue { + /** Resolved id of the chat backing this surface, if one exists yet. */ + chatId?: string + /** Id of the user interacting with this surface. */ + userId?: string + /** Notifies the surface owner that a context chip was added to the input. */ + onContextAdd: (context: ChatContext) => void + /** Notifies the surface owner that a context chip was removed from the input. */ + onContextRemove: (context: ChatContext) => void + /** Opens a workspace resource referenced from rendered message content. */ + onWorkspaceResourceSelect: (resource: MothershipResource) => void +} + +const noop = () => {} + +const ChatSurfaceContext = createContext({ + onContextAdd: noop, + onContextRemove: noop, + onWorkspaceResourceSelect: noop, +}) + +interface ChatSurfaceProviderProps { + chatId?: string + userId?: string + onContextAdd?: (context: ChatContext) => void + onContextRemove?: (context: ChatContext) => void + onWorkspaceResourceSelect?: (resource: MothershipResource) => void + children: ReactNode +} + +/** + * Provides the chat-surface identity and interaction callbacks to descendants. + * Callbacks are latched in refs and exposed as stable wrappers so the memoized + * context value only changes when `chatId` or `userId` change — consumers do + * not re-render when a parent re-creates a handler. + */ +export function ChatSurfaceProvider({ + chatId, + userId, + onContextAdd, + onContextRemove, + onWorkspaceResourceSelect, + children, +}: ChatSurfaceProviderProps) { + const onContextAddRef = useRef(onContextAdd) + const onContextRemoveRef = useRef(onContextRemove) + const onWorkspaceResourceSelectRef = useRef(onWorkspaceResourceSelect) + + useLayoutEffect(() => { + onContextAddRef.current = onContextAdd + onContextRemoveRef.current = onContextRemove + onWorkspaceResourceSelectRef.current = onWorkspaceResourceSelect + }) + + const stableOnContextAdd = useCallback((context: ChatContext) => { + onContextAddRef.current?.(context) + }, []) + const stableOnContextRemove = useCallback((context: ChatContext) => { + onContextRemoveRef.current?.(context) + }, []) + const stableOnWorkspaceResourceSelect = useCallback((resource: MothershipResource) => { + onWorkspaceResourceSelectRef.current?.(resource) + }, []) + + const value = useMemo( + () => ({ + chatId, + userId, + onContextAdd: stableOnContextAdd, + onContextRemove: stableOnContextRemove, + onWorkspaceResourceSelect: stableOnWorkspaceResourceSelect, + }), + [chatId, userId, stableOnContextAdd, stableOnContextRemove, stableOnWorkspaceResourceSelect] + ) + + return {children} +} + +/** + * Reads the surrounding chat surface. Outside a provider this returns no-op + * callbacks and undefined identity, matching the previous optional-prop + * behavior. + */ +export function useChatSurface(): ChatSurfaceContextValue { + return useContext(ChatSurfaceContext) +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/chat-surface-context/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/chat-surface-context/index.ts new file mode 100644 index 00000000000..a9bd0ecd91b --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/chat-surface-context/index.ts @@ -0,0 +1 @@ +export { ChatSurfaceProvider, useChatSurface } from './chat-surface-context' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/index.ts index 8728fc036c4..799c9f2c2fd 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/index.ts @@ -1,4 +1,5 @@ export { ChatMessageAttachments } from './chat-message-attachments' +export { ChatSurfaceProvider, useChatSurface } from './chat-surface-context' export { ContextMentionIcon } from './context-mention-icon' export { CreditsChip } from './credits-chip' export { @@ -6,6 +7,10 @@ export { MessageContent, } from './message-content' export { MothershipChat } from './mothership-chat' +export { + MothershipResourcesProvider, + useMothershipResources, +} from './mothership-resources-context' export { MothershipView } from './mothership-view' export { QueuedMessages } from './queued-messages' export { SuggestedActions } from './suggested-actions' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index 8704a36ebea..304c63a4290 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -6,7 +6,8 @@ import { Read as ReadTool, WorkspaceFile } from '@/lib/copilot/generated/tool-ca import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools' import { resolveToolDisplay } from '@/lib/copilot/tools/client/store-utils' import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-call-state' -import type { ContentBlock, MothershipResource, OptionItem, ToolCallData } from '../../types' +import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context' +import type { ContentBlock, OptionItem, ToolCallData } from '../../types' import { SUBAGENT_LABELS, TOOL_UI_METADATA } from '../../types' import type { AgentGroupItem } from './components' import { @@ -676,7 +677,6 @@ interface MessageContentProps { fallbackContent: string isStreaming: boolean onOptionSelect?: (id: string) => void - onWorkspaceResourceSelect?: (resource: MothershipResource) => void } function MessageContentInner({ @@ -684,8 +684,8 @@ function MessageContentInner({ fallbackContent, isStreaming = false, onOptionSelect, - onWorkspaceResourceSelect, }: MessageContentProps) { + const { onWorkspaceResourceSelect } = useChatSurface() const parsed = useMemo(() => (blocks.length > 0 ? parseBlocks(blocks) : []), [blocks]) const segments: MessageSegment[] = diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx index a605459da3f..6bd66067600 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx @@ -4,6 +4,7 @@ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef } from ' import { cn } from '@/lib/core/utils/cn' import { MessageActions } from '@/app/workspace/[workspaceId]/components' import { ChatMessageAttachments } from '@/app/workspace/[workspaceId]/home/components/chat-message-attachments' +import { ChatSurfaceProvider } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context' import { assistantMessageHasRenderableContent, MessageContent, @@ -124,20 +125,16 @@ interface AssistantMessageRowProps { message: ChatMessage isStreaming: boolean precedingUserContent?: string - chatId?: string rowClassName: string onOptionSelect?: (id: string) => void - onWorkspaceResourceSelect?: (resource: MothershipResource) => void } const AssistantMessageRow = memo(function AssistantMessageRow({ message, isStreaming, precedingUserContent, - chatId, rowClassName, onOptionSelect, - onWorkspaceResourceSelect, }: AssistantMessageRowProps) { const blocks = message.contentBlocks ?? EMPTY_BLOCKS const hasAnyBlocks = blocks.length > 0 @@ -161,13 +158,11 @@ const AssistantMessageRow = memo(function AssistantMessageRow({ fallbackContent={message.content} isStreaming={isStreaming} onOptionSelect={onOptionSelect} - onWorkspaceResourceSelect={onWorkspaceResourceSelect} /> {showActions && (
(null) const onSubmitRef = useRef(onSubmit) - const onWorkspaceResourceSelectRef = useRef(onWorkspaceResourceSelect) useEffect(() => { onSubmitRef.current = onSubmit - onWorkspaceResourceSelectRef.current = onWorkspaceResourceSelect - }, [onSubmit, onWorkspaceResourceSelect]) + }, [onSubmit]) const stableOnOptionSelect = useCallback((id: string) => { onSubmitRef.current(id) }, []) - const stableOnWorkspaceResourceSelect = useCallback((resource: MothershipResource) => { - onWorkspaceResourceSelectRef.current?.(resource) - }, []) function handleSendQueuedHead() { const topMessage = messageQueue[0] @@ -286,75 +276,78 @@ export function MothershipChat({ }, [isStaging, stagedMessageCount, initialScrollBlocked, scrollToBottom]) return ( -
-
- {isLoading && !hasMessages ? ( - - ) : ( -
- {stagedMessages.map((msg, localIndex) => { - const index = stagedOffset + localIndex - if (msg.role === 'user') { + +
+
+ {isLoading && !hasMessages ? ( + + ) : ( +
+ {stagedMessages.map((msg, localIndex) => { + const index = stagedOffset + localIndex + if (msg.role === 'user') { + return ( + + ) + } + + const isLast = index === messages.length - 1 return ( - ) - } + })} +
+ )} +
- const isLast = index === messages.length - 1 - return ( - - ) - })} +
+
+ +
- )} -
- -
-
- -
-
+
) } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-resources-context/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-resources-context/index.ts new file mode 100644 index 00000000000..aa545330a59 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-resources-context/index.ts @@ -0,0 +1,4 @@ +export { + MothershipResourcesProvider, + useMothershipResources, +} from './mothership-resources-context' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-resources-context/mothership-resources-context.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-resources-context/mothership-resources-context.tsx new file mode 100644 index 00000000000..1c3c5d90858 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-resources-context/mothership-resources-context.tsx @@ -0,0 +1,69 @@ +'use client' + +import { createContext, type ReactNode, useContext, useMemo } from 'react' +import type { + MothershipResource, + MothershipResourceType, +} from '@/app/workspace/[workspaceId]/home/types' + +/** + * Resource-management operations for the Mothership resource panel. Provided + * by the home surface (which owns the resource state via `useChat`) and + * consumed at the leaves (`ResourceTabs`, `MothershipView`) so the operations + * do not have to be relayed through intermediate components. + */ +interface MothershipResourcesContextValue { + /** Makes the given resource the active tab. */ + selectResource: (id: string) => void + /** Adds a resource to the panel and activates it. */ + addResource: (resource: MothershipResource) => void + /** Removes a resource from the panel. */ + removeResource: (resourceType: MothershipResourceType, resourceId: string) => void + /** Replaces the resource list with a new ordering. */ + reorderResources: (resources: MothershipResource[]) => void + /** Collapses the resource panel. */ + collapseResource: () => void +} + +const MothershipResourcesContext = createContext(null) + +interface MothershipResourcesProviderProps extends MothershipResourcesContextValue { + children: ReactNode +} + +/** + * Provides resource-management operations to the resource panel subtree. All + * operations are expected to be referentially stable; the context value is + * memoized on their identities. + */ +export function MothershipResourcesProvider({ + selectResource, + addResource, + removeResource, + reorderResources, + collapseResource, + children, +}: MothershipResourcesProviderProps) { + const value = useMemo( + () => ({ selectResource, addResource, removeResource, reorderResources, collapseResource }), + [selectResource, addResource, removeResource, reorderResources, collapseResource] + ) + + return ( + + {children} + + ) +} + +/** + * Reads the resource-management operations for the surrounding resource panel. + * Must be called under a {@link MothershipResourcesProvider}. + */ +export function useMothershipResources(): MothershipResourcesContextValue { + const value = useContext(MothershipResourcesContext) + if (!value) { + throw new Error('useMothershipResources must be used within a MothershipResourcesProvider') + } + return value +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx index c8411552407..dfab22cc4fd 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx @@ -16,6 +16,7 @@ import { SIM_RESOURCE_DRAG_TYPE, SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/r import { isEphemeralResource } from '@/lib/copilot/resources/types' import { cn } from '@/lib/core/utils/cn' import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer' +import { useMothershipResources } from '@/app/workspace/[workspaceId]/home/components/mothership-resources-context' import { AddResourceDropdown } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown' import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry' import { @@ -241,11 +242,6 @@ interface ResourceTabsProps { chatId?: string resources: MothershipResource[] activeId: string | null - onSelect: (id: string) => void - onAddResource: (resource: MothershipResource) => void - onRemoveResource: (resourceType: MothershipResourceType, resourceId: string) => void - onReorderResources: (resources: MothershipResource[]) => void - onCollapse: () => void previewMode?: PreviewMode onCyclePreviewMode?: () => void actions?: ReactNode @@ -256,17 +252,19 @@ export function ResourceTabs({ chatId, resources, activeId, - onSelect, - onAddResource, - onRemoveResource, - onReorderResources, - onCollapse, previewMode, onCyclePreviewMode, actions, }: ResourceTabsProps) { const PreviewModeIcon = PREVIEW_MODE_ICONS[previewMode ?? 'split'] const nameLookup = useResourceNameLookup(workspaceId) + const { + selectResource, + addResource: onAddResource, + removeResource: onRemoveResource, + reorderResources: onReorderResources, + collapseResource, + } = useMothershipResources() const scrollNodeRef = useRef(null) useEffect(() => { @@ -354,7 +352,7 @@ export function ResourceTabs({ const next = new Set() for (let i = start; i <= end; i++) next.add(resources[i].id) setSelectedIds(next) - onSelect(resource.id) + selectResource(resource.id) return } } @@ -370,11 +368,11 @@ export function ResourceTabs({ if (activeId === resource.id) { const fallback = findNearestId(resources, idx, next) ?? findNearestId(resources, idx, null) - if (fallback) onSelect(fallback) + if (fallback) selectResource(fallback) } } else { setSelectedIds((prev) => new Set(prev).add(resource.id)) - onSelect(resource.id) + selectResource(resource.id) } if (!anchorIdRef.current) anchorIdRef.current = resource.id return @@ -383,9 +381,9 @@ export function ResourceTabs({ // Plain click: single-select anchorIdRef.current = resource.id setSelectedIds(new Set([resource.id])) - onSelect(resource.id) + selectResource(resource.id) }, - [resources, onSelect, selectedIds, activeId] + [resources, selectResource, selectedIds, activeId] ) const handleRemove = useCallback( @@ -553,7 +551,7 @@ export function ResourceTabs({
) @@ -758,7 +748,6 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue as any} disabled={isDisabled} - activeSearchTarget={activeSearchTarget} /> ) @@ -791,7 +780,6 @@ function SubBlockComponent({ } wandControlRef={wandControlRef} hideInternalWand={true} - activeSearchTarget={activeSearchTarget} /> ) @@ -816,7 +804,6 @@ function SubBlockComponent({ previewValue={previewValue} disabled={allowExpandInPreview ? false : isDisabled} allowExpandInPreview={allowExpandInPreview} - activeSearchTarget={activeSearchTarget} /> ) @@ -828,7 +815,6 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue} disabled={isDisabled} - activeSearchTarget={activeSearchTarget} /> ) @@ -848,7 +834,6 @@ function SubBlockComponent({ subBlockValues={subBlockValues} disabled={isDisabled} subBlockId={config.id} - activeSearchTarget={activeSearchTarget} /> ) @@ -863,7 +848,6 @@ function SubBlockComponent({ subBlockValues={subBlockValues ?? {}} disabled={isDisabled} maxHeight={config.maxHeight} - activeSearchTarget={activeSearchTarget} /> ) @@ -875,7 +859,6 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue as any} disabled={isDisabled} - activeSearchTarget={activeSearchTarget} /> ) @@ -888,7 +871,6 @@ function SubBlockComponent({ previewValue={previewValue as any} disabled={isDisabled} mode='router' - activeSearchTarget={activeSearchTarget} /> ) @@ -900,7 +882,6 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue as any} disabled={isDisabled} - activeSearchTarget={activeSearchTarget} /> ) @@ -913,7 +894,6 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue as any} disabled={isDisabled} - activeSearchTarget={activeSearchTarget} /> ) @@ -928,7 +908,6 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue as any} disabled={isDisabled} - activeSearchTarget={activeSearchTarget} /> ) @@ -944,7 +923,6 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue} previewContextValues={contextValues} - activeSearchTarget={activeSearchTarget} /> ) @@ -959,7 +937,6 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue} previewContextValues={contextValues} - activeSearchTarget={activeSearchTarget} /> ) @@ -973,7 +950,6 @@ function SubBlockComponent({ previewValue={previewValue} previewContextValues={contextValues} overrides={FOLDER_OVERRIDES} - activeSearchTarget={activeSearchTarget} /> ) @@ -985,7 +961,6 @@ function SubBlockComponent({ disabled={isDisabled} isPreview={isPreview} previewValue={previewValue as any} - activeSearchTarget={activeSearchTarget} /> ) @@ -998,7 +973,6 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue as any} previewContextValues={contextValues} - activeSearchTarget={activeSearchTarget} /> ) @@ -1011,7 +985,6 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue as any} previewContextValues={contextValues} - activeSearchTarget={activeSearchTarget} /> ) @@ -1024,7 +997,6 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue} previewContextValues={contextValues} - activeSearchTarget={activeSearchTarget} /> ) @@ -1038,7 +1010,6 @@ function SubBlockComponent({ disabled={isDisabled} config={config} showValue={true} - activeSearchTarget={activeSearchTarget} /> ) @@ -1051,7 +1022,6 @@ function SubBlockComponent({ previewValue={previewValue as any} disabled={isDisabled} previewContextValues={contextValues} - activeSearchTarget={activeSearchTarget} /> ) @@ -1063,7 +1033,6 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue as any} disabled={isDisabled} - activeSearchTarget={activeSearchTarget} /> ) @@ -1076,7 +1045,6 @@ function SubBlockComponent({ previewValue={previewValue} config={config} disabled={isDisabled} - activeSearchTarget={activeSearchTarget} /> ) @@ -1088,7 +1056,6 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue as FilterRule[] | null | undefined} disabled={isDisabled} - activeSearchTarget={activeSearchTarget} /> ) @@ -1100,7 +1067,6 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue as SortRule[] | null | undefined} disabled={isDisabled} - activeSearchTarget={activeSearchTarget} /> ) @@ -1115,7 +1081,6 @@ function SubBlockComponent({ previewValue={previewValue} previewContextValues={contextValues} overrides={SLACK_OVERRIDES} - activeSearchTarget={activeSearchTarget} /> ) @@ -1127,7 +1092,6 @@ function SubBlockComponent({ disabled={isDisabled} isPreview={isPreview} previewValue={previewValue as string | null} - activeSearchTarget={activeSearchTarget} /> ) @@ -1139,7 +1103,6 @@ function SubBlockComponent({ disabled={isDisabled} isPreview={isPreview} previewValue={previewValue as any} - activeSearchTarget={activeSearchTarget} /> ) @@ -1152,7 +1115,6 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue as any} previewContextValues={contextValues} - activeSearchTarget={activeSearchTarget} /> ) @@ -1165,7 +1127,6 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue} previewContextValues={contextValues} - activeSearchTarget={activeSearchTarget} /> ) @@ -1202,7 +1163,6 @@ function SubBlockComponent({ previewValue={previewValue as any} disabled={isDisabled} wandControlRef={wandControlRef} - activeSearchTarget={activeSearchTarget} /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/subflow-editor/subflow-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/subflow-editor/subflow-editor.tsx index 0f767ef26e6..61829c185f8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/subflow-editor/subflow-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/subflow-editor/subflow-editor.tsx @@ -10,7 +10,7 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' -import type { ActiveSearchTarget } from '@/stores/panel/editor/store' +import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider' import type { BlockState } from '@/stores/workflows/workflow/types' import type { ConnectedBlock } from '../../hooks/use-block-connections' import { useSubflowEditor } from '../../hooks/use-subflow-editor' @@ -39,7 +39,6 @@ interface SubflowEditorProps { toggleConnectionsCollapsed: () => void userCanEdit: boolean isConnectionsAtMinHeight: boolean - activeSearchTarget?: ActiveSearchTarget | null } /** @@ -60,8 +59,8 @@ export function SubflowEditor({ toggleConnectionsCollapsed, userCanEdit, isConnectionsAtMinHeight, - activeSearchTarget, }: SubflowEditorProps) { + const activeSearchTarget = useActiveSearchTarget() const { subflowConfig, currentType, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index 9e32b7fb919..5039895a7ed 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -39,6 +39,7 @@ import { useEditorBlockProperties, useEditorSubblockLayout, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks' +import { ActiveSearchTargetProvider } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider' import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config' import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config' import { getSubBlockStableKey } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/utils' @@ -368,411 +369,412 @@ export function Editor() { const isConnectionsAtMinHeight = connectionsHeight <= 35 return ( -
- {/* Header */} -
-
- {(blockConfig || isSubflow) && currentBlock?.type !== 'note' && ( -
- +
+ {/* Header */} +
+
+ {(blockConfig || isSubflow) && currentBlock?.type !== 'note' && ( +
+ +
+ )} + {isRenaming ? ( + setEditedName(e.target.value)} + onBlur={handleSaveRename} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleSaveRename() + } else if (e.key === 'Escape') { + handleCancelRename() + } + }} + className='min-w-0 flex-1 truncate bg-transparent pr-2 font-medium text-[var(--text-primary)] text-sm outline-none' /> -
- )} - {isRenaming ? ( - setEditedName(e.target.value)} - onBlur={handleSaveRename} - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleSaveRename() - } else if (e.key === 'Escape') { - handleCancelRename() - } - }} - className='min-w-0 flex-1 truncate bg-transparent pr-2 font-medium text-[var(--text-primary)] text-sm outline-none' - /> - ) : ( -

{ - if (e.detail === 2) { - e.preventDefault() - } - }} - > - {blockNameSearchHighlight - ? formatDisplayText(title, { workflowSearchHighlight: blockNameSearchHighlight }) - : title} -

- )} -
-
- {/* Locked indicator - clickable to unlock if user has admin permissions, block is locked directly, and not locked by an ancestor */} - {isLocked && currentBlock && ( - - - {userPermissions.canAdmin && currentBlock.locked && !isAncestorLocked ? ( + ) : ( +

{ + if (e.detail === 2) { + e.preventDefault() + } + }} + > + {blockNameSearchHighlight + ? formatDisplayText(title, { workflowSearchHighlight: blockNameSearchHighlight }) + : title} +

+ )} +
+
+ {/* Locked indicator - clickable to unlock if user has admin permissions, block is locked directly, and not locked by an ancestor */} + {isLocked && currentBlock && ( + + + {userPermissions.canAdmin && currentBlock.locked && !isAncestorLocked ? ( + + ) : ( +
+ +
+ )} +
+ +

+ {isAncestorLocked + ? 'Ancestor container is locked' + : userPermissions.canAdmin && currentBlock.locked + ? 'Unlock block' + : 'Block is locked'} +

+
+
+ )} + {/* Rename button */} + {currentBlock && ( + + - ) : ( -
- -
- )} -
- -

- {isAncestorLocked - ? 'Ancestor container is locked' - : userPermissions.canAdmin && currentBlock.locked - ? 'Unlock block' - : 'Block is locked'} -

-
-
- )} - {/* Rename button */} - {currentBlock && ( + + +

{isRenaming ? 'Save name' : 'Rename block'}

+
+ + )} + {/* Focus on block button */} + {/* {currentBlock && ( -

{isRenaming ? 'Save name' : 'Rename block'}

+

Focus on block

- )} - {/* Focus on block button */} - {/* {currentBlock && ( + )} */} -

Focus on block

+

Open docs

- )} */} - - - - - -

Open docs

-
-
+
-
- {!currentBlockId || !currentBlock ? ( -
- Select a block to edit -
- ) : isSubflow ? ( - - ) : ( -
- {/* Subblocks Section */} -
-
- {/* Workflow Preview - only for workflow blocks with a selected child workflow */} - {isWorkflowBlock && childWorkflowId && ( - <> -
-
- Workflow Preview -
-
- {isLoadingChildWorkflow ? ( -
- -
- ) : childWorkflowState ? ( - <> -
- + {!currentBlockId || !currentBlock ? ( +
+ Select a block to edit +
+ ) : isSubflow ? ( + + ) : ( +
+ {/* Subblocks Section */} +
+
+ {/* Workflow Preview - only for workflow blocks with a selected child workflow */} + {isWorkflowBlock && childWorkflowId && ( + <> +
+
+ Workflow Preview +
+
+ {isLoadingChildWorkflow ? ( +
+
- - - - - Open workflow - - - ) : ( -
- - Unable to load preview - -
- )} + ) : childWorkflowState ? ( + <> +
+ +
+ + + + + Open workflow + + + ) : ( +
+ + Unable to load preview + +
+ )} +
+ + + )} + {subBlocks.length === 0 && !isWorkflowBlock ? ( +
+ This block has no subblocks
- - - )} - {subBlocks.length === 0 && !isWorkflowBlock ? ( -
- This block has no subblocks -
- ) : ( -
- {regularSubBlocks.map((subBlock, index) => { - const stableKey = getSubBlockStableKey( - currentBlockId || '', - subBlock, - subBlockState - ) - const canonicalId = canonicalIndex.canonicalIdBySubBlockId[subBlock.id] - const canonicalGroup = canonicalId - ? canonicalIndex.groupsById[canonicalId] - : undefined - const isCanonicalSwap = isCanonicalPair(canonicalGroup) - const canonicalMode = - canonicalGroup && isCanonicalSwap - ? resolveCanonicalMode( - canonicalGroup, - blockSubBlockValues, - canonicalModeOverrides - ) + ) : ( +
+ {regularSubBlocks.map((subBlock, index) => { + const stableKey = getSubBlockStableKey( + currentBlockId || '', + subBlock, + subBlockState + ) + const canonicalId = canonicalIndex.canonicalIdBySubBlockId[subBlock.id] + const canonicalGroup = canonicalId + ? canonicalIndex.groupsById[canonicalId] : undefined - - const showDivider = - index < regularSubBlocks.length - 1 || - (!hasAdvancedOnlyFields && index < subBlocks.length - 1) - - return ( -
- { - if (!currentBlockId) return - const nextMode = - canonicalMode === 'advanced' ? 'basic' : 'advanced' - collaborativeSetBlockCanonicalMode( - currentBlockId, - canonicalId, - nextMode - ) - }, - } - : undefined - } - /> - {showDivider && } + const isCanonicalSwap = isCanonicalPair(canonicalGroup) + const canonicalMode = + canonicalGroup && isCanonicalSwap + ? resolveCanonicalMode( + canonicalGroup, + blockSubBlockValues, + canonicalModeOverrides + ) + : undefined + + const showDivider = + index < regularSubBlocks.length - 1 || + (!hasAdvancedOnlyFields && index < subBlocks.length - 1) + + return ( +
+ { + if (!currentBlockId) return + const nextMode = + canonicalMode === 'advanced' ? 'basic' : 'advanced' + collaborativeSetBlockCanonicalMode( + currentBlockId, + canonicalId, + nextMode + ) + }, + } + : undefined + } + /> + {showDivider && } +
+ ) + })} + + {hasAdvancedOnlyFields && canEditBlock && ( +
+
+ +
- ) - })} - - {hasAdvancedOnlyFields && canEditBlock && ( -
-
- -
-
- )} - {hasAdvancedOnlyFields && !canEditBlock && displayAdvancedOptions && ( -
-
- - Additional fields - -
-
- )} - - {advancedOnlySubBlocks.map((subBlock, index) => { - const stableKey = getSubBlockStableKey( - currentBlockId || '', - subBlock, - subBlockState - ) - - return ( -
- - {index < advancedOnlySubBlocks.length - 1 && ( - - )} + )} + {hasAdvancedOnlyFields && !canEditBlock && displayAdvancedOptions && ( +
+
+ + Additional fields + +
- ) - })} -
- )} -
-
- - {/* Connections Section - Only show when there are connections */} - {hasIncomingConnections && ( -
- {/* Resize Handle */} -
-
+ )} + + {advancedOnlySubBlocks.map((subBlock, index) => { + const stableKey = getSubBlockStableKey( + currentBlockId || '', + subBlock, + subBlockState + ) + + return ( +
+ + {index < advancedOnlySubBlocks.length - 1 && ( + + )} +
+ ) + })} +
+ )}
+
- {/* Connections Header with Chevron */} + {/* Connections Section - Only show when there are connections */} + {hasIncomingConnections && (
{ - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault() - toggleConnectionsCollapsed() - } - }} - role='button' - tabIndex={0} - aria-label={ - isConnectionsAtMinHeight ? 'Expand connections' : 'Collapse connections' + className={ + 'connections-section flex flex-shrink-0 flex-col overflow-hidden border-[var(--border)] border-t' + + (!isResizing ? ' transition-[height] duration-100 ease-out' : '') } + style={{ height: `${connectionsHeight}px` }} > - +
+
+ + {/* Connections Header with Chevron */} +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + toggleConnectionsCollapsed() + } + }} + role='button' + tabIndex={0} + aria-label={ + isConnectionsAtMinHeight ? 'Expand connections' : 'Collapse connections' } - /> -
Connections
-
+ > + +
+ Connections +
+
- {/* Connections Content - Always visible */} -
- + {/* Connections Content - Always visible */} +
+ +
-
- )} -
- )} -
+ )} +
+ )} +
+ ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider.tsx new file mode 100644 index 00000000000..2a6e99410cd --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider.tsx @@ -0,0 +1,38 @@ +'use client' + +import type React from 'react' +import { createContext, useContext } from 'react' +import type { ActiveSearchTarget } from '@/stores/panel/editor/store' + +const ActiveSearchTargetContext = createContext(null) + +interface ActiveSearchTargetProviderProps { + value: ActiveSearchTarget | null + children: React.ReactNode +} + +/** + * Provides the active workflow-search target to the panel editor sub-block tree. + * + * @remarks + * The editor provides the target scoped to the currently edited block. Components + * that project sub-block values into synthetic sub-blocks (e.g. tool-input params) + * re-provide a transformed target so nested inputs receive the rewritten + * `subBlockId`/`valuePath`. Outside any provider (e.g. preview), consumers see + * `null`, which disables search highlighting. + */ +export function ActiveSearchTargetProvider({ value, children }: ActiveSearchTargetProviderProps) { + return ( + + {children} + + ) +} + +/** + * Returns the active workflow-search target for the nearest editor scope, or + * `null` when no search target applies (no provider, or target outside scope). + */ +export function useActiveSearchTarget(): ActiveSearchTarget | null { + return useContext(ActiveSearchTargetContext) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx index 75020e50eca..4a5d9c78084 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx @@ -18,7 +18,7 @@ import { useFolderExpand, useItemDrag, useItemRename, - useSidebarDragContext, + useSidebarListContext, } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' import { SIDEBAR_SCROLL_EVENT } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar' import { @@ -51,25 +51,11 @@ const logger = createLogger('FolderItem') interface FolderItemProps { folder: FolderTreeNode - dragDisabled?: boolean - hoverHandlers?: { - onDragEnter?: (e: React.DragEvent) => void - onDragLeave?: (e: React.DragEvent) => void - } - onFolderClick?: (folderId: string, shiftKey: boolean, metaKey: boolean) => void - onDragStart?: () => void - onDragEnd?: () => void } -export function FolderItem({ - folder, - dragDisabled = false, - hoverHandlers, - onFolderClick, - onDragStart: onDragStartProp, - onDragEnd: onDragEndProp, -}: FolderItemProps) { - const { isAnyDragActive } = useSidebarDragContext() +export function FolderItem({ folder }: FolderItemProps) { + const { isAnyDragActive, dragDisabled, onFolderClick, onItemDragStart, onItemDragEnd } = + useSidebarListContext() const params = useParams() const router = useRouter() const workspaceId = params.workspaceId as string @@ -234,9 +220,9 @@ export function FolderItem({ e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2) dragGhostRef.current = ghost - onDragStartProp?.() + onItemDragStart(folder.parentId) }, - [folder.id, folder.name, workspaceId, onDragStartProp] + [folder.id, folder.name, folder.parentId, workspaceId, onItemDragStart] ) const { @@ -254,8 +240,8 @@ export function FolderItem({ dragGhostRef.current = null } handleDragEndBase() - onDragEndProp?.() - }, [handleDragEndBase, onDragEndProp]) + onItemDragEnd() + }, [handleDragEndBase, onItemDragEnd]) const { isOpen: isContextMenuOpen, @@ -362,7 +348,7 @@ export function FolderItem({ const isModifierClick = e.shiftKey || e.metaKey || e.ctrlKey - if (isModifierClick && onFolderClick) { + if (isModifierClick) { e.preventDefault() onFolderClick(folder.id, e.shiftKey, e.metaKey || e.ctrlKey) return @@ -496,7 +482,6 @@ export function FolderItem({ draggable={!isEditing && !dragDisabled && !effectiveLocked} onDragStart={handleDragStart} onDragEnd={handleDragEnd} - {...hoverHandlers} > void - onDragStart?: () => void - onDragEnd?: () => void } /** * WorkflowItem component displaying a single workflow with drag and selection support. - * Uses the item drag hook for unified drag behavior. + * Selection and drag callbacks come from the sidebar list context; uses the item drag + * hook for unified drag behavior. * * @param props - Component props * @returns Workflow item with drag and selection support */ -export function WorkflowItem({ - workflow, - active, - dragDisabled = false, - onWorkflowClick, - onDragStart: onDragStartProp, - onDragEnd: onDragEndProp, -}: WorkflowItemProps) { - const { isAnyDragActive } = useSidebarDragContext() +export function WorkflowItem({ workflow, active }: WorkflowItemProps) { + const { isAnyDragActive, dragDisabled, onWorkflowClick, onItemDragStart, onItemDragEnd } = + useSidebarListContext() const params = useParams() const workspaceId = params.workspaceId as string const selectedWorkflows = useFolderStore((state) => state.selectedWorkflows) @@ -345,9 +336,9 @@ export function WorkflowItem({ e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2) dragGhostRef.current = ghost - onDragStartProp?.() + onItemDragStart(workflow.folderId || null) }, - [workflow.id, workflow.name, workspaceId, onDragStartProp] + [workflow.id, workflow.name, workflow.folderId, workspaceId, onItemDragStart] ) const { @@ -365,8 +356,8 @@ export function WorkflowItem({ dragGhostRef.current = null } handleDragEndBase() - onDragEndProp?.() - }, [handleDragEndBase, onDragEndProp]) + onItemDragEnd() + }, [handleDragEndBase, onItemDragEnd]) const handleDoubleClick = useCallback( (e: React.MouseEvent) => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx index 0c0e112e764..5ace96b0459 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx @@ -8,11 +8,11 @@ import { EmptyAreaContextMenu } from '@/app/workspace/[workspaceId]/w/components import { FolderItem } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item' import { WorkflowItem } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item' import { - SidebarDragContext, + SidebarListContext, useContextMenu, useDragDrop, useFolderSelection, - useSidebarDragContextValue, + useSidebarListContextValue, useWorkflowSelection, } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' import { @@ -34,8 +34,6 @@ interface WorkflowListProps { regularWorkflows: WorkflowMetadata[] isLoading?: boolean canReorder?: boolean - handleFileChange: (event: React.ChangeEvent) => void - fileInputRef: React.RefObject scrollContainerRef: React.RefObject onCreateWorkflow?: () => void onCreateFolder?: () => void @@ -71,8 +69,6 @@ export const WorkflowList = memo(function WorkflowList({ regularWorkflows, isLoading = false, canReorder = true, - handleFileChange, - fileInputRef, scrollContainerRef, onCreateWorkflow, onCreateFolder, @@ -110,8 +106,6 @@ export const WorkflowList = memo(function WorkflowList({ handleDragEnd, } = useDragDrop({ disabled: !canReorder }) - const dragContextValue = useSidebarDragContextValue(isDragging) - useEffect(() => { if (scrollContainerRef.current) { setScrollContainer(scrollContainerRef.current) @@ -347,6 +341,15 @@ export const WorkflowList = memo(function WorkflowList({ folderDescendantIds, }) + const listContextValue = useSidebarListContextValue({ + isAnyDragActive: isDragging, + dragDisabled, + onWorkflowClick: handleWorkflowClick, + onFolderClick: handleFolderClick, + onItemDragStart: handleDragStart, + onItemDragEnd: handleDragEnd, + }) + const isWorkflowActive = useCallback((wfId: string) => wfId === workflowId, [workflowId]) useEffect(() => { @@ -377,28 +380,13 @@ export const WorkflowList = memo(function WorkflowList({ style={{ paddingLeft: `${level * TREE_SPACING.INDENT_PER_LEVEL}px` }} {...createWorkflowDragHandlers(workflow.id, folderId)} > - handleDragStart(folderId)} - onDragEnd={handleDragEnd} - /> +
) }, - [ - dropIndicator, - isWorkflowActive, - dragDisabled, - createWorkflowDragHandlers, - handleWorkflowClick, - handleDragStart, - handleDragEnd, - ] + [dropIndicator, isWorkflowActive, createWorkflowDragHandlers] ) const renderFolderSection = useCallback( @@ -457,13 +445,7 @@ export const WorkflowList = memo(function WorkflowList({ style={{ paddingLeft: `${level * TREE_SPACING.INDENT_PER_LEVEL}px` }} {...createFolderDragHandlers(folder.id, parentFolderId)} > - handleDragStart(parentFolderId)} - onDragEnd={handleDragEnd} - /> +
@@ -493,13 +475,9 @@ export const WorkflowList = memo(function WorkflowList({ expandedFolders, dropIndicator, isDragging, - dragDisabled, createFolderDragHandlers, createEmptyFolderDropZone, createFolderContentDropZone, - handleDragStart, - handleDragEnd, - handleFolderClick, renderWorkflowItem, ] ) @@ -565,7 +543,7 @@ export const WorkflowList = memo(function WorkflowList({ ) return ( - +
)}
- -
{onCreateWorkflow && onCreateFolder && ( @@ -635,6 +604,6 @@ export const WorkflowList = memo(function WorkflowList({ disableCreateFolder={disableCreate} /> )} - + ) }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts index 58c9f83092a..a9f5875e5c4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts @@ -10,10 +10,10 @@ export { useHoverMenu } from './use-hover-menu' export { useItemDrag } from './use-item-drag' export { useItemRename } from './use-item-rename' export { - SidebarDragContext, - useSidebarDragContext, - useSidebarDragContextValue, -} from './use-sidebar-drag-context' + SidebarListContext, + useSidebarListContext, + useSidebarListContextValue, +} from './use-sidebar-list-context' export { useSidebarResize } from './use-sidebar-resize' export { useWorkflowOperations } from './use-workflow-operations' export { useWorkflowSelection } from './use-workflow-selection' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts index 185be69aa6a..c741f658930 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts @@ -31,6 +31,17 @@ type SiblingItem = { createdAt: Date } +/** Stable no-op drop-zone handlers returned when drag-and-drop is disabled. */ +const NOOP_DRAG_HANDLERS = { + onDragOver: (e: React.DragEvent) => e.preventDefault(), + onDrop: (e: React.DragEvent) => e.preventDefault(), + onDragLeave: () => {}, +} + +const createNoopDragHandlers = () => NOOP_DRAG_HANDLERS + +const noop = () => {} + /** Root folder vs root workflow scope: API/cache may use null or undefined for "no parent". */ function isSameFolderScope( parentOrFolderId: string | null | undefined, @@ -650,26 +661,20 @@ export function useDragDrop(options: UseDragDropOptions = {}) { scrollContainerRef.current = element }, []) - const noopDragHandlers = { - onDragOver: (e: React.DragEvent) => e.preventDefault(), - onDrop: (e: React.DragEvent) => e.preventDefault(), - onDragLeave: () => {}, - } - if (disabled) { return { dropIndicator: null, isDragging: false, disabled: true, setScrollContainer, - createWorkflowDragHandlers: () => noopDragHandlers, - createFolderDragHandlers: () => noopDragHandlers, - createEmptyFolderDropZone: () => noopDragHandlers, - createFolderContentDropZone: () => noopDragHandlers, - createRootDropZone: () => noopDragHandlers, - createEdgeDropZone: () => noopDragHandlers, - handleDragStart: () => {}, - handleDragEnd: () => {}, + createWorkflowDragHandlers: createNoopDragHandlers, + createFolderDragHandlers: createNoopDragHandlers, + createEmptyFolderDropZone: createNoopDragHandlers, + createFolderContentDropZone: createNoopDragHandlers, + createRootDropZone: createNoopDragHandlers, + createEdgeDropZone: createNoopDragHandlers, + handleDragStart: noop, + handleDragEnd: noop, } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-sidebar-drag-context.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-sidebar-drag-context.ts deleted file mode 100644 index 0fe5aee3ba1..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-sidebar-drag-context.ts +++ /dev/null @@ -1,36 +0,0 @@ -'use client' - -import { createContext, useContext, useMemo } from 'react' - -interface SidebarDragContextValue { - /** Whether any drag operation is currently in progress */ - isAnyDragActive: boolean -} - -/** - * Context for sharing drag state across sidebar components. - * Eliminates prop drilling of isAnyDragActive through component tree. - */ -export const SidebarDragContext = createContext({ - isAnyDragActive: false, -}) - -/** - * Hook to access the sidebar drag state. - * Use this in WorkflowItem, FolderItem, etc. to check if any drag is in progress. - * - * @returns The current drag state - */ -export function useSidebarDragContext(): SidebarDragContextValue { - return useContext(SidebarDragContext) -} - -/** - * Hook to create the sidebar drag context value. - * - * @param isDragging - Whether a drag is currently in progress - * @returns Context value to provide to SidebarDragContext.Provider - */ -export function useSidebarDragContextValue(isDragging: boolean): SidebarDragContextValue { - return useMemo(() => ({ isAnyDragActive: isDragging }), [isDragging]) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-sidebar-list-context.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-sidebar-list-context.ts new file mode 100644 index 00000000000..bb296c988ec --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-sidebar-list-context.ts @@ -0,0 +1,75 @@ +'use client' + +import { createContext, useContext, useMemo } from 'react' + +interface SidebarListContextValue { + /** Whether any drag operation is currently in progress */ + isAnyDragActive: boolean + /** Whether item dragging is disabled (e.g. viewer permissions) */ + dragDisabled: boolean + /** Selects a workflow on click (single or shift-range selection) */ + onWorkflowClick: (workflowId: string, shiftKey: boolean) => void + /** Selects a folder on modifier-click (shift-range or cmd/ctrl-toggle selection) */ + onFolderClick: (folderId: string, shiftKey: boolean, metaKey: boolean) => void + /** Notifies the list that an item drag started from the given parent folder */ + onItemDragStart: (parentFolderId: string | null) => void + /** Notifies the list that an item drag ended */ + onItemDragEnd: () => void +} + +const noop = () => {} + +/** + * Context for sharing list-item interaction handlers and drag state across + * sidebar workflow-list components. Eliminates prop drilling of selection + * and drag callbacks into WorkflowItem/FolderItem. + */ +export const SidebarListContext = createContext({ + isAnyDragActive: false, + dragDisabled: false, + onWorkflowClick: noop, + onFolderClick: noop, + onItemDragStart: noop, + onItemDragEnd: noop, +}) + +/** + * Hook to access the sidebar list context. + * Use this in WorkflowItem, FolderItem, etc. for selection/drag callbacks and drag state. + * + * @returns The current sidebar list context value + */ +export function useSidebarListContext(): SidebarListContextValue { + return useContext(SidebarListContext) +} + +/** + * Hook to create a memoized sidebar list context value. + * + * @param value - The handlers and drag state to expose to list items + * @returns Memoized context value to provide to SidebarListContext.Provider + */ +export function useSidebarListContextValue( + value: SidebarListContextValue +): SidebarListContextValue { + const { + isAnyDragActive, + dragDisabled, + onWorkflowClick, + onFolderClick, + onItemDragStart, + onItemDragEnd, + } = value + + return useMemo( + () => ({ + isAnyDragActive, + dragDisabled, + onWorkflowClick, + onFolderClick, + onItemDragStart, + onItemDragEnd, + }), + [isAnyDragActive, dragDisabled, onWorkflowClick, onFolderClick, onItemDragStart, onItemDragEnd] + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index cd4308c6e32..f2b091b8945 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -1230,6 +1230,14 @@ export const Sidebar = memo(function Sidebar() { className='hidden' onChange={handleLogoFileChange} /> +