From a445727851ae058981d8fe239d7263afcdb9bad0 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 13 Oct 2026 15:07:47 -0700 Subject: [PATCH 1/2] improvement(workflow): scope the editor by props instead of the URL The workflow editor read workspaceId/workflowId from useParams() all over its tree, so it only worked mounted at /workspace/[workspaceId]/w/[workflowId]. - WorkflowScopeProvider + useWorkflowScope/useScopedWorkflowId supply the ids; takes them as required props and the route page passes them in. - Previews (Preview, PreviewWorkflow, PreviewEditor) scope SubBlocks to the workspace with no workflow; /w renders Panel under a null-workflow scope. - The socket room is only what mounted editors request: Workflow always joins its workflowId and releases it on unmount; the route-param fallback is gone. - Editor navigation goes through useResourceRouter so a containing pane can intercept it. - useWand, SkillModal and the custom-tool generation hooks take workspaceId explicitly, since they also render outside the editor. Claude-Session: https://claude.ai/code/session_01UMjSAnE9M7r86o5P19ash5 --- .../use-custom-tool-generation.ts | 6 + .../resource/resource-navigation.tsx | 33 ++++ .../providers/workflow-scope-provider.tsx | 48 ++++++ .../custom-tool-detail/custom-tool-detail.tsx | 2 + .../components/skill-modal/skill-modal.tsx | 13 +- .../components/command-list/command-list.tsx | 13 +- .../connection-block-selector.tsx | 10 +- .../note-block/use-note-image-upload.ts | 5 +- .../general/components/api-info-modal.tsx | 4 +- .../components/version-description-modal.tsx | 5 +- .../deploy-modal/components/mcp/mcp.tsx | 5 +- .../components/deploy-modal/deploy-modal.tsx | 5 +- .../panel/components/deploy/deploy.tsx | 5 +- .../sub-block/components/code/code.test.tsx | 20 ++- .../sub-block/components/code/code.tsx | 6 +- .../condition-input/condition-input.tsx | 5 +- .../credential-selector.tsx | 5 +- .../components/file-upload/file-upload.tsx | 5 +- .../knowledge-base-selector.tsx | 5 +- .../components/long-input/long-input.test.tsx | 5 +- .../components/long-input/long-input.tsx | 3 + .../mcp-dynamic-args/mcp-dynamic-args.tsx | 5 +- .../mcp-server-modal/mcp-server-selector.tsx | 5 +- .../mcp-server-modal/mcp-tool-selector.tsx | 5 +- .../messages-input/messages-input.tsx | 3 + .../model-fallback-list.test.tsx | 15 +- .../model-fallback-list.tsx | 5 +- .../schedule-info/schedule-info.tsx | 9 +- .../components/short-input/short-input.tsx | 3 + .../components/skill-input/skill-input.tsx | 6 +- .../table-selector/table-selector.tsx | 5 +- .../sub-block/components/table/table.test.tsx | 20 ++- .../sub-block/components/table/table.tsx | 5 +- .../custom-tool-modal/custom-tool-modal.tsx | 7 +- .../components/tool-input/tool-input.tsx | 6 +- .../variables-input/variables-input.tsx | 5 +- .../workflow-selector-input.tsx | 4 +- .../workspace-folder-selector.tsx | 5 +- .../sub-block/hooks/use-selector-setup.ts | 8 +- .../sub-block/hooks/use-sub-block-input.ts | 5 +- .../sub-block/sub-block-password.test.tsx | 12 +- .../editor/components/sub-block/sub-block.tsx | 5 +- .../panel/components/editor/editor.tsx | 7 +- .../editor/hooks/use-subflow-editor.ts | 5 +- .../panel/components/toolbar/toolbar.tsx | 6 +- .../w/[workflowId]/components/panel/panel.tsx | 11 +- .../workflow-search-replace.tsx | 12 +- .../workflow-block/workflow-block.tsx | 10 +- .../w/[workflowId]/hooks/use-wand.ts | 6 +- .../hooks/use-workflow-execution.ts | 4 +- .../[workspaceId]/w/[workflowId]/page.tsx | 9 +- .../[workspaceId]/w/[workflowId]/workflow.tsx | 43 +++-- .../preview-editor/preview-editor.tsx | 14 +- .../preview-workflow/preview-workflow.tsx | 19 ++- .../w/components/preview/preview-scope.tsx | 24 +++ .../w/components/preview/preview.tsx | 147 +++++++++--------- .../w/hooks/use-delete-workflow.ts | 4 +- .../app/workspace/[workspaceId]/w/page.tsx | 9 +- .../providers/socket-join-target.test.ts | 41 ++++- .../workspace/providers/socket-join-target.ts | 32 ++-- .../workspace/providers/socket-provider.tsx | 48 ++---- apps/sim/hooks/use-webhook-management.ts | 7 +- 62 files changed, 466 insertions(+), 343 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/components/resource/resource-navigation.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/providers/workflow-scope-provider.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/preview/preview-scope.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/use-custom-tool-generation.ts b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/use-custom-tool-generation.ts index 3ed9c0c98e2..0dad3aca38e 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/use-custom-tool-generation.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/use-custom-tool-generation.ts @@ -121,6 +121,7 @@ try { } interface UseSchemaGenerationParams { + workspaceId: string jsonSchema: string setJsonSchema: (updater: (prev: string) => string) => void replaceJsonSchema: (value: string) => void @@ -128,11 +129,13 @@ interface UseSchemaGenerationParams { /** Wand-driven generation for a custom tool's JSON schema. */ export function useSchemaGeneration({ + workspaceId, jsonSchema, setJsonSchema, replaceJsonSchema, }: UseSchemaGenerationParams) { return useWand({ + workspaceId, wandConfig: { enabled: true, maintainHistory: true, @@ -148,6 +151,7 @@ export function useSchemaGeneration({ } interface UseCodeGenerationParams { + workspaceId: string functionCode: string schemaParameters: SchemaParameter[] setFunctionCode: (updater: (prev: string) => string) => void @@ -156,6 +160,7 @@ interface UseCodeGenerationParams { /** Wand-driven generation for a custom tool's function body, aware of the schema's parameters. */ export function useCodeGeneration({ + workspaceId, functionCode, schemaParameters, setFunctionCode, @@ -178,6 +183,7 @@ export function useCodeGeneration({ }, [schemaParameters]) return useWand({ + workspaceId, wandConfig: { enabled: true, maintainHistory: true, diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/resource-navigation.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/resource-navigation.tsx new file mode 100644 index 00000000000..812b4230f96 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/resource-navigation.tsx @@ -0,0 +1,33 @@ +'use client' + +import { createContext, useCallback, useContext, useMemo } from 'react' +import { useRouter } from 'next/navigation' + +interface ResourceNavigation { + /** Handles workspace destinations within a containing resource pane. */ + navigate: (href: string, replace?: boolean) => void +} + +/** Provided by a containing resource pane; absent on the routed pages. */ +const ResourceNavigationContext = createContext(null) + +/** Navigates the route, or the containing resource pane when one provides navigation. */ +export function useResourceRouter() { + const router = useRouter() + const navigation = useContext(ResourceNavigationContext) + const push = useCallback( + (href: string) => { + if (navigation) navigation.navigate(href) + else router.push(href) + }, + [navigation, router] + ) + const replace = useCallback( + (href: string) => { + if (navigation) navigation.navigate(href, true) + else router.replace(href) + }, + [navigation, router] + ) + return useMemo(() => ({ ...router, push, replace }), [router, push, replace]) +} diff --git a/apps/sim/app/workspace/[workspaceId]/providers/workflow-scope-provider.tsx b/apps/sim/app/workspace/[workspaceId]/providers/workflow-scope-provider.tsx new file mode 100644 index 00000000000..6f14fe3433d --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/providers/workflow-scope-provider.tsx @@ -0,0 +1,48 @@ +'use client' + +import { createContext, type ReactNode, useContext, useMemo } from 'react' + +interface WorkflowScope { + workspaceId: string + /** Null in read-only previews, which render editor sub-blocks outside an open workflow. */ + workflowId: string | null +} + +const WorkflowScopeContext = createContext(null) + +interface WorkflowScopeProviderProps extends WorkflowScope { + children: ReactNode +} + +/** + * Supplies the workspace and workflow ids to the editor tree, so the editor + * works wherever it is mounted instead of reading them from the route. + */ +export function WorkflowScopeProvider({ + workspaceId, + workflowId, + children, +}: WorkflowScopeProviderProps) { + if (!workspaceId) { + throw new Error('WorkflowScopeProvider requires a workspaceId') + } + const value = useMemo(() => ({ workspaceId, workflowId }), [workspaceId, workflowId]) + return {children} +} + +export function useWorkflowScope(): WorkflowScope { + const scope = useContext(WorkflowScopeContext) + if (!scope) { + throw new Error('useWorkflowScope must be used within a WorkflowScopeProvider') + } + return scope +} + +/** For editor-only components: throws in a preview scope, which has no workflow. */ +export function useScopedWorkflowId(): string { + const { workflowId } = useWorkflowScope() + if (!workflowId) { + throw new Error('useScopedWorkflowId must be used within an editor workflow scope') + } + return workflowId +} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/components/custom-tool-detail/custom-tool-detail.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/components/custom-tool-detail/custom-tool-detail.tsx index ccce42cda14..fe58c46f96f 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/components/custom-tool-detail/custom-tool-detail.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/components/custom-tool-detail/custom-tool-detail.tsx @@ -88,6 +88,7 @@ export function CustomToolDetail({ const identity = useMemo(() => extractSchemaIdentity(jsonSchema), [jsonSchema]) const schemaGeneration = useSchemaGeneration({ + workspaceId, jsonSchema, setJsonSchema: (updater) => { setJsonSchema(updater) @@ -100,6 +101,7 @@ export function CustomToolDetail({ }) const codeGeneration = useCodeGeneration({ + workspaceId, functionCode, schemaParameters, setFunctionCode: (updater) => { diff --git a/apps/sim/app/workspace/[workspaceId]/skills/components/skill-modal/skill-modal.tsx b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-modal/skill-modal.tsx index 12e97e2d71c..7bd9297b24c 100644 --- a/apps/sim/app/workspace/[workspaceId]/skills/components/skill-modal/skill-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-modal/skill-modal.tsx @@ -14,7 +14,6 @@ import { } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' import dynamic from 'next/dynamic' -import { useParams } from 'next/navigation' import { SKILL_CONTENT_PLACEHOLDER, SKILL_DESCRIPTION_MAX_LENGTH, @@ -43,6 +42,7 @@ const RichMarkdownField = dynamic( ) interface SkillModalProps { + workspaceId: string open: boolean onOpenChange: (open: boolean) => void onSave: () => void @@ -63,10 +63,13 @@ const CREATE_TABS = [ { value: 'import', label: 'Import' }, ] as const -export function SkillModal({ open, onOpenChange, onSave, initialValues }: SkillModalProps) { - const params = useParams() - const workspaceId = params.workspaceId as string - +export function SkillModal({ + workspaceId, + open, + onOpenChange, + onSave, + initialValues, +}: SkillModalProps) { const createSkill = useCreateSkill() const updateSkill = useUpdateSkill() diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx index 3d2e7e8b88a..ff1936ee51e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx @@ -5,8 +5,9 @@ import { Button, cn, handleKeyboardActivation, Library } from '@sim/emcn' import { Search } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import Image from 'next/image' -import { useParams, useRouter } from 'next/navigation' import { AgentIcon } from '@/components/icons' +import { useResourceRouter } from '@/app/workspace/[workspaceId]/components/resource/resource-navigation' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' import { useSearchModalStore } from '@/stores/modals/search/store' @@ -50,13 +51,11 @@ const commands: CommandItem[] = [ * Centered on the screen for empty workflows */ export function CommandList() { - const params = useParams() - const router = useRouter() + const { workspaceId } = useWorkflowScope() + const router = useResourceRouter() const openSearchModal = useSearchModalStore((s) => s.open) const preventZoomRef = usePreventZoom() - const workspaceId = params.workspaceId as string | undefined - /** * Handle click on a command row. * @@ -79,10 +78,6 @@ export function CommandList() { return } case 'Logs': { - if (!workspaceId) { - logger.warn('No workspace ID found, cannot navigate to logs from command list') - return - } router.push(`/workspace/${workspaceId}/logs`) return } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/connection-block-selector/connection-block-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/connection-block-selector/connection-block-selector.tsx index b12086f7a27..0c22d1f2137 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/connection-block-selector/connection-block-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/connection-block-selector/connection-block-selector.tsx @@ -14,9 +14,12 @@ import { X } from '@sim/emcn/icons' import { WorkflowBlockBorder, type WorkflowBorderPort } from '@sim/workflow-renderer' import { Handle, type Node, type NodeProps, Position } from '@xyflow/react' import { Command } from 'cmdk' -import { useParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { captureEvent } from '@/lib/posthog/client' +import { + useScopedWorkflowId, + useWorkflowScope, +} from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { CommandFadedList, CommandSearch, @@ -150,9 +153,8 @@ function isRecentSelection(value: unknown): value is RecentSelection { * temporary edge reads as connected to the block that will replace it. */ export function ConnectionBlockSelector({ id, data }: NodeProps) { - const params = useParams() - const workspaceId = params.workspaceId as string - const currentWorkflowId = params.workflowId as string | undefined + const { workspaceId } = useWorkflowScope() + const currentWorkflowId = useScopedWorkflowId() const posthog = usePostHog() const inputRef = useRef(null) const listRef = useRef(null) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/use-note-image-upload.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/use-note-image-upload.ts index 46bcf9dafa6..0616c20695e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/use-note-image-upload.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/use-note-image-upload.ts @@ -1,7 +1,7 @@ 'use client' import { useCallback } from 'react' -import { useParams } from 'next/navigation' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { useUploadWorkspaceFile } from '@/hooks/queries/workspace-files' /** @@ -19,8 +19,7 @@ import { useUploadWorkspaceFile } from '@/hooks/queries/workspace-files' * flow and the canvas context menu's "Add image". */ export function useNoteImageUpload(): (file: File) => Promise<{ url: string; alt: string } | null> { - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const uploadFile = useUploadWorkspaceFile() return useCallback( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/api-info-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/api-info-modal.tsx index 66b018a4def..e3ed534326f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/api-info-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/api-info-modal.tsx @@ -16,11 +16,11 @@ import { Label, } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' -import { useParams } from 'next/navigation' import { getMeaningfulWorkflowDescription } from '@/lib/mcp/workflow-tool-schema' import { normalizeInputFormatValue } from '@/lib/workflows/input-format' import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers' import type { InputFormatField } from '@/lib/workflows/types' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { useDeploymentInfo, useUpdatePublicApi } from '@/hooks/queries/deployments' import { useUpdateWorkflow, useWorkflowMap } from '@/hooks/queries/workflows' import { usePermissionConfig } from '@/hooks/use-permission-config' @@ -37,7 +37,7 @@ interface ApiInfoModalProps { } export function ApiInfoModal({ open, onOpenChange, workflowId }: ApiInfoModalProps) { - const { workspaceId } = useParams<{ workspaceId: string }>() + const { workspaceId } = useWorkflowScope() const blocks = useWorkflowStore((state) => state.blocks) const setValue = useSubBlockStore((state) => state.setValue) const subBlockValues = useSubBlockStore( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/version-description-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/version-description-modal.tsx index 18a2f6998bb..e11fc35fe55 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/version-description-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/version-description-modal.tsx @@ -13,7 +13,7 @@ import { cn, } from '@sim/emcn' import dynamic from 'next/dynamic' -import { useParams } from 'next/navigation' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { useGenerateVersionDescription, useUpdateDeploymentVersion, @@ -50,8 +50,7 @@ export function VersionDescriptionModal({ versionName, currentDescription, }: VersionDescriptionModalProps) { - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const initialDescriptionRef = useRef(currentDescription || '') const [description, setDescription] = useState(initialDescriptionRef.current) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx index 85eb10ed9dd..226ea8f03f8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx @@ -13,7 +13,6 @@ import { Textarea, } from '@sim/emcn' import { createLogger } from '@sim/logger' -import { useParams } from 'next/navigation' import { ApiClientError } from '@/lib/api/client/errors' import { extractDescriptionOverrides, @@ -23,6 +22,7 @@ import { sanitizeToolName, } from '@/lib/mcp/workflow-tool-schema' import type { InputFormatField } from '@/lib/workflows/types' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { CreateWorkflowMcpServerModal } from '@/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/components/create-workflow-mcp-server-modal' import { useAddWorkflowMcpTool, @@ -132,8 +132,7 @@ export function McpDeploy({ onSaveDisabledReasonChange, onActiveServerChange, }: McpDeployProps) { - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const [showCreateModal, setShowCreateModal] = useState(false) const { data: servers = [], isLoading: isLoadingServers } = useWorkflowMcpServers(workspaceId) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx index a88ac9fe855..5a2f8ccfac6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx @@ -18,10 +18,10 @@ import { import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { useQueryClient } from '@tanstack/react-query' -import { useParams } from 'next/navigation' import type { DeploymentOperationSummary } from '@/lib/api/contracts/deployments' import { getBaseUrl } from '@/lib/core/utils/urls' import { getInputFormatExample as getInputFormatExampleUtil } from '@/lib/workflows/operations/deployment-utils' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { CreateApiKeyModal } from '@/app/workspace/[workspaceId]/settings/components/api-keys/components' import { @@ -104,8 +104,7 @@ export function DeployModal({ /* A snapshot that is expected but absent reads as loading everywhere. */ const isLoadingDeployedState = isAwaitingSnapshot const queryClient = useQueryClient() - const params = useParams() - const workspaceId = params?.workspaceId as string + const { workspaceId } = useWorkflowScope() const { navigateToSettings } = useSettingsNavigation() const isDeployed = isDeployedProp const { data: workflowMap = {} } = useWorkflowMap(workspaceId) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx index caa9c7a88ce..42fdf27e0d5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx @@ -3,8 +3,8 @@ import { useState } from 'react' import { Chip, Tooltip, toast } from '@sim/emcn' import { useQueryClient } from '@tanstack/react-query' -import { useParams } from 'next/navigation' import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { DeployModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal' import { useDeployment, @@ -26,8 +26,7 @@ interface DeployProps { export function Deploy({ activeWorkflowId, userPermissions, disabled = false }: DeployProps) { const queryClient = useQueryClient() - const params = useParams() - const workspaceId = params.workspaceId as string | undefined + const { workspaceId } = useWorkflowScope() const [isModalOpen, setIsModalOpen] = useState(false) const hydrationPhase = useWorkflowRegistry((state) => state.hydration.phase) const isRegistryLoading = hydrationPhase === 'idle' || hydrationPhase === 'state-loading' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.test.tsx index ce0b7338e72..4b56d89efac 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.test.tsx @@ -4,7 +4,6 @@ import { act, type ReactNode } from 'react' import { emcnIconsMock } from '@sim/testing/mocks/emcn-icons.mock' -import { nextNavigationMock, nextNavigationMockFns } from '@sim/testing/mocks/next-navigation.mock' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -79,8 +78,6 @@ vi.mock('react-simple-code-editor', () => ({ ), })) -vi.mock('next/navigation', () => nextNavigationMock) - vi.mock( '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown', () => ({ @@ -161,10 +158,9 @@ vi.mock('@/stores/workflows/workflow/store', () => ({ selector({ blocks: {} }), })) +import { WorkflowScopeProvider } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { Code } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code' -nextNavigationMockFns.mockUseParams.mockReturnValue({ workspaceId: 'workspace-1' }) - let container: HTMLDivElement let root: Root @@ -183,12 +179,14 @@ afterEach(() => { function mount(password: boolean) { act(() => { root.render( - + + + ) }) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.tsx index 4fc1e9f94b8..69e746c2d38 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.tsx @@ -13,7 +13,6 @@ import { } from '@sim/emcn' import { Check } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' -import { useParams } from 'next/navigation' import Editor from 'react-simple-code-editor' import { CodeLanguage } from '@/lib/execution/languages' import { @@ -21,6 +20,7 @@ import { SYSTEM_REFERENCE_PREFIXES, splitReferenceSegment, } from '@/lib/workflows/sanitization/references' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { WORKFLOW_SEARCH_HIGHLIGHT_CLASS } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/constants' import { checkEnvVarTrigger, @@ -301,8 +301,7 @@ export const Code = memo(function Code({ hideInternalWand = false, }: CodeProps) { const activeSearchTarget = useActiveSearchTarget() - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const [code, setCode] = useState('') const [showTags, setShowTags] = useState(false) @@ -404,6 +403,7 @@ export const Code = memo(function Code({ const [sandboxIdValue] = useSubBlockValue(blockId, 'sandboxId') const wandHook = useWand({ + workspaceId, wandConfig: dynamicWandConfig || { enabled: false, prompt: '' }, currentValue: code, contextParams: { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/condition-input/condition-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/condition-input/condition-input.tsx index a92f6b04e41..d924c62af3c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/condition-input/condition-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/condition-input/condition-input.tsx @@ -13,7 +13,6 @@ import { } from '@sim/emcn' import { ChevronDown, ChevronsUpDown, ChevronUp, Plus, Trash } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' -import { useParams } from 'next/navigation' import Editor from 'react-simple-code-editor' import { isElseConditionTitle } from '@/lib/workflows/conditions' import { @@ -21,6 +20,7 @@ import { SYSTEM_REFERENCE_PREFIXES, splitReferenceSegment, } from '@/lib/workflows/sanitization/references' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { WORKFLOW_SEARCH_HIGHLIGHT_CLASS } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/constants' import { checkEnvVarTrigger, @@ -136,8 +136,7 @@ export function ConditionInput({ }: ConditionInputProps) { const activeSearchTarget = useActiveSearchTarget() const isRouterMode = mode === 'router' - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const emitTagSelection = useTagSelection(blockId, subBlockId) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx index afd2aecc681..5d65eb013a5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx @@ -3,7 +3,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { Chip, Combobox, type ComboboxOptionGroup } from '@sim/emcn' import { Key, SquareArrowUpRight } from '@sim/emcn/icons' -import { useParams } from 'next/navigation' import { consumeOAuthReturnContext, writeOAuthReturnContext } from '@/lib/credentials/client-state' import { getCanonicalScopesForProvider, @@ -19,6 +18,7 @@ import { type ServiceAccountProviderId, useServiceAccountConnectTarget, } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { resolveMicrosoftDataverseCredentialPolicy } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/microsoft-dataverse-policy' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' @@ -50,8 +50,7 @@ export function CredentialSelector({ previewContextValues, }: CredentialSelectorProps) { const activeSearchTarget = useActiveSearchTarget() - const params = useParams() - const workspaceId = (params?.workspaceId as string) || '' + const { workspaceId } = useWorkflowScope() const [showConnectModal, setShowConnectModal] = useState(false) const [showOAuthModal, setShowOAuthModal] = useState(false) const [showSetupModal, setShowSetupModal] = useState(false) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx index 44065aa2c4f..4db94709dab 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx @@ -8,7 +8,6 @@ import { getErrorMessage } from '@sim/utils/errors' import { generateShortId } from '@sim/utils/id' import { randomFloat } from '@sim/utils/random' import { useQueryClient } from '@tanstack/react-query' -import { useParams } from 'next/navigation' import { Progress } from '@/components/ui/progress' import { isApiClientError } from '@/lib/api/client/errors' import { requestJson } from '@/lib/api/client/request' @@ -20,6 +19,7 @@ import { containsReference } from '@/lib/workflows/sanitization/references' import { parseWorkspaceFileFolderDisplayPath } from '@/lib/workspace-files/folder-display-path' import { isFileInFolderScope } from '@/lib/workspace-files/folder-path-selection' import { findSelectedWorkspaceFile } from '@/lib/workspace-files/selection' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' import { useActiveCanonicalSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-canonical-sub-block-value' @@ -272,8 +272,7 @@ export function FileUpload({ const fileInputRef = useRef(null) const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) - const params = useParams() - const workspaceId = params?.workspaceId as string + const { workspaceId } = useWorkflowScope() const { data: workspaceFiles = [], diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx index e7d794bf22b..9848d3a78ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx @@ -4,9 +4,9 @@ import { useCallback, useMemo } from 'react' import { ChipTag, Combobox, type ComboboxOption } from '@sim/emcn' import { X } from '@sim/emcn/icons' import { useQueries } from '@tanstack/react-query' -import { useParams } from 'next/navigation' import { PackageSearchIcon } from '@/components/icons' import type { KnowledgeBaseData } from '@/lib/knowledge/types' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' @@ -36,8 +36,7 @@ export function KnowledgeBaseSelector({ previewValue, }: KnowledgeBaseSelectorProps) { const activeSearchTarget = useActiveSearchTarget() - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const { knowledgeBases, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input.test.tsx index d27257adfc8..ee50c40df88 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input.test.tsx @@ -89,6 +89,7 @@ vi.mock('@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand', () => ({ }), })) +import { WorkflowScopeProvider } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { LongInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input' import type { SubBlockConfig } from '@/blocks/types' @@ -100,7 +101,9 @@ const MATCH_START = SECRET.indexOf(SECRET_MATCH) function render(password: boolean) { return renderToStaticMarkup( - + + + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input.tsx index e25ac99312f..6fa9f1f78e0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input.tsx @@ -10,6 +10,7 @@ import { import { cn, Textarea } from '@sim/emcn' import { ChevronsUpDown } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { maskSecretText, @@ -111,7 +112,9 @@ export function LongInput({ const persistSubBlockValueRef = useRef<(value: string) => void>(() => {}) // Wand functionality - always call the hook unconditionally + const { workspaceId } = useWorkflowScope() const wandHook = useWand({ + workspaceId, wandConfig: config.wandConfig, currentValue: localContent, onStreamStart: () => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx index cd8420a9600..e60297ab42d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react' import { Combobox, FieldDivider, Label, Slider, Switch } from '@sim/emcn' import { createLogger } from '@sim/logger' -import { useParams } from 'next/navigation' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { LongInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input' import { ShortInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input' @@ -108,8 +108,7 @@ export function McpDynamicArgs({ previewContextValues, }: McpDynamicArgsProps) { const activeSearchTarget = useActiveSearchTarget() - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const { mcpTools, isLoading } = useMcpTools(workspaceId) const [schemaFromStore] = useSubBlockValue(blockId, '_toolSchema') const { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-server-modal/mcp-server-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-server-modal/mcp-server-selector.tsx index 829e9a19866..7afcfa0475b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-server-modal/mcp-server-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-server-modal/mcp-server-selector.tsx @@ -1,10 +1,10 @@ 'use client' import { Combobox } from '@sim/emcn' -import { useParams } from 'next/navigation' import { McpIcon } from '@/components/icons' import { getMcpTargetOptions } from '@/components/mcp/target-options' import { getManagedMcpConnectorIcon } from '@/lib/credential-groups/managed-mcp-connector-icons' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' @@ -28,8 +28,7 @@ export function McpServerSelector({ previewValue, }: McpServerSelectorProps) { const activeSearchTarget = useActiveSearchTarget() - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const { data: servers = [], isLoading, error } = useMcpToolServers(workspaceId) const targetOptions = getMcpTargetOptions(servers) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-server-modal/mcp-tool-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-server-modal/mcp-tool-selector.tsx index fca7044a1cb..89bd0df225f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-server-modal/mcp-tool-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-server-modal/mcp-tool-selector.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react' import { Combobox } from '@sim/emcn' -import { useParams } from 'next/navigation' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' import { useMcpBlockConfig } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-mcp-block-config' @@ -29,8 +29,7 @@ export function McpToolSelector({ previewContextValues, }: McpToolSelectorProps) { const activeSearchTarget = useActiveSearchTarget() - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const { mcpTools, isLoading, error, refreshTools } = useMcpTools(workspaceId) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx index 4e5284575a6..3ae1d4372d5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx @@ -11,6 +11,7 @@ import { Button, cn, Popover, PopoverContent, PopoverItem, PopoverTrigger } from import { ChevronDown, ChevronsUpDown, ChevronUp, Plus, Trash } from '@sim/emcn/icons' import { generateShortId } from '@sim/utils/id' import { isEqual } from 'es-toolkit' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { EnvVarDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown' import { formatDisplayText } 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' @@ -191,7 +192,9 @@ export function MessagesInput({ /** * Wand hook for AI-assisted content generation */ + const { workspaceId } = useWorkflowScope() const wandHook = useWand({ + workspaceId, wandConfig: config.wandConfig, currentValue: getMessagesJson(), onStreamStart: () => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/model-fallback-list/model-fallback-list.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/model-fallback-list/model-fallback-list.test.tsx index 049a1170e21..2ffbc79a5a5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/model-fallback-list/model-fallback-list.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/model-fallback-list/model-fallback-list.test.tsx @@ -3,7 +3,6 @@ */ import { emcnIconsMock } from '@sim/testing/mocks/emcn-icons.mock' -import { nextNavigationMock, nextNavigationMockFns } from '@sim/testing/mocks/next-navigation.mock' import { renderToStaticMarkup } from 'react-dom/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { @@ -26,8 +25,6 @@ const { subBlockValues, mockSetValue } = vi.hoisted(() => ({ mockSetValue: vi.fn(), })) -vi.mock('next/navigation', () => nextNavigationMock) - vi.mock('@sim/emcn', () => ({ Button: ({ children, ...props }: React.ButtonHTMLAttributes) => ( @@ -130,10 +127,9 @@ vi.mock('@/lib/workflows/blocks/fallback-models', async (importOriginal) => { } }) +import { WorkflowScopeProvider } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { ModelFallbackList } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/model-fallback-list/model-fallback-list' -nextNavigationMockFns.mockUseParams.mockReturnValue({ workspaceId: 'workspace-1' }) - beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true) seedDeploymentShape({ ...resolveDeploymentShape(), hosted: true }) @@ -145,7 +141,14 @@ afterEach(() => { function render(extra: Partial> = {}) { return renderToStaticMarkup( - + + + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/model-fallback-list/model-fallback-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/model-fallback-list/model-fallback-list.tsx index 6ddf80cb533..bf4b66f65db 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/model-fallback-list/model-fallback-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/model-fallback-list/model-fallback-list.tsx @@ -4,7 +4,6 @@ import { memo, useCallback, useEffect, useMemo, useRef } from 'react' import { Button, Chip, Combobox, type ComboboxOption, Label, Tooltip } from '@sim/emcn' import { ChevronDown, ChevronUp, Plus, Trash } from '@sim/emcn/icons' import { generateShortId } from '@sim/utils/id' -import { useParams } from 'next/navigation' import { useDeploymentShape } from '@/lib/core/config/deployment-shape' import { writePendingCredentialCreateRequest } from '@/lib/credentials/client-state' import { @@ -25,6 +24,7 @@ import { ordinalChoiceLabel, removeFallbackRow, } from '@/lib/workflows/blocks/fallback-models' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { getModelOptions } from '@/blocks/utils' import { usePersonalEnvironment, useWorkspaceEnvironment } from '@/hooks/queries/environment' @@ -246,8 +246,7 @@ export function ModelFallbackList({ previewPrimary, disabled = false, }: ModelFallbackListProps) { - const params = useParams() - const workspaceId = typeof params?.workspaceId === 'string' ? params.workspaceId : '' + const { workspaceId } = useWorkflowScope() const { navigateToSettings } = useSettingsNavigation() const { isModelUsable } = usePermissionConfig() const deploymentShape = useDeploymentShape() diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/schedule-info/schedule-info.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/schedule-info/schedule-info.tsx index 1a704ad6a77..81957c2e8af 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/schedule-info/schedule-info.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/schedule-info/schedule-info.tsx @@ -1,6 +1,6 @@ import { Badge } from '@sim/emcn' -import { useParams } from 'next/navigation' import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { useRedeployWorkflowSchedule, useScheduleQuery } from '@/hooks/queries/schedules' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants' @@ -16,21 +16,20 @@ interface ScheduleInfoProps { * Schedule creation/deletion is handled during workflow deploy/undeploy. */ export function ScheduleInfo({ blockId, isPreview = false }: ScheduleInfoProps) { - const params = useParams() - const workflowId = params.workflowId as string + const { workflowId } = useWorkflowScope() const scheduleTimezone = useSubBlockStore((state) => state.getValue(blockId, 'timezone')) as | string | undefined - const { data: schedule, isLoading } = useScheduleQuery(workflowId, blockId, { + const { data: schedule, isLoading } = useScheduleQuery(workflowId ?? undefined, blockId, { enabled: !isPreview, }) const redeployMutation = useRedeployWorkflowSchedule() const handleRedeploy = () => { - if (isPreview || redeployMutation.isPending) return + if (isPreview || !workflowId || redeployMutation.isPending) return redeployMutation.mutate({ workflowId, blockId }) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx index c5ac7b4c69f..0b6de44d8ef 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx @@ -1,6 +1,7 @@ import { memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' import { cn, Input } from '@sim/emcn' import { useReactFlow } from '@xyflow/react' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { maskSecretText, @@ -102,7 +103,9 @@ export const ShortInput = memo(function ShortInput({ providerWebhookUrl: config.providerWebhookUrl, }) + const { workspaceId } = useWorkflowScope() const wandHook = useWand({ + workspaceId, wandConfig: config.wandConfig, currentValue: localContent, onStreamStart: () => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx index dea669b0d4e..3a8ace1714e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx @@ -3,8 +3,8 @@ import { useCallback, useMemo, useState } from 'react' import { Combobox, type ComboboxOptionGroup } from '@sim/emcn' import { Plus, X } from '@sim/emcn/icons' -import { useParams } from 'next/navigation' import { AgentSkillsIcon } from '@/components/icons' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { SkillModal } from '@/app/workspace/[workspaceId]/skills/components/skill-modal' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' @@ -34,8 +34,7 @@ export function SkillInput({ disabled, }: SkillInputProps) { const activeSearchTarget = useActiveSearchTarget() - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const { config: permissionConfig } = usePermissionConfig() const { data: workspaceSkills = [] } = useSkills(workspaceId) @@ -199,6 +198,7 @@ export function SkillInput({ { if (!isOpen) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table-selector/table-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table-selector/table-selector.tsx index 2a57b60aca1..a7c102ec67b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table-selector/table-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table-selector/table-selector.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react' import { Combobox, type ComboboxOption } from '@sim/emcn' -import { useParams } from 'next/navigation' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' @@ -36,8 +36,7 @@ export function TableSelector({ previewValue, }: TableSelectorProps) { const activeSearchTarget = useActiveSearchTarget() - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table/table.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table/table.test.tsx index 1db89ee4c34..2ee13cf9b2c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table/table.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table/table.test.tsx @@ -1,6 +1,5 @@ import { emcnMock } from '@sim/testing/mocks/emcn.mock' import { emcnIconsMock } from '@sim/testing/mocks/emcn-icons.mock' -import { nextNavigationMock, nextNavigationMockFns } from '@sim/testing/mocks/next-navigation.mock' import { renderToStaticMarkup } from 'react-dom/server' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -19,8 +18,6 @@ vi.mock('@sim/emcn', () => ({ vi.mock('@sim/emcn/icons', () => emcnIconsMock) -vi.mock('next/navigation', () => nextNavigationMock) - vi.mock( '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown', () => ({ EnvVarDropdown: () => null }) @@ -75,18 +72,19 @@ vi.mock( () => ({ useAccessibleReferencePrefixes: () => undefined }) ) +import { WorkflowScopeProvider } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { Table } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table' -nextNavigationMockFns.mockUseParams.mockReturnValue({ workspaceId: 'workspace-1' }) - function render(password: boolean) { return renderToStaticMarkup( - + +
+ ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table/table.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table/table.tsx index 0f6d8d18da0..a346edbaeba 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table/table.tsx @@ -3,7 +3,7 @@ import { Button, cn } from '@sim/emcn' import { Trash } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' -import { useParams } from 'next/navigation' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { EnvVarDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { @@ -275,8 +275,7 @@ export function Table({ disabled = false, }: TableProps) { const activeSearchTarget = useActiveSearchTarget() - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx index 10dc434c4ae..4421bfa4357 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx @@ -12,7 +12,6 @@ import { } from '@sim/emcn' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { useParams } from 'next/navigation' import { CUSTOM_TOOL_DELETE_CONFIRM_TEXT, CustomToolCodeField, @@ -25,6 +24,7 @@ import { useSchemaGeneration, validateCustomToolSchema, } from '@/app/workspace/[workspaceId]/components/custom-tool-editor' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { useCreateCustomTool, useCustomTools, @@ -74,8 +74,7 @@ export function CustomToolModal({ blockId, initialValues, }: CustomToolModalProps) { - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const [activeSection, setActiveSection] = useState('schema') const [jsonSchema, setJsonSchema] = useState('') const [functionCode, setFunctionCode] = useState('') @@ -91,6 +90,7 @@ export function CustomToolModal({ const schemaParameters = useMemo(() => extractSchemaParameters(jsonSchema), [jsonSchema]) const schemaGeneration = useSchemaGeneration({ + workspaceId, jsonSchema, setJsonSchema: (updater) => { setJsonSchema(updater) @@ -103,6 +103,7 @@ export function CustomToolModal({ }) const codeGeneration = useCodeGeneration({ + workspaceId, functionCode, schemaParameters, setFunctionCode: (updater) => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index bb82ec4e2d5..e21624dc885 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -16,7 +16,6 @@ import { } from '@sim/emcn' import { ArrowLeft, ChevronRight, Pencil, Server, Wrench, X } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' -import { useParams } from 'next/navigation' import { McpIcon, WorkflowIcon } from '@/components/icons' import { McpOperationPolicyEditor } from '@/components/mcp/operation-policy-editor' import { getManagedMcpConnectorIcon } from '@/lib/credential-groups/managed-mcp-connector-icons' @@ -50,6 +49,7 @@ import { getAgentToolUsageControlMode, resolveAgentToolUsageControl, } from '@/lib/workflows/tool-input/usage-control' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { McpServerFormModal } from '@/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' @@ -380,9 +380,7 @@ export const ToolInput = memo(function ToolInput({ disabled = false, allowExpandInPreview, }: ToolInputProps) { - const params = useParams() - const workspaceId = params.workspaceId as string - const workflowId = params.workflowId as string + const { workspaceId, workflowId } = useWorkflowScope() const activeSearchTarget = useActiveSearchTarget() const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const [open, setOpen] = useState(false) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx index bb9352bef92..2106661e8ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx @@ -13,7 +13,7 @@ import { } from '@sim/emcn' import { ArrowLeftRight, FileText, Plus, Trash } from '@sim/emcn/icons' import { generateId } from '@sim/utils/id' -import { useParams } from 'next/navigation' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { checkTagTrigger, @@ -97,8 +97,7 @@ export function VariablesInput({ disabled = false, }: VariablesInputProps) { const activeSearchTarget = useActiveSearchTarget() - const params = useParams() - const workflowId = params.workflowId as string + const { workflowId } = useWorkflowScope() const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const workflowVariables = useVariablesStore((s) => s.variables) const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-selector/workflow-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-selector/workflow-selector-input.tsx index 5a07598efcd..5b35c3d4cc4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-selector/workflow-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-selector/workflow-selector-input.tsx @@ -1,8 +1,8 @@ 'use client' import { useMemo } from 'react' -import { useParams } from 'next/navigation' import { DELETED_WORKFLOW_LABEL } from '@/lib/workflows/workflow-labels' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox' import type { SubBlockConfig } from '@/blocks/types' import type { SelectorClientContext } from '@/hooks/queries/selectors' @@ -23,7 +23,7 @@ export function WorkflowSelectorInput({ isPreview = false, previewValue, }: WorkflowSelectorInputProps) { - const { workspaceId } = useParams<{ workspaceId: string }>() + const { workspaceId } = useWorkflowScope() const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) const context: SelectorClientContext = useMemo( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workspace-folder-selector/workspace-folder-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workspace-folder-selector/workspace-folder-selector.tsx index bf16b037dca..3940bd26f10 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workspace-folder-selector/workspace-folder-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workspace-folder-selector/workspace-folder-selector.tsx @@ -2,10 +2,10 @@ import { useMemo } from 'react' import { Combobox } from '@sim/emcn' -import { useParams } from 'next/navigation' import type { FolderResourceType } from '@/lib/api/contracts/folders' import { parseFolderPath } from '@/lib/folders/paths' import { readFolderPath, readFolderPaths } from '@/lib/folders/selection' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { useResourceFolders } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-resource-folders' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import type { SubBlockConfig } from '@/blocks/types' @@ -35,8 +35,7 @@ export function WorkspaceFolderSelector({ isPreview = false, previewValue, }: WorkspaceFolderSelectorProps) { - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const resourceType = (subBlock.resourceType as FolderResourceType | undefined) ?? 'file' const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts index eab078ce61e..6818adf8a7c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts @@ -1,12 +1,12 @@ 'use client' import { useMemo } from 'react' -import { useParams } from 'next/navigation' import { buildSelectorContextFromValues, getSelectorContextSubBlocks, } from '@/lib/selectors/context' import type { SelectorKey } from '@/lib/selectors/manifest' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import type { SubBlockConfig } from '@/blocks/types' import type { SelectorClientContext } from '@/hooks/queries/selectors' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -32,10 +32,10 @@ export function useSelectorSetup( subBlock: SubBlockConfig, opts?: { disabled?: boolean; isPreview?: boolean; previewContextValues?: Record } ) { - const params = useParams() + const scope = useWorkflowScope() const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) - const workflowId = (params?.workflowId as string) || activeWorkflowId || '' - const workspaceId = (params?.workspaceId as string) || '' + const workflowId = scope.workflowId ?? activeWorkflowId ?? '' + const workspaceId = scope.workspaceId const { finalDisabled, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input.ts index af86a28197b..6ecc6fde738 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { useParams } from 'next/navigation' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { checkEnvVarTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown' import { checkTagTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' @@ -167,8 +167,7 @@ export function useSubBlockInput(options: UseSubBlockInputOptions): UseSubBlockI shouldForceTagDropdown, } = options - const params = useParams() - const workspaceId = (params?.workspaceId as string) ?? undefined + const { workspaceId } = useWorkflowScope() const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId, false, { isStreaming, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block-password.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block-password.test.tsx index cfbfebbdb30..93ed2fb85ea 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block-password.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block-password.test.tsx @@ -1,6 +1,5 @@ import type { ReactNode } from 'react' import { emcnIconsMock } from '@sim/testing/mocks/emcn-icons.mock' -import { nextNavigationMock, nextNavigationMockFns } from '@sim/testing/mocks/next-navigation.mock' import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it, vi } from 'vitest' @@ -28,8 +27,6 @@ vi.mock('@sim/emcn', () => ({ vi.mock('@sim/emcn/icons', () => emcnIconsMock) -vi.mock('next/navigation', () => nextNavigationMock) - vi.mock( '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components', () => ({ @@ -85,14 +82,17 @@ vi.mock('@/hooks/use-webhook-management', () => ({ useWebhookManagement: () => ({ webhookUrl: null }), })) +import { WorkflowScopeProvider } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { PASSWORD_MASKED_SUBBLOCK_TYPES } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/password-mask' import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block' import type { SubBlockConfig } from '@/blocks/types' -nextNavigationMockFns.mockUseParams.mockReturnValue({ workspaceId: 'workspace-1' }) - function renderSubBlock(config: SubBlockConfig) { - return renderToStaticMarkup() + return renderToStaticMarkup( + + + + ) } describe('SubBlock password forwarding', () => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index 4795928d1c6..247af54d085 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -9,9 +9,9 @@ import { TriangleAlert, } from '@sim/emcn/icons' import { isEqual } from 'es-toolkit' -import { useParams } from 'next/navigation' import type { FilterRule, SortRule } from '@/lib/table/query-builder/constants' import type { FallbackModelEntry } from '@/lib/workflows/blocks/fallback-models' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { CheckboxList, Code, @@ -473,8 +473,7 @@ function SubBlockComponent({ labelSuffix, dependencyContext, }: SubBlockProps): JSX.Element { - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const [isValidJson, setIsValidJson] = useState(true) const [isSearchActive, setIsSearchActive] = useState(false) 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 6a8f5fc45c3..272051ded90 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 @@ -18,7 +18,6 @@ import { isWorkflowBlockProtected, } from '@sim/workflow-types/workflow' import { isEqual } from 'es-toolkit' -import { useParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { useShallow } from 'zustand/react/shallow' import { useStoreWithEqualityFn } from 'zustand/traditional' @@ -35,6 +34,7 @@ import { isStandaloneAdvancedMode, resolveCanonicalMode, } from '@/lib/workflows/subblocks/visibility' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { ConnectionBlocks, @@ -121,15 +121,14 @@ export function Editor() { if (isNoteBlock) clearCurrentBlock() }, [clearCurrentBlock, isNoteBlock]) - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId, workflowId: scopedWorkflowId } = useWorkflowScope() const posthog = usePostHog() const subBlocksRef = useRef(null) const userPermissions = useUserPermissionsContext() const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) - const workflowId = activeWorkflowId ?? (params.workflowId as string | undefined) + const workflowId = activeWorkflowId ?? scopedWorkflowId ?? undefined const { data: workflows = {} } = useWorkflowMap(workspaceId) const { data: folders = {} } = useFolderMap(workspaceId) const workflowMetadata = workflowId ? workflows[workflowId] : undefined diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-subflow-editor.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-subflow-editor.ts index 3c65b9ee003..c7415593de1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-subflow-editor.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-subflow-editor.ts @@ -1,11 +1,11 @@ import { useCallback, useMemo, useRef, useState } from 'react' import { highlight, languages } from '@sim/emcn' -import { useParams } from 'next/navigation' import { isLikelyReferenceSegment, SYSTEM_REFERENCE_PREFIXES, splitReferenceSegment, } from '@/lib/workflows/sanitization/references' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { checkTagTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { restoreCursorAfterInsertion } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/utils' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' @@ -54,8 +54,7 @@ const SUBFLOW_CONFIG = { * @returns Subflow editor state and handlers */ export function useSubflowEditor(currentBlock: BlockState | null, currentBlockId: string | null) { - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const textareaRef = useRef(null) const editorContainerRef = useRef(null) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx index 09ede00b538..d299ca93c93 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx @@ -22,10 +22,10 @@ import { OverflowText, } from '@sim/emcn' import { ChevronDown, Lock, Search } from '@sim/emcn/icons' -import { useParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { captureEvent } from '@/lib/posthog/client' import { getTriggersForSidebar, hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { type DragItemInfo, ToolbarItemContextMenu, @@ -481,9 +481,7 @@ export const Toolbar = memo( const { handleDragStart, handleItemClick } = useToolbarItemInteractions() - const params = useParams() - const workspaceId = params?.workspaceId as string | undefined - const currentWorkflowId = params?.workflowId as string | undefined + const { workspaceId, workflowId: currentWorkflowId } = useWorkflowScope() const { data: customBlocksData } = useCustomBlocks(workspaceId) /** No-icon custom blocks use the access-authorized workspace host logo, then the glyph. */ const fallbackIconUrl = useOrgBrandConfig().logoUrl ?? null diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 246354343b5..42ecda5df92 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -25,7 +25,6 @@ import { BubbleChatDelay, Download, Lock, Plus, Unlock, X } from '@sim/emcn/icon import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { useQueryClient } from '@tanstack/react-query' -import { useParams, useRouter } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { useShallow } from 'zustand/react/shallow' import { VariableIcon } from '@/components/icons' @@ -46,10 +45,12 @@ import { import { captureEvent } from '@/lib/posthog/client' import { generateWorkflowJson } from '@/lib/workflows/operations/import-export' import { ConversationListItem } from '@/app/workspace/[workspaceId]/components/conversation-list-item' +import { useResourceRouter } from '@/app/workspace/[workspaceId]/components/resource/resource-navigation' import { MothershipChat } from '@/app/workspace/[workspaceId]/home/components/mothership-chat' import { getWorkflowCopilotUseChatOptions, useChat } from '@/app/workspace/[workspaceId]/home/hooks' import type { FileAttachmentForApi } from '@/app/workspace/[workspaceId]/home/types' import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { useWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils' @@ -133,10 +134,8 @@ function copilotDraftKey( * @returns Panel on the right side of the workflow */ export const Panel = memo(function Panel() { - const router = useRouter() - const params = useParams() - const workspaceId = params.workspaceId as string - const routeWorkflowId = params.workflowId as string | undefined + const router = useResourceRouter() + const { workspaceId, workflowId: scopedWorkflowId } = useWorkflowScope() const posthog = usePostHog() const { chatEnabled } = useDeploymentShape() @@ -320,7 +319,7 @@ export const Panel = memo(function Panel() { activeWorkflowId ?? undefined ) - const copilotDraftWorkflowId = activeWorkflowId ?? routeWorkflowId + const copilotDraftWorkflowId = activeWorkflowId ?? scopedWorkflowId ?? undefined const copilotDraftScopeKey = copilotDraftKey(workspaceId, copilotDraftWorkflowId, copilotChatId) const { data: copilotChatList = EMPTY_COPILOT_CHATS } = useCopilotChats( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx index a375224ec2f..83f593bba1a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx @@ -11,7 +11,6 @@ import { useScrollEdges, } from '@sim/emcn' import { ChevronDown, ChevronRight, ChevronUp, X } from '@sim/emcn/icons' -import { useParams } from 'next/navigation' import { useShallow } from 'zustand/react/shallow' import { getPanelWidth, getTerminalHeight } from '@/lib/workflows/layout' import { indexWorkflowSearchMatches } from '@/lib/workflows/search-replace/indexer' @@ -30,6 +29,10 @@ import { WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS } from '@/lib/workflows/search-replac import type { WorkflowSearchReplaceSubflowUpdate } from '@/lib/workflows/search-replace/types' import { getTransitiveSubBlockDependents } from '@/lib/workflows/subblocks/dependencies' import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' +import { + useScopedWorkflowId, + useWorkflowScope, +} from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { createCommand } from '@/app/workspace/[workspaceId]/utils/commands-utils' import { ReplacementControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/components' @@ -140,11 +143,10 @@ interface WorkflowSearchReplacePanelProps { } function WorkflowSearchReplacePanel({ focusRef }: WorkflowSearchReplacePanelProps) { - const params = useParams() - const workspaceId = params.workspaceId as string | undefined - const routeWorkflowId = params.workflowId as string | undefined + const { workspaceId } = useWorkflowScope() + const scopedWorkflowId = useScopedWorkflowId() const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) - const workflowId = activeWorkflowId ?? routeWorkflowId + const workflowId = activeWorkflowId ?? scopedWorkflowId const currentWorkflow = useCurrentWorkflow() const workflowSubblockValues = useSubBlockStore((state) => workflowId ? state.workflowValues[workflowId] : undefined diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 4cae73d381c..e0350286f52 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -30,7 +30,6 @@ import { useUpdateNodeInternals, } from '@xyflow/react' import { isEqual } from 'es-toolkit' -import { useParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { useStoreWithEqualityFn } from 'zustand/traditional' import { useDeploymentShape } from '@/lib/core/config/deployment-shape' @@ -77,6 +76,10 @@ import { hasAdvancedValues, resolveDependencyValue, } from '@/lib/workflows/subblocks/visibility' +import { + useScopedWorkflowId, + useWorkflowScope, +} from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { ActionBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar' import { @@ -649,9 +652,9 @@ export const WorkflowBlock = memo(function WorkflowBlock({ const contentRef = useRef(null) - const params = useParams() const { chatEnabled } = useDeploymentShape() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() + const currentWorkflowId = useScopedWorkflowId() const { currentWorkflow, @@ -674,7 +677,6 @@ export const WorkflowBlock = memo(function WorkflowBlock({ */ const isWorkflowRunning = useIsCurrentWorkflowExecuting() const isExecutionHighlighted = useIsBlockInActiveExecutionHandoff(id) - const currentWorkflowId = (params.workflowId as string) || activeWorkflowId || '' const currentBlock = currentWorkflow.getBlockById(id) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand.ts index cb6573d6db0..b6caf349e3c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand.ts @@ -3,7 +3,6 @@ import { toast } from '@sim/emcn' import { createLogger } from '@sim/logger' import { filterUndefined } from '@sim/utils/object' import { useQueryClient } from '@tanstack/react-query' -import { useParams } from 'next/navigation' import { requestRaw } from '@/lib/api/client' import { isApiClientError } from '@/lib/api/client/errors' import { wandGenerateStreamContract } from '@/lib/api/contracts' @@ -102,6 +101,7 @@ function buildWandContext(params?: WandContextParams): Record | } interface UseWandProps { + workspaceId: string wandConfig?: WandConfig currentValue?: string contextParams?: WandContextParams @@ -118,6 +118,7 @@ interface UseWandProps { } export function useWand({ + workspaceId, wandConfig, currentValue, contextParams, @@ -128,7 +129,6 @@ export function useWand({ }: UseWandProps) { const queryClient = useQueryClient() const { navigateToSettings } = useSettingsNavigation() - const { workspaceId } = useParams<{ workspaceId: string }>() const workflowId = useWorkflowRegistry((state) => state.hydration.workflowId) const [isLoading, setIsLoading] = useState(false) const [isPromptVisible, setIsPromptVisible] = useState(false) @@ -245,7 +245,7 @@ export function useWand({ history: wandConfig?.maintainHistory ? conversationHistory : [], generationType: wandConfig?.generationType, workflowId: workflowId ?? undefined, - workspaceId: workspaceId ?? undefined, + workspaceId, wandContext: buildWandContext(contextParams), }, signal: abortControllerRef.current.signal, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index c794cad4af9..e12407b8679 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -6,7 +6,6 @@ import { sleep } from '@sim/utils/helpers' import { generateId } from '@sim/utils/id' import { isRecordLike } from '@sim/utils/object' import { useQueryClient } from '@tanstack/react-query' -import { useParams } from 'next/navigation' import { useShallow } from 'zustand/react/shallow' import { type AgentStreamToolCall, @@ -400,7 +399,6 @@ function createAgentStreamChrome({ executionIdRef, updateConsole }: AgentStreamC } export function useWorkflowExecution() { - const { workspaceId: routeWorkspaceId } = useParams<{ workspaceId: string }>() const hydrationWorkspaceId = useWorkflowRegistry((s) => s.hydration.workspaceId) const queryClient = useQueryClient() const currentWorkflow = useCurrentWorkflow() @@ -729,7 +727,7 @@ export function useWorkflowExecution() { async (workflowInput?: unknown, enableDebug = false) => { if (!activeWorkflowId) return - const scopedWorkspaceId = routeWorkspaceId ?? hydrationWorkspaceId ?? undefined + const scopedWorkspaceId = hydrationWorkspaceId ?? undefined const cachedWorkflows = scopedWorkspaceId ? getWorkflows(scopedWorkspaceId) : [] const activeWorkflow = cachedWorkflows.find((w) => w.id === activeWorkflowId) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/page.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/page.tsx index 61abd393c3f..9cec2d2449c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/page.tsx @@ -5,4 +5,11 @@ export const metadata: Metadata = { title: 'Workflow', } -export default Workflow +interface WorkflowPageProps { + params: Promise<{ workspaceId: string; workflowId: string }> +} + +export default async function WorkflowPage({ params }: WorkflowPageProps) { + const { workspaceId, workflowId } = await params + return +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index ec9ea43a0f9..f39cb971070 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -14,7 +14,6 @@ import { SelectionMode, useReactFlow, } from '@xyflow/react' -import { useParams, useRouter } from 'next/navigation' import '@xyflow/react/dist/style.css' import { cn, toast, useToast } from '@sim/emcn' import { createLogger } from '@sim/logger' @@ -57,6 +56,8 @@ import { getDefaultBlockName } from '@/lib/workflows/blocks/canvas-presentation' import { requestNoteImage, requestNoteRename } from '@/lib/workflows/notes/canvas-requests' import { TriggerUtils } from '@/lib/workflows/triggers/triggers' import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal' +import { useResourceRouter } from '@/app/workspace/[workspaceId]/components/resource/resource-navigation' +import { WorkflowScopeProvider } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { CommandList, @@ -313,17 +314,13 @@ interface AddBlockFromToolbarDetail { * Renders the ReactFlow canvas with blocks, edges, and all interactive features. */ interface WorkflowContentProps { - workspaceId?: string - workflowId?: string + workspaceId: string + workflowId: string embedded?: boolean } const WorkflowContent = React.memo( - ({ - workspaceId: propWorkspaceId, - workflowId: propWorkflowId, - embedded, - }: WorkflowContentProps = {}) => { + ({ workspaceId, workflowId: workflowIdParam, embedded }: WorkflowContentProps) => { const [isCanvasReady, setIsCanvasReady] = useState(false) const [potentialParentId, setPotentialParentId] = useState(null) const [selectedEdges, setSelectedEdges] = useState(new Map()) @@ -361,8 +358,7 @@ const WorkflowContent = React.memo( ? usesCredentialConfiguredOAuthClient(oauthModal.provider) : false - const params = useParams() - const router = useRouter() + const router = useResourceRouter() const colorMode = useCanvasColorMode() const reactFlowInstance = useReactFlow() const { screenToFlowPosition, getNodes, setNodes } = reactFlowInstance @@ -372,16 +368,12 @@ const WorkflowContent = React.memo( const { emitCursorUpdate, joinWorkflow, leaveWorkflow } = useSocket() useDynamicHandleRefresh() - const workspaceId = propWorkspaceId || (params.workspaceId as string) - const workflowIdParam = propWorkflowId || (params.workflowId as string) - useEffect(() => { - if (!embedded || !workflowIdParam) return joinWorkflow(workflowIdParam) return () => { - leaveWorkflow() + leaveWorkflow(workflowIdParam) } - }, [embedded, workflowIdParam, joinWorkflow, leaveWorkflow]) + }, [workflowIdParam, joinWorkflow, leaveWorkflow]) useOAuthReturnForWorkflow(workflowIdParam) @@ -5373,19 +5365,20 @@ const WorkflowContent = React.memo( WorkflowContent.displayName = 'WorkflowContent' interface WorkflowProps { - workspaceId?: string - workflowId?: string + workspaceId: string + workflowId: string embedded?: boolean } -/** Workflow page with ReactFlowProvider and error boundary wrapper. */ -const Workflow = React.memo(({ workspaceId, workflowId, embedded }: WorkflowProps = {}) => { +const Workflow = React.memo(({ workspaceId, workflowId, embedded }: WorkflowProps) => { return ( - - - - - + + + + + + + ) }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx index b3c752fb6d0..00fe338e573 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx @@ -28,7 +28,6 @@ import { } from '@sim/emcn/icons' import { formatDuration } from '@sim/utils/formatting' import { ReactFlowProvider } from '@xyflow/react' -import { useParams } from 'next/navigation' import { extractReferencePrefixes } from '@/lib/workflows/sanitization/references' import { buildCanonicalIndexForSurface, @@ -39,9 +38,11 @@ import { isToolInputOnlySubBlock, } from '@/lib/workflows/subblocks/visibility' import { DELETED_WORKFLOW_LABEL } from '@/lib/workflows/workflow-labels' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components' import { PreviewContextMenu } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-context-menu' import { PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow' +import { PreviewScope } from '@/app/workspace/[workspaceId]/w/components/preview/preview-scope' import { getBlock } from '@/blocks' import { BlockTile } from '@/blocks/block-tile' import type { BlockConfig, SubBlockConfig, SubBlockType } from '@/blocks/types' @@ -732,8 +733,7 @@ function PreviewEditorContent({ const blockConfig = getBlock(block.type) as BlockConfig | undefined const subBlockValues = block.subBlocks || {} - const params = useParams() - const workspaceId = params.workspaceId as string + const { workspaceId } = useWorkflowScope() const isWorkflowBlock = block.type === 'workflow' || block.type === 'workflow_input' @@ -1559,8 +1559,10 @@ function PreviewEditorContent({ */ export function PreviewEditor(props: PreviewEditorProps) { return ( - - - + + + + + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/preview-workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/preview-workflow.tsx index b7993dad20d..8ab9b208172 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/preview-workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/preview-workflow.tsx @@ -11,7 +11,6 @@ import { ReactFlowProvider, useReactFlow, } from '@xyflow/react' -import { useParams } from 'next/navigation' import '@xyflow/react/dist/style.css' import { cn } from '@sim/emcn' @@ -34,9 +33,11 @@ import { PreviewBlock } from '@/app/workspace/[workspaceId]/w/components/preview import { PreviewSubflow } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/subflow' import { getPreviewBlockDimensions } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/preview-dimensions' import { PreviewEdge } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/preview-edge' +import { PreviewScope } from '@/app/workspace/[workspaceId]/w/components/preview/preview-scope' import { useWorkflowMap } from '@/hooks/queries/workflows' import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types' import '@/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/preview-workflow.css' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' const logger = createLogger('PreviewWorkflow') @@ -173,9 +174,16 @@ function FitViewOnChange({ nodeIds, fitPadding, containerRef }: FitViewOnChangeP } /** Readonly workflow visualization with execution status highlighting. */ -export function PreviewWorkflow({ +export function PreviewWorkflow({ workspaceId, ...props }: PreviewWorkflowProps) { + return ( + + + + ) +} + +function PreviewWorkflowContent({ workflowState, - workspaceId: propWorkspaceId, className, height = '100%', width = '100%', @@ -194,9 +202,8 @@ export function PreviewWorkflow({ edgeDiffStatus, changedFieldsByBlock, removedPortsByBlock, -}: PreviewWorkflowProps) { - const params = useParams<{ workspaceId: string }>() - const workspaceId = propWorkspaceId ?? params.workspaceId +}: Omit) { + const { workspaceId } = useWorkflowScope() const { data: workflowMap = {}, isSuccess: isWorkflowMapLoaded, diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview-scope.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview-scope.tsx new file mode 100644 index 00000000000..e941c336e7c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview-scope.tsx @@ -0,0 +1,24 @@ +'use client' + +import type { ReactNode } from 'react' +import { WorkflowScopeProvider } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' +import { useOptionalWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider' + +interface PreviewScopeProps { + workspaceId?: string + children: ReactNode +} + +/** Scopes preview sub-blocks to a workspace with no open workflow. */ +export function PreviewScope({ workspaceId, children }: PreviewScopeProps) { + const host = useOptionalWorkspaceHostContext() + const scopedWorkspaceId = workspaceId ?? host?.workspace.id + if (!scopedWorkspaceId) { + throw new Error('Preview requires a workspaceId prop or a WorkspaceHostProvider') + } + return ( + + {children} + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx index 1a93e446901..cc28fc858d0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx @@ -10,6 +10,7 @@ import { getLeftmostBlockId, PreviewWorkflow, } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow' +import { PreviewScope } from '@/app/workspace/[workspaceId]/w/components/preview/preview-scope' import type { WorkflowState } from '@/stores/workflows/workflow/types' interface TraceSpan { @@ -283,79 +284,81 @@ export function Preview({ const currentWorkflowName = isNested ? workflowStack[workflowStack.length - 1].workflowName : null return ( -
- {isNested && ( -
- - - - - Go back to parent workflow - - {currentWorkflowName && ( -
- -
- )} -
- )} - -
- -
- - {pinnedBlockId && workflowState.blocks[pinnedBlockId] && ( -
- {/* Left-edge resize handle */} -
- +
+ {isNested && ( +
+ + + + + Go back to parent workflow + + {currentWorkflowName && ( +
+ +
+ )} +
+ )} + +
+
- )} -
+ + {pinnedBlockId && workflowState.blocks[pinnedBlockId] && ( +
+ {/* Left-edge resize handle */} +
+ +
+ )} +
+ ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-delete-workflow.ts b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-delete-workflow.ts index 9db4fa5a163..0a52458bdaa 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-delete-workflow.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-delete-workflow.ts @@ -1,6 +1,6 @@ import { useCallback, useState } from 'react' import { createLogger } from '@sim/logger' -import { useRouter } from 'next/navigation' +import { useResourceRouter } from '@/app/workspace/[workspaceId]/components/resource/resource-navigation' import { useDeleteWorkflowMutation, useWorkflows } from '@/hooks/queries/workflows' import { useFolderStore } from '@/stores/folders/store' @@ -38,7 +38,7 @@ export function useDeleteWorkflow({ isActive = false, onSuccess, }: UseDeleteWorkflowProps) { - const router = useRouter() + const router = useResourceRouter() const { data: workflowList = [] } = useWorkflows(workspaceId) const deleteWorkflowMutation = useDeleteWorkflowMutation() const [isDeleting, setIsDeleting] = useState(false) diff --git a/apps/sim/app/workspace/[workspaceId]/w/page.tsx b/apps/sim/app/workspace/[workspaceId]/w/page.tsx index 3ab2796c8e3..1f8e6f2806a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/page.tsx @@ -6,6 +6,7 @@ import { createLogger } from '@sim/logger' import { ReactFlowProvider } from '@xyflow/react' import { useParams, useRouter } from 'next/navigation' import { ApplicationLoading } from '@/app/_shell/application-loading' +import { WorkflowScopeProvider } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { Panel, Terminal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components' import { useWorkflowOperations } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' @@ -95,9 +96,11 @@ export default function WorkflowsPage() { )}
- - - + + + + +
diff --git a/apps/sim/app/workspace/providers/socket-join-target.test.ts b/apps/sim/app/workspace/providers/socket-join-target.test.ts index 6f832631fb9..67175e5c9bc 100644 --- a/apps/sim/app/workspace/providers/socket-join-target.test.ts +++ b/apps/sim/app/workspace/providers/socket-join-target.test.ts @@ -1,14 +1,49 @@ import { describe, expect, it } from 'vitest' -import { isSocketWorkflowVisible } from '@/app/workspace/providers/socket-join-target' +import { + isSocketWorkflowVisible, + releaseWorkflowRequest, +} from '@/app/workspace/providers/socket-join-target' describe('socket join target helpers', () => { it('rejects mismatched workflow visibility', () => { expect( isSocketWorkflowVisible({ workflowId: 'workflow-other', - routeWorkflowId: 'workflow-route', - explicitWorkflowId: null, + requestedWorkflowId: 'workflow-requested', }) ).toBe(false) }) + + it('accepts the requested workflow as visible', () => { + expect( + isSocketWorkflowVisible({ workflowId: 'workflow-a', requestedWorkflowId: 'workflow-a' }) + ).toBe(true) + }) + + it('treats no requested room as nothing visible', () => { + expect(isSocketWorkflowVisible({ workflowId: null, requestedWorkflowId: null })).toBe(false) + }) + + it('keeps an earlier editor requested when a later one unmounts', () => { + expect(releaseWorkflowRequest(['workflow-a', 'workflow-b'], 'workflow-b')).toEqual([ + 'workflow-a', + ]) + }) + + it('removes an earlier editor without disturbing the one mounted above it', () => { + expect(releaseWorkflowRequest(['workflow-a', 'workflow-b'], 'workflow-a')).toEqual([ + 'workflow-b', + ]) + }) + + it('releases only one request when the same workflow is mounted twice', () => { + expect(releaseWorkflowRequest(['workflow-a', 'workflow-a'], 'workflow-a')).toEqual([ + 'workflow-a', + ]) + }) + + it('ignores a release for a workflow that never joined', () => { + const requests = ['workflow-a'] + expect(releaseWorkflowRequest(requests, 'workflow-b')).toBe(requests) + }) }) diff --git a/apps/sim/app/workspace/providers/socket-join-target.ts b/apps/sim/app/workspace/providers/socket-join-target.ts index 8ecc2458e19..e07048987d2 100644 --- a/apps/sim/app/workspace/providers/socket-join-target.ts +++ b/apps/sim/app/workspace/providers/socket-join-target.ts @@ -1,28 +1,22 @@ -interface ResolveSocketWorkflowTargetArgs { - routeWorkflowId?: string | null - explicitWorkflowId?: string | null +/** + * Removes one mounted editor's room request. Each mounted editor pushes its + * workflow on mount and releases it on unmount, so the room follows the most + * recently mounted editor and falls back to the one beneath it when it leaves. + */ +export function releaseWorkflowRequest(requests: string[], workflowId: string): string[] { + const index = requests.lastIndexOf(workflowId) + if (index === -1) return requests + return [...requests.slice(0, index), ...requests.slice(index + 1)] } -export function resolveSocketWorkflowTarget({ - routeWorkflowId, - explicitWorkflowId, -}: ResolveSocketWorkflowTargetArgs): string | null { - return explicitWorkflowId ?? routeWorkflowId ?? null -} - -interface IsSocketWorkflowVisibleArgs extends ResolveSocketWorkflowTargetArgs { +interface IsSocketWorkflowVisibleArgs { workflowId?: string | null + requestedWorkflowId: string | null } export function isSocketWorkflowVisible({ workflowId, - routeWorkflowId, - explicitWorkflowId, + requestedWorkflowId, }: IsSocketWorkflowVisibleArgs): boolean { - const targetWorkflowId = workflowId ?? null - if (!targetWorkflowId) { - return false - } - - return targetWorkflowId === resolveSocketWorkflowTarget({ routeWorkflowId, explicitWorkflowId }) + return Boolean(workflowId) && workflowId === requestedWorkflowId } diff --git a/apps/sim/app/workspace/providers/socket-provider.tsx b/apps/sim/app/workspace/providers/socket-provider.tsx index 7b2dc6e2024..921a4b00151 100644 --- a/apps/sim/app/workspace/providers/socket-provider.tsx +++ b/apps/sim/app/workspace/providers/socket-provider.tsx @@ -30,7 +30,6 @@ import { generateId } from '@sim/utils/id' import { randomFloat } from '@sim/utils/random' import { backoffWithJitter, parseRetryAfter } from '@sim/utils/retry' import { useQueryClient } from '@tanstack/react-query' -import { useParams } from 'next/navigation' import type { Socket } from 'socket.io-client' import { getEnv } from '@/lib/core/config/env' import { getSocketUrl } from '@/lib/core/utils/urls' @@ -41,7 +40,7 @@ import { } from '@/app/workspace/providers/socket-join-controller' import { isSocketWorkflowVisible, - resolveSocketWorkflowTarget, + releaseWorkflowRequest, } from '@/app/workspace/providers/socket-join-target' import { mergePresenceRoster } from '@/app/workspace/providers/socket-presence-merge' import { refreshSessionQuery } from '@/hooks/queries/session' @@ -124,7 +123,7 @@ interface SocketContextType { currentWorkflowId: string | null currentSocketId: string | null joinWorkflow: (workflowId: string) => void - leaveWorkflow: () => void + leaveWorkflow: (workflowId: string) => void retryConnection: () => void emitWorkflowOperation: WorkflowOperationEmit emitSubblockUpdate: SubblockUpdateEmit @@ -196,11 +195,12 @@ export function SocketProvider({ children, user }: SocketProviderProps) { const [currentSocketId, setCurrentSocketId] = useState(null) const [authFailed, setAuthFailed] = useState(false) const [blockedJoinWorkflowId, setBlockedJoinWorkflowId] = useState(null) - const [explicitWorkflowId, setExplicitWorkflowId] = useState(null) + const [workflowRequests, setWorkflowRequests] = useState([]) + const requestedWorkflowId = workflowRequests[workflowRequests.length - 1] ?? null const initializedRef = useRef(false) const socketRef = useRef(null) const currentWorkflowIdRef = useRef(null) - const explicitWorkflowIdRef = useRef(explicitWorkflowId) + const requestedWorkflowIdRef = useRef(requestedWorkflowId) const joinControllerRef = useRef(null) const joinController = (joinControllerRef.current ??= new SocketJoinController()) const joinRetryTimeoutRef = useRef | null>(null) @@ -212,11 +212,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { const connectFailureReportedRef = useRef(false) const queryClient = useQueryClient() - const params = useParams() - const urlWorkflowId = params?.workflowId as string | undefined - const urlWorkflowIdRef = useRef(urlWorkflowId) - urlWorkflowIdRef.current = urlWorkflowId - explicitWorkflowIdRef.current = explicitWorkflowId + requestedWorkflowIdRef.current = requestedWorkflowId const eventHandlers = useRef<{ workflowOperation?: (data: WorkflowOperationBroadcast) => void @@ -258,18 +254,10 @@ export function SocketProvider({ children, user }: SocketProviderProps) { setCurrentWorkflowId(workflowId) }, []) - const getRequestedWorkflowId = useCallback(() => { - return resolveSocketWorkflowTarget({ - routeWorkflowId: urlWorkflowIdRef.current ?? null, - explicitWorkflowId: explicitWorkflowIdRef.current, - }) - }, []) - const isWorkflowVisible = useCallback((workflowId?: string | null) => { return isSocketWorkflowVisible({ workflowId: workflowId ?? currentWorkflowIdRef.current, - routeWorkflowId: urlWorkflowIdRef.current ?? null, - explicitWorkflowId: explicitWorkflowIdRef.current, + requestedWorkflowId: requestedWorkflowIdRef.current, }) }, []) @@ -918,7 +906,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { logger.warn('Operation forbidden:', error) if (error?.type === 'SESSION_ERROR') { - const workflowId = getRequestedWorkflowId() + const workflowId = requestedWorkflowIdRef.current if (workflowId) { logger.info(`Session expired, rejoining workflow: ${workflowId}`) @@ -949,7 +937,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { logger.info('Ignoring workflow state for inactive room', { workflowId: workflowData?.id, currentWorkflowId: currentWorkflowIdRef.current, - desiredWorkflowId: urlWorkflowIdRef.current, + requestedWorkflowId: requestedWorkflowIdRef.current, }) return } @@ -1058,17 +1046,9 @@ export function SocketProvider({ children, user }: SocketProviderProps) { return } - const requestedWorkflowId = getRequestedWorkflowId() - setBlockedJoinWorkflowId((prev) => (prev && prev !== requestedWorkflowId ? null : prev)) executeJoinCommands(joinController.requestWorkflow(requestedWorkflowId)) - }, [ - explicitWorkflowId, - getRequestedWorkflowId, - hydrationPhase, - urlWorkflowId, - executeJoinCommands, - ]) + }, [requestedWorkflowId, hydrationPhase, executeJoinCommands]) const joinWorkflow = useCallback( (workflowId: string) => { @@ -1077,13 +1057,13 @@ export function SocketProvider({ children, user }: SocketProviderProps) { return } - setExplicitWorkflowId(workflowId) + setWorkflowRequests((requests) => [...requests, workflowId]) }, [user] ) - const leaveWorkflow = useCallback(() => { - setExplicitWorkflowId(null) + const leaveWorkflow = useCallback((workflowId: string) => { + setWorkflowRequests((requests) => releaseWorkflowRequest(requests, workflowId)) }, []) /** @@ -1118,7 +1098,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { logger.debug('Skipping workflow operation emit for inactive room', { workflowId, currentWorkflowId, - desiredWorkflowId: urlWorkflowIdRef.current, + requestedWorkflowId: requestedWorkflowIdRef.current, operation, target, }) diff --git a/apps/sim/hooks/use-webhook-management.ts b/apps/sim/hooks/use-webhook-management.ts index 2bd0911b290..cadf8478171 100644 --- a/apps/sim/hooks/use-webhook-management.ts +++ b/apps/sim/hooks/use-webhook-management.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react' import { createLogger } from '@sim/logger' -import { useParams } from 'next/navigation' import { getBaseUrl } from '@/lib/core/utils/urls' +import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { getBlock } from '@/blocks' import { useWebhookQuery } from '@/hooks/queries/webhooks' import { populateTriggerFieldsFromConfig } from '@/hooks/use-trigger-config-aggregation' @@ -94,8 +94,7 @@ export function useWebhookManagement({ useWebhookUrl = false, providerWebhookUrl, }: UseWebhookManagementProps): WebhookManagementState { - const params = useParams() - const workflowId = params.workflowId as string + const { workflowId } = useWorkflowScope() const syncedRef = useRef(false) const webhookId = useSubBlockStore( @@ -160,7 +159,7 @@ export function useWebhookManagement({ } const { data: webhook, isLoading: queryLoading } = useWebhookQuery( - workflowId, + workflowId ?? '', blockId, queryEnabled ) From 03e1d7928746ab168127d7053e4b08ac6e0d7d75 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 13 Oct 2026 15:45:58 -0700 Subject: [PATCH 2/2] fix(workflow): release socket room requests per mount, scope previews to the enclosing editor - joinWorkflow returns a release for its own mount token, so unmounting one of two editors on the same workflow can no longer drop the other's room; leaveWorkflow is gone - PreviewScope inherits the enclosing editor's workspace before the workspace host - TSDoc on useWorkflowScope Claude-Session: https://claude.ai/code/session_01UMjSAnE9M7r86o5P19ash5 --- .../providers/workflow-scope-provider.tsx | 11 ++++++- .../[workspaceId]/w/[workflowId]/workflow.tsx | 9 ++---- .../w/components/preview/preview-scope.tsx | 17 +++++++--- .../providers/socket-join-target.test.ts | 32 +++++++++++-------- .../workspace/providers/socket-join-target.ts | 19 ++++++++--- .../workspace/providers/socket-provider.tsx | 26 +++++++-------- 6 files changed, 70 insertions(+), 44 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/providers/workflow-scope-provider.tsx b/apps/sim/app/workspace/[workspaceId]/providers/workflow-scope-provider.tsx index 6f14fe3433d..99561ca680e 100644 --- a/apps/sim/app/workspace/[workspaceId]/providers/workflow-scope-provider.tsx +++ b/apps/sim/app/workspace/[workspaceId]/providers/workflow-scope-provider.tsx @@ -30,8 +30,17 @@ export function WorkflowScopeProvider({ return {children} } +/** Returns the enclosing scope, or null outside a WorkflowScopeProvider. */ +export function useOptionalWorkflowScope(): WorkflowScope | null { + return useContext(WorkflowScopeContext) +} + +/** + * Returns the editor's workspace and workflow ids. `workflowId` is null in previews. + * Throws outside a WorkflowScopeProvider. + */ export function useWorkflowScope(): WorkflowScope { - const scope = useContext(WorkflowScopeContext) + const scope = useOptionalWorkflowScope() if (!scope) { throw new Error('useWorkflowScope must be used within a WorkflowScopeProvider') } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index f39cb971070..686a7accb11 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -365,15 +365,10 @@ const WorkflowContent = React.memo( const { fitViewToBounds, getViewportCenter } = useCanvasViewport(reactFlowInstance, { embedded, }) - const { emitCursorUpdate, joinWorkflow, leaveWorkflow } = useSocket() + const { emitCursorUpdate, joinWorkflow } = useSocket() useDynamicHandleRefresh() - useEffect(() => { - joinWorkflow(workflowIdParam) - return () => { - leaveWorkflow(workflowIdParam) - } - }, [workflowIdParam, joinWorkflow, leaveWorkflow]) + useEffect(() => joinWorkflow(workflowIdParam), [workflowIdParam, joinWorkflow]) useOAuthReturnForWorkflow(workflowIdParam) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview-scope.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview-scope.tsx index e941c336e7c..93c82691b21 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview-scope.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview-scope.tsx @@ -1,7 +1,10 @@ 'use client' import type { ReactNode } from 'react' -import { WorkflowScopeProvider } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' +import { + useOptionalWorkflowScope, + WorkflowScopeProvider, +} from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { useOptionalWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider' interface PreviewScopeProps { @@ -9,12 +12,18 @@ interface PreviewScopeProps { children: ReactNode } -/** Scopes preview sub-blocks to a workspace with no open workflow. */ +/** + * Scopes preview sub-blocks to a workspace with no open workflow: the explicit prop, else the + * enclosing editor's workspace, else the workspace host. + */ export function PreviewScope({ workspaceId, children }: PreviewScopeProps) { + const enclosing = useOptionalWorkflowScope() const host = useOptionalWorkspaceHostContext() - const scopedWorkspaceId = workspaceId ?? host?.workspace.id + const scopedWorkspaceId = workspaceId ?? enclosing?.workspaceId ?? host?.workspace.id if (!scopedWorkspaceId) { - throw new Error('Preview requires a workspaceId prop or a WorkspaceHostProvider') + throw new Error( + 'Preview requires a workspaceId prop, an enclosing WorkflowScopeProvider, or a WorkspaceHostProvider' + ) } return ( diff --git a/apps/sim/app/workspace/providers/socket-join-target.test.ts b/apps/sim/app/workspace/providers/socket-join-target.test.ts index 67175e5c9bc..8a2283c1e7d 100644 --- a/apps/sim/app/workspace/providers/socket-join-target.test.ts +++ b/apps/sim/app/workspace/providers/socket-join-target.test.ts @@ -2,8 +2,11 @@ import { describe, expect, it } from 'vitest' import { isSocketWorkflowVisible, releaseWorkflowRequest, + type WorkflowRequest, } from '@/app/workspace/providers/socket-join-target' +const request = (token: string, workflowId: string): WorkflowRequest => ({ token, workflowId }) + describe('socket join target helpers', () => { it('rejects mismatched workflow visibility', () => { expect( @@ -25,25 +28,28 @@ describe('socket join target helpers', () => { }) it('keeps an earlier editor requested when a later one unmounts', () => { - expect(releaseWorkflowRequest(['workflow-a', 'workflow-b'], 'workflow-b')).toEqual([ - 'workflow-a', - ]) + expect( + releaseWorkflowRequest([request('t1', 'workflow-a'), request('t2', 'workflow-b')], 't2') + ).toEqual([request('t1', 'workflow-a')]) }) it('removes an earlier editor without disturbing the one mounted above it', () => { - expect(releaseWorkflowRequest(['workflow-a', 'workflow-b'], 'workflow-a')).toEqual([ - 'workflow-b', - ]) + expect( + releaseWorkflowRequest([request('t1', 'workflow-a'), request('t2', 'workflow-b')], 't1') + ).toEqual([request('t2', 'workflow-b')]) }) - it('releases only one request when the same workflow is mounted twice', () => { - expect(releaseWorkflowRequest(['workflow-a', 'workflow-a'], 'workflow-a')).toEqual([ - 'workflow-a', - ]) + it('keeps the newer mount of a workflow on top when the older mount of it unmounts', () => { + expect( + releaseWorkflowRequest( + [request('t1', 'workflow-a'), request('t2', 'workflow-b'), request('t3', 'workflow-a')], + 't1' + ) + ).toEqual([request('t2', 'workflow-b'), request('t3', 'workflow-a')]) }) - it('ignores a release for a workflow that never joined', () => { - const requests = ['workflow-a'] - expect(releaseWorkflowRequest(requests, 'workflow-b')).toBe(requests) + it('ignores a release for a mount that never joined', () => { + const requests = [request('t1', 'workflow-a')] + expect(releaseWorkflowRequest(requests, 't9')).toBe(requests) }) }) diff --git a/apps/sim/app/workspace/providers/socket-join-target.ts b/apps/sim/app/workspace/providers/socket-join-target.ts index e07048987d2..5f8373fbdd3 100644 --- a/apps/sim/app/workspace/providers/socket-join-target.ts +++ b/apps/sim/app/workspace/providers/socket-join-target.ts @@ -1,10 +1,19 @@ +/** One mounted editor's request for a workflow room; the token identifies that mount. */ +export interface WorkflowRequest { + token: string + workflowId: string +} + /** - * Removes one mounted editor's room request. Each mounted editor pushes its - * workflow on mount and releases it on unmount, so the room follows the most - * recently mounted editor and falls back to the one beneath it when it leaves. + * Removes one mounted editor's room request. Each mount pushes a request and releases it by + * token on unmount, so the room follows the most recently mounted editor and falls back to + * the one beneath it when it leaves. */ -export function releaseWorkflowRequest(requests: string[], workflowId: string): string[] { - const index = requests.lastIndexOf(workflowId) +export function releaseWorkflowRequest( + requests: WorkflowRequest[], + token: string +): WorkflowRequest[] { + const index = requests.findIndex((request) => request.token === token) if (index === -1) return requests return [...requests.slice(0, index), ...requests.slice(index + 1)] } diff --git a/apps/sim/app/workspace/providers/socket-provider.tsx b/apps/sim/app/workspace/providers/socket-provider.tsx index 921a4b00151..400c09d9d79 100644 --- a/apps/sim/app/workspace/providers/socket-provider.tsx +++ b/apps/sim/app/workspace/providers/socket-provider.tsx @@ -41,6 +41,7 @@ import { import { isSocketWorkflowVisible, releaseWorkflowRequest, + type WorkflowRequest, } from '@/app/workspace/providers/socket-join-target' import { mergePresenceRoster } from '@/app/workspace/providers/socket-presence-merge' import { refreshSessionQuery } from '@/hooks/queries/session' @@ -122,8 +123,8 @@ interface SocketContextType { blockedJoinWorkflowId: string | null currentWorkflowId: string | null currentSocketId: string | null - joinWorkflow: (workflowId: string) => void - leaveWorkflow: (workflowId: string) => void + /** Requests the workflow's room for one mounted editor; call the returned release on unmount. */ + joinWorkflow: (workflowId: string) => () => void retryConnection: () => void emitWorkflowOperation: WorkflowOperationEmit emitSubblockUpdate: SubblockUpdateEmit @@ -156,8 +157,7 @@ const SocketContext = createContext({ blockedJoinWorkflowId: null, currentWorkflowId: null, currentSocketId: null, - joinWorkflow: () => {}, - leaveWorkflow: () => {}, + joinWorkflow: () => () => {}, retryConnection: () => {}, emitWorkflowOperation: () => false, emitSubblockUpdate: () => false, @@ -195,8 +195,8 @@ export function SocketProvider({ children, user }: SocketProviderProps) { const [currentSocketId, setCurrentSocketId] = useState(null) const [authFailed, setAuthFailed] = useState(false) const [blockedJoinWorkflowId, setBlockedJoinWorkflowId] = useState(null) - const [workflowRequests, setWorkflowRequests] = useState([]) - const requestedWorkflowId = workflowRequests[workflowRequests.length - 1] ?? null + const [workflowRequests, setWorkflowRequests] = useState([]) + const requestedWorkflowId = workflowRequests[workflowRequests.length - 1]?.workflowId ?? null const initializedRef = useRef(false) const socketRef = useRef(null) const currentWorkflowIdRef = useRef(null) @@ -1054,18 +1054,18 @@ export function SocketProvider({ children, user }: SocketProviderProps) { (workflowId: string) => { if (!user?.id) { logger.warn('Cannot join workflow: user not available') - return + return () => {} } - setWorkflowRequests((requests) => [...requests, workflowId]) + const token = generateId() + setWorkflowRequests((requests) => [...requests, { token, workflowId }]) + return () => { + setWorkflowRequests((requests) => releaseWorkflowRequest(requests, token)) + } }, [user] ) - const leaveWorkflow = useCallback((workflowId: string) => { - setWorkflowRequests((requests) => releaseWorkflowRequest(requests, workflowId)) - }, []) - /** * Retry socket connection after auth failure. * Call this when user has re-authenticated (e.g., after login redirect). @@ -1343,7 +1343,6 @@ export function SocketProvider({ children, user }: SocketProviderProps) { currentWorkflowId, currentSocketId, joinWorkflow, - leaveWorkflow, retryConnection, emitWorkflowOperation, emitSubblockUpdate, @@ -1374,7 +1373,6 @@ export function SocketProvider({ children, user }: SocketProviderProps) { currentWorkflowId, currentSocketId, joinWorkflow, - leaveWorkflow, retryConnection, emitWorkflowOperation, emitSubblockUpdate,