From 511df81201e163530a927d92ac18c2358de39453 Mon Sep 17 00:00:00 2001 From: hjlarry Date: Sun, 18 Jan 2026 13:40:12 +0800 Subject: [PATCH] fix web style --- .../[appId]/overview/card-view.tsx | 13 +- web/app/components/app-sidebar/app-info.tsx | 2 +- .../app/app-publisher/features-wrapper.tsx | 7 +- .../components/app/app-publisher/index.tsx | 22 ++- .../components/app/configuration/index.tsx | 4 +- .../hooks/use-available-nodes-meta-data.ts | 4 +- .../hooks/use-get-run-and-trace-url.ts | 8 +- .../components/tools/mcp/mcp-service-card.tsx | 51 ++++-- .../workflow-header/features-trigger.spec.tsx | 2 +- .../workflow-header/features-trigger.tsx | 8 +- .../workflow-app/components/workflow-main.tsx | 41 +++-- .../hooks/use-available-nodes-meta-data.ts | 4 +- .../hooks/use-get-run-and-trace-url.ts | 8 +- .../hooks/use-nodes-sync-draft.ts | 8 +- .../workflow/block-selector/index.tsx | 2 +- .../core/collaboration-manager.ts | 166 +++++++++++------- .../collaboration/core/crdt-provider.ts | 2 +- .../collaboration/core/event-emitter.ts | 14 +- .../collaboration/core/websocket-manager.ts | 18 +- .../collaboration/hooks/use-collaboration.ts | 69 +++++--- .../workflow/collaboration/types/events.ts | 42 ++--- .../workflow/comment/mention-input.tsx | 23 ++- .../components/workflow/comment/thread.tsx | 4 +- web/app/components/workflow/features.tsx | 2 - .../workflow/header/online-users.tsx | 7 +- .../components/workflow/header/undo-redo.tsx | 4 +- .../components/workflow/hooks-store/store.ts | 30 ++-- .../workflow/hooks/use-checklist.ts | 5 +- .../workflow/hooks/use-node-data-update.ts | 6 +- .../workflow/hooks/use-nodes-interactions.ts | 45 +++-- .../workflow/hooks/use-workflow-comment.ts | 39 ++-- web/app/components/workflow/index.tsx | 56 ++++-- .../components/title-description-input.tsx | 4 +- .../workflow/nodes/data-source-empty/hooks.ts | 7 +- .../nodes/http/hooks/use-key-value-list.ts | 16 +- .../nodes/iteration/use-interactions.ts | 3 +- .../workflow/nodes/loop/use-interactions.ts | 2 +- .../workflow/operator/add-block.tsx | 7 +- web/eslint-suppressions.json | 26 --- web/next.config.js | 2 +- 40 files changed, 452 insertions(+), 331 deletions(-) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx index 7968c55e36..c05094357f 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx @@ -91,13 +91,15 @@ const CardView: FC = ({ appId, isInPanel, className }) => { ? buildTriggerModeMessage(t('mcp.server.title', { ns: 'tools' })) : null - const updateAppDetail = async () => { + const updateAppDetail = useCallback(async () => { try { const res = await fetchAppDetail({ url: '/apps', id: appId }) setAppDetail({ ...res }) } - catch (error) { console.error(error) } - } + catch (error) { + console.error(error) + } + }, [appId, setAppDetail]) const handleCallbackResult = (err: Error | null, message?: I18nKeysByPrefix<'common', 'actionMsg.'>) => { const type = err ? 'error' : 'success' @@ -129,9 +131,8 @@ const CardView: FC = ({ appId, isInPanel, className }) => { if (!appId) return - const unsubscribe = collaborationManager.onAppStateUpdate(async (update: any) => { + const unsubscribe = collaborationManager.onAppStateUpdate(async () => { try { - console.log('Received app state update from collaboration:', update) // Update app detail when other clients modify app state await updateAppDetail() } @@ -141,7 +142,7 @@ const CardView: FC = ({ appId, isInPanel, className }) => { }) return unsubscribe - }, [appId]) + }, [appId, updateAppDetail]) const onChangeSiteStatus = async (value: boolean) => { const [err] = await asyncRunSafe( diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx index ad928e7f14..ebfc75891f 100644 --- a/web/app/components/app-sidebar/app-info.tsx +++ b/web/app/components/app-sidebar/app-info.tsx @@ -90,7 +90,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx timestamp: Date.now(), }) } - }, [appDetail?.id]) + }, [appDetail]) const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({ name, diff --git a/web/app/components/app/app-publisher/features-wrapper.tsx b/web/app/components/app/app-publisher/features-wrapper.tsx index 381e9a553e..d1a23b008e 100644 --- a/web/app/components/app/app-publisher/features-wrapper.tsx +++ b/web/app/components/app/app-publisher/features-wrapper.tsx @@ -1,6 +1,7 @@ import type { AppPublisherProps } from '@/app/components/app/app-publisher' import type { ModelAndParameter } from '@/app/components/app/configuration/debug/types' import type { FileUpload } from '@/app/components/base/features/types' +import type { PublishWorkflowParams } from '@/types/workflow' import { produce } from 'immer' import * as React from 'react' import { useCallback, useState } from 'react' @@ -13,7 +14,7 @@ import { SupportUploadFileTypes } from '@/app/components/workflow/types' import { Resolution } from '@/types/app' type Props = Omit & { - onPublish?: (modelAndParameter?: ModelAndParameter, features?: any) => Promise | any + onPublish?: (params?: ModelAndParameter | PublishWorkflowParams, features?: any) => Promise | any publishedConfig?: any resetAppConfig?: () => void } @@ -62,8 +63,8 @@ const FeaturesWrappedAppPublisher = (props: Props) => { setRestoreConfirmOpen(false) }, [featuresStore, props]) - const handlePublish = useCallback((modelAndParameter?: ModelAndParameter) => { - return props.onPublish?.(modelAndParameter, features) + const handlePublish = useCallback((params?: ModelAndParameter | PublishWorkflowParams) => { + return props.onPublish?.(params, features) }, [features, props]) return ( diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 5db9ec73e5..3f882dd435 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -1,5 +1,7 @@ import type { ModelAndParameter } from '../configuration/debug/types' +import type { CollaborationUpdate } from '@/app/components/workflow/collaboration/types/collaboration' import type { InputVar, Variable } from '@/app/components/workflow/types' +import type { InstalledApp } from '@/models/explore' import type { I18nKeysByPrefix } from '@/types/i18n' import type { PublishWorkflowParams } from '@/types/workflow' import { @@ -59,6 +61,10 @@ import SuggestedAction from './suggested-action' type AccessModeLabel = I18nKeysByPrefix<'app', 'accessControlDialog.accessItems.'> +type InstalledAppsResponse = { + installed_apps?: InstalledApp[] +} + const ACCESS_MODE_MAP: Record = { [AccessMode.ORGANIZATION]: { label: 'organization', @@ -105,8 +111,8 @@ export type AppPublisherProps = { debugWithMultipleModel?: boolean multipleModelConfigs?: ModelAndParameter[] /** modelAndParameter is passed when debugWithMultipleModel is true */ - onPublish?: (params?: any) => Promise | any - onRestore?: () => Promise | any + onPublish?: (params?: ModelAndParameter | PublishWorkflowParams) => Promise | void + onRestore?: () => Promise | void onToggle?: (state: boolean) => void crossAxisOffset?: number toolPublished?: boolean @@ -248,9 +254,10 @@ const AppPublisher = ({ await openAsyncWindow(async () => { if (!appDetail?.id) throw new Error('App not found') - const { installed_apps }: any = await fetchInstalledAppList(appDetail?.id) || {} - if (installed_apps?.length > 0) - return `${basePath}/explore/installed/${installed_apps[0].id}` + const response = (await fetchInstalledAppList(appDetail?.id)) as InstalledAppsResponse + const installedApps = response?.installed_apps + if (installedApps?.length) + return `${basePath}/explore/installed/${installedApps[0].id}` throw new Error('No app found in Explore') }, { onError: (err) => { @@ -283,8 +290,9 @@ const AppPublisher = ({ if (!appId) return - const unsubscribe = collaborationManager.onAppPublishUpdate((update: any) => { - if (update?.data?.action === 'published') + const unsubscribe = collaborationManager.onAppPublishUpdate((update: CollaborationUpdate) => { + const action = typeof update.data.action === 'string' ? update.data.action : undefined + if (action === 'published') invalidateAppWorkflow(appId) }) diff --git a/web/app/components/app/configuration/index.tsx b/web/app/components/app/configuration/index.tsx index 919b7c355a..e0ae70272d 100644 --- a/web/app/components/app/configuration/index.tsx +++ b/web/app/components/app/configuration/index.tsx @@ -18,6 +18,7 @@ import type { TextToSpeechConfig, } from '@/models/debug' import type { ModelConfig as BackendModelConfig, UserInputFormItem, VisionSettings } from '@/types/app' +import type { PublishWorkflowParams } from '@/types/workflow' import { CodeBracketIcon } from '@heroicons/react/20/solid' import { useBoolean, useGetState } from 'ahooks' import { clone } from 'es-toolkit/object' @@ -760,7 +761,8 @@ const Configuration: FC = () => { else { return promptEmpty } })() const contextVarEmpty = mode === AppModeEnum.COMPLETION && dataSets.length > 0 && !hasSetContextVar - const onPublish = async (modelAndParameter?: ModelAndParameter, features?: FeaturesData) => { + const onPublish = async (params?: ModelAndParameter | PublishWorkflowParams, features?: FeaturesData) => { + const modelAndParameter = params && 'model' in params ? params : undefined const modelId = modelAndParameter?.model || modelConfig.model_id const promptTemplate = modelConfig.configs.prompt_template const promptVariables = modelConfig.configs.prompt_variables diff --git a/web/app/components/rag-pipeline/hooks/use-available-nodes-meta-data.ts b/web/app/components/rag-pipeline/hooks/use-available-nodes-meta-data.ts index 0b574145b4..797a87f976 100644 --- a/web/app/components/rag-pipeline/hooks/use-available-nodes-meta-data.ts +++ b/web/app/components/rag-pipeline/hooks/use-available-nodes-meta-data.ts @@ -23,7 +23,7 @@ export const useAvailableNodesMetaData = () => { }, knowledgeBaseDefault, dataSourceEmptyDefault, - ], []) + ] as AvailableNodesMetaData['nodes'], []) const helpLinkUri = useMemo(() => { if (language === 'zh_Hans') @@ -52,7 +52,7 @@ export const useAvailableNodesMetaData = () => { title, }, } - }), [mergedNodesMetaData, t]) + }) as AvailableNodesMetaData['nodes'], [mergedNodesMetaData, t]) const availableNodesMetaDataMap = useMemo(() => availableNodesMetaData.reduce((acc, node) => { acc![node.metaData.type] = node diff --git a/web/app/components/rag-pipeline/hooks/use-get-run-and-trace-url.ts b/web/app/components/rag-pipeline/hooks/use-get-run-and-trace-url.ts index f9988b60f8..8ad0861689 100644 --- a/web/app/components/rag-pipeline/hooks/use-get-run-and-trace-url.ts +++ b/web/app/components/rag-pipeline/hooks/use-get-run-and-trace-url.ts @@ -3,8 +3,14 @@ import { useWorkflowStore } from '@/app/components/workflow/store' export const useGetRunAndTraceUrl = () => { const workflowStore = useWorkflowStore() - const getWorkflowRunAndTraceUrl = useCallback((runId: string) => { + const getWorkflowRunAndTraceUrl = useCallback((runId?: string) => { const { pipelineId } = workflowStore.getState() + if (!pipelineId || !runId) { + return { + runUrl: '', + traceUrl: '', + } + } return { runUrl: `/rag/pipelines/${pipelineId}/workflow-runs/${runId}`, diff --git a/web/app/components/tools/mcp/mcp-service-card.tsx b/web/app/components/tools/mcp/mcp-service-card.tsx index d707e0574e..9213dc3e7f 100644 --- a/web/app/components/tools/mcp/mcp-service-card.tsx +++ b/web/app/components/tools/mcp/mcp-service-card.tsx @@ -1,6 +1,8 @@ 'use client' +import type { CollaborationUpdate } from '@/app/components/workflow/collaboration/types/collaboration' +import type { InputVar } from '@/app/components/workflow/types' import type { AppDetailResponse } from '@/models/app' -import type { AppSSO } from '@/types/app' +import type { AppSSO, ModelConfig, UserInputFormItem } from '@/types/app' import { RiEditLine, RiLoopLeftLine } from '@remixicon/react' import * as React from 'react' import { useEffect, useMemo, useState } from 'react' @@ -38,6 +40,16 @@ export type IAppCardProps = { triggerModeMessage?: React.ReactNode // display-only message explaining the trigger restriction } +type BasicAppConfig = Partial & { + updated_at?: number +} + +type McpServerParam = { + label: string + variable: string + type: string +} + function MCPServiceCard({ appInfo, triggerModeDisabled = false, @@ -56,16 +68,16 @@ function MCPServiceCard({ const isAdvancedApp = appInfo?.mode === AppModeEnum.ADVANCED_CHAT || appInfo?.mode === AppModeEnum.WORKFLOW const isBasicApp = !isAdvancedApp const { data: currentWorkflow } = useAppWorkflow(isAdvancedApp ? appId : '') - const [basicAppConfig, setBasicAppConfig] = useState({}) - const basicAppInputForm = useMemo(() => { - if (!isBasicApp || !basicAppConfig?.user_input_form) + const [basicAppConfig, setBasicAppConfig] = useState({}) + const basicAppInputForm = useMemo(() => { + if (!isBasicApp || !basicAppConfig.user_input_form) return [] - return basicAppConfig.user_input_form.map((item: any) => { - const type = Object.keys(item)[0] - return { - ...item[type], - type: type || 'text-input', - } + return basicAppConfig.user_input_form.map((item: UserInputFormItem) => { + if ('text-input' in item) + return { label: item['text-input'].label, variable: item['text-input'].variable, type: 'text-input' } + if ('select' in item) + return { label: item.select.label, variable: item.select.variable, type: 'select' } + return { label: item.paragraph.label, variable: item.paragraph.variable, type: 'paragraph' } }) }, [basicAppConfig.user_input_form, isBasicApp]) useEffect(() => { @@ -92,12 +104,22 @@ function MCPServiceCard({ const [activated, setActivated] = useState(serverActivated) - const latestParams = useMemo(() => { + const latestParams = useMemo(() => { if (isAdvancedApp) { if (!currentWorkflow?.graph) return [] - const startNode = currentWorkflow?.graph.nodes.find(node => node.data.type === BlockEnum.Start) as any - return startNode?.data.variables as any[] || [] + const startNode = currentWorkflow?.graph.nodes.find(node => node.data.type === BlockEnum.Start) + const variables = (startNode?.data as { variables?: InputVar[] } | undefined)?.variables || [] + return variables.map((variable) => { + const label = typeof variable.label === 'string' + ? variable.label + : (variable.label.variable || variable.label.nodeName) + return { + label, + variable: variable.variable, + type: variable.type, + } + }) } return basicAppInputForm }, [currentWorkflow, basicAppInputForm, isAdvancedApp]) @@ -178,9 +200,8 @@ function MCPServiceCard({ if (!appId) return - const unsubscribe = collaborationManager.onMcpServerUpdate(async (update: any) => { + const unsubscribe = collaborationManager.onMcpServerUpdate(async (_update: CollaborationUpdate) => { try { - console.log('Received MCP server update from collaboration:', update) invalidateMCPServerDetail(appId) } catch (error) { diff --git a/web/app/components/workflow-app/components/workflow-header/features-trigger.spec.tsx b/web/app/components/workflow-app/components/workflow-header/features-trigger.spec.tsx index 757e7c8a97..68572d2f1d 100644 --- a/web/app/components/workflow-app/components/workflow-header/features-trigger.spec.tsx +++ b/web/app/components/workflow-app/components/workflow-header/features-trigger.spec.tsx @@ -108,7 +108,7 @@ vi.mock('@/app/components/app/app-publisher', () => ({ - diff --git a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx index 466220b611..f713a78e2b 100644 --- a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx +++ b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx @@ -1,3 +1,4 @@ +import type { ModelAndParameter } from '@/app/components/app/configuration/debug/types' import type { EndNodeType } from '@/app/components/workflow/nodes/end/types' import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' import type { @@ -140,7 +141,8 @@ const FeaturesTrigger = () => { const needWarningNodes = useChecklist(nodes, edges) const updatePublishedWorkflow = useInvalidateAppWorkflow() - const onPublish = useCallback(async (params?: PublishWorkflowParams) => { + const onPublish = useCallback(async (params?: ModelAndParameter | PublishWorkflowParams) => { + const publishParams = params && 'title' in params ? params : undefined // First check if there are any items in the checklist // if (!validateBeforeRun()) // throw new Error('Checklist has unresolved items') @@ -154,8 +156,8 @@ const FeaturesTrigger = () => { if (await handleCheckBeforePublish()) { const res = await publishWorkflow({ url: `/apps/${appID}/workflows/publish`, - title: params?.title || '', - releaseNotes: params?.releaseNotes || '', + title: publishParams?.title || '', + releaseNotes: publishParams?.releaseNotes || '', }) if (res) { diff --git a/web/app/components/workflow-app/components/workflow-main.tsx b/web/app/components/workflow-app/components/workflow-main.tsx index 1ba9a90d7c..6fa11810a9 100644 --- a/web/app/components/workflow-app/components/workflow-main.tsx +++ b/web/app/components/workflow-app/components/workflow-main.tsx @@ -1,13 +1,16 @@ import type { Features as FeaturesData } from '@/app/components/base/features/types' import type { WorkflowProps } from '@/app/components/workflow' +import type { CollaborationUpdate } from '@/app/components/workflow/collaboration/types/collaboration' +import type { Shape as HooksStoreShape } from '@/app/components/workflow/hooks-store/store' +import type { Edge, Node } from '@/app/components/workflow/types' +import type { FetchWorkflowDraftResponse } from '@/types/workflow' import { useCallback, useEffect, useMemo, useRef, - useState, } from 'react' -import { useReactFlow, useStoreApi } from 'reactflow' +import { useReactFlow } from 'reactflow' import { useFeaturesStore } from '@/app/components/base/features/hooks' import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' import { WorkflowWithInnerContext } from '@/app/components/workflow' @@ -31,6 +34,7 @@ import { import WorkflowChildren from './workflow-children' type WorkflowMainProps = Pick +type WorkflowDataUpdatePayload = Pick const WorkflowMain = ({ nodes, edges, @@ -42,7 +46,14 @@ const WorkflowMain = ({ const containerRef = useRef(null) const reactFlow = useReactFlow() - const store = useStoreApi() + const reactFlowStore = useMemo(() => ({ + getState: () => ({ + getNodes: () => reactFlow.getNodes(), + setNodes: (nodesToSet: Node[]) => reactFlow.setNodes(nodesToSet), + getEdges: () => reactFlow.getEdges(), + setEdges: (edgesToSet: Edge[]) => reactFlow.setEdges(edgesToSet), + }), + }), [reactFlow]) const { startCursorTracking, stopCursorTracking, @@ -50,15 +61,11 @@ const WorkflowMain = ({ cursors, isConnected, isEnabled: isCollaborationEnabled, - } = useCollaboration(appId || '', store) - const [myUserId, setMyUserId] = useState(null) - - useEffect(() => { - if (isCollaborationEnabled && isConnected) - setMyUserId('current-user') - else - setMyUserId(null) - }, [isCollaborationEnabled, isConnected]) + } = useCollaboration(appId || '', reactFlowStore) + const myUserId = useMemo( + () => (isCollaborationEnabled && isConnected ? 'current-user' : null), + [isCollaborationEnabled, isConnected], + ) const filteredCursors = Object.fromEntries( Object.entries(cursors).filter(([userId]) => userId !== myUserId), @@ -76,7 +83,7 @@ const WorkflowMain = ({ } }, [startCursorTracking, stopCursorTracking, reactFlow, isCollaborationEnabled]) - const handleWorkflowDataUpdate = useCallback((payload: any) => { + const handleWorkflowDataUpdate = useCallback((payload: WorkflowDataUpdatePayload) => { const { features, conversation_variables, @@ -141,7 +148,7 @@ const WorkflowMain = ({ if (!appId || !isCollaborationEnabled) return - const unsubscribe = collaborationManager.onVarsAndFeaturesUpdate(async (update: any) => { + const unsubscribe = collaborationManager.onVarsAndFeaturesUpdate(async (_update: CollaborationUpdate) => { try { const response = await fetchWorkflowDraft(`/apps/${appId}/workflows/draft`) handleWorkflowDataUpdate(response) @@ -160,7 +167,6 @@ const WorkflowMain = ({ return const unsubscribe = collaborationManager.onWorkflowUpdate(async () => { - console.log('Received workflow update from collaborator, fetching latest workflow data') try { const response = await fetchWorkflowDraft(`/apps/${appId}/workflows/draft`) @@ -190,7 +196,6 @@ const WorkflowMain = ({ return const unsubscribe = collaborationManager.onSyncRequest(() => { - console.log('Leader received sync request, performing sync') doSyncWorkflowDraft() }) @@ -234,7 +239,7 @@ const WorkflowMain = ({ invalidateConversationVarValues, } = useInspectVarsCrud() - const hooksStore = useMemo(() => { + const hooksStore = useMemo>(() => { return { syncWorkflowDraftWhenPageClose, doSyncWorkflowDraft, @@ -320,7 +325,7 @@ const WorkflowMain = ({ edges={edges} viewport={viewport} onWorkflowDataUpdate={handleWorkflowDataUpdate} - hooksStore={hooksStore as any} + hooksStore={hooksStore} cursors={filteredCursors} myUserId={myUserId} onlineUsers={onlineUsers} diff --git a/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts b/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts index 60f0bf3b28..454ddd44ea 100644 --- a/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts +++ b/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts @@ -38,7 +38,7 @@ export const useAvailableNodesMetaData = () => { TriggerPluginDefault, ] ), - ], [isChatMode, startNodeMetaData]) + ] as AvailableNodesMetaData['nodes'], [isChatMode, startNodeMetaData]) const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => { const { metaData } = node @@ -59,7 +59,7 @@ export const useAvailableNodesMetaData = () => { title, }, } - }), [mergedNodesMetaData, t, docLink]) + }) as AvailableNodesMetaData['nodes'], [mergedNodesMetaData, t, docLink]) const availableNodesMetaDataMap = useMemo(() => availableNodesMetaData.reduce((acc, node) => { acc![node.metaData.type] = node diff --git a/web/app/components/workflow-app/hooks/use-get-run-and-trace-url.ts b/web/app/components/workflow-app/hooks/use-get-run-and-trace-url.ts index 28bcd017f8..043ea25ed7 100644 --- a/web/app/components/workflow-app/hooks/use-get-run-and-trace-url.ts +++ b/web/app/components/workflow-app/hooks/use-get-run-and-trace-url.ts @@ -3,8 +3,14 @@ import { useWorkflowStore } from '@/app/components/workflow/store' export const useGetRunAndTraceUrl = () => { const workflowStore = useWorkflowStore() - const getWorkflowRunAndTraceUrl = useCallback((runId: string) => { + const getWorkflowRunAndTraceUrl = useCallback((runId?: string) => { const { appId } = workflowStore.getState() + if (!appId || !runId) { + return { + runUrl: '', + traceUrl: '', + } + } return { runUrl: `/apps/${appId}/workflow-runs/${runId}`, diff --git a/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts b/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts index 7a4111f020..5b0214d078 100644 --- a/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts +++ b/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts @@ -98,15 +98,12 @@ export const useNodesSyncDraft = () => { const currentIsLeader = isCollaborationEnabled ? collaborationManager.getIsLeader() : true // Only allow leader to sync data - if (isCollaborationEnabled && !currentIsLeader) { - console.log('Not leader, skipping sync on page close') + if (isCollaborationEnabled && !currentIsLeader) return - } const postParams = getPostParams() if (postParams) { - console.log('Leader syncing workflow draft on page close') navigator.sendBeacon( `${API_PREFIX}/apps/${params.appId}/workflows/draft`, JSON.stringify(postParams.params), @@ -131,14 +128,11 @@ export const useNodesSyncDraft = () => { // If not leader and not forcing upload, request the leader to sync if (isCollaborationEnabled && !currentIsLeader && !forceUpload) { - console.log('Not leader, requesting leader to sync workflow draft') if (isCollaborationEnabled) collaborationManager.emitSyncRequest() callback?.onSettled?.() return } - - console.log(forceUpload ? 'Force uploading workflow draft' : 'Leader performing workflow draft sync') const postParams = getPostParams() if (postParams) { diff --git a/web/app/components/workflow/block-selector/index.tsx b/web/app/components/workflow/block-selector/index.tsx index 5b9d86d6d4..0475b6bfcf 100644 --- a/web/app/components/workflow/block-selector/index.tsx +++ b/web/app/components/workflow/block-selector/index.tsx @@ -35,7 +35,7 @@ const NodeSelectorWrapper = (props: NodeSelectorProps) => { return true }) - }, [availableNodesMetaData?.nodes]) + }, [availableNodesMetaData?.nodes]) as NodeSelectorProps['blocks'] return ( { + getNodes: () => Node[] + setNodes: (nodes: Node[]) => void + getEdges: () => Edge[] + setEdges: (edges: Edge[]) => void + } +} + +type CollaborationEventPayload = { + type: CollaborationUpdate['type'] + data: Record + timestamp: number + userId?: string +} + +type LoroSubscribeEvent = { + by?: string +} + +type LoroContainer = { + kind?: () => string + getAttached?: () => unknown +} + +const toLoroValue = (value: unknown): Value => cloneDeep(value) as Value +const toLoroRecord = (value: unknown): Record => cloneDeep(value) as Record + export class CollaborationManager { private doc: LoroDoc | null = null private undoManager: UndoManager | null = null private provider: CRDTProvider | null = null - private nodesMap: LoroMap | null = null - private edgesMap: LoroMap | null = null + private nodesMap: LoroMap> | null = null + private edgesMap: LoroMap> | null = null private eventEmitter = new EventEmitter() private currentAppId: string | null = null - private reactFlowStore: any = null + private reactFlowStore: ReactFlowStore | null = null private isLeader = false private leaderId: string | null = null private cursors: Record = {} @@ -80,7 +109,7 @@ export class CollaborationManager { ) } - private sendCollaborationEvent(payload: any): void { + private sendCollaborationEvent(payload: CollaborationEventPayload): void { const socket = this.getActiveSocket() if (!socket) return @@ -88,7 +117,7 @@ export class CollaborationManager { emitWithAuthGuard(socket, 'collaboration_event', payload, { onUnauthorized: this.handleSessionUnauthorized }) } - private sendGraphEvent(payload: any): void { + private sendGraphEvent(payload: Uint8Array): void { const socket = this.getActiveSocket() if (!socket) return @@ -96,59 +125,67 @@ export class CollaborationManager { emitWithAuthGuard(socket, 'graph_event', payload, { onUnauthorized: this.handleSessionUnauthorized }) } - private getNodeContainer(nodeId: string): LoroMap { + private getNodeContainer(nodeId: string): LoroMap> { if (!this.nodesMap) throw new Error('Nodes map not initialized') - let container = this.nodesMap.get(nodeId) as any + let container = this.nodesMap.get(nodeId) as unknown - if (!container || typeof container.kind !== 'function' || container.kind() !== 'Map') { + const isMapContainer = (value: unknown): value is LoroMap> & LoroContainer => { + return !!value && typeof (value as LoroContainer).kind === 'function' && (value as LoroContainer).kind?.() === 'Map' + } + + if (!container || !isMapContainer(container)) { const previousValue = container const newContainer = this.nodesMap.setContainer(nodeId, new LoroMap()) - container = typeof newContainer.getAttached === 'function' ? newContainer.getAttached() ?? newContainer : newContainer + const attached = (newContainer as LoroContainer).getAttached?.() ?? newContainer + container = attached if (previousValue && typeof previousValue === 'object') - this.populateNodeContainer(container, previousValue as Node) + this.populateNodeContainer(container as LoroMap>, previousValue as Node) } else { - container = typeof container.getAttached === 'function' ? container.getAttached() ?? container : container + const attached = (container as LoroContainer).getAttached?.() ?? container + container = attached } - return container + return container as LoroMap> } - private ensureDataContainer(nodeContainer: LoroMap): LoroMap { - let dataContainer = nodeContainer.get('data') as any + private ensureDataContainer(nodeContainer: LoroMap>): LoroMap> { + let dataContainer = nodeContainer.get('data') as unknown - if (!dataContainer || typeof dataContainer.kind !== 'function' || dataContainer.kind() !== 'Map') + if (!dataContainer || typeof (dataContainer as LoroContainer).kind !== 'function' || (dataContainer as LoroContainer).kind?.() !== 'Map') dataContainer = nodeContainer.setContainer('data', new LoroMap()) - return typeof dataContainer.getAttached === 'function' ? dataContainer.getAttached() ?? dataContainer : dataContainer + const attached = (dataContainer as LoroContainer).getAttached?.() ?? dataContainer + return attached as LoroMap> } - private ensureList(nodeContainer: LoroMap, key: string): LoroList { + private ensureList(nodeContainer: LoroMap>, key: string): LoroList { const dataContainer = this.ensureDataContainer(nodeContainer) - let list = dataContainer.get(key) as any + let list = dataContainer.get(key) as unknown - if (!list || typeof list.kind !== 'function' || list.kind() !== 'List') + if (!list || typeof (list as LoroContainer).kind !== 'function' || (list as LoroContainer).kind?.() !== 'List') list = dataContainer.setContainer(key, new LoroList()) - return typeof list.getAttached === 'function' ? list.getAttached() ?? list : list + const attached = (list as LoroContainer).getAttached?.() ?? list + return attached as LoroList } private exportNode(nodeId: string): Node { const container = this.getNodeContainer(nodeId) - const json = container.toJSON() as any + const json = container.toJSON() as Node return { ...json, data: json.data || {}, } } - private populateNodeContainer(container: LoroMap, node: Node): void { + private populateNodeContainer(container: LoroMap>, node: Node): void { const listFields = new Set(['variables', 'prompt_template', 'parameters']) container.set('id', node.id) container.set('type', node.type) - container.set('position', cloneDeep(node.position)) + container.set('position', toLoroValue(node.position)) container.set('sourcePosition', node.sourcePosition) container.set('targetPosition', node.targetPosition) @@ -189,7 +226,7 @@ export class CollaborationManager { if (value === undefined) container.delete(prop as string) else - container.set(prop as string, cloneDeep(value as any)) + container.set(prop as string, toLoroValue(value)) }) const dataContainer = this.ensureDataContainer(container) @@ -203,7 +240,7 @@ export class CollaborationManager { if (listFields.has(key)) this.syncList(container, key, Array.isArray(value) ? value : []) else - dataContainer.set(key, cloneDeep(value)) + dataContainer.set(key, toLoroValue(value)) }) const existingData = dataContainer.toJSON() || {} @@ -222,9 +259,9 @@ export class CollaborationManager { return (syncDataAllowList.has(key) || !key.startsWith('_')) && key !== 'selected' } - private syncList(nodeContainer: LoroMap, key: string, desired: any[]): void { + private syncList(nodeContainer: LoroMap>, key: string, desired: Array): void { const list = this.ensureList(nodeContainer, key) - const current = list.toJSON() as any[] + const current = list.toJSON() as Array const target = Array.isArray(desired) ? desired : [] const minLength = Math.min(current.length, target.length) @@ -309,7 +346,7 @@ export class CollaborationManager { this.eventEmitter.emit('nodePanelPresence', this.getNodePanelPresenceSnapshot()) } - init = (appId: string, reactFlowStore: any): void => { + init = (appId: string, reactFlowStore: ReactFlowStore): void => { if (!reactFlowStore) { console.warn('CollaborationManager.init called without reactFlowStore, deferring to connect()') return @@ -345,7 +382,7 @@ export class CollaborationManager { this.disconnect() } - async connect(appId: string, reactFlowStore?: any): Promise { + async connect(appId: string, reactFlowStore?: ReactFlowStore): Promise { const connectionId = Math.random().toString(36).substring(2, 11) this.activeConnections.add(connectionId) @@ -373,15 +410,15 @@ export class CollaborationManager { this.setupSocketEventListeners(socket) this.doc = new LoroDoc() - this.nodesMap = this.doc.getMap('nodes') - this.edgesMap = this.doc.getMap('edges') + this.nodesMap = this.doc.getMap('nodes') as LoroMap> + this.edgesMap = this.doc.getMap('edges') as LoroMap> // Initialize UndoManager for collaborative undo/redo this.undoManager = new UndoManager(this.doc, { maxUndoSteps: 100, mergeInterval: 500, // Merge operations within 500ms excludeOriginPrefixes: [], // Don't exclude anything - let UndoManager track all local operations - onPush: (isUndo, range, event) => { + onPush: (_isUndo, _range, _event) => { // Store current selection state when an operation is pushed const selectedNode = this.reactFlowStore?.getState().getNodes().find((n: Node) => n.data?.selected) @@ -401,10 +438,10 @@ export class CollaborationManager { cursors: [], } }, - onPop: (isUndo, value, counterRange) => { + onPop: (_isUndo, value, _counterRange) => { // Restore selection state when undoing/redoing if (value?.value && typeof value.value === 'object' && 'selectedNodeId' in value.value && this.reactFlowStore) { - const selectedNodeId = (value.value as any).selectedNodeId + const selectedNodeId = (value.value as { selectedNodeId?: string | null }).selectedNodeId if (selectedNodeId) { const { setNodes } = this.reactFlowStore.getState() const nodes = this.reactFlowStore.getState().getNodes() @@ -481,7 +518,7 @@ export class CollaborationManager { } getEdges(): Edge[] { - return this.edgesMap ? Array.from(this.edgesMap.values()) : [] + return this.edgesMap ? Array.from(this.edgesMap.values()) as Edge[] : [] } emitCursorMove(position: CursorPosition): void { @@ -567,23 +604,23 @@ export class CollaborationManager { return this.eventEmitter.on('workflowUpdate', callback) } - onVarsAndFeaturesUpdate(callback: (update: any) => void): () => void { + onVarsAndFeaturesUpdate(callback: (update: CollaborationUpdate) => void): () => void { return this.eventEmitter.on('varsAndFeaturesUpdate', callback) } - onAppStateUpdate(callback: (update: any) => void): () => void { + onAppStateUpdate(callback: (update: CollaborationUpdate) => void): () => void { return this.eventEmitter.on('appStateUpdate', callback) } - onAppPublishUpdate(callback: (update: any) => void): () => void { + onAppPublishUpdate(callback: (update: CollaborationUpdate) => void): () => void { return this.eventEmitter.on('appPublishUpdate', callback) } - onAppMetaUpdate(callback: (update: any) => void): () => void { + onAppMetaUpdate(callback: (update: CollaborationUpdate) => void): () => void { return this.eventEmitter.on('appMetaUpdate', callback) } - onMcpServerUpdate(callback: (update: any) => void): () => void { + onMcpServerUpdate(callback: (update: CollaborationUpdate) => void): () => void { return this.eventEmitter.on('mcpServerUpdate', callback) } @@ -635,12 +672,13 @@ export class CollaborationManager { const result = this.undoManager.undo() // After undo, manually update React state from CRDT without triggering collaboration - if (result && this.reactFlowStore) { + const reactFlowStore = this.reactFlowStore + if (result && reactFlowStore) { requestAnimationFrame(() => { // Get ReactFlow's native setters, not the collaborative ones - const state = this.reactFlowStore.getState() - const updatedNodes = Array.from(this.nodesMap?.values() || []) - const updatedEdges = Array.from(this.edgesMap?.values() || []) + const state = reactFlowStore.getState() + const updatedNodes = Array.from(this.nodesMap?.values() || []) as Node[] + const updatedEdges = Array.from(this.edgesMap?.values() || []) as Edge[] // Call ReactFlow's native setters directly to avoid triggering collaboration state.setNodes(updatedNodes) state.setEdges(updatedEdges) @@ -674,12 +712,13 @@ export class CollaborationManager { const result = this.undoManager.redo() // After redo, manually update React state from CRDT without triggering collaboration - if (result && this.reactFlowStore) { + const reactFlowStore = this.reactFlowStore + if (result && reactFlowStore) { requestAnimationFrame(() => { // Get ReactFlow's native setters, not the collaborative ones - const state = this.reactFlowStore.getState() - const updatedNodes = Array.from(this.nodesMap?.values() || []) - const updatedEdges = Array.from(this.edgesMap?.values() || []) + const state = reactFlowStore.getState() + const updatedNodes = Array.from(this.nodesMap?.values() || []) as Node[] + const updatedEdges = Array.from(this.edgesMap?.values() || []) as Edge[] // Call ReactFlow's native setters directly to avoid triggering collaboration state.setNodes(updatedNodes) state.setEdges(updatedEdges) @@ -753,21 +792,22 @@ export class CollaborationManager { newEdges.forEach((newEdge) => { const oldEdge = oldEdgesMap.get(newEdge.id) if (!oldEdge || !isEqual(oldEdge, newEdge)) { - const clonedEdge = cloneDeep(newEdge) + const clonedEdge = toLoroRecord(newEdge) this.edgesMap?.set(newEdge.id, clonedEdge) } }) } private setupSubscriptions(): void { - this.nodesMap?.subscribe((event: any) => { - if (event.by === 'import' && this.reactFlowStore) { + this.nodesMap?.subscribe((event: LoroSubscribeEvent) => { + const reactFlowStore = this.reactFlowStore + if (event.by === 'import' && reactFlowStore) { // Don't update React nodes during undo/redo to prevent loops if (this.isUndoRedoInProgress) return requestAnimationFrame(() => { - const state = this.reactFlowStore.getState() + const state = reactFlowStore.getState() const previousNodes: Node[] = state.getNodes() const previousNodeMap = new Map(previousNodes.map(node => [node.id, node])) const selectedIds = new Set( @@ -813,16 +853,17 @@ export class CollaborationManager { } }) - this.edgesMap?.subscribe((event: any) => { - if (event.by === 'import' && this.reactFlowStore) { + this.edgesMap?.subscribe((event: LoroSubscribeEvent) => { + const reactFlowStore = this.reactFlowStore + if (event.by === 'import' && reactFlowStore) { // Don't update React edges during undo/redo to prevent loops if (this.isUndoRedoInProgress) return requestAnimationFrame(() => { // Get ReactFlow's native setters, not the collaborative ones - const state = this.reactFlowStore.getState() - const updatedEdges = Array.from(this.edgesMap?.values() || []) + const state = reactFlowStore.getState() + const updatedEdges = Array.from(this.edgesMap?.values() || []) as Edge[] this.pendingInitialSync = false @@ -926,9 +967,6 @@ export class CollaborationManager { const wasLeader = this.isLeader this.isLeader = data.isLeader - if (wasLeader !== this.isLeader) - console.log(`Collaboration: I am now the ${this.isLeader ? 'Leader' : 'Follower'}.`) - if (this.isLeader) this.pendingInitialSync = false else @@ -943,13 +981,11 @@ export class CollaborationManager { }) socket.on('connect', () => { - console.log('WebSocket connected successfully') this.eventEmitter.emit('stateChange', { isConnected: true }) this.pendingInitialSync = true }) - socket.on('disconnect', (reason: string) => { - console.log('WebSocket disconnected:', reason) + socket.on('disconnect', () => { this.cursors = {} this.isLeader = false this.leaderId = null @@ -958,12 +994,12 @@ export class CollaborationManager { this.eventEmitter.emit('cursors', {}) }) - socket.on('connect_error', (error: any) => { + socket.on('connect_error', (error: Error) => { console.error('WebSocket connection error:', error) this.eventEmitter.emit('stateChange', { isConnected: false, error: error.message }) }) - socket.on('error', (error: any) => { + socket.on('error', (error: Error) => { console.error('WebSocket error:', error) }) } diff --git a/web/app/components/workflow/collaboration/core/crdt-provider.ts b/web/app/components/workflow/collaboration/core/crdt-provider.ts index fbe4b13e02..ce3fff4b32 100644 --- a/web/app/components/workflow/collaboration/core/crdt-provider.ts +++ b/web/app/components/workflow/collaboration/core/crdt-provider.ts @@ -15,7 +15,7 @@ export class CRDTProvider { } private setupEventListeners(): void { - this.doc.subscribe((event: any) => { + this.doc.subscribe((event: { by?: string }) => { if (event.by === 'local') { const update = this.doc.export({ mode: 'update' }) emitWithAuthGuard(this.socket, 'graph_event', update, { onUnauthorized: this.onUnauthorized }) diff --git a/web/app/components/workflow/collaboration/core/event-emitter.ts b/web/app/components/workflow/collaboration/core/event-emitter.ts index efe7ef81d9..b4f79b7922 100644 --- a/web/app/components/workflow/collaboration/core/event-emitter.ts +++ b/web/app/components/workflow/collaboration/core/event-emitter.ts @@ -1,24 +1,24 @@ -export type EventHandler = (data: T) => void +export type EventHandler = (data: T) => void export class EventEmitter { - private events: Map> = new Map() + private events: Map>> = new Map() - on(event: string, handler: EventHandler): () => void { + on(event: string, handler: EventHandler): () => void { if (!this.events.has(event)) this.events.set(event, new Set()) - this.events.get(event)!.add(handler) + this.events.get(event)!.add(handler as EventHandler) return () => this.off(event, handler) } - off(event: string, handler?: EventHandler): void { + off(event: string, handler?: EventHandler): void { if (!this.events.has(event)) return const handlers = this.events.get(event)! if (handler) - handlers.delete(handler) + handlers.delete(handler as EventHandler) else handlers.clear() @@ -26,7 +26,7 @@ export class EventEmitter { this.events.delete(event) } - emit(event: string, data: T): void { + emit(event: string, data: T): void { if (!this.events.has(event)) return diff --git a/web/app/components/workflow/collaboration/core/websocket-manager.ts b/web/app/components/workflow/collaboration/core/websocket-manager.ts index 73c08035cb..1a143f9687 100644 --- a/web/app/components/workflow/collaboration/core/websocket-manager.ts +++ b/web/app/components/workflow/collaboration/core/websocket-manager.ts @@ -3,27 +3,31 @@ import type { DebugInfo, WebSocketConfig } from '../types/websocket' import { io } from 'socket.io-client' import { ACCESS_TOKEN_LOCAL_STORAGE_NAME } from '@/config' -const isUnauthorizedAck = (...ackArgs: any[]): boolean => { +type AckArgs = unknown[] + +const isUnauthorizedAck = (...ackArgs: AckArgs): boolean => { const [first, second] = ackArgs if (second === 401 || first === 401) return true - if (first && typeof first === 'object' && first.msg === 'unauthorized') - return true + if (first && typeof first === 'object' && 'msg' in first) { + const message = (first as { msg?: unknown }).msg + return message === 'unauthorized' + } return false } export type EmitAckOptions = { - onAck?: (...ackArgs: any[]) => void - onUnauthorized?: (...ackArgs: any[]) => void + onAck?: (...ackArgs: AckArgs) => void + onUnauthorized?: (...ackArgs: AckArgs) => void } export const emitWithAuthGuard = ( socket: Socket | null | undefined, event: string, - payload: any, + payload: unknown, options?: EmitAckOptions, ): void => { if (!socket) @@ -32,7 +36,7 @@ export const emitWithAuthGuard = ( socket.emit( event, payload, - (...ackArgs: any[]) => { + (...ackArgs: AckArgs) => { options?.onAck?.(...ackArgs) if (isUnauthorizedAck(...ackArgs)) options?.onUnauthorized?.(...ackArgs) diff --git a/web/app/components/workflow/collaboration/hooks/use-collaboration.ts b/web/app/components/workflow/collaboration/hooks/use-collaboration.ts index a6526def45..a8715d7571 100644 --- a/web/app/components/workflow/collaboration/hooks/use-collaboration.ts +++ b/web/app/components/workflow/collaboration/hooks/use-collaboration.ts @@ -1,31 +1,44 @@ import type { ReactFlowInstance } from 'reactflow' -import type { CollaborationState } from '../types/collaboration' +import type { + CollaborationState, + CursorPosition, + NodePanelPresenceMap, + OnlineUser, +} from '../types/collaboration' import { useEffect, useRef, useState } from 'react' import Toast from '@/app/components/base/toast' import { useGlobalPublicStore } from '@/context/global-public-context' import { collaborationManager } from '../core/collaboration-manager' import { CursorService } from '../services/cursor-service' -export function useCollaboration(appId: string, reactFlowStore?: any) { - const [state, setState] = useState>({ - isConnected: false, - onlineUsers: [], - cursors: {}, - nodePanelPresence: {}, - isLeader: false, - }) +type CollaborationViewState = { + isConnected: boolean + onlineUsers: OnlineUser[] + cursors: Record + nodePanelPresence: NodePanelPresenceMap + isLeader: boolean +} + +type ReactFlowStore = NonNullable[1]> + +const initialState: CollaborationViewState = { + isConnected: false, + onlineUsers: [], + cursors: {}, + nodePanelPresence: {}, + isLeader: false, +} + +export function useCollaboration(appId: string, reactFlowStore?: ReactFlowStore) { + const [state, setState] = useState(initialState) const cursorServiceRef = useRef(null) const isCollaborationEnabled = useGlobalPublicStore(s => s.systemFeatures.enable_collaboration_mode) useEffect(() => { if (!appId || !isCollaborationEnabled) { - setState({ - isConnected: false, - onlineUsers: [], - cursors: {}, - nodePanelPresence: {}, - isLeader: false, + Promise.resolve().then(() => { + setState(initialState) }) return } @@ -44,7 +57,7 @@ export function useCollaboration(appId: string, reactFlowStore?: any) { return } connectionId = id - setState((prev: any) => ({ ...prev, appId, isConnected: collaborationManager.isConnected() })) + setState(prev => ({ ...prev, isConnected: collaborationManager.isConnected() })) } catch (error) { console.error('Failed to initialize collaboration:', error) @@ -53,27 +66,27 @@ export function useCollaboration(appId: string, reactFlowStore?: any) { initCollaboration() - const unsubscribeStateChange = collaborationManager.onStateChange((newState: any) => { - console.log('Collaboration state change:', newState) - setState((prev: any) => ({ ...prev, ...newState })) + const unsubscribeStateChange = collaborationManager.onStateChange((newState: Partial) => { + if (newState.isConnected === undefined) + return + + setState(prev => ({ ...prev, isConnected: newState.isConnected ?? prev.isConnected })) }) - const unsubscribeCursors = collaborationManager.onCursorUpdate((cursors: any) => { - setState((prev: any) => ({ ...prev, cursors })) + const unsubscribeCursors = collaborationManager.onCursorUpdate((cursors: Record) => { + setState(prev => ({ ...prev, cursors })) }) - const unsubscribeUsers = collaborationManager.onOnlineUsersUpdate((users: any) => { - console.log('Online users update:', users) - setState((prev: any) => ({ ...prev, onlineUsers: users })) + const unsubscribeUsers = collaborationManager.onOnlineUsersUpdate((users: OnlineUser[]) => { + setState(prev => ({ ...prev, onlineUsers: users })) }) - const unsubscribeNodePanelPresence = collaborationManager.onNodePanelPresenceUpdate((presence) => { - setState((prev: any) => ({ ...prev, nodePanelPresence: presence })) + const unsubscribeNodePanelPresence = collaborationManager.onNodePanelPresenceUpdate((presence: NodePanelPresenceMap) => { + setState(prev => ({ ...prev, nodePanelPresence: presence })) }) const unsubscribeLeaderChange = collaborationManager.onLeaderChange((isLeader: boolean) => { - console.log('Leader status changed:', isLeader) - setState((prev: any) => ({ ...prev, isLeader })) + setState(prev => ({ ...prev, isLeader })) }) return () => { diff --git a/web/app/components/workflow/collaboration/types/events.ts b/web/app/components/workflow/collaboration/types/events.ts index e995f9e876..69d228357e 100644 --- a/web/app/components/workflow/collaboration/types/events.ts +++ b/web/app/components/workflow/collaboration/types/events.ts @@ -1,38 +1,34 @@ -export type CollaborationEvent = { +export type CollaborationEvent = { type: string - data: any + data: TData timestamp: number } export type GraphUpdateEvent = { type: 'graph_update' - data: Uint8Array -} & CollaborationEvent +} & CollaborationEvent export type CursorMoveEvent = { type: 'cursor_move' - data: { - x: number - y: number - userId: string - } -} & CollaborationEvent +} & CollaborationEvent<{ + x: number + y: number + userId: string +}> export type UserConnectEvent = { type: 'user_connect' - data: { - workflow_id: string - } -} & CollaborationEvent +} & CollaborationEvent<{ + workflow_id: string +}> export type OnlineUsersEvent = { type: 'online_users' - data: { - users: Array<{ - user_id: string - username: string - avatar: string - sid: string - }> - } -} & CollaborationEvent +} & CollaborationEvent<{ + users: Array<{ + user_id: string + username: string + avatar: string + sid: string + }> +}> diff --git a/web/app/components/workflow/comment/mention-input.tsx b/web/app/components/workflow/comment/mention-input.tsx index c7224f01a6..ed51feb90d 100644 --- a/web/app/components/workflow/comment/mention-input.tsx +++ b/web/app/components/workflow/comment/mention-input.tsx @@ -253,11 +253,15 @@ const MentionInputInner = forwardRef(({ }, [value, syncHighlightScroll]) useLayoutEffect(() => { - evaluateContentLayout() + Promise.resolve().then(() => { + evaluateContentLayout() + }) }, [value, evaluateContentLayout]) useLayoutEffect(() => { - updateLayoutPadding() + Promise.resolve().then(() => { + updateLayoutPadding() + }) }, [updateLayoutPadding, isEditing, shouldReserveButtonGap]) useEffect(() => { @@ -271,9 +275,11 @@ const MentionInputInner = forwardRef(({ }, [evaluateContentLayout, updateLayoutPadding]) useEffect(() => { - baseTextareaHeightRef.current = null - evaluateContentLayout() - setShouldReserveHorizontalSpace(!isEditing) + Promise.resolve().then(() => { + baseTextareaHeightRef.current = null + evaluateContentLayout() + setShouldReserveHorizontalSpace(!isEditing) + }) }, [isEditing, evaluateContentLayout]) const filteredMentionUsers = useMemo(() => { @@ -481,8 +487,11 @@ const MentionInputInner = forwardRef(({ }, []) useEffect(() => { - if (!value) - resetMentionState() + if (!value) { + Promise.resolve().then(() => { + resetMentionState() + }) + } }, [value, resetMentionState]) useEffect(() => { diff --git a/web/app/components/workflow/comment/thread.tsx b/web/app/components/workflow/comment/thread.tsx index 63351c81f7..1b14dfbc0c 100644 --- a/web/app/components/workflow/comment/thread.tsx +++ b/web/app/components/workflow/comment/thread.tsx @@ -190,7 +190,9 @@ export const CommentThread: FC = memo(({ }, [mentionUsers]) useEffect(() => { - setReplyContent('') + Promise.resolve().then(() => { + setReplyContent('') + }) }, [comment.id]) useEffect(() => () => { diff --git a/web/app/components/workflow/features.tsx b/web/app/components/workflow/features.tsx index 0390b8ddaa..56b8b79ced 100644 --- a/web/app/components/workflow/features.tsx +++ b/web/app/components/workflow/features.tsx @@ -62,8 +62,6 @@ const Features = () => { file_upload: currentFeatures.file, } - console.log('Sending features to server:', transformedFeatures) - await updateFeatures({ appId, features: transformedFeatures, diff --git a/web/app/components/workflow/header/online-users.tsx b/web/app/components/workflow/header/online-users.tsx index 849137df57..7dfc38dd39 100644 --- a/web/app/components/workflow/header/online-users.tsx +++ b/web/app/components/workflow/header/online-users.tsx @@ -1,4 +1,5 @@ 'use client' +import type { OnlineUser } from '../collaboration/types' import { ChevronDownIcon } from '@heroicons/react/20/solid' import { useEffect, useState } from 'react' import { useReactFlow } from 'reactflow' @@ -16,7 +17,7 @@ import { useCollaboration } from '../collaboration/hooks/use-collaboration' import { getUserColor } from '../collaboration/utils/user-color' import { useStore } from '../store' -const useAvatarUrls = (users: any[]) => { +const useAvatarUrls = (users: OnlineUser[]) => { const [avatarUrls, setAvatarUrls] = useState>({}) useEffect(() => { @@ -59,7 +60,7 @@ const OnlineUsers = () => { const currentUserId = userProfile?.id const renderDisplayName = ( - user: any, + user: OnlineUser, baseClassName: string, suffixClassName: string, ) => { @@ -99,7 +100,7 @@ const OnlineUsers = () => { const visibleUsers = onlineUsers.slice(0, maxVisible) const remainingCount = onlineUsers.length - maxVisible - const getAvatarUrl = (user: any) => { + const getAvatarUrl = (user: OnlineUser) => { return avatarUrls[user.sid] || user.avatar } diff --git a/web/app/components/workflow/header/undo-redo.tsx b/web/app/components/workflow/header/undo-redo.tsx index 20cd16e5c3..5d7a2bdb5c 100644 --- a/web/app/components/workflow/header/undo-redo.tsx +++ b/web/app/components/workflow/header/undo-redo.tsx @@ -27,7 +27,9 @@ const UndoRedo: FC = ({ handleUndo, handleRedo }) => { } // Initial state - updateButtonStates() + Promise.resolve().then(() => { + updateButtonStates() + }) // Listen for undo/redo state changes const unsubscribe = collaborationManager.onUndoRedoStateChange((state) => { diff --git a/web/app/components/workflow/hooks-store/store.ts b/web/app/components/workflow/hooks-store/store.ts index b4fb63d740..9567823929 100644 --- a/web/app/components/workflow/hooks-store/store.ts +++ b/web/app/components/workflow/hooks-store/store.ts @@ -1,6 +1,8 @@ import type { FileUpload } from '../../base/features/types' +import type { TriggerType } from '@/app/components/workflow/header/test-run-menu' import type { BlockEnum, + CommonNodeType, Node, NodeDefault, ToolWithProvider, @@ -9,7 +11,7 @@ import type { import type { IOtherOptions } from '@/service/base' import type { SchemaTypeDefinition } from '@/service/use-common' import type { FlowType } from '@/types/common' -import type { VarInInspect } from '@/types/workflow' +import type { FetchWorkflowDraftResponse, VarInInspect } from '@/types/workflow' import { noop } from 'es-toolkit/function' import { useContext } from 'react' import { @@ -18,9 +20,17 @@ import { import { createStore } from 'zustand/vanilla' import { HooksStoreContext } from './provider' +export type AvailableNodeDefault = NodeDefault>> +export type WorkflowRunOptions = { + mode?: TriggerType + scheduleNodeId?: string + webhookNodeId?: string + pluginNodeId?: string + allNodeIds?: string[] +} export type AvailableNodesMetaData = { - nodes: NodeDefault[] - nodesMap?: Record> + nodes: AvailableNodeDefault[] + nodesMap?: Partial> } export type CommonHooksFnMap = { doSyncWorkflowDraft: ( @@ -36,9 +46,9 @@ export type CommonHooksFnMap = { handleRefreshWorkflowDraft: () => void handleBackupDraft: () => void handleLoadBackupDraft: () => void - handleRestoreFromPublishedWorkflow: (...args: any[]) => void - handleRun: (params: any, callback?: IOtherOptions, options?: any) => void - handleStopRun: (...args: any[]) => void + handleRestoreFromPublishedWorkflow: (publishedWorkflow: FetchWorkflowDraftResponse) => void + handleRun: (params: unknown, callback?: IOtherOptions, options?: WorkflowRunOptions) => void | Promise + handleStopRun: (taskId: string) => void handleStartWorkflowRun: () => void handleWorkflowStartRunInWorkflow: () => void handleWorkflowStartRunInChatflow: () => void @@ -54,7 +64,7 @@ export type CommonHooksFnMap = { hasNodeInspectVars: (nodeId: string) => boolean hasSetInspectVar: (nodeId: string, name: string, sysVars: VarInInspect[], conversationVars: VarInInspect[]) => boolean fetchInspectVarValue: (selector: ValueSelector, schemaTypeDefinitions: SchemaTypeDefinition[]) => Promise - editInspectVarValue: (nodeId: string, varId: string, value: any) => Promise + editInspectVarValue: (nodeId: string, varId: string, value: unknown) => Promise renameInspectVarName: (nodeId: string, oldName: string, newName: string) => Promise appendNodeInspectVars: (nodeId: string, payload: VarInInspect[], allNodes: Node[]) => void deleteInspectVar: (nodeId: string, varId: string) => Promise @@ -68,7 +78,7 @@ export type CommonHooksFnMap = { configsMap?: { flowId: string flowType: FlowType - fileSettings: FileUpload + fileSettings?: FileUpload } } @@ -82,9 +92,9 @@ export const createHooksStore = ({ handleRefreshWorkflowDraft = noop, handleBackupDraft = noop, handleLoadBackupDraft = noop, - handleRestoreFromPublishedWorkflow = noop, + handleRestoreFromPublishedWorkflow = (_publishedWorkflow: FetchWorkflowDraftResponse) => noop(), handleRun = noop, - handleStopRun = noop, + handleStopRun = (_taskId: string) => noop(), handleStartWorkflowRun = noop, handleWorkflowStartRunInWorkflow = noop, handleWorkflowStartRunInChatflow = noop, diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 5a9e4dacb7..38867ab166 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -362,7 +362,10 @@ export const useChecklistBeforePublish = () => { usedVars = getNodeUsedVars(node).filter(v => v.length > 0) } const checkData = getCheckData(node.data, datasets) - const { errorMessage } = nodesExtraData![node.data.type as BlockEnum].checkValid(checkData, t, moreDataForCheckValid) + const nodeMetaData = nodesExtraData?.[node.data.type as BlockEnum] + if (!nodeMetaData) + continue + const { errorMessage } = nodeMetaData.checkValid(checkData, t, moreDataForCheckValid) if (errorMessage) { notify({ type: 'error', message: `[${node.data.title}] ${errorMessage}` }) diff --git a/web/app/components/workflow/hooks/use-node-data-update.ts b/web/app/components/workflow/hooks/use-node-data-update.ts index 3ac9ad2783..e3ee645543 100644 --- a/web/app/components/workflow/hooks/use-node-data-update.ts +++ b/web/app/components/workflow/hooks/use-node-data-update.ts @@ -1,18 +1,16 @@ import type { SyncCallback } from './use-nodes-sync-draft' import { produce } from 'immer' import { useCallback } from 'react' -import { useStoreApi } from 'reactflow' import { useCollaborativeWorkflow } from './use-collaborative-workflow' import { useNodesSyncDraft } from './use-nodes-sync-draft' import { useNodesReadOnly } from './use-workflow' type NodeDataUpdatePayload = { id: string - data: Record + data: Record } export const useNodeDataUpdate = () => { - const store = useStoreApi() const { handleSyncWorkflowDraft } = useNodesSyncDraft() const { getNodesReadOnly } = useNodesReadOnly() const collaborativeWorkflow = useCollaborativeWorkflow() @@ -26,7 +24,7 @@ export const useNodeDataUpdate = () => { currentNode.data = { ...currentNode.data, ...data } }) setNodes(newNodes) - }, [store]) + }, [collaborativeWorkflow]) const handleNodeDataUpdateWithSyncDraft = useCallback(( payload: NodeDataUpdatePayload, diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 726df0e66c..3c87ee4f79 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -815,7 +815,10 @@ export const useNodesInteractions = () => { const nodesWithSameType = nodes.filter( node => node.data.type === nodeType, ) - const { defaultValue } = nodesMetaDataMap![nodeType] + const nodeMetaData = nodesMetaDataMap?.[nodeType] + if (!nodeMetaData) + return + const { defaultValue } = nodeMetaData const { newNode, newIterationStartNode, newLoopStartNode } = generateNewNode({ type: getNodeCustomTypeByNodeDataType(nodeType), @@ -1376,7 +1379,10 @@ export const useNodesInteractions = () => { const nodesWithSameType = nodes.filter( node => node.data.type === nodeType, ) - const { defaultValue } = nodesMetaDataMap![nodeType] + const nodeMetaData = nodesMetaDataMap?.[nodeType] + if (!nodeMetaData) + return + const { defaultValue } = nodeMetaData const { newNode: newCurrentNode, newIterationStartNode, @@ -1537,7 +1543,9 @@ export const useNodesInteractions = () => { return false if (node.type === CUSTOM_NOTE_NODE) return true - const { metaData } = nodesMetaDataMap![node.data.type as BlockEnum] + const metaData = nodesMetaDataMap?.[node.data.type as BlockEnum]?.metaData + if (!metaData) + return false if (metaData.isSingleton) return false return !node.data.isInIteration && !node.data.isInLoop @@ -1553,7 +1561,9 @@ export const useNodesInteractions = () => { return false if (node.type === CUSTOM_NOTE_NODE) return true - const { metaData } = nodesMetaDataMap![node.data.type as BlockEnum] + const metaData = nodesMetaDataMap?.[node.data.type as BlockEnum]?.metaData + if (!metaData) + return false return !metaData.isSingleton }) @@ -1588,12 +1598,15 @@ export const useNodesInteractions = () => { const parentChildrenToAppend: { parentId: string, childId: string, childType: BlockEnum }[] = [] clipboardElements.forEach((nodeToPaste, index) => { const nodeType = nodeToPaste.data.type + const nodeDefaultValue = nodeToPaste.type !== CUSTOM_NOTE_NODE + ? nodesMetaDataMap?.[nodeType]?.defaultValue + : undefined const { newNode, newIterationStartNode, newLoopStartNode } = generateNewNode({ type: nodeToPaste.type, data: { - ...(nodeToPaste.type !== CUSTOM_NOTE_NODE && nodesMetaDataMap![nodeType].defaultValue), + ...(nodeDefaultValue || {}), ...nodeToPaste.data, selected: false, _isBundled: false, @@ -1898,16 +1911,7 @@ export const useNodesInteractions = () => { return // Use collaborative undo from Loro - const undoResult = collaborationManager.undo() - - if (undoResult) { - // The undo operation will automatically trigger subscriptions - // which will update the nodes and edges through setupSubscriptions - console.log('Collaborative undo performed') - } - else { - console.log('Nothing to undo') - } + collaborationManager.undo() const { edges, nodes } = workflowHistoryStore.getState() if (edges.length === 0 && nodes.length === 0) return @@ -1928,16 +1932,7 @@ export const useNodesInteractions = () => { return // Use collaborative redo from Loro - const redoResult = collaborationManager.redo() - - if (redoResult) { - // The redo operation will automatically trigger subscriptions - // which will update the nodes and edges through setupSubscriptions - console.log('Collaborative redo performed') - } - else { - console.log('Nothing to redo') - } + collaborationManager.redo() const { edges, nodes } = workflowHistoryStore.getState() if (edges.length === 0 && nodes.length === 0) return diff --git a/web/app/components/workflow/hooks/use-workflow-comment.ts b/web/app/components/workflow/hooks/use-workflow-comment.ts index 18ed87b78c..d7d5db8e8d 100644 --- a/web/app/components/workflow/hooks/use-workflow-comment.ts +++ b/web/app/components/workflow/hooks/use-workflow-comment.ts @@ -10,6 +10,13 @@ import { useStore } from '../store' import { ControlMode } from '../types' const EMPTY_USERS: UserProfile[] = [] +type CommentDetailResponse = WorkflowCommentDetail | { data: WorkflowCommentDetail } + +const getCommentDetail = (response: CommentDetailResponse): WorkflowCommentDetail => { + if ('data' in response) + return response.data + return response +} export const useWorkflowComment = () => { const params = useParams() @@ -56,8 +63,8 @@ export const useWorkflowComment = () => { if (!appId) return - const detailResponse = await fetchWorkflowComment(appId, commentId) - const detail = (detailResponse as any)?.data ?? detailResponse + const detailResponse = await fetchWorkflowComment(appId, commentId) as CommentDetailResponse + const detail = getCommentDetail(detailResponse) commentDetailCacheRef.current = { ...commentDetailCacheRef.current, @@ -106,8 +113,6 @@ export const useWorkflowComment = () => { if (!pendingComment) return - console.log('Submitting comment:', { appId, pendingComment, content, mentionedUserIds }) - if (!appId) { console.error('AppId is missing') return @@ -128,9 +133,10 @@ export const useWorkflowComment = () => { mentioned_user_ids: mentionedUserIds, }) - console.log('Comment created successfully:', newComment) - - const createdAt = (newComment as any)?.created_at + const createdAt = Number(newComment.created_at) + const createdAtSeconds = Number.isNaN(createdAt) + ? Math.floor(Date.parse(newComment.created_at) / 1000) + : createdAt const createdByAccount = { id: userProfile?.id ?? '', name: userProfile?.name ?? '', @@ -162,8 +168,8 @@ export const useWorkflowComment = () => { content, created_by: createdByAccount.id, created_by_account: createdByAccount, - created_at: createdAt, - updated_at: createdAt, + created_at: createdAtSeconds, + updated_at: createdAtSeconds, resolved: false, mention_count: mentionedUserIds.length, reply_count: 0, @@ -177,8 +183,8 @@ export const useWorkflowComment = () => { content, created_by: createdByAccount.id, created_by_account: createdByAccount, - created_at: createdAt, - updated_at: createdAt, + created_at: createdAtSeconds, + updated_at: createdAtSeconds, resolved: false, replies: [], mentions: mentionedUserIds.map(mentionedId => ({ @@ -246,8 +252,8 @@ export const useWorkflowComment = () => { setActiveCommentLoading(!cachedDetail) try { - const detailResponse = await fetchWorkflowComment(appId, comment.id) - const detail = (detailResponse as any)?.data ?? detailResponse + const detailResponse = await fetchWorkflowComment(appId, comment.id) as CommentDetailResponse + const detail = getCommentDetail(detailResponse) commentDetailCacheRef.current = { ...commentDetailCacheRef.current, @@ -499,13 +505,8 @@ export const useWorkflowComment = () => { elementX: number elementY: number }) => { - if (controlMode === ControlMode.Comment) { - console.log('Setting pending comment at screen position:', mousePosition) + if (controlMode === ControlMode.Comment) setPendingComment(mousePosition) - } - else { - console.log('Control mode is not Comment:', controlMode) - } }, [controlMode, setPendingComment]) return { diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 1f7943c7e4..3941dae7c0 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -4,10 +4,13 @@ import type { FC } from 'react' import type { Viewport, } from 'reactflow' +import type { CursorPosition, OnlineUser } from './collaboration/types' import type { Shape as HooksStoreShape } from './hooks-store' import type { WorkflowSliceShape } from './store/workflow/workflow-slice' import type { + ConversationVariable, Edge, + EnvironmentVariable, Node, } from './types' import type { VarInInspect } from '@/types/workflow' @@ -124,15 +127,37 @@ const edgeTypes = { [CUSTOM_EDGE]: CustomEdge, } +type WorkflowDataUpdatePayload = { + nodes: Node[] + edges: Edge[] + viewport?: Viewport + hash?: string + features?: unknown + conversation_variables?: ConversationVariable[] + environment_variables?: EnvironmentVariable[] +} + +type WorkflowEvent = { + type?: string + payload?: unknown +} + +const isWorkflowDataUpdatePayload = (payload: unknown): payload is WorkflowDataUpdatePayload => { + if (!payload || typeof payload !== 'object') + return false + const candidate = payload as WorkflowDataUpdatePayload + return Array.isArray(candidate.nodes) && Array.isArray(candidate.edges) +} + export type WorkflowProps = { nodes: Node[] edges: Edge[] viewport?: Viewport children?: React.ReactNode - onWorkflowDataUpdate?: (v: any) => void - cursors?: Record + onWorkflowDataUpdate?: (v: WorkflowDataUpdatePayload) => void + cursors?: Record myUserId?: string | null - onlineUsers?: any[] + onlineUsers?: OnlineUser[] } export const Workflow: FC = memo(({ nodes: originalNodes, @@ -236,19 +261,20 @@ export const Workflow: FC = memo(({ const { t } = useTranslation() const store = useStoreApi() - eventEmitter?.useSubscription((v: any) => { - if (v.type === WORKFLOW_DATA_UPDATE) { - setNodes(v.payload.nodes) - store.getState().setNodes(v.payload.nodes) - setEdges(v.payload.edges) + eventEmitter?.useSubscription((event) => { + const workflowEvent = event as unknown as WorkflowEvent + if (workflowEvent.type === WORKFLOW_DATA_UPDATE && isWorkflowDataUpdatePayload(workflowEvent.payload)) { + setNodes(workflowEvent.payload.nodes) + store.getState().setNodes(workflowEvent.payload.nodes) + setEdges(workflowEvent.payload.edges) - if (v.payload.viewport) - reactflow.setViewport(v.payload.viewport) + if (workflowEvent.payload.viewport) + reactflow.setViewport(workflowEvent.payload.viewport) - if (v.payload.hash) - setSyncWorkflowDraftHash(v.payload.hash) + if (workflowEvent.payload.hash) + setSyncWorkflowDraftHash(workflowEvent.payload.hash) - onWorkflowDataUpdate?.(v.payload) + onWorkflowDataUpdate?.(workflowEvent.payload) setTimeout(() => setControlPromptEditorRerenderKey(Date.now())) } @@ -635,9 +661,9 @@ export const Workflow: FC = memo(({ type WorkflowWithInnerContextProps = WorkflowProps & { hooksStore?: Partial - cursors?: Record + cursors?: Record myUserId?: string | null - onlineUsers?: any[] + onlineUsers?: OnlineUser[] } export const WorkflowWithInnerContext = memo(({ hooksStore, diff --git a/web/app/components/workflow/nodes/_base/components/title-description-input.tsx b/web/app/components/workflow/nodes/_base/components/title-description-input.tsx index 8f8ed7e705..4794431619 100644 --- a/web/app/components/workflow/nodes/_base/components/title-description-input.tsx +++ b/web/app/components/workflow/nodes/_base/components/title-description-input.tsx @@ -42,7 +42,9 @@ export const TitleInput = memo(({ // Sync local state with incoming collaborative updates so remote title edits appear immediately. useEffect(() => { - setLocalValue(value) + Promise.resolve().then(() => { + setLocalValue(value) + }) }, [value]) return ( diff --git a/web/app/components/workflow/nodes/data-source-empty/hooks.ts b/web/app/components/workflow/nodes/data-source-empty/hooks.ts index 93191b0261..ffeb02788c 100644 --- a/web/app/components/workflow/nodes/data-source-empty/hooks.ts +++ b/web/app/components/workflow/nodes/data-source-empty/hooks.ts @@ -22,9 +22,10 @@ export const useReplaceDataSourceNode = (id: string) => { if (emptyNodeIndex < 0) return - const { - defaultValue, - } = nodesMetaDataMap![type] + const nodeMetaData = nodesMetaDataMap?.[type] + if (!nodeMetaData) + return + const { defaultValue } = nodeMetaData const emptyNode = nodes[emptyNodeIndex] const { newNode } = generateNewNode({ data: { diff --git a/web/app/components/workflow/nodes/http/hooks/use-key-value-list.ts b/web/app/components/workflow/nodes/http/hooks/use-key-value-list.ts index 5ea2438020..f48738b059 100644 --- a/web/app/components/workflow/nodes/http/hooks/use-key-value-list.ts +++ b/web/app/components/workflow/nodes/http/hooks/use-key-value-list.ts @@ -41,13 +41,15 @@ const useKeyValueList = (value: string, onChange: (value: string) => void, noFil }, [noFilter, onChange, value]) useEffect(() => { - doSetList((prev) => { - const targetItems = value ? strToKeyValueList(value) : [] - const currentValue = stringifyList(prev, noFilter) - const targetValue = stringifyList(targetItems, noFilter) - if (currentValue === targetValue) - return prev - return normalizeList(targetItems) + Promise.resolve().then(() => { + doSetList((prev) => { + const targetItems = value ? strToKeyValueList(value) : [] + const currentValue = stringifyList(prev, noFilter) + const targetValue = stringifyList(targetItems, noFilter) + if (currentValue === targetValue) + return prev + return normalizeList(targetItems) + }) }) }, [value, noFilter]) const addItem = useCallback(() => { diff --git a/web/app/components/workflow/nodes/iteration/use-interactions.ts b/web/app/components/workflow/nodes/iteration/use-interactions.ts index 1e471590e4..f1c299c5f8 100644 --- a/web/app/components/workflow/nodes/iteration/use-interactions.ts +++ b/web/app/components/workflow/nodes/iteration/use-interactions.ts @@ -115,6 +115,7 @@ export const useNodeIterationInteractions = () => { const copyChildren = childrenNodes.map((child, index) => { const childNodeType = child.data.type as BlockEnum const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType) + const defaultValue = nodesMetaDataMap?.[childNodeType]?.defaultValue ?? {} if (!childNodeTypeCount[childNodeType]) childNodeTypeCount[childNodeType] = nodesWithSameType.length + 1 @@ -124,7 +125,7 @@ export const useNodeIterationInteractions = () => { const { newNode } = generateNewNode({ type: getNodeCustomTypeByNodeDataType(childNodeType), data: { - ...nodesMetaDataMap![childNodeType].defaultValue, + ...defaultValue, ...child.data, selected: false, _isBundled: false, diff --git a/web/app/components/workflow/nodes/loop/use-interactions.ts b/web/app/components/workflow/nodes/loop/use-interactions.ts index f611991975..f884ef6751 100644 --- a/web/app/components/workflow/nodes/loop/use-interactions.ts +++ b/web/app/components/workflow/nodes/loop/use-interactions.ts @@ -109,7 +109,7 @@ export const useNodeLoopInteractions = () => { return childrenNodes.map((child, index) => { const childNodeType = child.data.type as BlockEnum - const { defaultValue } = nodesMetaDataMap![childNodeType] + const defaultValue = nodesMetaDataMap?.[childNodeType]?.defaultValue ?? {} const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType) const { newNode } = generateNewNode({ type: getNodeCustomTypeByNodeDataType(childNodeType), diff --git a/web/app/components/workflow/operator/add-block.tsx b/web/app/components/workflow/operator/add-block.tsx index a5c25a5b32..328d204a4f 100644 --- a/web/app/components/workflow/operator/add-block.tsx +++ b/web/app/components/workflow/operator/add-block.tsx @@ -63,9 +63,10 @@ const AddBlock = ({ } = store.getState() const nodes = getNodes() const nodesWithSameType = nodes.filter(node => node.data.type === type) - const { - defaultValue, - } = nodesMetaDataMap![type] + const nodeMetaData = nodesMetaDataMap?.[type] + if (!nodeMetaData) + return + const { defaultValue } = nodeMetaData const { newNode } = generateNewNode({ type: getNodeCustomTypeByNodeDataType(type), data: { diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index b043a2d951..aa4382b1b8 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -263,9 +263,6 @@ "app/components/app/app-publisher/index.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 3 - }, - "ts/no-explicit-any": { - "count": 6 } }, "app/components/app/configuration/config-prompt/advanced-prompt-input.tsx": { @@ -3045,9 +3042,6 @@ "app/components/tools/mcp/mcp-service-card.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 1 - }, - "ts/no-explicit-any": { - "count": 4 } }, "app/components/tools/mcp/modal.tsx": { @@ -3113,11 +3107,6 @@ "count": 1 } }, - "app/components/workflow-app/components/workflow-main.tsx": { - "ts/no-explicit-any": { - "count": 2 - } - }, "app/components/workflow-app/components/workflow-onboarding-modal/index.spec.tsx": { "ts/no-explicit-any": { "count": 2 @@ -3297,11 +3286,6 @@ "count": 2 } }, - "app/components/workflow/hooks-store/store.ts": { - "ts/no-explicit-any": { - "count": 6 - } - }, "app/components/workflow/hooks/use-checklist.ts": { "ts/no-empty-object-type": { "count": 2 @@ -3325,11 +3309,6 @@ "count": 1 } }, - "app/components/workflow/hooks/use-node-data-update.ts": { - "ts/no-explicit-any": { - "count": 1 - } - }, "app/components/workflow/hooks/use-nodes-interactions.ts": { "react-hooks/immutability": { "count": 1 @@ -3373,11 +3352,6 @@ "count": 1 } }, - "app/components/workflow/index.tsx": { - "ts/no-explicit-any": { - "count": 2 - } - }, "app/components/workflow/nodes/_base/components/add-variable-popup-with-position.tsx": { "ts/no-explicit-any": { "count": 2 diff --git a/web/next.config.js b/web/next.config.js index fbe6b0bc1e..c6892f6997 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -35,7 +35,7 @@ const nextConfig = { bundler: 'turbopack', }), }, - webpack: (config, { dev, isServer }) => { + webpack: (config, { dev: _dev, isServer: _isServer }) => { config.plugins.push(codeInspectorPlugin({ bundler: 'webpack' })) config.experiments = {