mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b9019d9a2 | ||
|
|
6d00d6bf2c | ||
|
|
3267d8cc24 | ||
|
|
2e69f85364 | ||
|
|
57e5bac121 | ||
|
|
8ce0299400 | ||
|
|
a0796f088b | ||
|
|
98fe4cd40b | ||
|
|
34d210c66c | ||
|
|
2334f2dca4 | ||
|
|
65fc138bfc |
@@ -10,7 +10,7 @@
|
||||
* @see stores/constants.ts for the source of truth
|
||||
*/
|
||||
:root {
|
||||
--sidebar-width: 248px; /* SIDEBAR_WIDTH.DEFAULT */
|
||||
--sidebar-width: 0px; /* 0 outside workspace; blocking script always sets actual value on workspace pages */
|
||||
--panel-width: 320px; /* PANEL_WIDTH.DEFAULT */
|
||||
--toolbar-triggers-height: 300px; /* TOOLBAR_TRIGGERS_HEIGHT.DEFAULT */
|
||||
--editor-connections-height: 172px; /* EDITOR_CONNECTIONS_HEIGHT.DEFAULT */
|
||||
|
||||
@@ -304,7 +304,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isDeployed: true,
|
||||
deploymentStatuses: { production: 'deployed' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -349,7 +348,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isDeployed: true,
|
||||
deploymentStatuses: { production: 'deployed' },
|
||||
lastSaved: 1640995200000,
|
||||
},
|
||||
},
|
||||
@@ -370,7 +368,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isDeployed: true,
|
||||
deploymentStatuses: { production: 'deployed' },
|
||||
lastSaved: 1640995200000,
|
||||
}),
|
||||
}
|
||||
@@ -473,7 +470,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
|
||||
edges: undefined,
|
||||
loops: null,
|
||||
parallels: undefined,
|
||||
deploymentStatuses: null,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -508,7 +504,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isDeployed: false,
|
||||
deploymentStatuses: {},
|
||||
lastSaved: 1640995200000,
|
||||
})
|
||||
})
|
||||
@@ -768,10 +763,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
|
||||
parallel1: { branches: ['branch1', 'branch2'] },
|
||||
},
|
||||
isDeployed: true,
|
||||
deploymentStatuses: {
|
||||
production: 'deployed',
|
||||
staging: 'pending',
|
||||
},
|
||||
deployedAt: '2024-01-01T10:00:00.000Z',
|
||||
},
|
||||
}
|
||||
@@ -816,10 +807,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
|
||||
parallel1: { branches: ['branch1', 'branch2'] },
|
||||
},
|
||||
isDeployed: true,
|
||||
deploymentStatuses: {
|
||||
production: 'deployed',
|
||||
staging: 'pending',
|
||||
},
|
||||
deployedAt: '2024-01-01T10:00:00.000Z',
|
||||
lastSaved: 1640995200000,
|
||||
})
|
||||
|
||||
@@ -82,7 +82,6 @@ export async function POST(request: NextRequest) {
|
||||
loops: checkpointState?.loops || {},
|
||||
parallels: checkpointState?.parallels || {},
|
||||
isDeployed: checkpointState?.isDeployed || false,
|
||||
deploymentStatuses: checkpointState?.deploymentStatuses || {},
|
||||
lastSaved: Date.now(),
|
||||
...(checkpointState?.deployedAt &&
|
||||
checkpointState.deployedAt !== null &&
|
||||
|
||||
@@ -79,7 +79,6 @@ export async function POST(
|
||||
loops: deployedState.loops || {},
|
||||
parallels: deployedState.parallels || {},
|
||||
lastSaved: Date.now(),
|
||||
deploymentStatuses: deployedState.deploymentStatuses || {},
|
||||
})
|
||||
|
||||
if (!saveResult.success) {
|
||||
|
||||
@@ -89,7 +89,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
const finalWorkflowData = {
|
||||
...workflowData,
|
||||
state: {
|
||||
deploymentStatuses: {},
|
||||
blocks: normalizedData.blocks,
|
||||
edges: normalizedData.edges,
|
||||
loops: normalizedData.loops,
|
||||
@@ -115,7 +114,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
const emptyWorkflowData = {
|
||||
...workflowData,
|
||||
state: {
|
||||
deploymentStatuses: {},
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
||||
import type { Variable } from '@/stores/panel/variables/types'
|
||||
import type { Variable } from '@/stores/variables/types'
|
||||
|
||||
const logger = createLogger('WorkflowVariablesAPI')
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
}
|
||||
|
||||
// Sidebar width
|
||||
var defaultSidebarWidth = '248px';
|
||||
try {
|
||||
var stored = localStorage.getItem('sidebar-state');
|
||||
if (stored) {
|
||||
@@ -108,11 +109,15 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
document.documentElement.style.setProperty('--sidebar-width', width + 'px');
|
||||
} else if (width > maxSidebarWidth) {
|
||||
document.documentElement.style.setProperty('--sidebar-width', maxSidebarWidth + 'px');
|
||||
} else {
|
||||
document.documentElement.style.setProperty('--sidebar-width', defaultSidebarWidth);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
document.documentElement.style.setProperty('--sidebar-width', defaultSidebarWidth);
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback handled by CSS defaults
|
||||
document.documentElement.style.setProperty('--sidebar-width', defaultSidebarWidth);
|
||||
}
|
||||
|
||||
// Panel width and active tab
|
||||
|
||||
@@ -108,8 +108,6 @@ function normalizeWorkflowState(input?: any): WorkflowState | null {
|
||||
lastUpdate: input.lastUpdate,
|
||||
metadata: input.metadata,
|
||||
variables: input.variables,
|
||||
deploymentStatuses: input.deploymentStatuses,
|
||||
needsRedeployment: input.needsRedeployment,
|
||||
dragStartPosition: input.dragStartPosition ?? null,
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from '@/components/emcn'
|
||||
import { client, useSession } from '@/lib/auth/auth-client'
|
||||
import type { OAuthReturnContext } from '@/lib/credentials/client-state'
|
||||
import { writeOAuthReturnContext } from '@/lib/credentials/client-state'
|
||||
import { ADD_CONNECTOR_SEARCH_PARAM, writeOAuthReturnContext } from '@/lib/credentials/client-state'
|
||||
import {
|
||||
getCanonicalScopesForProvider,
|
||||
getProviderIdFromServiceId,
|
||||
@@ -59,8 +59,8 @@ type OAuthModalConnectProps = OAuthModalBaseProps & {
|
||||
workspaceId: string
|
||||
credentialCount: number
|
||||
} & (
|
||||
| { workflowId: string; knowledgeBaseId?: never }
|
||||
| { workflowId?: never; knowledgeBaseId: string }
|
||||
| { workflowId: string; knowledgeBaseId?: never; connectorType?: never }
|
||||
| { workflowId?: never; knowledgeBaseId: string; connectorType?: string }
|
||||
)
|
||||
|
||||
interface OAuthModalReauthorizeProps extends OAuthModalBaseProps {
|
||||
@@ -81,6 +81,7 @@ export function OAuthModal(props: OAuthModalProps) {
|
||||
const workspaceId = isConnect ? props.workspaceId : ''
|
||||
const workflowId = isConnect ? props.workflowId : undefined
|
||||
const knowledgeBaseId = isConnect ? props.knowledgeBaseId : undefined
|
||||
const connectorType = isConnect ? props.connectorType : undefined
|
||||
const toolName = !isConnect ? props.toolName : ''
|
||||
const requiredScopes = !isConnect ? (props.requiredScopes ?? EMPTY_SCOPES) : EMPTY_SCOPES
|
||||
const newScopes = !isConnect ? (props.newScopes ?? EMPTY_SCOPES) : EMPTY_SCOPES
|
||||
@@ -172,7 +173,7 @@ export function OAuthModal(props: OAuthModalProps) {
|
||||
}
|
||||
|
||||
const returnContext: OAuthReturnContext = knowledgeBaseId
|
||||
? { ...baseContext, origin: 'kb-connectors' as const, knowledgeBaseId }
|
||||
? { ...baseContext, origin: 'kb-connectors' as const, knowledgeBaseId, connectorType }
|
||||
: { ...baseContext, origin: 'workflow' as const, workflowId: workflowId! }
|
||||
|
||||
writeOAuthReturnContext(returnContext)
|
||||
@@ -205,7 +206,11 @@ export function OAuthModal(props: OAuthModalProps) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.oauth2.link({ providerId, callbackURL: window.location.href })
|
||||
const callbackURL = new URL(window.location.href)
|
||||
if (connectorType) {
|
||||
callbackURL.searchParams.set(ADD_CONNECTOR_SEARCH_PARAM, connectorType)
|
||||
}
|
||||
await client.oauth2.link({ providerId, callbackURL: callbackURL.toString() })
|
||||
handleClose()
|
||||
} catch (err) {
|
||||
logger.error('Failed to initiate OAuth connection', { error: err })
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
export default function FileViewLoading() {
|
||||
return (
|
||||
<div className='fixed inset-0 z-50 flex items-center justify-center bg-[var(--bg)]'>
|
||||
<Loader2 className='h-[20px] w-[20px] animate-spin text-[var(--text-tertiary)]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1407,17 +1407,6 @@ export function useChat(
|
||||
const output = tc.result?.output as Record<string, unknown> | undefined
|
||||
const deployedWorkflowId = (output?.workflowId as string) ?? undefined
|
||||
if (deployedWorkflowId && typeof output?.isDeployed === 'boolean') {
|
||||
const isDeployed = output.isDeployed as boolean
|
||||
const serverDeployedAt = output.deployedAt
|
||||
? new Date(output.deployedAt as string)
|
||||
: undefined
|
||||
useWorkflowRegistry
|
||||
.getState()
|
||||
.setDeploymentStatus(
|
||||
deployedWorkflowId,
|
||||
isDeployed,
|
||||
isDeployed ? (serverDeployedAt ?? new Date()) : undefined
|
||||
)
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentKeys.info(deployedWorkflowId),
|
||||
})
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
const SKELETON_LINE_COUNT = 4
|
||||
|
||||
export default function HomeLoading() {
|
||||
return (
|
||||
<div className='flex h-full flex-col bg-[var(--bg)]'>
|
||||
<div className='min-h-0 flex-1 overflow-hidden px-6 py-4'>
|
||||
<div className='mx-auto max-w-[42rem] space-y-[10px] pt-3'>
|
||||
{Array.from({ length: SKELETON_LINE_COUNT }).map((_, i) => (
|
||||
<Skeleton key={i} className='h-[16px]' style={{ width: `${120 + (i % 4) * 48}px` }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-shrink-0 px-[24px] pb-[16px]'>
|
||||
<div className='mx-auto max-w-[42rem]'>
|
||||
<Skeleton className='h-[48px] w-full rounded-[12px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { format } from 'date-fns'
|
||||
import { AlertCircle, Loader2, Pencil, Plus, Tag, X } from 'lucide-react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useParams, usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import {
|
||||
Badge,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { Database, DatabaseX } from '@/components/emcn/icons'
|
||||
import { SearchHighlight } from '@/components/ui/search-highlight'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { ADD_CONNECTOR_SEARCH_PARAM } from '@/lib/credentials/client-state'
|
||||
import { ALL_TAG_SLOTS, type AllTagSlot, getFieldTypeForSlot } from '@/lib/knowledge/constants'
|
||||
import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types'
|
||||
import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/filters/types'
|
||||
@@ -192,6 +193,10 @@ export function KnowledgeBase({
|
||||
}: KnowledgeBaseProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = propWorkspaceId || (params.workspaceId as string)
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const pathname = usePathname()
|
||||
const addConnectorParam = searchParams.get(ADD_CONNECTOR_SEARCH_PARAM)
|
||||
const posthog = usePostHog()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -278,7 +283,29 @@ export function KnowledgeBase({
|
||||
const [contextMenuDocument, setContextMenuDocument] = useState<DocumentData | null>(null)
|
||||
const [showRenameModal, setShowRenameModal] = useState(false)
|
||||
const [documentToRename, setDocumentToRename] = useState<DocumentData | null>(null)
|
||||
const [showAddConnectorModal, setShowAddConnectorModal] = useState(false)
|
||||
const showAddConnectorModal = addConnectorParam != null
|
||||
const searchParamsRef = useRef(searchParams)
|
||||
searchParamsRef.current = searchParams
|
||||
const updateAddConnectorParam = useCallback(
|
||||
(value: string | null) => {
|
||||
const current = searchParamsRef.current
|
||||
const currentValue = current.get(ADD_CONNECTOR_SEARCH_PARAM)
|
||||
if (value === currentValue || (value === null && currentValue === null)) return
|
||||
const next = new URLSearchParams(current.toString())
|
||||
if (value === null) {
|
||||
next.delete(ADD_CONNECTOR_SEARCH_PARAM)
|
||||
} else {
|
||||
next.set(ADD_CONNECTOR_SEARCH_PARAM, value)
|
||||
}
|
||||
const qs = next.toString()
|
||||
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false })
|
||||
},
|
||||
[pathname, router]
|
||||
)
|
||||
const setShowAddConnectorModal = useCallback(
|
||||
(open: boolean) => updateAddConnectorParam(open ? '' : null),
|
||||
[updateAddConnectorParam]
|
||||
)
|
||||
|
||||
const {
|
||||
isOpen: isContextMenuOpen,
|
||||
@@ -340,8 +367,6 @@ export function KnowledgeBase({
|
||||
prevHadSyncingRef.current = hasSyncingConnectors
|
||||
}, [hasSyncingConnectors, refreshKnowledgeBase, refreshDocuments])
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const knowledgeBaseName = knowledgeBase?.name || passedKnowledgeBaseName || 'Knowledge Base'
|
||||
const error = knowledgeBaseError || documentsError
|
||||
|
||||
@@ -1254,7 +1279,13 @@ export function KnowledgeBase({
|
||||
/>
|
||||
|
||||
{showAddConnectorModal && (
|
||||
<AddConnectorModal open onOpenChange={setShowAddConnectorModal} knowledgeBaseId={id} />
|
||||
<AddConnectorModal
|
||||
open
|
||||
onOpenChange={setShowAddConnectorModal}
|
||||
onConnectorTypeChange={updateAddConnectorParam}
|
||||
knowledgeBaseId={id}
|
||||
initialConnectorType={addConnectorParam || undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{documentToRename && (
|
||||
|
||||
@@ -44,14 +44,22 @@ const CONNECTOR_ENTRIES = Object.entries(CONNECTOR_REGISTRY)
|
||||
interface AddConnectorModalProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onConnectorTypeChange?: (connectorType: string | null) => void
|
||||
knowledgeBaseId: string
|
||||
initialConnectorType?: string | null
|
||||
}
|
||||
|
||||
type Step = 'select-type' | 'configure'
|
||||
|
||||
export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddConnectorModalProps) {
|
||||
const [step, setStep] = useState<Step>('select-type')
|
||||
const [selectedType, setSelectedType] = useState<string | null>(null)
|
||||
export function AddConnectorModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConnectorTypeChange,
|
||||
knowledgeBaseId,
|
||||
initialConnectorType,
|
||||
}: AddConnectorModalProps) {
|
||||
const [step, setStep] = useState<Step>(() => (initialConnectorType ? 'configure' : 'select-type'))
|
||||
const [selectedType, setSelectedType] = useState<string | null>(initialConnectorType ?? null)
|
||||
const [sourceConfig, setSourceConfig] = useState<Record<string, string>>({})
|
||||
const [syncInterval, setSyncInterval] = useState(1440)
|
||||
const [selectedCredentialId, setSelectedCredentialId] = useState<string | null>(null)
|
||||
@@ -151,6 +159,7 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
|
||||
setError(null)
|
||||
setSearchTerm('')
|
||||
setStep('configure')
|
||||
onConnectorTypeChange?.(type)
|
||||
}
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
@@ -286,7 +295,10 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='mr-2 h-6 w-6 p-0'
|
||||
onClick={() => setStep('select-type')}
|
||||
onClick={() => {
|
||||
setStep('select-type')
|
||||
onConnectorTypeChange?.('')
|
||||
}}
|
||||
>
|
||||
<ArrowLeft className='h-4 w-4' />
|
||||
</Button>
|
||||
@@ -565,6 +577,7 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
|
||||
workspaceId={workspaceId}
|
||||
knowledgeBaseId={knowledgeBaseId}
|
||||
credentialCount={credentials.length}
|
||||
connectorType={selectedType ?? undefined}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
export default function TaskLoading() {
|
||||
return (
|
||||
<div className='flex h-full bg-[var(--bg)]'>
|
||||
<div className='flex h-full min-w-0 flex-1 flex-col'>
|
||||
<div className='flex min-h-0 flex-1 items-center justify-center'>
|
||||
<Loader2 className='h-[20px] w-[20px] animate-spin text-[var(--text-tertiary)]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -111,8 +111,6 @@ function normalizeWorkflowState(input?: any): WorkflowState | null {
|
||||
lastUpdate: input.lastUpdate,
|
||||
metadata: input.metadata,
|
||||
variables: input.variables,
|
||||
deploymentStatuses: input.deploymentStatuses,
|
||||
needsRedeployment: input.needsRedeployment,
|
||||
dragStartPosition: input.dragStartPosition ?? null,
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ import { useWorkflowMap } from '@/hooks/queries/workflows'
|
||||
import { useWorkspaceSettings } from '@/hooks/queries/workspace'
|
||||
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { mergeSubblockState } from '@/stores/workflows/utils'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
@@ -90,10 +89,7 @@ export function DeployModal({
|
||||
const params = useParams()
|
||||
const workspaceId = params?.workspaceId as string
|
||||
const { navigateToSettings } = useSettingsNavigation()
|
||||
const deploymentStatus = useWorkflowRegistry((state) =>
|
||||
state.getWorkflowDeploymentStatus(workflowId)
|
||||
)
|
||||
const isDeployed = deploymentStatus?.isDeployed ?? isDeployedProp
|
||||
const isDeployed = isDeployedProp
|
||||
const { data: workflowMap = {} } = useWorkflowMap(workspaceId)
|
||||
const workflowMetadata = workflowId ? workflowMap[workflowId] : undefined
|
||||
const workflowWorkspaceId = workflowMetadata?.workspaceId ?? null
|
||||
@@ -381,8 +377,6 @@ export function DeployModal({
|
||||
|
||||
invalidateDeploymentQueries(queryClient, workflowId)
|
||||
|
||||
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(workflowId, false)
|
||||
|
||||
if (chatSuccessTimeoutRef.current) {
|
||||
clearTimeout(chatSuccessTimeoutRef.current)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
useDeployment,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks'
|
||||
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
|
||||
import { useDeployedWorkflowState } from '@/hooks/queries/deployments'
|
||||
import { useDeployedWorkflowState, useDeploymentInfo } from '@/hooks/queries/deployments'
|
||||
import type { WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
@@ -25,10 +25,10 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP
|
||||
const isRegistryLoading = hydrationPhase === 'idle' || hydrationPhase === 'state-loading'
|
||||
const { hasBlocks } = useCurrentWorkflow()
|
||||
|
||||
const deploymentStatus = useWorkflowRegistry((state) =>
|
||||
state.getWorkflowDeploymentStatus(activeWorkflowId)
|
||||
)
|
||||
const isDeployed = deploymentStatus?.isDeployed || false
|
||||
const { data: deploymentInfo } = useDeploymentInfo(activeWorkflowId, {
|
||||
enabled: !isRegistryLoading,
|
||||
})
|
||||
const isDeployed = deploymentInfo?.isDeployed ?? false
|
||||
|
||||
const isDeployedStateEnabled = Boolean(activeWorkflowId) && isDeployed && !isRegistryLoading
|
||||
const {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react'
|
||||
import { hasWorkflowChanged } from '@/lib/workflows/comparison'
|
||||
import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
||||
import { useVariablesStore } from '@/stores/panel/variables/store'
|
||||
import { useVariablesStore } from '@/stores/variables/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
@@ -31,8 +31,8 @@ import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/
|
||||
import { getBlock } from '@/blocks'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { normalizeName } from '@/executor/constants'
|
||||
import type { Variable } from '@/stores/panel'
|
||||
import { useVariablesStore } from '@/stores/panel'
|
||||
import { useVariablesStore } from '@/stores/variables/store'
|
||||
import type { Variable } from '@/stores/variables/types'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
import type { Variable } from '@/stores/panel'
|
||||
import { useVariablesStore } from '@/stores/panel'
|
||||
import { useVariablesStore } from '@/stores/variables/store'
|
||||
import type { Variable } from '@/stores/variables/types'
|
||||
|
||||
interface VariableAssignment {
|
||||
id: string
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useParams } from 'next/navigation'
|
||||
import { SELECTOR_CONTEXT_FIELDS } from '@/lib/workflows/subblocks/context'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { extractEnvVarName, isEnvVarReference, isReference } from '@/executor/constants'
|
||||
import { usePersonalEnvironment } from '@/hooks/queries/environment'
|
||||
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
||||
import { useEnvironmentStore } from '@/stores/settings/environment'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useDependsOnGate } from './use-depends-on-gate'
|
||||
import { useSubBlockValue } from './use-sub-block-value'
|
||||
@@ -32,7 +32,7 @@ export function useSelectorSetup(
|
||||
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
|
||||
const workflowId = (params?.workflowId as string) || activeWorkflowId || ''
|
||||
|
||||
const envVariables = useEnvironmentStore((s) => s.variables)
|
||||
const { data: envVariables = {} } = usePersonalEnvironment()
|
||||
|
||||
const { finalDisabled, dependencyValues, canonicalIndex } = useDependsOnGate(
|
||||
blockId,
|
||||
|
||||
@@ -63,7 +63,8 @@ import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
|
||||
import { useChatStore } from '@/stores/chat/store'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
import type { ChatContext, PanelTab } from '@/stores/panel'
|
||||
import { usePanelStore, useVariablesStore as usePanelVariablesStore } from '@/stores/panel'
|
||||
import { usePanelStore } from '@/stores/panel'
|
||||
import { useVariablesModalStore } from '@/stores/variables/modal'
|
||||
import { useVariablesStore } from '@/stores/variables/store'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||
import { captureBaselineSnapshot } from '@/stores/workflow-diff/utils'
|
||||
@@ -205,7 +206,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
|
||||
setIsChatOpen: state.setIsChatOpen,
|
||||
}))
|
||||
)
|
||||
const { isOpen: isVariablesOpen, setIsOpen: setVariablesOpen } = useVariablesStore(
|
||||
const { isOpen: isVariablesOpen, setIsOpen: setVariablesOpen } = useVariablesModalStore(
|
||||
useShallow((state) => ({
|
||||
isOpen: state.isOpen,
|
||||
setIsOpen: state.setIsOpen,
|
||||
@@ -410,6 +411,17 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
|
||||
setHasHydrated(true)
|
||||
}, [setHasHydrated])
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
const message = (e as CustomEvent<{ message: string }>).detail?.message
|
||||
if (!message) return
|
||||
setActiveTab('copilot')
|
||||
copilotSendMessage(message)
|
||||
}
|
||||
window.addEventListener('mothership-send-message', handler)
|
||||
return () => window.removeEventListener('mothership-send-message', handler)
|
||||
}, [setActiveTab, copilotSendMessage])
|
||||
|
||||
/**
|
||||
* Handles tab click events
|
||||
*/
|
||||
@@ -482,7 +494,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
|
||||
throw new Error('No workflow state found')
|
||||
}
|
||||
|
||||
const workflowVariables = usePanelVariablesStore
|
||||
const workflowVariables = useVariablesStore
|
||||
.getState()
|
||||
.getVariablesByWorkflowId(activeWorkflowId)
|
||||
|
||||
|
||||
@@ -27,15 +27,15 @@ import {
|
||||
usePreventZoom,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { useVariablesStore as usePanelVariablesStore } from '@/stores/panel'
|
||||
import {
|
||||
getVariablesPosition,
|
||||
MAX_VARIABLES_HEIGHT,
|
||||
MAX_VARIABLES_WIDTH,
|
||||
MIN_VARIABLES_HEIGHT,
|
||||
MIN_VARIABLES_WIDTH,
|
||||
useVariablesStore,
|
||||
} from '@/stores/variables/store'
|
||||
useVariablesModalStore,
|
||||
} from '@/stores/variables/modal'
|
||||
import { useVariablesStore } from '@/stores/variables/store'
|
||||
import type { Variable } from '@/stores/variables/types'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
@@ -96,7 +96,7 @@ export function Variables() {
|
||||
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
|
||||
|
||||
const { isOpen, position, width, height, setIsOpen, setPosition, setDimensions } =
|
||||
useVariablesStore(
|
||||
useVariablesModalStore(
|
||||
useShallow((s) => ({
|
||||
isOpen: s.isOpen,
|
||||
position: s.position,
|
||||
@@ -108,7 +108,7 @@ export function Variables() {
|
||||
}))
|
||||
)
|
||||
|
||||
const variables = usePanelVariablesStore((s) => s.variables)
|
||||
const variables = useVariablesStore((s) => s.variables)
|
||||
|
||||
const { collaborativeUpdateVariable, collaborativeAddVariable, collaborativeDeleteVariable } =
|
||||
useCollaborativeWorkflow()
|
||||
|
||||
@@ -48,7 +48,7 @@ import { useSkills } from '@/hooks/queries/skills'
|
||||
import { useTablesList } from '@/hooks/queries/tables'
|
||||
import { useWorkflowMap } from '@/hooks/queries/workflows'
|
||||
import { useSelectorDisplayName } from '@/hooks/use-selector-display-name'
|
||||
import { useVariablesStore } from '@/stores/panel'
|
||||
import { useVariablesStore } from '@/stores/variables/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import { wouldCreateCycle } from '@/stores/workflows/workflow/utils'
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useMemo } from 'react'
|
||||
import type { Edge } from 'reactflow'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||
import type { DeploymentStatus } from '@/stores/workflows/registry/types'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
@@ -16,8 +15,6 @@ export interface CurrentWorkflow {
|
||||
loops: Record<string, Loop>
|
||||
parallels: Record<string, Parallel>
|
||||
lastSaved?: number
|
||||
deploymentStatuses?: Record<string, DeploymentStatus>
|
||||
needsRedeployment?: boolean
|
||||
|
||||
// Mode information
|
||||
isDiffMode: boolean
|
||||
@@ -48,8 +45,6 @@ export function useCurrentWorkflow(): CurrentWorkflow {
|
||||
loops: state.loops,
|
||||
parallels: state.parallels,
|
||||
lastSaved: state.lastSaved,
|
||||
deploymentStatuses: state.deploymentStatuses,
|
||||
needsRedeployment: state.needsRedeployment,
|
||||
}))
|
||||
)
|
||||
|
||||
@@ -78,8 +73,6 @@ export function useCurrentWorkflow(): CurrentWorkflow {
|
||||
loops: activeWorkflow.loops || {},
|
||||
parallels: activeWorkflow.parallels || {},
|
||||
lastSaved: activeWorkflow.lastSaved,
|
||||
deploymentStatuses: activeWorkflow.deploymentStatuses,
|
||||
needsRedeployment: activeWorkflow.needsRedeployment,
|
||||
|
||||
// Mode information - update to reflect ready state
|
||||
isDiffMode: hasActiveDiff && isShowingDiff,
|
||||
|
||||
@@ -36,8 +36,6 @@ import { useExecutionStream } from '@/hooks/use-execution-stream'
|
||||
import { WorkflowValidationError } from '@/serializer'
|
||||
import { useCurrentWorkflowExecution, useExecutionStore } from '@/stores/execution'
|
||||
import { useNotificationStore } from '@/stores/notifications'
|
||||
import { useVariablesStore } from '@/stores/panel'
|
||||
import { useEnvironmentStore } from '@/stores/settings/environment'
|
||||
import {
|
||||
clearExecutionPointer,
|
||||
consolePersistence,
|
||||
@@ -45,6 +43,7 @@ import {
|
||||
saveExecutionPointer,
|
||||
useTerminalConsoleStore,
|
||||
} from '@/stores/terminal'
|
||||
import { useVariablesStore } from '@/stores/variables/store'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { mergeSubblockState } from '@/stores/workflows/utils'
|
||||
@@ -120,7 +119,6 @@ export function useWorkflowExecution() {
|
||||
}))
|
||||
)
|
||||
const hasHydrated = useTerminalConsoleStore((s) => s._hasHydrated)
|
||||
const getAllVariables = useEnvironmentStore((s) => s.getAllVariables)
|
||||
const { getVariablesByWorkflowId, variables } = useVariablesStore(
|
||||
useShallow((s) => ({
|
||||
getVariablesByWorkflowId: s.getVariablesByWorkflowId,
|
||||
@@ -744,7 +742,6 @@ export function useWorkflowExecution() {
|
||||
activeWorkflowId,
|
||||
currentWorkflow,
|
||||
toggleConsole,
|
||||
getAllVariables,
|
||||
getVariablesByWorkflowId,
|
||||
setIsExecuting,
|
||||
setIsDebugging,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
export default function WorkflowLoading() {
|
||||
return (
|
||||
<div className='flex h-full w-full flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
<div className='relative flex h-full w-full flex-1 items-center justify-center bg-[var(--bg)]'>
|
||||
<Loader2 className='h-[20px] w-[20px] animate-spin text-[var(--text-tertiary)]' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -124,8 +124,7 @@ export async function applyAutoLayoutAndUpdateStore(
|
||||
try {
|
||||
useWorkflowStore.getState().updateLastSaved()
|
||||
|
||||
const { deploymentStatuses, needsRedeployment, dragStartPosition, ...stateToSave } =
|
||||
newWorkflowState
|
||||
const { dragStartPosition, ...stateToSave } = newWorkflowState
|
||||
|
||||
const cleanedWorkflowState = {
|
||||
...stateToSave,
|
||||
|
||||
@@ -85,7 +85,7 @@ import { useSearchModalStore } from '@/stores/modals/search/store'
|
||||
import { useNotificationStore } from '@/stores/notifications'
|
||||
import { usePanelEditorStore } from '@/stores/panel'
|
||||
import { useUndoRedoStore } from '@/stores/undo-redo'
|
||||
import { useVariablesStore } from '@/stores/variables/store'
|
||||
import { useVariablesModalStore } from '@/stores/variables/modal'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { getUniqueBlockName, prepareBlockState } from '@/stores/workflows/utils'
|
||||
@@ -265,7 +265,7 @@ const WorkflowContent = React.memo(
|
||||
const { fitViewToBounds, getViewportCenter } = useCanvasViewport(reactFlowInstance, {
|
||||
embedded,
|
||||
})
|
||||
const { emitCursorUpdate } = useSocket()
|
||||
const { emitCursorUpdate, joinWorkflow, leaveWorkflow } = useSocket()
|
||||
useDynamicHandleRefresh()
|
||||
|
||||
const workspaceId = propWorkspaceId || (params.workspaceId as string)
|
||||
@@ -273,6 +273,14 @@ const WorkflowContent = React.memo(
|
||||
|
||||
const addNotification = useNotificationStore((state) => state.addNotification)
|
||||
|
||||
useEffect(() => {
|
||||
if (!embedded || !workflowIdParam) return
|
||||
joinWorkflow(workflowIdParam)
|
||||
return () => {
|
||||
leaveWorkflow()
|
||||
}
|
||||
}, [embedded, workflowIdParam, joinWorkflow, leaveWorkflow])
|
||||
|
||||
useOAuthReturnForWorkflow(workflowIdParam)
|
||||
|
||||
const {
|
||||
@@ -337,7 +345,7 @@ const WorkflowContent = React.memo(
|
||||
autoConnectRef.current = isAutoConnectEnabled
|
||||
|
||||
// Panel open states for context menu
|
||||
const isVariablesOpen = useVariablesStore((state) => state.isOpen)
|
||||
const isVariablesOpen = useVariablesModalStore((state) => state.isOpen)
|
||||
const isChatOpen = useChatStore((state) => state.isChatOpen)
|
||||
|
||||
const snapGrid: [number, number] = useMemo(
|
||||
@@ -1374,7 +1382,7 @@ const WorkflowContent = React.memo(
|
||||
}, [router, workspaceId, workflowIdParam])
|
||||
|
||||
const handleContextToggleVariables = useCallback(() => {
|
||||
const { isOpen, setIsOpen } = useVariablesStore.getState()
|
||||
const { isOpen, setIsOpen } = useVariablesModalStore.getState()
|
||||
setIsOpen(!isOpen)
|
||||
}, [])
|
||||
|
||||
@@ -2144,12 +2152,9 @@ const WorkflowContent = React.memo(
|
||||
|
||||
const handleCanvasPointerMove = useCallback(
|
||||
(event: React.PointerEvent<Element>) => {
|
||||
const target = event.currentTarget as HTMLElement
|
||||
const bounds = target.getBoundingClientRect()
|
||||
|
||||
const position = screenToFlowPosition({
|
||||
x: event.clientX - bounds.left,
|
||||
y: event.clientY - bounds.top,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
})
|
||||
|
||||
emitCursorUpdate(position)
|
||||
|
||||
@@ -13,7 +13,7 @@ import { DELETED_WORKFLOW_LABEL } from '@/app/workspace/[workspaceId]/logs/utils
|
||||
import { getDisplayValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types'
|
||||
import { useVariablesStore } from '@/stores/panel/variables/store'
|
||||
import { useVariablesStore } from '@/stores/variables/store'
|
||||
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
|
||||
|
||||
/** Execution status for blocks in preview mode */
|
||||
|
||||
@@ -343,7 +343,11 @@ export function SearchModal({
|
||||
'-translate-x-1/2 fixed top-[15%] z-50 w-[500px] rounded-xl border-[4px] border-black/[0.06] bg-[var(--bg)] shadow-[0_24px_80px_-16px_rgba(0,0,0,0.15)] dark:border-white/[0.06] dark:shadow-[0_24px_80px_-16px_rgba(0,0,0,0.4)]',
|
||||
open ? 'visible opacity-100' : 'invisible opacity-0'
|
||||
)}
|
||||
style={{ left: 'calc(var(--sidebar-width) / 2 + 50%)' }}
|
||||
style={{
|
||||
left: isOnWorkflowPage
|
||||
? 'calc(50% + (var(--sidebar-width) - var(--panel-width)) / 2)'
|
||||
: 'calc(var(--sidebar-width) / 2 + 50%)',
|
||||
}}
|
||||
>
|
||||
<Command label='Search' shouldFilter={false}>
|
||||
<div className='mx-2 mt-2 mb-1 flex items-center gap-1.5 rounded-lg border border-[var(--border-1)] bg-[var(--surface-5)] px-2 dark:bg-[var(--surface-4)]'>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
export default function WorkflowsLoading() {
|
||||
return (
|
||||
<div className='flex h-full w-full flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
<div className='relative flex h-full w-full flex-1 items-center justify-center bg-[var(--bg)]'>
|
||||
<Loader2 className='h-[20px] w-[20px] animate-spin text-[var(--text-tertiary)]' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -90,6 +90,7 @@ interface SocketContextType {
|
||||
onSelectionUpdate: (handler: (data: any) => void) => void
|
||||
onWorkflowDeleted: (handler: (data: any) => void) => void
|
||||
onWorkflowReverted: (handler: (data: any) => void) => void
|
||||
onWorkflowUpdated: (handler: (data: any) => void) => void
|
||||
onOperationConfirmed: (handler: (data: any) => void) => void
|
||||
onOperationFailed: (handler: (data: any) => void) => void
|
||||
}
|
||||
@@ -118,6 +119,7 @@ const SocketContext = createContext<SocketContextType>({
|
||||
onSelectionUpdate: () => {},
|
||||
onWorkflowDeleted: () => {},
|
||||
onWorkflowReverted: () => {},
|
||||
onWorkflowUpdated: () => {},
|
||||
onOperationConfirmed: () => {},
|
||||
onOperationFailed: () => {},
|
||||
})
|
||||
@@ -155,6 +157,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
||||
selectionUpdate?: (data: any) => void
|
||||
workflowDeleted?: (data: any) => void
|
||||
workflowReverted?: (data: any) => void
|
||||
workflowUpdated?: (data: any) => void
|
||||
operationConfirmed?: (data: any) => void
|
||||
operationFailed?: (data: any) => void
|
||||
}>({})
|
||||
@@ -334,7 +337,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
||||
socketInstance.on('join-workflow-success', ({ workflowId, presenceUsers }) => {
|
||||
isRejoiningRef.current = false
|
||||
// Ignore stale success responses from previous navigation
|
||||
if (workflowId !== urlWorkflowIdRef.current) {
|
||||
if (urlWorkflowIdRef.current && workflowId !== urlWorkflowIdRef.current) {
|
||||
logger.debug(`Ignoring stale join-workflow-success for ${workflowId}`)
|
||||
return
|
||||
}
|
||||
@@ -382,6 +385,11 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
||||
eventHandlers.current.workflowReverted?.(data)
|
||||
})
|
||||
|
||||
socketInstance.on('workflow-updated', (data) => {
|
||||
logger.info(`Workflow ${data.workflowId} has been updated externally`)
|
||||
eventHandlers.current.workflowUpdated?.(data)
|
||||
})
|
||||
|
||||
const rehydrateWorkflowStores = async (workflowId: string, workflowState: any) => {
|
||||
const [
|
||||
{ useOperationQueueStore },
|
||||
@@ -424,7 +432,6 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
||||
loops: workflowState.loops || {},
|
||||
parallels: workflowState.parallels || {},
|
||||
lastSaved: workflowState.lastSaved || Date.now(),
|
||||
deploymentStatuses: workflowState.deploymentStatuses || {},
|
||||
})
|
||||
|
||||
useSubBlockStore.setState((state: any) => ({
|
||||
@@ -804,6 +811,10 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
||||
eventHandlers.current.workflowReverted = handler
|
||||
}, [])
|
||||
|
||||
const onWorkflowUpdated = useCallback((handler: (data: any) => void) => {
|
||||
eventHandlers.current.workflowUpdated = handler
|
||||
}, [])
|
||||
|
||||
const onOperationConfirmed = useCallback((handler: (data: any) => void) => {
|
||||
eventHandlers.current.operationConfirmed = handler
|
||||
}, [])
|
||||
@@ -837,6 +848,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
||||
onSelectionUpdate,
|
||||
onWorkflowDeleted,
|
||||
onWorkflowReverted,
|
||||
onWorkflowUpdated,
|
||||
onOperationConfirmed,
|
||||
onOperationFailed,
|
||||
}),
|
||||
@@ -864,6 +876,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
||||
onSelectionUpdate,
|
||||
onWorkflowDeleted,
|
||||
onWorkflowReverted,
|
||||
onWorkflowUpdated,
|
||||
onOperationConfirmed,
|
||||
onOperationFailed,
|
||||
]
|
||||
|
||||
@@ -40,6 +40,7 @@ import * as React from 'react'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
||||
import { X } from 'lucide-react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { Button } from '../button/button'
|
||||
|
||||
@@ -50,13 +51,6 @@ import { Button } from '../button/button'
|
||||
const ANIMATION_CLASSES =
|
||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=open]:animate-in motion-reduce:animate-none'
|
||||
|
||||
/**
|
||||
* Modal content animation classes.
|
||||
* We keep only the slide animations (no zoom) to stabilize positioning while avoiding scale effects.
|
||||
*/
|
||||
const CONTENT_ANIMATION_CLASSES =
|
||||
'data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[50%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[50%] motion-reduce:animate-none'
|
||||
|
||||
/**
|
||||
* Root modal component. Manages open state.
|
||||
*/
|
||||
@@ -145,6 +139,8 @@ const ModalContent = React.forwardRef<
|
||||
ModalContentProps
|
||||
>(({ className, children, showClose = true, size = 'md', style, ...props }, ref) => {
|
||||
const [isInteractionReady, setIsInteractionReady] = React.useState(false)
|
||||
const pathname = usePathname()
|
||||
const isWorkflowPage = pathname?.includes('/w/') ?? false
|
||||
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => setIsInteractionReady(true), 100)
|
||||
@@ -157,14 +153,15 @@ const ModalContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
ANIMATION_CLASSES,
|
||||
CONTENT_ANIMATION_CLASSES,
|
||||
'fixed top-[50%] z-[var(--z-modal)] flex max-h-[84vh] translate-x-[-50%] translate-y-[-50%] flex-col overflow-hidden rounded-xl bg-[var(--bg)] text-small ring-1 ring-foreground/10 duration-200',
|
||||
MODAL_SIZES[size],
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
left: '50%',
|
||||
left: isWorkflowPage
|
||||
? // --panel-width is always the rendered panel width on /w/ routes (panel is never hidden/collapsed)
|
||||
'calc(50% + (var(--sidebar-width) - var(--panel-width)) / 2)'
|
||||
: 'calc(var(--sidebar-width) / 2 + 50%)',
|
||||
...style,
|
||||
}}
|
||||
onEscapeKeyDown={(e) => {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tansta
|
||||
import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/persistence/utils'
|
||||
import { fetchDeploymentVersionState } from '@/hooks/queries/utils/fetch-deployment-version-state'
|
||||
import { workflowKeys } from '@/hooks/queries/utils/workflow-keys'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const logger = createLogger('DeploymentQueries')
|
||||
@@ -321,7 +320,6 @@ interface DeployWorkflowResult {
|
||||
*/
|
||||
export function useDeployWorkflow() {
|
||||
const queryClient = useQueryClient()
|
||||
const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus)
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
@@ -351,18 +349,12 @@ export function useDeployWorkflow() {
|
||||
warnings: data.warnings,
|
||||
}
|
||||
},
|
||||
onSuccess: (data, variables) => {
|
||||
logger.info('Workflow deployed successfully', { workflowId: variables.workflowId })
|
||||
|
||||
setDeploymentStatus(
|
||||
variables.workflowId,
|
||||
data.isDeployed,
|
||||
data.deployedAt ? new Date(data.deployedAt) : undefined,
|
||||
data.apiKey
|
||||
)
|
||||
|
||||
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(variables.workflowId, false)
|
||||
|
||||
onSettled: (_data, error, variables) => {
|
||||
if (error) {
|
||||
logger.error('Failed to deploy workflow', { error })
|
||||
} else {
|
||||
logger.info('Workflow deployed successfully', { workflowId: variables.workflowId })
|
||||
}
|
||||
return Promise.all([
|
||||
invalidateDeploymentQueries(queryClient, variables.workflowId),
|
||||
queryClient.invalidateQueries({
|
||||
@@ -370,9 +362,6 @@ export function useDeployWorkflow() {
|
||||
}),
|
||||
])
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error('Failed to deploy workflow', { error })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -389,7 +378,6 @@ interface UndeployWorkflowVariables {
|
||||
*/
|
||||
export function useUndeployWorkflow() {
|
||||
const queryClient = useQueryClient()
|
||||
const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus)
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ workflowId }: UndeployWorkflowVariables): Promise<void> => {
|
||||
@@ -402,11 +390,12 @@ export function useUndeployWorkflow() {
|
||||
throw new Error(errorData.error || 'Failed to undeploy workflow')
|
||||
}
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
logger.info('Workflow undeployed successfully', { workflowId: variables.workflowId })
|
||||
|
||||
setDeploymentStatus(variables.workflowId, false)
|
||||
|
||||
onSettled: (_data, error, variables) => {
|
||||
if (error) {
|
||||
logger.error('Failed to undeploy workflow', { error })
|
||||
} else {
|
||||
logger.info('Workflow undeployed successfully', { workflowId: variables.workflowId })
|
||||
}
|
||||
return Promise.all([
|
||||
invalidateDeploymentQueries(queryClient, variables.workflowId),
|
||||
queryClient.invalidateQueries({
|
||||
@@ -414,9 +403,6 @@ export function useUndeployWorkflow() {
|
||||
}),
|
||||
])
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error('Failed to undeploy workflow', { error })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -613,7 +599,6 @@ interface ActivateVersionResult {
|
||||
*/
|
||||
export function useActivateDeploymentVersion() {
|
||||
const queryClient = useQueryClient()
|
||||
const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus)
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
@@ -663,20 +648,13 @@ export function useActivateDeploymentVersion() {
|
||||
)
|
||||
}
|
||||
},
|
||||
onSuccess: (data, variables) => {
|
||||
logger.info('Deployment version activated', {
|
||||
workflowId: variables.workflowId,
|
||||
version: variables.version,
|
||||
})
|
||||
|
||||
setDeploymentStatus(
|
||||
variables.workflowId,
|
||||
true,
|
||||
data.deployedAt ? new Date(data.deployedAt) : undefined,
|
||||
data.apiKey
|
||||
)
|
||||
},
|
||||
onSettled: (_, __, variables) => {
|
||||
onSettled: (_data, error, variables) => {
|
||||
if (!error) {
|
||||
logger.info('Deployment version activated', {
|
||||
workflowId: variables.workflowId,
|
||||
version: variables.version,
|
||||
})
|
||||
}
|
||||
return invalidateDeploymentQueries(queryClient, variables.workflowId)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useEffect } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import type { WorkspaceEnvironmentData } from '@/lib/environment/api'
|
||||
@@ -6,7 +5,6 @@ import { fetchPersonalEnvironment, fetchWorkspaceEnvironment } from '@/lib/envir
|
||||
import { workspaceCredentialKeys } from '@/hooks/queries/credentials'
|
||||
import { API_ENDPOINTS } from '@/stores/constants'
|
||||
import type { EnvironmentVariable } from '@/stores/settings/environment'
|
||||
import { useEnvironmentStore } from '@/stores/settings/environment'
|
||||
|
||||
export type { WorkspaceEnvironmentData } from '@/lib/environment/api'
|
||||
export type { EnvironmentVariable } from '@/stores/settings/environment'
|
||||
@@ -22,29 +20,16 @@ export const environmentKeys = {
|
||||
workspace: (workspaceId: string) => [...environmentKeys.all, 'workspace', workspaceId] as const,
|
||||
}
|
||||
|
||||
/**
|
||||
* Environment Variable Types
|
||||
*/
|
||||
/**
|
||||
* Hook to fetch personal environment variables
|
||||
*/
|
||||
export function usePersonalEnvironment() {
|
||||
const setVariables = useEnvironmentStore((state) => state.setVariables)
|
||||
|
||||
const query = useQuery({
|
||||
return useQuery({
|
||||
queryKey: environmentKeys.personal(),
|
||||
queryFn: ({ signal }) => fetchPersonalEnvironment(signal),
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (query.data) {
|
||||
setVariables(query.data)
|
||||
}
|
||||
}, [query.data, setVariables])
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { getQueryDataMock } = vi.hoisted(() => ({
|
||||
getQueryDataMock: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/_shell/providers/get-query-client', () => ({
|
||||
getQueryClient: vi.fn(() => ({
|
||||
getQueryData: getQueryDataMock,
|
||||
})),
|
||||
}))
|
||||
|
||||
import { getCustomTool, getCustomTools } from '@/hooks/queries/utils/custom-tool-cache'
|
||||
|
||||
describe('custom tool cache helpers', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('reads workspace-scoped custom tools from the cache', () => {
|
||||
const tools = [{ id: 'tool-1', title: 'Weather', schema: {}, code: '', workspaceId: 'ws-1' }]
|
||||
getQueryDataMock.mockReturnValue(tools)
|
||||
|
||||
expect(getCustomTools('ws-1')).toBe(tools)
|
||||
expect(getQueryDataMock).toHaveBeenCalledWith(['customTools', 'list', 'ws-1'])
|
||||
})
|
||||
|
||||
it('resolves custom tools by id or title', () => {
|
||||
getQueryDataMock.mockReturnValue([
|
||||
{ id: 'tool-1', title: 'Weather', schema: {}, code: '', workspaceId: 'ws-1' },
|
||||
])
|
||||
|
||||
expect(getCustomTool('tool-1', 'ws-1')?.title).toBe('Weather')
|
||||
expect(getCustomTool('Weather', 'ws-1')?.id).toBe('tool-1')
|
||||
})
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
|
||||
import type { CustomToolDefinition } from '@/hooks/queries/custom-tools'
|
||||
import { customToolsKeys } from '@/hooks/queries/utils/custom-tool-keys'
|
||||
|
||||
/**
|
||||
* Reads custom tools for a workspace directly from the React Query cache.
|
||||
*/
|
||||
export function getCustomTools(workspaceId: string): CustomToolDefinition[] {
|
||||
return (
|
||||
getQueryClient().getQueryData<CustomToolDefinition[]>(customToolsKeys.list(workspaceId)) ?? []
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a custom tool from the cache by id or title.
|
||||
*/
|
||||
export function getCustomTool(
|
||||
identifier: string,
|
||||
workspaceId: string
|
||||
): CustomToolDefinition | undefined {
|
||||
const tools = getCustomTools(workspaceId)
|
||||
return tools.find((tool) => tool.id === identifier || tool.title === identifier)
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { extractEnvVarName, isEnvVarReference, isReference } from '@/executor/constants'
|
||||
import { usePersonalEnvironment } from '@/hooks/queries/environment'
|
||||
import { getSelectorDefinition, mergeOption } from '@/hooks/selectors/registry'
|
||||
import type { SelectorKey, SelectorOption, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
import { useEnvironmentStore } from '@/stores/settings/environment'
|
||||
|
||||
interface SelectorHookArgs extends Omit<SelectorQueryArgs, 'key'> {
|
||||
search?: string
|
||||
@@ -31,7 +31,7 @@ export function useSelectorOptionDetail(
|
||||
key: SelectorKey,
|
||||
args: SelectorHookArgs & { detailId?: string }
|
||||
) {
|
||||
const envVariables = useEnvironmentStore((s) => s.variables)
|
||||
const { data: envVariables = {} } = usePersonalEnvironment()
|
||||
const definition = getSelectorDefinition(key)
|
||||
|
||||
const resolvedDetailId = useMemo(() => {
|
||||
|
||||
@@ -19,8 +19,9 @@ import {
|
||||
} from '@/socket/constants'
|
||||
import { useNotificationStore } from '@/stores/notifications'
|
||||
import { registerEmitFunctions, useOperationQueue } from '@/stores/operation-queue/store'
|
||||
import { usePanelEditorStore, useVariablesStore } from '@/stores/panel'
|
||||
import { usePanelEditorStore } from '@/stores/panel'
|
||||
import { useCodeUndoRedoStore, useUndoRedoStore } from '@/stores/undo-redo'
|
||||
import { useVariablesStore } from '@/stores/variables/store'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
@@ -122,6 +123,7 @@ export function useCollaborativeWorkflow() {
|
||||
onVariableUpdate,
|
||||
onWorkflowDeleted,
|
||||
onWorkflowReverted,
|
||||
onWorkflowUpdated,
|
||||
onOperationConfirmed,
|
||||
onOperationFailed,
|
||||
} = useSocket()
|
||||
@@ -536,82 +538,99 @@ export function useCollaborativeWorkflow() {
|
||||
}
|
||||
}
|
||||
|
||||
const reloadWorkflowFromApi = async (workflowId: string, reason: string): Promise<boolean> => {
|
||||
const response = await fetch(`/api/workflows/${workflowId}`)
|
||||
if (!response.ok) {
|
||||
logger.error(`Failed to fetch workflow data after ${reason}: ${response.statusText}`)
|
||||
return false
|
||||
}
|
||||
|
||||
const responseData = await response.json()
|
||||
const workflowData = responseData.data
|
||||
|
||||
if (!workflowData?.state) {
|
||||
logger.error(`No state found in workflow data after ${reason}`, { workflowData })
|
||||
return false
|
||||
}
|
||||
|
||||
isApplyingRemoteChange.current = true
|
||||
try {
|
||||
useWorkflowStore.getState().replaceWorkflowState({
|
||||
blocks: workflowData.state.blocks || {},
|
||||
edges: workflowData.state.edges || [],
|
||||
loops: workflowData.state.loops || {},
|
||||
parallels: workflowData.state.parallels || {},
|
||||
lastSaved: workflowData.state.lastSaved || Date.now(),
|
||||
})
|
||||
|
||||
const subblockValues: Record<string, Record<string, any>> = {}
|
||||
Object.entries(workflowData.state.blocks || {}).forEach(([blockId, block]) => {
|
||||
const blockState = block as any
|
||||
subblockValues[blockId] = {}
|
||||
Object.entries(blockState.subBlocks || {}).forEach(([subblockId, subblock]) => {
|
||||
subblockValues[blockId][subblockId] = (subblock as any).value
|
||||
})
|
||||
})
|
||||
|
||||
useSubBlockStore.setState((state: any) => ({
|
||||
workflowValues: {
|
||||
...state.workflowValues,
|
||||
[workflowId]: subblockValues,
|
||||
},
|
||||
}))
|
||||
|
||||
const graph = {
|
||||
blocksById: workflowData.state.blocks || {},
|
||||
edgesById: Object.fromEntries(
|
||||
(workflowData.state.edges || []).map((e: any) => [e.id, e])
|
||||
),
|
||||
}
|
||||
|
||||
const undoRedoStore = useUndoRedoStore.getState()
|
||||
const stackKeys = Object.keys(undoRedoStore.stacks)
|
||||
stackKeys.forEach((key) => {
|
||||
const [wfId, userId] = key.split(':')
|
||||
if (wfId === workflowId) {
|
||||
undoRedoStore.pruneInvalidEntries(wfId, userId, graph)
|
||||
}
|
||||
})
|
||||
|
||||
logger.info(`Successfully reloaded workflow state after ${reason}`, { workflowId })
|
||||
return true
|
||||
} finally {
|
||||
isApplyingRemoteChange.current = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleWorkflowReverted = async (data: any) => {
|
||||
const { workflowId } = data
|
||||
logger.info(`Workflow ${workflowId} has been reverted to deployed state`)
|
||||
|
||||
// If the reverted workflow is the currently active one, reload the workflow state
|
||||
if (activeWorkflowId === workflowId) {
|
||||
logger.info(`Currently active workflow ${workflowId} was reverted, reloading state`)
|
||||
if (activeWorkflowId !== workflowId) return
|
||||
|
||||
try {
|
||||
// Fetch the updated workflow state from the server (which loads from normalized tables)
|
||||
const response = await fetch(`/api/workflows/${workflowId}`)
|
||||
if (response.ok) {
|
||||
const responseData = await response.json()
|
||||
const workflowData = responseData.data
|
||||
try {
|
||||
await reloadWorkflowFromApi(workflowId, 'revert')
|
||||
} catch (error) {
|
||||
logger.error('Error reloading workflow state after revert:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (workflowData?.state) {
|
||||
// Update the workflow store with the reverted state
|
||||
isApplyingRemoteChange.current = true
|
||||
try {
|
||||
// Update the main workflow state using the API response
|
||||
useWorkflowStore.getState().replaceWorkflowState({
|
||||
blocks: workflowData.state.blocks || {},
|
||||
edges: workflowData.state.edges || [],
|
||||
loops: workflowData.state.loops || {},
|
||||
parallels: workflowData.state.parallels || {},
|
||||
lastSaved: workflowData.state.lastSaved || Date.now(),
|
||||
deploymentStatuses: workflowData.state.deploymentStatuses || {},
|
||||
})
|
||||
const handleWorkflowUpdated = async (data: any) => {
|
||||
const { workflowId } = data
|
||||
logger.info(`Workflow ${workflowId} has been updated externally`)
|
||||
|
||||
// Update subblock store with reverted values
|
||||
const subblockValues: Record<string, Record<string, any>> = {}
|
||||
Object.entries(workflowData.state.blocks || {}).forEach(([blockId, block]) => {
|
||||
const blockState = block as any
|
||||
subblockValues[blockId] = {}
|
||||
Object.entries(blockState.subBlocks || {}).forEach(([subblockId, subblock]) => {
|
||||
subblockValues[blockId][subblockId] = (subblock as any).value
|
||||
})
|
||||
})
|
||||
if (activeWorkflowId !== workflowId) return
|
||||
|
||||
// Update subblock store for this workflow
|
||||
useSubBlockStore.setState((state: any) => ({
|
||||
workflowValues: {
|
||||
...state.workflowValues,
|
||||
[workflowId]: subblockValues,
|
||||
},
|
||||
}))
|
||||
const { hasActiveDiff } = useWorkflowDiffStore.getState()
|
||||
if (hasActiveDiff) {
|
||||
logger.info('Skipping workflow-updated: active diff in progress', { workflowId })
|
||||
return
|
||||
}
|
||||
|
||||
logger.info(`Successfully loaded reverted workflow state for ${workflowId}`)
|
||||
|
||||
const graph = {
|
||||
blocksById: workflowData.state.blocks || {},
|
||||
edgesById: Object.fromEntries(
|
||||
(workflowData.state.edges || []).map((e: any) => [e.id, e])
|
||||
),
|
||||
}
|
||||
|
||||
const undoRedoStore = useUndoRedoStore.getState()
|
||||
const stackKeys = Object.keys(undoRedoStore.stacks)
|
||||
stackKeys.forEach((key) => {
|
||||
const [wfId, userId] = key.split(':')
|
||||
if (wfId === workflowId) {
|
||||
undoRedoStore.pruneInvalidEntries(wfId, userId, graph)
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
isApplyingRemoteChange.current = false
|
||||
}
|
||||
} else {
|
||||
logger.error('No state found in workflow data after revert', { workflowData })
|
||||
}
|
||||
} else {
|
||||
logger.error(`Failed to fetch workflow data after revert: ${response.statusText}`)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error reloading workflow state after revert:', error)
|
||||
}
|
||||
try {
|
||||
await reloadWorkflowFromApi(workflowId, 'external update')
|
||||
} catch (error) {
|
||||
logger.error('Error reloading workflow state after external update:', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -633,6 +652,7 @@ export function useCollaborativeWorkflow() {
|
||||
onVariableUpdate(handleVariableUpdate)
|
||||
onWorkflowDeleted(handleWorkflowDeleted)
|
||||
onWorkflowReverted(handleWorkflowReverted)
|
||||
onWorkflowUpdated(handleWorkflowUpdated)
|
||||
onOperationConfirmed(handleOperationConfirmed)
|
||||
onOperationFailed(handleOperationFailed)
|
||||
}, [
|
||||
@@ -641,6 +661,7 @@ export function useCollaborativeWorkflow() {
|
||||
onVariableUpdate,
|
||||
onWorkflowDeleted,
|
||||
onWorkflowReverted,
|
||||
onWorkflowUpdated,
|
||||
onOperationConfirmed,
|
||||
onOperationFailed,
|
||||
activeWorkflowId,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useRef } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { toast } from '@/components/emcn'
|
||||
import {
|
||||
ADD_CONNECTOR_SEARCH_PARAM,
|
||||
consumeOAuthReturnContext,
|
||||
type OAuthReturnContext,
|
||||
readOAuthReturnContext,
|
||||
@@ -98,7 +99,11 @@ export function useOAuthReturnRouter() {
|
||||
try {
|
||||
sessionStorage.removeItem(SETTINGS_RETURN_URL_KEY)
|
||||
} catch {}
|
||||
router.replace(`/workspace/${workspaceId}/knowledge/${ctx.knowledgeBaseId}`)
|
||||
const kbUrl = `/workspace/${workspaceId}/knowledge/${ctx.knowledgeBaseId}`
|
||||
const connectorParam = ctx.connectorType
|
||||
? `?${ADD_CONNECTOR_SEARCH_PARAM}=${encodeURIComponent(ctx.connectorType)}`
|
||||
: ''
|
||||
router.replace(`${kbUrl}${connectorParam}`)
|
||||
return
|
||||
}
|
||||
}, [router, workspaceId])
|
||||
|
||||
@@ -40,7 +40,11 @@ import {
|
||||
XCircle,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
import { getCustomTool } from '@/hooks/queries/utils/custom-tool-cache'
|
||||
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
|
||||
import type { CustomToolDefinition } from '@/hooks/queries/custom-tools'
|
||||
import type { WorkflowDeploymentInfo } from '@/hooks/queries/deployments'
|
||||
import { deploymentKeys } from '@/hooks/queries/deployments'
|
||||
import { customToolsKeys } from '@/hooks/queries/utils/custom-tool-keys'
|
||||
import { getWorkflowById } from '@/hooks/queries/utils/workflow-cache'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
@@ -445,7 +449,8 @@ const META_deploy_api: ToolMetadata = {
|
||||
|
||||
const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId
|
||||
const isAlreadyDeployed = workflowId
|
||||
? useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)?.isDeployed
|
||||
? (getQueryClient().getQueryData<WorkflowDeploymentInfo>(deploymentKeys.info(workflowId))
|
||||
?.isDeployed ?? false)
|
||||
: false
|
||||
|
||||
let actionText = action
|
||||
@@ -1053,7 +1058,11 @@ const META_manage_custom_tool: ToolMetadata = {
|
||||
let toolName = params?.schema?.function?.name
|
||||
if (!toolName && params?.toolId && workspaceId) {
|
||||
try {
|
||||
const tool = getCustomTool(params.toolId, workspaceId)
|
||||
const tools =
|
||||
getQueryClient().getQueryData<CustomToolDefinition[]>(
|
||||
customToolsKeys.list(workspaceId)
|
||||
) ?? []
|
||||
const tool = tools.find((t) => t.id === params.toolId || t.title === params.toolId)
|
||||
toolName = tool?.schema?.function?.name
|
||||
} catch {
|
||||
// Ignore errors accessing cache
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type BaseServerTool,
|
||||
type ServerToolContext,
|
||||
} from '@/lib/copilot/tools/server/base-tool'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { applyTargetedLayout, getTargetedLayoutImpact } from '@/lib/workflows/autolayout'
|
||||
import {
|
||||
DEFAULT_HORIZONTAL_SPACING,
|
||||
@@ -287,6 +288,18 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, unknown>
|
||||
|
||||
logger.info('Workflow state persisted to database', { workflowId })
|
||||
|
||||
const socketUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002'
|
||||
fetch(`${socketUrl}/api/workflow-updated`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': env.INTERNAL_API_SECRET,
|
||||
},
|
||||
body: JSON.stringify({ workflowId }),
|
||||
}).catch((error) => {
|
||||
logger.warn('Failed to notify socket server of workflow update', { workflowId, error })
|
||||
})
|
||||
|
||||
const sanitizationWarnings = validation.warnings.length > 0 ? validation.warnings : undefined
|
||||
|
||||
return {
|
||||
|
||||
@@ -91,6 +91,8 @@ export function clearPendingCredentialCreateRequest() {
|
||||
window.sessionStorage.removeItem(PENDING_CREDENTIAL_CREATE_REQUEST_KEY)
|
||||
}
|
||||
|
||||
export const ADD_CONNECTOR_SEARCH_PARAM = 'addConnector' as const
|
||||
|
||||
const OAUTH_RETURN_CONTEXT_KEY = 'sim.oauth-return-context'
|
||||
|
||||
export type OAuthReturnOrigin = 'workflow' | 'integrations' | 'kb-connectors'
|
||||
@@ -116,6 +118,7 @@ interface OAuthReturnIntegrations extends OAuthReturnBase {
|
||||
interface OAuthReturnKBConnectors extends OAuthReturnBase {
|
||||
origin: 'kb-connectors'
|
||||
knowledgeBaseId: string
|
||||
connectorType?: string
|
||||
}
|
||||
|
||||
export type OAuthReturnContext =
|
||||
|
||||
@@ -2,10 +2,9 @@ import type { Edge } from 'reactflow'
|
||||
import type { AsyncExecutionCorrelation } from '@/lib/core/async-jobs/types'
|
||||
import type { ParentIteration, SerializableExecutionState } from '@/executor/execution/types'
|
||||
import type { BlockLog, NormalizedBlockOutput } from '@/executor/types'
|
||||
import type { DeploymentStatus } from '@/stores/workflows/registry/types'
|
||||
import type { Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
export type { WorkflowState, Loop, Parallel, DeploymentStatus }
|
||||
export type { WorkflowState, Loop, Parallel }
|
||||
export type WorkflowEdge = Edge
|
||||
export type { NormalizedBlockOutput, BlockLog }
|
||||
|
||||
|
||||
@@ -123,8 +123,6 @@ export function buildDefaultWorkflowArtifacts(): DefaultWorkflowArtifacts {
|
||||
loops: {},
|
||||
parallels: {},
|
||||
lastSaved: Date.now(),
|
||||
deploymentStatuses: {},
|
||||
needsRedeployment: false,
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
deduplicateWorkflowName,
|
||||
} from '@/lib/workflows/utils'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
import type { Variable } from '@/stores/panel/variables/types'
|
||||
import type { Variable } from '@/stores/variables/types'
|
||||
import type { LoopConfig, ParallelConfig } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const logger = createLogger('WorkflowDuplicateHelper')
|
||||
|
||||
@@ -985,7 +985,6 @@ describe('Database Helpers', () => {
|
||||
edges: loadedState!.edges,
|
||||
loops: {},
|
||||
parallels: {},
|
||||
deploymentStatuses: {},
|
||||
}
|
||||
|
||||
const mockTransaction = vi.fn().mockImplementation(async (callback) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { VariableType } from '@/stores/panel/variables/types'
|
||||
import type { VariableType } from '@/stores/variables/types'
|
||||
|
||||
/**
|
||||
* Central manager for handling all variable-related operations.
|
||||
|
||||
@@ -186,7 +186,6 @@ export async function getWorkflowState(workflowId: string) {
|
||||
|
||||
if (normalizedData) {
|
||||
const finalState = {
|
||||
deploymentStatuses: {},
|
||||
hasActiveWebhook: false,
|
||||
blocks: normalizedData.blocks,
|
||||
edges: normalizedData.edges,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
|
||||
import { environmentKeys } from '@/hooks/queries/environment'
|
||||
import { useExecutionStore } from '@/stores/execution'
|
||||
import { useVariablesStore } from '@/stores/panel'
|
||||
import { useEnvironmentStore } from '@/stores/settings/environment'
|
||||
import { consolePersistence, useTerminalConsoleStore } from '@/stores/terminal'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
@@ -12,198 +11,13 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
const logger = createLogger('Stores')
|
||||
|
||||
// Track initialization state
|
||||
let isInitializing = false
|
||||
let appFullyInitialized = false
|
||||
let dataInitialized = false // Flag for actual data loading completion
|
||||
|
||||
/**
|
||||
* Initialize the application state and sync system
|
||||
* localStorage persistence has been removed - relies on DB and Zustand stores only
|
||||
* Reset all Zustand stores and React Query caches to initial state.
|
||||
*/
|
||||
async function initializeApplication(): Promise<void> {
|
||||
if (typeof window === 'undefined' || isInitializing) return
|
||||
|
||||
isInitializing = true
|
||||
appFullyInitialized = false
|
||||
|
||||
// Track initialization start time
|
||||
const initStartTime = Date.now()
|
||||
|
||||
try {
|
||||
// Load environment variables directly from DB
|
||||
await useEnvironmentStore.getState().loadEnvironmentVariables()
|
||||
|
||||
// Mark data as initialized only after sync managers have loaded data from DB
|
||||
dataInitialized = true
|
||||
|
||||
// Log initialization timing information
|
||||
const initDuration = Date.now() - initStartTime
|
||||
logger.info(`Application initialization completed in ${initDuration}ms`)
|
||||
|
||||
// Mark application as fully initialized
|
||||
appFullyInitialized = true
|
||||
} catch (error) {
|
||||
logger.error('Error during application initialization:', { error })
|
||||
// Still mark as initialized to prevent being stuck in initializing state
|
||||
appFullyInitialized = true
|
||||
// But don't mark data as initialized on error
|
||||
dataInitialized = false
|
||||
} finally {
|
||||
isInitializing = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if application is fully initialized
|
||||
*/
|
||||
export function isAppInitialized(): boolean {
|
||||
return appFullyInitialized
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if data has been loaded from the database
|
||||
* This should be checked before any sync operations
|
||||
*/
|
||||
export function isDataInitialized(): boolean {
|
||||
return dataInitialized
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle application cleanup before unload
|
||||
*/
|
||||
function handleBeforeUnload(event: BeforeUnloadEvent): void {
|
||||
// Check if we're on an authentication page and skip confirmation if we are
|
||||
if (typeof window !== 'undefined') {
|
||||
const path = window.location.pathname
|
||||
// Skip confirmation for auth-related pages
|
||||
if (
|
||||
path === '/login' ||
|
||||
path === '/signup' ||
|
||||
path === '/reset-password' ||
|
||||
path === '/verify'
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Standard beforeunload pattern
|
||||
event.preventDefault()
|
||||
event.returnValue = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up sync system
|
||||
*/
|
||||
function cleanupApplication(): void {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||
// Note: No sync managers to dispose - Socket.IO handles cleanup
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all user data when signing out
|
||||
* localStorage persistence has been removed
|
||||
*/
|
||||
export async function clearUserData(): Promise<void> {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
// Note: No sync managers to dispose - Socket.IO handles cleanup
|
||||
|
||||
// Reset all stores to their initial state
|
||||
resetAllStores()
|
||||
|
||||
// Clear localStorage except for essential app settings (minimal usage)
|
||||
const keysToKeep = ['next-favicon', 'theme']
|
||||
const keysToRemove = Object.keys(localStorage).filter((key) => !keysToKeep.includes(key))
|
||||
keysToRemove.forEach((key) => localStorage.removeItem(key))
|
||||
|
||||
// Reset application initialization state
|
||||
appFullyInitialized = false
|
||||
dataInitialized = false
|
||||
|
||||
logger.info('User data cleared successfully')
|
||||
} catch (error) {
|
||||
logger.error('Error clearing user data:', { error })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to manage application lifecycle
|
||||
*/
|
||||
export function useAppInitialization() {
|
||||
useEffect(() => {
|
||||
// Use Promise to handle async initialization
|
||||
initializeApplication()
|
||||
|
||||
return () => {
|
||||
cleanupApplication()
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to reinitialize the application after successful login
|
||||
* Use this in the login success handler or post-login page
|
||||
*/
|
||||
export function useLoginInitialization() {
|
||||
useEffect(() => {
|
||||
reinitializeAfterLogin()
|
||||
}, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinitialize the application after login
|
||||
* This ensures we load fresh data from the database for the new user
|
||||
*/
|
||||
export async function reinitializeAfterLogin(): Promise<void> {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
// Reset application initialization state
|
||||
appFullyInitialized = false
|
||||
dataInitialized = false
|
||||
|
||||
// Note: No sync managers to dispose - Socket.IO handles cleanup
|
||||
|
||||
// Clean existing state to avoid stale data
|
||||
resetAllStores()
|
||||
|
||||
// Reset initialization flags to force a fresh load
|
||||
isInitializing = false
|
||||
|
||||
// Reinitialize the application
|
||||
await initializeApplication()
|
||||
|
||||
logger.info('Application reinitialized after login')
|
||||
} catch (error) {
|
||||
logger.error('Error reinitializing application:', { error })
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize immediately when imported on client
|
||||
if (typeof window !== 'undefined') {
|
||||
initializeApplication()
|
||||
}
|
||||
|
||||
// Export all stores
|
||||
export {
|
||||
useWorkflowStore,
|
||||
useWorkflowRegistry,
|
||||
useEnvironmentStore,
|
||||
useExecutionStore,
|
||||
useTerminalConsoleStore,
|
||||
useVariablesStore,
|
||||
useSubBlockStore,
|
||||
}
|
||||
|
||||
// Helper function to reset all stores
|
||||
export const resetAllStores = () => {
|
||||
// Reset all stores to initial state
|
||||
useWorkflowRegistry.setState({
|
||||
activeWorkflowId: null,
|
||||
error: null,
|
||||
deploymentStatuses: {},
|
||||
hydration: {
|
||||
phase: 'idle',
|
||||
workspaceId: null,
|
||||
@@ -214,7 +28,7 @@ export const resetAllStores = () => {
|
||||
})
|
||||
useWorkflowStore.getState().clear()
|
||||
useSubBlockStore.getState().clear()
|
||||
useEnvironmentStore.getState().reset()
|
||||
getQueryClient().removeQueries({ queryKey: environmentKeys.all })
|
||||
useExecutionStore.getState().reset()
|
||||
useTerminalConsoleStore.setState({
|
||||
workflowEntries: {},
|
||||
@@ -223,21 +37,24 @@ export const resetAllStores = () => {
|
||||
isOpen: false,
|
||||
})
|
||||
consolePersistence.persist()
|
||||
// Custom tools are managed by React Query cache, not a Zustand store
|
||||
// Variables store has no tracking to reset; registry hydrates
|
||||
}
|
||||
|
||||
// Helper function to log all store states
|
||||
export const logAllStores = () => {
|
||||
const state = {
|
||||
workflow: useWorkflowStore.getState(),
|
||||
workflowRegistry: useWorkflowRegistry.getState(),
|
||||
environment: useEnvironmentStore.getState(),
|
||||
execution: useExecutionStore.getState(),
|
||||
console: useTerminalConsoleStore.getState(),
|
||||
subBlock: useSubBlockStore.getState(),
|
||||
variables: useVariablesStore.getState(),
|
||||
/**
|
||||
* Clear all user data when signing out.
|
||||
*/
|
||||
export async function clearUserData(): Promise<void> {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
resetAllStores()
|
||||
|
||||
// Clear localStorage except for essential app settings
|
||||
const keysToKeep = ['next-favicon', 'theme']
|
||||
const keysToRemove = Object.keys(localStorage).filter((key) => !keysToKeep.includes(key))
|
||||
keysToRemove.forEach((key) => localStorage.removeItem(key))
|
||||
|
||||
logger.info('User data cleared successfully')
|
||||
} catch (error) {
|
||||
logger.error('Error clearing user data:', { error })
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
@@ -9,6 +9,3 @@ export { usePanelStore } from './store'
|
||||
// Toolbar
|
||||
export { useToolbarStore } from './toolbar'
|
||||
export type { ChatContext, PanelState, PanelTab } from './types'
|
||||
export type { Variable, VariablesStore, VariableType } from './variables'
|
||||
// Variables
|
||||
export { useVariablesStore } from './variables'
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export { useVariablesStore } from './store'
|
||||
export type { Variable, VariablesStore, VariableType } from './types'
|
||||
@@ -1,290 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import JSON5 from 'json5'
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import { normalizeName } from '@/executor/constants'
|
||||
import { useOperationQueueStore } from '@/stores/operation-queue/store'
|
||||
import type { Variable, VariablesStore } from '@/stores/panel/variables/types'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
|
||||
const logger = createLogger('VariablesStore')
|
||||
|
||||
function validateVariable(variable: Variable): string | undefined {
|
||||
try {
|
||||
switch (variable.type) {
|
||||
case 'number':
|
||||
if (Number.isNaN(Number(variable.value))) {
|
||||
return 'Not a valid number'
|
||||
}
|
||||
break
|
||||
case 'boolean':
|
||||
if (!/^(true|false)$/i.test(String(variable.value).trim())) {
|
||||
return 'Expected "true" or "false"'
|
||||
}
|
||||
break
|
||||
case 'object':
|
||||
try {
|
||||
const valueToEvaluate = String(variable.value).trim()
|
||||
|
||||
if (!valueToEvaluate.startsWith('{') || !valueToEvaluate.endsWith('}')) {
|
||||
return 'Not a valid object format'
|
||||
}
|
||||
|
||||
const parsed = JSON5.parse(valueToEvaluate)
|
||||
|
||||
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return 'Not a valid object'
|
||||
}
|
||||
|
||||
return undefined
|
||||
} catch (e) {
|
||||
logger.error('Object parsing error:', e)
|
||||
return 'Invalid object syntax'
|
||||
}
|
||||
case 'array':
|
||||
try {
|
||||
const parsed = JSON5.parse(String(variable.value))
|
||||
if (!Array.isArray(parsed)) {
|
||||
return 'Not a valid array'
|
||||
}
|
||||
} catch {
|
||||
return 'Invalid array syntax'
|
||||
}
|
||||
break
|
||||
}
|
||||
return undefined
|
||||
} catch (e) {
|
||||
return e instanceof Error ? e.message : 'Invalid format'
|
||||
}
|
||||
}
|
||||
|
||||
function migrateStringToPlain(variable: Variable): Variable {
|
||||
if (variable.type !== 'string') {
|
||||
return variable
|
||||
}
|
||||
|
||||
const updated = {
|
||||
...variable,
|
||||
type: 'plain' as const,
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
export const useVariablesStore = create<VariablesStore>()(
|
||||
devtools((set, get) => ({
|
||||
variables: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isEditing: null,
|
||||
|
||||
async loadForWorkflow(workflowId) {
|
||||
try {
|
||||
set({ isLoading: true, error: null })
|
||||
const res = await fetch(`/api/workflows/${workflowId}/variables`, { method: 'GET' })
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '')
|
||||
throw new Error(text || `Failed to load variables: ${res.statusText}`)
|
||||
}
|
||||
const data = await res.json()
|
||||
const variables = (data?.data as Record<string, Variable>) || {}
|
||||
set((state) => {
|
||||
const withoutWorkflow = Object.fromEntries(
|
||||
Object.entries(state.variables).filter(
|
||||
(entry): entry is [string, Variable] => entry[1].workflowId !== workflowId
|
||||
)
|
||||
)
|
||||
return {
|
||||
variables: { ...withoutWorkflow, ...variables },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : 'Unknown error'
|
||||
set({ isLoading: false, error: message })
|
||||
}
|
||||
},
|
||||
|
||||
addVariable: (variable, providedId?: string) => {
|
||||
const id = providedId || crypto.randomUUID()
|
||||
|
||||
const workflowVariables = get().getVariablesByWorkflowId(variable.workflowId)
|
||||
|
||||
if (!variable.name || /^variable\d+$/.test(variable.name)) {
|
||||
const existingNumbers = workflowVariables
|
||||
.map((v) => {
|
||||
const match = v.name.match(/^variable(\d+)$/)
|
||||
return match ? Number.parseInt(match[1]) : 0
|
||||
})
|
||||
.filter((n) => !Number.isNaN(n))
|
||||
|
||||
const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1
|
||||
|
||||
variable.name = `variable${nextNumber}`
|
||||
}
|
||||
|
||||
let uniqueName = variable.name
|
||||
let nameIndex = 1
|
||||
|
||||
while (workflowVariables.some((v) => v.name === uniqueName)) {
|
||||
uniqueName = `${variable.name} (${nameIndex})`
|
||||
nameIndex++
|
||||
}
|
||||
|
||||
if (variable.type === 'string') {
|
||||
variable.type = 'plain'
|
||||
}
|
||||
|
||||
const newVariable: Variable = {
|
||||
id,
|
||||
workflowId: variable.workflowId,
|
||||
name: uniqueName,
|
||||
type: variable.type,
|
||||
value: variable.value || '',
|
||||
validationError: undefined,
|
||||
}
|
||||
|
||||
const validationError = validateVariable(newVariable)
|
||||
if (validationError) {
|
||||
newVariable.validationError = validationError
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
variables: {
|
||||
...state.variables,
|
||||
[id]: newVariable,
|
||||
},
|
||||
}))
|
||||
|
||||
return id
|
||||
},
|
||||
|
||||
updateVariable: (id, update) => {
|
||||
set((state) => {
|
||||
if (!state.variables[id]) return state
|
||||
|
||||
if (update.name !== undefined) {
|
||||
const oldVariable = state.variables[id]
|
||||
const oldVariableName = oldVariable.name
|
||||
const newName = update.name.trim()
|
||||
|
||||
if (!newName) {
|
||||
update = { ...update }
|
||||
update.name = undefined
|
||||
} else if (newName !== oldVariableName) {
|
||||
const subBlockStore = useSubBlockStore.getState()
|
||||
const targetWorkflowId = oldVariable.workflowId
|
||||
|
||||
if (targetWorkflowId) {
|
||||
const workflowValues = subBlockStore.workflowValues[targetWorkflowId] || {}
|
||||
const updatedWorkflowValues = { ...workflowValues }
|
||||
const changedSubBlocks: Array<{ blockId: string; subBlockId: string; value: any }> =
|
||||
[]
|
||||
|
||||
const oldVarName = normalizeName(oldVariableName)
|
||||
const newVarName = normalizeName(newName)
|
||||
const regex = new RegExp(`<variable\\.${oldVarName}>`, 'gi')
|
||||
|
||||
const updateReferences = (value: any, pattern: RegExp, replacement: string): any => {
|
||||
if (typeof value === 'string') {
|
||||
return pattern.test(value) ? value.replace(pattern, replacement) : value
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => updateReferences(item, pattern, replacement))
|
||||
}
|
||||
|
||||
if (value !== null && typeof value === 'object') {
|
||||
const result = { ...value }
|
||||
for (const key in result) {
|
||||
result[key] = updateReferences(result[key], pattern, replacement)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
Object.entries(workflowValues).forEach(([blockId, blockValues]) => {
|
||||
Object.entries(blockValues as Record<string, any>).forEach(
|
||||
([subBlockId, value]) => {
|
||||
const updatedValue = updateReferences(value, regex, `<variable.${newVarName}>`)
|
||||
|
||||
if (JSON.stringify(updatedValue) !== JSON.stringify(value)) {
|
||||
if (!updatedWorkflowValues[blockId]) {
|
||||
updatedWorkflowValues[blockId] = { ...workflowValues[blockId] }
|
||||
}
|
||||
updatedWorkflowValues[blockId][subBlockId] = updatedValue
|
||||
changedSubBlocks.push({ blockId, subBlockId, value: updatedValue })
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// Update local state
|
||||
useSubBlockStore.setState({
|
||||
workflowValues: {
|
||||
...subBlockStore.workflowValues,
|
||||
[targetWorkflowId]: updatedWorkflowValues,
|
||||
},
|
||||
})
|
||||
|
||||
// Queue operations for persistence via socket
|
||||
const operationQueue = useOperationQueueStore.getState()
|
||||
|
||||
for (const { blockId, subBlockId, value } of changedSubBlocks) {
|
||||
operationQueue.addToQueue({
|
||||
id: crypto.randomUUID(),
|
||||
operation: {
|
||||
operation: 'subblock-update',
|
||||
target: 'subblock',
|
||||
payload: { blockId, subblockId: subBlockId, value },
|
||||
},
|
||||
workflowId: targetWorkflowId,
|
||||
userId: 'system',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (update.type === 'string') {
|
||||
update = { ...update, type: 'plain' }
|
||||
}
|
||||
|
||||
const updatedVariable: Variable = {
|
||||
...state.variables[id],
|
||||
...update,
|
||||
validationError: undefined,
|
||||
}
|
||||
|
||||
if (update.type || update.value !== undefined) {
|
||||
updatedVariable.validationError = validateVariable(updatedVariable)
|
||||
}
|
||||
|
||||
const updated = {
|
||||
...state.variables,
|
||||
[id]: updatedVariable,
|
||||
}
|
||||
|
||||
return { variables: updated }
|
||||
})
|
||||
},
|
||||
|
||||
deleteVariable: (id) => {
|
||||
set((state) => {
|
||||
if (!state.variables[id]) return state
|
||||
|
||||
const workflowId = state.variables[id].workflowId
|
||||
const { [id]: _, ...rest } = state.variables
|
||||
|
||||
return { variables: rest }
|
||||
})
|
||||
},
|
||||
|
||||
getVariablesByWorkflowId: (workflowId) => {
|
||||
return Object.values(get().variables).filter((variable) => variable.workflowId === workflowId)
|
||||
},
|
||||
}))
|
||||
)
|
||||
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* Variable types supported in the application
|
||||
* Note: 'string' is deprecated - use 'plain' for text values instead
|
||||
*/
|
||||
export type VariableType = 'plain' | 'number' | 'boolean' | 'object' | 'array' | 'string'
|
||||
|
||||
/**
|
||||
* Represents a workflow variable with workflow-specific naming
|
||||
* Variable names must be unique within each workflow
|
||||
*/
|
||||
export interface Variable {
|
||||
id: string
|
||||
workflowId: string
|
||||
name: string // Must be unique per workflow
|
||||
type: VariableType
|
||||
value: unknown
|
||||
validationError?: string // Tracks format validation errors
|
||||
}
|
||||
|
||||
export interface VariablesStore {
|
||||
variables: Record<string, Variable>
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
isEditing: string | null
|
||||
|
||||
/**
|
||||
* Loads variables for a specific workflow from the API and hydrates the store.
|
||||
*/
|
||||
loadForWorkflow: (workflowId: string) => Promise<void>
|
||||
|
||||
/**
|
||||
* Adds a new variable with automatic name uniqueness validation
|
||||
* If a variable with the same name exists, it will be suffixed with a number
|
||||
* Optionally accepts a predetermined ID for collaborative operations
|
||||
*/
|
||||
addVariable: (variable: Omit<Variable, 'id'>, providedId?: string) => string
|
||||
|
||||
/**
|
||||
* Updates a variable, ensuring name remains unique within the workflow
|
||||
* If an updated name conflicts with existing ones, a numbered suffix is added
|
||||
*/
|
||||
updateVariable: (id: string, update: Partial<Omit<Variable, 'id' | 'workflowId'>>) => void
|
||||
|
||||
deleteVariable: (id: string) => void
|
||||
|
||||
/**
|
||||
* Returns all variables for a specific workflow
|
||||
*/
|
||||
getVariablesByWorkflowId: (workflowId: string) => Variable[]
|
||||
}
|
||||
@@ -1,7 +1 @@
|
||||
export { useEnvironmentStore } from './store'
|
||||
export type {
|
||||
CachedWorkspaceEnvData,
|
||||
EnvironmentState,
|
||||
EnvironmentStore,
|
||||
EnvironmentVariable,
|
||||
} from './types'
|
||||
export type { CachedWorkspaceEnvData, EnvironmentState, EnvironmentVariable } from './types'
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { create } from 'zustand'
|
||||
import { fetchPersonalEnvironment } from '@/lib/environment/api'
|
||||
import type { EnvironmentStore, EnvironmentVariable } from './types'
|
||||
|
||||
const logger = createLogger('EnvironmentStore')
|
||||
|
||||
export const useEnvironmentStore = create<EnvironmentStore>()((set, get) => ({
|
||||
variables: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
loadEnvironmentVariables: async () => {
|
||||
try {
|
||||
set({ isLoading: true, error: null })
|
||||
const data = await fetchPersonalEnvironment()
|
||||
set({ variables: data, isLoading: false })
|
||||
} catch (error) {
|
||||
logger.error('Error loading environment variables:', { error })
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
isLoading: false,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
setVariables: (variables: Record<string, EnvironmentVariable>) => {
|
||||
set({ variables })
|
||||
},
|
||||
|
||||
getAllVariables: () => {
|
||||
return get().variables
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
set({
|
||||
variables: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
},
|
||||
}))
|
||||
@@ -15,10 +15,3 @@ export interface EnvironmentState {
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export interface EnvironmentStore extends EnvironmentState {
|
||||
loadEnvironmentVariables: () => Promise<void>
|
||||
setVariables: (variables: Record<string, EnvironmentVariable>) => void
|
||||
getAllVariables: () => Record<string, EnvironmentVariable>
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
11
apps/sim/stores/variables/index.ts
Normal file
11
apps/sim/stores/variables/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export {
|
||||
getDefaultVariablesDimensions,
|
||||
getVariablesPosition,
|
||||
MAX_VARIABLES_HEIGHT,
|
||||
MAX_VARIABLES_WIDTH,
|
||||
MIN_VARIABLES_HEIGHT,
|
||||
MIN_VARIABLES_WIDTH,
|
||||
useVariablesModalStore,
|
||||
} from './modal'
|
||||
export { useVariablesStore } from './store'
|
||||
export type { Variable, VariablesStore, VariableType } from './types'
|
||||
145
apps/sim/stores/variables/modal.ts
Normal file
145
apps/sim/stores/variables/modal.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { create } from 'zustand'
|
||||
import { devtools, persist } from 'zustand/middleware'
|
||||
import type {
|
||||
VariablesDimensions,
|
||||
VariablesModalStore,
|
||||
VariablesPosition,
|
||||
} from '@/stores/variables/types'
|
||||
|
||||
/**
|
||||
* Floating variables modal default dimensions.
|
||||
* Slightly larger than the chat modal for more comfortable editing.
|
||||
*/
|
||||
const DEFAULT_WIDTH = 320
|
||||
const DEFAULT_HEIGHT = 320
|
||||
|
||||
/**
|
||||
* Minimum and maximum modal dimensions.
|
||||
*/
|
||||
export const MIN_VARIABLES_WIDTH = DEFAULT_WIDTH
|
||||
export const MIN_VARIABLES_HEIGHT = DEFAULT_HEIGHT
|
||||
export const MAX_VARIABLES_WIDTH = 500
|
||||
export const MAX_VARIABLES_HEIGHT = 600
|
||||
|
||||
/** Inset gap between the viewport edge and the content window */
|
||||
const CONTENT_WINDOW_GAP = 8
|
||||
|
||||
/**
|
||||
* Compute a center-biased default position, factoring in current layout chrome
|
||||
* (sidebar, right panel, terminal) and content window inset.
|
||||
*/
|
||||
const calculateDefaultPosition = (): VariablesPosition => {
|
||||
if (typeof window === 'undefined') {
|
||||
return { x: 100, y: 100 }
|
||||
}
|
||||
|
||||
const sidebarWidth = Number.parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
|
||||
)
|
||||
const panelWidth = Number.parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0'
|
||||
)
|
||||
const terminalHeight = Number.parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
|
||||
)
|
||||
|
||||
const availableWidth = window.innerWidth - sidebarWidth - CONTENT_WINDOW_GAP - panelWidth
|
||||
const availableHeight = window.innerHeight - CONTENT_WINDOW_GAP * 2 - terminalHeight
|
||||
const x = sidebarWidth + (availableWidth - DEFAULT_WIDTH) / 2
|
||||
const y = CONTENT_WINDOW_GAP + (availableHeight - DEFAULT_HEIGHT) / 2
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrain a position to the visible canvas, considering layout chrome.
|
||||
*/
|
||||
const constrainPosition = (
|
||||
position: VariablesPosition,
|
||||
width: number = DEFAULT_WIDTH,
|
||||
height: number = DEFAULT_HEIGHT
|
||||
): VariablesPosition => {
|
||||
if (typeof window === 'undefined') return position
|
||||
|
||||
const sidebarWidth = Number.parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
|
||||
)
|
||||
const panelWidth = Number.parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0'
|
||||
)
|
||||
const terminalHeight = Number.parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
|
||||
)
|
||||
|
||||
const minX = sidebarWidth
|
||||
const maxX = window.innerWidth - CONTENT_WINDOW_GAP - panelWidth - width
|
||||
const minY = CONTENT_WINDOW_GAP
|
||||
const maxY = window.innerHeight - CONTENT_WINDOW_GAP - terminalHeight - height
|
||||
|
||||
return {
|
||||
x: Math.max(minX, Math.min(maxX, position.x)),
|
||||
y: Math.max(minY, Math.min(maxY, position.y)),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a valid, constrained position. If the stored one is off-bounds due to
|
||||
* layout changes, prefer a fresh default center position.
|
||||
*/
|
||||
export const getVariablesPosition = (
|
||||
stored: VariablesPosition | null,
|
||||
width: number = DEFAULT_WIDTH,
|
||||
height: number = DEFAULT_HEIGHT
|
||||
): VariablesPosition => {
|
||||
if (!stored) return calculateDefaultPosition()
|
||||
const constrained = constrainPosition(stored, width, height)
|
||||
const deltaX = Math.abs(constrained.x - stored.x)
|
||||
const deltaY = Math.abs(constrained.y - stored.y)
|
||||
if (deltaX > 100 || deltaY > 100) return calculateDefaultPosition()
|
||||
return constrained
|
||||
}
|
||||
|
||||
/**
|
||||
* UI-only store for the floating variables modal.
|
||||
* Variable data lives in the variables data store (`@/stores/variables/store`).
|
||||
*/
|
||||
export const useVariablesModalStore = create<VariablesModalStore>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set) => ({
|
||||
isOpen: false,
|
||||
position: null,
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
|
||||
setIsOpen: (open) => set({ isOpen: open }),
|
||||
setPosition: (position) => set({ position }),
|
||||
setDimensions: (dimensions) =>
|
||||
set({
|
||||
width: Math.max(MIN_VARIABLES_WIDTH, Math.min(MAX_VARIABLES_WIDTH, dimensions.width)),
|
||||
height: Math.max(
|
||||
MIN_VARIABLES_HEIGHT,
|
||||
Math.min(MAX_VARIABLES_HEIGHT, dimensions.height)
|
||||
),
|
||||
}),
|
||||
resetPosition: () => set({ position: null }),
|
||||
}),
|
||||
{
|
||||
name: 'variables-modal-store',
|
||||
partialize: (state) => ({
|
||||
position: state.position,
|
||||
width: state.width,
|
||||
height: state.height,
|
||||
}),
|
||||
}
|
||||
),
|
||||
{ name: 'variables-modal-store' }
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* Get default floating variables modal dimensions.
|
||||
*/
|
||||
export const getDefaultVariablesDimensions = (): VariablesDimensions => ({
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
})
|
||||
@@ -1,145 +1,47 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import JSON5 from 'json5'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { create } from 'zustand'
|
||||
import { devtools, persist } from 'zustand/middleware'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import { normalizeName } from '@/executor/constants'
|
||||
import type {
|
||||
Variable,
|
||||
VariablesDimensions,
|
||||
VariablesPosition,
|
||||
VariablesStore,
|
||||
VariableType,
|
||||
} from '@/stores/variables/types'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useOperationQueueStore } from '@/stores/operation-queue/store'
|
||||
import type { Variable, VariablesStore } from '@/stores/variables/types'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
|
||||
const logger = createLogger('VariablesModalStore')
|
||||
const logger = createLogger('VariablesStore')
|
||||
|
||||
/**
|
||||
* Floating variables modal default dimensions.
|
||||
* Slightly larger than the chat modal for more comfortable editing.
|
||||
*/
|
||||
const DEFAULT_WIDTH = 320
|
||||
const DEFAULT_HEIGHT = 320
|
||||
|
||||
/**
|
||||
* Minimum and maximum modal dimensions.
|
||||
* Kept in sync with the chat modal experience.
|
||||
*/
|
||||
export const MIN_VARIABLES_WIDTH = DEFAULT_WIDTH
|
||||
export const MIN_VARIABLES_HEIGHT = DEFAULT_HEIGHT
|
||||
export const MAX_VARIABLES_WIDTH = 500
|
||||
export const MAX_VARIABLES_HEIGHT = 600
|
||||
|
||||
/** Inset gap between the viewport edge and the content window */
|
||||
const CONTENT_WINDOW_GAP = 8
|
||||
|
||||
/**
|
||||
* Compute a center-biased default position, factoring in current layout chrome
|
||||
* (sidebar, right panel, terminal) and content window inset.
|
||||
*/
|
||||
const calculateDefaultPosition = (): VariablesPosition => {
|
||||
if (typeof window === 'undefined') {
|
||||
return { x: 100, y: 100 }
|
||||
}
|
||||
|
||||
const sidebarWidth = Number.parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
|
||||
)
|
||||
const panelWidth = Number.parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0'
|
||||
)
|
||||
const terminalHeight = Number.parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
|
||||
)
|
||||
|
||||
const availableWidth = window.innerWidth - sidebarWidth - CONTENT_WINDOW_GAP - panelWidth
|
||||
const availableHeight = window.innerHeight - CONTENT_WINDOW_GAP * 2 - terminalHeight
|
||||
const x = sidebarWidth + (availableWidth - DEFAULT_WIDTH) / 2
|
||||
const y = CONTENT_WINDOW_GAP + (availableHeight - DEFAULT_HEIGHT) / 2
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrain a position to the visible canvas, considering layout chrome.
|
||||
*/
|
||||
const constrainPosition = (
|
||||
position: VariablesPosition,
|
||||
width: number = DEFAULT_WIDTH,
|
||||
height: number = DEFAULT_HEIGHT
|
||||
): VariablesPosition => {
|
||||
if (typeof window === 'undefined') return position
|
||||
|
||||
const sidebarWidth = Number.parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
|
||||
)
|
||||
const panelWidth = Number.parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0'
|
||||
)
|
||||
const terminalHeight = Number.parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
|
||||
)
|
||||
|
||||
const minX = sidebarWidth
|
||||
const maxX = window.innerWidth - CONTENT_WINDOW_GAP - panelWidth - width
|
||||
const minY = CONTENT_WINDOW_GAP
|
||||
const maxY = window.innerHeight - CONTENT_WINDOW_GAP - terminalHeight - height
|
||||
|
||||
return {
|
||||
x: Math.max(minX, Math.min(maxX, position.x)),
|
||||
y: Math.max(minY, Math.min(maxY, position.y)),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a valid, constrained position. If the stored one is off-bounds due to
|
||||
* layout changes, prefer a fresh default center position.
|
||||
*/
|
||||
export const getVariablesPosition = (
|
||||
stored: VariablesPosition | null,
|
||||
width: number = DEFAULT_WIDTH,
|
||||
height: number = DEFAULT_HEIGHT
|
||||
): VariablesPosition => {
|
||||
if (!stored) return calculateDefaultPosition()
|
||||
const constrained = constrainPosition(stored, width, height)
|
||||
const deltaX = Math.abs(constrained.x - stored.x)
|
||||
const deltaY = Math.abs(constrained.y - stored.y)
|
||||
if (deltaX > 100 || deltaY > 100) return calculateDefaultPosition()
|
||||
return constrained
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a variable's value given its type. Returns an error message or undefined.
|
||||
*/
|
||||
function validateVariable(variable: Variable): string | undefined {
|
||||
try {
|
||||
switch (variable.type) {
|
||||
case 'number': {
|
||||
return Number.isNaN(Number(variable.value)) ? 'Not a valid number' : undefined
|
||||
}
|
||||
case 'boolean': {
|
||||
return !/^(true|false)$/i.test(String(variable.value).trim())
|
||||
? 'Expected "true" or "false"'
|
||||
: undefined
|
||||
}
|
||||
case 'object': {
|
||||
case 'number':
|
||||
if (Number.isNaN(Number(variable.value))) {
|
||||
return 'Not a valid number'
|
||||
}
|
||||
break
|
||||
case 'boolean':
|
||||
if (!/^(true|false)$/i.test(String(variable.value).trim())) {
|
||||
return 'Expected "true" or "false"'
|
||||
}
|
||||
break
|
||||
case 'object':
|
||||
try {
|
||||
const valueToEvaluate = String(variable.value).trim()
|
||||
|
||||
if (!valueToEvaluate.startsWith('{') || !valueToEvaluate.endsWith('}')) {
|
||||
return 'Not a valid object format'
|
||||
}
|
||||
|
||||
const parsed = JSON5.parse(valueToEvaluate)
|
||||
|
||||
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return 'Not a valid object'
|
||||
}
|
||||
|
||||
return undefined
|
||||
} catch (e) {
|
||||
logger.error('Object parsing error:', e)
|
||||
return 'Invalid object syntax'
|
||||
}
|
||||
}
|
||||
case 'array': {
|
||||
case 'array':
|
||||
try {
|
||||
const parsed = JSON5.parse(String(variable.value))
|
||||
if (!Array.isArray(parsed)) {
|
||||
@@ -148,257 +50,199 @@ function validateVariable(variable: Variable): string | undefined {
|
||||
} catch {
|
||||
return 'Invalid array syntax'
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
default:
|
||||
return undefined
|
||||
break
|
||||
}
|
||||
return undefined
|
||||
} catch (e) {
|
||||
return e instanceof Error ? e.message : 'Invalid format'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate deprecated type 'string' -> 'plain'.
|
||||
*/
|
||||
function migrateStringToPlain(variable: Variable): Variable {
|
||||
if (variable.type !== 'string') return variable
|
||||
return { ...variable, type: 'plain' as const }
|
||||
}
|
||||
|
||||
/**
|
||||
* Floating Variables modal + Variables data store.
|
||||
*/
|
||||
export const useVariablesStore = create<VariablesStore>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// UI
|
||||
isOpen: false,
|
||||
position: null,
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
devtools((set, get) => ({
|
||||
variables: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isEditing: null,
|
||||
|
||||
setIsOpen: (open) => set({ isOpen: open }),
|
||||
setPosition: (position) => set({ position }),
|
||||
setDimensions: (dimensions) =>
|
||||
set({
|
||||
width: Math.max(MIN_VARIABLES_WIDTH, Math.min(MAX_VARIABLES_WIDTH, dimensions.width)),
|
||||
height: Math.max(
|
||||
MIN_VARIABLES_HEIGHT,
|
||||
Math.min(MAX_VARIABLES_HEIGHT, dimensions.height)
|
||||
),
|
||||
}),
|
||||
resetPosition: () => set({ position: null }),
|
||||
addVariable: (variable, providedId?: string) => {
|
||||
const id = providedId || crypto.randomUUID()
|
||||
|
||||
// Data
|
||||
variables: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
const workflowVariables = get().getVariablesByWorkflowId(variable.workflowId)
|
||||
|
||||
async loadForWorkflow(workflowId) {
|
||||
try {
|
||||
set({ isLoading: true, error: null })
|
||||
const res = await fetch(`/api/workflows/${workflowId}/variables`, { method: 'GET' })
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '')
|
||||
throw new Error(text || `Failed to load variables: ${res.statusText}`)
|
||||
}
|
||||
const data = await res.json()
|
||||
const variables = (data?.data as Record<string, Variable>) || {}
|
||||
// Migrate any deprecated types and merge into store (remove other workflow entries)
|
||||
const migrated: Record<string, Variable> = Object.fromEntries(
|
||||
Object.entries(variables).map(([id, v]) => [id, migrateStringToPlain(v)])
|
||||
)
|
||||
set((state) => {
|
||||
const withoutThisWorkflow = Object.fromEntries(
|
||||
Object.entries(state.variables).filter(
|
||||
(entry): entry is [string, Variable] => entry[1].workflowId !== workflowId
|
||||
)
|
||||
)
|
||||
return {
|
||||
variables: { ...withoutThisWorkflow, ...migrated },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : 'Unknown error'
|
||||
set({ isLoading: false, error: message })
|
||||
}
|
||||
},
|
||||
|
||||
addVariable: (variable, providedId) => {
|
||||
const id = providedId || uuidv4()
|
||||
const state = get()
|
||||
|
||||
const workflowVariables = state
|
||||
.getVariablesByWorkflowId(variable.workflowId)
|
||||
.map((v) => ({ id: v.id, name: v.name }))
|
||||
|
||||
// Default naming: variableN
|
||||
if (!variable.name || /^variable\d+$/.test(variable.name)) {
|
||||
const existingNumbers = workflowVariables
|
||||
.map((v) => {
|
||||
const match = v.name.match(/^variable(\d+)$/)
|
||||
return match ? Number.parseInt(match[1]) : 0
|
||||
})
|
||||
.filter((n) => !Number.isNaN(n))
|
||||
const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1
|
||||
variable.name = `variable${nextNumber}`
|
||||
}
|
||||
|
||||
// Ensure uniqueness
|
||||
let uniqueName = variable.name
|
||||
let nameIndex = 1
|
||||
while (workflowVariables.some((v) => v.name === uniqueName)) {
|
||||
uniqueName = `${variable.name} (${nameIndex})`
|
||||
nameIndex++
|
||||
}
|
||||
|
||||
if (variable.type === 'string') {
|
||||
variable.type = 'plain'
|
||||
}
|
||||
|
||||
const newVariable: Variable = {
|
||||
id,
|
||||
workflowId: variable.workflowId,
|
||||
name: uniqueName,
|
||||
type: variable.type,
|
||||
value: variable.value ?? '',
|
||||
validationError: undefined,
|
||||
}
|
||||
|
||||
const validationError = validateVariable(newVariable)
|
||||
if (validationError) {
|
||||
newVariable.validationError = validationError
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
variables: {
|
||||
...state.variables,
|
||||
[id]: newVariable,
|
||||
},
|
||||
}))
|
||||
|
||||
return id
|
||||
},
|
||||
|
||||
updateVariable: (id, update) => {
|
||||
set((state) => {
|
||||
const existing = state.variables[id]
|
||||
if (!existing) return state
|
||||
|
||||
// Handle name changes: keep references in sync across workflow values
|
||||
if (update.name !== undefined) {
|
||||
const oldVariableName = existing.name
|
||||
const newName = String(update.name).trim()
|
||||
|
||||
if (!newName) {
|
||||
update = { ...update, name: undefined }
|
||||
} else if (newName !== oldVariableName) {
|
||||
const subBlockStore = useSubBlockStore.getState()
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
|
||||
if (activeWorkflowId) {
|
||||
const workflowValues = subBlockStore.workflowValues[activeWorkflowId] || {}
|
||||
const updatedWorkflowValues = { ...workflowValues }
|
||||
|
||||
Object.entries(workflowValues).forEach(([blockId, blockValues]) => {
|
||||
Object.entries(blockValues as Record<string, any>).forEach(
|
||||
([subBlockId, value]) => {
|
||||
const oldVarName = normalizeName(oldVariableName)
|
||||
const newVarName = normalizeName(newName)
|
||||
const regex = new RegExp(`<variable\\.${oldVarName}>`, 'gi')
|
||||
|
||||
updatedWorkflowValues[blockId][subBlockId] = updateReferences(
|
||||
value,
|
||||
regex,
|
||||
`<variable.${newVarName}>`
|
||||
)
|
||||
|
||||
function updateReferences(
|
||||
val: any,
|
||||
refRegex: RegExp,
|
||||
replacement: string
|
||||
): any {
|
||||
if (typeof val === 'string') {
|
||||
return refRegex.test(val) ? val.replace(refRegex, replacement) : val
|
||||
}
|
||||
if (Array.isArray(val)) {
|
||||
return val.map((item) => updateReferences(item, refRegex, replacement))
|
||||
}
|
||||
if (val !== null && typeof val === 'object') {
|
||||
const result: Record<string, any> = { ...val }
|
||||
for (const key in result) {
|
||||
result[key] = updateReferences(result[key], refRegex, replacement)
|
||||
}
|
||||
return result
|
||||
}
|
||||
return val
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
useSubBlockStore.setState({
|
||||
workflowValues: {
|
||||
...subBlockStore.workflowValues,
|
||||
[activeWorkflowId]: updatedWorkflowValues,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle deprecated -> new type migration
|
||||
if (update.type === 'string') {
|
||||
update = { ...update, type: 'plain' as VariableType }
|
||||
}
|
||||
|
||||
const updated: Variable = {
|
||||
...existing,
|
||||
...update,
|
||||
validationError: undefined,
|
||||
}
|
||||
|
||||
// Validate only when type or value changed
|
||||
if (update.type || update.value !== undefined) {
|
||||
updated.validationError = validateVariable(updated)
|
||||
}
|
||||
|
||||
return {
|
||||
variables: {
|
||||
...state.variables,
|
||||
[id]: updated,
|
||||
},
|
||||
}
|
||||
if (!variable.name || /^variable\d+$/.test(variable.name)) {
|
||||
const existingNumbers = workflowVariables
|
||||
.map((v) => {
|
||||
const match = v.name.match(/^variable(\d+)$/)
|
||||
return match ? Number.parseInt(match[1]) : 0
|
||||
})
|
||||
},
|
||||
.filter((n) => !Number.isNaN(n))
|
||||
|
||||
deleteVariable: (id) => {
|
||||
set((state) => {
|
||||
if (!state.variables[id]) return state
|
||||
const { [id]: _deleted, ...rest } = state.variables
|
||||
return { variables: rest }
|
||||
})
|
||||
},
|
||||
const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1
|
||||
|
||||
getVariablesByWorkflowId: (workflowId) => {
|
||||
return Object.values(get().variables).filter((v) => v.workflowId === workflowId)
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'variables-modal-store',
|
||||
variable.name = `variable${nextNumber}`
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* Get default floating variables modal dimensions.
|
||||
*/
|
||||
export const getDefaultVariablesDimensions = (): VariablesDimensions => ({
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
})
|
||||
let uniqueName = variable.name
|
||||
let nameIndex = 1
|
||||
|
||||
while (workflowVariables.some((v) => v.name === uniqueName)) {
|
||||
uniqueName = `${variable.name} (${nameIndex})`
|
||||
nameIndex++
|
||||
}
|
||||
|
||||
if (variable.type === 'string') {
|
||||
variable.type = 'plain'
|
||||
}
|
||||
|
||||
const newVariable: Variable = {
|
||||
id,
|
||||
workflowId: variable.workflowId,
|
||||
name: uniqueName,
|
||||
type: variable.type,
|
||||
value: variable.value || '',
|
||||
validationError: undefined,
|
||||
}
|
||||
|
||||
const validationError = validateVariable(newVariable)
|
||||
if (validationError) {
|
||||
newVariable.validationError = validationError
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
variables: {
|
||||
...state.variables,
|
||||
[id]: newVariable,
|
||||
},
|
||||
}))
|
||||
|
||||
return id
|
||||
},
|
||||
|
||||
updateVariable: (id, update) => {
|
||||
set((state) => {
|
||||
if (!state.variables[id]) return state
|
||||
|
||||
if (update.name !== undefined) {
|
||||
const oldVariable = state.variables[id]
|
||||
const oldVariableName = oldVariable.name
|
||||
const newName = update.name.trim()
|
||||
|
||||
if (!newName) {
|
||||
update = { ...update }
|
||||
update.name = undefined
|
||||
} else if (newName !== oldVariableName) {
|
||||
const subBlockStore = useSubBlockStore.getState()
|
||||
const targetWorkflowId = oldVariable.workflowId
|
||||
|
||||
if (targetWorkflowId) {
|
||||
const workflowValues = subBlockStore.workflowValues[targetWorkflowId] || {}
|
||||
const updatedWorkflowValues = { ...workflowValues }
|
||||
const changedSubBlocks: Array<{ blockId: string; subBlockId: string; value: any }> =
|
||||
[]
|
||||
|
||||
const oldVarName = normalizeName(oldVariableName)
|
||||
const newVarName = normalizeName(newName)
|
||||
const regex = new RegExp(`<variable\\.${oldVarName}>`, 'gi')
|
||||
|
||||
const updateReferences = (value: any, pattern: RegExp, replacement: string): any => {
|
||||
if (typeof value === 'string') {
|
||||
return pattern.test(value) ? value.replace(pattern, replacement) : value
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => updateReferences(item, pattern, replacement))
|
||||
}
|
||||
|
||||
if (value !== null && typeof value === 'object') {
|
||||
const result = { ...value }
|
||||
for (const key in result) {
|
||||
result[key] = updateReferences(result[key], pattern, replacement)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
Object.entries(workflowValues).forEach(([blockId, blockValues]) => {
|
||||
Object.entries(blockValues as Record<string, any>).forEach(
|
||||
([subBlockId, value]) => {
|
||||
const updatedValue = updateReferences(value, regex, `<variable.${newVarName}>`)
|
||||
|
||||
if (JSON.stringify(updatedValue) !== JSON.stringify(value)) {
|
||||
if (!updatedWorkflowValues[blockId]) {
|
||||
updatedWorkflowValues[blockId] = { ...workflowValues[blockId] }
|
||||
}
|
||||
updatedWorkflowValues[blockId][subBlockId] = updatedValue
|
||||
changedSubBlocks.push({ blockId, subBlockId, value: updatedValue })
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// Update local state
|
||||
useSubBlockStore.setState({
|
||||
workflowValues: {
|
||||
...subBlockStore.workflowValues,
|
||||
[targetWorkflowId]: updatedWorkflowValues,
|
||||
},
|
||||
})
|
||||
|
||||
// Queue operations for persistence via socket
|
||||
const operationQueue = useOperationQueueStore.getState()
|
||||
|
||||
for (const { blockId, subBlockId, value } of changedSubBlocks) {
|
||||
operationQueue.addToQueue({
|
||||
id: crypto.randomUUID(),
|
||||
operation: {
|
||||
operation: 'subblock-update',
|
||||
target: 'subblock',
|
||||
payload: { blockId, subblockId: subBlockId, value },
|
||||
},
|
||||
workflowId: targetWorkflowId,
|
||||
userId: 'system',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (update.type === 'string') {
|
||||
update = { ...update, type: 'plain' }
|
||||
}
|
||||
|
||||
const updatedVariable: Variable = {
|
||||
...state.variables[id],
|
||||
...update,
|
||||
validationError: undefined,
|
||||
}
|
||||
|
||||
if (update.type || update.value !== undefined) {
|
||||
updatedVariable.validationError = validateVariable(updatedVariable)
|
||||
}
|
||||
|
||||
const updated = {
|
||||
...state.variables,
|
||||
[id]: updatedVariable,
|
||||
}
|
||||
|
||||
return { variables: updated }
|
||||
})
|
||||
},
|
||||
|
||||
deleteVariable: (id) => {
|
||||
set((state) => {
|
||||
if (!state.variables[id]) return state
|
||||
|
||||
const { [id]: _, ...rest } = state.variables
|
||||
|
||||
return { variables: rest }
|
||||
})
|
||||
},
|
||||
|
||||
getVariablesByWorkflowId: (workflowId) => {
|
||||
return Object.values(get().variables).filter((variable) => variable.workflowId === workflowId)
|
||||
},
|
||||
}))
|
||||
)
|
||||
|
||||
@@ -1,19 +1,47 @@
|
||||
/**
|
||||
* Variable types supported by the variables modal/editor.
|
||||
* Note: 'string' is deprecated. Use 'plain' for freeform text values instead.
|
||||
* Variable types supported in the application
|
||||
* Note: 'string' is deprecated - use 'plain' for text values instead
|
||||
*/
|
||||
export type VariableType = 'plain' | 'number' | 'boolean' | 'object' | 'array' | 'string'
|
||||
|
||||
/**
|
||||
* Workflow-scoped variable model.
|
||||
* Represents a workflow variable with workflow-specific naming
|
||||
* Variable names must be unique within each workflow
|
||||
*/
|
||||
export interface Variable {
|
||||
id: string
|
||||
workflowId: string
|
||||
name: string
|
||||
name: string // Must be unique per workflow
|
||||
type: VariableType
|
||||
value: unknown
|
||||
validationError?: string
|
||||
validationError?: string // Tracks format validation errors
|
||||
}
|
||||
|
||||
export interface VariablesStore {
|
||||
variables: Record<string, Variable>
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
isEditing: string | null
|
||||
|
||||
/**
|
||||
* Adds a new variable with automatic name uniqueness validation
|
||||
* If a variable with the same name exists, it will be suffixed with a number
|
||||
* Optionally accepts a predetermined ID for collaborative operations
|
||||
*/
|
||||
addVariable: (variable: Omit<Variable, 'id'>, providedId?: string) => string
|
||||
|
||||
/**
|
||||
* Updates a variable, ensuring name remains unique within the workflow
|
||||
* If an updated name conflicts with existing ones, a numbered suffix is added
|
||||
*/
|
||||
updateVariable: (id: string, update: Partial<Omit<Variable, 'id' | 'workflowId'>>) => void
|
||||
|
||||
deleteVariable: (id: string) => void
|
||||
|
||||
/**
|
||||
* Returns all variables for a specific workflow
|
||||
*/
|
||||
getVariablesByWorkflowId: (workflowId: string) => Variable[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,11 +61,10 @@ export interface VariablesDimensions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Public store interface for variables editor/modal.
|
||||
* Combines UI state of the floating modal and the variables data/actions.
|
||||
* UI-only store interface for the floating variables modal.
|
||||
* Variable data lives in the variables data store (`@/stores/variables/store`).
|
||||
*/
|
||||
export interface VariablesStore {
|
||||
// UI State
|
||||
export interface VariablesModalStore {
|
||||
isOpen: boolean
|
||||
position: VariablesPosition | null
|
||||
width: number
|
||||
@@ -46,16 +73,4 @@ export interface VariablesStore {
|
||||
setPosition: (position: VariablesPosition) => void
|
||||
setDimensions: (dimensions: VariablesDimensions) => void
|
||||
resetPosition: () => void
|
||||
|
||||
// Data
|
||||
variables: Record<string, Variable>
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
|
||||
// Actions
|
||||
loadForWorkflow: (workflowId: string) => Promise<void>
|
||||
addVariable: (variable: Omit<Variable, 'id'>, providedId?: string) => string
|
||||
updateVariable: (id: string, update: Partial<Omit<Variable, 'id' | 'workflowId'>>) => void
|
||||
deleteVariable: (id: string) => void
|
||||
getVariablesByWorkflowId: (workflowId: string) => Variable[]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getWorkflows } from '@/hooks/queries/utils/workflow-cache'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { mergeSubblockState } from '@/stores/workflows/utils'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const logger = createLogger('Workflows')
|
||||
|
||||
@@ -30,9 +30,6 @@ export function getWorkflowWithValues(workflowId: string, workspaceId: string) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Get deployment status from registry
|
||||
const deploymentStatus = useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)
|
||||
|
||||
// Use the current state from the store (only available for active workflow)
|
||||
const workflowState: WorkflowState = useWorkflowStore.getState().getWorkflowState()
|
||||
|
||||
@@ -52,110 +49,10 @@ export function getWorkflowWithValues(workflowId: string, workspaceId: string) {
|
||||
loops: workflowState.loops,
|
||||
parallels: workflowState.parallels,
|
||||
lastSaved: workflowState.lastSaved,
|
||||
// Get deployment fields from registry for API compatibility
|
||||
isDeployed: deploymentStatus?.isDeployed || false,
|
||||
deployedAt: deploymentStatus?.deployedAt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific block with its subblock values merged in
|
||||
* @param blockId ID of the block to retrieve
|
||||
* @returns The block with merged subblock values or null if not found
|
||||
*/
|
||||
export function getBlockWithValues(blockId: string): BlockState | null {
|
||||
const workflowState = useWorkflowStore.getState()
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
|
||||
if (!activeWorkflowId || !workflowState.blocks[blockId]) return null
|
||||
|
||||
const mergedBlocks = mergeSubblockState(workflowState.blocks, activeWorkflowId, blockId)
|
||||
return mergedBlocks[blockId] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all workflows with their values merged
|
||||
* Note: Since localStorage has been removed, this only includes the active workflow state
|
||||
* @param workspaceId Workspace containing the workflow metadata
|
||||
* @returns An object containing workflows, with state only for the active workflow
|
||||
*/
|
||||
export function getAllWorkflowsWithValues(workspaceId: string) {
|
||||
const workflows = getWorkflows(workspaceId)
|
||||
const result: Record<
|
||||
string,
|
||||
{
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
color: string
|
||||
folderId?: string | null
|
||||
workspaceId?: string
|
||||
apiKey?: string
|
||||
state: WorkflowState & { isDeployed: boolean; deployedAt?: Date }
|
||||
}
|
||||
> = {}
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
const currentState = useWorkflowStore.getState()
|
||||
|
||||
// Only sync the active workflow to ensure we always send valid state data
|
||||
const activeMetadata = activeWorkflowId
|
||||
? workflows.find((w) => w.id === activeWorkflowId)
|
||||
: undefined
|
||||
if (activeWorkflowId && activeMetadata) {
|
||||
const metadata = activeMetadata
|
||||
|
||||
// Get deployment status from registry
|
||||
const deploymentStatus = useWorkflowRegistry
|
||||
.getState()
|
||||
.getWorkflowDeploymentStatus(activeWorkflowId)
|
||||
|
||||
// Ensure state has all required fields for Zod validation
|
||||
const workflowState: WorkflowState = {
|
||||
...useWorkflowStore.getState().getWorkflowState(),
|
||||
// Ensure fallback values for safer handling
|
||||
blocks: currentState.blocks || {},
|
||||
edges: currentState.edges || [],
|
||||
loops: currentState.loops || {},
|
||||
parallels: currentState.parallels || {},
|
||||
lastSaved: currentState.lastSaved || Date.now(),
|
||||
}
|
||||
|
||||
// Merge the subblock values for this specific workflow
|
||||
const mergedBlocks = mergeSubblockState(workflowState.blocks, activeWorkflowId)
|
||||
|
||||
// Include the API key in the state if it exists in the deployment status
|
||||
const apiKey = deploymentStatus?.apiKey
|
||||
|
||||
result[activeWorkflowId] = {
|
||||
id: activeWorkflowId,
|
||||
name: metadata.name,
|
||||
description: metadata.description,
|
||||
color: metadata.color || '#3972F6',
|
||||
folderId: metadata.folderId,
|
||||
state: {
|
||||
blocks: mergedBlocks,
|
||||
edges: workflowState.edges,
|
||||
loops: workflowState.loops,
|
||||
parallels: workflowState.parallels,
|
||||
lastSaved: workflowState.lastSaved,
|
||||
// Get deployment fields from registry for API compatibility
|
||||
isDeployed: deploymentStatus?.isDeployed || false,
|
||||
deployedAt: deploymentStatus?.deployedAt,
|
||||
},
|
||||
// Include API key if available
|
||||
apiKey,
|
||||
}
|
||||
|
||||
// Only include workspaceId if it's not null/undefined
|
||||
if (metadata.workspaceId) {
|
||||
result[activeWorkflowId].workspaceId = metadata.workspaceId
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
export type { WorkflowMetadata } from '@/stores/workflows/registry/types'
|
||||
export { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
|
||||
@@ -3,14 +3,12 @@ import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
|
||||
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
|
||||
import type { WorkflowDeploymentInfo } from '@/hooks/queries/deployments'
|
||||
import { deploymentKeys } from '@/hooks/queries/deployments'
|
||||
import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists'
|
||||
import { useVariablesStore } from '@/stores/panel/variables/store'
|
||||
import type { Variable } from '@/stores/panel/variables/types'
|
||||
import type {
|
||||
DeploymentStatus,
|
||||
HydrationState,
|
||||
WorkflowRegistry,
|
||||
} from '@/stores/workflows/registry/types'
|
||||
import { useVariablesStore } from '@/stores/variables/store'
|
||||
import type { Variable } from '@/stores/variables/types'
|
||||
import type { HydrationState, WorkflowRegistry } from '@/stores/workflows/registry/types'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { getUniqueBlockName, regenerateBlockIds } from '@/stores/workflows/utils'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
@@ -34,7 +32,6 @@ function resetWorkflowStores() {
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
deploymentStatuses: {},
|
||||
lastSaved: Date.now(),
|
||||
})
|
||||
|
||||
@@ -48,7 +45,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
(set, get) => ({
|
||||
activeWorkflowId: null,
|
||||
error: null,
|
||||
deploymentStatuses: {},
|
||||
hydration: initialHydration,
|
||||
clipboard: null,
|
||||
pendingSelection: null,
|
||||
@@ -61,7 +57,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
|
||||
set({
|
||||
activeWorkflowId: null,
|
||||
deploymentStatuses: {},
|
||||
error: null,
|
||||
hydration: {
|
||||
phase: 'idle',
|
||||
@@ -73,74 +68,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
})
|
||||
},
|
||||
|
||||
getWorkflowDeploymentStatus: (workflowId: string | null): DeploymentStatus | null => {
|
||||
if (!workflowId) {
|
||||
workflowId = get().activeWorkflowId
|
||||
if (!workflowId) return null
|
||||
}
|
||||
|
||||
const { deploymentStatuses = {} } = get()
|
||||
|
||||
if (deploymentStatuses[workflowId]) {
|
||||
return deploymentStatuses[workflowId]
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
setDeploymentStatus: (
|
||||
workflowId: string | null,
|
||||
isDeployed: boolean,
|
||||
deployedAt?: Date,
|
||||
apiKey?: string
|
||||
) => {
|
||||
if (!workflowId) {
|
||||
workflowId = get().activeWorkflowId
|
||||
if (!workflowId) return
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
deploymentStatuses: {
|
||||
...state.deploymentStatuses,
|
||||
[workflowId as string]: {
|
||||
isDeployed,
|
||||
deployedAt: deployedAt || (isDeployed ? new Date() : undefined),
|
||||
apiKey,
|
||||
needsRedeployment: isDeployed
|
||||
? false
|
||||
: (state.deploymentStatuses?.[workflowId as string]?.needsRedeployment ?? false),
|
||||
},
|
||||
},
|
||||
}))
|
||||
},
|
||||
|
||||
setWorkflowNeedsRedeployment: (workflowId: string | null, needsRedeployment: boolean) => {
|
||||
if (!workflowId) {
|
||||
workflowId = get().activeWorkflowId
|
||||
if (!workflowId) return
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
const deploymentStatuses = state.deploymentStatuses || {}
|
||||
const currentStatus = deploymentStatuses[workflowId as string] || { isDeployed: false }
|
||||
|
||||
return {
|
||||
deploymentStatuses: {
|
||||
...deploymentStatuses,
|
||||
[workflowId as string]: {
|
||||
...currentStatus,
|
||||
needsRedeployment,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const { activeWorkflowId } = get()
|
||||
if (workflowId === activeWorkflowId) {
|
||||
useWorkflowStore.getState().setNeedsRedeploymentFlag(needsRedeployment)
|
||||
}
|
||||
},
|
||||
|
||||
loadWorkflowState: async (workflowId: string) => {
|
||||
const workspaceId = get().hydration.workspaceId
|
||||
if (!workspaceId) {
|
||||
@@ -170,20 +97,19 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
}
|
||||
|
||||
const workflowData = (await response.json()).data
|
||||
const nextDeploymentStatuses =
|
||||
workflowData?.isDeployed || workflowData?.deployedAt
|
||||
? {
|
||||
...get().deploymentStatuses,
|
||||
[workflowId]: {
|
||||
isDeployed: workflowData.isDeployed || false,
|
||||
deployedAt: workflowData.deployedAt
|
||||
? new Date(workflowData.deployedAt)
|
||||
: undefined,
|
||||
apiKey: workflowData.apiKey || undefined,
|
||||
needsRedeployment: false,
|
||||
},
|
||||
}
|
||||
: get().deploymentStatuses
|
||||
|
||||
if (workflowData?.isDeployed !== undefined) {
|
||||
getQueryClient().setQueryData<WorkflowDeploymentInfo>(
|
||||
deploymentKeys.info(workflowId),
|
||||
(prev) => ({
|
||||
isDeployed: workflowData.isDeployed ?? false,
|
||||
deployedAt: workflowData.deployedAt ?? null,
|
||||
apiKey: workflowData.apiKey ?? prev?.apiKey ?? null,
|
||||
needsRedeployment: prev?.needsRedeployment ?? false,
|
||||
isPublicApi: prev?.isPublicApi ?? false,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
let workflowState: WorkflowState
|
||||
|
||||
@@ -195,7 +121,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
loops: workflowData.state.loops || {},
|
||||
parallels: workflowData.state.parallels || {},
|
||||
lastSaved: Date.now(),
|
||||
deploymentStatuses: nextDeploymentStatuses,
|
||||
}
|
||||
} else {
|
||||
workflowState = {
|
||||
@@ -204,7 +129,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
deploymentStatuses: nextDeploymentStatuses,
|
||||
lastSaved: Date.now(),
|
||||
}
|
||||
|
||||
@@ -250,7 +174,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
set((state) => ({
|
||||
activeWorkflowId: workflowId,
|
||||
error: null,
|
||||
deploymentStatuses: nextDeploymentStatuses,
|
||||
hydration: {
|
||||
phase: 'ready',
|
||||
workspaceId: state.hydration.workspaceId,
|
||||
@@ -367,7 +290,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
|
||||
set({
|
||||
activeWorkflowId: null,
|
||||
deploymentStatuses: {},
|
||||
error: null,
|
||||
hydration: initialHydration,
|
||||
clipboard: null,
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import type { Edge } from 'reactflow'
|
||||
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
|
||||
|
||||
export interface DeploymentStatus {
|
||||
isDeployed: boolean
|
||||
deployedAt?: Date
|
||||
apiKey?: string
|
||||
needsRedeployment?: boolean
|
||||
}
|
||||
|
||||
export interface ClipboardData {
|
||||
blocks: Record<string, BlockState>
|
||||
edges: Edge[]
|
||||
@@ -45,7 +38,6 @@ export interface HydrationState {
|
||||
export interface WorkflowRegistryState {
|
||||
activeWorkflowId: string | null
|
||||
error: string | null
|
||||
deploymentStatuses: Record<string, DeploymentStatus>
|
||||
hydration: HydrationState
|
||||
clipboard: ClipboardData | null
|
||||
pendingSelection: string[] | null
|
||||
@@ -57,14 +49,6 @@ export interface WorkflowRegistryActions {
|
||||
switchToWorkspace: (id: string) => void
|
||||
markWorkflowCreating: (workflowId: string) => void
|
||||
markWorkflowCreated: (workflowId: string | null) => void
|
||||
getWorkflowDeploymentStatus: (workflowId: string | null) => DeploymentStatus | null
|
||||
setDeploymentStatus: (
|
||||
workflowId: string | null,
|
||||
isDeployed: boolean,
|
||||
deployedAt?: Date,
|
||||
apiKey?: string
|
||||
) => void
|
||||
setWorkflowNeedsRedeployment: (workflowId: string | null, needsRedeployment: boolean) => void
|
||||
copyBlocks: (blockIds: string[]) => void
|
||||
preparePasteData: (positionOffset?: { x: number; y: number }) => {
|
||||
blocks: Record<string, BlockState>
|
||||
|
||||
@@ -107,8 +107,6 @@ const initialState = {
|
||||
loops: {},
|
||||
parallels: {},
|
||||
lastSaved: undefined,
|
||||
deploymentStatuses: {},
|
||||
needsRedeployment: false,
|
||||
}
|
||||
|
||||
export const useWorkflowStore = create<WorkflowStore>()(
|
||||
@@ -116,10 +114,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
||||
(set, get) => ({
|
||||
...initialState,
|
||||
|
||||
setNeedsRedeploymentFlag: (needsRedeployment: boolean) => {
|
||||
set({ needsRedeployment })
|
||||
},
|
||||
|
||||
setCurrentWorkflowId: (currentWorkflowId) => {
|
||||
set({ currentWorkflowId })
|
||||
},
|
||||
@@ -540,8 +534,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
||||
loops: state.loops,
|
||||
parallels: state.parallels,
|
||||
lastSaved: state.lastSaved,
|
||||
deploymentStatuses: state.deploymentStatuses,
|
||||
needsRedeployment: state.needsRedeployment,
|
||||
}
|
||||
},
|
||||
replaceWorkflowState: (
|
||||
@@ -580,11 +572,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
||||
edges: nextEdges,
|
||||
loops: nextLoops,
|
||||
parallels: nextParallels,
|
||||
deploymentStatuses: nextState.deploymentStatuses || state.deploymentStatuses,
|
||||
needsRedeployment:
|
||||
nextState.needsRedeployment !== undefined
|
||||
? nextState.needsRedeployment
|
||||
: state.needsRedeployment,
|
||||
lastSaved:
|
||||
options?.updateLastSaved === true
|
||||
? Date.now()
|
||||
@@ -1141,67 +1128,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
||||
}))
|
||||
},
|
||||
|
||||
revertToDeployedState: async (deployedState: WorkflowState) => {
|
||||
const activeWorkflowId = get().currentWorkflowId
|
||||
|
||||
if (!activeWorkflowId) {
|
||||
logger.error('Cannot revert: no active workflow ID')
|
||||
return
|
||||
}
|
||||
|
||||
const deploymentStatus = get().deploymentStatuses?.[activeWorkflowId]
|
||||
|
||||
get().replaceWorkflowState({
|
||||
...deployedState,
|
||||
needsRedeployment: false,
|
||||
deploymentStatuses: {
|
||||
...get().deploymentStatuses,
|
||||
...(deploymentStatus ? { [activeWorkflowId]: deploymentStatus } : {}),
|
||||
},
|
||||
})
|
||||
|
||||
const values: Record<string, Record<string, any>> = {}
|
||||
Object.entries(deployedState.blocks).forEach(([blockId, block]) => {
|
||||
values[blockId] = {}
|
||||
Object.entries(block.subBlocks || {}).forEach(([subBlockId, subBlock]) => {
|
||||
values[blockId][subBlockId] = subBlock.value
|
||||
})
|
||||
})
|
||||
|
||||
useSubBlockStore.setState({
|
||||
workflowValues: {
|
||||
...useSubBlockStore.getState().workflowValues,
|
||||
[activeWorkflowId]: values,
|
||||
},
|
||||
})
|
||||
|
||||
get().updateLastSaved()
|
||||
|
||||
// Call API to persist the revert to normalized tables
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/workflows/${activeWorkflowId}/deployments/active/revert`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
logger.error('Failed to persist revert to deployed state:', errorData.error)
|
||||
// Don't throw error to avoid breaking the UI, but log it
|
||||
} else {
|
||||
logger.info('Successfully persisted revert to deployed state')
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error calling revert to deployed API:', error)
|
||||
// Don't throw error to avoid breaking the UI
|
||||
}
|
||||
},
|
||||
|
||||
toggleBlockAdvancedMode: (id: string) => {
|
||||
const block = get().blocks[id]
|
||||
if (!block) return
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Edge } from 'reactflow'
|
||||
import type { OutputFieldDefinition, SubBlockType } from '@/blocks/types'
|
||||
import type { DeploymentStatus } from '@/stores/workflows/registry/types'
|
||||
|
||||
export const SUBFLOW_TYPES = {
|
||||
LOOP: 'loop',
|
||||
@@ -173,8 +172,6 @@ export interface WorkflowState {
|
||||
exportedAt?: string
|
||||
}
|
||||
variables?: Record<string, Variable>
|
||||
deploymentStatuses?: Record<string, DeploymentStatus>
|
||||
needsRedeployment?: boolean
|
||||
dragStartPosition?: DragStartPosition | null
|
||||
}
|
||||
|
||||
@@ -228,8 +225,6 @@ export interface WorkflowActions {
|
||||
updateParallelType: (parallelId: string, parallelType: 'count' | 'collection') => void
|
||||
generateLoopBlocks: () => Record<string, Loop>
|
||||
generateParallelBlocks: () => Record<string, Parallel>
|
||||
setNeedsRedeploymentFlag: (needsRedeployment: boolean) => void
|
||||
revertToDeployedState: (deployedState: WorkflowState) => void
|
||||
toggleBlockAdvancedMode: (id: string) => void
|
||||
setDragStartPosition: (position: DragStartPosition | null) => void
|
||||
getDragStartPosition: () => DragStartPosition | null
|
||||
|
||||
@@ -25,6 +25,7 @@ const {
|
||||
mockGetCustomToolById,
|
||||
mockListCustomTools,
|
||||
mockGetCustomToolByIdOrTitle,
|
||||
mockGenerateInternalToken,
|
||||
} = vi.hoisted(() => ({
|
||||
mockIsHosted: { value: false },
|
||||
mockEnv: { NEXT_PUBLIC_APP_URL: 'http://localhost:3000' } as Record<string, string | undefined>,
|
||||
@@ -38,6 +39,7 @@ const {
|
||||
mockGetCustomToolById: vi.fn(),
|
||||
mockListCustomTools: vi.fn(),
|
||||
mockGetCustomToolByIdOrTitle: vi.fn(),
|
||||
mockGenerateInternalToken: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock feature flags
|
||||
@@ -65,6 +67,10 @@ vi.mock('@/lib/api-key/byok', () => ({
|
||||
getBYOKKey: (...args: unknown[]) => mockGetBYOKKey(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/internal', () => ({
|
||||
generateInternalToken: (...args: unknown[]) => mockGenerateInternalToken(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/billing/core/usage-log', () => ({}))
|
||||
|
||||
vi.mock('@/lib/core/rate-limiter/hosted-key', () => ({
|
||||
@@ -193,8 +199,8 @@ vi.mock('@/tools/registry', () => {
|
||||
return { tools: mockTools }
|
||||
})
|
||||
|
||||
// Mock custom tools - define mock data inside factory function
|
||||
vi.mock('@/hooks/queries/utils/custom-tool-cache', () => {
|
||||
// Mock query client for custom tool cache reads
|
||||
vi.mock('@/app/_shell/providers/get-query-client', () => {
|
||||
const mockCustomTool = {
|
||||
id: 'custom-tool-123',
|
||||
title: 'Custom Weather Tool',
|
||||
@@ -214,13 +220,12 @@ vi.mock('@/hooks/queries/utils/custom-tool-cache', () => {
|
||||
},
|
||||
}
|
||||
return {
|
||||
getCustomTool: (toolId: string) => {
|
||||
if (toolId === 'custom-tool-123') {
|
||||
return mockCustomTool
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
getCustomTools: () => [mockCustomTool],
|
||||
getQueryClient: () => ({
|
||||
getQueryData: (key: string[]) => {
|
||||
if (key[0] === 'customTools') return [mockCustomTool]
|
||||
return undefined
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1155,6 +1160,34 @@ describe('MCP Tool Execution', () => {
|
||||
expect(result.timing).toBeDefined()
|
||||
})
|
||||
|
||||
it('should embed userId in JWT when executionContext is undefined (agent block path)', async () => {
|
||||
mockGenerateInternalToken.mockResolvedValue('test-token')
|
||||
|
||||
global.fetch = Object.assign(
|
||||
vi.fn().mockImplementation(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
success: true,
|
||||
data: { output: { content: [{ type: 'text', text: 'OK' }] } },
|
||||
}),
|
||||
})),
|
||||
{ preconnect: vi.fn() }
|
||||
) as typeof fetch
|
||||
|
||||
await executeTool('mcp-123-test_tool', {
|
||||
query: 'test',
|
||||
_context: {
|
||||
workspaceId: 'workspace-456',
|
||||
workflowId: 'workflow-789',
|
||||
userId: 'user-abc',
|
||||
},
|
||||
})
|
||||
|
||||
expect(mockGenerateInternalToken).toHaveBeenCalledWith('user-abc')
|
||||
})
|
||||
|
||||
describe('Tool request retries', () => {
|
||||
function makeJsonResponse(
|
||||
status: number,
|
||||
|
||||
@@ -1552,11 +1552,13 @@ async function executeMcpTool(
|
||||
|
||||
const baseUrl = getInternalApiBaseUrl()
|
||||
|
||||
const mcpScope = resolveToolScope(params, executionContext)
|
||||
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
try {
|
||||
const internalToken = await generateInternalToken(executionContext?.userId)
|
||||
const internalToken = await generateInternalToken(mcpScope.userId)
|
||||
headers.Authorization = `Bearer ${internalToken}`
|
||||
} catch (error) {
|
||||
logger.error(`[${actualRequestId}] Failed to generate internal token:`, error)
|
||||
@@ -1587,8 +1589,6 @@ async function executeMcpTool(
|
||||
)
|
||||
}
|
||||
|
||||
const mcpScope = resolveToolScope(params, executionContext)
|
||||
|
||||
if (mcpScope.callChain && mcpScope.callChain.length > 0) {
|
||||
headers[SIM_VIA_HEADER] = serializeCallChain(mcpScope.callChain)
|
||||
}
|
||||
|
||||
@@ -21,24 +21,23 @@ vi.mock('@/lib/core/security/input-validation.server', () => ({
|
||||
secureFetchWithPinnedIP: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/settings/environment', () => {
|
||||
const mockStore = {
|
||||
getAllVariables: vi.fn().mockReturnValue({
|
||||
API_KEY: { value: 'mock-api-key' },
|
||||
BASE_URL: { value: 'https://example.com' },
|
||||
}),
|
||||
}
|
||||
const { mockGetQueryData } = vi.hoisted(() => ({
|
||||
mockGetQueryData: vi.fn(),
|
||||
}))
|
||||
|
||||
return {
|
||||
useEnvironmentStore: {
|
||||
getState: vi.fn().mockImplementation(() => mockStore),
|
||||
},
|
||||
}
|
||||
})
|
||||
vi.mock('@/app/_shell/providers/get-query-client', () => ({
|
||||
getQueryClient: () => ({
|
||||
getQueryData: mockGetQueryData,
|
||||
}),
|
||||
}))
|
||||
|
||||
const originalWindow = global.window
|
||||
beforeEach(() => {
|
||||
global.window = {} as any
|
||||
mockGetQueryData.mockReturnValue({
|
||||
API_KEY: { key: 'API_KEY', value: 'mock-api-key' },
|
||||
BASE_URL: { key: 'BASE_URL', value: 'https://example.com' },
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -651,15 +650,8 @@ describe('createParamSchema', () => {
|
||||
})
|
||||
|
||||
describe('getClientEnvVars', () => {
|
||||
it.concurrent('should return environment variables from store in browser environment', () => {
|
||||
const mockStoreGetter = () => ({
|
||||
getAllVariables: () => ({
|
||||
API_KEY: { value: 'mock-api-key' },
|
||||
BASE_URL: { value: 'https://example.com' },
|
||||
}),
|
||||
})
|
||||
|
||||
const result = getClientEnvVars(mockStoreGetter)
|
||||
it('should return environment variables from React Query cache in browser environment', () => {
|
||||
const result = getClientEnvVars()
|
||||
|
||||
expect(result).toEqual({
|
||||
API_KEY: 'mock-api-key',
|
||||
@@ -667,7 +659,7 @@ describe('getClientEnvVars', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it.concurrent('should return empty object in server environment', () => {
|
||||
it('should return empty object in server environment', () => {
|
||||
global.window = undefined as any
|
||||
|
||||
const result = getClientEnvVars()
|
||||
@@ -677,7 +669,7 @@ describe('getClientEnvVars', () => {
|
||||
})
|
||||
|
||||
describe('createCustomToolRequestBody', () => {
|
||||
it.concurrent('should create request body function for client-side execution', () => {
|
||||
it('should create request body function for client-side execution', () => {
|
||||
const customTool = {
|
||||
code: 'return a + b',
|
||||
schema: {
|
||||
@@ -687,14 +679,7 @@ describe('createCustomToolRequestBody', () => {
|
||||
},
|
||||
}
|
||||
|
||||
const mockStoreGetter = () => ({
|
||||
getAllVariables: () => ({
|
||||
API_KEY: { value: 'mock-api-key' },
|
||||
BASE_URL: { value: 'https://example.com' },
|
||||
}),
|
||||
})
|
||||
|
||||
const bodyFn = createCustomToolRequestBody(customTool, true, undefined, mockStoreGetter)
|
||||
const bodyFn = createCustomToolRequestBody(customTool, true)
|
||||
const result = bodyFn({ a: 5, b: 3 })
|
||||
|
||||
expect(result).toEqual({
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
|
||||
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
|
||||
import type { CustomToolDefinition } from '@/hooks/queries/custom-tools'
|
||||
import { useEnvironmentStore } from '@/stores/settings/environment'
|
||||
import { environmentKeys } from '@/hooks/queries/environment'
|
||||
import type { EnvironmentVariable } from '@/stores/settings/environment'
|
||||
import { tools } from '@/tools/registry'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
@@ -215,20 +217,20 @@ export function createParamSchema(customTool: any): Record<string, any> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get environment variables from store (client-side only)
|
||||
* @param getStore Optional function to get the store (useful for testing)
|
||||
* Get environment variables from React Query cache (client-side only)
|
||||
*/
|
||||
export function getClientEnvVars(getStore?: () => any): Record<string, string> {
|
||||
export function getClientEnvVars(): Record<string, string> {
|
||||
if (typeof window === 'undefined') return {}
|
||||
|
||||
try {
|
||||
// Allow injecting the store for testing
|
||||
const envStore = getStore ? getStore() : useEnvironmentStore.getState()
|
||||
const allEnvVars = envStore.getAllVariables()
|
||||
const allEnvVars =
|
||||
getQueryClient().getQueryData<Record<string, EnvironmentVariable>>(
|
||||
environmentKeys.personal()
|
||||
) ?? {}
|
||||
|
||||
// Convert environment variables to a simple key-value object
|
||||
return Object.entries(allEnvVars).reduce(
|
||||
(acc, [key, variable]: [string, any]) => {
|
||||
(acc, [key, variable]) => {
|
||||
acc[key] = variable.value
|
||||
return acc
|
||||
},
|
||||
@@ -245,20 +247,14 @@ export function getClientEnvVars(getStore?: () => any): Record<string, string> {
|
||||
* @param customTool The custom tool configuration
|
||||
* @param isClient Whether running on client side
|
||||
* @param workflowId Optional workflow ID for server-side
|
||||
* @param getStore Optional function to get the store (useful for testing)
|
||||
*/
|
||||
export function createCustomToolRequestBody(
|
||||
customTool: any,
|
||||
isClient = true,
|
||||
workflowId?: string,
|
||||
getStore?: () => any
|
||||
) {
|
||||
export function createCustomToolRequestBody(customTool: any, isClient = true, workflowId?: string) {
|
||||
return (params: Record<string, any>) => {
|
||||
// Get environment variables - try multiple sources in order of preference:
|
||||
// 1. envVars parameter (passed from provider/agent context)
|
||||
// 2. Client-side store (if running in browser)
|
||||
// 3. Empty object (fallback)
|
||||
const envVars = params.envVars || (isClient ? getClientEnvVars(getStore) : {})
|
||||
const envVars = params.envVars || (isClient ? getClientEnvVars() : {})
|
||||
|
||||
// Get workflow variables from params (passed from execution context)
|
||||
const workflowVariables = params.workflowVariables || {}
|
||||
|
||||
Reference in New Issue
Block a user