Skip to content

Commit 9cc1f0f

Browse files
improvement(workflow): scope the editor by props instead of the URL (#8996)
* 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; <Workflow> 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 * 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
1 parent 4d2fa26 commit 9cc1f0f

62 files changed

Lines changed: 503 additions & 354 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/use-custom-tool-generation.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,18 +121,21 @@ try {
121121
}
122122

123123
interface UseSchemaGenerationParams {
124+
workspaceId: string
124125
jsonSchema: string
125126
setJsonSchema: (updater: (prev: string) => string) => void
126127
replaceJsonSchema: (value: string) => void
127128
}
128129

129130
/** Wand-driven generation for a custom tool's JSON schema. */
130131
export function useSchemaGeneration({
132+
workspaceId,
131133
jsonSchema,
132134
setJsonSchema,
133135
replaceJsonSchema,
134136
}: UseSchemaGenerationParams) {
135137
return useWand({
138+
workspaceId,
136139
wandConfig: {
137140
enabled: true,
138141
maintainHistory: true,
@@ -148,6 +151,7 @@ export function useSchemaGeneration({
148151
}
149152

150153
interface UseCodeGenerationParams {
154+
workspaceId: string
151155
functionCode: string
152156
schemaParameters: SchemaParameter[]
153157
setFunctionCode: (updater: (prev: string) => string) => void
@@ -156,6 +160,7 @@ interface UseCodeGenerationParams {
156160

157161
/** Wand-driven generation for a custom tool's function body, aware of the schema's parameters. */
158162
export function useCodeGeneration({
163+
workspaceId,
159164
functionCode,
160165
schemaParameters,
161166
setFunctionCode,
@@ -178,6 +183,7 @@ export function useCodeGeneration({
178183
}, [schemaParameters])
179184

180185
return useWand({
186+
workspaceId,
181187
wandConfig: {
182188
enabled: true,
183189
maintainHistory: true,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use client'
2+
3+
import { createContext, useCallback, useContext, useMemo } from 'react'
4+
import { useRouter } from 'next/navigation'
5+
6+
interface ResourceNavigation {
7+
/** Handles workspace destinations within a containing resource pane. */
8+
navigate: (href: string, replace?: boolean) => void
9+
}
10+
11+
/** Provided by a containing resource pane; absent on the routed pages. */
12+
const ResourceNavigationContext = createContext<ResourceNavigation | null>(null)
13+
14+
/** Navigates the route, or the containing resource pane when one provides navigation. */
15+
export function useResourceRouter() {
16+
const router = useRouter()
17+
const navigation = useContext(ResourceNavigationContext)
18+
const push = useCallback(
19+
(href: string) => {
20+
if (navigation) navigation.navigate(href)
21+
else router.push(href)
22+
},
23+
[navigation, router]
24+
)
25+
const replace = useCallback(
26+
(href: string) => {
27+
if (navigation) navigation.navigate(href, true)
28+
else router.replace(href)
29+
},
30+
[navigation, router]
31+
)
32+
return useMemo(() => ({ ...router, push, replace }), [router, push, replace])
33+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use client'
2+
3+
import { createContext, type ReactNode, useContext, useMemo } from 'react'
4+
5+
interface WorkflowScope {
6+
workspaceId: string
7+
/** Null in read-only previews, which render editor sub-blocks outside an open workflow. */
8+
workflowId: string | null
9+
}
10+
11+
const WorkflowScopeContext = createContext<WorkflowScope | null>(null)
12+
13+
interface WorkflowScopeProviderProps extends WorkflowScope {
14+
children: ReactNode
15+
}
16+
17+
/**
18+
* Supplies the workspace and workflow ids to the editor tree, so the editor
19+
* works wherever it is mounted instead of reading them from the route.
20+
*/
21+
export function WorkflowScopeProvider({
22+
workspaceId,
23+
workflowId,
24+
children,
25+
}: WorkflowScopeProviderProps) {
26+
if (!workspaceId) {
27+
throw new Error('WorkflowScopeProvider requires a workspaceId')
28+
}
29+
const value = useMemo(() => ({ workspaceId, workflowId }), [workspaceId, workflowId])
30+
return <WorkflowScopeContext.Provider value={value}>{children}</WorkflowScopeContext.Provider>
31+
}
32+
33+
/** Returns the enclosing scope, or null outside a WorkflowScopeProvider. */
34+
export function useOptionalWorkflowScope(): WorkflowScope | null {
35+
return useContext(WorkflowScopeContext)
36+
}
37+
38+
/**
39+
* Returns the editor's workspace and workflow ids. `workflowId` is null in previews.
40+
* Throws outside a WorkflowScopeProvider.
41+
*/
42+
export function useWorkflowScope(): WorkflowScope {
43+
const scope = useOptionalWorkflowScope()
44+
if (!scope) {
45+
throw new Error('useWorkflowScope must be used within a WorkflowScopeProvider')
46+
}
47+
return scope
48+
}
49+
50+
/** For editor-only components: throws in a preview scope, which has no workflow. */
51+
export function useScopedWorkflowId(): string {
52+
const { workflowId } = useWorkflowScope()
53+
if (!workflowId) {
54+
throw new Error('useScopedWorkflowId must be used within an editor workflow scope')
55+
}
56+
return workflowId
57+
}

‎apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/components/custom-tool-detail/custom-tool-detail.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export function CustomToolDetail({
8888
const identity = useMemo(() => extractSchemaIdentity(jsonSchema), [jsonSchema])
8989

9090
const schemaGeneration = useSchemaGeneration({
91+
workspaceId,
9192
jsonSchema,
9293
setJsonSchema: (updater) => {
9394
setJsonSchema(updater)
@@ -100,6 +101,7 @@ export function CustomToolDetail({
100101
})
101102

102103
const codeGeneration = useCodeGeneration({
104+
workspaceId,
103105
functionCode,
104106
schemaParameters,
105107
setFunctionCode: (updater) => {

‎apps/sim/app/workspace/[workspaceId]/skills/components/skill-modal/skill-modal.tsx‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
} from '@sim/emcn'
1515
import { getErrorMessage } from '@sim/utils/errors'
1616
import dynamic from 'next/dynamic'
17-
import { useParams } from 'next/navigation'
1817
import {
1918
SKILL_CONTENT_PLACEHOLDER,
2019
SKILL_DESCRIPTION_MAX_LENGTH,
@@ -43,6 +42,7 @@ const RichMarkdownField = dynamic(
4342
)
4443

4544
interface SkillModalProps {
45+
workspaceId: string
4646
open: boolean
4747
onOpenChange: (open: boolean) => void
4848
onSave: () => void
@@ -63,10 +63,13 @@ const CREATE_TABS = [
6363
{ value: 'import', label: 'Import' },
6464
] as const
6565

66-
export function SkillModal({ open, onOpenChange, onSave, initialValues }: SkillModalProps) {
67-
const params = useParams()
68-
const workspaceId = params.workspaceId as string
69-
66+
export function SkillModal({
67+
workspaceId,
68+
open,
69+
onOpenChange,
70+
onSave,
71+
initialValues,
72+
}: SkillModalProps) {
7073
const createSkill = useCreateSkill()
7174
const updateSkill = useUpdateSkill()
7275

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { Button, cn, handleKeyboardActivation, Library } from '@sim/emcn'
55
import { Search } from '@sim/emcn/icons'
66
import { createLogger } from '@sim/logger'
77
import Image from 'next/image'
8-
import { useParams, useRouter } from 'next/navigation'
98
import { AgentIcon } from '@/components/icons'
9+
import { useResourceRouter } from '@/app/workspace/[workspaceId]/components/resource/resource-navigation'
10+
import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider'
1011
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
1112
import { useSearchModalStore } from '@/stores/modals/search/store'
1213

@@ -50,13 +51,11 @@ const commands: CommandItem[] = [
5051
* Centered on the screen for empty workflows
5152
*/
5253
export function CommandList() {
53-
const params = useParams()
54-
const router = useRouter()
54+
const { workspaceId } = useWorkflowScope()
55+
const router = useResourceRouter()
5556
const openSearchModal = useSearchModalStore((s) => s.open)
5657
const preventZoomRef = usePreventZoom()
5758

58-
const workspaceId = params.workspaceId as string | undefined
59-
6059
/**
6160
* Handle click on a command row.
6261
*
@@ -79,10 +78,6 @@ export function CommandList() {
7978
return
8079
}
8180
case 'Logs': {
82-
if (!workspaceId) {
83-
logger.warn('No workspace ID found, cannot navigate to logs from command list')
84-
return
85-
}
8681
router.push(`/workspace/${workspaceId}/logs`)
8782
return
8883
}

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/connection-block-selector/connection-block-selector.tsx‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ import { X } from '@sim/emcn/icons'
1414
import { WorkflowBlockBorder, type WorkflowBorderPort } from '@sim/workflow-renderer'
1515
import { Handle, type Node, type NodeProps, Position } from '@xyflow/react'
1616
import { Command } from 'cmdk'
17-
import { useParams } from 'next/navigation'
1817
import { usePostHog } from 'posthog-js/react'
1918
import { captureEvent } from '@/lib/posthog/client'
19+
import {
20+
useScopedWorkflowId,
21+
useWorkflowScope,
22+
} from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider'
2023
import {
2124
CommandFadedList,
2225
CommandSearch,
@@ -150,9 +153,8 @@ function isRecentSelection(value: unknown): value is RecentSelection {
150153
* temporary edge reads as connected to the block that will replace it.
151154
*/
152155
export function ConnectionBlockSelector({ id, data }: NodeProps<ConnectionBlockSelectorNode>) {
153-
const params = useParams()
154-
const workspaceId = params.workspaceId as string
155-
const currentWorkflowId = params.workflowId as string | undefined
156+
const { workspaceId } = useWorkflowScope()
157+
const currentWorkflowId = useScopedWorkflowId()
156158
const posthog = usePostHog()
157159
const inputRef = useRef<HTMLInputElement>(null)
158160
const listRef = useRef<HTMLDivElement>(null)

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/use-note-image-upload.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { useCallback } from 'react'
4-
import { useParams } from 'next/navigation'
4+
import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider'
55
import { useUploadWorkspaceFile } from '@/hooks/queries/workspace-files'
66

77
/**
@@ -19,8 +19,7 @@ import { useUploadWorkspaceFile } from '@/hooks/queries/workspace-files'
1919
* flow and the canvas context menu's "Add image".
2020
*/
2121
export function useNoteImageUpload(): (file: File) => Promise<{ url: string; alt: string } | null> {
22-
const params = useParams()
23-
const workspaceId = params.workspaceId as string
22+
const { workspaceId } = useWorkflowScope()
2423
const uploadFile = useUploadWorkspaceFile()
2524

2625
return useCallback(

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/api-info-modal.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ import {
1616
Label,
1717
} from '@sim/emcn'
1818
import { getErrorMessage } from '@sim/utils/errors'
19-
import { useParams } from 'next/navigation'
2019
import { getMeaningfulWorkflowDescription } from '@/lib/mcp/workflow-tool-schema'
2120
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
2221
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
2322
import type { InputFormatField } from '@/lib/workflows/types'
23+
import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider'
2424
import { useDeploymentInfo, useUpdatePublicApi } from '@/hooks/queries/deployments'
2525
import { useUpdateWorkflow, useWorkflowMap } from '@/hooks/queries/workflows'
2626
import { usePermissionConfig } from '@/hooks/use-permission-config'
@@ -37,7 +37,7 @@ interface ApiInfoModalProps {
3737
}
3838

3939
export function ApiInfoModal({ open, onOpenChange, workflowId }: ApiInfoModalProps) {
40-
const { workspaceId } = useParams<{ workspaceId: string }>()
40+
const { workspaceId } = useWorkflowScope()
4141
const blocks = useWorkflowStore((state) => state.blocks)
4242
const setValue = useSubBlockStore((state) => state.setValue)
4343
const subBlockValues = useSubBlockStore(

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/version-description-modal.tsx‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
cn,
1414
} from '@sim/emcn'
1515
import dynamic from 'next/dynamic'
16-
import { useParams } from 'next/navigation'
16+
import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider'
1717
import {
1818
useGenerateVersionDescription,
1919
useUpdateDeploymentVersion,
@@ -50,8 +50,7 @@ export function VersionDescriptionModal({
5050
versionName,
5151
currentDescription,
5252
}: VersionDescriptionModalProps) {
53-
const params = useParams()
54-
const workspaceId = params.workspaceId as string
53+
const { workspaceId } = useWorkflowScope()
5554

5655
const initialDescriptionRef = useRef(currentDescription || '')
5756
const [description, setDescription] = useState(initialDescriptionRef.current)

0 commit comments

Comments
 (0)