-
+
Go to Dashboard
diff --git a/apps/sim/app/w/error.tsx b/apps/sim/app/w/error.tsx
deleted file mode 100644
index adac0456b81..00000000000
--- a/apps/sim/app/w/error.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-'use client'
-
-import { NextError } from './[id]/components/error'
-
-export default NextError
diff --git a/apps/sim/app/w/global-error.tsx b/apps/sim/app/w/global-error.tsx
deleted file mode 100644
index 9c7bd975759..00000000000
--- a/apps/sim/app/w/global-error.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-'use client'
-
-import { NextGlobalError } from './[id]/components/error'
-
-export default NextGlobalError
diff --git a/apps/sim/app/workspace/[workspaceId]/error.tsx b/apps/sim/app/workspace/[workspaceId]/error.tsx
new file mode 100644
index 00000000000..c5e6e668b88
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/error.tsx
@@ -0,0 +1,5 @@
+'use client'
+
+import { NextError } from './w/[workflowId]/components/error'
+
+export default NextError
diff --git a/apps/sim/app/workspace/[workspaceId]/global-error.tsx b/apps/sim/app/workspace/[workspaceId]/global-error.tsx
new file mode 100644
index 00000000000..d92a73ce254
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/global-error.tsx
@@ -0,0 +1,5 @@
+'use client'
+
+import { NextGlobalError } from './w/[workflowId]/components/error'
+
+export default NextGlobalError
diff --git a/apps/sim/app/w/knowledge/[id]/[documentId]/components/create-chunk-modal/create-chunk-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/create-chunk-modal/create-chunk-modal.tsx
similarity index 100%
rename from apps/sim/app/w/knowledge/[id]/[documentId]/components/create-chunk-modal/create-chunk-modal.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/create-chunk-modal/create-chunk-modal.tsx
diff --git a/apps/sim/app/w/knowledge/[id]/[documentId]/components/delete-chunk-modal/delete-chunk-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/delete-chunk-modal/delete-chunk-modal.tsx
similarity index 100%
rename from apps/sim/app/w/knowledge/[id]/[documentId]/components/delete-chunk-modal/delete-chunk-modal.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/delete-chunk-modal/delete-chunk-modal.tsx
diff --git a/apps/sim/app/w/knowledge/[id]/[documentId]/components/document-loading.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-loading.tsx
similarity index 97%
rename from apps/sim/app/w/knowledge/[id]/[documentId]/components/document-loading.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-loading.tsx
index 2b9d811d733..aad70d0ed4f 100644
--- a/apps/sim/app/w/knowledge/[id]/[documentId]/components/document-loading.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-loading.tsx
@@ -25,12 +25,12 @@ export function DocumentLoading({
{
id: 'knowledge-root',
label: 'Knowledge',
- href: '/w/knowledge',
+ href: '/knowledge',
},
{
id: `knowledge-base-${knowledgeBaseId}`,
label: knowledgeBaseName,
- href: `/w/knowledge/${knowledgeBaseId}`,
+ href: `/knowledge/${knowledgeBaseId}`,
},
{
id: `document-${knowledgeBaseId}-${documentName}`,
diff --git a/apps/sim/app/w/knowledge/[id]/[documentId]/components/edit-chunk-modal/edit-chunk-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/edit-chunk-modal/edit-chunk-modal.tsx
similarity index 100%
rename from apps/sim/app/w/knowledge/[id]/[documentId]/components/edit-chunk-modal/edit-chunk-modal.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/edit-chunk-modal/edit-chunk-modal.tsx
diff --git a/apps/sim/app/w/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx
similarity index 99%
rename from apps/sim/app/w/knowledge/[id]/[documentId]/document.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx
index 9758f46fa8e..fc25327f6db 100644
--- a/apps/sim/app/w/knowledge/[id]/[documentId]/document.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx
@@ -16,7 +16,7 @@ import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { createLogger } from '@/lib/logs/console-logger'
-import { ActionBar } from '@/app/w/knowledge/[id]/components/action-bar/action-bar'
+import { ActionBar } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar'
import { useDocumentChunks } from '@/hooks/use-knowledge'
import { type ChunkData, type DocumentData, useKnowledgeStore } from '@/stores/knowledge/store'
import { useSidebarStore } from '@/stores/sidebar/store'
@@ -170,10 +170,10 @@ export function Document({
const effectiveDocumentName = document?.filename || documentName || 'Document'
const breadcrumbs = [
- { label: 'Knowledge', href: '/w/knowledge' },
+ { label: 'Knowledge', href: '/knowledge' },
{
label: effectiveKnowledgeBaseName,
- href: `/w/knowledge/${knowledgeBaseId}`,
+ href: `/knowledge/${knowledgeBaseId}`,
},
{ label: effectiveDocumentName },
]
@@ -360,10 +360,10 @@ export function Document({
if (combinedError && !isLoadingChunks) {
const errorBreadcrumbs = [
- { label: 'Knowledge', href: '/w/knowledge' },
+ { label: 'Knowledge', href: '/knowledge' },
{
label: effectiveKnowledgeBaseName,
- href: `/w/knowledge/${knowledgeBaseId}`,
+ href: `/knowledge/${knowledgeBaseId}`,
},
{ label: 'Error' },
]
diff --git a/apps/sim/app/w/knowledge/[id]/[documentId]/page.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/page.tsx
similarity index 100%
rename from apps/sim/app/w/knowledge/[id]/[documentId]/page.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/page.tsx
diff --git a/apps/sim/app/w/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx
similarity index 98%
rename from apps/sim/app/w/knowledge/[id]/base.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx
index 2b7ab83d394..eeffdb79e7d 100644
--- a/apps/sim/app/w/knowledge/[id]/base.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx
@@ -13,7 +13,7 @@ import {
Trash2,
X,
} from 'lucide-react'
-import { useRouter } from 'next/navigation'
+import { useParams, useRouter } from 'next/navigation'
import {
AlertDialog,
AlertDialogAction,
@@ -28,10 +28,10 @@ import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { createLogger } from '@/lib/logs/console-logger'
-import { ActionBar } from '@/app/w/knowledge/[id]/components/action-bar/action-bar'
-import { getDocumentIcon } from '@/app/w/knowledge/components/icons/document-icons'
-import { PrimaryButton } from '@/app/w/knowledge/components/primary-button/primary-button'
-import { SearchInput } from '@/app/w/knowledge/components/search-input/search-input'
+import { ActionBar } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar'
+import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components/icons/document-icons'
+import { PrimaryButton } from '@/app/workspace/[workspaceId]/knowledge/components/primary-button/primary-button'
+import { SearchInput } from '@/app/workspace/[workspaceId]/knowledge/components/search-input/search-input'
import { useKnowledgeBase, useKnowledgeBaseDocuments } from '@/hooks/use-knowledge'
import { type DocumentData, useKnowledgeStore } from '@/stores/knowledge/store'
import { useSidebarStore } from '@/stores/sidebar/store'
@@ -122,6 +122,8 @@ export function KnowledgeBase({
}: KnowledgeBaseProps) {
const { mode, isExpanded } = useSidebarStore()
const { removeKnowledgeBase } = useKnowledgeStore()
+ const params = useParams()
+ const workspaceId = params.workspaceId as string
const {
knowledgeBase,
isLoading: isLoadingKnowledgeBase,
@@ -402,11 +404,11 @@ export function KnowledgeBase({
const handleDocumentClick = (docId: string) => {
// Find the document to get its filename
const document = documents.find((doc) => doc.id === docId)
- const params = new URLSearchParams({
+ const urlParams = new URLSearchParams({
kbName: knowledgeBaseName, // Use the instantly available name
docName: document?.filename || 'Document',
})
- router.push(`/w/knowledge/${id}/${docId}?${params.toString()}`)
+ router.push(`/workspace/${workspaceId}/knowledge/${id}/${docId}?${urlParams.toString()}`)
}
const handleDeleteKnowledgeBase = async () => {
@@ -428,7 +430,7 @@ export function KnowledgeBase({
if (result.success) {
// Remove from store and redirect to knowledge bases list
removeKnowledgeBase(id)
- router.push('/w/knowledge')
+ router.push(`/workspace/${workspaceId}/knowledge`)
} else {
throw new Error(result.error || 'Failed to delete knowledge base')
}
@@ -741,7 +743,7 @@ export function KnowledgeBase({
{
id: 'knowledge-root',
label: 'Knowledge',
- href: '/w/knowledge',
+ href: '/knowledge',
},
{
id: `knowledge-base-${id}`,
@@ -760,7 +762,7 @@ export function KnowledgeBase({
{
id: 'knowledge-root',
label: 'Knowledge',
- href: '/w/knowledge',
+ href: '/knowledge',
},
{
id: 'error',
diff --git a/apps/sim/app/w/knowledge/[id]/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar.tsx
similarity index 100%
rename from apps/sim/app/w/knowledge/[id]/components/action-bar/action-bar.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar.tsx
diff --git a/apps/sim/app/w/knowledge/[id]/components/knowledge-base-loading/knowledge-base-loading.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/knowledge-base-loading/knowledge-base-loading.tsx
similarity index 99%
rename from apps/sim/app/w/knowledge/[id]/components/knowledge-base-loading/knowledge-base-loading.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/knowledge-base-loading/knowledge-base-loading.tsx
index f969e7ae7f2..150979ddb66 100644
--- a/apps/sim/app/w/knowledge/[id]/components/knowledge-base-loading/knowledge-base-loading.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/knowledge-base-loading/knowledge-base-loading.tsx
@@ -19,7 +19,7 @@ export function KnowledgeBaseLoading({ knowledgeBaseName }: KnowledgeBaseLoading
{
id: 'knowledge-root',
label: 'Knowledge',
- href: '/w/knowledge',
+ href: '/knowledge',
},
{
id: 'knowledge-base-loading',
diff --git a/apps/sim/app/w/knowledge/[id]/page.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/page.tsx
similarity index 100%
rename from apps/sim/app/w/knowledge/[id]/page.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/[id]/page.tsx
diff --git a/apps/sim/app/w/knowledge/components/base-overview/base-overview.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-overview/base-overview.tsx
similarity index 95%
rename from apps/sim/app/w/knowledge/components/base-overview/base-overview.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/components/base-overview/base-overview.tsx
index 4da5fdd9ab7..223c9e9c90f 100644
--- a/apps/sim/app/w/knowledge/components/base-overview/base-overview.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-overview/base-overview.tsx
@@ -18,7 +18,7 @@ export function BaseOverview({ id, title, docCount, description }: BaseOverviewP
const params = new URLSearchParams({
kbName: title,
})
- const href = `/w/knowledge/${id || title.toLowerCase().replace(/\s+/g, '-')}?${params.toString()}`
+ const href = `/knowledge/${id || title.toLowerCase().replace(/\s+/g, '-')}?${params.toString()}`
const handleCopy = async (e: React.MouseEvent) => {
e.preventDefault()
diff --git a/apps/sim/app/w/knowledge/components/create-modal/create-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-modal/create-modal.tsx
similarity index 99%
rename from apps/sim/app/w/knowledge/components/create-modal/create-modal.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/components/create-modal/create-modal.tsx
index 5fbfd6effa2..77bf98e8fb8 100644
--- a/apps/sim/app/w/knowledge/components/create-modal/create-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-modal/create-modal.tsx
@@ -12,7 +12,7 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { createLogger } from '@/lib/logs/console-logger'
-import { getDocumentIcon } from '@/app/w/knowledge/components/icons/document-icons'
+import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components/icons/document-icons'
import type { DocumentData, KnowledgeBaseData } from '@/stores/knowledge/store'
import { useKnowledgeStore } from '@/stores/knowledge/store'
diff --git a/apps/sim/app/w/knowledge/components/empty-state-card/empty-state-card.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/empty-state-card/empty-state-card.tsx
similarity index 100%
rename from apps/sim/app/w/knowledge/components/empty-state-card/empty-state-card.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/components/empty-state-card/empty-state-card.tsx
diff --git a/apps/sim/app/w/knowledge/components/icons/document-icons.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/icons/document-icons.tsx
similarity index 100%
rename from apps/sim/app/w/knowledge/components/icons/document-icons.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/components/icons/document-icons.tsx
diff --git a/apps/sim/app/w/knowledge/components/knowledge-header/knowledge-header.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-header/knowledge-header.tsx
similarity index 100%
rename from apps/sim/app/w/knowledge/components/knowledge-header/knowledge-header.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-header/knowledge-header.tsx
diff --git a/apps/sim/app/w/knowledge/components/primary-button/primary-button.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/primary-button/primary-button.tsx
similarity index 100%
rename from apps/sim/app/w/knowledge/components/primary-button/primary-button.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/components/primary-button/primary-button.tsx
diff --git a/apps/sim/app/w/knowledge/components/search-input/search-input.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/search-input/search-input.tsx
similarity index 100%
rename from apps/sim/app/w/knowledge/components/search-input/search-input.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/components/search-input/search-input.tsx
diff --git a/apps/sim/app/w/knowledge/components/skeletons/knowledge-base-card-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/skeletons/knowledge-base-card-skeleton.tsx
similarity index 100%
rename from apps/sim/app/w/knowledge/components/skeletons/knowledge-base-card-skeleton.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/components/skeletons/knowledge-base-card-skeleton.tsx
diff --git a/apps/sim/app/w/knowledge/components/skeletons/table-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/skeletons/table-skeleton.tsx
similarity index 100%
rename from apps/sim/app/w/knowledge/components/skeletons/table-skeleton.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/components/skeletons/table-skeleton.tsx
diff --git a/apps/sim/app/w/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx
similarity index 100%
rename from apps/sim/app/w/knowledge/knowledge.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx
diff --git a/apps/sim/app/w/knowledge/loading.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/loading.tsx
similarity index 100%
rename from apps/sim/app/w/knowledge/loading.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/loading.tsx
diff --git a/apps/sim/app/w/knowledge/page.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/page.tsx
similarity index 100%
rename from apps/sim/app/w/knowledge/page.tsx
rename to apps/sim/app/workspace/[workspaceId]/knowledge/page.tsx
diff --git a/apps/sim/app/w/layout.tsx b/apps/sim/app/workspace/[workspaceId]/layout.tsx
similarity index 79%
rename from apps/sim/app/w/layout.tsx
rename to apps/sim/app/workspace/[workspaceId]/layout.tsx
index 42ac184a820..261b5d42224 100644
--- a/apps/sim/app/w/layout.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/layout.tsx
@@ -1,6 +1,6 @@
import { WorkspaceProvider } from '@/providers/workspace-provider'
-import Providers from './components/providers/providers'
-import { Sidebar } from './components/sidebar/sidebar'
+import Providers from './w/components/providers/providers'
+import { Sidebar } from './w/components/sidebar/sidebar'
export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
return (
diff --git a/apps/sim/app/w/logs/components/control-bar/control-bar.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/control-bar/control-bar.tsx
similarity index 100%
rename from apps/sim/app/w/logs/components/control-bar/control-bar.tsx
rename to apps/sim/app/workspace/[workspaceId]/logs/components/control-bar/control-bar.tsx
diff --git a/apps/sim/app/w/logs/components/filters/components/filter-section.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/filter-section.tsx
similarity index 100%
rename from apps/sim/app/w/logs/components/filters/components/filter-section.tsx
rename to apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/filter-section.tsx
diff --git a/apps/sim/app/w/logs/components/filters/components/folder.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/folder.tsx
similarity index 93%
rename from apps/sim/app/w/logs/components/filters/components/folder.tsx
rename to apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/folder.tsx
index 025e20a7193..bc7432758f7 100644
--- a/apps/sim/app/w/logs/components/filters/components/folder.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/folder.tsx
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'
import { Check, ChevronDown, Folder } from 'lucide-react'
+import { useParams } from 'next/navigation'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
@@ -8,9 +9,8 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
-import { useFilterStore } from '@/app/w/logs/stores/store'
+import { useFilterStore } from '@/app/workspace/[workspaceId]/logs/stores/store'
import { useFolderStore } from '@/stores/folders/store'
-import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
interface FolderOption {
id: string
@@ -22,7 +22,8 @@ interface FolderOption {
export default function FolderFilter() {
const { folderIds, toggleFolderId, setFolderIds } = useFilterStore()
const { getFolderTree, getFolderPath, fetchFolders } = useFolderStore()
- const { activeWorkspaceId } = useWorkflowRegistry()
+ const params = useParams()
+ const workspaceId = params.workspaceId as string
const [folders, setFolders] = useState
([])
const [loading, setLoading] = useState(true)
@@ -31,9 +32,9 @@ export default function FolderFilter() {
const fetchFoldersData = async () => {
try {
setLoading(true)
- if (activeWorkspaceId) {
- await fetchFolders(activeWorkspaceId)
- const folderTree = getFolderTree(activeWorkspaceId)
+ if (workspaceId) {
+ await fetchFolders(workspaceId)
+ const folderTree = getFolderTree(workspaceId)
// Flatten the folder tree and create options with full paths
const flattenFolders = (nodes: any[], parentPath = ''): FolderOption[] => {
@@ -68,7 +69,7 @@ export default function FolderFilter() {
}
fetchFoldersData()
- }, [activeWorkspaceId, fetchFolders, getFolderTree])
+ }, [workspaceId, fetchFolders, getFolderTree])
// Get display text for the dropdown button
const getSelectedFoldersText = () => {
diff --git a/apps/sim/app/w/logs/components/filters/components/level.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/level.tsx
similarity index 91%
rename from apps/sim/app/w/logs/components/filters/components/level.tsx
rename to apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/level.tsx
index 78550907e93..c83706a5b40 100644
--- a/apps/sim/app/w/logs/components/filters/components/level.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/level.tsx
@@ -6,8 +6,8 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
-import { useFilterStore } from '@/app/w/logs/stores/store'
-import type { LogLevel } from '@/app/w/logs/stores/types'
+import { useFilterStore } from '@/app/workspace/[workspaceId]/logs/stores/store'
+import type { LogLevel } from '@/app/workspace/[workspaceId]/logs/stores/types'
export default function Level() {
const { level, setLevel } = useFilterStore()
diff --git a/apps/sim/app/w/logs/components/filters/components/timeline.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/timeline.tsx
similarity index 88%
rename from apps/sim/app/w/logs/components/filters/components/timeline.tsx
rename to apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/timeline.tsx
index 39ae02fb0d1..0a475b6f1dc 100644
--- a/apps/sim/app/w/logs/components/filters/components/timeline.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/timeline.tsx
@@ -6,8 +6,8 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
-import { useFilterStore } from '@/app/w/logs/stores/store'
-import type { TimeRange } from '@/app/w/logs/stores/types'
+import { useFilterStore } from '@/app/workspace/[workspaceId]/logs/stores/store'
+import type { TimeRange } from '@/app/workspace/[workspaceId]/logs/stores/types'
export default function Timeline() {
const { timeRange, setTimeRange } = useFilterStore()
diff --git a/apps/sim/app/w/logs/components/filters/components/trigger.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/trigger.tsx
similarity index 97%
rename from apps/sim/app/w/logs/components/filters/components/trigger.tsx
rename to apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/trigger.tsx
index 760649d35be..1cabe0583c9 100644
--- a/apps/sim/app/w/logs/components/filters/components/trigger.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/trigger.tsx
@@ -7,7 +7,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
-import { useFilterStore } from '@/app/w/logs/stores/store'
+import { useFilterStore } from '@/app/workspace/[workspaceId]/logs/stores/store'
import type { TriggerType } from '../../../stores/types'
export default function Trigger() {
diff --git a/apps/sim/app/w/logs/components/filters/components/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx
similarity index 97%
rename from apps/sim/app/w/logs/components/filters/components/workflow.tsx
rename to apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx
index d5f94b6de74..47081d31674 100644
--- a/apps/sim/app/w/logs/components/filters/components/workflow.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx
@@ -8,7 +8,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
-import { useFilterStore } from '@/app/w/logs/stores/store'
+import { useFilterStore } from '@/app/workspace/[workspaceId]/logs/stores/store'
interface WorkflowOption {
id: string
diff --git a/apps/sim/app/w/logs/components/filters/filters.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/filters.tsx
similarity index 100%
rename from apps/sim/app/w/logs/components/filters/filters.tsx
rename to apps/sim/app/workspace/[workspaceId]/logs/components/filters/filters.tsx
diff --git a/apps/sim/app/w/logs/components/sidebar/components/markdown-renderer.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/components/markdown-renderer.tsx
similarity index 100%
rename from apps/sim/app/w/logs/components/sidebar/components/markdown-renderer.tsx
rename to apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/components/markdown-renderer.tsx
diff --git a/apps/sim/app/w/logs/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx
similarity index 99%
rename from apps/sim/app/w/logs/components/sidebar/sidebar.tsx
rename to apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx
index a47131e42d5..65d070356b8 100644
--- a/apps/sim/app/w/logs/components/sidebar/sidebar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx
@@ -7,8 +7,8 @@ import { CopyButton } from '@/components/ui/copy-button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { redactApiKeys } from '@/lib/utils'
-import type { WorkflowLog } from '@/app/w/logs/stores/types'
-import { formatDate } from '@/app/w/logs/utils/format-date'
+import type { WorkflowLog } from '@/app/workspace/[workspaceId]/logs/stores/types'
+import { formatDate } from '@/app/workspace/[workspaceId]/logs/utils/format-date'
import { formatCost } from '@/providers/utils'
import { ToolCallsDisplay } from '../tool-calls/tool-calls-display'
import { TraceSpansDisplay } from '../trace-spans/trace-spans-display'
diff --git a/apps/sim/app/w/logs/components/tool-calls/tool-calls-display.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/tool-calls/tool-calls-display.tsx
similarity index 100%
rename from apps/sim/app/w/logs/components/tool-calls/tool-calls-display.tsx
rename to apps/sim/app/workspace/[workspaceId]/logs/components/tool-calls/tool-calls-display.tsx
diff --git a/apps/sim/app/w/logs/components/trace-spans/trace-spans-display.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans-display.tsx
similarity index 100%
rename from apps/sim/app/w/logs/components/trace-spans/trace-spans-display.tsx
rename to apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans-display.tsx
diff --git a/apps/sim/app/w/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
similarity index 100%
rename from apps/sim/app/w/logs/logs.tsx
rename to apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
diff --git a/apps/sim/app/w/logs/page.tsx b/apps/sim/app/workspace/[workspaceId]/logs/page.tsx
similarity index 100%
rename from apps/sim/app/w/logs/page.tsx
rename to apps/sim/app/workspace/[workspaceId]/logs/page.tsx
diff --git a/apps/sim/app/w/logs/stores/store.ts b/apps/sim/app/workspace/[workspaceId]/logs/stores/store.ts
similarity index 100%
rename from apps/sim/app/w/logs/stores/store.ts
rename to apps/sim/app/workspace/[workspaceId]/logs/stores/store.ts
diff --git a/apps/sim/app/w/logs/stores/types.ts b/apps/sim/app/workspace/[workspaceId]/logs/stores/types.ts
similarity index 100%
rename from apps/sim/app/w/logs/stores/types.ts
rename to apps/sim/app/workspace/[workspaceId]/logs/stores/types.ts
diff --git a/apps/sim/app/w/logs/utils/format-date.ts b/apps/sim/app/workspace/[workspaceId]/logs/utils/format-date.ts
similarity index 100%
rename from apps/sim/app/w/logs/utils/format-date.ts
rename to apps/sim/app/workspace/[workspaceId]/logs/utils/format-date.ts
diff --git a/apps/sim/app/w/marketplace/components/control-bar/control-bar.tsx b/apps/sim/app/workspace/[workspaceId]/marketplace/components/control-bar/control-bar.tsx
similarity index 100%
rename from apps/sim/app/w/marketplace/components/control-bar/control-bar.tsx
rename to apps/sim/app/workspace/[workspaceId]/marketplace/components/control-bar/control-bar.tsx
diff --git a/apps/sim/app/w/marketplace/components/error-message.tsx b/apps/sim/app/workspace/[workspaceId]/marketplace/components/error-message.tsx
similarity index 100%
rename from apps/sim/app/w/marketplace/components/error-message.tsx
rename to apps/sim/app/workspace/[workspaceId]/marketplace/components/error-message.tsx
diff --git a/apps/sim/app/w/marketplace/components/section.tsx b/apps/sim/app/workspace/[workspaceId]/marketplace/components/section.tsx
similarity index 100%
rename from apps/sim/app/w/marketplace/components/section.tsx
rename to apps/sim/app/workspace/[workspaceId]/marketplace/components/section.tsx
diff --git a/apps/sim/app/w/marketplace/components/toolbar/toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/marketplace/components/toolbar/toolbar.tsx
similarity index 100%
rename from apps/sim/app/w/marketplace/components/toolbar/toolbar.tsx
rename to apps/sim/app/workspace/[workspaceId]/marketplace/components/toolbar/toolbar.tsx
diff --git a/apps/sim/app/w/marketplace/components/workflow-card-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/marketplace/components/workflow-card-skeleton.tsx
similarity index 100%
rename from apps/sim/app/w/marketplace/components/workflow-card-skeleton.tsx
rename to apps/sim/app/workspace/[workspaceId]/marketplace/components/workflow-card-skeleton.tsx
diff --git a/apps/sim/app/w/marketplace/components/workflow-card.tsx b/apps/sim/app/workspace/[workspaceId]/marketplace/components/workflow-card.tsx
similarity index 94%
rename from apps/sim/app/w/marketplace/components/workflow-card.tsx
rename to apps/sim/app/workspace/[workspaceId]/marketplace/components/workflow-card.tsx
index 82db83df0b7..76667b1d724 100644
--- a/apps/sim/app/w/marketplace/components/workflow-card.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/marketplace/components/workflow-card.tsx
@@ -2,9 +2,9 @@
import { useEffect, useState } from 'react'
import { Eye } from 'lucide-react'
-import { useRouter } from 'next/navigation'
+import { useParams, useRouter } from 'next/navigation'
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
-import { WorkflowPreview } from '@/app/w/components/workflow-preview/workflow-preview'
+import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { Workflow } from '../marketplace'
@@ -28,6 +28,8 @@ interface WorkflowCardProps {
export function WorkflowCard({ workflow, onHover }: WorkflowCardProps) {
const [isPreviewReady, setIsPreviewReady] = useState(!!workflow.workflowState)
const router = useRouter()
+ const params = useParams()
+ const workspaceId = params.workspaceId as string
const { createWorkflow } = useWorkflowRegistry()
// When workflow state becomes available, update preview ready state
@@ -71,7 +73,7 @@ export function WorkflowCard({ workflow, onHover }: WorkflowCardProps) {
})
// Navigate to the new workflow
- router.push(`/w/${newWorkflowId}`)
+ router.push(`/workspace/${workspaceId}/w/${newWorkflowId}`)
} else {
console.error('Cannot import workflow: state is not available')
}
diff --git a/apps/sim/app/w/marketplace/constants/categories.tsx b/apps/sim/app/workspace/[workspaceId]/marketplace/constants/categories.tsx
similarity index 100%
rename from apps/sim/app/w/marketplace/constants/categories.tsx
rename to apps/sim/app/workspace/[workspaceId]/marketplace/constants/categories.tsx
diff --git a/apps/sim/app/w/marketplace/marketplace.tsx b/apps/sim/app/workspace/[workspaceId]/marketplace/marketplace.tsx
similarity index 100%
rename from apps/sim/app/w/marketplace/marketplace.tsx
rename to apps/sim/app/workspace/[workspaceId]/marketplace/marketplace.tsx
diff --git a/apps/sim/app/w/marketplace/page.tsx b/apps/sim/app/workspace/[workspaceId]/marketplace/page.tsx
similarity index 100%
rename from apps/sim/app/w/marketplace/page.tsx
rename to apps/sim/app/workspace/[workspaceId]/marketplace/page.tsx
diff --git a/apps/sim/app/workspace/[workspaceId]/page.tsx b/apps/sim/app/workspace/[workspaceId]/page.tsx
new file mode 100644
index 00000000000..bd93acadf88
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/page.tsx
@@ -0,0 +1,10 @@
+import { redirect } from 'next/navigation'
+
+export default async function WorkspacePage({
+ params,
+}: {
+ params: Promise<{ workspaceId: string }>
+}) {
+ const { workspaceId } = await params
+ redirect(`/workspace/${workspaceId}/w`)
+}
diff --git a/apps/sim/app/w/[id]/components/code-prompt-bar/code-prompt-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/code-prompt-bar/code-prompt-bar.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/code-prompt-bar/code-prompt-bar.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/code-prompt-bar/code-prompt-bar.tsx
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/chat-deploy/chat-deploy.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/chat-deploy.tsx
similarity index 99%
rename from apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/chat-deploy/chat-deploy.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/chat-deploy.tsx
index ef4b60ed270..f680c2ffa99 100644
--- a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/chat-deploy/chat-deploy.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/chat-deploy.tsx
@@ -33,7 +33,7 @@ import { Textarea } from '@/components/ui/textarea'
import { createLogger } from '@/lib/logs/console-logger'
import { getBaseDomain } from '@/lib/urls/utils'
import { cn } from '@/lib/utils'
-import { OutputSelect } from '@/app/w/[id]/components/panel/components/chat/components/output-select/output-select'
+import { OutputSelect } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/components/output-select/output-select'
import { useNotificationStore } from '@/stores/notifications/store'
import type { OutputConfig } from '@/stores/panel/chat/types'
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deploy-form/deploy-form.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deploy-form/deploy-form.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deploy-form/deploy-form.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deploy-form/deploy-form.tsx
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-endpoint/api-endpoint.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-endpoint/api-endpoint.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-endpoint/api-endpoint.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-endpoint/api-endpoint.tsx
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-key/api-key.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-key/api-key.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-key/api-key.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-key/api-key.tsx
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/deploy-status/deploy-status.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/deploy-status/deploy-status.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/deploy-status/deploy-status.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/deploy-status/deploy-status.tsx
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx
similarity index 88%
rename from apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx
index f8a794b7fd1..5f98febf4ce 100644
--- a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx
@@ -15,10 +15,10 @@ import {
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
-import { ApiEndpoint } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-endpoint/api-endpoint'
-import { ApiKey } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-key/api-key'
-import { DeployStatus } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/deploy-status/deploy-status'
-import { ExampleCommand } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command'
+import { ApiEndpoint } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-endpoint/api-endpoint'
+import { ApiKey } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-key/api-key'
+import { DeployStatus } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/deploy-status/deploy-status'
+import { ExampleCommand } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command'
import { useNotificationStore } from '@/stores/notifications/store'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
import { DeployedWorkflowModal } from '../../../deployment-controls/components/deployed-workflow-modal'
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx
similarity index 98%
rename from apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/deploy-modal.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx
index af068534848..8595d333f88 100644
--- a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/deploy-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx
@@ -20,9 +20,9 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
-import { ChatDeploy } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/chat-deploy/chat-deploy'
-import { DeployForm } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/deploy-form/deploy-form'
-import { DeploymentInfo } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info'
+import { ChatDeploy } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/chat-deploy'
+import { DeployForm } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deploy-form/deploy-form'
+import { DeploymentInfo } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info'
import { useNotificationStore } from '@/stores/notifications/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx
similarity index 96%
rename from apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx
index 187c4fe10e9..500eac1ff38 100644
--- a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx
@@ -6,7 +6,7 @@ import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
-import { WorkflowPreview } from '@/app/w/components/workflow-preview/workflow-preview'
+import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/deployment-controls.test.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deployment-controls/deployment-controls.test.ts
similarity index 100%
rename from apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/deployment-controls.test.ts
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deployment-controls/deployment-controls.test.ts
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/deployment-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deployment-controls/deployment-controls.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/deployment-controls.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deployment-controls/deployment-controls.tsx
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/history-dropdown-item/history-dropdown-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/history-dropdown-item/history-dropdown-item.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/control-bar/components/history-dropdown-item/history-dropdown-item.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/history-dropdown-item/history-dropdown-item.tsx
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/marketplace-modal/marketplace-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/marketplace-modal/marketplace-modal.tsx
similarity index 99%
rename from apps/sim/app/w/[id]/components/control-bar/components/marketplace-modal/marketplace-modal.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/marketplace-modal/marketplace-modal.tsx
index 3d64dcc06f2..7b7bdb59687 100644
--- a/apps/sim/app/w/[id]/components/control-bar/components/marketplace-modal/marketplace-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/marketplace-modal/marketplace-modal.tsx
@@ -35,7 +35,7 @@ import {
getCategoryColor,
getCategoryIcon,
getCategoryLabel,
-} from '@/app/w/marketplace/constants/categories'
+} from '@/app/workspace/[workspaceId]/marketplace/constants/categories'
import { useNotificationStore } from '@/stores/notifications/store'
import { getWorkflowWithValues } from '@/stores/workflows'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/notification-dropdown-item/notification-dropdown-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/notification-dropdown-item/notification-dropdown-item.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/control-bar/components/notification-dropdown-item/notification-dropdown-item.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/notification-dropdown-item/notification-dropdown-item.tsx
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/user-avatar-stack/components/user-avatar/user-avatar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/components/user-avatar/user-avatar.tsx
similarity index 84%
rename from apps/sim/app/w/[id]/components/control-bar/components/user-avatar-stack/components/user-avatar/user-avatar.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/components/user-avatar/user-avatar.tsx
index 764b3fcc931..0fe708ae786 100644
--- a/apps/sim/app/w/[id]/components/control-bar/components/user-avatar-stack/components/user-avatar/user-avatar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/components/user-avatar/user-avatar.tsx
@@ -4,7 +4,7 @@ import { type CSSProperties, useMemo } from 'react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
interface AvatarProps {
- connectionId: number
+ connectionId: string | number
name?: string
color?: string
tooltipContent?: React.ReactNode | null
@@ -25,12 +25,18 @@ const APP_COLORS = [
/**
* Generate a deterministic gradient based on a connection ID
*/
-function generateGradient(connectionId: number): string {
- // Use the connection ID to select a color pair from our palette
- const colorPair = APP_COLORS[connectionId % APP_COLORS.length]
+function generateGradient(connectionId: string | number): string {
+ // Convert connectionId to a number for consistent hashing
+ const numericId =
+ typeof connectionId === 'string'
+ ? Math.abs(connectionId.split('').reduce((a, b) => a + b.charCodeAt(0), 0))
+ : connectionId
+
+ // Use the numeric ID to select a color pair from our palette
+ const colorPair = APP_COLORS[numericId % APP_COLORS.length]
// Add a slight rotation to the gradient based on connection ID for variety
- const rotation = (connectionId * 25) % 360
+ const rotation = (numericId * 25) % 360
return `linear-gradient(${rotation}deg, ${colorPair.from}, ${colorPair.to})`
}
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/user-avatar-stack/user-avatar-stack.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/user-avatar-stack.tsx
similarity index 95%
rename from apps/sim/app/w/[id]/components/control-bar/components/user-avatar-stack/user-avatar-stack.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/user-avatar-stack.tsx
index b9b0973d3f5..2489fda0622 100644
--- a/apps/sim/app/w/[id]/components/control-bar/components/user-avatar-stack/user-avatar-stack.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/user-avatar-stack.tsx
@@ -5,7 +5,7 @@ import { usePresence } from '../../../../hooks/use-presence'
import { UserAvatar } from './components/user-avatar/user-avatar'
interface User {
- connectionId: number
+ connectionId: string | number
name?: string
color?: string
info?: string
@@ -80,7 +80,7 @@ export function UserAvatarStack({
{/* Render overflow indicator if there are more users */}
{overflowCount > 0 && (
{
+ // Get and sort regular workflows by last modified (newest first)
+ const regularWorkflows = Object.values(workflows)
+ .filter((workflow) => workflow.workspaceId === workspaceId)
+ .filter((workflow) => workflow.marketplaceData?.status !== 'temp')
+ .sort((a, b) => {
+ const dateA =
+ a.lastModified instanceof Date
+ ? a.lastModified.getTime()
+ : new Date(a.lastModified).getTime()
+ const dateB =
+ b.lastModified instanceof Date
+ ? b.lastModified.getTime()
+ : new Date(b.lastModified).getTime()
+ return dateB - dateA
+ })
+
+ // Group workflows by folder
+ const workflowsByFolder = regularWorkflows.reduce(
+ (acc, workflow) => {
+ const folderId = workflow.folderId || 'root'
+ if (!acc[folderId]) acc[folderId] = []
+ acc[folderId].push(workflow)
+ return acc
+ },
+ {} as Record
+ )
+
+ const orderedWorkflows: typeof regularWorkflows = []
+
+ // Recursively collect workflows from expanded folders
+ const collectFromFolders = (folders: ReturnType) => {
+ folders.forEach((folder) => {
+ if (expandedFolders.has(folder.id)) {
+ orderedWorkflows.push(...(workflowsByFolder[folder.id] || []))
+ if (folder.children.length > 0) {
+ collectFromFolders(folder.children)
+ }
+ }
+ })
+ }
+
+ // Get workflows from expanded folders first, then root workflows
+ if (workspaceId) collectFromFolders(getFolderTree(workspaceId))
+ orderedWorkflows.push(...(workflowsByFolder.root || []))
+
+ return orderedWorkflows
+ }
+
/**
* Handle deleting the current workflow
*/
const handleDeleteWorkflow = () => {
if (!activeWorkflowId || !userPermissions.canEdit) return
- const workflowIds = Object.keys(workflows)
- const currentIndex = workflowIds.indexOf(activeWorkflowId)
+ const sidebarWorkflows = getSidebarOrderedWorkflows()
+ const currentIndex = sidebarWorkflows.findIndex((w) => w.id === activeWorkflowId)
- // Find the next workflow to navigate to
- let nextWorkflowId = null
- if (workflowIds.length > 1) {
- // Try next workflow, then previous, then any other
- if (currentIndex < workflowIds.length - 1) {
- nextWorkflowId = workflowIds[currentIndex + 1]
+ // Find next workflow: try next, then previous
+ let nextWorkflowId: string | null = null
+ if (sidebarWorkflows.length > 1) {
+ if (currentIndex < sidebarWorkflows.length - 1) {
+ nextWorkflowId = sidebarWorkflows[currentIndex + 1].id
} else if (currentIndex > 0) {
- nextWorkflowId = workflowIds[currentIndex - 1]
- } else {
- nextWorkflowId = workflowIds.find((id) => id !== activeWorkflowId) || null
+ nextWorkflowId = sidebarWorkflows[currentIndex - 1].id
}
}
- // Navigate to the next workflow or home
+ // Navigate to next workflow or workspace home
if (nextWorkflowId) {
- router.push(`/w/${nextWorkflowId}`)
+ router.push(`/workspace/${workspaceId}/w/${nextWorkflowId}`)
} else {
- router.push('/')
+ router.push(`/workspace/${workspaceId}`)
}
// Remove the workflow from the registry
@@ -573,8 +625,17 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
const handleDuplicateWorkflow = async () => {
if (!activeWorkflowId || !userPermissions.canEdit) return
- // Duplicate the workflow - no automatic navigation
- await duplicateWorkflow(activeWorkflowId)
+ try {
+ const newWorkflow = await duplicateWorkflow(activeWorkflowId)
+ if (newWorkflow) {
+ router.push(`/workspace/${workspaceId}/w/${newWorkflow}`)
+ } else {
+ addNotification('error', 'Failed to duplicate workflow', activeWorkflowId)
+ }
+ } catch (error) {
+ logger.error('Error duplicating workflow:', { error })
+ addNotification('error', 'Failed to duplicate workflow', activeWorkflowId)
+ }
}
/**
diff --git a/apps/sim/app/w/[id]/components/copilot/copilot.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/copilot/copilot.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/copilot/copilot.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/copilot/copilot.tsx
diff --git a/apps/sim/app/w/[id]/components/error/index.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/error/index.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/error/index.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/error/index.tsx
diff --git a/apps/sim/app/w/[id]/components/loop-node/components/loop-badges.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/loop-node/components/loop-badges.test.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/loop-node/components/loop-badges.test.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/loop-node/components/loop-badges.test.tsx
diff --git a/apps/sim/app/w/[id]/components/loop-node/components/loop-badges.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/loop-node/components/loop-badges.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/loop-node/components/loop-badges.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/loop-node/components/loop-badges.tsx
diff --git a/apps/sim/app/w/[id]/components/loop-node/loop-config.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/loop-node/loop-config.ts
similarity index 100%
rename from apps/sim/app/w/[id]/components/loop-node/loop-config.ts
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/loop-node/loop-config.ts
diff --git a/apps/sim/app/w/[id]/components/loop-node/loop-node.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/loop-node/loop-node.test.tsx
similarity index 94%
rename from apps/sim/app/w/[id]/components/loop-node/loop-node.test.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/loop-node/loop-node.test.tsx
index 9625cb5f611..14f5343666f 100644
--- a/apps/sim/app/w/[id]/components/loop-node/loop-node.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/loop-node/loop-node.test.tsx
@@ -2,7 +2,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { LoopNodeComponent } from './loop-node'
-// Mock dependencies that don't need DOM
vi.mock('@/stores/workflows/workflow/store', () => ({
useWorkflowStore: vi.fn(),
}))
@@ -16,7 +15,6 @@ vi.mock('@/lib/logs/console-logger', () => ({
})),
}))
-// Mock ReactFlow components and hooks
vi.mock('reactflow', () => ({
Handle: ({ id, type, position }: any) => ({ id, type, position }),
Position: {
@@ -32,7 +30,6 @@ vi.mock('reactflow', () => ({
memo: (component: any) => component,
}))
-// Mock React hooks
vi.mock('react', async () => {
const actual = await vi.importActual('react')
return {
@@ -43,7 +40,6 @@ vi.mock('react', async () => {
}
})
-// Mock UI components
vi.mock('@/components/ui/button', () => ({
Button: ({ children, onClick, ...props }: any) => ({ children, onClick, ...props }),
}))
@@ -60,7 +56,6 @@ vi.mock('@/lib/utils', () => ({
cn: (...classes: any[]) => classes.filter(Boolean).join(' '),
}))
-// Mock the LoopBadges component
vi.mock('./components/loop-badges', () => ({
LoopBadges: ({ loopId }: any) => ({ loopId }),
}))
@@ -87,8 +82,6 @@ describe('LoopNodeComponent', () => {
beforeEach(() => {
vi.clearAllMocks()
- // Mock useWorkflowStore
-
;(useWorkflowStore as any).mockImplementation((selector: any) => {
const state = {
removeBlock: mockRemoveBlock,
@@ -96,7 +89,6 @@ describe('LoopNodeComponent', () => {
return selector(state)
})
- // Mock getNodes
mockGetNodes.mockReturnValue([])
})
@@ -111,14 +103,12 @@ describe('LoopNodeComponent', () => {
})
it('should be a memoized component', () => {
- // Since we mocked memo to return the component as-is, we can verify it exists
expect(LoopNodeComponent).toBeDefined()
})
})
describe('Props Validation and Type Safety', () => {
it('should accept NodeProps interface', () => {
- // Test that the component accepts the correct prop types
const validProps = {
id: 'test-id',
type: 'loopNode' as const,
@@ -135,9 +125,7 @@ describe('LoopNodeComponent', () => {
dragging: false,
}
- // This tests that TypeScript compilation succeeds with these props
expect(() => {
- // We're not calling the component, just verifying the types
const _component: typeof LoopNodeComponent = LoopNodeComponent
expect(_component).toBeDefined()
}).not.toThrow()
@@ -163,10 +151,8 @@ describe('LoopNodeComponent', () => {
describe('Store Integration', () => {
it('should integrate with workflow store', () => {
- // Test that the component uses the store correctly
expect(useWorkflowStore).toBeDefined()
- // Verify the store selector function works
const mockState = { removeBlock: mockRemoveBlock }
const selector = vi.fn((state) => state.removeBlock)
@@ -181,7 +167,6 @@ describe('LoopNodeComponent', () => {
expect(mockRemoveBlock).toBeDefined()
expect(typeof mockRemoveBlock).toBe('function')
- // Test calling removeBlock
mockRemoveBlock('test-id')
expect(mockRemoveBlock).toHaveBeenCalledWith('test-id')
})
@@ -189,7 +174,6 @@ describe('LoopNodeComponent', () => {
describe('Component Logic Tests', () => {
it('should handle nesting level calculation logic', () => {
- // Test the nesting level calculation logic
const testCases = [
{ nodes: [], parentId: undefined, expectedLevel: 0 },
{ nodes: [{ id: 'parent', data: {} }], parentId: 'parent', expectedLevel: 1 },
diff --git a/apps/sim/app/w/[id]/components/loop-node/loop-node.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/loop-node/loop-node.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/loop-node/loop-node.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/loop-node/loop-node.tsx
diff --git a/apps/sim/app/w/[id]/components/notifications/notifications.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/notifications/notifications.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx
diff --git a/apps/sim/app/w/[id]/components/panel/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/chat.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/panel/components/chat/chat.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/chat.tsx
diff --git a/apps/sim/app/w/[id]/components/panel/components/chat/components/chat-message/chat-message.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/components/chat-message/chat-message.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/panel/components/chat/components/chat-message/chat-message.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/components/chat-message/chat-message.tsx
diff --git a/apps/sim/app/w/[id]/components/panel/components/chat/components/chat-modal/chat-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/components/chat-modal/chat-modal.tsx
similarity index 97%
rename from apps/sim/app/w/[id]/components/panel/components/chat/components/chat-modal/chat-modal.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/components/chat-modal/chat-modal.tsx
index 31b887312a2..f0b8d9498a2 100644
--- a/apps/sim/app/w/[id]/components/panel/components/chat/components/chat-modal/chat-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/components/chat-modal/chat-modal.tsx
@@ -4,8 +4,8 @@ import { type KeyboardEvent, useEffect, useMemo, useRef } from 'react'
import { ArrowUp, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
-import { JSONView } from '@/app/w/[id]/components/panel/components/console/components/json-view/json-view'
-import { useWorkflowExecution } from '@/app/w/[id]/hooks/use-workflow-execution'
+import { JSONView } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/components/json-view/json-view'
+import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
import { useExecutionStore } from '@/stores/execution/store'
import { useChatStore } from '@/stores/panel/chat/store'
import type { ChatMessage as ChatMessageType } from '@/stores/panel/chat/types'
diff --git a/apps/sim/app/w/[id]/components/panel/components/chat/components/output-select/output-select.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/components/output-select/output-select.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/panel/components/chat/components/output-select/output-select.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/components/output-select/output-select.tsx
diff --git a/apps/sim/app/w/[id]/components/panel/components/console/components/audio-player/audio-player.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/components/audio-player/audio-player.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/panel/components/console/components/audio-player/audio-player.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/components/audio-player/audio-player.tsx
diff --git a/apps/sim/app/w/[id]/components/panel/components/console/components/console-entry/console-entry.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/components/console-entry/console-entry.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/panel/components/console/components/console-entry/console-entry.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/components/console-entry/console-entry.tsx
diff --git a/apps/sim/app/w/[id]/components/panel/components/console/components/json-view/json-view.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/components/json-view/json-view.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/panel/components/console/components/json-view/json-view.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/components/json-view/json-view.tsx
diff --git a/apps/sim/app/w/[id]/components/panel/components/console/console.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/console.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/panel/components/console/console.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/console.tsx
diff --git a/apps/sim/app/w/[id]/components/panel/components/variables/variables.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/variables/variables.tsx
similarity index 99%
rename from apps/sim/app/w/[id]/components/panel/components/variables/variables.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/variables/variables.tsx
index 7954408c104..598a5aab282 100644
--- a/apps/sim/app/w/[id]/components/panel/components/variables/variables.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/variables/variables.tsx
@@ -42,12 +42,12 @@ export function Variables({ panelWidth }: VariablesProps) {
// Get variables for the current workflow
const workflowVariables = activeWorkflowId ? getVariablesByWorkflowId(activeWorkflowId) : []
- // Load variables when workflow changes
+ // Load variables when active workflow changes
useEffect(() => {
- if (activeWorkflowId && workflows[activeWorkflowId]) {
+ if (activeWorkflowId) {
loadVariables(activeWorkflowId)
}
- }, [activeWorkflowId, workflows, loadVariables])
+ }, [activeWorkflowId, loadVariables])
// Track editor references
const editorRefs = useRef>({})
diff --git a/apps/sim/app/w/[id]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx
similarity index 99%
rename from apps/sim/app/w/[id]/components/panel/panel.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx
index 7afc5e3eb1e..27494712dc0 100644
--- a/apps/sim/app/w/[id]/components/panel/panel.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx
@@ -5,8 +5,8 @@ import { Expand, PanelRight } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useChatStore } from '@/stores/panel/chat/store'
import { useConsoleStore } from '@/stores/panel/console/store'
+import { usePanelStore } from '@/stores/panel/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
-import { usePanelStore } from '../../../../../stores/panel/store'
import { Chat } from './components/chat/chat'
import { ChatModal } from './components/chat/components/chat-modal/chat-modal'
import { Console } from './components/console/console'
diff --git a/apps/sim/app/w/[id]/components/parallel-node/components/parallel-badges.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/parallel-node/components/parallel-badges.tsx
similarity index 94%
rename from apps/sim/app/w/[id]/components/parallel-node/components/parallel-badges.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/parallel-node/components/parallel-badges.tsx
index 5b1604e09ce..de57ce42071 100644
--- a/apps/sim/app/w/[id]/components/parallel-node/components/parallel-badges.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/parallel-node/components/parallel-badges.tsx
@@ -82,33 +82,12 @@ export function ParallelBadges({ nodeId, data }: ParallelBadgesProps) {
(newType: 'count' | 'collection') => {
if (isPreview) return // Don't allow changes in preview mode
- // Update the parallel type using collaborative function - this will persist to database
+ // Use single collaborative function that handles all the state changes atomically
collaborativeUpdateParallelType(nodeId, newType)
- // Reset values based on type
- if (newType === 'count') {
- collaborativeUpdateParallelCollection(nodeId, '')
- collaborativeUpdateParallelCount(nodeId, iterations)
- } else {
- collaborativeUpdateParallelCount(nodeId, 1)
- const collectionValue =
- typeof editorValue === 'string'
- ? editorValue || '[]'
- : JSON.stringify(editorValue) || '[]'
- collaborativeUpdateParallelCollection(nodeId, collectionValue)
- }
-
setTypePopoverOpen(false)
},
- [
- nodeId,
- iterations,
- editorValue,
- collaborativeUpdateParallelCount,
- collaborativeUpdateParallelCollection,
- collaborativeUpdateParallelType,
- isPreview,
- ]
+ [nodeId, collaborativeUpdateParallelType, isPreview]
)
// Handle iterations input change
diff --git a/apps/sim/app/w/[id]/components/parallel-node/parallel-config.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/parallel-node/parallel-config.ts
similarity index 100%
rename from apps/sim/app/w/[id]/components/parallel-node/parallel-config.ts
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/parallel-node/parallel-config.ts
diff --git a/apps/sim/app/w/[id]/components/parallel-node/parallel-node.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/parallel-node/parallel-node.test.tsx
similarity index 81%
rename from apps/sim/app/w/[id]/components/parallel-node/parallel-node.test.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/parallel-node/parallel-node.test.tsx
index 260a691795d..a6bf26b5f57 100644
--- a/apps/sim/app/w/[id]/components/parallel-node/parallel-node.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/parallel-node/parallel-node.test.tsx
@@ -2,7 +2,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { ParallelNodeComponent } from './parallel-node'
-// Mock dependencies that don't need DOM
vi.mock('@/stores/workflows/workflow/store', () => ({
useWorkflowStore: vi.fn(),
}))
@@ -16,7 +15,6 @@ vi.mock('@/lib/logs/console-logger', () => ({
})),
}))
-// Mock ReactFlow components and hooks
vi.mock('reactflow', () => ({
Handle: ({ id, type, position }: any) => ({ id, type, position }),
Position: {
@@ -32,7 +30,6 @@ vi.mock('reactflow', () => ({
memo: (component: any) => component,
}))
-// Mock React hooks
vi.mock('react', async () => {
const actual = await vi.importActual('react')
return {
@@ -43,7 +40,6 @@ vi.mock('react', async () => {
}
})
-// Mock UI components
vi.mock('@/components/ui/button', () => ({
Button: ({ children, onClick, ...props }: any) => ({ children, onClick, ...props }),
}))
@@ -52,15 +48,21 @@ vi.mock('@/components/ui/card', () => ({
Card: ({ children, ...props }: any) => ({ children, ...props }),
}))
-vi.mock('@/components/icons', () => ({
- StartIcon: ({ className }: any) => ({ className }),
+vi.mock('@/blocks/registry', () => ({
+ getBlock: vi.fn(() => ({
+ name: 'Mock Block',
+ description: 'Mock block description',
+ icon: () => null,
+ subBlocks: [],
+ outputs: {},
+ })),
+ getAllBlocks: vi.fn(() => ({})),
}))
vi.mock('@/lib/utils', () => ({
cn: (...classes: any[]) => classes.filter(Boolean).join(' '),
}))
-// Mock the ParallelBadges component
vi.mock('./components/parallel-badges', () => ({
ParallelBadges: ({ parallelId }: any) => ({ parallelId }),
}))
@@ -87,8 +89,6 @@ describe('ParallelNodeComponent', () => {
beforeEach(() => {
vi.clearAllMocks()
- // Mock useWorkflowStore
-
;(useWorkflowStore as any).mockImplementation((selector: any) => {
const state = {
removeBlock: mockRemoveBlock,
@@ -96,54 +96,33 @@ describe('ParallelNodeComponent', () => {
return selector(state)
})
- // Mock getNodes
mockGetNodes.mockReturnValue([])
})
describe('Component Definition and Structure', () => {
- it('should be defined as a function component', () => {
+ it.concurrent('should be defined as a function component', () => {
expect(ParallelNodeComponent).toBeDefined()
expect(typeof ParallelNodeComponent).toBe('function')
})
- it('should have correct display name', () => {
+ it.concurrent('should have correct display name', () => {
expect(ParallelNodeComponent.displayName).toBe('ParallelNodeComponent')
})
- it('should be a memoized component', () => {
- // Since we mocked memo to return the component as-is, we can verify it exists
+ it.concurrent('should be a memoized component', () => {
expect(ParallelNodeComponent).toBeDefined()
})
})
describe('Props Validation and Type Safety', () => {
- it('should accept NodeProps interface', () => {
- // Test that the component accepts the correct prop types
- const validProps = {
- id: 'test-id',
- type: 'parallelNode' as const,
- data: {
- width: 400,
- height: 300,
- state: 'valid' as const,
- },
- selected: false,
- zIndex: 1,
- isConnectable: true,
- xPos: 0,
- yPos: 0,
- dragging: false,
- }
-
- // This tests that TypeScript compilation succeeds with these props
+ it.concurrent('should accept NodeProps interface', () => {
expect(() => {
- // We're not calling the component, just verifying the types
const _component: typeof ParallelNodeComponent = ParallelNodeComponent
expect(_component).toBeDefined()
}).not.toThrow()
})
- it('should handle different data configurations', () => {
+ it.concurrent('should handle different data configurations', () => {
const configurations = [
{ width: 500, height: 300, state: 'valid' },
{ width: 800, height: 600, state: 'invalid' },
@@ -162,11 +141,9 @@ describe('ParallelNodeComponent', () => {
})
describe('Store Integration', () => {
- it('should integrate with workflow store', () => {
- // Test that the component uses the store correctly
+ it.concurrent('should integrate with workflow store', () => {
expect(useWorkflowStore).toBeDefined()
- // Verify the store selector function works
const mockState = { removeBlock: mockRemoveBlock }
const selector = vi.fn((state) => state.removeBlock)
@@ -177,19 +154,17 @@ describe('ParallelNodeComponent', () => {
expect(selector(mockState)).toBe(mockRemoveBlock)
})
- it('should handle removeBlock function', () => {
+ it.concurrent('should handle removeBlock function', () => {
expect(mockRemoveBlock).toBeDefined()
expect(typeof mockRemoveBlock).toBe('function')
- // Test calling removeBlock
mockRemoveBlock('test-id')
expect(mockRemoveBlock).toHaveBeenCalledWith('test-id')
})
})
describe('Component Logic Tests', () => {
- it('should handle nesting level calculation logic', () => {
- // Test the nesting level calculation logic (same as loop node)
+ it.concurrent('should handle nesting level calculation logic', () => {
const testCases = [
{ nodes: [], parentId: undefined, expectedLevel: 0 },
{ nodes: [{ id: 'parent', data: {} }], parentId: 'parent', expectedLevel: 1 },
@@ -206,7 +181,6 @@ describe('ParallelNodeComponent', () => {
testCases.forEach(({ nodes, parentId, expectedLevel }) => {
mockGetNodes.mockReturnValue(nodes)
- // Simulate the nesting level calculation logic
let level = 0
let currentParentId = parentId
@@ -221,8 +195,7 @@ describe('ParallelNodeComponent', () => {
})
})
- it('should handle nested styles generation for parallel nodes', () => {
- // Test the nested styles logic with parallel-specific colors
+ it.concurrent('should handle nested styles generation for parallel nodes', () => {
const testCases = [
{ nestingLevel: 0, state: 'valid', expectedBg: 'rgba(254,225,43,0.05)' },
{ nestingLevel: 0, state: 'invalid', expectedBg: 'transparent' },
@@ -231,7 +204,6 @@ describe('ParallelNodeComponent', () => {
]
testCases.forEach(({ nestingLevel, state, expectedBg }) => {
- // Simulate the getNestedStyles logic for parallel nodes
const styles: Record = {
backgroundColor: state === 'valid' ? 'rgba(254,225,43,0.05)' : 'transparent',
}
@@ -248,14 +220,13 @@ describe('ParallelNodeComponent', () => {
})
describe('Parallel-Specific Features', () => {
- it('should handle parallel execution states', () => {
+ it.concurrent('should handle parallel execution states', () => {
const parallelStates = ['valid', 'invalid', 'executing', 'completed', 'pending']
parallelStates.forEach((state) => {
const data = { width: 500, height: 300, state }
expect(data.state).toBe(state)
- // Test parallel-specific state handling
const isExecuting = state === 'executing'
const isCompleted = state === 'completed'
@@ -264,8 +235,7 @@ describe('ParallelNodeComponent', () => {
})
})
- it('should handle parallel node color scheme', () => {
- // Test that parallel nodes use yellow color scheme
+ it.concurrent('should handle parallel node color scheme', () => {
const parallelColors = {
background: 'rgba(254,225,43,0.05)',
ring: '#FEE12B',
@@ -277,8 +247,7 @@ describe('ParallelNodeComponent', () => {
expect(parallelColors.startIcon).toBe('#FEE12B')
})
- it('should differentiate from loop node styling', () => {
- // Ensure parallel nodes have different styling than loop nodes
+ it.concurrent('should differentiate from loop node styling', () => {
const loopColors = {
background: 'rgba(34,197,94,0.05)',
ring: '#2FB3FF',
@@ -298,7 +267,7 @@ describe('ParallelNodeComponent', () => {
})
describe('Component Configuration', () => {
- it('should handle different dimensions', () => {
+ it.concurrent('should handle different dimensions', () => {
const dimensionTests = [
{ width: 500, height: 300 },
{ width: 800, height: 600 },
@@ -313,7 +282,7 @@ describe('ParallelNodeComponent', () => {
})
})
- it('should handle different states', () => {
+ it.concurrent('should handle different states', () => {
const stateTests = ['valid', 'invalid', 'pending', 'executing', 'completed']
stateTests.forEach((state) => {
@@ -324,12 +293,11 @@ describe('ParallelNodeComponent', () => {
})
describe('Event Handling Logic', () => {
- it('should handle delete button click logic', () => {
+ it.concurrent('should handle delete button click logic', () => {
const mockEvent = {
stopPropagation: vi.fn(),
}
- // Simulate the delete button click handler
const handleDelete = (e: any, nodeId: string) => {
e.stopPropagation()
mockRemoveBlock(nodeId)
@@ -341,19 +309,18 @@ describe('ParallelNodeComponent', () => {
expect(mockRemoveBlock).toHaveBeenCalledWith('test-id')
})
- it('should handle event propagation prevention', () => {
+ it.concurrent('should handle event propagation prevention', () => {
const mockEvent = {
stopPropagation: vi.fn(),
}
- // Test that stopPropagation is called
mockEvent.stopPropagation()
expect(mockEvent.stopPropagation).toHaveBeenCalled()
})
})
describe('Component Data Handling', () => {
- it('should handle missing data properties gracefully', () => {
+ it.concurrent('should handle missing data properties gracefully', () => {
const testCases = [
undefined,
{},
@@ -375,7 +342,7 @@ describe('ParallelNodeComponent', () => {
})
})
- it('should handle parent ID relationships', () => {
+ it.concurrent('should handle parent ID relationships', () => {
const testCases = [
{ parentId: undefined, hasParent: false },
{ parentId: 'parent-1', hasParent: true },
@@ -390,7 +357,7 @@ describe('ParallelNodeComponent', () => {
})
describe('Handle Configuration', () => {
- it('should have correct handle IDs for parallel nodes', () => {
+ it.concurrent('should have correct handle IDs for parallel nodes', () => {
const handleIds = {
startSource: 'parallel-start-source',
endSource: 'parallel-end-source',
@@ -402,7 +369,7 @@ describe('ParallelNodeComponent', () => {
expect(handleIds.endSource).not.toContain('loop')
})
- it('should handle different handle positions', () => {
+ it.concurrent('should handle different handle positions', () => {
const positions = {
left: 'left',
right: 'right',
@@ -418,7 +385,7 @@ describe('ParallelNodeComponent', () => {
})
describe('Edge Cases and Error Handling', () => {
- it('should handle circular parent references', () => {
+ it.concurrent('should handle circular parent references', () => {
// Test circular reference prevention
const nodes = [
{ id: 'node1', data: { parentId: 'node2' } },
@@ -456,7 +423,7 @@ describe('ParallelNodeComponent', () => {
expect(visited.has('node2')).toBe(true)
})
- it('should handle complex circular reference chains', () => {
+ it.concurrent('should handle complex circular reference chains', () => {
// Test more complex circular reference scenarios
const nodes = [
{ id: 'node1', data: { parentId: 'node2' } },
@@ -489,7 +456,7 @@ describe('ParallelNodeComponent', () => {
expect(visited.size).toBe(3)
})
- it('should handle self-referencing nodes', () => {
+ it.concurrent('should handle self-referencing nodes', () => {
// Test node that references itself
const nodes = [
{ id: 'node1', data: { parentId: 'node1' } }, // Self-reference
@@ -520,7 +487,7 @@ describe('ParallelNodeComponent', () => {
expect(visited.has('node1')).toBe(true)
})
- it('should handle extreme values', () => {
+ it.concurrent('should handle extreme values', () => {
const extremeValues = [
{ width: Number.MAX_SAFE_INTEGER, height: Number.MAX_SAFE_INTEGER },
{ width: -1, height: -1 },
@@ -538,7 +505,7 @@ describe('ParallelNodeComponent', () => {
})
})
- it('should handle negative position values', () => {
+ it.concurrent('should handle negative position values', () => {
const positions = [
{ xPos: -100, yPos: -200 },
{ xPos: 0, yPos: 0 },
@@ -556,7 +523,7 @@ describe('ParallelNodeComponent', () => {
})
describe('Component Comparison with Loop Node', () => {
- it('should have similar structure to loop node but different type', () => {
+ it.concurrent('should have similar structure to loop node but different type', () => {
expect(defaultProps.type).toBe('parallelNode')
expect(defaultProps.id).toContain('parallel')
@@ -565,7 +532,7 @@ describe('ParallelNodeComponent', () => {
expect(defaultProps.id).not.toContain('loop')
})
- it('should handle the same prop structure as loop node', () => {
+ it.concurrent('should handle the same prop structure as loop node', () => {
// Test that parallel node accepts the same prop structure as loop node
const sharedPropStructure = {
id: 'test-parallel',
@@ -594,8 +561,7 @@ describe('ParallelNodeComponent', () => {
expect(sharedPropStructure.data.height).toBe(300)
})
- it('should maintain consistency with loop node interface', () => {
- // Both components should accept the same base props
+ it.concurrent('should maintain consistency with loop node interface', () => {
const baseProps = [
'id',
'type',
diff --git a/apps/sim/app/w/[id]/components/parallel-node/parallel-node.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/parallel-node/parallel-node.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/parallel-node/parallel-node.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/parallel-node/parallel-node.tsx
diff --git a/apps/sim/app/w/[id]/components/skeleton-loading/skeleton-loading.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/skeleton-loading/skeleton-loading.tsx
similarity index 99%
rename from apps/sim/app/w/[id]/components/skeleton-loading/skeleton-loading.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/skeleton-loading/skeleton-loading.tsx
index 141700a1ec2..de8e90cdfab 100644
--- a/apps/sim/app/w/[id]/components/skeleton-loading/skeleton-loading.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/skeleton-loading/skeleton-loading.tsx
@@ -5,7 +5,6 @@ import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { useSidebarStore } from '@/stores/sidebar/store'
-// Skeleton Components
const SkeletonControlBar = () => {
return (
diff --git a/apps/sim/app/w/[id]/components/toolbar/components/toolbar-block/toolbar-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/components/toolbar-block/toolbar-block.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/toolbar/components/toolbar-block/toolbar-block.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/components/toolbar-block/toolbar-block.tsx
diff --git a/apps/sim/app/w/[id]/components/toolbar/components/toolbar-loop-block/toolbar-loop-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/components/toolbar-loop-block/toolbar-loop-block.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/toolbar/components/toolbar-loop-block/toolbar-loop-block.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/components/toolbar-loop-block/toolbar-loop-block.tsx
diff --git a/apps/sim/app/w/[id]/components/toolbar/components/toolbar-parallel-block/toolbar-parallel-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/components/toolbar-parallel-block/toolbar-parallel-block.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/toolbar/components/toolbar-parallel-block/toolbar-parallel-block.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/components/toolbar-parallel-block/toolbar-parallel-block.tsx
diff --git a/apps/sim/app/w/[id]/components/toolbar/components/toolbar-tabs/toolbar-tabs.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/components/toolbar-tabs/toolbar-tabs.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/toolbar/components/toolbar-tabs/toolbar-tabs.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/components/toolbar-tabs/toolbar-tabs.tsx
diff --git a/apps/sim/app/w/[id]/components/toolbar/toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/toolbar.tsx
similarity index 95%
rename from apps/sim/app/w/[id]/components/toolbar/toolbar.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/toolbar.tsx
index 8d86f8407f5..177413e6ed7 100644
--- a/apps/sim/app/w/[id]/components/toolbar/toolbar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/toolbar.tsx
@@ -6,7 +6,7 @@ import { useParams } from 'next/navigation'
import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
-import { useUserPermissionsContext } from '@/app/w/components/providers/workspace-permissions-provider'
+import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/w/components/providers/workspace-permissions-provider'
import { getAllBlocks, getBlocksByCategory } from '@/blocks'
import type { BlockCategory } from '@/blocks/types'
import { useSidebarStore } from '@/stores/sidebar/store'
@@ -43,16 +43,15 @@ export const Toolbar = React.memo(() => {
const params = useParams()
const workflowId = params?.id as string
- // Get the workspace ID from the workflow registry
- const { activeWorkspaceId, workflows } = useWorkflowRegistry()
+ // Get the workspace ID from URL params
+ const { workflows } = useWorkflowRegistry()
+ const workspaceId = params.workspaceId as string
const currentWorkflow = useMemo(
() => (workflowId ? workflows[workflowId] : null),
[workflowId, workflows]
)
- const workspaceId = currentWorkflow?.workspaceId || activeWorkspaceId
-
const userPermissions = useUserPermissionsContext()
const [activeTab, setActiveTab] = useState
('blocks')
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx
similarity index 96%
rename from apps/sim/app/w/[id]/components/workflow-block/components/action-bar/action-bar.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx
index 7f926ef4fa0..1e7c5e7e967 100644
--- a/apps/sim/app/w/[id]/components/workflow-block/components/action-bar/action-bar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx
@@ -12,8 +12,7 @@ interface ActionBarProps {
}
export function ActionBar({ blockId, blockType, disabled = false }: ActionBarProps) {
- const { collaborativeRemoveBlock } = useCollaborativeWorkflow()
- const toggleBlockEnabled = useWorkflowStore((state) => state.toggleBlockEnabled)
+ const { collaborativeRemoveBlock, collaborativeToggleBlockEnabled } = useCollaborativeWorkflow()
const toggleBlockHandles = useWorkflowStore((state) => state.toggleBlockHandles)
const duplicateBlock = useWorkflowStore((state) => state.duplicateBlock)
const isEnabled = useWorkflowStore((state) => state.blocks[blockId]?.enabled ?? true)
@@ -56,7 +55,7 @@ export function ActionBar({ blockId, blockType, disabled = false }: ActionBarPro
size='sm'
onClick={() => {
if (!disabled) {
- toggleBlockEnabled(blockId)
+ collaborativeToggleBlockEnabled(blockId)
}
}}
className={cn('text-gray-500', disabled && 'cursor-not-allowed opacity-50')}
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/connection-blocks/connection-blocks.tsx
similarity index 97%
rename from apps/sim/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/connection-blocks/connection-blocks.tsx
index baf322f53e7..b9f17f57024 100644
--- a/apps/sim/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/connection-blocks/connection-blocks.tsx
@@ -1,6 +1,9 @@
import { Card } from '@/components/ui/card'
import { cn } from '@/lib/utils'
-import { type ConnectedBlock, useBlockConnections } from '@/app/w/[id]/hooks/use-block-connections'
+import {
+ type ConnectedBlock,
+ useBlockConnections,
+} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
interface ConnectionBlocksProps {
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/channel-selector/channel-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/channel-selector/channel-selector-input.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/channel-selector/channel-selector-input.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/channel-selector/channel-selector-input.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/channel-selector/components/slack-channel-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/channel-selector/components/slack-channel-selector.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/channel-selector/components/slack-channel-selector.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/channel-selector/components/slack-channel-selector.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/checkbox-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/checkbox-list.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/checkbox-list.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/checkbox-list.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/code.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/code.tsx
similarity index 99%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/code.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/code.tsx
index d47adca2d8e..a228efb1714 100644
--- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/code.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/code.tsx
@@ -11,7 +11,7 @@ import { checkEnvVarTrigger, EnvVarDropdown } from '@/components/ui/env-var-drop
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
-import { useCodeGeneration } from '@/app/w/[id]/hooks/use-code-generation'
+import { useCodeGeneration } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-code-generation'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { CodePromptBar } from '../../../../code-prompt-bar/code-prompt-bar'
import { useSubBlockValue } from '../hooks/use-sub-block-value'
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/condition-input.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/condition-input.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/date-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/date-input.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/date-input.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/date-input.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/document-selector/document-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-selector/document-selector.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/document-selector/document-selector.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-selector/document-selector.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/dropdown.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/dropdown.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/dropdown.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/eval-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/eval-input.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/eval-input.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/eval-input.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/confluence-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/confluence-file-selector.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/confluence-file-selector.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/confluence-file-selector.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/discord-channel-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/discord-channel-selector.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/discord-channel-selector.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/discord-channel-selector.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/google-calendar-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-calendar-selector.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/google-calendar-selector.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-calendar-selector.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/teams-message-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/teams-message-selector.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/teams-message-selector.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/teams-message-selector.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-upload.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-upload.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-upload.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-upload.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/components/folder-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/folder-selector/components/folder-selector-input.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/components/folder-selector-input.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/folder-selector/components/folder-selector-input.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx
similarity index 98%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx
index 2a0d9990eab..31abe42a2b3 100644
--- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx
@@ -15,7 +15,7 @@ import {
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { createLogger } from '@/lib/logs/console-logger'
import { type Credential, getProviderIdFromServiceId, getServiceIdFromScopes } from '@/lib/oauth'
-import { OAuthRequiredModal } from '@/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal'
+import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal'
import { saveToStorage } from '@/stores/workflows/persistence'
const logger = createLogger('FolderSelector')
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/long-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/long-input.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/long-input.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/long-input.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/discord-server-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/components/discord-server-selector.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/discord-server-selector.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/components/discord-server-selector.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/jira-project-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/components/jira-project-selector.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/jira-project-selector.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/components/jira-project-selector.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/response/components/property-renderer.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/response/components/property-renderer.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/response/components/property-renderer.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/response/components/property-renderer.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/response/components/value-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/response/components/value-input.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/response/components/value-input.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/response/components/value-input.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/response/response-format.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/response/response-format.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/response/response-format.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/response/response-format.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/schedule/components/schedule-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/schedule/components/schedule-modal.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/schedule/components/schedule-modal.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/schedule/components/schedule-modal.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/schedule/schedule-config.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/schedule/schedule-config.tsx
similarity index 99%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/schedule/schedule-config.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/schedule/schedule-config.tsx
index 182b8ae6922..9b9c58f6d1c 100644
--- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/schedule/schedule-config.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/schedule/schedule-config.tsx
@@ -46,7 +46,7 @@ export function ScheduleConfig({
const [refreshCounter, setRefreshCounter] = useState(0)
const params = useParams()
- const workflowId = params.id as string
+ const workflowId = params.workflowId as string
// Get workflow state from store
const setScheduleStatus = useWorkflowStore((state) => state.setScheduleStatus)
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/short-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/short-input.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/short-input.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/short-input.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/slider-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/slider-input.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/slider-input.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/slider-input.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/starter/input-format.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/starter/input-format.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/starter/input-format.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/starter/input-format.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/switch.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/switch.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/switch.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/switch.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/table.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/table.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/table.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/table.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/time-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/time-input.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/time-input.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/time-input.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx
similarity index 99%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx
index aee40413bec..84f74fc0833 100644
--- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx
@@ -24,7 +24,7 @@ import { Label } from '@/components/ui/label'
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
-import { useCodeGeneration } from '@/app/w/[id]/hooks/use-code-generation'
+import { useCodeGeneration } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-code-generation'
import { useCustomToolsStore } from '@/stores/custom-tools/store'
import { CodePromptBar } from '../../../../../../../code-prompt-bar/code-prompt-bar'
import { CodeEditor } from '../code-editor/code-editor'
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/components/tool-command/tool-command.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/tool-command/tool-command.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/components/tool-command/tool-command.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/tool-command/tool-command.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/airtable.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/airtable.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/airtable.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/airtable.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/discord.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/discord.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/discord.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/discord.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/generic.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/generic.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/generic.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/generic.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/github.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/github.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/github.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/github.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/gmail.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/gmail.tsx
similarity index 98%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/gmail.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/gmail.tsx
index a23eb809d83..3168af8fbfe 100644
--- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/gmail.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/gmail.tsx
@@ -16,7 +16,7 @@ import {
import { Skeleton } from '@/components/ui/skeleton'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { Logger } from '@/lib/logs/console-logger'
-import { JSONView } from '@/app/w/[id]/components/panel/components/console/components/json-view/json-view'
+import { JSONView } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/components/json-view/json-view'
import { ConfigSection } from '../ui/config-section'
const logger = new Logger('GmailConfig')
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/slack.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/slack.tsx
similarity index 97%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/slack.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/slack.tsx
index cb584d2e751..4bb03e252f1 100644
--- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/slack.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/slack.tsx
@@ -1,6 +1,6 @@
import { SlackIcon } from '@/components/icons'
import { Notice } from '@/components/ui/notice'
-import { JSONView } from '@/app/w/[id]/components/panel/components/console/components/json-view/json-view'
+import { JSONView } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/components/json-view/json-view'
import { ConfigSection } from '../ui/config-section'
import { InstructionsSection } from '../ui/instructions-section'
import { TestResultDisplay } from '../ui/test-result'
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/stripe.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/stripe.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/stripe.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/stripe.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/telegram.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/telegram.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/telegram.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/telegram.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/whatsapp.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/whatsapp.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/whatsapp.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/whatsapp.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/config-field.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/config-field.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/config-field.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/config-field.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/config-section.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/config-section.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/config-section.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/config-section.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/confirmation.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/confirmation.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/confirmation.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/confirmation.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/copyable.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/copyable.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/copyable.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/copyable.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/instructions-section.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/instructions-section.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/instructions-section.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/instructions-section.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/test-result.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/test-result.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/test-result.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/test-result.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/webhook-config-field.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/webhook-config-field.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/webhook-config-field.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/webhook-config-field.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/webhook-footer.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/webhook-footer.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/webhook-footer.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/webhook-footer.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/webhook-url.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/webhook-url.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/webhook-url.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/ui/webhook-url.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/webhook.tsx
similarity index 99%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/webhook.tsx
index a7a82be82ab..041ddd7aa41 100644
--- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/webhook.tsx
@@ -310,7 +310,7 @@ export function WebhookConfig({
const [error, setError] = useState(null)
const [webhookId, setWebhookId] = useState(null)
const params = useParams()
- const workflowId = params.id as string
+ const workflowId = params.workflowId as string
const [isLoading, setIsLoading] = useState(false)
const [gmailCredentialId, setGmailCredentialId] = useState('')
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx
diff --git a/apps/sim/app/w/[id]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx
similarity index 96%
rename from apps/sim/app/w/[id]/components/workflow-block/workflow-block.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx
index 82cf37cfa1d..0ffb7258664 100644
--- a/apps/sim/app/w/[id]/components/workflow-block/workflow-block.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx
@@ -7,8 +7,9 @@ import { Card } from '@/components/ui/card'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { parseCronToHumanReadable } from '@/lib/schedules/utils'
import { cn, formatDateTime, validateName } from '@/lib/utils'
-import { useUserPermissionsContext } from '@/app/w/components/providers/workspace-permissions-provider'
+import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/w/components/providers/workspace-permissions-provider'
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
+import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useExecutionStore } from '@/stores/execution/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { mergeSubblockState } from '@/stores/workflows/utils'
@@ -67,11 +68,15 @@ export function WorkflowBlock({ id, data }: NodeProps) {
const blockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0)
const hasActiveWebhook = useWorkflowStore((state) => state.hasActiveWebhook ?? false)
const blockAdvancedMode = useWorkflowStore((state) => state.blocks[id]?.advancedMode ?? false)
- const toggleBlockAdvancedMode = useWorkflowStore((state) => state.toggleBlockAdvancedMode)
+
+ // Collaborative workflow actions
+ const {
+ collaborativeUpdateBlockName,
+ collaborativeToggleBlockWide,
+ collaborativeToggleBlockAdvancedMode,
+ } = useCollaborativeWorkflow()
// Workflow store actions
- const updateBlockName = useWorkflowStore((state) => state.updateBlockName)
- const toggleBlockWide = useWorkflowStore((state) => state.toggleBlockWide)
const updateBlockHeight = useWorkflowStore((state) => state.updateBlockHeight)
// Execution store
@@ -371,7 +376,7 @@ export function WorkflowBlock({ id, data }: NodeProps) {
const handleNameSubmit = () => {
const trimmedName = editedName.trim().slice(0, 18)
if (trimmedName && trimmedName !== name) {
- updateBlockName(id, trimmedName)
+ collaborativeUpdateBlockName(id, trimmedName)
}
setIsEditing(false)
}
@@ -622,14 +627,27 @@ export function WorkflowBlock({ id, data }: NodeProps) {
toggleBlockAdvancedMode(id)}
- className={cn('h-7 p-1 text-gray-500', blockAdvancedMode && 'text-[#701FFC]')}
+ onClick={() => {
+ if (userPermissions.canEdit) {
+ collaborativeToggleBlockAdvancedMode(id)
+ }
+ }}
+ className={cn(
+ 'h-7 p-1 text-gray-500',
+ blockAdvancedMode && 'text-[#701FFC]',
+ !userPermissions.canEdit && 'cursor-not-allowed opacity-50'
+ )}
+ disabled={!userPermissions.canEdit}
>
- {blockAdvancedMode ? 'Switch to Basic Mode' : 'Switch to Advanced Mode'}
+ {!userPermissions.canEdit
+ ? 'Read-only mode'
+ : blockAdvancedMode
+ ? 'Switch to Basic Mode'
+ : 'Switch to Advanced Mode'}
)}
@@ -704,7 +722,7 @@ export function WorkflowBlock({ id, data }: NodeProps) {
size='sm'
onClick={() => {
if (userPermissions.canEdit) {
- toggleBlockWide(id)
+ collaborativeToggleBlockWide(id)
}
}}
className={cn(
diff --git a/apps/sim/app/w/[id]/components/workflow-edge/workflow-edge.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/components/workflow-edge/workflow-edge.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx
diff --git a/apps/sim/app/w/[id]/hooks/use-block-connections.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections.ts
similarity index 100%
rename from apps/sim/app/w/[id]/hooks/use-block-connections.ts
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections.ts
diff --git a/apps/sim/app/w/[id]/hooks/use-code-generation.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-code-generation.ts
similarity index 100%
rename from apps/sim/app/w/[id]/hooks/use-code-generation.ts
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-code-generation.ts
diff --git a/apps/sim/app/w/[id]/hooks/use-presence.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-presence.ts
similarity index 76%
rename from apps/sim/app/w/[id]/hooks/use-presence.ts
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-presence.ts
index 3b86676e64f..2e3b1a5f8ab 100644
--- a/apps/sim/app/w/[id]/hooks/use-presence.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-presence.ts
@@ -3,8 +3,8 @@
import { useMemo } from 'react'
import { useSocket } from '@/contexts/socket-context'
-interface PresenceUser {
- connectionId: number
+type PresenceUser = {
+ connectionId: string | number
name?: string
color?: string
info?: string
@@ -25,9 +25,9 @@ export function usePresence(): UsePresenceReturn {
const users = useMemo(() => {
return presenceUsers.map((user, index) => ({
- connectionId: user.socketId
- ? Math.abs(user.socketId.split('').reduce((a, b) => a + b.charCodeAt(0), 0))
- : index + 1,
+ // Use socketId directly as connectionId to ensure uniqueness
+ // If no socketId, use a unique fallback based on userId + index
+ connectionId: user.socketId || `fallback-${user.userId}-${index}`,
name: user.userName,
color: undefined, // Let the avatar component generate colors
info: user.selection?.type ? `Editing ${user.selection.type}` : undefined,
diff --git a/apps/sim/app/w/[id]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts
similarity index 100%
rename from apps/sim/app/w/[id]/hooks/use-workflow-execution.ts
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts
diff --git a/apps/sim/app/w/[id]/layout.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/layout.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx
diff --git a/apps/sim/app/w/[id]/page.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/page.tsx
similarity index 100%
rename from apps/sim/app/w/[id]/page.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/page.tsx
diff --git a/apps/sim/app/w/[id]/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils.ts
similarity index 100%
rename from apps/sim/app/w/[id]/utils.ts
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils.ts
diff --git a/apps/sim/app/w/[id]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
similarity index 92%
rename from apps/sim/app/w/[id]/workflow.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
index e656b3fb1eb..72606e7f03a 100644
--- a/apps/sim/app/w/[id]/workflow.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
@@ -11,12 +11,16 @@ import ReactFlow, {
useReactFlow,
} from 'reactflow'
import 'reactflow/dist/style.css'
-
import { createLogger } from '@/lib/logs/console-logger'
-import { LoopNodeComponent } from '@/app/w/[id]/components/loop-node/loop-node'
-import { NotificationList } from '@/app/w/[id]/components/notifications/notifications'
-import { ParallelNodeComponent } from '@/app/w/[id]/components/parallel-node/parallel-node'
-import { useUserPermissionsContext } from '@/app/w/components/providers/workspace-permissions-provider'
+import { ControlBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar'
+import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/error/index'
+import { LoopNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/loop-node/loop-node'
+import { NotificationList } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications'
+import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel'
+import { ParallelNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/parallel-node/parallel-node'
+import { SkeletonLoading } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/skeleton-loading/skeleton-loading'
+import { Toolbar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/toolbar'
+import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/w/components/providers/workspace-permissions-provider'
import { getBlock } from '@/blocks'
import { useSocket } from '@/contexts/socket-context'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
@@ -26,14 +30,8 @@ import { useNotificationStore } from '@/stores/notifications/store'
import { useVariablesStore } from '@/stores/panel/variables/store'
import { useGeneralStore } from '@/stores/settings/general/store'
import { useSidebarStore } from '@/stores/sidebar/store'
-// Removed sync manager import - Socket.IO handles real-time sync
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
-import { ControlBar } from './components/control-bar/control-bar'
-import { ErrorBoundary } from './components/error/index'
-import { Panel } from './components/panel/panel'
-import { SkeletonLoading } from './components/skeleton-loading/skeleton-loading'
-import { Toolbar } from './components/toolbar/toolbar'
import { WorkflowBlock } from './components/workflow-block/workflow-block'
import { WorkflowEdge } from './components/workflow-edge/workflow-edge'
import {
@@ -95,11 +93,16 @@ const WorkflowContent = React.memo(() => {
const { project, getNodes, fitView } = useReactFlow()
// Get workspace ID from current workflow
- const workflowId = params.id as string
+ const workflowId = params.workflowId as string
const { workflows, activeWorkflowId, isLoading, setActiveWorkflow, createWorkflow } =
useWorkflowRegistry()
- const { blocks, edges, updateNodeDimensions } = useWorkflowStore()
+ const {
+ blocks,
+ edges,
+ updateNodeDimensions,
+ updateBlockPosition: storeUpdateBlockPosition,
+ } = useWorkflowStore()
// Use collaborative operations for real-time sync
const currentWorkflow = useMemo(() => workflows[workflowId], [workflows, workflowId])
const workspaceId = currentWorkflow?.workspaceId
@@ -117,7 +120,7 @@ const WorkflowContent = React.memo(() => {
collaborativeAddBlock: addBlock,
collaborativeAddEdge: addEdge,
collaborativeRemoveEdge: removeEdge,
- collaborativeUpdateBlockPosition: updateBlockPosition,
+ collaborativeUpdateBlockPosition,
collaborativeUpdateParentId: updateParentId,
isConnected,
currentWorkflowId,
@@ -186,12 +189,12 @@ const WorkflowContent = React.memo(() => {
nodeId,
newParentId,
getNodes,
- updateBlockPosition,
+ collaborativeUpdateBlockPosition,
updateParentId,
() => resizeLoopNodes(getNodes, updateNodeDimensions, blocks)
)
},
- [getNodes, updateBlockPosition, updateParentId, updateNodeDimensions, blocks]
+ [getNodes, collaborativeUpdateBlockPosition, updateParentId, updateNodeDimensions, blocks]
)
// Function to resize all loop nodes with improved hierarchy handling
@@ -256,13 +259,20 @@ const WorkflowContent = React.memo(() => {
[detectedOrientation]
)
- applyAutoLayoutSmooth(blocks, edges, updateBlockPosition, fitView, resizeLoopNodesWrapper, {
- ...orientationConfig,
- alignByLayer: true,
- animationDuration: 500, // Smooth 500ms animation
- isSidebarCollapsed,
- handleOrientation: detectedOrientation, // Explicitly set the detected orientation
- })
+ applyAutoLayoutSmooth(
+ blocks,
+ edges,
+ collaborativeUpdateBlockPosition,
+ fitView,
+ resizeLoopNodesWrapper,
+ {
+ ...orientationConfig,
+ alignByLayer: true,
+ animationDuration: 500, // Smooth 500ms animation
+ isSidebarCollapsed,
+ handleOrientation: detectedOrientation, // Explicitly set the detected orientation
+ }
+ )
const orientationMessage =
detectedOrientation === 'vertical'
@@ -273,7 +283,14 @@ const WorkflowContent = React.memo(() => {
orientation: detectedOrientation,
blockCount: Object.keys(blocks).length,
})
- }, [blocks, edges, updateBlockPosition, fitView, isSidebarCollapsed, resizeLoopNodesWrapper])
+ }, [
+ blocks,
+ edges,
+ collaborativeUpdateBlockPosition,
+ fitView,
+ isSidebarCollapsed,
+ resizeLoopNodesWrapper,
+ ])
const debouncedAutoLayout = useCallback(() => {
const debounceTimer = setTimeout(() => {
@@ -787,7 +804,7 @@ const WorkflowContent = React.memo(() => {
// Track when workflow is fully ready for rendering
useEffect(() => {
- const currentId = params.id as string
+ const currentId = params.workflowId as string
// Reset workflow ready state when workflow changes
if (activeWorkflowId !== currentId) {
@@ -813,13 +830,13 @@ const WorkflowContent = React.memo(() => {
return () => clearTimeout(timeoutId)
}
setIsWorkflowReady(false)
- }, [activeWorkflowId, params.id, workflows, isLoading])
+ }, [activeWorkflowId, params.workflowId, workflows, isLoading])
// Init workflow
useEffect(() => {
const validateAndNavigate = async () => {
const workflowIds = Object.keys(workflows)
- const currentId = params.id as string
+ const currentId = params.workflowId as string
// Wait for both initialization and workflow loading to complete
if (isLoading) {
@@ -830,27 +847,26 @@ const WorkflowContent = React.memo(() => {
// If no workflows exist, redirect to workspace root to let server handle workflow creation
if (workflowIds.length === 0 && !isLoading) {
logger.info('No workflows found, redirecting to workspace root')
- router.replace('/w')
+ router.replace(`/workspace/${workspaceId}/w`)
return
}
// Navigate to existing workflow or first available
if (!workflows[currentId]) {
logger.info(`Workflow ${currentId} not found, redirecting to first available workflow`)
- router.replace(`/w/${workflowIds[0]}`)
+ router.replace(`/workspace/${workspaceId}/w/${workflowIds[0]}`)
return
}
- // Reset variables loaded state before setting active workflow
- resetVariablesLoaded()
-
- // Always call setActiveWorkflow when workflow ID changes to ensure proper state
+ // Get current active workflow state
const { activeWorkflowId } = useWorkflowRegistry.getState()
if (activeWorkflowId !== currentId) {
+ // Only reset variables when actually switching workflows
+ resetVariablesLoaded()
setActiveWorkflow(currentId)
} else {
- // Even if the workflow is already active, call setActiveWorkflow to ensure state consistency
+ // Don't reset variables cache if we're not actually switching workflows
setActiveWorkflow(currentId)
}
@@ -859,7 +875,7 @@ const WorkflowContent = React.memo(() => {
validateAndNavigate()
}, [
- params.id,
+ params.workflowId,
workflows,
isLoading,
setActiveWorkflow,
@@ -956,18 +972,20 @@ const WorkflowContent = React.memo(() => {
return nodeArray
}, [blocks, activeBlockIds, pendingBlocks, isDebugModeEnabled, nestedSubflowErrors])
- // Update nodes
+ // Update nodes - use store version to avoid collaborative feedback loops
const onNodesChange = useCallback(
(changes: any) => {
changes.forEach((change: any) => {
if (change.type === 'position' && change.position) {
const node = nodes.find((n) => n.id === change.id)
if (!node) return
- updateBlockPosition(change.id, change.position)
+ // Use store version to avoid collaborative feedback loop
+ // React Flow position changes can be triggered by collaborative updates
+ storeUpdateBlockPosition(change.id, change.position)
}
})
},
- [nodes, updateBlockPosition]
+ [nodes, storeUpdateBlockPosition]
)
// Effect to resize loops when nodes change (add/remove/position change)
@@ -1002,11 +1020,16 @@ const WorkflowContent = React.memo(() => {
const absolutePosition = getNodeAbsolutePositionWrapper(id)
// Update the node to remove parent reference and use absolute position
- updateBlockPosition(id, absolutePosition)
+ collaborativeUpdateBlockPosition(id, absolutePosition)
updateParentId(id, '', 'parent')
}
})
- }, [blocks, updateBlockPosition, updateParentId, getNodeAbsolutePositionWrapper])
+ }, [blocks, collaborativeUpdateBlockPosition, updateParentId, getNodeAbsolutePositionWrapper])
+
+ // Validate nested subflows whenever blocks change
+ useEffect(() => {
+ validateNestedSubflows()
+ }, [blocks, validateNestedSubflows])
// Validate nested subflows whenever blocks change
useEffect(() => {
@@ -1109,6 +1132,9 @@ const WorkflowContent = React.memo(() => {
// Store currently dragged node ID
setDraggedNodeId(node.id)
+ // Emit collaborative position update during drag for smooth real-time movement
+ collaborativeUpdateBlockPosition(node.id, node.position)
+
// Get the current parent ID of the node being dragged
const currentParentId = blocks[node.id]?.data?.parentId || null
@@ -1255,6 +1281,7 @@ const WorkflowContent = React.memo(() => {
getNodeHierarchyWrapper,
getNodeAbsolutePositionWrapper,
getNodeDepthWrapper,
+ collaborativeUpdateBlockPosition,
]
)
@@ -1277,7 +1304,11 @@ const WorkflowContent = React.memo(() => {
})
document.body.style.cursor = ''
- // Don't process if the node hasn't actually changed parent or is being moved within same parent
+ // Emit collaborative position update for the final position
+ // This ensures other users see the smooth final position
+ collaborativeUpdateBlockPosition(node.id, node.position)
+
+ // Don't process parent changes if the node hasn't actually changed parent or is being moved within same parent
if (potentialParentId === dragStartParentId) return
// Check if this is a starter block - starter blocks should never be in containers
@@ -1320,7 +1351,14 @@ const WorkflowContent = React.memo(() => {
setDraggedNodeId(null)
setPotentialParentId(null)
},
- [getNodes, dragStartParentId, potentialParentId, updateNodeParent, getNodeHierarchyWrapper]
+ [
+ getNodes,
+ dragStartParentId,
+ potentialParentId,
+ updateNodeParent,
+ getNodeHierarchyWrapper,
+ collaborativeUpdateBlockPosition,
+ ]
)
// Update onPaneClick to only handle edge selection
@@ -1438,7 +1476,7 @@ const WorkflowContent = React.memo(() => {
@@ -1454,12 +1492,12 @@ const WorkflowContent = React.memo(() => {
return (
-
+
0} />
diff --git a/apps/sim/app/w/components/providers/providers.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/providers/providers.tsx
similarity index 100%
rename from apps/sim/app/w/components/providers/providers.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/providers/providers.tsx
diff --git a/apps/sim/app/w/components/providers/theme-provider.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/providers/theme-provider.tsx
similarity index 100%
rename from apps/sim/app/w/components/providers/theme-provider.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/providers/theme-provider.tsx
diff --git a/apps/sim/app/w/components/providers/workspace-permissions-provider.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/providers/workspace-permissions-provider.tsx
similarity index 86%
rename from apps/sim/app/w/components/providers/workspace-permissions-provider.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/providers/workspace-permissions-provider.tsx
index 77b4dfbf5d8..9ebbd4286e8 100644
--- a/apps/sim/app/w/components/providers/workspace-permissions-provider.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/providers/workspace-permissions-provider.tsx
@@ -1,12 +1,15 @@
'use client'
import React, { createContext, useContext, useMemo } from 'react'
+import { useParams } from 'next/navigation'
+import { createLogger } from '@/lib/logs/console-logger'
import { useUserPermissions, type WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
import {
useWorkspacePermissions,
type WorkspacePermissions,
} from '@/hooks/use-workspace-permissions'
-import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
+
+const logger = createLogger('WorkspacePermissionsProvider')
interface WorkspacePermissionsContextType {
// Raw workspace permissions data
@@ -27,17 +30,20 @@ interface WorkspacePermissionsProviderProps {
const WorkspacePermissionsProvider = React.memo
(
({ children }) => {
- const { activeWorkspaceId } = useWorkflowRegistry()
+ const params = useParams()
+ const workspaceId = params.workspaceId as string
+
+ if (!workspaceId) {
+ logger.warn('Workspace ID is undefined from params:', params)
+ }
- // Fetch workspace permissions once
const {
permissions: workspacePermissions,
loading: permissionsLoading,
error: permissionsError,
updatePermissions,
- } = useWorkspacePermissions(activeWorkspaceId)
+ } = useWorkspacePermissions(workspaceId)
- // Compute user permissions based on workspace permissions
const userPermissions = useUserPermissions(
workspacePermissions,
permissionsLoading,
diff --git a/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/create-menu/create-menu.tsx
similarity index 94%
rename from apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/create-menu/create-menu.tsx
index 5ff60db8657..188ee91ea8d 100644
--- a/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/create-menu/create-menu.tsx
@@ -1,7 +1,9 @@
'use client'
import { useState } from 'react'
+import { logger } from '@sentry/nextjs'
import { File, Folder, Plus } from 'lucide-react'
+import { useParams } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
@@ -9,7 +11,6 @@ import { Label } from '@/components/ui/label'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { cn } from '@/lib/utils'
import { useFolderStore } from '@/stores/folders/store'
-import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
interface CreateMenuProps {
onCreateWorkflow: (folderId?: string) => void
@@ -22,7 +23,8 @@ export function CreateMenu({ onCreateWorkflow, isCollapsed }: CreateMenuProps) {
const [isCreating, setIsCreating] = useState(false)
const [isHoverOpen, setIsHoverOpen] = useState(false)
- const { activeWorkspaceId } = useWorkflowRegistry()
+ const params = useParams()
+ const workspaceId = params.workspaceId as string
const { createFolder } = useFolderStore()
const handleCreateWorkflow = () => {
@@ -37,18 +39,18 @@ export function CreateMenu({ onCreateWorkflow, isCollapsed }: CreateMenuProps) {
const handleFolderSubmit = async (e: React.FormEvent) => {
e.preventDefault()
- if (!folderName.trim() || !activeWorkspaceId) return
+ if (!folderName.trim() || !workspaceId) return
setIsCreating(true)
try {
await createFolder({
name: folderName.trim(),
- workspaceId: activeWorkspaceId,
+ workspaceId: workspaceId,
})
setFolderName('')
setShowFolderDialog(false)
} catch (error) {
- console.error('Failed to create folder:', error)
+ logger.error('Failed to create folder:', { error })
} finally {
setIsCreating(false)
}
diff --git a/apps/sim/app/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx
similarity index 93%
rename from apps/sim/app/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx
index fcda8a79bec..08cebccd299 100644
--- a/apps/sim/app/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx
@@ -2,6 +2,7 @@
import { useState } from 'react'
import { File, Folder, MoreHorizontal, Pencil, Trash2 } from 'lucide-react'
+import { useParams } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import {
@@ -13,8 +14,10 @@ import {
} from '@/components/ui/dropdown-menu'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
+import { createLogger } from '@/lib/logs/console-logger'
import { useFolderStore } from '@/stores/folders/store'
-import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
+
+const logger = createLogger('FolderContextMenu')
interface FolderContextMenuProps {
folderId: string
@@ -37,8 +40,9 @@ export function FolderContextMenu({
const [renameName, setRenameName] = useState(folderName)
const [isCreating, setIsCreating] = useState(false)
const [isRenaming, setIsRenaming] = useState(false)
+ const params = useParams()
+ const workspaceId = params.workspaceId as string
- const { activeWorkspaceId } = useWorkflowRegistry()
const { createFolder, updateFolder, deleteFolder } = useFolderStore()
const handleCreateWorkflow = () => {
@@ -59,25 +63,25 @@ export function FolderContextMenu({
onDelete(folderId)
} else {
// Default delete behavior
- deleteFolder(folderId)
+ deleteFolder(folderId, workspaceId)
}
}
const handleSubfolderSubmit = async (e: React.FormEvent) => {
e.preventDefault()
- if (!subfolderName.trim() || !activeWorkspaceId) return
+ if (!subfolderName.trim() || !workspaceId) return
setIsCreating(true)
try {
await createFolder({
name: subfolderName.trim(),
- workspaceId: activeWorkspaceId,
+ workspaceId: workspaceId,
parentId: folderId,
})
setSubfolderName('')
setShowSubfolderDialog(false)
} catch (error) {
- console.error('Failed to create subfolder:', error)
+ logger.error('Failed to create subfolder:', { error })
} finally {
setIsCreating(false)
}
diff --git a/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-tree/components/folder-item.tsx
similarity index 94%
rename from apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-tree/components/folder-item.tsx
index db9ffe073a2..5d0accc0e3d 100644
--- a/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-tree/components/folder-item.tsx
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import clsx from 'clsx'
import { ChevronDown, ChevronRight, Folder, FolderOpen } from 'lucide-react'
+import { useParams } from 'next/navigation'
import {
AlertDialog,
AlertDialogAction,
@@ -14,9 +15,12 @@ import {
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
+import { createLogger } from '@/lib/logs/console-logger'
import { type FolderTreeNode, useFolderStore } from '@/stores/folders/store'
import { FolderContextMenu } from '../../folder-context-menu/folder-context-menu'
+const logger = createLogger('FolderItem')
+
interface FolderItemProps {
folder: FolderTreeNode
isCollapsed?: boolean
@@ -39,7 +43,8 @@ export function FolderItem({
const { expandedFolders, toggleExpanded, updateFolderAPI, deleteFolder } = useFolderStore()
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
-
+ const params = useParams()
+ const workspaceId = params.workspaceId as string
const isExpanded = expandedFolders.has(folder.id)
const updateTimeoutRef = useRef | undefined>(undefined)
const pendingStateRef = useRef(null)
@@ -76,7 +81,7 @@ export function FolderItem({
try {
await updateFolderAPI(folderId, { name: newName })
} catch (error) {
- console.error('Failed to rename folder:', error)
+ logger.error('Failed to rename folder:', { error })
}
}
@@ -87,10 +92,10 @@ export function FolderItem({
const confirmDelete = async () => {
setIsDeleting(true)
try {
- await deleteFolder(folder.id)
+ await deleteFolder(folder.id, workspaceId)
setShowDeleteDialog(false)
} catch (error) {
- console.error('Failed to delete folder:', error)
+ logger.error('Failed to delete folder:', { error })
} finally {
setIsDeleting(false)
}
diff --git a/apps/sim/app/w/components/sidebar/components/folder-tree/components/workflow-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-tree/components/workflow-item.tsx
similarity index 91%
rename from apps/sim/app/w/components/sidebar/components/folder-tree/components/workflow-item.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-tree/components/workflow-item.tsx
index 07bf393d9bb..f054a4ca7a7 100644
--- a/apps/sim/app/w/components/sidebar/components/folder-tree/components/workflow-item.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-tree/components/workflow-item.tsx
@@ -3,10 +3,14 @@
import { useRef, useState } from 'react'
import clsx from 'clsx'
import Link from 'next/link'
+import { useParams } from 'next/navigation'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
+import { createLogger } from '@/lib/logs/console-logger'
import { useFolderStore, useIsWorkflowSelected } from '@/stores/folders/store'
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
+const logger = createLogger('WorkflowItem')
+
interface WorkflowItemProps {
workflow: WorkflowMetadata
active: boolean
@@ -26,6 +30,8 @@ export function WorkflowItem({
}: WorkflowItemProps) {
const [isDragging, setIsDragging] = useState(false)
const dragStartedRef = useRef(false)
+ const params = useParams()
+ const workspaceId = params.workspaceId as string
const { selectedWorkflows, selectOnly, toggleWorkflowSelection } = useFolderStore()
const isSelected = useIsWorkflowSelected(workflow.id)
@@ -74,7 +80,7 @@ export function WorkflowItem({
{
- if (activeWorkspaceId) {
- fetchFolders(activeWorkspaceId)
+ if (workspaceId) {
+ fetchFolders(workspaceId)
}
- }, [activeWorkspaceId, fetchFolders])
+ }, [workspaceId, fetchFolders])
useEffect(() => {
clearSelection()
- }, [activeWorkspaceId, clearSelection])
+ }, [workspaceId, clearSelection])
- const folderTree = activeWorkspaceId ? getFolderTree(activeWorkspaceId) : []
+ const folderTree = workspaceId ? getFolderTree(workspaceId) : []
// Group workflows by folder
const workflowsByFolder = regularWorkflows.reduce(
@@ -255,7 +258,7 @@ export function FolderTree({
- No workflows or folders in {activeWorkspaceId ? 'this workspace' : 'your account'}.
- Create one to get started.
+ No workflows or folders in {workspaceId ? 'this workspace' : 'your account'}. Create one
+ to get started.
)}
diff --git a/apps/sim/app/w/components/sidebar/components/help-modal/components/help-form/help-form.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/help-modal/components/help-form/help-form.tsx
similarity index 100%
rename from apps/sim/app/w/components/sidebar/components/help-modal/components/help-form/help-form.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/help-modal/components/help-form/help-form.tsx
diff --git a/apps/sim/app/w/components/sidebar/components/help-modal/help-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/help-modal/help-modal.tsx
similarity index 100%
rename from apps/sim/app/w/components/sidebar/components/help-modal/help-modal.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/help-modal/help-modal.tsx
diff --git a/apps/sim/app/w/components/sidebar/components/invite-modal/invite-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/invite-modal/invite-modal.tsx
similarity index 98%
rename from apps/sim/app/w/components/sidebar/components/invite-modal/invite-modal.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/invite-modal/invite-modal.tsx
index fd28d8d910f..ff51008450e 100644
--- a/apps/sim/app/w/components/sidebar/components/invite-modal/invite-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/invite-modal/invite-modal.tsx
@@ -2,6 +2,7 @@
import React, { type KeyboardEvent, useCallback, useEffect, useMemo, useState } from 'react'
import { HelpCircle, Loader2, X } from 'lucide-react'
+import { useParams } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
@@ -15,10 +16,9 @@ import { cn } from '@/lib/utils'
import {
useUserPermissionsContext,
useWorkspacePermissionsContext,
-} from '@/app/w/components/providers/workspace-permissions-provider'
+} from '@/app/workspace/[workspaceId]/w/components/providers/workspace-permissions-provider'
import type { WorkspacePermissions } from '@/hooks/use-workspace-permissions'
import { API_ENDPOINTS } from '@/stores/constants'
-import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
const logger = createLogger('InviteModal')
@@ -397,7 +397,9 @@ export function InviteModal({ open, onOpenChange }: InviteModalProps) {
const [showSent, setShowSent] = useState(false)
const [errorMessage, setErrorMessage] = useState
(null)
const [successMessage, setSuccessMessage] = useState(null)
- const { activeWorkspaceId } = useWorkflowRegistry()
+ const params = useParams()
+ const workspaceId = params.workspaceId as string
+
const { data: session } = useSession()
const {
workspacePermissions,
@@ -410,7 +412,7 @@ export function InviteModal({ open, onOpenChange }: InviteModalProps) {
const hasNewInvites = emails.length > 0 || inputValue.trim()
const fetchPendingInvitations = useCallback(async () => {
- if (!activeWorkspaceId) return
+ if (!workspaceId) return
setIsPendingInvitationsLoading(true)
try {
@@ -421,7 +423,7 @@ export function InviteModal({ open, onOpenChange }: InviteModalProps) {
data.invitations
?.filter(
(inv: PendingInvitation) =>
- inv.status === 'pending' && inv.workspaceId === activeWorkspaceId
+ inv.status === 'pending' && inv.workspaceId === workspaceId
)
.map((inv: PendingInvitation) => ({
email: inv.email,
@@ -436,10 +438,10 @@ export function InviteModal({ open, onOpenChange }: InviteModalProps) {
} finally {
setIsPendingInvitationsLoading(false)
}
- }, [activeWorkspaceId])
+ }, [workspaceId])
useEffect(() => {
- if (open && activeWorkspaceId) {
+ if (open && workspaceId) {
fetchPendingInvitations()
}
}, [open, fetchPendingInvitations])
@@ -535,7 +537,7 @@ export function InviteModal({ open, onOpenChange }: InviteModalProps) {
)
const handleSaveChanges = useCallback(async () => {
- if (!userPerms.canAdmin || !hasPendingChanges || !activeWorkspaceId) return
+ if (!userPerms.canAdmin || !hasPendingChanges || !workspaceId) return
setIsSaving(true)
setErrorMessage(null)
@@ -546,7 +548,7 @@ export function InviteModal({ open, onOpenChange }: InviteModalProps) {
permissions: changes.permissionType || 'read',
}))
- const response = await fetch(API_ENDPOINTS.WORKSPACE_PERMISSIONS(activeWorkspaceId), {
+ const response = await fetch(API_ENDPOINTS.WORKSPACE_PERMISSIONS(workspaceId), {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
@@ -583,7 +585,7 @@ export function InviteModal({ open, onOpenChange }: InviteModalProps) {
}, [
userPerms.canAdmin,
hasPendingChanges,
- activeWorkspaceId,
+ workspaceId,
existingUserPermissionChanges,
updatePermissions,
])
@@ -646,7 +648,7 @@ export function InviteModal({ open, onOpenChange }: InviteModalProps) {
setErrorMessage(null)
setSuccessMessage(null)
- if (emails.length === 0 || !activeWorkspaceId) {
+ if (emails.length === 0 || !workspaceId) {
return
}
@@ -667,7 +669,7 @@ export function InviteModal({ open, onOpenChange }: InviteModalProps) {
'Content-Type': 'application/json',
},
body: JSON.stringify({
- workspaceId: activeWorkspaceId,
+ workspaceId,
email: email,
role: 'member',
permission: permissionType,
@@ -739,7 +741,7 @@ export function InviteModal({ open, onOpenChange }: InviteModalProps) {
inputValue,
addEmail,
emails,
- activeWorkspaceId,
+ workspaceId,
userPermissions,
invalidEmails,
fetchPendingInvitations,
@@ -922,7 +924,7 @@ export function InviteModal({ open, onOpenChange }: InviteModalProps) {
!hasNewInvites ||
isSubmitting ||
isSaving ||
- !activeWorkspaceId
+ !workspaceId
}
className={cn(
'ml-auto gap-2 font-medium',
diff --git a/apps/sim/app/w/components/sidebar/components/invite-modal/invites-sent/invites-sent.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/invite-modal/invites-sent/invites-sent.tsx
similarity index 96%
rename from apps/sim/app/w/components/sidebar/components/invite-modal/invites-sent/invites-sent.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/invite-modal/invites-sent/invites-sent.tsx
index dc04a9e719c..12ff428a0f5 100644
--- a/apps/sim/app/w/components/sidebar/components/invite-modal/invites-sent/invites-sent.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/invite-modal/invites-sent/invites-sent.tsx
@@ -1,6 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
+import { useParams } from 'next/navigation'
import { Skeleton } from '@/components/ui/skeleton'
import {
Table,
@@ -11,7 +12,6 @@ import {
TableRow,
} from '@/components/ui/table'
import { createLogger } from '@/lib/logs/console-logger'
-import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
const logger = createLogger('InvitesSent')
@@ -47,11 +47,12 @@ export function InvitesSent() {
const [invitations, setInvitations] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
- const { activeWorkspaceId } = useWorkflowRegistry()
+ const params = useParams()
+ const workspaceId = params.workspaceId as string
useEffect(() => {
async function fetchInvitations() {
- if (!activeWorkspaceId) {
+ if (!workspaceId) {
setIsLoading(false)
return
}
@@ -82,7 +83,7 @@ export function InvitesSent() {
}
fetchInvitations()
- }, [activeWorkspaceId])
+ }, [workspaceId])
const TableSkeleton = () => (
@@ -106,7 +107,7 @@ export function InvitesSent() {
)
}
- if (!activeWorkspaceId) {
+ if (!workspaceId) {
return null
}
diff --git a/apps/sim/app/w/components/sidebar/components/nav-section/nav-section.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/nav-section/nav-section.tsx
similarity index 100%
rename from apps/sim/app/w/components/sidebar/components/nav-section/nav-section.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/nav-section/nav-section.tsx
diff --git a/apps/sim/app/w/components/sidebar/components/settings-modal/components/account/account.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/account/account.tsx
similarity index 100%
rename from apps/sim/app/w/components/sidebar/components/settings-modal/components/account/account.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/account/account.tsx
diff --git a/apps/sim/app/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx
similarity index 100%
rename from apps/sim/app/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx
diff --git a/apps/sim/app/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx
similarity index 99%
rename from apps/sim/app/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx
index da4faef5dc4..21ad99b186b 100644
--- a/apps/sim/app/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx
@@ -172,10 +172,10 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
}
// Clear the URL parameters
- router.replace('/w')
+ router.replace('/workspace')
} else if (error) {
logger.error('OAuth error:', { error })
- router.replace('/w')
+ router.replace('/workspace')
}
}, [searchParams, router, userId])
diff --git a/apps/sim/app/w/components/sidebar/components/settings-modal/components/environment/environment.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/environment/environment.tsx
similarity index 100%
rename from apps/sim/app/w/components/sidebar/components/settings-modal/components/environment/environment.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/environment/environment.tsx
diff --git a/apps/sim/app/w/components/sidebar/components/settings-modal/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx
similarity index 100%
rename from apps/sim/app/w/components/sidebar/components/settings-modal/components/general/general.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx
diff --git a/apps/sim/app/w/components/sidebar/components/settings-modal/components/privacy/privacy.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/privacy/privacy.tsx
similarity index 100%
rename from apps/sim/app/w/components/sidebar/components/settings-modal/components/privacy/privacy.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/privacy/privacy.tsx
diff --git a/apps/sim/app/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx
similarity index 100%
rename from apps/sim/app/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx
diff --git a/apps/sim/app/w/components/sidebar/components/settings-modal/components/subscription/components/team-seats-dialog.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/team-seats-dialog.tsx
similarity index 100%
rename from apps/sim/app/w/components/sidebar/components/settings-modal/components/subscription/components/team-seats-dialog.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/team-seats-dialog.tsx
diff --git a/apps/sim/app/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx
similarity index 100%
rename from apps/sim/app/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx
diff --git a/apps/sim/app/w/components/sidebar/components/settings-modal/components/team-management/team-management.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/team-management.tsx
similarity index 100%
rename from apps/sim/app/w/components/sidebar/components/settings-modal/components/team-management/team-management.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/team-management.tsx
diff --git a/apps/sim/app/w/components/sidebar/components/settings-modal/settings-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx
similarity index 100%
rename from apps/sim/app/w/components/sidebar/components/settings-modal/settings-modal.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx
diff --git a/apps/sim/app/w/components/sidebar/components/sidebar-control/sidebar-control.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-control/sidebar-control.tsx
similarity index 100%
rename from apps/sim/app/w/components/sidebar/components/sidebar-control/sidebar-control.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-control/sidebar-control.tsx
diff --git a/apps/sim/app/w/components/sidebar/components/workflow-list/workflow-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx
similarity index 87%
rename from apps/sim/app/w/components/sidebar/components/workflow-list/workflow-list.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx
index bf4e067eb8f..c54e7d73e94 100644
--- a/apps/sim/app/w/components/sidebar/components/workflow-list/workflow-list.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx
@@ -3,10 +3,9 @@
import { useMemo } from 'react'
import clsx from 'clsx'
import Link from 'next/link'
-import { usePathname } from 'next/navigation'
+import { useParams, usePathname } from 'next/navigation'
import { Skeleton } from '@/components/ui/skeleton'
import { useSession } from '@/lib/auth-client'
-import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
interface WorkflowItemProps {
@@ -17,9 +16,12 @@ interface WorkflowItemProps {
}
function WorkflowItem({ workflow, active, isMarketplace, isCollapsed }: WorkflowItemProps) {
+ const params = useParams()
+ const workspaceId = params.workspaceId as string
+
return (
))}
@@ -121,7 +124,7 @@ export function WorkflowList({
@@ -132,8 +135,8 @@ export function WorkflowList({
{/* Empty state */}
{showEmptyState && !isCollapsed && (
- No workflows in {activeWorkspaceId ? 'this workspace' : 'your account'}. Create one to
- get started.
+ No workflows in {workspaceId ? 'this workspace' : 'your account'}. Create one to get
+ started.
)}
>
diff --git a/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx
similarity index 93%
rename from apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx
index 4f9351d0154..3d67c0087d6 100644
--- a/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx
@@ -3,7 +3,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { ChevronDown, Pencil, Trash2, X } from 'lucide-react'
import Link from 'next/link'
-import { useRouter } from 'next/navigation'
+import { useParams, useRouter } from 'next/navigation'
import { AgentIcon } from '@/components/icons'
import {
AlertDialog,
@@ -28,11 +28,14 @@ import {
import { Input } from '@/components/ui/input'
import { Skeleton } from '@/components/ui/skeleton'
import { useSession } from '@/lib/auth-client'
+import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
-import { useUserPermissionsContext } from '@/app/w/components/providers/workspace-permissions-provider'
+import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/w/components/providers/workspace-permissions-provider'
import { useSidebarStore } from '@/stores/sidebar/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
+const logger = createLogger('WorkspaceHeader')
+
interface Workspace {
id: string
name: string
@@ -254,7 +257,9 @@ export const WorkspaceHeader = React.memo
(
const router = useRouter()
// Get workflowRegistry state and actions
- const { activeWorkspaceId, switchToWorkspace, setActiveWorkspaceId } = useWorkflowRegistry()
+ const { switchToWorkspace } = useWorkflowRegistry()
+ const params = useParams()
+ const currentWorkspaceId = params.workspaceId as string
// Get user permissions for the active workspace
const userPermissions = useUserPermissionsContext()
@@ -275,7 +280,7 @@ export const WorkspaceHeader = React.memo(
const data = await response.json()
setPlan(data.isPro ? 'Pro Plan' : 'Free Plan')
} catch (err) {
- console.error('Error fetching subscription status:', err)
+ logger.error('Error fetching subscription status:', err)
}
}, [])
@@ -289,30 +294,36 @@ export const WorkspaceHeader = React.memo(
const fetchedWorkspaces = data.workspaces as Workspace[]
setWorkspaces(fetchedWorkspaces)
- // Only update workspace if we have a valid activeWorkspaceId from registry
- if (activeWorkspaceId) {
+ // Only update workspace if we have a valid currentWorkspaceId from URL
+ if (currentWorkspaceId) {
const matchingWorkspace = fetchedWorkspaces.find(
- (workspace) => workspace.id === activeWorkspaceId
+ (workspace) => workspace.id === currentWorkspaceId
)
if (matchingWorkspace) {
setActiveWorkspace(matchingWorkspace)
} else {
- // Active workspace not found, fallback to first workspace
- const fallbackWorkspace = fetchedWorkspaces[0]
- if (fallbackWorkspace) {
+ // Log the mismatch for debugging
+ logger.warn(`Workspace ${currentWorkspaceId} not found in user's workspaces`)
+
+ // Current workspace not found, fallback to first workspace
+ if (fetchedWorkspaces.length > 0) {
+ const fallbackWorkspace = fetchedWorkspaces[0]
setActiveWorkspace(fallbackWorkspace)
- setActiveWorkspaceId(fallbackWorkspace.id)
+ // Navigate to the fallback workspace
+ router.push(`/workspace/${fallbackWorkspace.id}/w`)
+ } else {
+ // No workspaces available - handle this edge case
+ logger.error('No workspaces available for user')
}
}
}
- // If no activeWorkspaceId, let loadWorkspaceFromWorkflowId handle workspace selection
}
} catch (err) {
- console.error('Error fetching workspaces:', err)
+ logger.error('Error fetching workspaces:', err)
} finally {
setIsWorkspacesLoading(false)
}
- }, [activeWorkspaceId, setActiveWorkspaceId])
+ }, [currentWorkspaceId, router])
useEffect(() => {
// Fetch subscription status if user is logged in
@@ -337,7 +348,7 @@ export const WorkspaceHeader = React.memo(
switchToWorkspace(workspace.id)
// Update URL to include workspace ID
- router.push(`/w/${workspace.id}`)
+ router.push(`/workspace/${workspace.id}/w`)
},
[activeWorkspace?.id, switchToWorkspace, router]
)
@@ -367,10 +378,10 @@ export const WorkspaceHeader = React.memo(
switchToWorkspace(newWorkspace.id)
// Update URL to include new workspace ID
- router.push(`/w/${newWorkspace.id}`)
+ router.push(`/workspace/${newWorkspace.id}/w`)
}
} catch (err) {
- console.error('Error creating workspace:', err)
+ logger.error('Error creating workspace:', err)
} finally {
setIsWorkspacesLoading(false)
}
@@ -396,7 +407,7 @@ export const WorkspaceHeader = React.memo(
if (!response.ok) {
if (response.status === 403) {
- console.error(
+ logger.error(
'Permission denied: Only users with admin permissions can update workspaces'
)
}
@@ -420,7 +431,7 @@ export const WorkspaceHeader = React.memo(
})
}
} catch (err) {
- console.error('Error updating workspace:', err)
+ logger.error('Error updating workspace:', err)
} finally {
setIsWorkspacesLoading(false)
}
@@ -442,7 +453,7 @@ export const WorkspaceHeader = React.memo(
if (!response.ok) {
if (response.status === 403) {
- console.error(
+ logger.error(
'Permission denied: Only users with admin permissions can delete workspaces'
)
}
@@ -463,7 +474,7 @@ export const WorkspaceHeader = React.memo(
setIsOpen(false)
} catch (err) {
- console.error('Error deleting workspace:', err)
+ logger.error('Error deleting workspace:', err)
} finally {
setIsDeleting(false)
}
@@ -486,7 +497,7 @@ export const WorkspaceHeader = React.memo(
// Determine URL for workspace links
const workspaceUrl = useMemo(
- () => (activeWorkspace ? `/w/${activeWorkspace.id}` : '/w'),
+ () => (activeWorkspace ? `/workspace/${activeWorkspace.id}/w` : '/workspace'),
[activeWorkspace]
)
diff --git a/apps/sim/app/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
similarity index 91%
rename from apps/sim/app/w/components/sidebar/sidebar.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
index e8485572e82..f1557a64796 100644
--- a/apps/sim/app/w/components/sidebar/sidebar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
@@ -3,12 +3,15 @@
import { useEffect, useMemo, useState } from 'react'
import clsx from 'clsx'
import { HelpCircle, LibraryBig, ScrollText, Send, Settings } from 'lucide-react'
-import { usePathname, useRouter } from 'next/navigation'
+import { useParams, usePathname, useRouter } from 'next/navigation'
import { Skeleton } from '@/components/ui/skeleton'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useSession } from '@/lib/auth-client'
import { createLogger } from '@/lib/logs/console-logger'
-import { getKeyboardShortcutText, useGlobalShortcuts } from '@/app/w/hooks/use-keyboard-shortcuts'
+import {
+ getKeyboardShortcutText,
+ useGlobalShortcuts,
+} from '@/app/workspace/[workspaceId]/w/hooks/use-keyboard-shortcuts'
import { useSidebarStore } from '@/stores/sidebar/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
@@ -31,16 +34,13 @@ export function Sidebar() {
useRegistryLoading()
useGlobalShortcuts()
- const {
- workflows,
- activeWorkspaceId,
- createWorkflow,
- isLoading: workflowsLoading,
- } = useWorkflowRegistry()
+ const { workflows, createWorkflow, isLoading: workflowsLoading } = useWorkflowRegistry()
const { isPending: sessionLoading } = useSession()
const userPermissions = useUserPermissionsContext()
const isLoading = workflowsLoading || sessionLoading
const router = useRouter()
+ const params = useParams()
+ const workspaceId = params.workspaceId as string
const pathname = usePathname()
const [showSettings, setShowSettings] = useState(false)
@@ -66,7 +66,7 @@ export function Sidebar() {
if (!isLoading) {
Object.values(workflows).forEach((workflow) => {
- if (workflow.workspaceId === activeWorkspaceId || !workflow.workspaceId) {
+ if (workflow.workspaceId === workspaceId || !workflow.workspaceId) {
if (workflow.marketplaceData?.status === 'temp') {
temp.push(workflow)
} else {
@@ -93,16 +93,16 @@ export function Sidebar() {
}
return { regularWorkflows: regular, tempWorkflows: temp }
- }, [workflows, isLoading, activeWorkspaceId])
+ }, [workflows, isLoading, workspaceId])
// Create workflow handler
const handleCreateWorkflow = async (folderId?: string) => {
try {
const id = await createWorkflow({
- workspaceId: activeWorkspaceId || undefined,
+ workspaceId: workspaceId || undefined,
folderId: folderId || undefined,
})
- router.push(`/w/${id}`)
+ router.push(`/workspace/${workspaceId}/w/${id}`)
} catch (error) {
logger.error('Error creating workflow:', error)
}
@@ -154,10 +154,10 @@ export function Sidebar() {
{/* Workflows Section */}
{isLoading ? : 'Workflows'}
@@ -179,18 +179,18 @@ export function Sidebar() {
}
- href='/w/logs'
+ href={`/workspace/${workspaceId}/logs`}
label='Logs'
- active={pathname === '/w/logs'}
+ active={pathname === `/workspace/${workspaceId}/logs`}
isCollapsed={isCollapsed}
shortcutCommand={getKeyboardShortcutText('L', true, true)}
shortcutCommandPosition='below'
/>
}
- href='/w/knowledge'
+ href={`/workspace/${workspaceId}/knowledge`}
label='Knowledge'
- active={pathname === '/w/knowledge'}
+ active={pathname === `/workspace/${workspaceId}/knowledge`}
isCollapsed={isCollapsed}
shortcutCommand={getKeyboardShortcutText('K', true, true)}
shortcutCommandPosition='below'
diff --git a/apps/sim/app/w/components/workflow-preview/workflow-preview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview.tsx
similarity index 93%
rename from apps/sim/app/w/components/workflow-preview/workflow-preview.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview.tsx
index 79abac5ab47..ff6a7c2e97a 100644
--- a/apps/sim/app/w/components/workflow-preview/workflow-preview.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview.tsx
@@ -15,10 +15,10 @@ import 'reactflow/dist/style.css'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
-import { LoopNodeComponent } from '@/app/w/[id]/components/loop-node/loop-node'
-import { ParallelNodeComponent } from '@/app/w/[id]/components/parallel-node/parallel-node'
-import { WorkflowBlock } from '@/app/w/[id]/components/workflow-block/workflow-block'
-import { WorkflowEdge } from '@/app/w/[id]/components/workflow-edge/workflow-edge'
+import { LoopNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/loop-node/loop-node'
+import { ParallelNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/parallel-node/parallel-node'
+import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
+import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge'
import { getBlock } from '@/blocks'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
diff --git a/apps/sim/app/w/hooks/use-keyboard-shortcuts.ts b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-keyboard-shortcuts.ts
similarity index 89%
rename from apps/sim/app/w/hooks/use-keyboard-shortcuts.ts
rename to apps/sim/app/workspace/[workspaceId]/w/hooks/use-keyboard-shortcuts.ts
index 9f49d8a1818..70d418eb620 100644
--- a/apps/sim/app/w/hooks/use-keyboard-shortcuts.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-keyboard-shortcuts.ts
@@ -95,7 +95,16 @@ export function useGlobalShortcuts() {
((isMac && event.metaKey) || (!isMac && event.ctrlKey))
) {
event.preventDefault()
- router.push('/w/logs')
+
+ const pathParts = window.location.pathname.split('/')
+ const workspaceIndex = pathParts.indexOf('workspace')
+
+ if (workspaceIndex !== -1 && pathParts[workspaceIndex + 1]) {
+ const workspaceId = pathParts[workspaceIndex + 1]
+ router.push(`/workspace/${workspaceId}/logs`)
+ } else {
+ router.push('/workspace')
+ }
}
}
diff --git a/apps/sim/app/w/hooks/use-registry-loading.ts b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-registry-loading.ts
similarity index 69%
rename from apps/sim/app/w/hooks/use-registry-loading.ts
rename to apps/sim/app/workspace/[workspaceId]/w/hooks/use-registry-loading.ts
index 207514e18fd..239a7148dbe 100644
--- a/apps/sim/app/w/hooks/use-registry-loading.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-registry-loading.ts
@@ -1,7 +1,7 @@
'use client'
import { useEffect } from 'react'
-import { usePathname, useRouter } from 'next/navigation'
+import { useParams, usePathname, useRouter } from 'next/navigation'
import { createLogger } from '@/lib/logs/console-logger'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -34,44 +34,41 @@ function extractWorkflowIdFromPathname(pathname: string): string | null {
* Custom hook to manage workflow registry loading state and handle first-time navigation
*
* This hook initializes the loading state and automatically clears it
- * when workflows are loaded. It also handles smart workspace selection
- * and navigation for first-time users.
+ * when workflows are loaded. It also handles navigation for first-time users.
*/
export function useRegistryLoading() {
- const { workflows, setLoading, isLoading, activeWorkspaceId, loadWorkspaceFromWorkflowId } =
- useWorkflowRegistry()
+ const { workflows, setLoading, isLoading, loadWorkflows } = useWorkflowRegistry()
const pathname = usePathname()
const router = useRouter()
+ const params = useParams()
+ const workspaceId = params.workspaceId as string
- // Handle workspace selection from URL
+ // Load workflows for current workspace
useEffect(() => {
- if (!activeWorkspaceId) {
- const workflowIdFromUrl = extractWorkflowIdFromPathname(pathname)
- if (workflowIdFromUrl) {
- loadWorkspaceFromWorkflowId(workflowIdFromUrl).catch((error) => {
- logger.warn('Failed to load workspace from workflow ID:', error)
- })
- }
+ if (workspaceId) {
+ loadWorkflows(workspaceId).catch((error) => {
+ logger.warn('Failed to load workflows for workspace:', error)
+ })
}
- }, [activeWorkspaceId, pathname, loadWorkspaceFromWorkflowId])
+ }, [workspaceId, loadWorkflows])
// Handle first-time navigation: if we're at /w and have workflows, navigate to first one
useEffect(() => {
- if (!isLoading && activeWorkspaceId && Object.keys(workflows).length > 0) {
- const workflowCount = Object.keys(workflows).length
+ if (!isLoading && workspaceId && Object.keys(workflows).length > 0) {
const currentWorkflowId = extractWorkflowIdFromPathname(pathname)
- // If we're at a generic workspace URL (/w, /w/, or /w/workspaceId) without a specific workflow
+ // Check if we're on the workspace root and need to redirect to first workflow
if (
- !currentWorkflowId &&
- (pathname === '/w' || pathname === '/w/' || pathname === `/w/${activeWorkspaceId}`)
+ (pathname === `/workspace/${workspaceId}/w` ||
+ pathname === `/workspace/${workspaceId}/w/`) &&
+ Object.keys(workflows).length > 0
) {
const firstWorkflowId = Object.keys(workflows)[0]
logger.info('First-time navigation: redirecting to first workflow:', firstWorkflowId)
- router.replace(`/w/${firstWorkflowId}`)
+ router.replace(`/workspace/${workspaceId}/w/${firstWorkflowId}`)
}
}
- }, [isLoading, activeWorkspaceId, workflows, pathname, router])
+ }, [isLoading, workspaceId, workflows, pathname, router])
// Handle loading states
useEffect(() => {
diff --git a/apps/sim/app/w/page.tsx b/apps/sim/app/workspace/[workspaceId]/w/page.tsx
similarity index 70%
rename from apps/sim/app/w/page.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/page.tsx
index fea6f1d2df9..998bd9a27b8 100644
--- a/apps/sim/app/w/page.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/page.tsx
@@ -1,12 +1,15 @@
'use client'
import { useEffect } from 'react'
-import { useRouter } from 'next/navigation'
+import { useParams, useRouter } from 'next/navigation'
+import { LoadingAgent } from '@/components/ui/loading-agent'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
export default function WorkflowsPage() {
const router = useRouter()
const { workflows, isLoading } = useWorkflowRegistry()
+ const params = useParams()
+ const workspaceId = params.workspaceId
useEffect(() => {
// Wait for workflows to load
@@ -16,7 +19,7 @@ export default function WorkflowsPage() {
// If we have workflows, redirect to the first one
if (workflowIds.length > 0) {
- router.replace(`/w/${workflowIds[0]}`)
+ router.replace(`/workspace/${workspaceId}/w/${workflowIds[0]}`)
return
}
@@ -24,14 +27,15 @@ export default function WorkflowsPage() {
// or the user doesn't have any workspaces. Redirect to home to let the system
// handle workspace/workflow creation properly.
router.replace('/')
- }, [workflows, isLoading, router])
+ }, [workflows, isLoading, router, workspaceId])
// Show loading state while determining where to redirect
return (
-
-
Loading workflows...
+
+
+
)
diff --git a/apps/sim/app/workspace/page.tsx b/apps/sim/app/workspace/page.tsx
new file mode 100644
index 00000000000..d6d6b54e62b
--- /dev/null
+++ b/apps/sim/app/workspace/page.tsx
@@ -0,0 +1,134 @@
+'use client'
+
+import { useEffect } from 'react'
+import { useRouter } from 'next/navigation'
+import { LoadingAgent } from '@/components/ui/loading-agent'
+import { useSession } from '@/lib/auth-client'
+import { createLogger } from '@/lib/logs/console-logger'
+
+const logger = createLogger('WorkspacePage')
+
+export default function WorkspacePage() {
+ const router = useRouter()
+ const { data: session, isPending } = useSession()
+
+ useEffect(() => {
+ const redirectToFirstWorkspace = async () => {
+ // Wait for session to load
+ if (isPending) {
+ return
+ }
+
+ // If user is not authenticated, redirect to login
+ if (!session?.user) {
+ logger.info('User not authenticated, redirecting to login')
+ router.replace('/login')
+ return
+ }
+
+ try {
+ // Check if we need to redirect a specific workflow from old URL format
+ const urlParams = new URLSearchParams(window.location.search)
+ const redirectWorkflowId = urlParams.get('redirect_workflow')
+
+ if (redirectWorkflowId) {
+ // Try to get the workspace for this workflow
+ try {
+ const workflowResponse = await fetch(`/api/workflows/${redirectWorkflowId}`)
+ if (workflowResponse.ok) {
+ const workflowData = await workflowResponse.json()
+ const workspaceId = workflowData.data?.workspaceId
+
+ if (workspaceId) {
+ logger.info(
+ `Redirecting workflow ${redirectWorkflowId} to workspace ${workspaceId}`
+ )
+ router.replace(`/workspace/${workspaceId}/w/${redirectWorkflowId}`)
+ return
+ }
+ }
+ } catch (error) {
+ logger.error('Error fetching workflow for redirect:', error)
+ }
+ }
+
+ // Fetch user's workspaces
+ const response = await fetch('/api/workspaces')
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch workspaces')
+ }
+
+ const data = await response.json()
+ const workspaces = data.workspaces || []
+
+ if (workspaces.length === 0) {
+ logger.warn('No workspaces found for user, creating default workspace')
+
+ try {
+ const createResponse = await fetch('/api/workspaces', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ name: 'My Workspace' }),
+ })
+
+ if (createResponse.ok) {
+ const createData = await createResponse.json()
+ const newWorkspace = createData.workspace
+
+ if (newWorkspace?.id) {
+ logger.info(`Created default workspace: ${newWorkspace.id}`)
+ router.replace(`/workspace/${newWorkspace.id}/w`)
+ return
+ }
+ }
+
+ logger.error('Failed to create default workspace')
+ } catch (createError) {
+ logger.error('Error creating default workspace:', createError)
+ }
+
+ // If we can't create a workspace, redirect to login to reset state
+ router.replace('/login')
+ return
+ }
+
+ // Get the first workspace (they should be ordered by most recent)
+ const firstWorkspace = workspaces[0]
+ logger.info(`Redirecting to first workspace: ${firstWorkspace.id}`)
+
+ // Redirect to the first workspace
+ router.replace(`/workspace/${firstWorkspace.id}/w`)
+ } catch (error) {
+ logger.error('Error fetching workspaces for redirect:', error)
+ // Don't redirect if there's an error - let the user stay on the page
+ }
+ }
+
+ // Only run this logic when we're at the root /workspace path
+ // If we're already in a specific workspace, the children components will handle it
+ if (typeof window !== 'undefined' && window.location.pathname === '/workspace') {
+ redirectToFirstWorkspace()
+ }
+ }, [session, isPending, router])
+
+ // Show loading state while we determine where to redirect
+ if (isPending) {
+ return (
+
+ )
+ }
+
+ // If user is not authenticated, show nothing (redirect will happen)
+ if (!session?.user) {
+ return null
+ }
+
+ return null
+}
diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx
index 86e9f39a5a3..cdd13dcfb37 100644
--- a/apps/sim/components/icons.tsx
+++ b/apps/sim/components/icons.tsx
@@ -2949,7 +2949,7 @@ export const ResponseIcon = (props: SVGProps) => (
>
)
diff --git a/apps/sim/components/ui/loading-agent.tsx b/apps/sim/components/ui/loading-agent.tsx
index 582d2ae609f..5f87b57f17d 100644
--- a/apps/sim/components/ui/loading-agent.tsx
+++ b/apps/sim/components/ui/loading-agent.tsx
@@ -11,7 +11,6 @@ export interface LoadingAgentProps {
export function LoadingAgent({ size = 'md' }: LoadingAgentProps) {
const pathLength = 120
- // Size mappings for width and height
const sizes = {
sm: { width: 16, height: 18 },
md: { width: 21, height: 24 },
diff --git a/apps/sim/components/ui/tag-dropdown.tsx b/apps/sim/components/ui/tag-dropdown.tsx
index 78c9bf0a146..cb3c15b9f00 100644
--- a/apps/sim/components/ui/tag-dropdown.tsx
+++ b/apps/sim/components/ui/tag-dropdown.tsx
@@ -2,7 +2,10 @@ import type React from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
-import { type ConnectedBlock, useBlockConnections } from '@/app/w/[id]/hooks/use-block-connections'
+import {
+ type ConnectedBlock,
+ useBlockConnections,
+} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections'
import { getBlock } from '@/blocks'
import { useVariablesStore } from '@/stores/panel/variables/store'
import type { Variable } from '@/stores/panel/variables/types'
diff --git a/apps/sim/contexts/socket-context.tsx b/apps/sim/contexts/socket-context.tsx
index 7b7627a3041..2453e25f800 100644
--- a/apps/sim/contexts/socket-context.tsx
+++ b/apps/sim/contexts/socket-context.tsx
@@ -85,6 +85,9 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
const [currentWorkflowId, setCurrentWorkflowId] = useState(null)
const [presenceUsers, setPresenceUsers] = useState([])
+ // Connection state tracking
+ const reconnectCount = useRef(0)
+
// Use refs to store event handlers to avoid stale closures
const eventHandlers = useRef<{
workflowOperation?: (data: any) => void
@@ -127,10 +130,10 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
})
const socketInstance = io(socketUrl, {
- transports: ['polling', 'websocket'],
+ transports: ['websocket', 'polling'], // Keep polling fallback for reliability
withCredentials: true,
- reconnectionAttempts: 5,
- timeout: 10000,
+ reconnectionAttempts: 5, // Back to original conservative setting
+ timeout: 10000, // Back to original timeout
auth: {
token, // Send one-time token for authentication
},
@@ -140,17 +143,24 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
socketInstance.on('connect', () => {
setIsConnected(true)
setIsConnecting(false)
+ reconnectCount.current = 0
+
logger.info('Socket connected successfully', {
socketId: socketInstance.id,
connected: socketInstance.connected,
transport: socketInstance.io.engine?.transport?.name,
+ reconnectCount: reconnectCount.current,
})
})
socketInstance.on('disconnect', (reason) => {
setIsConnected(false)
setIsConnecting(false)
- logger.info('Socket disconnected', { reason })
+
+ logger.info('Socket disconnected', {
+ reason,
+ reconnectCount: reconnectCount.current,
+ })
// Clear presence when disconnected
setPresenceUsers([])
@@ -169,7 +179,10 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
// Add reconnection logging
socketInstance.on('reconnect', (attemptNumber) => {
- logger.info('Socket reconnected', { attemptNumber })
+ reconnectCount.current = attemptNumber
+ logger.info('Socket reconnected', {
+ attemptNumber,
+ })
})
socketInstance.on('reconnect_attempt', (attemptNumber) => {
@@ -189,15 +202,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
setPresenceUsers(users)
})
- socketInstance.on('user-joined', (userData) => {
- setPresenceUsers((prev) => [...prev, userData])
- eventHandlers.current.userJoined?.(userData)
- })
-
- socketInstance.on('user-left', ({ userId, socketId }) => {
- setPresenceUsers((prev) => prev.filter((u) => u.socketId !== socketId))
- eventHandlers.current.userLeft?.({ userId, socketId })
- })
+ // Note: user-joined and user-left events removed in favor of authoritative presence-update
// Workflow operation events
socketInstance.on('workflow-operation', (data) => {
@@ -276,6 +281,15 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
// Start the socket initialization
initializeSocket()
+
+ // Cleanup on unmount
+ return () => {
+ positionUpdateTimeouts.current.forEach((timeoutId) => {
+ clearTimeout(timeoutId)
+ })
+ positionUpdateTimeouts.current.clear()
+ pendingPositionUpdates.current.clear()
+ }
}, [user?.id])
// Join workflow room
@@ -299,13 +313,55 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
socket.emit('leave-workflow')
setCurrentWorkflowId(null)
setPresenceUsers([])
+
+ // Clean up any pending position updates
+ positionUpdateTimeouts.current.forEach((timeoutId) => {
+ clearTimeout(timeoutId)
+ })
+ positionUpdateTimeouts.current.clear()
+ pendingPositionUpdates.current.clear()
}
}, [socket, currentWorkflowId])
+ // Light throttling for position updates to ensure smooth collaborative movement
+ const positionUpdateTimeouts = useRef>(new Map())
+ const pendingPositionUpdates = useRef>(new Map())
+
// Emit workflow operations (blocks, edges, subflows)
const emitWorkflowOperation = useCallback(
(operation: string, target: string, payload: any) => {
- if (socket && currentWorkflowId) {
+ if (!socket || !currentWorkflowId) return
+
+ // Apply light throttling only to position updates for smooth collaborative experience
+ const isPositionUpdate = operation === 'update-position' && target === 'block'
+
+ if (isPositionUpdate && payload.id) {
+ const blockId = payload.id
+
+ // Store the latest position update
+ pendingPositionUpdates.current.set(blockId, {
+ operation,
+ target,
+ payload,
+ timestamp: Date.now(),
+ })
+
+ // Check if we already have a pending timeout for this block
+ if (!positionUpdateTimeouts.current.has(blockId)) {
+ // Schedule emission with light throttling (120fps = ~8ms)
+ const timeoutId = window.setTimeout(() => {
+ const latestUpdate = pendingPositionUpdates.current.get(blockId)
+ if (latestUpdate) {
+ socket.emit('workflow-operation', latestUpdate)
+ pendingPositionUpdates.current.delete(blockId)
+ }
+ positionUpdateTimeouts.current.delete(blockId)
+ }, 8) // 120fps for smooth movement
+
+ positionUpdateTimeouts.current.set(blockId, timeoutId)
+ }
+ } else {
+ // For all non-position updates, emit immediately
socket.emit('workflow-operation', {
operation,
target,
@@ -340,11 +396,17 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
[socket, currentWorkflowId]
)
- // Emit cursor position updates
+ // Minimal cursor throttling (reduced from 30fps to 120fps)
+ const lastCursorEmit = useRef(0)
const emitCursorUpdate = useCallback(
(cursor: { x: number; y: number }) => {
if (socket && currentWorkflowId) {
- socket.emit('cursor-update', { cursor })
+ const now = performance.now()
+ // Very light throttling at 120fps (8ms) to prevent excessive spam
+ if (now - lastCursorEmit.current >= 8) {
+ socket.emit('cursor-update', { cursor })
+ lastCursorEmit.current = now
+ }
}
},
[socket, currentWorkflowId]
diff --git a/apps/sim/db/migrations/0047_new_triathlon.sql b/apps/sim/db/migrations/0047_new_triathlon.sql
new file mode 100644
index 00000000000..c48b11ff948
--- /dev/null
+++ b/apps/sim/db/migrations/0047_new_triathlon.sql
@@ -0,0 +1 @@
+ALTER TABLE "workflow_blocks" ADD COLUMN "advanced_mode" boolean DEFAULT false NOT NULL;
\ No newline at end of file
diff --git a/apps/sim/db/migrations/meta/0047_snapshot.json b/apps/sim/db/migrations/meta/0047_snapshot.json
new file mode 100644
index 00000000000..ccde6fbffe9
--- /dev/null
+++ b/apps/sim/db/migrations/meta/0047_snapshot.json
@@ -0,0 +1,3677 @@
+{
+ "id": "399915e4-dbee-440d-8c36-eb4bc0d83962",
+ "prevId": "cc643ea0-33d4-410a-9c53-82faa9d2c352",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.account": {
+ "name": "account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.api_key": {
+ "name": "api_key",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_used": {
+ "name": "last_used",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "api_key_user_id_user_id_fk": {
+ "name": "api_key_user_id_user_id_fk",
+ "tableFrom": "api_key",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "api_key_key_unique": {
+ "name": "api_key_key_unique",
+ "nullsNotDistinct": false,
+ "columns": ["key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.chat": {
+ "name": "chat",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subdomain": {
+ "name": "subdomain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "customizations": {
+ "name": "customizations",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "auth_type": {
+ "name": "auth_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'public'"
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "allowed_emails": {
+ "name": "allowed_emails",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'"
+ },
+ "output_configs": {
+ "name": "output_configs",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "subdomain_idx": {
+ "name": "subdomain_idx",
+ "columns": [
+ {
+ "expression": "subdomain",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "chat_workflow_id_workflow_id_fk": {
+ "name": "chat_workflow_id_workflow_id_fk",
+ "tableFrom": "chat",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chat_user_id_user_id_fk": {
+ "name": "chat_user_id_user_id_fk",
+ "tableFrom": "chat",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.custom_tools": {
+ "name": "custom_tools",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "schema": {
+ "name": "schema",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "custom_tools_user_id_user_id_fk": {
+ "name": "custom_tools_user_id_user_id_fk",
+ "tableFrom": "custom_tools",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.document": {
+ "name": "document",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "filename": {
+ "name": "filename",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_url": {
+ "name": "file_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chunk_count": {
+ "name": "chunk_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "character_count": {
+ "name": "character_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "processing_status": {
+ "name": "processing_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "processing_started_at": {
+ "name": "processing_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_completed_at": {
+ "name": "processing_completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_error": {
+ "name": "processing_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "doc_kb_id_idx": {
+ "name": "doc_kb_id_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_filename_idx": {
+ "name": "doc_filename_idx",
+ "columns": [
+ {
+ "expression": "filename",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_kb_uploaded_at_idx": {
+ "name": "doc_kb_uploaded_at_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "uploaded_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_processing_status_idx": {
+ "name": "doc_processing_status_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "processing_status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "document_knowledge_base_id_knowledge_base_id_fk": {
+ "name": "document_knowledge_base_id_knowledge_base_id_fk",
+ "tableFrom": "document",
+ "tableTo": "knowledge_base",
+ "columnsFrom": ["knowledge_base_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.embedding": {
+ "name": "embedding",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "document_id": {
+ "name": "document_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chunk_index": {
+ "name": "chunk_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chunk_hash": {
+ "name": "chunk_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_length": {
+ "name": "content_length",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "embedding": {
+ "name": "embedding",
+ "type": "vector(1536)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "embedding_model": {
+ "name": "embedding_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text-embedding-3-small'"
+ },
+ "start_offset": {
+ "name": "start_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_offset": {
+ "name": "end_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "content_tsv": {
+ "name": "content_tsv",
+ "type": "tsvector",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "to_tsvector('english', \"embedding\".\"content\")",
+ "type": "stored"
+ }
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "emb_kb_id_idx": {
+ "name": "emb_kb_id_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_doc_id_idx": {
+ "name": "emb_doc_id_idx",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_doc_chunk_idx": {
+ "name": "emb_doc_chunk_idx",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chunk_index",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_model_idx": {
+ "name": "emb_kb_model_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "embedding_model",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_enabled_idx": {
+ "name": "emb_kb_enabled_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_doc_enabled_idx": {
+ "name": "emb_doc_enabled_idx",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "embedding_vector_hnsw_idx": {
+ "name": "embedding_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "embedding",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "vector_cosine_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "emb_metadata_gin_idx": {
+ "name": "emb_metadata_gin_idx",
+ "columns": [
+ {
+ "expression": "metadata",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "emb_content_fts_idx": {
+ "name": "emb_content_fts_idx",
+ "columns": [
+ {
+ "expression": "content_tsv",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "embedding_knowledge_base_id_knowledge_base_id_fk": {
+ "name": "embedding_knowledge_base_id_knowledge_base_id_fk",
+ "tableFrom": "embedding",
+ "tableTo": "knowledge_base",
+ "columnsFrom": ["knowledge_base_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "embedding_document_id_document_id_fk": {
+ "name": "embedding_document_id_document_id_fk",
+ "tableFrom": "embedding",
+ "tableTo": "document",
+ "columnsFrom": ["document_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "embedding_not_null_check": {
+ "name": "embedding_not_null_check",
+ "value": "\"embedding\" IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.environment": {
+ "name": "environment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "variables": {
+ "name": "variables",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "environment_user_id_user_id_fk": {
+ "name": "environment_user_id_user_id_fk",
+ "tableFrom": "environment",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "environment_user_id_unique": {
+ "name": "environment_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.invitation": {
+ "name": "invitation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "inviter_id": {
+ "name": "inviter_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "invitation_inviter_id_user_id_fk": {
+ "name": "invitation_inviter_id_user_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "user",
+ "columnsFrom": ["inviter_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "invitation_organization_id_organization_id_fk": {
+ "name": "invitation_organization_id_organization_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knowledge_base": {
+ "name": "knowledge_base",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "embedding_model": {
+ "name": "embedding_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text-embedding-3-small'"
+ },
+ "embedding_dimension": {
+ "name": "embedding_dimension",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1536
+ },
+ "chunking_config": {
+ "name": "chunking_config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"maxSize\": 1024, \"minSize\": 100, \"overlap\": 200}'"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kb_user_id_idx": {
+ "name": "kb_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_workspace_id_idx": {
+ "name": "kb_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_user_workspace_idx": {
+ "name": "kb_user_workspace_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_deleted_at_idx": {
+ "name": "kb_deleted_at_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_base_user_id_user_id_fk": {
+ "name": "knowledge_base_user_id_user_id_fk",
+ "tableFrom": "knowledge_base",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_base_workspace_id_workspace_id_fk": {
+ "name": "knowledge_base_workspace_id_workspace_id_fk",
+ "tableFrom": "knowledge_base",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.marketplace": {
+ "name": "marketplace",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state": {
+ "name": "state",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author_id": {
+ "name": "author_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_name": {
+ "name": "author_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "views": {
+ "name": "views",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "category": {
+ "name": "category",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "marketplace_workflow_id_workflow_id_fk": {
+ "name": "marketplace_workflow_id_workflow_id_fk",
+ "tableFrom": "marketplace",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "marketplace_author_id_user_id_fk": {
+ "name": "marketplace_author_id_user_id_fk",
+ "tableFrom": "marketplace",
+ "tableTo": "user",
+ "columnsFrom": ["author_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.member": {
+ "name": "member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "member_user_id_user_id_fk": {
+ "name": "member_user_id_user_id_fk",
+ "tableFrom": "member",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "member_organization_id_organization_id_fk": {
+ "name": "member_organization_id_organization_id_fk",
+ "tableFrom": "member",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.memory": {
+ "name": "memory",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "memory_key_idx": {
+ "name": "memory_key_idx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "memory_workflow_idx": {
+ "name": "memory_workflow_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "memory_workflow_key_idx": {
+ "name": "memory_workflow_key_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "memory_workflow_id_workflow_id_fk": {
+ "name": "memory_workflow_id_workflow_id_fk",
+ "tableFrom": "memory",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organization": {
+ "name": "organization",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "logo": {
+ "name": "logo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.permissions": {
+ "name": "permissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id": {
+ "name": "entity_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_type": {
+ "name": "permission_type",
+ "type": "permission_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "permissions_user_id_idx": {
+ "name": "permissions_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_entity_idx": {
+ "name": "permissions_entity_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_user_entity_type_idx": {
+ "name": "permissions_user_entity_type_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_user_entity_permission_idx": {
+ "name": "permissions_user_entity_permission_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "permission_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_user_entity_idx": {
+ "name": "permissions_user_entity_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_unique_constraint": {
+ "name": "permissions_unique_constraint",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "permissions_user_id_user_id_fk": {
+ "name": "permissions_user_id_user_id_fk",
+ "tableFrom": "permissions",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active_organization_id": {
+ "name": "active_organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "session_active_organization_id_organization_id_fk": {
+ "name": "session_active_organization_id_organization_id_fk",
+ "tableFrom": "session",
+ "tableTo": "organization",
+ "columnsFrom": ["active_organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "session_token_unique": {
+ "name": "session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.settings": {
+ "name": "settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "theme": {
+ "name": "theme",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'system'"
+ },
+ "debug_mode": {
+ "name": "debug_mode",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "auto_connect": {
+ "name": "auto_connect",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "auto_fill_env_vars": {
+ "name": "auto_fill_env_vars",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "telemetry_enabled": {
+ "name": "telemetry_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "telemetry_notified_user": {
+ "name": "telemetry_notified_user",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "email_preferences": {
+ "name": "email_preferences",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "general": {
+ "name": "general",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "settings_user_id_user_id_fk": {
+ "name": "settings_user_id_user_id_fk",
+ "tableFrom": "settings",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "settings_user_id_unique": {
+ "name": "settings_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.subscription": {
+ "name": "subscription",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "plan": {
+ "name": "plan",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stripe_customer_id": {
+ "name": "stripe_customer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stripe_subscription_id": {
+ "name": "stripe_subscription_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "period_start": {
+ "name": "period_start",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "period_end": {
+ "name": "period_end",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cancel_at_period_end": {
+ "name": "cancel_at_period_end",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "seats": {
+ "name": "seats",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trial_start": {
+ "name": "trial_start",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trial_end": {
+ "name": "trial_end",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stripe_customer_id": {
+ "name": "stripe_customer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["email"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_stats": {
+ "name": "user_stats",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_manual_executions": {
+ "name": "total_manual_executions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_api_calls": {
+ "name": "total_api_calls",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_webhook_triggers": {
+ "name": "total_webhook_triggers",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_scheduled_executions": {
+ "name": "total_scheduled_executions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_chat_executions": {
+ "name": "total_chat_executions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_tokens_used": {
+ "name": "total_tokens_used",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_cost": {
+ "name": "total_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "last_active": {
+ "name": "last_active",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_stats_user_id_user_id_fk": {
+ "name": "user_stats_user_id_user_id_fk",
+ "tableFrom": "user_stats",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_stats_user_id_unique": {
+ "name": "user_stats_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verification": {
+ "name": "verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.waitlist": {
+ "name": "waitlist",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "waitlist_email_unique": {
+ "name": "waitlist_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["email"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.webhook": {
+ "name": "webhook",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_config": {
+ "name": "provider_config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "path_idx": {
+ "name": "path_idx",
+ "columns": [
+ {
+ "expression": "path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "webhook_workflow_id_workflow_id_fk": {
+ "name": "webhook_workflow_id_workflow_id_fk",
+ "tableFrom": "webhook",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow": {
+ "name": "workflow",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "folder_id": {
+ "name": "folder_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state": {
+ "name": "state",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'#3972F6'"
+ },
+ "last_synced": {
+ "name": "last_synced",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_deployed": {
+ "name": "is_deployed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "deployed_state": {
+ "name": "deployed_state",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deployed_at": {
+ "name": "deployed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "collaborators": {
+ "name": "collaborators",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "run_count": {
+ "name": "run_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_run_at": {
+ "name": "last_run_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "variables": {
+ "name": "variables",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "is_published": {
+ "name": "is_published",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "marketplace_data": {
+ "name": "marketplace_data",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workflow_user_id_user_id_fk": {
+ "name": "workflow_user_id_user_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_workspace_id_workspace_id_fk": {
+ "name": "workflow_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_folder_id_workflow_folder_id_fk": {
+ "name": "workflow_folder_id_workflow_folder_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "workflow_folder",
+ "columnsFrom": ["folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_blocks": {
+ "name": "workflow_blocks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position_x": {
+ "name": "position_x",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position_y": {
+ "name": "position_y",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "horizontal_handles": {
+ "name": "horizontal_handles",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "is_wide": {
+ "name": "is_wide",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "advanced_mode": {
+ "name": "advanced_mode",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "height": {
+ "name": "height",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "sub_blocks": {
+ "name": "sub_blocks",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "outputs": {
+ "name": "outputs",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "extent": {
+ "name": "extent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_blocks_workflow_id_idx": {
+ "name": "workflow_blocks_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_blocks_parent_id_idx": {
+ "name": "workflow_blocks_parent_id_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_blocks_workflow_parent_idx": {
+ "name": "workflow_blocks_workflow_parent_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_blocks_workflow_type_idx": {
+ "name": "workflow_blocks_workflow_type_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_blocks_workflow_id_workflow_id_fk": {
+ "name": "workflow_blocks_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_blocks",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_edges": {
+ "name": "workflow_edges",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_block_id": {
+ "name": "source_block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_block_id": {
+ "name": "target_block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_handle": {
+ "name": "source_handle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_handle": {
+ "name": "target_handle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_edges_workflow_id_idx": {
+ "name": "workflow_edges_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_edges_source_block_idx": {
+ "name": "workflow_edges_source_block_idx",
+ "columns": [
+ {
+ "expression": "source_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_edges_target_block_idx": {
+ "name": "workflow_edges_target_block_idx",
+ "columns": [
+ {
+ "expression": "target_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_edges_workflow_source_idx": {
+ "name": "workflow_edges_workflow_source_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_edges_workflow_target_idx": {
+ "name": "workflow_edges_workflow_target_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_edges_workflow_id_workflow_id_fk": {
+ "name": "workflow_edges_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_edges",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_edges_source_block_id_workflow_blocks_id_fk": {
+ "name": "workflow_edges_source_block_id_workflow_blocks_id_fk",
+ "tableFrom": "workflow_edges",
+ "tableTo": "workflow_blocks",
+ "columnsFrom": ["source_block_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_edges_target_block_id_workflow_blocks_id_fk": {
+ "name": "workflow_edges_target_block_id_workflow_blocks_id_fk",
+ "tableFrom": "workflow_edges",
+ "tableTo": "workflow_blocks",
+ "columnsFrom": ["target_block_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_folder": {
+ "name": "workflow_folder",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'#6B7280'"
+ },
+ "is_expanded": {
+ "name": "is_expanded",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_folder_user_idx": {
+ "name": "workflow_folder_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_folder_workspace_parent_idx": {
+ "name": "workflow_folder_workspace_parent_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_folder_parent_sort_idx": {
+ "name": "workflow_folder_parent_sort_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_folder_user_id_user_id_fk": {
+ "name": "workflow_folder_user_id_user_id_fk",
+ "tableFrom": "workflow_folder",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_folder_workspace_id_workspace_id_fk": {
+ "name": "workflow_folder_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow_folder",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_logs": {
+ "name": "workflow_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "level": {
+ "name": "level",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "duration": {
+ "name": "duration",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trigger": {
+ "name": "trigger",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workflow_logs_workflow_id_workflow_id_fk": {
+ "name": "workflow_logs_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_logs",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_schedule": {
+ "name": "workflow_schedule",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cron_expression": {
+ "name": "cron_expression",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_run_at": {
+ "name": "next_run_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_ran_at": {
+ "name": "last_ran_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trigger_type": {
+ "name": "trigger_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'UTC'"
+ },
+ "failed_count": {
+ "name": "failed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "last_failed_at": {
+ "name": "last_failed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workflow_schedule_workflow_id_workflow_id_fk": {
+ "name": "workflow_schedule_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_schedule",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "workflow_schedule_workflow_id_unique": {
+ "name": "workflow_schedule_workflow_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["workflow_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_subflows": {
+ "name": "workflow_subflows",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_subflows_workflow_id_idx": {
+ "name": "workflow_subflows_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_subflows_workflow_type_idx": {
+ "name": "workflow_subflows_workflow_type_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_subflows_workflow_id_workflow_id_fk": {
+ "name": "workflow_subflows_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_subflows",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace": {
+ "name": "workspace",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workspace_owner_id_user_id_fk": {
+ "name": "workspace_owner_id_user_id_fk",
+ "tableFrom": "workspace",
+ "tableTo": "user",
+ "columnsFrom": ["owner_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_invitation": {
+ "name": "workspace_invitation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "inviter_id": {
+ "name": "inviter_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permissions": {
+ "name": "permissions",
+ "type": "permission_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'admin'"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workspace_invitation_workspace_id_workspace_id_fk": {
+ "name": "workspace_invitation_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_invitation",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_invitation_inviter_id_user_id_fk": {
+ "name": "workspace_invitation_inviter_id_user_id_fk",
+ "tableFrom": "workspace_invitation",
+ "tableTo": "user",
+ "columnsFrom": ["inviter_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "workspace_invitation_token_unique": {
+ "name": "workspace_invitation_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_member": {
+ "name": "workspace_member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "joined_at": {
+ "name": "joined_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "user_workspace_idx": {
+ "name": "user_workspace_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_member_workspace_id_workspace_id_fk": {
+ "name": "workspace_member_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_member",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_member_user_id_user_id_fk": {
+ "name": "workspace_member_user_id_user_id_fk",
+ "tableFrom": "workspace_member",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.permission_type": {
+ "name": "permission_type",
+ "schema": "public",
+ "values": ["admin", "write", "read"]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/apps/sim/db/migrations/meta/_journal.json b/apps/sim/db/migrations/meta/_journal.json
index 1f03e0039d2..e42b081700f 100644
--- a/apps/sim/db/migrations/meta/_journal.json
+++ b/apps/sim/db/migrations/meta/_journal.json
@@ -323,6 +323,13 @@
"when": 1750527995274,
"tag": "0046_loose_blizzard",
"breakpoints": true
+ },
+ {
+ "idx": 47,
+ "version": "7",
+ "when": 1750794256278,
+ "tag": "0047_new_triathlon",
+ "breakpoints": true
}
]
}
diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts
index 8a5d7404219..a3040709e77 100644
--- a/apps/sim/db/schema.ts
+++ b/apps/sim/db/schema.ts
@@ -154,6 +154,7 @@ export const workflowBlocks = pgTable(
enabled: boolean('enabled').notNull().default(true), // Whether block is active
horizontalHandles: boolean('horizontal_handles').notNull().default(true), // UI layout preference
isWide: boolean('is_wide').notNull().default(false), // Whether block uses wide layout
+ advancedMode: boolean('advanced_mode').notNull().default(false), // Whether block is in advanced mode
height: decimal('height').notNull().default('0'), // Custom height override
// Block data (keeping JSON for flexibility as current system does)
diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts
index a62c34df101..2e0c0dfa858 100644
--- a/apps/sim/hooks/use-collaborative-workflow.ts
+++ b/apps/sim/hooks/use-collaborative-workflow.ts
@@ -1,6 +1,8 @@
import { useCallback, useEffect, useRef } from 'react'
import type { Edge } from 'reactflow'
import { createLogger } from '@/lib/logs/console-logger'
+import { getBlock } from '@/blocks'
+import { resolveOutputType } from '@/blocks/utils'
import { useSocket } from '@/contexts/socket-context'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -58,7 +60,7 @@ export function useCollaborativeWorkflow() {
// Handle incoming workflow operations from other users
useEffect(() => {
const handleWorkflowOperation = (data: any) => {
- const { operation, target, payload, senderId, userId } = data
+ const { operation, target, payload, userId } = data
// Don't apply our own operations
if (isApplyingRemoteChange.current) return
@@ -72,6 +74,8 @@ export function useCollaborativeWorkflow() {
if (target === 'block') {
switch (operation) {
case 'add':
+ // Use normal addBlock - the collaborative system now sends complete data
+ // and the validation schema preserves outputs and subBlocks
workflowStore.addBlock(
payload.id,
payload.type,
@@ -83,6 +87,7 @@ export function useCollaborativeWorkflow() {
)
break
case 'update-position':
+ // Apply immediate position update with smooth interpolation for other users
workflowStore.updateBlockPosition(payload.id, payload.position)
break
case 'update-name':
@@ -97,6 +102,14 @@ export function useCollaborativeWorkflow() {
case 'update-parent':
workflowStore.updateParentId(payload.id, payload.parentId, payload.extent)
break
+ case 'update-wide':
+ workflowStore.setBlockWide(payload.id, payload.isWide)
+ break
+ case 'update-advanced-mode':
+ // Note: toggleBlockAdvancedMode doesn't take a parameter, it just toggles
+ // For now, we'll use the existing toggle method
+ workflowStore.toggleBlockAdvancedMode(payload.id)
+ break
}
} else if (target === 'edge') {
switch (operation) {
@@ -107,6 +120,35 @@ export function useCollaborativeWorkflow() {
workflowStore.removeEdge(payload.id)
break
}
+ } else if (target === 'subflow') {
+ switch (operation) {
+ case 'update':
+ // Handle subflow configuration updates (loop/parallel type changes, etc.)
+ if (payload.type === 'loop') {
+ const { config } = payload
+ if (config.loopType !== undefined) {
+ workflowStore.updateLoopType(payload.id, config.loopType)
+ }
+ if (config.iterations !== undefined) {
+ workflowStore.updateLoopCount(payload.id, config.iterations)
+ }
+ if (config.forEachItems !== undefined) {
+ workflowStore.updateLoopCollection(payload.id, config.forEachItems)
+ }
+ } else if (payload.type === 'parallel') {
+ const { config } = payload
+ if (config.parallelType !== undefined) {
+ workflowStore.updateParallelType(payload.id, config.parallelType)
+ }
+ if (config.count !== undefined) {
+ workflowStore.updateParallelCount(payload.id, config.count)
+ }
+ if (config.distribution !== undefined) {
+ workflowStore.updateParallelCollection(payload.id, config.distribution)
+ }
+ }
+ break
+ }
}
} catch (error) {
logger.error('Error applying remote operation:', error)
@@ -116,7 +158,7 @@ export function useCollaborativeWorkflow() {
}
const handleSubblockUpdate = (data: any) => {
- const { blockId, subblockId, value, senderId, userId } = data
+ const { blockId, subblockId, value, userId } = data
if (isApplyingRemoteChange.current) return
@@ -189,21 +231,83 @@ export function useCollaborativeWorkflow() {
parentId?: string,
extent?: 'parent'
) => {
- // Apply locally first
- workflowStore.addBlock(id, type, name, position, data, parentId, extent)
+ // Create complete block data upfront using the same logic as the store
+ const blockConfig = getBlock(type)
- // Then broadcast to other clients
- if (!isApplyingRemoteChange.current) {
- emitWorkflowOperation('add', 'block', {
+ // Handle loop/parallel blocks that don't use BlockConfig
+ if (!blockConfig && (type === 'loop' || type === 'parallel')) {
+ // For loop/parallel blocks, use empty subBlocks and outputs
+ const completeBlockData = {
id,
type,
name,
position,
- data,
+ data: data || {},
+ subBlocks: {},
+ outputs: {},
+ enabled: true,
+ horizontalHandles: true,
+ isWide: false,
+ height: 0,
parentId,
extent,
+ }
+
+ // Apply locally first
+ workflowStore.addBlock(id, type, name, position, data, parentId, extent)
+
+ // Then broadcast to other clients with complete block data
+ if (!isApplyingRemoteChange.current) {
+ emitWorkflowOperation('add', 'block', completeBlockData)
+ }
+ return
+ }
+
+ if (!blockConfig) {
+ console.error(`Block type ${type} not found`)
+ return
+ }
+
+ // Generate subBlocks and outputs from the block configuration
+ const subBlocks: Record = {}
+
+ // Create subBlocks from the block configuration
+ if (blockConfig.subBlocks) {
+ blockConfig.subBlocks.forEach((subBlock) => {
+ subBlocks[subBlock.id] = {
+ id: subBlock.id,
+ type: subBlock.type,
+ value: null,
+ }
})
}
+
+ // Generate outputs using the same logic as the store
+ const outputs = resolveOutputType(blockConfig.outputs, subBlocks)
+
+ const completeBlockData = {
+ id,
+ type,
+ name,
+ position,
+ data: data || {},
+ subBlocks,
+ outputs,
+ enabled: true,
+ horizontalHandles: true,
+ isWide: false,
+ height: 0, // Default height, will be set by the UI
+ parentId,
+ extent,
+ }
+
+ // Apply locally first
+ workflowStore.addBlock(id, type, name, position, data, parentId, extent)
+
+ // Then broadcast to other clients with complete block data
+ if (!isApplyingRemoteChange.current) {
+ emitWorkflowOperation('add', 'block', completeBlockData)
+ }
},
[workflowStore, emitWorkflowOperation]
)
@@ -242,9 +346,22 @@ export function useCollaborativeWorkflow() {
// Then broadcast to other clients
if (!isApplyingRemoteChange.current) {
emitWorkflowOperation('update-name', 'block', { id, name })
+
+ // Check for pending subblock updates from the store
+ const globalWindow = window as any
+ const pendingUpdates = globalWindow.__pendingSubblockUpdates
+ if (pendingUpdates && Array.isArray(pendingUpdates)) {
+ // Emit collaborative subblock updates for each changed subblock
+ for (const update of pendingUpdates) {
+ const { blockId, subBlockId, newValue } = update
+ emitSubblockUpdate(blockId, subBlockId, newValue)
+ }
+ // Clear the pending updates
+ globalWindow.__pendingSubblockUpdates = undefined
+ }
}
},
- [workflowStore, emitWorkflowOperation]
+ [workflowStore, emitWorkflowOperation, emitSubblockUpdate]
)
const collaborativeToggleBlockEnabled = useCallback(
@@ -273,6 +390,49 @@ export function useCollaborativeWorkflow() {
[workflowStore, emitWorkflowOperation]
)
+ const collaborativeToggleBlockWide = useCallback(
+ (id: string) => {
+ // Get the current state before toggling
+ const currentBlock = workflowStore.blocks[id]
+ if (!currentBlock) return
+
+ // Calculate the new isWide value
+ const newIsWide = !currentBlock.isWide
+
+ // Apply locally first
+ workflowStore.toggleBlockWide(id)
+
+ // Emit with the calculated new value (don't rely on async state update)
+ if (!isApplyingRemoteChange.current) {
+ emitWorkflowOperation('update-wide', 'block', { id, isWide: newIsWide })
+ }
+ },
+ [workflowStore, emitWorkflowOperation]
+ )
+
+ const collaborativeToggleBlockAdvancedMode = useCallback(
+ (id: string) => {
+ // Get the current state before toggling
+ const currentBlock = workflowStore.blocks[id]
+ if (!currentBlock) return
+
+ // Calculate the new advancedMode value
+ const newAdvancedMode = !currentBlock.advancedMode
+
+ // Apply locally first
+ workflowStore.toggleBlockAdvancedMode(id)
+
+ // Emit with the calculated new value (don't rely on async state update)
+ if (!isApplyingRemoteChange.current) {
+ emitWorkflowOperation('update-advanced-mode', 'block', {
+ id,
+ advancedMode: newAdvancedMode,
+ })
+ }
+ },
+ [workflowStore, emitWorkflowOperation]
+ )
+
const collaborativeAddEdge = useCallback(
(edge: Edge) => {
// Apply locally first
@@ -328,33 +488,37 @@ export function useCollaborativeWorkflow() {
// Collaborative loop/parallel configuration updates
const collaborativeUpdateLoopCount = useCallback(
(loopId: string, count: number) => {
- // Apply locally first
+ // Get current state BEFORE making changes
+ const currentBlock = workflowStore.blocks[loopId]
+ if (!currentBlock || currentBlock.type !== 'loop') return
+
+ // Find child nodes before state changes
+ const childNodes = Object.values(workflowStore.blocks)
+ .filter((b) => b.data?.parentId === loopId)
+ .map((b) => b.id)
+
+ // Get current values to preserve them
+ const currentLoopType = currentBlock.data?.loopType || 'for'
+ const currentCollection = currentBlock.data?.collection || ''
+
+ // Apply local change
workflowStore.updateLoopCount(loopId, count)
- // Emit subflow update operation to persist configuration changes
+ // Emit subflow update operation with calculated values
if (!isApplyingRemoteChange.current) {
- // Build the configuration manually to ensure it matches the database structure
- const block = workflowStore.blocks[loopId]
- if (block && block.type === 'loop') {
- // Find child nodes
- const childNodes = Object.values(workflowStore.blocks)
- .filter((b) => b.data?.parentId === loopId)
- .map((b) => b.id)
-
- const config = {
- id: loopId,
- nodes: childNodes,
- iterations: count,
- loopType: block.data?.loopType || 'for',
- forEachItems: block.data?.collection || '',
- }
-
- emitWorkflowOperation('update', 'subflow', {
- id: loopId,
- type: 'loop',
- config,
- })
+ const config = {
+ id: loopId,
+ nodes: childNodes,
+ iterations: count,
+ loopType: currentLoopType,
+ forEachItems: currentCollection,
}
+
+ emitWorkflowOperation('update', 'subflow', {
+ id: loopId,
+ type: 'loop',
+ config,
+ })
}
},
[workflowStore, emitWorkflowOperation]
@@ -362,32 +526,37 @@ export function useCollaborativeWorkflow() {
const collaborativeUpdateLoopType = useCallback(
(loopId: string, loopType: 'for' | 'forEach') => {
- // Apply locally first
+ // Get current state BEFORE making changes
+ const currentBlock = workflowStore.blocks[loopId]
+ if (!currentBlock || currentBlock.type !== 'loop') return
+
+ // Find child nodes before state changes
+ const childNodes = Object.values(workflowStore.blocks)
+ .filter((b) => b.data?.parentId === loopId)
+ .map((b) => b.id)
+
+ // Get current values to preserve them
+ const currentIterations = currentBlock.data?.count || 5
+ const currentCollection = currentBlock.data?.collection || ''
+
+ // Apply local change
workflowStore.updateLoopType(loopId, loopType)
- // Emit subflow update operation to persist configuration changes
+ // Emit subflow update operation with calculated values
if (!isApplyingRemoteChange.current) {
- const block = workflowStore.blocks[loopId]
- if (block && block.type === 'loop') {
- // Find child nodes
- const childNodes = Object.values(workflowStore.blocks)
- .filter((b) => b.data?.parentId === loopId)
- .map((b) => b.id)
-
- const config = {
- id: loopId,
- nodes: childNodes,
- iterations: block.data?.count || 5,
- loopType,
- forEachItems: block.data?.collection || '',
- }
-
- emitWorkflowOperation('update', 'subflow', {
- id: loopId,
- type: 'loop',
- config,
- })
+ const config = {
+ id: loopId,
+ nodes: childNodes,
+ iterations: currentIterations,
+ loopType,
+ forEachItems: currentCollection,
}
+
+ emitWorkflowOperation('update', 'subflow', {
+ id: loopId,
+ type: 'loop',
+ config,
+ })
}
},
[workflowStore, emitWorkflowOperation]
@@ -395,32 +564,37 @@ export function useCollaborativeWorkflow() {
const collaborativeUpdateLoopCollection = useCallback(
(loopId: string, collection: string) => {
- // Apply locally first
+ // Get current state BEFORE making changes
+ const currentBlock = workflowStore.blocks[loopId]
+ if (!currentBlock || currentBlock.type !== 'loop') return
+
+ // Find child nodes before state changes
+ const childNodes = Object.values(workflowStore.blocks)
+ .filter((b) => b.data?.parentId === loopId)
+ .map((b) => b.id)
+
+ // Get current values to preserve them
+ const currentIterations = currentBlock.data?.count || 5
+ const currentLoopType = currentBlock.data?.loopType || 'for'
+
+ // Apply local change
workflowStore.updateLoopCollection(loopId, collection)
- // Emit subflow update operation to persist configuration changes
+ // Emit subflow update operation with calculated values
if (!isApplyingRemoteChange.current) {
- const block = workflowStore.blocks[loopId]
- if (block && block.type === 'loop') {
- // Find child nodes
- const childNodes = Object.values(workflowStore.blocks)
- .filter((b) => b.data?.parentId === loopId)
- .map((b) => b.id)
-
- const config = {
- id: loopId,
- nodes: childNodes,
- iterations: block.data?.count || 5,
- loopType: block.data?.loopType || 'for',
- forEachItems: collection,
- }
-
- emitWorkflowOperation('update', 'subflow', {
- id: loopId,
- type: 'loop',
- config,
- })
+ const config = {
+ id: loopId,
+ nodes: childNodes,
+ iterations: currentIterations,
+ loopType: currentLoopType,
+ forEachItems: collection,
}
+
+ emitWorkflowOperation('update', 'subflow', {
+ id: loopId,
+ type: 'loop',
+ config,
+ })
}
},
[workflowStore, emitWorkflowOperation]
@@ -428,32 +602,37 @@ export function useCollaborativeWorkflow() {
const collaborativeUpdateParallelCount = useCallback(
(parallelId: string, count: number) => {
- // Apply locally first
+ // Get current state BEFORE making changes
+ const currentBlock = workflowStore.blocks[parallelId]
+ if (!currentBlock || currentBlock.type !== 'parallel') return
+
+ // Find child nodes before state changes
+ const childNodes = Object.values(workflowStore.blocks)
+ .filter((b) => b.data?.parentId === parallelId)
+ .map((b) => b.id)
+
+ // Get current values to preserve them
+ const currentDistribution = currentBlock.data?.collection || ''
+ const currentParallelType = currentBlock.data?.parallelType || 'collection'
+
+ // Apply local change
workflowStore.updateParallelCount(parallelId, count)
- // Emit subflow update operation to persist configuration changes
+ // Emit subflow update operation with calculated values
if (!isApplyingRemoteChange.current) {
- const block = workflowStore.blocks[parallelId]
- if (block && block.type === 'parallel') {
- // Find child nodes
- const childNodes = Object.values(workflowStore.blocks)
- .filter((b) => b.data?.parentId === parallelId)
- .map((b) => b.id)
-
- const config = {
- id: parallelId,
- nodes: childNodes,
- count: Math.max(1, Math.min(20, count)), // Clamp between 1-20
- distribution: block.data?.collection || '',
- parallelType: block.data?.parallelType || 'collection',
- }
-
- emitWorkflowOperation('update', 'subflow', {
- id: parallelId,
- type: 'parallel',
- config,
- })
+ const config = {
+ id: parallelId,
+ nodes: childNodes,
+ count: Math.max(1, Math.min(20, count)), // Clamp between 1-20
+ distribution: currentDistribution,
+ parallelType: currentParallelType,
}
+
+ emitWorkflowOperation('update', 'subflow', {
+ id: parallelId,
+ type: 'parallel',
+ config,
+ })
}
},
[workflowStore, emitWorkflowOperation]
@@ -461,26 +640,37 @@ export function useCollaborativeWorkflow() {
const collaborativeUpdateParallelCollection = useCallback(
(parallelId: string, collection: string) => {
- // Apply locally first
+ // Get current state BEFORE making changes
+ const currentBlock = workflowStore.blocks[parallelId]
+ if (!currentBlock || currentBlock.type !== 'parallel') return
+
+ // Find child nodes before state changes
+ const childNodes = Object.values(workflowStore.blocks)
+ .filter((b) => b.data?.parentId === parallelId)
+ .map((b) => b.id)
+
+ // Get current values to preserve them
+ const currentCount = currentBlock.data?.count || 5
+ const currentParallelType = currentBlock.data?.parallelType || 'collection'
+
+ // Apply local change
workflowStore.updateParallelCollection(parallelId, collection)
- // Emit subflow update operation to persist configuration changes
+ // Emit subflow update operation with calculated values
if (!isApplyingRemoteChange.current) {
- const parallels = workflowStore.parallels
- const config = parallels[parallelId]
-
- if (config) {
- const block = workflowStore.blocks[parallelId]
- emitWorkflowOperation('update', 'subflow', {
- id: parallelId,
- type: 'parallel',
- config: {
- ...config,
- distribution: collection, // Ensure the new collection is included
- parallelType: block?.data?.parallelType || 'collection', // Include parallelType
- },
- })
+ const config = {
+ id: parallelId,
+ nodes: childNodes,
+ count: currentCount,
+ distribution: collection,
+ parallelType: currentParallelType,
}
+
+ emitWorkflowOperation('update', 'subflow', {
+ id: parallelId,
+ type: 'parallel',
+ config,
+ })
}
},
[workflowStore, emitWorkflowOperation]
@@ -488,32 +678,48 @@ export function useCollaborativeWorkflow() {
const collaborativeUpdateParallelType = useCallback(
(parallelId: string, parallelType: 'count' | 'collection') => {
- // Apply locally first
+ // Get current state BEFORE making changes
+ const currentBlock = workflowStore.blocks[parallelId]
+ if (!currentBlock || currentBlock.type !== 'parallel') return
+
+ // Find child nodes before state changes
+ const childNodes = Object.values(workflowStore.blocks)
+ .filter((b) => b.data?.parentId === parallelId)
+ .map((b) => b.id)
+
+ // Calculate new values based on type change
+ let newCount = currentBlock.data?.count || 5
+ let newDistribution = currentBlock.data?.collection || ''
+
+ // Reset values based on type (same logic as the UI)
+ if (parallelType === 'count') {
+ newDistribution = ''
+ // Keep existing count
+ } else {
+ newCount = 1
+ newDistribution = newDistribution || ''
+ }
+
+ // Apply all changes locally first
workflowStore.updateParallelType(parallelId, parallelType)
+ workflowStore.updateParallelCount(parallelId, newCount)
+ workflowStore.updateParallelCollection(parallelId, newDistribution)
- // Emit subflow update operation to persist configuration changes
+ // Emit single subflow update with all changes
if (!isApplyingRemoteChange.current) {
- const block = workflowStore.blocks[parallelId]
- if (block && block.type === 'parallel') {
- // Find child nodes
- const childNodes = Object.values(workflowStore.blocks)
- .filter((b) => b.data?.parentId === parallelId)
- .map((b) => b.id)
-
- const config = {
- id: parallelId,
- nodes: childNodes,
- count: block.data?.count || 5,
- distribution: block.data?.collection || '',
- parallelType,
- }
-
- emitWorkflowOperation('update', 'subflow', {
- id: parallelId,
- type: 'parallel',
- config,
- })
+ const config = {
+ id: parallelId,
+ nodes: childNodes,
+ count: newCount,
+ distribution: newDistribution,
+ parallelType,
}
+
+ emitWorkflowOperation('update', 'subflow', {
+ id: parallelId,
+ type: 'parallel',
+ config,
+ })
}
},
[workflowStore, emitWorkflowOperation]
@@ -536,6 +742,8 @@ export function useCollaborativeWorkflow() {
collaborativeRemoveBlock,
collaborativeToggleBlockEnabled,
collaborativeUpdateParentId,
+ collaborativeToggleBlockWide,
+ collaborativeToggleBlockAdvancedMode,
collaborativeAddEdge,
collaborativeRemoveEdge,
collaborativeSetSubblockValue,
diff --git a/apps/sim/hooks/use-workspace-permissions.ts b/apps/sim/hooks/use-workspace-permissions.ts
index fb5ec21ba67..c4b4941ea60 100644
--- a/apps/sim/hooks/use-workspace-permissions.ts
+++ b/apps/sim/hooks/use-workspace-permissions.ts
@@ -5,7 +5,6 @@ import { API_ENDPOINTS } from '@/stores/constants'
const logger = createLogger('useWorkspacePermissions')
-// Use the enum from the database schema for type safety
export type PermissionType = (typeof permissionTypeEnum.enumValues)[number]
export interface WorkspaceUser {
diff --git a/apps/sim/lib/logs/trace-spans.ts b/apps/sim/lib/logs/trace-spans.ts
index eed9418a59f..02ef418f954 100644
--- a/apps/sim/lib/logs/trace-spans.ts
+++ b/apps/sim/lib/logs/trace-spans.ts
@@ -1,4 +1,4 @@
-import type { TraceSpan } from '@/app/w/logs/stores/types'
+import type { TraceSpan } from '@/app/workspace/[workspaceId]/logs/stores/types'
import type { ExecutionResult } from '@/executor/types'
// Helper function to build a tree of trace spans from execution logs
diff --git a/apps/sim/lib/workflows/db-helpers.ts b/apps/sim/lib/workflows/db-helpers.ts
index a48454e81a6..f4c61e7cddd 100644
--- a/apps/sim/lib/workflows/db-helpers.ts
+++ b/apps/sim/lib/workflows/db-helpers.ts
@@ -60,6 +60,7 @@ export async function loadWorkflowFromNormalizedTables(
enabled: block.enabled,
horizontalHandles: block.horizontalHandles,
isWide: block.isWide,
+ advancedMode: block.advancedMode,
height: Number(block.height),
subBlocks: block.subBlocks || {},
outputs: block.outputs || {},
diff --git a/apps/sim/middleware.ts b/apps/sim/middleware.ts
index 6e958c91d16..286620c31b1 100644
--- a/apps/sim/middleware.ts
+++ b/apps/sim/middleware.ts
@@ -44,13 +44,24 @@ export async function middleware(request: NextRequest) {
return NextResponse.rewrite(new URL(`/chat/${subdomain}${url.pathname}`, request.url))
}
- // Check if the path is exactly /w
- if (url.pathname === '/w') {
- return NextResponse.redirect(new URL('/w/1', request.url))
+ // Legacy redirect: /w -> /workspace (will be handled by workspace layout)
+ if (url.pathname === '/w' || url.pathname.startsWith('/w/')) {
+ // Extract workflow ID if present
+ const pathParts = url.pathname.split('/')
+ if (pathParts.length >= 3 && pathParts[1] === 'w') {
+ const workflowId = pathParts[2]
+ // Redirect old workflow URLs to new format
+ // We'll need to resolve the workspace ID for this workflow
+ return NextResponse.redirect(
+ new URL(`/workspace?redirect_workflow=${workflowId}`, request.url)
+ )
+ }
+ // Simple /w redirect to workspace root
+ return NextResponse.redirect(new URL('/workspace', request.url))
}
// Handle protected routes that require authentication
- if (url.pathname.startsWith('/w/') || url.pathname === '/w') {
+ if (url.pathname.startsWith('/workspace')) {
if (!hasActiveSession) {
return NextResponse.redirect(new URL('/login', request.url))
}
@@ -137,8 +148,9 @@ export async function middleware(request: NextRequest) {
// Update matcher to include invitation routes
export const config = {
matcher: [
- '/w', // Match exactly /w
- '/w/:path*', // Match protected routes
+ '/w', // Legacy /w redirect
+ '/w/:path*', // Legacy /w/* redirects
+ '/workspace/:path*', // New workspace routes
'/login',
'/signup',
'/invite/:path*', // Match invitation routes
diff --git a/apps/sim/socket-server/config/socket.ts b/apps/sim/socket-server/config/socket.ts
new file mode 100644
index 00000000000..0ec9f326132
--- /dev/null
+++ b/apps/sim/socket-server/config/socket.ts
@@ -0,0 +1,64 @@
+import type { Server as HttpServer } from 'http'
+import { Server } from 'socket.io'
+import { createLogger } from '../../lib/logs/console-logger'
+
+const logger = createLogger('SocketIOConfig')
+
+/**
+ * Get allowed origins for Socket.IO CORS configuration
+ */
+function getAllowedOrigins(): string[] {
+ const allowedOrigins = [
+ process.env.NEXT_PUBLIC_APP_URL,
+ process.env.VERCEL_URL,
+ 'http://localhost:3000',
+ 'http://localhost:3001',
+ ...(process.env.ALLOWED_ORIGINS?.split(',') || []),
+ ].filter((url): url is string => Boolean(url))
+
+ logger.info('Socket.IO CORS configuration:', { allowedOrigins })
+
+ return allowedOrigins
+}
+
+/**
+ * Create and configure a Socket.IO server instance
+ * @param httpServer - The HTTP server instance to attach Socket.IO to
+ * @returns Configured Socket.IO server instance
+ */
+export function createSocketIOServer(httpServer: HttpServer): Server {
+ const allowedOrigins = getAllowedOrigins()
+
+ const io = new Server(httpServer, {
+ cors: {
+ origin: allowedOrigins,
+ methods: ['GET', 'POST', 'OPTIONS'],
+ allowedHeaders: ['Content-Type', 'Authorization', 'Cookie', 'socket.io'],
+ credentials: true, // Enable credentials to accept cookies
+ },
+ transports: ['polling', 'websocket'], // Keep both transports for reliability
+ allowEIO3: true, // Keep legacy support for compatibility
+ pingTimeout: 60000, // Back to original conservative setting
+ pingInterval: 25000, // Back to original interval
+ maxHttpBufferSize: 1e6,
+ cookie: {
+ name: 'io',
+ path: '/',
+ httpOnly: true,
+ sameSite: 'none', // Required for cross-origin cookies
+ secure: process.env.NODE_ENV === 'production', // HTTPS in production
+ },
+ })
+
+ logger.info('Socket.IO server configured with:', {
+ allowedOrigins: allowedOrigins.length,
+ transports: ['polling', 'websocket'],
+ pingTimeout: 60000,
+ pingInterval: 25000,
+ maxHttpBufferSize: 1e6,
+ cookieSecure: process.env.NODE_ENV === 'production',
+ corsCredentials: true,
+ })
+
+ return io
+}
diff --git a/apps/sim/socket-server/database/operations.ts b/apps/sim/socket-server/database/operations.ts
new file mode 100644
index 00000000000..056fc915ffb
--- /dev/null
+++ b/apps/sim/socket-server/database/operations.ts
@@ -0,0 +1,641 @@
+import { and, eq, or } from 'drizzle-orm'
+import { db } from '../../db'
+import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '../../db/schema'
+import { createLogger } from '../../lib/logs/console-logger'
+import { loadWorkflowFromNormalizedTables } from '../../lib/workflows/db-helpers'
+
+const logger = createLogger('SocketDatabase')
+
+// Constants
+const DEFAULT_LOOP_ITERATIONS = 5
+
+// Enum for subflow types
+enum SubflowType {
+ LOOP = 'loop',
+ PARALLEL = 'parallel',
+}
+
+// Helper function to check if a block type is a subflow type
+function isSubflowBlockType(blockType: string): blockType is SubflowType {
+ return Object.values(SubflowType).includes(blockType as SubflowType)
+}
+
+// Helper function to update subflow node lists when child blocks are added/removed
+export async function updateSubflowNodeList(dbOrTx: any, workflowId: string, parentId: string) {
+ try {
+ // Get all child blocks of this parent
+ const childBlocks = await dbOrTx
+ .select({ id: workflowBlocks.id })
+ .from(workflowBlocks)
+ .where(and(eq(workflowBlocks.workflowId, workflowId), eq(workflowBlocks.parentId, parentId)))
+
+ const childNodeIds = childBlocks.map((block: any) => block.id)
+
+ // Get current subflow config
+ const subflowData = await dbOrTx
+ .select({ config: workflowSubflows.config })
+ .from(workflowSubflows)
+ .where(and(eq(workflowSubflows.id, parentId), eq(workflowSubflows.workflowId, workflowId)))
+ .limit(1)
+
+ if (subflowData.length > 0) {
+ const updatedConfig = {
+ ...subflowData[0].config,
+ nodes: childNodeIds,
+ }
+
+ await dbOrTx
+ .update(workflowSubflows)
+ .set({
+ config: updatedConfig,
+ updatedAt: new Date(),
+ })
+ .where(and(eq(workflowSubflows.id, parentId), eq(workflowSubflows.workflowId, workflowId)))
+
+ logger.debug(`Updated subflow ${parentId} node list: [${childNodeIds.join(', ')}]`)
+ }
+ } catch (error) {
+ logger.error(`Error updating subflow node list for ${parentId}:`, error)
+ }
+}
+
+// Get workflow state
+export async function getWorkflowState(workflowId: string) {
+ try {
+ const workflowData = await db
+ .select()
+ .from(workflow)
+ .where(eq(workflow.id, workflowId))
+ .limit(1)
+
+ if (!workflowData.length) {
+ throw new Error(`Workflow ${workflowId} not found`)
+ }
+
+ // Load from normalized tables first (same logic as REST API)
+ const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
+
+ if (normalizedData) {
+ // Use normalized data as source of truth
+ const existingState = workflowData[0].state || {}
+
+ const finalState = {
+ // Default values for expected properties
+ deploymentStatuses: {},
+ hasActiveSchedule: false,
+ hasActiveWebhook: false,
+ // Preserve any existing state properties
+ ...existingState,
+ // Override with normalized data (this takes precedence)
+ blocks: normalizedData.blocks,
+ edges: normalizedData.edges,
+ loops: normalizedData.loops,
+ parallels: normalizedData.parallels,
+ lastSaved: Date.now(),
+ isDeployed: workflowData[0].isDeployed || false,
+ deployedAt: workflowData[0].deployedAt,
+ }
+
+ return {
+ ...workflowData[0],
+ state: finalState,
+ lastModified: Date.now(),
+ }
+ }
+ // Fallback to JSON blob
+ return {
+ ...workflowData[0],
+ lastModified: Date.now(),
+ }
+ } catch (error) {
+ logger.error(`Error fetching workflow state for ${workflowId}:`, error)
+ throw error
+ }
+}
+
+// Persist workflow operation
+export async function persistWorkflowOperation(workflowId: string, operation: any) {
+ try {
+ const { operation: op, target, payload, timestamp, userId } = operation
+
+ await db.transaction(async (tx) => {
+ // Update the workflow's last modified timestamp first
+ await tx
+ .update(workflow)
+ .set({ updatedAt: new Date(timestamp) })
+ .where(eq(workflow.id, workflowId))
+
+ // Handle different operation types within the transaction
+ switch (target) {
+ case 'block':
+ await handleBlockOperationTx(tx, workflowId, op, payload, userId)
+ break
+ case 'edge':
+ await handleEdgeOperationTx(tx, workflowId, op, payload, userId)
+ break
+ case 'subflow':
+ await handleSubflowOperationTx(tx, workflowId, op, payload, userId)
+ break
+ default:
+ throw new Error(`Unknown operation target: ${target}`)
+ }
+ })
+ } catch (error) {
+ logger.error(
+ `❌ Error persisting workflow operation (${operation.operation} on ${operation.target}):`,
+ error
+ )
+ throw error
+ }
+}
+
+// Block operations
+async function handleBlockOperationTx(
+ tx: any,
+ workflowId: string,
+ operation: string,
+ payload: any,
+ userId: string
+) {
+ switch (operation) {
+ case 'add': {
+ // Validate required fields for add operation
+ if (!payload.id || !payload.type || !payload.name || !payload.position) {
+ throw new Error('Missing required fields for add block operation')
+ }
+
+ logger.debug(`[SERVER] Adding block: ${payload.type} (${payload.id})`, {
+ isSubflowType: isSubflowBlockType(payload.type),
+ payload,
+ })
+
+ // Extract parentId and extent from payload.data if they exist there, otherwise from payload directly
+ const parentId = payload.parentId || payload.data?.parentId || null
+ const extent = payload.extent || payload.data?.extent || null
+
+ logger.debug(`[SERVER] Block parent info:`, {
+ blockId: payload.id,
+ hasParent: !!parentId,
+ parentId,
+ extent,
+ payloadParentId: payload.parentId,
+ dataParentId: payload.data?.parentId,
+ })
+
+ try {
+ const insertData = {
+ id: payload.id,
+ workflowId,
+ type: payload.type,
+ name: payload.name,
+ positionX: payload.position.x,
+ positionY: payload.position.y,
+ data: payload.data || {},
+ subBlocks: payload.subBlocks || {},
+ outputs: payload.outputs || {},
+ parentId,
+ extent,
+ enabled: payload.enabled ?? true,
+ horizontalHandles: payload.horizontalHandles ?? true,
+ isWide: payload.isWide ?? false,
+ height: payload.height || 0,
+ }
+
+ await tx.insert(workflowBlocks).values(insertData)
+ } catch (insertError) {
+ logger.error(`[SERVER] ❌ Failed to insert block ${payload.id}:`, insertError)
+ throw insertError
+ }
+
+ // Auto-create subflow entry for loop/parallel blocks
+ if (isSubflowBlockType(payload.type)) {
+ try {
+ const subflowConfig =
+ payload.type === SubflowType.LOOP
+ ? {
+ id: payload.id,
+ nodes: [], // Empty initially, will be populated when child blocks are added
+ iterations: payload.data?.count || DEFAULT_LOOP_ITERATIONS,
+ loopType: payload.data?.loopType || 'for',
+ forEachItems: payload.data?.collection || '',
+ }
+ : {
+ id: payload.id,
+ nodes: [], // Empty initially, will be populated when child blocks are added
+ distribution: payload.data?.collection || '',
+ }
+
+ logger.debug(
+ `[SERVER] Auto-creating ${payload.type} subflow ${payload.id}:`,
+ subflowConfig
+ )
+
+ await tx.insert(workflowSubflows).values({
+ id: payload.id,
+ workflowId,
+ type: payload.type,
+ config: subflowConfig,
+ })
+ } catch (subflowError) {
+ logger.error(
+ `[SERVER] ❌ Failed to create ${payload.type} subflow ${payload.id}:`,
+ subflowError
+ )
+ throw subflowError
+ }
+ }
+
+ // If this block has a parent, update the parent's subflow node list
+ if (parentId) {
+ await updateSubflowNodeList(tx, workflowId, parentId)
+ }
+
+ logger.debug(`Added block ${payload.id} (${payload.type}) to workflow ${workflowId}`)
+ break
+ }
+
+ case 'update-position': {
+ if (!payload.id || !payload.position) {
+ throw new Error('Missing required fields for update position operation')
+ }
+
+ const updateResult = await tx
+ .update(workflowBlocks)
+ .set({
+ positionX: payload.position.x,
+ positionY: payload.position.y,
+ updatedAt: new Date(),
+ })
+ .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
+ .returning({ id: workflowBlocks.id })
+
+ if (updateResult.length === 0) {
+ throw new Error(`Block ${payload.id} not found in workflow ${workflowId}`)
+ }
+ break
+ }
+
+ case 'remove': {
+ if (!payload.id) {
+ throw new Error('Missing block ID for remove operation')
+ }
+
+ // Check if this is a subflow block that needs cascade deletion
+ const blockToRemove = await tx
+ .select({ type: workflowBlocks.type, parentId: workflowBlocks.parentId })
+ .from(workflowBlocks)
+ .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
+ .limit(1)
+
+ if (blockToRemove.length > 0 && isSubflowBlockType(blockToRemove[0].type)) {
+ // Cascade delete: Remove all child blocks first
+ const childBlocks = await tx
+ .select({ id: workflowBlocks.id, type: workflowBlocks.type })
+ .from(workflowBlocks)
+ .where(
+ and(eq(workflowBlocks.workflowId, workflowId), eq(workflowBlocks.parentId, payload.id))
+ )
+
+ logger.debug(
+ `[SERVER] Starting cascade deletion for subflow block ${payload.id} (type: ${blockToRemove[0].type})`
+ )
+ logger.debug(
+ `[SERVER] Found ${childBlocks.length} child blocks to delete: [${childBlocks.map((b: any) => `${b.id} (${b.type})`).join(', ')}]`
+ )
+
+ // Remove edges connected to child blocks
+ for (const childBlock of childBlocks) {
+ await tx
+ .delete(workflowEdges)
+ .where(
+ and(
+ eq(workflowEdges.workflowId, workflowId),
+ or(
+ eq(workflowEdges.sourceBlockId, childBlock.id),
+ eq(workflowEdges.targetBlockId, childBlock.id)
+ )
+ )
+ )
+ }
+
+ // Remove child blocks from database
+ await tx
+ .delete(workflowBlocks)
+ .where(
+ and(eq(workflowBlocks.workflowId, workflowId), eq(workflowBlocks.parentId, payload.id))
+ )
+
+ // Remove the subflow entry
+ await tx
+ .delete(workflowSubflows)
+ .where(
+ and(eq(workflowSubflows.id, payload.id), eq(workflowSubflows.workflowId, workflowId))
+ )
+ }
+
+ // Remove any edges connected to this block
+ await tx
+ .delete(workflowEdges)
+ .where(
+ and(
+ eq(workflowEdges.workflowId, workflowId),
+ or(
+ eq(workflowEdges.sourceBlockId, payload.id),
+ eq(workflowEdges.targetBlockId, payload.id)
+ )
+ )
+ )
+
+ // Finally remove the block itself
+ await tx
+ .delete(workflowBlocks)
+ .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
+
+ // If this block had a parent, update the parent's subflow node list
+ if (blockToRemove.length > 0 && blockToRemove[0].parentId) {
+ await updateSubflowNodeList(tx, workflowId, blockToRemove[0].parentId)
+ }
+
+ logger.debug(`Removed block ${payload.id} and its connections from workflow ${workflowId}`)
+ break
+ }
+
+ case 'update-name': {
+ if (!payload.id || !payload.name) {
+ throw new Error('Missing required fields for update name operation')
+ }
+
+ const updateResult = await tx
+ .update(workflowBlocks)
+ .set({
+ name: payload.name,
+ updatedAt: new Date(),
+ })
+ .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
+ .returning({ id: workflowBlocks.id })
+
+ if (updateResult.length === 0) {
+ throw new Error(`Block ${payload.id} not found in workflow ${workflowId}`)
+ }
+
+ logger.debug(`Updated block name: ${payload.id} -> "${payload.name}"`)
+ break
+ }
+
+ case 'toggle-enabled': {
+ if (!payload.id) {
+ throw new Error('Missing block ID for toggle enabled operation')
+ }
+
+ // Get current enabled state
+ const currentBlock = await tx
+ .select({ enabled: workflowBlocks.enabled })
+ .from(workflowBlocks)
+ .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
+ .limit(1)
+
+ if (currentBlock.length === 0) {
+ throw new Error(`Block ${payload.id} not found in workflow ${workflowId}`)
+ }
+
+ const newEnabledState = !currentBlock[0].enabled
+
+ await tx
+ .update(workflowBlocks)
+ .set({
+ enabled: newEnabledState,
+ updatedAt: new Date(),
+ })
+ .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
+
+ logger.debug(`Toggled block enabled: ${payload.id} -> ${newEnabledState}`)
+ break
+ }
+
+ case 'update-parent': {
+ if (!payload.id) {
+ throw new Error('Missing block ID for update parent operation')
+ }
+
+ const updateResult = await tx
+ .update(workflowBlocks)
+ .set({
+ parentId: payload.parentId || null,
+ extent: payload.extent || null,
+ updatedAt: new Date(),
+ })
+ .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
+ .returning({ id: workflowBlocks.id })
+
+ if (updateResult.length === 0) {
+ throw new Error(`Block ${payload.id} not found in workflow ${workflowId}`)
+ }
+
+ // If the block now has a parent, update the parent's subflow node list
+ if (payload.parentId) {
+ await updateSubflowNodeList(tx, workflowId, payload.parentId)
+ }
+
+ logger.debug(
+ `Updated block parent: ${payload.id} -> parent: ${payload.parentId}, extent: ${payload.extent}`
+ )
+ break
+ }
+
+ case 'update-wide': {
+ if (!payload.id || payload.isWide === undefined) {
+ throw new Error('Missing required fields for update wide operation')
+ }
+
+ const updateResult = await tx
+ .update(workflowBlocks)
+ .set({
+ isWide: payload.isWide,
+ updatedAt: new Date(),
+ })
+ .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
+ .returning({ id: workflowBlocks.id })
+
+ if (updateResult.length === 0) {
+ throw new Error(`Block ${payload.id} not found in workflow ${workflowId}`)
+ }
+
+ logger.debug(`Updated block wide state: ${payload.id} -> ${payload.isWide}`)
+ break
+ }
+
+ case 'update-advanced-mode': {
+ if (!payload.id || payload.advancedMode === undefined) {
+ throw new Error('Missing required fields for update advanced mode operation')
+ }
+
+ const updateResult = await tx
+ .update(workflowBlocks)
+ .set({
+ advancedMode: payload.advancedMode,
+ updatedAt: new Date(),
+ })
+ .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
+ .returning({ id: workflowBlocks.id })
+
+ if (updateResult.length === 0) {
+ throw new Error(`Block ${payload.id} not found in workflow ${workflowId}`)
+ }
+
+ logger.debug(`Updated block advanced mode: ${payload.id} -> ${payload.advancedMode}`)
+ break
+ }
+
+ // Add other block operations as needed
+ default:
+ logger.warn(`Unknown block operation: ${operation}`)
+ throw new Error(`Unsupported block operation: ${operation}`)
+ }
+}
+
+// Edge operations
+async function handleEdgeOperationTx(
+ tx: any,
+ workflowId: string,
+ operation: string,
+ payload: any,
+ userId: string
+) {
+ switch (operation) {
+ case 'add': {
+ // Validate required fields
+ if (!payload.id || !payload.source || !payload.target) {
+ throw new Error('Missing required fields for add edge operation')
+ }
+
+ await tx.insert(workflowEdges).values({
+ id: payload.id,
+ workflowId,
+ sourceBlockId: payload.source,
+ targetBlockId: payload.target,
+ sourceHandle: payload.sourceHandle || null,
+ targetHandle: payload.targetHandle || null,
+ })
+
+ logger.debug(`Added edge ${payload.id}: ${payload.source} -> ${payload.target}`)
+ break
+ }
+
+ case 'remove': {
+ if (!payload.id) {
+ throw new Error('Missing edge ID for remove operation')
+ }
+
+ const deleteResult = await tx
+ .delete(workflowEdges)
+ .where(and(eq(workflowEdges.id, payload.id), eq(workflowEdges.workflowId, workflowId)))
+ .returning({ id: workflowEdges.id })
+
+ if (deleteResult.length === 0) {
+ throw new Error(`Edge ${payload.id} not found in workflow ${workflowId}`)
+ }
+
+ logger.debug(`Removed edge ${payload.id} from workflow ${workflowId}`)
+ break
+ }
+
+ default:
+ logger.warn(`Unknown edge operation: ${operation}`)
+ throw new Error(`Unsupported edge operation: ${operation}`)
+ }
+}
+
+// Subflow operations
+async function handleSubflowOperationTx(
+ tx: any,
+ workflowId: string,
+ operation: string,
+ payload: any,
+ userId: string
+) {
+ switch (operation) {
+ case 'update': {
+ if (!payload.id || !payload.config) {
+ throw new Error('Missing required fields for update subflow operation')
+ }
+
+ logger.debug(`[SERVER] Updating subflow ${payload.id} with config:`, payload.config)
+
+ // Update the subflow configuration
+ const updateResult = await tx
+ .update(workflowSubflows)
+ .set({
+ config: payload.config,
+ updatedAt: new Date(),
+ })
+ .where(
+ and(eq(workflowSubflows.id, payload.id), eq(workflowSubflows.workflowId, workflowId))
+ )
+ .returning({ id: workflowSubflows.id })
+
+ if (updateResult.length === 0) {
+ throw new Error(`Subflow ${payload.id} not found in workflow ${workflowId}`)
+ }
+
+ logger.debug(`[SERVER] Successfully updated subflow ${payload.id} in database`)
+
+ // Also update the corresponding block's data to keep UI in sync
+ if (payload.type === 'loop' && payload.config.iterations !== undefined) {
+ // Update the loop block's data.count property
+ await tx
+ .update(workflowBlocks)
+ .set({
+ data: {
+ ...payload.config,
+ count: payload.config.iterations,
+ loopType: payload.config.loopType,
+ collection: payload.config.forEachItems,
+ width: 500,
+ height: 300,
+ type: 'loopNode',
+ },
+ updatedAt: new Date(),
+ })
+ .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
+ } else if (payload.type === 'parallel') {
+ // Update the parallel block's data properties
+ const blockData = {
+ ...payload.config,
+ width: 500,
+ height: 300,
+ type: 'parallelNode',
+ }
+
+ // Include count if provided
+ if (payload.config.count !== undefined) {
+ blockData.count = payload.config.count
+ }
+
+ // Include collection if provided
+ if (payload.config.distribution !== undefined) {
+ blockData.collection = payload.config.distribution
+ }
+
+ // Include parallelType if provided
+ if (payload.config.parallelType !== undefined) {
+ blockData.parallelType = payload.config.parallelType
+ }
+
+ await tx
+ .update(workflowBlocks)
+ .set({
+ data: blockData,
+ updatedAt: new Date(),
+ })
+ .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
+ }
+
+ break
+ }
+
+ // Add other subflow operations as needed
+ default:
+ logger.warn(`Unknown subflow operation: ${operation}`)
+ throw new Error(`Unsupported subflow operation: ${operation}`)
+ }
+}
diff --git a/apps/sim/socket-server/handlers/connection.ts b/apps/sim/socket-server/handlers/connection.ts
new file mode 100644
index 00000000000..b66511f7d9c
--- /dev/null
+++ b/apps/sim/socket-server/handlers/connection.ts
@@ -0,0 +1,42 @@
+import { createLogger } from '../../lib/logs/console-logger'
+import type { AuthenticatedSocket } from '../middleware/auth'
+import type { RoomManager } from '../rooms/manager'
+import type { HandlerDependencies } from './workflow'
+
+const logger = createLogger('ConnectionHandlers')
+
+export function setupConnectionHandlers(
+ socket: AuthenticatedSocket,
+ deps: HandlerDependencies | RoomManager
+) {
+ const roomManager =
+ deps instanceof Object && 'roomManager' in deps ? deps.roomManager : (deps as RoomManager)
+
+ socket.on('error', (error) => {
+ logger.error(`Socket ${socket.id} error:`, error)
+ })
+
+ socket.conn.on('error', (error) => {
+ logger.error(`Socket ${socket.id} connection error:`, error)
+ })
+
+ socket.on('disconnect', (reason) => {
+ const workflowId = roomManager.getWorkflowIdForSocket(socket.id)
+ const session = roomManager.getUserSession(socket.id)
+
+ logger.info(`Socket ${socket.id} disconnected: ${reason}`)
+
+ if (workflowId && session) {
+ roomManager.cleanupUserFromRoom(socket.id, workflowId)
+
+ // Broadcast updated presence list to all remaining users
+ roomManager.broadcastPresenceUpdate(workflowId)
+
+ logger.info(
+ `User ${session.userId} (${session.userName}) disconnected from workflow ${workflowId} - reason: ${reason}`
+ )
+ }
+
+ roomManager.clearPendingOperations(socket.id)
+ })
+}
diff --git a/apps/sim/socket-server/handlers/index.ts b/apps/sim/socket-server/handlers/index.ts
new file mode 100644
index 00000000000..cf8a2f46136
--- /dev/null
+++ b/apps/sim/socket-server/handlers/index.ts
@@ -0,0 +1,30 @@
+import type { AuthenticatedSocket } from '../middleware/auth'
+import type { RoomManager, UserPresence, WorkflowRoom } from '../rooms/manager'
+import { setupConnectionHandlers } from './connection'
+import { setupOperationsHandlers } from './operations'
+import { setupPresenceHandlers } from './presence'
+import { setupSubblocksHandlers } from './subblocks'
+import { setupWorkflowHandlers } from './workflow'
+
+export type { UserPresence, WorkflowRoom }
+
+/**
+ * Sets up all socket event handlers for an authenticated socket connection
+ * @param socket - The authenticated socket instance
+ * @param roomManager - Room manager instance for state management
+ */
+export function setupAllHandlers(socket: AuthenticatedSocket, roomManager: RoomManager) {
+ setupWorkflowHandlers(socket, roomManager)
+ setupOperationsHandlers(socket, roomManager)
+ setupSubblocksHandlers(socket, roomManager)
+ setupPresenceHandlers(socket, roomManager)
+ setupConnectionHandlers(socket, roomManager)
+}
+
+export {
+ setupWorkflowHandlers,
+ setupOperationsHandlers,
+ setupSubblocksHandlers,
+ setupPresenceHandlers,
+ setupConnectionHandlers,
+}
diff --git a/apps/sim/socket-server/handlers/operations.ts b/apps/sim/socket-server/handlers/operations.ts
new file mode 100644
index 00000000000..b56bd827668
--- /dev/null
+++ b/apps/sim/socket-server/handlers/operations.ts
@@ -0,0 +1,169 @@
+import { ZodError } from 'zod'
+import { createLogger } from '../../lib/logs/console-logger'
+import { persistWorkflowOperation } from '../database/operations'
+import type { AuthenticatedSocket } from '../middleware/auth'
+import { verifyOperationPermission } from '../middleware/permissions'
+import type { RoomManager } from '../rooms/manager'
+import { WorkflowOperationSchema } from '../validation/schemas'
+import type { HandlerDependencies } from './workflow'
+
+const logger = createLogger('OperationsHandlers')
+
+// Simplified conflict resolution - just last-write-wins since we have normalized tables
+function shouldAcceptOperation(operation: any, roomLastModified: number): boolean {
+ // Accept all operations - with normalized tables, conflicts are very unlikely
+ return true
+}
+
+export function setupOperationsHandlers(
+ socket: AuthenticatedSocket,
+ deps: HandlerDependencies | RoomManager
+) {
+ const roomManager =
+ deps instanceof Object && 'roomManager' in deps ? deps.roomManager : (deps as RoomManager)
+ socket.on('workflow-operation', async (data) => {
+ const workflowId = roomManager.getWorkflowIdForSocket(socket.id)
+ const session = roomManager.getUserSession(socket.id)
+
+ if (!workflowId || !session) {
+ socket.emit('error', {
+ type: 'NOT_JOINED',
+ message: 'Not joined to any workflow',
+ })
+ return
+ }
+
+ const room = roomManager.getWorkflowRoom(workflowId)
+ if (!room) {
+ socket.emit('error', {
+ type: 'ROOM_NOT_FOUND',
+ message: 'Workflow room not found',
+ })
+ return
+ }
+
+ try {
+ const validatedOperation = WorkflowOperationSchema.parse(data)
+ const { operation, target, payload, timestamp } = validatedOperation
+
+ if (!shouldAcceptOperation(validatedOperation, room.lastModified)) {
+ socket.emit('operation-rejected', {
+ type: 'OPERATION_REJECTED',
+ message: 'Operation rejected',
+ operation,
+ target,
+ serverTimestamp: Date.now(),
+ })
+ return
+ }
+
+ // Check operation permissions
+ const permissionCheck = await verifyOperationPermission(
+ session.userId,
+ workflowId,
+ operation,
+ target
+ )
+ if (!permissionCheck.allowed) {
+ logger.warn(
+ `User ${session.userId} forbidden from ${operation} on ${target}: ${permissionCheck.reason}`
+ )
+ socket.emit('operation-forbidden', {
+ type: 'INSUFFICIENT_PERMISSIONS',
+ message: permissionCheck.reason || 'Insufficient permissions for this operation',
+ operation,
+ target,
+ })
+ return
+ }
+
+ const userPresence = room.users.get(socket.id)
+ if (userPresence) {
+ userPresence.lastActivity = Date.now()
+ }
+
+ // Persist to database with transaction (last-write-wins)
+ const serverTimestamp = Date.now()
+ await persistWorkflowOperation(workflowId, {
+ operation,
+ target,
+ payload,
+ timestamp: serverTimestamp,
+ userId: session.userId,
+ })
+
+ room.lastModified = serverTimestamp
+
+ const broadcastData = {
+ operation,
+ target,
+ payload,
+ timestamp: serverTimestamp,
+ senderId: socket.id,
+ userId: session.userId,
+ userName: session.userName,
+ // Add operation metadata for better client handling
+ metadata: {
+ workflowId,
+ operationId: crypto.randomUUID(),
+ },
+ }
+
+ socket.to(workflowId).emit('workflow-operation', broadcastData)
+
+ socket.emit('operation-confirmed', {
+ operation,
+ target,
+ operationId: broadcastData.metadata.operationId,
+ serverTimestamp,
+ })
+ } catch (error) {
+ if (error instanceof ZodError) {
+ socket.emit('operation-error', {
+ type: 'VALIDATION_ERROR',
+ message: 'Invalid operation data',
+ errors: error.errors,
+ operation: data.operation,
+ target: data.target,
+ })
+ logger.warn(`Validation error for operation from ${session.userId}:`, error.errors)
+ } else if (error instanceof Error) {
+ // Handle specific database errors
+ if (error.message.includes('not found')) {
+ socket.emit('operation-error', {
+ type: 'RESOURCE_NOT_FOUND',
+ message: error.message,
+ operation: data.operation,
+ target: data.target,
+ })
+ } else if (error.message.includes('duplicate') || error.message.includes('unique')) {
+ socket.emit('operation-error', {
+ type: 'DUPLICATE_RESOURCE',
+ message: 'Resource already exists',
+ operation: data.operation,
+ target: data.target,
+ })
+ } else {
+ socket.emit('operation-error', {
+ type: 'OPERATION_FAILED',
+ message: error.message,
+ operation: data.operation,
+ target: data.target,
+ })
+ }
+ logger.error(
+ `Operation error for ${session.userId} (${data.operation} on ${data.target}):`,
+ error
+ )
+ } else {
+ socket.emit('operation-error', {
+ type: 'UNKNOWN_ERROR',
+ message: 'An unknown error occurred',
+ operation: data.operation,
+ target: data.target,
+ })
+ logger.error('Unknown error handling workflow operation:', error)
+ }
+ }
+ })
+}
diff --git a/apps/sim/socket-server/handlers/presence.ts b/apps/sim/socket-server/handlers/presence.ts
new file mode 100644
index 00000000000..0596a2aced1
--- /dev/null
+++ b/apps/sim/socket-server/handlers/presence.ts
@@ -0,0 +1,60 @@
+import { createLogger } from '../../lib/logs/console-logger'
+import type { AuthenticatedSocket } from '../middleware/auth'
+import type { RoomManager } from '../rooms/manager'
+import type { HandlerDependencies } from './workflow'
+
+const logger = createLogger('PresenceHandlers')
+
+export function setupPresenceHandlers(
+ socket: AuthenticatedSocket,
+ deps: HandlerDependencies | RoomManager
+) {
+ const roomManager =
+ deps instanceof Object && 'roomManager' in deps ? deps.roomManager : (deps as RoomManager)
+ socket.on('cursor-update', ({ cursor }) => {
+ const workflowId = roomManager.getWorkflowIdForSocket(socket.id)
+ const session = roomManager.getUserSession(socket.id)
+
+ if (!workflowId || !session) return
+
+ const room = roomManager.getWorkflowRoom(workflowId)
+ if (!room) return
+
+ const userPresence = room.users.get(socket.id)
+ if (userPresence) {
+ userPresence.cursor = cursor
+ userPresence.lastActivity = Date.now()
+ }
+
+ socket.to(workflowId).emit('cursor-update', {
+ socketId: socket.id,
+ userId: session.userId,
+ userName: session.userName,
+ cursor,
+ })
+ })
+
+ // Handle user selection (for showing what block/element a user has selected)
+ socket.on('selection-update', ({ selection }) => {
+ const workflowId = roomManager.getWorkflowIdForSocket(socket.id)
+ const session = roomManager.getUserSession(socket.id)
+
+ if (!workflowId || !session) return
+
+ const room = roomManager.getWorkflowRoom(workflowId)
+ if (!room) return
+
+ const userPresence = room.users.get(socket.id)
+ if (userPresence) {
+ userPresence.selection = selection
+ userPresence.lastActivity = Date.now()
+ }
+
+ socket.to(workflowId).emit('selection-update', {
+ socketId: socket.id,
+ userId: session.userId,
+ userName: session.userName,
+ selection,
+ })
+ })
+}
diff --git a/apps/sim/socket-server/handlers/subblocks.ts b/apps/sim/socket-server/handlers/subblocks.ts
new file mode 100644
index 00000000000..bf060409c8f
--- /dev/null
+++ b/apps/sim/socket-server/handlers/subblocks.ts
@@ -0,0 +1,134 @@
+import { and, eq } from 'drizzle-orm'
+import { db } from '../../db'
+import { workflow, workflowBlocks } from '../../db/schema'
+import { createLogger } from '../../lib/logs/console-logger'
+import type { AuthenticatedSocket } from '../middleware/auth'
+import type { RoomManager } from '../rooms/manager'
+import type { HandlerDependencies } from './workflow'
+
+const logger = createLogger('SubblocksHandlers')
+
+export function setupSubblocksHandlers(
+ socket: AuthenticatedSocket,
+ deps: HandlerDependencies | RoomManager
+) {
+ const roomManager =
+ deps instanceof Object && 'roomManager' in deps ? deps.roomManager : (deps as RoomManager)
+ socket.on('subblock-update', async (data) => {
+ const workflowId = roomManager.getWorkflowIdForSocket(socket.id)
+ const session = roomManager.getUserSession(socket.id)
+
+ if (!workflowId || !session) {
+ logger.debug(`Ignoring subblock update: socket not connected to any workflow room`, {
+ socketId: socket.id,
+ hasWorkflowId: !!workflowId,
+ hasSession: !!session,
+ })
+ return
+ }
+
+ const { blockId, subblockId, value, timestamp } = data
+ const room = roomManager.getWorkflowRoom(workflowId)
+
+ if (!room) {
+ logger.debug(`Ignoring subblock update: workflow room not found`, {
+ socketId: socket.id,
+ workflowId,
+ blockId,
+ subblockId,
+ })
+ return
+ }
+
+ try {
+ const userPresence = room.users.get(socket.id)
+ if (userPresence) {
+ userPresence.lastActivity = Date.now()
+ }
+
+ // First, verify that the workflow still exists in the database
+ const workflowExists = await db
+ .select({ id: workflow.id })
+ .from(workflow)
+ .where(eq(workflow.id, workflowId))
+ .limit(1)
+
+ if (workflowExists.length === 0) {
+ logger.warn(`Ignoring subblock update: workflow ${workflowId} no longer exists`, {
+ socketId: socket.id,
+ blockId,
+ subblockId,
+ })
+ roomManager.cleanupUserFromRoom(socket.id, workflowId)
+ return
+ }
+
+ let updateSuccessful = false
+ await db.transaction(async (tx) => {
+ const [block] = await tx
+ .select({ subBlocks: workflowBlocks.subBlocks })
+ .from(workflowBlocks)
+ .where(and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId)))
+ .limit(1)
+
+ if (!block) {
+ // Block was deleted - this is a normal race condition in collaborative editing
+ logger.debug(
+ `Ignoring subblock update for deleted block: ${workflowId}/${blockId}.${subblockId}`
+ )
+ return
+ }
+
+ const subBlocks = (block.subBlocks as any) || {}
+
+ if (!subBlocks[subblockId]) {
+ // Create new subblock with minimal structure
+ subBlocks[subblockId] = {
+ id: subblockId,
+ type: 'unknown', // Will be corrected by next collaborative update
+ value: value,
+ }
+ } else {
+ // Preserve existing id and type, only update value
+ subBlocks[subblockId] = {
+ ...subBlocks[subblockId],
+ value: value,
+ }
+ }
+
+ await tx
+ .update(workflowBlocks)
+ .set({
+ subBlocks: subBlocks,
+ updatedAt: new Date(),
+ })
+ .where(and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId)))
+
+ updateSuccessful = true
+ })
+
+ // Only broadcast to other clients if the update was successful
+ if (updateSuccessful) {
+ socket.to(workflowId).emit('subblock-update', {
+ blockId,
+ subblockId,
+ value,
+ timestamp,
+ senderId: socket.id,
+ userId: session.userId,
+ })
+ }
+
+ logger.debug(`Subblock update in workflow ${workflowId}: ${blockId}.${subblockId}`)
+ } catch (error) {
+ logger.error('Error handling subblock update:', error)
+
+ socket.emit('operation-error', {
+ type: 'SUBBLOCK_UPDATE_FAILED',
+ message: `Failed to update subblock ${blockId}.${subblockId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
+ operation: 'subblock-update',
+ target: 'subblock',
+ })
+ }
+ })
+}
diff --git a/apps/sim/socket-server/handlers/workflow.ts b/apps/sim/socket-server/handlers/workflow.ts
new file mode 100644
index 00000000000..0fba11ddba8
--- /dev/null
+++ b/apps/sim/socket-server/handlers/workflow.ts
@@ -0,0 +1,149 @@
+import { createLogger } from '../../lib/logs/console-logger'
+import { getWorkflowState } from '../database/operations'
+import type { AuthenticatedSocket } from '../middleware/auth'
+import { verifyWorkflowAccess } from '../middleware/permissions'
+import type { RoomManager, UserPresence, WorkflowRoom } from '../rooms/manager'
+
+const logger = createLogger('WorkflowHandlers')
+
+export type { UserPresence, WorkflowRoom }
+
+export interface HandlerDependencies {
+ roomManager: RoomManager
+}
+
+export const createWorkflowRoom = (workflowId: string): WorkflowRoom => ({
+ workflowId,
+ users: new Map(),
+ lastModified: Date.now(),
+ activeConnections: 0,
+})
+
+export const cleanupUserFromRoom = (
+ socketId: string,
+ workflowId: string,
+ roomManager: RoomManager
+) => {
+ roomManager.cleanupUserFromRoom(socketId, workflowId)
+}
+
+export function setupWorkflowHandlers(
+ socket: AuthenticatedSocket,
+ deps: HandlerDependencies | RoomManager
+) {
+ const roomManager =
+ deps instanceof Object && 'roomManager' in deps ? deps.roomManager : (deps as RoomManager)
+ socket.on('join-workflow', async ({ workflowId }) => {
+ try {
+ const userId = socket.userId
+ const userName = socket.userName
+
+ if (!userId || !userName) {
+ logger.warn(`Join workflow rejected: Socket ${socket.id} not authenticated`)
+ socket.emit('join-workflow-error', { error: 'Authentication required' })
+ return
+ }
+
+ logger.info(`Join workflow request from ${userId} (${userName}) for workflow ${workflowId}`)
+
+ try {
+ const accessInfo = await verifyWorkflowAccess(userId, workflowId)
+ if (!accessInfo.hasAccess) {
+ logger.warn(`User ${userId} (${userName}) denied access to workflow ${workflowId}`)
+ socket.emit('join-workflow-error', { error: 'Access denied to workflow' })
+ return
+ }
+ } catch (error) {
+ logger.warn(`Error verifying workflow access for ${userId}:`, error)
+ socket.emit('join-workflow-error', { error: 'Failed to verify workflow access' })
+ return
+ }
+
+ // Ensure user only joins one workflow at a time
+ const currentWorkflowId = roomManager.getWorkflowIdForSocket(socket.id)
+ if (currentWorkflowId) {
+ socket.leave(currentWorkflowId)
+ roomManager.cleanupUserFromRoom(socket.id, currentWorkflowId)
+
+ // Broadcast updated presence list to all remaining users
+ roomManager.broadcastPresenceUpdate(currentWorkflowId)
+ }
+
+ socket.join(workflowId)
+
+ if (!roomManager.hasWorkflowRoom(workflowId)) {
+ roomManager.setWorkflowRoom(workflowId, roomManager.createWorkflowRoom(workflowId))
+ }
+
+ const room = roomManager.getWorkflowRoom(workflowId)!
+ room.activeConnections++
+
+ const userPresence: UserPresence = {
+ userId,
+ workflowId,
+ userName,
+ socketId: socket.id,
+ joinedAt: Date.now(),
+ lastActivity: Date.now(),
+ }
+
+ room.users.set(socket.id, userPresence)
+ roomManager.setWorkflowForSocket(socket.id, workflowId)
+ roomManager.setUserSession(socket.id, { userId, userName })
+
+ const workflowState = await getWorkflowState(workflowId)
+ socket.emit('workflow-state', workflowState)
+
+ // Send complete presence list to all users in the room (including the new user)
+ roomManager.broadcastPresenceUpdate(workflowId)
+
+ logger.info(
+ `User ${userId} (${userName}) joined workflow ${workflowId}. Room now has ${room.activeConnections} users.`
+ )
+ } catch (error) {
+ logger.error('Error joining workflow:', error)
+ socket.emit('error', {
+ type: 'JOIN_ERROR',
+ message: 'Failed to join workflow',
+ })
+ }
+ })
+
+ socket.on('request-sync', async ({ workflowId }) => {
+ try {
+ if (!socket.userId) {
+ socket.emit('error', { type: 'NOT_AUTHENTICATED', message: 'Not authenticated' })
+ return
+ }
+
+ const accessInfo = await verifyWorkflowAccess(socket.userId, workflowId)
+ if (!accessInfo.hasAccess) {
+ socket.emit('error', { type: 'ACCESS_DENIED', message: 'Access denied' })
+ return
+ }
+
+ const workflowState = await getWorkflowState(workflowId)
+ socket.emit('workflow-state', workflowState)
+
+ logger.info(`Sent sync data to ${socket.userId} for workflow ${workflowId}`)
+ } catch (error) {
+ logger.error('Error handling sync request:', error)
+ socket.emit('error', { type: 'SYNC_FAILED', message: 'Failed to sync workflow state' })
+ }
+ })
+
+ socket.on('leave-workflow', () => {
+ const workflowId = roomManager.getWorkflowIdForSocket(socket.id)
+ const session = roomManager.getUserSession(socket.id)
+
+ if (workflowId && session) {
+ socket.leave(workflowId)
+ roomManager.cleanupUserFromRoom(socket.id, workflowId)
+
+ // Broadcast updated presence list to all remaining users
+ roomManager.broadcastPresenceUpdate(workflowId)
+
+ logger.info(`User ${session.userId} (${session.userName}) left workflow ${workflowId}`)
+ }
+ })
+}
diff --git a/apps/sim/socket-server/index.test.ts b/apps/sim/socket-server/index.test.ts
new file mode 100644
index 00000000000..e09c6f1b90f
--- /dev/null
+++ b/apps/sim/socket-server/index.test.ts
@@ -0,0 +1,316 @@
+/**
+ * Tests for the socket server index.ts
+ *
+ * @vitest-environment node
+ */
+import { createServer } from 'http'
+import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
+import { createLogger } from '../lib/logs/console-logger'
+import { createSocketIOServer } from './config/socket'
+import { RoomManager } from './rooms/manager'
+import { createHttpHandler } from './routes/http'
+
+vi.mock('../lib/auth', () => ({
+ auth: {
+ api: {
+ verifyOneTimeToken: vi.fn(),
+ },
+ },
+}))
+
+vi.mock('../db', () => ({
+ db: {
+ select: vi.fn(),
+ insert: vi.fn(),
+ update: vi.fn(),
+ delete: vi.fn(),
+ transaction: vi.fn(),
+ },
+}))
+
+vi.mock('./middleware/auth', () => ({
+ authenticateSocket: vi.fn((socket, next) => {
+ socket.userId = 'test-user-id'
+ socket.userName = 'Test User'
+ socket.userEmail = 'test@example.com'
+ next()
+ }),
+}))
+
+vi.mock('./middleware/permissions', () => ({
+ verifyWorkflowAccess: vi.fn().mockResolvedValue({
+ hasAccess: true,
+ role: 'owner',
+ }),
+ verifyOperationPermission: vi.fn().mockResolvedValue({
+ allowed: true,
+ }),
+}))
+
+vi.mock('./database/operations', () => ({
+ getWorkflowState: vi.fn().mockResolvedValue({
+ id: 'test-workflow',
+ name: 'Test Workflow',
+ lastModified: Date.now(),
+ }),
+ persistWorkflowOperation: vi.fn().mockResolvedValue(undefined),
+}))
+
+describe('Socket Server Index Integration', () => {
+ let httpServer: any
+ let io: any
+ let roomManager: RoomManager
+ let logger: any
+ let PORT: number
+
+ beforeAll(() => {
+ logger = createLogger('SocketServerTest')
+ })
+
+ beforeEach(async () => {
+ // Use a random port for each test to avoid conflicts
+ PORT = 3333 + Math.floor(Math.random() * 1000)
+
+ // Create HTTP server
+ httpServer = createServer()
+
+ // Create Socket.IO server using extracted config
+ io = createSocketIOServer(httpServer)
+
+ // Initialize room manager after io is created
+ roomManager = new RoomManager(io)
+
+ // Configure HTTP request handler
+ const httpHandler = createHttpHandler(roomManager, logger)
+ httpServer.on('request', httpHandler)
+
+ // Start server
+ await new Promise((resolve) => {
+ httpServer.listen(PORT, '0.0.0.0', () => {
+ resolve()
+ })
+ })
+ })
+
+ afterEach(async () => {
+ if (io) {
+ io.close()
+ }
+ if (httpServer) {
+ httpServer.close()
+ }
+ vi.clearAllMocks()
+ })
+
+ describe('HTTP Server Configuration', () => {
+ it('should create HTTP server successfully', () => {
+ expect(httpServer).toBeDefined()
+ expect(httpServer.listening).toBe(true)
+ })
+
+ it('should handle health check endpoint', async () => {
+ try {
+ const response = await fetch(`http://localhost:${PORT}/health`)
+ expect(response.status).toBe(200)
+
+ const data = await response.json()
+ expect(data).toHaveProperty('status', 'ok')
+ expect(data).toHaveProperty('timestamp')
+ expect(data).toHaveProperty('connections')
+ } catch (error) {
+ // Skip this test if fetch fails (likely due to test environment)
+ console.warn('Health check test skipped due to fetch error:', error)
+ }
+ })
+ })
+
+ describe('Socket.IO Server Configuration', () => {
+ it('should create Socket.IO server with proper configuration', () => {
+ expect(io).toBeDefined()
+ expect(io.engine).toBeDefined()
+ })
+
+ it('should have proper CORS configuration', () => {
+ const corsOptions = io.engine.opts.cors
+ expect(corsOptions).toBeDefined()
+ expect(corsOptions.methods).toContain('GET')
+ expect(corsOptions.methods).toContain('POST')
+ expect(corsOptions.credentials).toBe(true)
+ })
+
+ it('should have proper transport configuration', () => {
+ const transports = io.engine.opts.transports
+ expect(transports).toContain('polling')
+ expect(transports).toContain('websocket')
+ })
+ })
+
+ describe('Room Manager Integration', () => {
+ it('should create room manager successfully', () => {
+ expect(roomManager).toBeDefined()
+ expect(roomManager.getTotalActiveConnections()).toBe(0)
+ })
+
+ it('should create workflow rooms', () => {
+ const workflowId = 'test-workflow-123'
+ const room = roomManager.createWorkflowRoom(workflowId)
+ roomManager.setWorkflowRoom(workflowId, room)
+
+ expect(roomManager.hasWorkflowRoom(workflowId)).toBe(true)
+ const retrievedRoom = roomManager.getWorkflowRoom(workflowId)
+ expect(retrievedRoom).toBeDefined()
+ expect(retrievedRoom?.workflowId).toBe(workflowId)
+ })
+
+ it('should manage user sessions', () => {
+ const socketId = 'test-socket-123'
+ const workflowId = 'test-workflow-456'
+ const session = { userId: 'user-123', userName: 'Test User' }
+
+ roomManager.setWorkflowForSocket(socketId, workflowId)
+ roomManager.setUserSession(socketId, session)
+
+ expect(roomManager.getWorkflowIdForSocket(socketId)).toBe(workflowId)
+ expect(roomManager.getUserSession(socketId)).toEqual(session)
+ })
+
+ it('should clean up rooms properly', () => {
+ const workflowId = 'test-workflow-789'
+ const socketId = 'test-socket-789'
+
+ const room = roomManager.createWorkflowRoom(workflowId)
+ roomManager.setWorkflowRoom(workflowId, room)
+
+ // Add user to room
+ room.users.set(socketId, {
+ userId: 'user-789',
+ workflowId,
+ userName: 'Test User',
+ socketId,
+ joinedAt: Date.now(),
+ lastActivity: Date.now(),
+ })
+ room.activeConnections = 1
+
+ roomManager.setWorkflowForSocket(socketId, workflowId)
+
+ // Clean up user
+ roomManager.cleanupUserFromRoom(socketId, workflowId)
+
+ expect(roomManager.hasWorkflowRoom(workflowId)).toBe(false)
+ expect(roomManager.getWorkflowIdForSocket(socketId)).toBeUndefined()
+ })
+ })
+
+ describe('Module Integration', () => {
+ it.concurrent('should properly import all extracted modules', async () => {
+ // Test that all modules can be imported without errors
+ const { createSocketIOServer } = await import('./config/socket')
+ const { createHttpHandler } = await import('./routes/http')
+ const { RoomManager } = await import('./rooms/manager')
+ const { authenticateSocket } = await import('./middleware/auth')
+ const { verifyWorkflowAccess } = await import('./middleware/permissions')
+ const { getWorkflowState } = await import('./database/operations')
+ const { WorkflowOperationSchema } = await import('./validation/schemas')
+
+ expect(createSocketIOServer).toBeTypeOf('function')
+ expect(createHttpHandler).toBeTypeOf('function')
+ expect(RoomManager).toBeTypeOf('function')
+ expect(authenticateSocket).toBeTypeOf('function')
+ expect(verifyWorkflowAccess).toBeTypeOf('function')
+ expect(getWorkflowState).toBeTypeOf('function')
+ expect(WorkflowOperationSchema).toBeDefined()
+ })
+
+ it.concurrent('should maintain all original functionality after refactoring', () => {
+ // Verify that the main components are properly instantiated
+ expect(httpServer).toBeDefined()
+ expect(io).toBeDefined()
+ expect(roomManager).toBeDefined()
+
+ // Verify core methods exist and are callable
+ expect(typeof roomManager.createWorkflowRoom).toBe('function')
+ expect(typeof roomManager.cleanupUserFromRoom).toBe('function')
+ expect(typeof roomManager.handleWorkflowDeletion).toBe('function')
+ expect(typeof roomManager.validateWorkflowConsistency).toBe('function')
+ })
+ })
+
+ describe('Error Handling', () => {
+ it('should have global error handlers configured', () => {
+ expect(typeof process.on).toBe('function')
+ })
+
+ it('should handle server setup', () => {
+ expect(httpServer).toBeDefined()
+ expect(io).toBeDefined()
+ })
+ })
+
+ describe('Authentication Middleware', () => {
+ it('should apply authentication middleware to Socket.IO', () => {
+ expect(io._parser).toBeDefined()
+ })
+ })
+
+ describe('Graceful Shutdown', () => {
+ it('should have shutdown capability', () => {
+ expect(typeof httpServer.close).toBe('function')
+ expect(typeof io.close).toBe('function')
+ })
+ })
+
+ describe('Validation and Utils', () => {
+ it.concurrent('should validate workflow operations', async () => {
+ const { WorkflowOperationSchema } = await import('./validation/schemas')
+
+ const validOperation = {
+ operation: 'add',
+ target: 'block',
+ payload: {
+ id: 'test-block',
+ type: 'action',
+ name: 'Test Block',
+ position: { x: 100, y: 200 },
+ },
+ timestamp: Date.now(),
+ }
+
+ expect(() => WorkflowOperationSchema.parse(validOperation)).not.toThrow()
+ })
+
+ it.concurrent('should validate edge operations', async () => {
+ const { WorkflowOperationSchema } = await import('./validation/schemas')
+
+ const validEdgeOperation = {
+ operation: 'add',
+ target: 'edge',
+ payload: {
+ id: 'test-edge',
+ source: 'block-1',
+ target: 'block-2',
+ },
+ timestamp: Date.now(),
+ }
+
+ expect(() => WorkflowOperationSchema.parse(validEdgeOperation)).not.toThrow()
+ })
+
+ it.concurrent('should validate subflow operations', async () => {
+ const { WorkflowOperationSchema } = await import('./validation/schemas')
+
+ const validSubflowOperation = {
+ operation: 'update',
+ target: 'subflow',
+ payload: {
+ id: 'test-subflow',
+ type: 'loop',
+ config: { iterations: 5 },
+ },
+ timestamp: Date.now(),
+ }
+
+ expect(() => WorkflowOperationSchema.parse(validSubflowOperation)).not.toThrow()
+ })
+ })
+})
diff --git a/apps/sim/socket-server/index.ts b/apps/sim/socket-server/index.ts
index f91e709b2e7..be9588aadb5 100644
--- a/apps/sim/socket-server/index.ts
+++ b/apps/sim/socket-server/index.ts
@@ -1,1245 +1,26 @@
import { createServer } from 'http'
-import { Server, type Socket } from 'socket.io'
-
-// Extend Socket interface to include user data
-interface AuthenticatedSocket extends Socket {
- userId?: string
- userName?: string
- userEmail?: string
- activeOrganizationId?: string
-}
-
-import { and, eq, isNull, or } from 'drizzle-orm'
-import { z } from 'zod'
-import { db } from '../db'
-import {
- workflow,
- workflowBlocks,
- workflowEdges,
- workflowSubflows,
- workspaceMember,
-} from '../db/schema'
-import { auth } from '../lib/auth'
import { createLogger } from '../lib/logs/console-logger'
+import { createSocketIOServer } from './config/socket'
+import { setupAllHandlers } from './handlers'
+import { type AuthenticatedSocket, authenticateSocket } from './middleware/auth'
+import { RoomManager } from './rooms/manager'
+import { createHttpHandler } from './routes/http'
const logger = createLogger('CollaborativeSocketServer')
-// Enhanced server configuration
-const httpServer = createServer((req, res) => {
- // Handle health check for Railway
- if (req.method === 'GET' && req.url === '/health') {
- res.writeHead(200, { 'Content-Type': 'application/json' })
- res.end(
- JSON.stringify({
- status: 'ok',
- timestamp: new Date().toISOString(),
- connections: Array.from(workflowRooms.values()).reduce(
- (total, room) => total + room.activeConnections,
- 0
- ),
- })
- )
- return
- }
-
- // Handle workflow deletion notifications from the main API
- if (req.method === 'POST' && req.url === '/api/workflow-deleted') {
- let body = ''
- req.on('data', (chunk) => {
- body += chunk.toString()
- })
- req.on('end', () => {
- try {
- const { workflowId } = JSON.parse(body)
- handleWorkflowDeletion(workflowId)
- res.writeHead(200, { 'Content-Type': 'application/json' })
- res.end(JSON.stringify({ success: true }))
- } catch (error) {
- logger.error('Error handling workflow deletion notification:', error)
- res.writeHead(500, { 'Content-Type': 'application/json' })
- res.end(JSON.stringify({ error: 'Failed to process deletion notification' }))
- }
- })
- return
- }
-
- // Default response for other requests
- res.writeHead(404, { 'Content-Type': 'application/json' })
- res.end(JSON.stringify({ error: 'Not found' }))
-})
-
-// Configure allowed origins
-const allowedOrigins = [
- process.env.NEXT_PUBLIC_APP_URL,
- process.env.VERCEL_URL,
- 'http://localhost:3000',
- 'http://localhost:3001',
- ...(process.env.ALLOWED_ORIGINS?.split(',') || []),
-].filter((url): url is string => Boolean(url))
-
-logger.info('Socket.IO CORS configuration:', { allowedOrigins })
-
-const io = new Server(httpServer, {
- cors: {
- origin: allowedOrigins,
- methods: ['GET', 'POST', 'OPTIONS'],
- allowedHeaders: ['Content-Type', 'Authorization', 'Cookie', 'socket.io'],
- credentials: true, // Enable credentials to accept cookies
- },
- transports: ['polling', 'websocket'],
- allowEIO3: true,
- pingTimeout: 60000,
- pingInterval: 25000,
- maxHttpBufferSize: 1e6,
- cookie: {
- name: 'io',
- path: '/',
- httpOnly: true,
- sameSite: 'none', // Required for cross-origin cookies
- secure: process.env.NODE_ENV === 'production', // HTTPS in production
- },
-})
-
-// Enhanced connection and presence tracking
-interface UserPresence {
- userId: string
- workflowId: string
- userName: string
- socketId: string
- joinedAt: number
- lastActivity: number
- cursor?: { x: number; y: number }
- selection?: { type: 'block' | 'edge' | 'none'; id?: string }
-}
-
-interface WorkflowRoom {
- workflowId: string
- users: Map // socketId -> UserPresence
- lastModified: number
- activeConnections: number
-}
-
-// Global state management
-const workflowRooms = new Map() // workflowId -> WorkflowRoom
-const socketToWorkflow = new Map() // socketId -> workflowId
-const userSessions = new Map() // socketId -> session
-
-// Enhanced database operation queue for batching and performance
-const pendingDbOperations = new Map() // workflowId -> operations[]
-const batchTimeouts = new Map() // workflowId -> timeout
-const BATCH_DELAY = 100 // ms - batch operations within 100ms window
-const MAX_BATCH_SIZE = 50 // Maximum operations per batch
-
-// Validation schemas for workflow operations
-const PositionSchema = z.object({
- x: z.number(),
- y: z.number(),
-})
-
-const BlockOperationSchema = z.object({
- operation: z.enum([
- 'add',
- 'remove',
- 'update-position',
- 'update-name',
- 'toggle-enabled',
- 'update-parent',
- 'duplicate',
- ]),
- target: z.literal('block'),
- payload: z.object({
- id: z.string(),
- type: z.string().optional(),
- name: z.string().optional(),
- position: PositionSchema.optional(),
- data: z.record(z.any()).optional(),
- parentId: z.string().optional(),
- extent: z.enum(['parent']).optional(),
- enabled: z.boolean().optional(),
- }),
- timestamp: z.number(),
-})
-
-const EdgeOperationSchema = z.object({
- operation: z.enum(['add', 'remove']),
- target: z.literal('edge'),
- payload: z.object({
- id: z.string(),
- source: z.string().optional(),
- target: z.string().optional(),
- sourceHandle: z.string().nullable().optional(),
- targetHandle: z.string().nullable().optional(),
- }),
- timestamp: z.number(),
-})
-
-// Constants
-const DEFAULT_LOOP_ITERATIONS = 5
-
-// Enum for subflow types
-enum SubflowType {
- LOOP = 'loop',
- PARALLEL = 'parallel',
-}
-
-// Helper function to check if a block type is a subflow type
-function isSubflowBlockType(blockType: string): blockType is SubflowType {
- return Object.values(SubflowType).includes(blockType as SubflowType)
-}
-
-// Helper function to update subflow node lists when child blocks are added/removed
-async function updateSubflowNodeList(dbOrTx: any, workflowId: string, parentId: string) {
- try {
- // Get all child blocks of this parent
- const childBlocks = await dbOrTx
- .select({ id: workflowBlocks.id })
- .from(workflowBlocks)
- .where(and(eq(workflowBlocks.workflowId, workflowId), eq(workflowBlocks.parentId, parentId)))
-
- const childNodeIds = childBlocks.map((block: any) => block.id)
-
- // Get current subflow config
- const subflowData = await dbOrTx
- .select({ config: workflowSubflows.config })
- .from(workflowSubflows)
- .where(and(eq(workflowSubflows.id, parentId), eq(workflowSubflows.workflowId, workflowId)))
- .limit(1)
-
- if (subflowData.length > 0) {
- const updatedConfig = {
- ...subflowData[0].config,
- nodes: childNodeIds,
- }
-
- await dbOrTx
- .update(workflowSubflows)
- .set({
- config: updatedConfig,
- updatedAt: new Date(),
- })
- .where(and(eq(workflowSubflows.id, parentId), eq(workflowSubflows.workflowId, workflowId)))
-
- logger.debug(`Updated subflow ${parentId} node list: [${childNodeIds.join(', ')}]`)
- }
- } catch (error) {
- logger.error(`Error updating subflow node list for ${parentId}:`, error)
- }
-}
-
-const SubflowOperationSchema = z.object({
- operation: z.enum(['add', 'remove', 'update']),
- target: z.literal('subflow'),
- payload: z.object({
- id: z.string(),
- type: z.enum(['loop', 'parallel']).optional(),
- config: z.record(z.any()).optional(),
- }),
- timestamp: z.number(),
-})
-
-const WorkflowOperationSchema = z.union([
- BlockOperationSchema,
- EdgeOperationSchema,
- SubflowOperationSchema,
-])
-
-// Simplified conflict resolution - just last-write-wins since we have normalized tables
-function shouldAcceptOperation(operation: any, roomLastModified: number): boolean {
- // Accept all operations - with normalized tables, conflicts are very unlikely
- // We could add basic timestamp validation if needed, but for now just accept everything
- return true
-}
-
-// Enhanced authentication middleware
-async function authenticateSocket(socket: AuthenticatedSocket, next: any) {
- try {
- // Extract authentication data from socket handshake
- const token = socket.handshake.auth?.token
- const origin = socket.handshake.headers.origin
- const referer = socket.handshake.headers.referer
-
- logger.info(`Socket ${socket.id} authentication attempt:`, {
- hasToken: !!token,
- origin,
- referer,
- allHeaders: Object.keys(socket.handshake.headers),
- })
+// Enhanced server configuration - HTTP server will be configured with handler after all dependencies are set up
+const httpServer = createServer()
- if (!token) {
- logger.warn(`Socket ${socket.id} rejected: No authentication token found`)
- return next(new Error('Authentication required'))
- }
+const io = createSocketIOServer(httpServer)
- // Validate one-time token with Better Auth
- try {
- logger.debug(`Attempting token validation for socket ${socket.id}`, {
- tokenLength: token?.length || 0,
- origin,
- })
+// Initialize room manager after io is created
+const roomManager = new RoomManager(io)
- const session = await auth.api.verifyOneTimeToken({
- body: {
- token,
- },
- })
-
- if (!session?.user?.id) {
- logger.warn(`Socket ${socket.id} rejected: Invalid token - no user found`)
- return next(new Error('Invalid session'))
- }
-
- // Store user info in socket for later use
- socket.userId = session.user.id
- socket.userName = session.user.name || session.user.email || 'Unknown User'
- socket.userEmail = session.user.email
- socket.activeOrganizationId = session.session.activeOrganizationId || undefined
-
- next()
- } catch (tokenError) {
- const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError)
- const errorStack = tokenError instanceof Error ? tokenError.stack : undefined
-
- logger.warn(`Token validation failed for socket ${socket.id}:`, {
- error: errorMessage,
- stack: errorStack,
- origin,
- referer,
- })
- return next(new Error('Token validation failed'))
- }
- } catch (error) {
- logger.error(`Socket authentication error for ${socket.id}:`, error)
- next(new Error('Authentication failed'))
- }
-}
-
-// Apply authentication middleware
io.use(authenticateSocket)
-// Utility functions
-async function verifyWorkspaceMembership(
- userId: string,
- workspaceId: string
-): Promise {
- try {
- const membership = await db
- .select({ role: workspaceMember.role })
- .from(workspaceMember)
- .where(and(eq(workspaceMember.workspaceId, workspaceId), eq(workspaceMember.userId, userId)))
- .limit(1)
-
- return membership.length > 0 ? membership[0].role : null
- } catch (error) {
- logger.error(`Error verifying workspace membership for ${userId} in ${workspaceId}:`, error)
- return null
- }
-}
-async function verifyWorkflowAccess(
- userId: string,
- workflowId: string
-): Promise<{ hasAccess: boolean; role?: string; workspaceId?: string }> {
- try {
- const workflowData = await db
- .select({
- userId: workflow.userId,
- workspaceId: workflow.workspaceId,
- name: workflow.name,
- })
- .from(workflow)
- .where(eq(workflow.id, workflowId))
- .limit(1)
-
- if (!workflowData.length) {
- logger.warn(`Workflow ${workflowId} not found`)
- return { hasAccess: false }
- }
-
- const { userId: workflowUserId, workspaceId, name: workflowName } = workflowData[0]
-
- // Check if user owns the workflow
- if (workflowUserId === userId) {
- logger.debug(`User ${userId} has owner access to workflow ${workflowId} (${workflowName})`)
- return { hasAccess: true, role: 'owner', workspaceId: workspaceId || undefined }
- }
-
- // Check workspace membership if workflow belongs to a workspace
- if (workspaceId) {
- const userRole = await verifyWorkspaceMembership(userId, workspaceId)
- if (userRole) {
- logger.debug(
- `User ${userId} has ${userRole} access to workflow ${workflowId} via workspace ${workspaceId}`
- )
- return { hasAccess: true, role: userRole, workspaceId }
- }
- logger.warn(
- `User ${userId} is not a member of workspace ${workspaceId} for workflow ${workflowId}`
- )
- return { hasAccess: false }
- }
-
- // Workflow doesn't belong to a workspace and user doesn't own it
- logger.warn(`User ${userId} has no access to workflow ${workflowId} (no workspace, not owner)`)
- return { hasAccess: false }
- } catch (error) {
- logger.error(
- `Error verifying workflow access for user ${userId}, workflow ${workflowId}:`,
- error
- )
- return { hasAccess: false }
- }
-}
-
-// Enhanced authorization for specific operations
-async function verifyOperationPermission(
- userId: string,
- workflowId: string,
- operation: string,
- target: string
-): Promise<{ allowed: boolean; reason?: string }> {
- try {
- const accessInfo = await verifyWorkflowAccess(userId, workflowId)
-
- if (!accessInfo.hasAccess) {
- return { allowed: false, reason: 'No access to workflow' }
- }
-
- // Define operation permissions based on role
- const rolePermissions = {
- owner: [
- 'add',
- 'remove',
- 'update',
- 'update-position',
- 'update-name',
- 'toggle-enabled',
- 'update-parent',
- 'duplicate',
- ],
- admin: [
- 'add',
- 'remove',
- 'update',
- 'update-position',
- 'update-name',
- 'toggle-enabled',
- 'update-parent',
- 'duplicate',
- ],
- member: [
- 'add',
- 'remove',
- 'update',
- 'update-position',
- 'update-name',
- 'toggle-enabled',
- 'update-parent',
- 'duplicate',
- ],
- viewer: ['update-position'], // Viewers can only move things around
- }
-
- const allowedOperations = rolePermissions[accessInfo.role as keyof typeof rolePermissions] || []
-
- if (!allowedOperations.includes(operation)) {
- return {
- allowed: false,
- reason: `Role '${accessInfo.role}' not permitted to perform '${operation}' on '${target}'`,
- }
- }
-
- return { allowed: true }
- } catch (error) {
- logger.error(`Error verifying operation permission:`, error)
- return { allowed: false, reason: 'Permission check failed' }
- }
-}
-
-function createWorkflowRoom(workflowId: string): WorkflowRoom {
- return {
- workflowId,
- users: new Map(),
- lastModified: Date.now(),
- activeConnections: 0,
- }
-}
-
-function cleanupUserFromRoom(socketId: string, workflowId: string) {
- const room = workflowRooms.get(workflowId)
- if (room) {
- room.users.delete(socketId)
- room.activeConnections = Math.max(0, room.activeConnections - 1)
-
- if (room.activeConnections === 0) {
- workflowRooms.delete(workflowId)
- logger.info(`Cleaned up empty workflow room: ${workflowId}`)
- }
- }
-
- socketToWorkflow.delete(socketId)
- userSessions.delete(socketId)
-}
-
-function clearPendingOperations(socketId: string) {
- // Clear any pending operations for this socket
- // This would be used if we implement operation queuing
- logger.debug(`Cleared pending operations for socket ${socketId}`)
-}
-
-// Handle workflow deletion notifications
-function handleWorkflowDeletion(workflowId: string) {
- logger.info(`Handling workflow deletion notification for ${workflowId}`)
-
- const room = workflowRooms.get(workflowId)
- if (!room) {
- logger.debug(`No active room found for deleted workflow ${workflowId}`)
- return
- }
-
- // Notify all users in the room that the workflow has been deleted
- io.to(workflowId).emit('workflow-deleted', {
- workflowId,
- message: 'This workflow has been deleted',
- timestamp: Date.now(),
- })
-
- // Disconnect all sockets from the workflow room
- const socketsToDisconnect: string[] = []
- room.users.forEach((presence, socketId) => {
- socketsToDisconnect.push(socketId)
- })
-
- // Clean up each socket connection
- socketsToDisconnect.forEach((socketId) => {
- const socket = io.sockets.sockets.get(socketId)
- if (socket) {
- socket.leave(workflowId)
- logger.debug(`Disconnected socket ${socketId} from deleted workflow ${workflowId}`)
- }
- cleanupUserFromRoom(socketId, workflowId)
- })
-
- // Clean up the room completely
- workflowRooms.delete(workflowId)
- logger.info(
- `Cleaned up workflow room ${workflowId} after deletion (${socketsToDisconnect.length} users disconnected)`
- )
-}
-
-// Database helper functions
-async function getWorkflowState(workflowId: string) {
- try {
- const workflowData = await db
- .select()
- .from(workflow)
- .where(eq(workflow.id, workflowId))
- .limit(1)
-
- if (!workflowData.length) {
- throw new Error(`Workflow ${workflowId} not found`)
- }
-
- return {
- ...workflowData[0],
- lastModified: Date.now(),
- }
- } catch (error) {
- logger.error(`Error fetching workflow state for ${workflowId}:`, error)
- throw error
- }
-}
-
-async function persistWorkflowOperation(workflowId: string, operation: any) {
- // Use database transaction for consistency
- try {
- const { operation: op, target, payload, timestamp, userId } = operation
-
- await db.transaction(async (tx) => {
- // Update the workflow's last modified timestamp first
- await tx
- .update(workflow)
- .set({ updatedAt: new Date(timestamp) })
- .where(eq(workflow.id, workflowId))
-
- // Handle different operation types within the transaction
- switch (target) {
- case 'block':
- await handleBlockOperationTx(tx, workflowId, op, payload, userId)
- break
- case 'edge':
- await handleEdgeOperationTx(tx, workflowId, op, payload, userId)
- break
- case 'subflow':
- await handleSubflowOperationTx(tx, workflowId, op, payload, userId)
- break
- default:
- throw new Error(`Unknown operation target: ${target}`)
- }
- })
- } catch (error) {
- logger.error(
- `❌ Error persisting workflow operation (${operation.operation} on ${operation.target}):`,
- error
- )
- throw error
- }
-}
-
-// Add data consistency validation
-async function validateWorkflowConsistency(
- workflowId: string
-): Promise<{ valid: boolean; issues: string[] }> {
- try {
- const issues: string[] = []
-
- // Check for orphaned edges (edges pointing to non-existent blocks)
- const orphanedEdges = await db
- .select({
- id: workflowEdges.id,
- sourceBlockId: workflowEdges.sourceBlockId,
- targetBlockId: workflowEdges.targetBlockId,
- })
- .from(workflowEdges)
- .leftJoin(workflowBlocks, eq(workflowEdges.sourceBlockId, workflowBlocks.id))
- .where(
- and(
- eq(workflowEdges.workflowId, workflowId),
- isNull(workflowBlocks.id) // Source block doesn't exist
- )
- )
-
- if (orphanedEdges.length > 0) {
- issues.push(`Found ${orphanedEdges.length} orphaned edges with missing source blocks`)
- }
-
- // Could add more consistency checks here as needed
-
- return { valid: issues.length === 0, issues }
- } catch (error) {
- logger.error('Error validating workflow consistency:', error)
- return { valid: false, issues: ['Consistency check failed'] }
- }
-}
-
-// Transaction-based operation handlers for data consistency
-async function handleBlockOperationTx(
- tx: any,
- workflowId: string,
- operation: string,
- payload: any,
- userId: string
-) {
- return handleBlockOperationImpl(tx, workflowId, operation, payload, userId)
-}
-
-async function handleEdgeOperationTx(
- tx: any,
- workflowId: string,
- operation: string,
- payload: any,
- userId: string
-) {
- return handleEdgeOperationImpl(tx, workflowId, operation, payload, userId)
-}
-
-async function handleSubflowOperationTx(
- tx: any,
- workflowId: string,
- operation: string,
- payload: any,
- userId: string
-) {
- return handleSubflowOperationImpl(tx, workflowId, operation, payload, userId)
-}
-
-// Implementation functions that work with both db and transaction
-async function handleEdgeOperationImpl(
- dbOrTx: any,
- workflowId: string,
- operation: string,
- payload: any,
- userId: string
-) {
- // Move the existing handleEdgeOperation logic here
- return handleEdgeOperation(workflowId, operation, payload, userId)
-}
-
-async function handleSubflowOperationImpl(
- dbOrTx: any,
- workflowId: string,
- operation: string,
- payload: any,
- userId: string
-) {
- try {
- switch (operation) {
- case 'add':
- // Validate required fields
- if (!payload.id || !payload.type || !payload.config) {
- throw new Error('Missing required fields for add subflow operation')
- }
-
- // Validate subflow type
- if (!['loop', 'parallel'].includes(payload.type)) {
- throw new Error(`Invalid subflow type: ${payload.type}`)
- }
-
- // Validate config structure based on type
- if (payload.type === 'loop') {
- if (!payload.config.nodes || !Array.isArray(payload.config.nodes)) {
- throw new Error('Loop subflow requires nodes array in config')
- }
- if (!payload.config.loopType || !['for', 'forEach'].includes(payload.config.loopType)) {
- throw new Error('Loop subflow requires valid loopType (for or forEach)')
- }
- } else if (payload.type === 'parallel') {
- if (!payload.config.nodes || !Array.isArray(payload.config.nodes)) {
- throw new Error('Parallel subflow requires nodes array in config')
- }
- }
-
- await dbOrTx.insert(workflowSubflows).values({
- id: payload.id,
- workflowId,
- type: payload.type,
- config: payload.config,
- })
-
- logger.debug(`Added ${payload.type} subflow ${payload.id} to workflow ${workflowId}`)
- break
-
- case 'update': {
- if (!payload.id || !payload.config) {
- throw new Error('Missing required fields for update subflow operation')
- }
-
- logger.debug(`[SERVER] Updating subflow ${payload.id} with config:`, payload.config)
-
- // Update the subflow configuration
- const updateResult = await dbOrTx
- .update(workflowSubflows)
- .set({
- config: payload.config,
- updatedAt: new Date(),
- })
- .where(
- and(eq(workflowSubflows.id, payload.id), eq(workflowSubflows.workflowId, workflowId))
- )
- .returning({ id: workflowSubflows.id })
-
- if (updateResult.length === 0) {
- throw new Error(`Subflow ${payload.id} not found in workflow ${workflowId}`)
- }
-
- // Also update the corresponding block's data to keep UI in sync
- if (payload.type === 'loop' && payload.config.iterations !== undefined) {
- // Update the loop block's data.count property
- await dbOrTx
- .update(workflowBlocks)
- .set({
- data: {
- ...payload.config,
- count: payload.config.iterations,
- loopType: payload.config.loopType,
- collection: payload.config.forEachItems,
- width: 500,
- height: 300,
- type: 'loopNode',
- },
- updatedAt: new Date(),
- })
- .where(
- and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId))
- )
- } else if (payload.type === 'parallel') {
- // Update the parallel block's data properties
- const blockData = {
- ...payload.config,
- width: 500,
- height: 300,
- type: 'parallelNode',
- }
-
- // Include count if provided
- if (payload.config.count !== undefined) {
- blockData.count = payload.config.count
- }
-
- // Include collection if provided
- if (payload.config.distribution !== undefined) {
- blockData.collection = payload.config.distribution
- }
-
- // Include parallelType if provided
- if (payload.config.parallelType !== undefined) {
- blockData.parallelType = payload.config.parallelType
- }
-
- await dbOrTx
- .update(workflowBlocks)
- .set({
- data: blockData,
- updatedAt: new Date(),
- })
- .where(
- and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId))
- )
- }
-
- break
- }
-
- case 'remove': {
- if (!payload.id) {
- throw new Error('Missing subflow ID for remove operation')
- }
-
- const deleteResult = await dbOrTx
- .delete(workflowSubflows)
- .where(
- and(eq(workflowSubflows.id, payload.id), eq(workflowSubflows.workflowId, workflowId))
- )
- .returning({ id: workflowSubflows.id })
-
- if (deleteResult.length === 0) {
- throw new Error(`Subflow ${payload.id} not found in workflow ${workflowId}`)
- }
-
- logger.debug(`Removed subflow ${payload.id} from workflow ${workflowId}`)
- break
- }
-
- default:
- logger.warn(`Unknown subflow operation: ${operation}`)
- throw new Error(`Unsupported subflow operation: ${operation}`)
- }
- } catch (error) {
- logger.error(`Error in handleSubflowOperation (${operation}):`, error)
- throw error
- }
-}
-
-// Enhanced operation handlers with comprehensive validation
-async function handleBlockOperation(
- workflowId: string,
- operation: string,
- payload: any,
- userId: string
-) {
- return handleBlockOperationImpl(db, workflowId, operation, payload, userId)
-}
-
-async function handleBlockOperationImpl(
- dbOrTx: any,
- workflowId: string,
- operation: string,
- payload: any,
- userId: string
-) {
- try {
- switch (operation) {
- case 'add': {
- // Validate required fields for add operation
- if (!payload.id || !payload.type || !payload.name || !payload.position) {
- throw new Error('Missing required fields for add block operation')
- }
-
- logger.debug(`[SERVER] Adding block: ${payload.type} (${payload.id})`, {
- isSubflowType: isSubflowBlockType(payload.type),
- payload,
- })
-
- // Extract parentId and extent from payload.data if they exist there, otherwise from payload directly
- const parentId = payload.parentId || payload.data?.parentId || null
- const extent = payload.extent || payload.data?.extent || null
-
- logger.debug(`[SERVER] Block parent info:`, {
- blockId: payload.id,
- hasParent: !!parentId,
- parentId,
- extent,
- payloadParentId: payload.parentId,
- dataParentId: payload.data?.parentId,
- })
-
- await dbOrTx.insert(workflowBlocks).values({
- id: payload.id,
- workflowId,
- type: payload.type,
- name: payload.name,
- positionX: payload.position.x,
- positionY: payload.position.y,
- data: payload.data || {},
- parentId,
- extent,
- enabled: true, // Default to enabled
- })
-
- // Auto-create subflow entry for loop/parallel blocks
- if (isSubflowBlockType(payload.type)) {
- try {
- const subflowConfig =
- payload.type === SubflowType.LOOP
- ? {
- id: payload.id,
- nodes: [], // Empty initially, will be populated when child blocks are added
- iterations: payload.data?.count || DEFAULT_LOOP_ITERATIONS,
- loopType: payload.data?.loopType || 'for',
- forEachItems: payload.data?.collection || '',
- }
- : {
- id: payload.id,
- nodes: [], // Empty initially, will be populated when child blocks are added
- distribution: payload.data?.collection || '',
- }
-
- logger.debug(
- `[SERVER] Auto-creating ${payload.type} subflow ${payload.id}:`,
- subflowConfig
- )
-
- await dbOrTx.insert(workflowSubflows).values({
- id: payload.id,
- workflowId,
- type: payload.type,
- config: subflowConfig,
- })
- } catch (subflowError) {
- logger.error(
- `[SERVER] ❌ Failed to create ${payload.type} subflow ${payload.id}:`,
- subflowError
- )
- throw subflowError
- }
- }
-
- // If this block has a parent, update the parent's subflow node list
- if (parentId) {
- await updateSubflowNodeList(dbOrTx, workflowId, parentId)
- }
-
- logger.debug(`Added block ${payload.id} (${payload.type}) to workflow ${workflowId}`)
- break
- }
-
- case 'update-position': {
- if (!payload.id || !payload.position) {
- throw new Error('Missing required fields for update position operation')
- }
-
- const updateResult = await dbOrTx
- .update(workflowBlocks)
- .set({
- positionX: payload.position.x,
- positionY: payload.position.y,
- updatedAt: new Date(),
- })
- .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
- .returning({ id: workflowBlocks.id })
-
- if (updateResult.length === 0) {
- throw new Error(`Block ${payload.id} not found in workflow ${workflowId}`)
- }
- break
- }
-
- case 'update-name':
- if (!payload.id || !payload.name) {
- throw new Error('Missing required fields for update name operation')
- }
-
- await db
- .update(workflowBlocks)
- .set({
- name: payload.name,
- updatedAt: new Date(),
- })
- .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
- break
-
- case 'update-parent': {
- if (!payload.id) {
- throw new Error('Missing block ID for update parent operation')
- }
-
- // Get the current parent before updating
- const currentBlock = await dbOrTx
- .select({ parentId: workflowBlocks.parentId })
- .from(workflowBlocks)
- .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
- .limit(1)
+const httpHandler = createHttpHandler(roomManager, logger)
+httpServer.on('request', httpHandler)
- const oldParentId = currentBlock.length > 0 ? currentBlock[0].parentId : null
-
- await dbOrTx
- .update(workflowBlocks)
- .set({
- parentId: payload.parentId || null,
- extent: payload.extent || null,
- updatedAt: new Date(),
- })
- .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
-
- // Update subflow node lists for both old and new parents
- if (oldParentId) {
- await updateSubflowNodeList(dbOrTx, workflowId, oldParentId)
- }
- if (payload.parentId && payload.parentId !== oldParentId) {
- await updateSubflowNodeList(dbOrTx, workflowId, payload.parentId)
- }
- break
- }
-
- case 'remove': {
- if (!payload.id) {
- throw new Error('Missing block ID for remove operation')
- }
-
- // Check if this is a subflow block that needs cascade deletion
- const blockToRemove = await dbOrTx
- .select({ type: workflowBlocks.type, parentId: workflowBlocks.parentId })
- .from(workflowBlocks)
- .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
- .limit(1)
-
- if (blockToRemove.length > 0 && isSubflowBlockType(blockToRemove[0].type)) {
- // Cascade delete: Remove all child blocks first
- const childBlocks = await dbOrTx
- .select({ id: workflowBlocks.id, type: workflowBlocks.type })
- .from(workflowBlocks)
- .where(
- and(
- eq(workflowBlocks.workflowId, workflowId),
- eq(workflowBlocks.parentId, payload.id)
- )
- )
-
- logger.debug(
- `[SERVER] Starting cascade deletion for subflow block ${payload.id} (type: ${blockToRemove[0].type})`
- )
- logger.debug(
- `[SERVER] Found ${childBlocks.length} child blocks to delete: [${childBlocks.map((b: any) => `${b.id} (${b.type})`).join(', ')}]`
- )
-
- // Remove edges connected to child blocks
- for (const childBlock of childBlocks) {
- await dbOrTx
- .delete(workflowEdges)
- .where(
- and(
- eq(workflowEdges.workflowId, workflowId),
- or(
- eq(workflowEdges.sourceBlockId, childBlock.id),
- eq(workflowEdges.targetBlockId, childBlock.id)
- )
- )
- )
- }
-
- // Remove child blocks from database
- await dbOrTx
- .delete(workflowBlocks)
- .where(
- and(
- eq(workflowBlocks.workflowId, workflowId),
- eq(workflowBlocks.parentId, payload.id)
- )
- )
-
- // Remove the subflow entry
- await dbOrTx
- .delete(workflowSubflows)
- .where(
- and(eq(workflowSubflows.id, payload.id), eq(workflowSubflows.workflowId, workflowId))
- )
- }
-
- // Remove any edges connected to this block
- await dbOrTx
- .delete(workflowEdges)
- .where(
- and(
- eq(workflowEdges.workflowId, workflowId),
- or(
- eq(workflowEdges.sourceBlockId, payload.id),
- eq(workflowEdges.targetBlockId, payload.id)
- )
- )
- )
-
- // Finally remove the block itself
- await dbOrTx
- .delete(workflowBlocks)
- .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
-
- // If this block had a parent, update the parent's subflow node list
- if (blockToRemove.length > 0 && blockToRemove[0].parentId) {
- await updateSubflowNodeList(dbOrTx, workflowId, blockToRemove[0].parentId)
- }
-
- logger.debug(`Removed block ${payload.id} and its connections from workflow ${workflowId}`)
- break
- }
-
- case 'toggle-enabled':
- if (!payload.id || payload.enabled === undefined) {
- throw new Error('Missing required fields for toggle enabled operation')
- }
-
- await db
- .update(workflowBlocks)
- .set({
- enabled: payload.enabled,
- updatedAt: new Date(),
- })
- .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
- break
-
- case 'duplicate': {
- if (!payload.id || !payload.newId || !payload.position) {
- throw new Error('Missing required fields for duplicate operation')
- }
-
- // Get the original block
- const originalBlock = await db
- .select()
- .from(workflowBlocks)
- .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
- .limit(1)
-
- if (originalBlock.length === 0) {
- throw new Error(`Original block ${payload.id} not found`)
- }
-
- // Create duplicate with new ID and position
- await db.insert(workflowBlocks).values({
- ...originalBlock[0],
- id: payload.newId,
- name: `${originalBlock[0].name} (Copy)`,
- positionX: payload.position.x,
- positionY: payload.position.y,
- createdAt: new Date(),
- updatedAt: new Date(),
- })
- break
- }
-
- default:
- logger.warn(`Unknown block operation: ${operation}`)
- throw new Error(`Unsupported block operation: ${operation}`)
- }
- } catch (error) {
- logger.error(`Error in handleBlockOperation (${operation}):`, error)
- throw error
- }
-}
-
-async function handleEdgeOperation(
- workflowId: string,
- operation: string,
- payload: any,
- userId: string
-) {
- try {
- switch (operation) {
- case 'add': {
- // Validate required fields
- if (!payload.id || !payload.source || !payload.target) {
- throw new Error('Missing required fields for add edge operation')
- }
-
- // Check if source and target blocks exist
- const sourceBlock = await db
- .select({ id: workflowBlocks.id })
- .from(workflowBlocks)
- .where(
- and(eq(workflowBlocks.id, payload.source), eq(workflowBlocks.workflowId, workflowId))
- )
- .limit(1)
-
- const targetBlock = await db
- .select({ id: workflowBlocks.id })
- .from(workflowBlocks)
- .where(
- and(eq(workflowBlocks.id, payload.target), eq(workflowBlocks.workflowId, workflowId))
- )
- .limit(1)
-
- if (sourceBlock.length === 0) {
- // For new workflows, blocks might not be persisted yet - log warning but don't fail
- logger.warn(
- `Source block ${payload.source} not found in database - may be a new workflow`
- )
- throw new Error(`Source block ${payload.source} not found`)
- }
- if (targetBlock.length === 0) {
- logger.warn(
- `Target block ${payload.target} not found in database - may be a new workflow`
- )
- throw new Error(`Target block ${payload.target} not found`)
- }
-
- // Check for duplicate edges
- const existingEdge = await db
- .select({ id: workflowEdges.id })
- .from(workflowEdges)
- .where(
- and(
- eq(workflowEdges.workflowId, workflowId),
- eq(workflowEdges.sourceBlockId, payload.source),
- eq(workflowEdges.targetBlockId, payload.target),
- eq(workflowEdges.sourceHandle, payload.sourceHandle || ''),
- eq(workflowEdges.targetHandle, payload.targetHandle || '')
- )
- )
- .limit(1)
-
- if (existingEdge.length > 0) {
- logger.warn(`Duplicate edge detected: ${payload.source} -> ${payload.target}`)
- return // Skip duplicate edge creation
- }
-
- await db.insert(workflowEdges).values({
- id: payload.id,
- workflowId,
- sourceBlockId: payload.source,
- targetBlockId: payload.target,
- sourceHandle: payload.sourceHandle || null,
- targetHandle: payload.targetHandle || null,
- })
-
- logger.debug(`Added edge ${payload.id}: ${payload.source} -> ${payload.target}`)
- break
- }
-
- case 'remove': {
- if (!payload.id) {
- throw new Error('Missing edge ID for remove operation')
- }
-
- const deleteResult = await db
- .delete(workflowEdges)
- .where(and(eq(workflowEdges.id, payload.id), eq(workflowEdges.workflowId, workflowId)))
- .returning({ id: workflowEdges.id })
-
- if (deleteResult.length === 0) {
- throw new Error(`Edge ${payload.id} not found in workflow ${workflowId}`)
- }
-
- logger.debug(`Removed edge ${payload.id} from workflow ${workflowId}`)
- break
- }
-
- default:
- logger.warn(`Unknown edge operation: ${operation}`)
- throw new Error(`Unsupported edge operation: ${operation}`)
- }
- } catch (error) {
- logger.error(`Error in handleEdgeOperation (${operation}):`, error)
- throw error
- }
-}
-
-// Global error handling
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception:', error)
// Don't exit in production, just log
@@ -1249,7 +30,6 @@ process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at:', promise, 'reason:', reason)
})
-// Socket server error handling
httpServer.on('error', (error) => {
logger.error('HTTP server error:', error)
})
@@ -1264,517 +44,11 @@ io.engine.on('connection_error', (err) => {
})
io.on('connection', (socket: AuthenticatedSocket) => {
- // Set up error handling for this socket
- socket.on('error', (error) => {
- logger.error(`Socket ${socket.id} error:`, error)
- })
-
- socket.conn.on('error', (error) => {
- logger.error(`Socket ${socket.id} connection error:`, error)
- })
+ logger.info(`New socket connection: ${socket.id}`)
- // Handle joining a workflow room with enhanced authentication
- socket.on('join-workflow', async ({ workflowId }) => {
- try {
- // Use authenticated user info from socket
- const userId = socket.userId
- const userName = socket.userName
-
- if (!userId || !userName) {
- logger.warn(`Join workflow rejected: Socket ${socket.id} not authenticated`)
- socket.emit('join-workflow-error', { error: 'Authentication required' })
- return
- }
-
- logger.info(`Join workflow request from ${userId} (${userName}) for workflow ${workflowId}`)
-
- // Verify workflow access
- try {
- const accessInfo = await verifyWorkflowAccess(userId, workflowId)
- if (!accessInfo.hasAccess) {
- logger.warn(`User ${userId} (${userName}) denied access to workflow ${workflowId}`)
- socket.emit('join-workflow-error', { error: 'Access denied to workflow' })
- return
- }
- } catch (error) {
- logger.warn(`Error verifying workflow access for ${userId}:`, error)
- socket.emit('join-workflow-error', { error: 'Failed to verify workflow access' })
- return
- }
-
- // Leave any previous workflow room
- const currentWorkflowId = socketToWorkflow.get(socket.id)
- if (currentWorkflowId) {
- socket.leave(currentWorkflowId)
- cleanupUserFromRoom(socket.id, currentWorkflowId)
-
- // Notify previous room about user leaving
- socket.to(currentWorkflowId).emit('user-left', {
- userId,
- socketId: socket.id,
- })
- }
-
- // Join the new workflow room
- socket.join(workflowId)
-
- // Create or get workflow room
- if (!workflowRooms.has(workflowId)) {
- workflowRooms.set(workflowId, createWorkflowRoom(workflowId))
- }
-
- const room = workflowRooms.get(workflowId)!
- room.activeConnections++
-
- // Store user presence
- const userPresence: UserPresence = {
- userId,
- workflowId,
- userName,
- socketId: socket.id,
- joinedAt: Date.now(),
- lastActivity: Date.now(),
- }
-
- room.users.set(socket.id, userPresence)
- socketToWorkflow.set(socket.id, workflowId)
- userSessions.set(socket.id, { userId, userName })
-
- // Get current room presence for the new user
- const roomPresence = Array.from(room.users.values())
-
- // Send current workflow state and presence to the new user
- const workflowState = await getWorkflowState(workflowId)
- socket.emit('workflow-state', workflowState)
- socket.emit('presence-update', roomPresence)
-
- // Notify others in the room about new user
- socket.to(workflowId).emit('user-joined', {
- userId,
- userName,
- socketId: socket.id,
- })
-
- logger.info(
- `User ${userId} (${userName}) joined workflow ${workflowId}. Room now has ${room.activeConnections} users.`
- )
- } catch (error) {
- logger.error('Error joining workflow:', error)
- socket.emit('error', {
- type: 'JOIN_ERROR',
- message: 'Failed to join workflow',
- })
- }
- })
-
- // Handle workflow operations (blocks, edges, subflows) with enhanced validation and conflict resolution
- socket.on('workflow-operation', async (data) => {
- const workflowId = socketToWorkflow.get(socket.id)
- const session = userSessions.get(socket.id)
-
- if (!workflowId || !session) {
- socket.emit('error', {
- type: 'NOT_JOINED',
- message: 'Not joined to any workflow',
- })
- return
- }
-
- const room = workflowRooms.get(workflowId)
- if (!room) {
- socket.emit('error', {
- type: 'ROOM_NOT_FOUND',
- message: 'Workflow room not found',
- })
- return
- }
-
- try {
- // Validate operation schema
- const validatedOperation = WorkflowOperationSchema.parse(data)
- const { operation, target, payload, timestamp } = validatedOperation
-
- // Check if operation should be accepted (simplified conflict resolution)
- if (!shouldAcceptOperation(validatedOperation, room.lastModified)) {
- socket.emit('operation-rejected', {
- type: 'OPERATION_REJECTED',
- message: 'Operation rejected',
- operation,
- target,
- serverTimestamp: Date.now(),
- })
- return
- }
-
- // Check operation permissions (temporarily bypassed for testing)
- const permissionCheck = await verifyOperationPermission(
- session.userId,
- workflowId,
- operation,
- target
- )
- if (!permissionCheck.allowed) {
- logger.warn(
- `User ${session.userId} forbidden from ${operation} on ${target}: ${permissionCheck.reason}`
- )
- socket.emit('operation-forbidden', {
- type: 'INSUFFICIENT_PERMISSIONS',
- message: permissionCheck.reason || 'Insufficient permissions for this operation',
- operation,
- target,
- })
- return
- }
-
- // Update user activity
- const userPresence = room.users.get(socket.id)
- if (userPresence) {
- userPresence.lastActivity = Date.now()
- }
-
- // Persist to database with transaction (last-write-wins)
- const serverTimestamp = Date.now()
- await persistWorkflowOperation(workflowId, {
- operation,
- target,
- payload,
- timestamp: serverTimestamp, // Use server timestamp for consistency
- userId: session.userId,
- })
-
- // Update room's last modified timestamp
- room.lastModified = serverTimestamp
-
- // Broadcast to all other clients in the room (excluding sender)
- const broadcastData = {
- operation,
- target,
- payload,
- timestamp: serverTimestamp,
- senderId: socket.id,
- userId: session.userId,
- userName: session.userName,
- // Add operation metadata for better client handling
- metadata: {
- workflowId,
- operationId: crypto.randomUUID(), // Unique operation ID for tracking
- },
- }
-
- socket.to(workflowId).emit('workflow-operation', broadcastData)
-
- // Send confirmation back to sender with operation ID for tracking
- socket.emit('operation-confirmed', {
- operation,
- target,
- operationId: broadcastData.metadata.operationId,
- serverTimestamp,
- })
- } catch (error) {
- if (error instanceof z.ZodError) {
- socket.emit('operation-error', {
- type: 'VALIDATION_ERROR',
- message: 'Invalid operation data',
- errors: error.errors,
- operation: data.operation,
- target: data.target,
- })
- logger.warn(`Validation error for operation from ${session.userId}:`, error.errors)
- } else if (error instanceof Error) {
- // Handle specific database errors
- if (error.message.includes('not found')) {
- socket.emit('operation-error', {
- type: 'RESOURCE_NOT_FOUND',
- message: error.message,
- operation: data.operation,
- target: data.target,
- })
- } else if (error.message.includes('duplicate') || error.message.includes('unique')) {
- socket.emit('operation-error', {
- type: 'DUPLICATE_RESOURCE',
- message: 'Resource already exists',
- operation: data.operation,
- target: data.target,
- })
- } else {
- socket.emit('operation-error', {
- type: 'OPERATION_FAILED',
- message: error.message,
- operation: data.operation,
- target: data.target,
- })
- }
- logger.error(
- `Operation error for ${session.userId} (${data.operation} on ${data.target}):`,
- error
- )
- } else {
- socket.emit('operation-error', {
- type: 'UNKNOWN_ERROR',
- message: 'An unknown error occurred',
- operation: data.operation,
- target: data.target,
- })
- logger.error('Unknown error handling workflow operation:', error)
- }
- }
- })
-
- // Handle subblock value updates
- socket.on('subblock-update', async (data) => {
- const workflowId = socketToWorkflow.get(socket.id)
- const session = userSessions.get(socket.id)
-
- if (!workflowId || !session) {
- logger.debug(`Ignoring subblock update: socket not connected to any workflow room`, {
- socketId: socket.id,
- hasWorkflowId: !!workflowId,
- hasSession: !!session,
- })
- return
- }
-
- const { blockId, subblockId, value, timestamp } = data
- const room = workflowRooms.get(workflowId)
-
- if (!room) {
- logger.debug(`Ignoring subblock update: workflow room not found`, {
- socketId: socket.id,
- workflowId,
- blockId,
- subblockId,
- })
- return
- }
-
- try {
- // Update user activity
- const userPresence = room.users.get(socket.id)
- if (userPresence) {
- userPresence.lastActivity = Date.now()
- }
-
- // First, verify that the workflow still exists in the database
- const workflowExists = await db
- .select({ id: workflow.id })
- .from(workflow)
- .where(eq(workflow.id, workflowId))
- .limit(1)
-
- if (workflowExists.length === 0) {
- logger.warn(`Ignoring subblock update: workflow ${workflowId} no longer exists`, {
- socketId: socket.id,
- blockId,
- subblockId,
- })
- // Clean up the socket from this non-existent workflow
- cleanupUserFromRoom(socket.id, workflowId)
- return
- }
-
- // Persist subblock update to database
- let updateSuccessful = false
- await db.transaction(async (tx) => {
- // Get the current block subBlocks data
- const [block] = await tx
- .select({ subBlocks: workflowBlocks.subBlocks })
- .from(workflowBlocks)
- .where(and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId)))
- .limit(1)
-
- if (!block) {
- // Block was deleted - this is a normal race condition in collaborative editing
- // Log it as debug info and gracefully ignore the update
- logger.debug(
- `Ignoring subblock update for deleted block: ${workflowId}/${blockId}.${subblockId}`
- )
- return // Exit transaction gracefully without error
- }
-
- // Parse the current subBlocks data
- const subBlocks = (block.subBlocks as any) || {}
-
- // Update the subblock value in the subBlocks data
- if (!subBlocks[subblockId]) {
- subBlocks[subblockId] = {}
- }
- subBlocks[subblockId].value = value
-
- // Save the updated subBlocks data back to the database
- await tx
- .update(workflowBlocks)
- .set({
- subBlocks: subBlocks,
- updatedAt: new Date(),
- })
- .where(and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId)))
-
- updateSuccessful = true
- })
-
- // Only broadcast to other clients if the update was successful
- if (updateSuccessful) {
- socket.to(workflowId).emit('subblock-update', {
- blockId,
- subblockId,
- value,
- timestamp,
- senderId: socket.id,
- userId: session.userId,
- })
- }
-
- logger.debug(`Subblock update in workflow ${workflowId}: ${blockId}.${subblockId}`)
- } catch (error) {
- logger.error('Error handling subblock update:', error)
-
- // Send error back to client
- socket.emit('operation-error', {
- type: 'SUBBLOCK_UPDATE_FAILED',
- message: `Failed to update subblock ${blockId}.${subblockId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
- operation: 'subblock-update',
- target: 'subblock',
- })
- }
- })
-
- // Handle cursor/presence updates
- socket.on('cursor-update', ({ cursor }) => {
- const workflowId = socketToWorkflow.get(socket.id)
- const session = userSessions.get(socket.id)
-
- if (!workflowId || !session) return
-
- const room = workflowRooms.get(workflowId)
- if (!room) return
-
- // Update stored cursor position
- const userPresence = room.users.get(socket.id)
- if (userPresence) {
- userPresence.cursor = cursor
- userPresence.lastActivity = Date.now()
- }
-
- // Broadcast cursor position to others in the room
- socket.to(workflowId).emit('cursor-update', {
- socketId: socket.id,
- userId: session.userId,
- userName: session.userName,
- cursor,
- })
- })
-
- // Handle user selection (for showing what block/element a user has selected)
- socket.on('selection-update', ({ selection }) => {
- const workflowId = socketToWorkflow.get(socket.id)
- const session = userSessions.get(socket.id)
-
- if (!workflowId || !session) return
-
- const room = workflowRooms.get(workflowId)
- if (!room) return
-
- // Update stored selection
- const userPresence = room.users.get(socket.id)
- if (userPresence) {
- userPresence.selection = selection
- userPresence.lastActivity = Date.now()
- }
-
- socket.to(workflowId).emit('selection-update', {
- socketId: socket.id,
- userId: session.userId,
- userName: session.userName,
- selection, // { type: 'block' | 'edge' | 'none', id?: string }
- })
- })
-
- // Handle disconnect with enhanced cleanup and recovery
- socket.on('disconnect', (reason) => {
- const workflowId = socketToWorkflow.get(socket.id)
- const session = userSessions.get(socket.id)
-
- logger.info(`Socket ${socket.id} disconnected: ${reason}`)
-
- if (workflowId && session) {
- // Clean up user from room
- cleanupUserFromRoom(socket.id, workflowId)
-
- // Notify others in the room
- socket.to(workflowId).emit('user-left', {
- userId: session.userId,
- socketId: socket.id,
- reason: reason,
- })
-
- logger.info(
- `User ${session.userId} (${session.userName}) disconnected from workflow ${workflowId} - reason: ${reason}`
- )
- }
-
- // Clear any pending operations for this socket
- clearPendingOperations(socket.id)
- })
-
- // Handle connection errors
- socket.on('error', (error) => {
- logger.error(`Socket ${socket.id} error:`, error)
- const session = userSessions.get(socket.id)
- if (session) {
- logger.error(`Error for user ${session.userId} (${session.userName}):`, error)
- }
- })
-
- // Handle ping/pong for connection health
- socket.on('ping', () => {
- socket.emit('pong')
- })
-
- // Handle manual reconnection requests
- socket.on('request-sync', async ({ workflowId }) => {
- try {
- if (!socket.userId) {
- socket.emit('error', { type: 'NOT_AUTHENTICATED', message: 'Not authenticated' })
- return
- }
-
- const accessInfo = await verifyWorkflowAccess(socket.userId, workflowId)
- if (!accessInfo.hasAccess) {
- socket.emit('error', { type: 'ACCESS_DENIED', message: 'Access denied' })
- return
- }
-
- // Send current workflow state
- const workflowState = await getWorkflowState(workflowId)
- socket.emit('workflow-state', workflowState)
-
- logger.info(`Sent sync data to ${socket.userId} for workflow ${workflowId}`)
- } catch (error) {
- logger.error('Error handling sync request:', error)
- socket.emit('error', { type: 'SYNC_FAILED', message: 'Failed to sync workflow state' })
- }
- })
-
- // Handle explicit leave workflow
- socket.on('leave-workflow', () => {
- const workflowId = socketToWorkflow.get(socket.id)
- const session = userSessions.get(socket.id)
-
- if (workflowId && session) {
- socket.leave(workflowId)
- cleanupUserFromRoom(socket.id, workflowId)
-
- socket.to(workflowId).emit('user-left', {
- userId: session.userId,
- socketId: socket.id,
- })
-
- logger.info(`User ${session.userId} (${session.userName}) left workflow ${workflowId}`)
- }
- })
+ setupAllHandlers(socket, roomManager)
})
-// Add detailed request logging
httpServer.on('request', (req, res) => {
logger.info(`🌐 HTTP Request: ${req.method} ${req.url}`, {
method: req.method,
@@ -1786,7 +60,6 @@ httpServer.on('request', (req, res) => {
})
})
-// Enhanced connection logging
io.engine.on('connection_error', (err) => {
logger.error('❌ Engine.IO Connection error:', {
code: err.code,
@@ -1802,7 +75,6 @@ io.engine.on('connection_error', (err) => {
})
})
-// Start the server
const PORT = Number(process.env.PORT || process.env.SOCKET_PORT || 3002)
logger.info('Starting Socket.IO server...', {
@@ -1822,7 +94,6 @@ httpServer.on('error', (error) => {
process.exit(1)
})
-// Graceful shutdown
process.on('SIGINT', () => {
logger.info('Shutting down Socket.IO server...')
httpServer.close(() => {
diff --git a/apps/sim/socket-server/middleware/auth.ts b/apps/sim/socket-server/middleware/auth.ts
new file mode 100644
index 00000000000..1611b9d0855
--- /dev/null
+++ b/apps/sim/socket-server/middleware/auth.ts
@@ -0,0 +1,76 @@
+import type { Socket } from 'socket.io'
+import { auth } from '../../lib/auth'
+import { createLogger } from '../../lib/logs/console-logger'
+
+const logger = createLogger('SocketAuth')
+
+// Extend Socket interface to include user data
+export interface AuthenticatedSocket extends Socket {
+ userId?: string
+ userName?: string
+ userEmail?: string
+ activeOrganizationId?: string
+}
+
+// Enhanced authentication middleware
+export async function authenticateSocket(socket: AuthenticatedSocket, next: any) {
+ try {
+ // Extract authentication data from socket handshake
+ const token = socket.handshake.auth?.token
+ const origin = socket.handshake.headers.origin
+ const referer = socket.handshake.headers.referer
+
+ logger.info(`Socket ${socket.id} authentication attempt:`, {
+ hasToken: !!token,
+ origin,
+ referer,
+ allHeaders: Object.keys(socket.handshake.headers),
+ })
+
+ if (!token) {
+ logger.warn(`Socket ${socket.id} rejected: No authentication token found`)
+ return next(new Error('Authentication required'))
+ }
+
+ // Validate one-time token with Better Auth
+ try {
+ logger.debug(`Attempting token validation for socket ${socket.id}`, {
+ tokenLength: token?.length || 0,
+ origin,
+ })
+
+ const session = await auth.api.verifyOneTimeToken({
+ body: {
+ token,
+ },
+ })
+
+ if (!session?.user?.id) {
+ logger.warn(`Socket ${socket.id} rejected: Invalid token - no user found`)
+ return next(new Error('Invalid session'))
+ }
+
+ // Store user info in socket for later use
+ socket.userId = session.user.id
+ socket.userName = session.user.name || session.user.email || 'Unknown User'
+ socket.userEmail = session.user.email
+ socket.activeOrganizationId = session.session.activeOrganizationId || undefined
+
+ next()
+ } catch (tokenError) {
+ const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError)
+ const errorStack = tokenError instanceof Error ? tokenError.stack : undefined
+
+ logger.warn(`Token validation failed for socket ${socket.id}:`, {
+ error: errorMessage,
+ stack: errorStack,
+ origin,
+ referer,
+ })
+ return next(new Error('Token validation failed'))
+ }
+ } catch (error) {
+ logger.error(`Socket authentication error for ${socket.id}:`, error)
+ next(new Error('Authentication failed'))
+ }
+}
diff --git a/apps/sim/socket-server/middleware/permissions.ts b/apps/sim/socket-server/middleware/permissions.ts
new file mode 100644
index 00000000000..4d4044b73e6
--- /dev/null
+++ b/apps/sim/socket-server/middleware/permissions.ts
@@ -0,0 +1,150 @@
+import { and, eq } from 'drizzle-orm'
+import { db } from '../../db'
+import { workflow, workspaceMember } from '../../db/schema'
+import { createLogger } from '../../lib/logs/console-logger'
+
+const logger = createLogger('SocketPermissions')
+
+export async function verifyWorkspaceMembership(
+ userId: string,
+ workspaceId: string
+): Promise {
+ try {
+ const membership = await db
+ .select({ role: workspaceMember.role })
+ .from(workspaceMember)
+ .where(and(eq(workspaceMember.workspaceId, workspaceId), eq(workspaceMember.userId, userId)))
+ .limit(1)
+
+ return membership.length > 0 ? membership[0].role : null
+ } catch (error) {
+ logger.error(`Error verifying workspace membership for ${userId} in ${workspaceId}:`, error)
+ return null
+ }
+}
+
+export async function verifyWorkflowAccess(
+ userId: string,
+ workflowId: string
+): Promise<{ hasAccess: boolean; role?: string; workspaceId?: string }> {
+ try {
+ const workflowData = await db
+ .select({
+ userId: workflow.userId,
+ workspaceId: workflow.workspaceId,
+ name: workflow.name,
+ })
+ .from(workflow)
+ .where(eq(workflow.id, workflowId))
+ .limit(1)
+
+ if (!workflowData.length) {
+ logger.warn(`Workflow ${workflowId} not found`)
+ return { hasAccess: false }
+ }
+
+ const { userId: workflowUserId, workspaceId, name: workflowName } = workflowData[0]
+
+ // Check if user owns the workflow
+ if (workflowUserId === userId) {
+ logger.debug(`User ${userId} has owner access to workflow ${workflowId} (${workflowName})`)
+ return { hasAccess: true, role: 'owner', workspaceId: workspaceId || undefined }
+ }
+
+ // Check workspace membership if workflow belongs to a workspace
+ if (workspaceId) {
+ const userRole = await verifyWorkspaceMembership(userId, workspaceId)
+ if (userRole) {
+ logger.debug(
+ `User ${userId} has ${userRole} access to workflow ${workflowId} via workspace ${workspaceId}`
+ )
+ return { hasAccess: true, role: userRole, workspaceId }
+ }
+ logger.warn(
+ `User ${userId} is not a member of workspace ${workspaceId} for workflow ${workflowId}`
+ )
+ return { hasAccess: false }
+ }
+
+ // Workflow doesn't belong to a workspace and user doesn't own it
+ logger.warn(`User ${userId} has no access to workflow ${workflowId} (no workspace, not owner)`)
+ return { hasAccess: false }
+ } catch (error) {
+ logger.error(
+ `Error verifying workflow access for user ${userId}, workflow ${workflowId}:`,
+ error
+ )
+ return { hasAccess: false }
+ }
+}
+
+// Enhanced authorization for specific operations
+export async function verifyOperationPermission(
+ userId: string,
+ workflowId: string,
+ operation: string,
+ target: string
+): Promise<{ allowed: boolean; reason?: string }> {
+ try {
+ const accessInfo = await verifyWorkflowAccess(userId, workflowId)
+
+ if (!accessInfo.hasAccess) {
+ return { allowed: false, reason: 'No access to workflow' }
+ }
+
+ // Define operation permissions based on role
+ const rolePermissions = {
+ owner: [
+ 'add',
+ 'remove',
+ 'update',
+ 'update-position',
+ 'update-name',
+ 'toggle-enabled',
+ 'update-parent',
+ 'update-wide',
+ 'update-advanced-mode',
+ 'duplicate',
+ ],
+ admin: [
+ 'add',
+ 'remove',
+ 'update',
+ 'update-position',
+ 'update-name',
+ 'toggle-enabled',
+ 'update-parent',
+ 'update-wide',
+ 'update-advanced-mode',
+ 'duplicate',
+ ],
+ member: [
+ 'add',
+ 'remove',
+ 'update',
+ 'update-position',
+ 'update-name',
+ 'toggle-enabled',
+ 'update-parent',
+ 'update-wide',
+ 'update-advanced-mode',
+ 'duplicate',
+ ],
+ viewer: ['update-position'], // Viewers can only move things around
+ }
+
+ const allowedOperations = rolePermissions[accessInfo.role as keyof typeof rolePermissions] || []
+
+ if (!allowedOperations.includes(operation)) {
+ return {
+ allowed: false,
+ reason: `Role '${accessInfo.role}' not permitted to perform '${operation}' on '${target}'`,
+ }
+ }
+
+ return { allowed: true }
+ } catch (error) {
+ logger.error(`Error verifying operation permission:`, error)
+ return { allowed: false, reason: 'Permission check failed' }
+ }
+}
diff --git a/apps/sim/socket-server/rooms/manager.ts b/apps/sim/socket-server/rooms/manager.ts
new file mode 100644
index 00000000000..c9a5efc3c3a
--- /dev/null
+++ b/apps/sim/socket-server/rooms/manager.ts
@@ -0,0 +1,183 @@
+import { and, eq, isNull } from 'drizzle-orm'
+import type { Server } from 'socket.io'
+import { db } from '../../db'
+import { workflowBlocks, workflowEdges } from '../../db/schema'
+import { createLogger } from '../../lib/logs/console-logger'
+
+const logger = createLogger('RoomManager')
+
+export interface UserPresence {
+ userId: string
+ workflowId: string
+ userName: string
+ socketId: string
+ joinedAt: number
+ lastActivity: number
+ cursor?: { x: number; y: number }
+ selection?: { type: 'block' | 'edge' | 'none'; id?: string }
+}
+
+export interface WorkflowRoom {
+ workflowId: string
+ users: Map // socketId -> UserPresence
+ lastModified: number
+ activeConnections: number
+}
+
+export class RoomManager {
+ private workflowRooms = new Map()
+ private socketToWorkflow = new Map()
+ private userSessions = new Map()
+ private io: Server
+
+ constructor(io: Server) {
+ this.io = io
+ }
+
+ createWorkflowRoom(workflowId: string): WorkflowRoom {
+ return {
+ workflowId,
+ users: new Map(),
+ lastModified: Date.now(),
+ activeConnections: 0,
+ }
+ }
+
+ cleanupUserFromRoom(socketId: string, workflowId: string) {
+ const room = this.workflowRooms.get(workflowId)
+ if (room) {
+ room.users.delete(socketId)
+ room.activeConnections = Math.max(0, room.activeConnections - 1)
+
+ if (room.activeConnections === 0) {
+ this.workflowRooms.delete(workflowId)
+ logger.info(`Cleaned up empty workflow room: ${workflowId}`)
+ }
+ }
+
+ this.socketToWorkflow.delete(socketId)
+ this.userSessions.delete(socketId)
+ }
+
+ // This would be used if we implement operation queuing
+ clearPendingOperations(socketId: string) {
+ logger.debug(`Cleared pending operations for socket ${socketId}`)
+ }
+
+ handleWorkflowDeletion(workflowId: string) {
+ logger.info(`Handling workflow deletion notification for ${workflowId}`)
+
+ const room = this.workflowRooms.get(workflowId)
+ if (!room) {
+ logger.debug(`No active room found for deleted workflow ${workflowId}`)
+ return
+ }
+
+ this.io.to(workflowId).emit('workflow-deleted', {
+ workflowId,
+ message: 'This workflow has been deleted',
+ timestamp: Date.now(),
+ })
+
+ const socketsToDisconnect: string[] = []
+ room.users.forEach((presence, socketId) => {
+ socketsToDisconnect.push(socketId)
+ })
+
+ socketsToDisconnect.forEach((socketId) => {
+ const socket = this.io.sockets.sockets.get(socketId)
+ if (socket) {
+ socket.leave(workflowId)
+ logger.debug(`Disconnected socket ${socketId} from deleted workflow ${workflowId}`)
+ }
+ this.cleanupUserFromRoom(socketId, workflowId)
+ })
+
+ this.workflowRooms.delete(workflowId)
+ logger.info(
+ `Cleaned up workflow room ${workflowId} after deletion (${socketsToDisconnect.length} users disconnected)`
+ )
+ }
+
+ async validateWorkflowConsistency(
+ workflowId: string
+ ): Promise<{ valid: boolean; issues: string[] }> {
+ try {
+ const issues: string[] = []
+
+ const orphanedEdges = await db
+ .select({
+ id: workflowEdges.id,
+ sourceBlockId: workflowEdges.sourceBlockId,
+ targetBlockId: workflowEdges.targetBlockId,
+ })
+ .from(workflowEdges)
+ .leftJoin(workflowBlocks, eq(workflowEdges.sourceBlockId, workflowBlocks.id))
+ .where(and(eq(workflowEdges.workflowId, workflowId), isNull(workflowBlocks.id)))
+
+ if (orphanedEdges.length > 0) {
+ issues.push(`Found ${orphanedEdges.length} orphaned edges with missing source blocks`)
+ }
+
+ return { valid: issues.length === 0, issues }
+ } catch (error) {
+ logger.error('Error validating workflow consistency:', error)
+ return { valid: false, issues: ['Consistency check failed'] }
+ }
+ }
+
+ getWorkflowRooms(): ReadonlyMap {
+ return this.workflowRooms
+ }
+
+ getSocketToWorkflow(): ReadonlyMap {
+ return this.socketToWorkflow
+ }
+
+ getUserSessions(): ReadonlyMap {
+ return this.userSessions
+ }
+
+ hasWorkflowRoom(workflowId: string): boolean {
+ return this.workflowRooms.has(workflowId)
+ }
+
+ getWorkflowRoom(workflowId: string): WorkflowRoom | undefined {
+ return this.workflowRooms.get(workflowId)
+ }
+
+ setWorkflowRoom(workflowId: string, room: WorkflowRoom): void {
+ this.workflowRooms.set(workflowId, room)
+ }
+
+ getWorkflowIdForSocket(socketId: string): string | undefined {
+ return this.socketToWorkflow.get(socketId)
+ }
+
+ setWorkflowForSocket(socketId: string, workflowId: string): void {
+ this.socketToWorkflow.set(socketId, workflowId)
+ }
+
+ getUserSession(socketId: string): { userId: string; userName: string } | undefined {
+ return this.userSessions.get(socketId)
+ }
+
+ setUserSession(socketId: string, session: { userId: string; userName: string }): void {
+ this.userSessions.set(socketId, session)
+ }
+
+ getTotalActiveConnections(): number {
+ return Array.from(this.workflowRooms.values()).reduce(
+ (total, room) => total + room.activeConnections,
+ 0
+ )
+ }
+
+ broadcastPresenceUpdate(workflowId: string): void {
+ const room = this.workflowRooms.get(workflowId)
+ if (room) {
+ const roomPresence = Array.from(room.users.values())
+ this.io.to(workflowId).emit('presence-update', roomPresence)
+ }
+ }
+}
diff --git a/apps/sim/socket-server/routes/http.ts b/apps/sim/socket-server/routes/http.ts
new file mode 100644
index 00000000000..10dc275057a
--- /dev/null
+++ b/apps/sim/socket-server/routes/http.ts
@@ -0,0 +1,56 @@
+import type { IncomingMessage, ServerResponse } from 'http'
+import type { RoomManager } from '../rooms/manager'
+
+interface Logger {
+ info: (message: string, ...args: any[]) => void
+ error: (message: string, ...args: any[]) => void
+ debug: (message: string, ...args: any[]) => void
+ warn: (message: string, ...args: any[]) => void
+}
+
+/**
+ * Creates an HTTP request handler for the socket server
+ * @param roomManager - RoomManager instance for managing workflow rooms and state
+ * @param logger - Logger instance for logging requests and errors
+ * @returns HTTP request handler function
+ */
+export function createHttpHandler(roomManager: RoomManager, logger: Logger) {
+ return (req: IncomingMessage, res: ServerResponse) => {
+ // Handle health check for Railway
+ if (req.method === 'GET' && req.url === '/health') {
+ res.writeHead(200, { 'Content-Type': 'application/json' })
+ res.end(
+ JSON.stringify({
+ status: 'ok',
+ timestamp: new Date().toISOString(),
+ connections: roomManager.getTotalActiveConnections(),
+ })
+ )
+ return
+ }
+
+ // Handle workflow deletion notifications from the main API
+ if (req.method === 'POST' && req.url === '/api/workflow-deleted') {
+ let body = ''
+ req.on('data', (chunk) => {
+ body += chunk.toString()
+ })
+ req.on('end', () => {
+ try {
+ const { workflowId } = JSON.parse(body)
+ roomManager.handleWorkflowDeletion(workflowId)
+ res.writeHead(200, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ success: true }))
+ } catch (error) {
+ logger.error('Error handling workflow deletion notification:', error)
+ res.writeHead(500, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: 'Failed to process deletion notification' }))
+ }
+ })
+ return
+ }
+
+ res.writeHead(404, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: 'Not found' }))
+ }
+}
diff --git a/apps/sim/socket-server/validation/schemas.ts b/apps/sim/socket-server/validation/schemas.ts
new file mode 100644
index 00000000000..3c4d713d245
--- /dev/null
+++ b/apps/sim/socket-server/validation/schemas.ts
@@ -0,0 +1,70 @@
+import { z } from 'zod'
+
+const PositionSchema = z.object({
+ x: z.number(),
+ y: z.number(),
+})
+
+export const BlockOperationSchema = z.object({
+ operation: z.enum([
+ 'add',
+ 'remove',
+ 'update-position',
+ 'update-name',
+ 'toggle-enabled',
+ 'update-parent',
+ 'update-wide',
+ 'update-advanced-mode',
+ 'duplicate',
+ ]),
+ target: z.literal('block'),
+ payload: z.object({
+ id: z.string(),
+ type: z.string().optional(),
+ name: z.string().optional(),
+ position: PositionSchema.optional(),
+ data: z.record(z.any()).optional(),
+ subBlocks: z.record(z.any()).optional(),
+ outputs: z.record(z.any()).optional(),
+ parentId: z.string().optional(),
+ extent: z.enum(['parent']).optional(),
+ enabled: z.boolean().optional(),
+ horizontalHandles: z.boolean().optional(),
+ isWide: z.boolean().optional(),
+ advancedMode: z.boolean().optional(),
+ height: z.number().optional(),
+ }),
+ timestamp: z.number(),
+})
+
+export const EdgeOperationSchema = z.object({
+ operation: z.enum(['add', 'remove']),
+ target: z.literal('edge'),
+ payload: z.object({
+ id: z.string(),
+ source: z.string().optional(),
+ target: z.string().optional(),
+ sourceHandle: z.string().nullable().optional(),
+ targetHandle: z.string().nullable().optional(),
+ }),
+ timestamp: z.number(),
+})
+
+export const SubflowOperationSchema = z.object({
+ operation: z.enum(['add', 'remove', 'update']),
+ target: z.literal('subflow'),
+ payload: z.object({
+ id: z.string(),
+ type: z.enum(['loop', 'parallel']).optional(),
+ config: z.record(z.any()).optional(),
+ }),
+ timestamp: z.number(),
+})
+
+export const WorkflowOperationSchema = z.union([
+ BlockOperationSchema,
+ EdgeOperationSchema,
+ SubflowOperationSchema,
+])
+
+export { PositionSchema }
diff --git a/apps/sim/stores/folders/store.ts b/apps/sim/stores/folders/store.ts
index efa25f2ef79..57ad937eb4f 100644
--- a/apps/sim/stores/folders/store.ts
+++ b/apps/sim/stores/folders/store.ts
@@ -71,7 +71,7 @@ interface FolderState {
color?: string
}) => Promise
updateFolderAPI: (id: string, updates: Partial) => Promise
- deleteFolder: (id: string) => Promise
+ deleteFolder: (id: string, workspaceId: string) => Promise
// Helper functions
isWorkflowInDeletedSubfolder: (workflow: Workflow, deletedFolderId: string) => boolean
@@ -304,7 +304,7 @@ export const useFolderStore = create()(
return processedFolder
},
- deleteFolder: async (id: string) => {
+ deleteFolder: async (id: string, workspaceId: string) => {
const response = await fetch(`/api/folders/${id}`, { method: 'DELETE' })
if (!response.ok) {
@@ -346,9 +346,9 @@ export const useFolderStore = create()(
}
}
- if (workflowRegistry.activeWorkspaceId) {
+ if (workspaceId) {
// Trigger workflow refresh through registry store
- await workflowRegistry.switchToWorkspace(workflowRegistry.activeWorkspaceId)
+ await workflowRegistry.switchToWorkspace(workspaceId)
}
},
diff --git a/apps/sim/stores/index.ts b/apps/sim/stores/index.ts
index 347685dce12..8c265e03452 100644
--- a/apps/sim/stores/index.ts
+++ b/apps/sim/stores/index.ts
@@ -9,7 +9,6 @@ import { useNotificationStore } from './notifications/store'
import { useConsoleStore } from './panel/console/store'
import { useVariablesStore } from './panel/variables/store'
import { useEnvironmentStore } from './settings/environment/store'
-// Removed sync system imports - Socket.IO handles real-time sync
import { useWorkflowRegistry } from './workflows/registry/store'
import { useSubBlockStore } from './workflows/subblock/store'
import { useWorkflowStore } from './workflows/workflow/store'
@@ -41,12 +40,6 @@ async function initializeApplication(): Promise {
// Load custom tools from server
await useCustomToolsStore.getState().loadCustomTools()
- // Extract workflow ID from URL for smart workspace selection
- const workflowIdFromUrl = extractWorkflowIdFromUrl()
-
- // Load workspace based on workflow ID in URL, with fallback to last active workspace
- await useWorkflowRegistry.getState().loadWorkspaceFromWorkflowId(workflowIdFromUrl)
-
// Load workflows from database (replaced sync system)
await useWorkflowRegistry.getState().loadWorkflows()
diff --git a/apps/sim/stores/workflows/index.ts b/apps/sim/stores/workflows/index.ts
index 161957de7c7..5fe909c51f6 100644
--- a/apps/sim/stores/workflows/index.ts
+++ b/apps/sim/stores/workflows/index.ts
@@ -88,7 +88,7 @@ export function getBlockWithValues(blockId: string): BlockState | null {
* @returns An object containing workflows, with state only for the active workflow
*/
export function getAllWorkflowsWithValues() {
- const { workflows, activeWorkspaceId } = useWorkflowRegistry.getState()
+ const { workflows } = useWorkflowRegistry.getState()
const result: Record = {}
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
const currentState = useWorkflowStore.getState()
@@ -97,14 +97,6 @@ export function getAllWorkflowsWithValues() {
if (activeWorkflowId && workflows[activeWorkflowId]) {
const metadata = workflows[activeWorkflowId]
- // Skip if workflow doesn't belong to the active workspace
- if (activeWorkspaceId && metadata.workspaceId !== activeWorkspaceId) {
- logger.debug(
- `Skipping active workflow ${activeWorkflowId} - belongs to workspace ${metadata.workspaceId}, not active workspace ${activeWorkspaceId}`
- )
- return result
- }
-
// Get deployment status from registry
const deploymentStatus = useWorkflowRegistry
.getState()
@@ -157,17 +149,10 @@ export function getAllWorkflowsWithValues() {
return result
}
-// Removed syncWorkflows - Socket.IO handles real-time sync automatically
-
-// Workflows store exports - localStorage persistence removed
-
export { useWorkflowRegistry } from './registry/store'
export type { WorkflowMetadata } from './registry/types'
export { useSubBlockStore } from './subblock/store'
export type { SubBlockStore } from './subblock/types'
-// Re-export utilities
export { mergeSubblockState } from './utils'
-// Re-export store hooks
export { useWorkflowStore } from './workflow/store'
-// Re-export types
export type { WorkflowState } from './workflow/types'
diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts
index 0ce08f959d5..4968c2dac29 100644
--- a/apps/sim/stores/workflows/registry/store.ts
+++ b/apps/sim/stores/workflows/registry/store.ts
@@ -4,7 +4,6 @@ import { createLogger } from '@/lib/logs/console-logger'
import { clearWorkflowVariablesTracking } from '@/stores/panel/variables/store'
import { API_ENDPOINTS } from '../../constants'
import { useSubBlockStore } from '../subblock/store'
-// Removed fetchWorkflowsFromDB import - moved to local function
import { useWorkflowStore } from '../workflow/store'
import type { BlockState } from '../workflow/types'
import type { DeploymentStatus, WorkflowMetadata, WorkflowRegistry } from './types'
@@ -12,11 +11,10 @@ import { generateUniqueName, getNextWorkflowColor } from './utils'
const logger = createLogger('WorkflowRegistry')
-// Simplified function to fetch workflows from DB (moved from sync.ts)
let isFetching = false
let lastFetchTimestamp = 0
-async function fetchWorkflowsFromDB(): Promise {
+async function fetchWorkflowsFromDB(workspaceId?: string): Promise {
if (typeof window === 'undefined') return
// Prevent concurrent fetch operations
@@ -31,11 +29,10 @@ async function fetchWorkflowsFromDB(): Promise {
try {
useWorkflowRegistry.getState().setLoading(true)
- const activeWorkspaceId = useWorkflowRegistry.getState().activeWorkspaceId
const url = new URL(API_ENDPOINTS.SYNC, window.location.origin)
- if (activeWorkspaceId) {
- url.searchParams.append('workspaceId', activeWorkspaceId)
+ if (workspaceId) {
+ url.searchParams.append('workspaceId', workspaceId)
}
const response = await fetch(url.toString(), { method: 'GET' })
@@ -99,10 +96,7 @@ async function fetchWorkflowsFromDB(): Promise {
apiKey,
} = workflow
- // Skip if workflow doesn't belong to active workspace
- if (activeWorkspaceId && workspaceId !== activeWorkspaceId) {
- return
- }
+ // No need to filter by workspace since we're already fetching for specific workspace
// Add to registry
registryWorkflows[id] = {
@@ -256,7 +250,6 @@ export const useWorkflowRegistry = create()(
// Store state
workflows: {},
activeWorkflowId: null,
- activeWorkspaceId: null, // No longer persisted in localStorage
isLoading: true,
error: null,
// Initialize deployment statuses
@@ -270,24 +263,17 @@ export const useWorkflowRegistry = create()(
},
// Simple method to load workflows (replaces sync system)
- loadWorkflows: async () => {
- await fetchWorkflowsFromDB()
+ loadWorkflows: async (workspaceId?: string) => {
+ await fetchWorkflowsFromDB(workspaceId)
},
// Handle cleanup on workspace deletion
handleWorkspaceDeletion: async (newWorkspaceId: string) => {
- const currentWorkspaceId = get().activeWorkspaceId
-
- if (!newWorkspaceId || newWorkspaceId === currentWorkspaceId) {
- logger.error('Cannot switch to invalid workspace after deletion')
- return
- }
-
// Set transition state
setWorkspaceTransitioning(true)
try {
- logger.info(`Switching from deleted workspace ${currentWorkspaceId} to ${newWorkspaceId}`)
+ logger.info(`Switching to new workspace after deletion: ${newWorkspaceId}`)
// Reset all workflow state
resetWorkflowStores()
@@ -296,12 +282,11 @@ export const useWorkflowRegistry = create()(
set({
isLoading: true,
workflows: {},
- activeWorkspaceId: newWorkspaceId,
activeWorkflowId: null,
})
// Properly await workflow fetching to prevent race conditions
- await fetchWorkflowsFromDB()
+ await fetchWorkflowsFromDB(newWorkspaceId)
set({ isLoading: false })
logger.info(`Successfully switched to workspace after deletion: ${newWorkspaceId}`)
@@ -327,29 +312,17 @@ export const useWorkflowRegistry = create()(
return
}
- const { activeWorkspaceId: currentWorkspaceId } = get()
-
- // Early return if switching to the same workspace (before setting flag)
- if (currentWorkspaceId === workspaceId) {
- logger.info(`Already in workspace ${workspaceId}`)
- return
- }
-
- // Only set transition flag AFTER validating the switch is needed
+ // Set transition flag
setWorkspaceTransitioning(true)
try {
- logger.info(`Switching workspace from ${currentWorkspaceId || 'none'} to ${workspaceId}`)
-
- // Save to localStorage first before any async operations
- get().setActiveWorkspaceId(workspaceId)
+ logger.info(`Switching to workspace: ${workspaceId}`)
// Clear current workspace state
resetWorkflowStores()
- // Update workspace in state
+ // Update state
set({
- activeWorkspaceId: workspaceId,
activeWorkflowId: null,
workflows: {},
isLoading: true,
@@ -357,7 +330,7 @@ export const useWorkflowRegistry = create()(
})
// Fetch workflows for the new workspace
- await fetchWorkflowsFromDB()
+ await fetchWorkflowsFromDB(workspaceId)
logger.info(`Successfully switched to workspace: ${workspaceId}`)
} catch (error) {
@@ -371,128 +344,6 @@ export const useWorkflowRegistry = create()(
}
},
- // Load user's last active workspace from localStorage
- loadLastActiveWorkspace: async () => {
- try {
- const savedWorkspaceId = localStorage.getItem('lastActiveWorkspaceId')
- if (!savedWorkspaceId || savedWorkspaceId === get().activeWorkspaceId) {
- return // No saved workspace or already active
- }
-
- logger.info(`Attempting to restore last active workspace: ${savedWorkspaceId}`)
-
- // Validate that the workspace exists by making a simple API call
- try {
- const response = await fetch('/api/workspaces')
- if (response.ok) {
- const data = await response.json()
- const workspaces = data.workspaces || []
- const workspaceExists = workspaces.some((ws: any) => ws.id === savedWorkspaceId)
-
- if (workspaceExists) {
- // Set the validated workspace ID
- set({ activeWorkspaceId: savedWorkspaceId })
- logger.info(`Restored last active workspace from localStorage: ${savedWorkspaceId}`)
- } else {
- logger.warn(
- `Saved workspace ${savedWorkspaceId} no longer exists, clearing from localStorage`
- )
- localStorage.removeItem('lastActiveWorkspaceId')
- }
- }
- } catch (apiError) {
- logger.warn('Failed to validate saved workspace, will use default:', apiError)
- // Don't remove from localStorage in case it's a temporary network issue
- }
- } catch (error) {
- logger.warn('Failed to load last active workspace from localStorage:', error)
- // This is non-critical, so we continue with default behavior
- }
- },
-
- // Load workspace based on workflow ID from URL, with fallback to last active workspace
- loadWorkspaceFromWorkflowId: async (workflowId: string | null) => {
- try {
- logger.info(`Loading workspace for workflow ID: ${workflowId}`)
-
- // If workflow ID provided, try to get its workspace
- if (workflowId) {
- try {
- const response = await fetch(`/api/workflows/${workflowId}`)
- if (response.ok) {
- const data = await response.json()
- const workflow = data.data
-
- if (workflow?.workspaceId) {
- // Validate workspace access
- const workspacesResponse = await fetch('/api/workspaces')
- if (workspacesResponse.ok) {
- const workspacesData = await workspacesResponse.json()
- const workspaces = workspacesData.workspaces || []
- const workspaceExists = workspaces.some(
- (ws: any) => ws.id === workflow.workspaceId
- )
-
- if (workspaceExists) {
- set({ activeWorkspaceId: workflow.workspaceId })
- localStorage.setItem('lastActiveWorkspaceId', workflow.workspaceId)
- logger.info(`Set active workspace from workflow: ${workflow.workspaceId}`)
- return
- }
- }
- }
- }
- } catch (error) {
- logger.warn('Error fetching workflow:', error)
- }
- }
-
- // Fallback: use last active workspace or first available
- const savedWorkspaceId = localStorage.getItem('lastActiveWorkspaceId')
- const response = await fetch('/api/workspaces')
-
- if (response.ok) {
- const data = await response.json()
- const workspaces = data.workspaces || []
-
- if (workspaces.length === 0) {
- logger.warn('No workspaces found')
- return
- }
-
- // Try saved workspace first
- let targetWorkspace = savedWorkspaceId
- ? workspaces.find((ws: any) => ws.id === savedWorkspaceId)
- : null
-
- // Fall back to first workspace
- if (!targetWorkspace) {
- targetWorkspace = workspaces[0]
- if (savedWorkspaceId) {
- localStorage.removeItem('lastActiveWorkspaceId')
- }
- }
-
- set({ activeWorkspaceId: targetWorkspace.id })
- localStorage.setItem('lastActiveWorkspaceId', targetWorkspace.id)
- logger.info(`Set active workspace: ${targetWorkspace.id}`)
- }
- } catch (error) {
- logger.error('Error in loadWorkspaceFromWorkflowId:', error)
- }
- },
-
- // Simple method to set active workspace ID without triggering full switch
- setActiveWorkspaceId: (id: string) => {
- set({ activeWorkspaceId: id })
- // Save to localStorage as well
- try {
- localStorage.setItem('lastActiveWorkspaceId', id)
- } catch (error) {
- logger.warn('Failed to save workspace to localStorage:', error)
- }
- },
-
// Method to get deployment status for a specific workflow
getWorkflowDeploymentStatus: (workflowId: string | null): DeploymentStatus | null => {
if (!workflowId) {
@@ -735,14 +586,19 @@ export const useWorkflowRegistry = create()(
* @returns The ID of the newly created workflow
*/
createWorkflow: async (options = {}) => {
- const { workflows, activeWorkspaceId } = get()
+ const { workflows } = get()
const id = crypto.randomUUID()
- // Use provided workspace ID or fall back to active workspace ID
- const workspaceId = options.workspaceId || activeWorkspaceId || undefined
+ // Use provided workspace ID (must be provided since we no longer track active workspace)
+ const workspaceId = options.workspaceId
- logger.info(`Creating new workflow in workspace: ${workspaceId || 'none'}`)
+ if (!workspaceId) {
+ logger.error('Cannot create workflow without workspaceId')
+ set({ error: 'Workspace ID is required to create a workflow' })
+ throw new Error('Workspace ID is required to create a workflow')
+ }
+ logger.info(`Creating new workflow in workspace: ${workspaceId || 'none'}`)
// Generate workflow metadata with appropriate name and color
const newWorkflow: WorkflowMetadata = {
id,
@@ -1153,7 +1009,7 @@ export const useWorkflowRegistry = create()(
* Duplicates an existing workflow
*/
duplicateWorkflow: async (sourceId: string) => {
- const { workflows, activeWorkspaceId } = get()
+ const { workflows } = get()
const sourceWorkflow = workflows[sourceId]
if (!sourceWorkflow) {
@@ -1161,8 +1017,8 @@ export const useWorkflowRegistry = create()(
return null
}
- // Get the workspace ID from the source workflow or fall back to active workspace
- const workspaceId = sourceWorkflow.workspaceId || activeWorkspaceId || undefined
+ // Get the workspace ID from the source workflow (required)
+ const workspaceId = sourceWorkflow.workspaceId
// Call the server to duplicate the workflow - server generates all IDs
let duplicatedWorkflow
@@ -1594,7 +1450,6 @@ export const useWorkflowRegistry = create()(
set({
workflows: {},
activeWorkflowId: null,
- activeWorkspaceId: null,
isLoading: true,
error: null,
})
diff --git a/apps/sim/stores/workflows/registry/types.ts b/apps/sim/stores/workflows/registry/types.ts
index efc854b835b..5e9234b28bb 100644
--- a/apps/sim/stores/workflows/registry/types.ts
+++ b/apps/sim/stores/workflows/registry/types.ts
@@ -24,7 +24,6 @@ export interface WorkflowMetadata {
export interface WorkflowRegistryState {
workflows: Record
activeWorkflowId: string | null
- activeWorkspaceId: string | null
isLoading: boolean
error: string | null
deploymentStatuses: Record
@@ -34,10 +33,7 @@ export interface WorkflowRegistryActions {
setLoading: (loading: boolean) => void
setActiveWorkflow: (id: string) => Promise
switchToWorkspace: (id: string) => void
- setActiveWorkspaceId: (id: string) => void
- loadLastActiveWorkspace: () => Promise
- loadWorkspaceFromWorkflowId: (workflowId: string | null) => Promise
- loadWorkflows: () => Promise
+ loadWorkflows: (workspaceId?: string) => Promise
handleWorkspaceDeletion: (newWorkspaceId: string) => void
removeWorkflow: (id: string) => Promise
updateWorkflow: (id: string, metadata: Partial) => Promise
diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts
index 9a7ff740648..98946151498 100644
--- a/apps/sim/stores/workflows/workflow/store.ts
+++ b/apps/sim/stores/workflows/workflow/store.ts
@@ -581,6 +581,7 @@ export const useWorkflowStore = create()(
// workflowValues: {[block_id]:{[subblock_id]:[subblock_value]}}
const workflowValues = subBlockStore.workflowValues[activeWorkflowId] || {}
const updatedWorkflowValues = { ...workflowValues }
+ const changedSubblocks: Array<{ blockId: string; subBlockId: string; newValue: any }> = []
// Loop through blocks
Object.entries(workflowValues).forEach(([blockId, blockValues]) => {
@@ -593,11 +594,17 @@ export const useWorkflowStore = create()(
const regex = new RegExp(`<${oldBlockName}\\.`, 'g')
// Use a recursive function to handle all object types
- updatedWorkflowValues[blockId][subBlockId] = updateReferences(
- value,
- regex,
- `<${newBlockName}.`
- )
+ const updatedValue = updateReferences(value, regex, `<${newBlockName}.`)
+
+ // Check if the value actually changed
+ if (JSON.stringify(updatedValue) !== JSON.stringify(value)) {
+ updatedWorkflowValues[blockId][subBlockId] = updatedValue
+ changedSubblocks.push({
+ blockId,
+ subBlockId,
+ newValue: updatedValue,
+ })
+ }
// Helper function to recursively update references in any data structure
function updateReferences(value: any, regex: RegExp, replacement: string): any {
@@ -633,6 +640,12 @@ export const useWorkflowStore = create()(
[activeWorkflowId]: updatedWorkflowValues,
},
})
+
+ // Store changed subblocks for collaborative sync
+ if (changedSubblocks.length > 0) {
+ // Store the changed subblocks for the collaborative function to pick up
+ ;(window as any).__pendingSubblockUpdates = changedSubblocks
+ }
}
set(newState)
@@ -657,6 +670,38 @@ export const useWorkflowStore = create()(
// Note: Socket.IO handles real-time sync automatically
},
+ setBlockWide: (id: string, isWide: boolean) => {
+ set((state) => ({
+ blocks: {
+ ...state.blocks,
+ [id]: {
+ ...state.blocks[id],
+ isWide,
+ },
+ },
+ edges: [...state.edges],
+ loops: { ...state.loops },
+ }))
+ get().updateLastSaved()
+ // Note: Socket.IO handles real-time sync automatically
+ },
+
+ setBlockAdvancedMode: (id: string, advancedMode: boolean) => {
+ set((state) => ({
+ blocks: {
+ ...state.blocks,
+ [id]: {
+ ...state.blocks[id],
+ advancedMode,
+ },
+ },
+ edges: [...state.edges],
+ loops: { ...state.loops },
+ }))
+ get().updateLastSaved()
+ // Note: Socket.IO handles real-time sync automatically
+ },
+
updateBlockHeight: (id: string, height: number) => {
set((state) => ({
blocks: {
diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts
index 2baf5a1fa07..65ef3624515 100644
--- a/apps/sim/stores/workflows/workflow/types.ts
+++ b/apps/sim/stores/workflows/workflow/types.ts
@@ -2,24 +2,17 @@ import type { Edge } from 'reactflow'
import type { BlockOutput, SubBlockType } from '@/blocks/types'
import type { DeploymentStatus } from '../registry/types'
-// Centralized subflow type system - easy to extend without database changes
export const SUBFLOW_TYPES = {
LOOP: 'loop',
PARALLEL: 'parallel',
- // Future types can be added here:
- // CONDITIONAL: 'conditional',
- // RETRY: 'retry',
- // BATCH: 'batch',
} as const
export type SubflowType = (typeof SUBFLOW_TYPES)[keyof typeof SUBFLOW_TYPES]
-// Type guard for runtime validation
export function isValidSubflowType(type: string): type is SubflowType {
return Object.values(SUBFLOW_TYPES).includes(type as SubflowType)
}
-// Subflow configuration interfaces
export interface LoopConfig {
nodes: string[]
iterations: number
@@ -184,6 +177,7 @@ export interface WorkflowActions {
toggleBlockHandles: (id: string) => void
updateBlockName: (id: string, name: string) => void
toggleBlockWide: (id: string) => void
+ setBlockWide: (id: string, isWide: boolean) => void
updateBlockHeight: (id: string, height: number) => void
triggerUpdate: () => void
updateLoopCount: (loopId: string, count: number) => void
diff --git a/apps/sim/test-socket-integration.html b/apps/sim/test-socket-integration.html
deleted file mode 100644
index 61af75221a5..00000000000
--- a/apps/sim/test-socket-integration.html
+++ /dev/null
@@ -1,275 +0,0 @@
-
-
-
-
-
- Socket Integration Test
-
-
-
-
-
-
Socket.IO Collaborative Workflow Test
-
-
- Disconnected
-
-
-
-
Presence Users:
-
None
-
-
-
-
Test Workflow Operations:
-
- Join Workflow
- Leave Workflow
-
-
-
-
Block Operations:
- Add Test Block
- Remove Test Block
- Update Block Position
-
-
-
-
Edge Operations:
- Add Test Edge
- Remove Test Edge
-
-
-
-
-
-
-
-
diff --git a/apps/sim/vitest.setup.ts b/apps/sim/vitest.setup.ts
index 426a0093027..e0d2679eb41 100644
--- a/apps/sim/vitest.setup.ts
+++ b/apps/sim/vitest.setup.ts
@@ -40,6 +40,17 @@ vi.mock('@/stores/execution/store', () => ({
},
}))
+vi.mock('@/blocks/registry', () => ({
+ getBlock: vi.fn(() => ({
+ name: 'Mock Block',
+ description: 'Mock block description',
+ icon: () => null,
+ subBlocks: [],
+ outputs: {},
+ })),
+ getAllBlocks: vi.fn(() => ({})),
+}))
+
const originalConsoleError = console.error
const originalConsoleWarn = console.warn