diff --git a/web/app/components/snippets/components/snippet-main.tsx b/web/app/components/snippets/components/snippet-main.tsx index bf7c68a8ba5..19327b5faad 100644 --- a/web/app/components/snippets/components/snippet-main.tsx +++ b/web/app/components/snippets/components/snippet-main.tsx @@ -9,6 +9,7 @@ import { useCallback, useEffect, useMemo, + useRef, useState, } from 'react' import { useTranslation } from 'react-i18next' @@ -135,12 +136,16 @@ const SnippetMain = ({ const effectiveDraftNodes = localDraftState?.nodes ?? draftNodes const effectiveDraftEdges = localDraftState?.edges ?? draftEdges const effectiveDraftViewport = localDraftState?.viewport ?? draftViewport + const currentInputFieldsRef = useRef(effectiveDraftPayload.inputFields) const { graph, snippet } = effectiveDraftPayload const canSave = currentCanvasNodeCount > 0 + const getCurrentInputFields = useCallback(() => currentInputFieldsRef.current, []) const { doSyncWorkflowDraft: syncWorkflowDraft, syncWorkflowDraftWhenPageClose, - } = useNodesSyncDraft(snippetId) + } = useNodesSyncDraft(snippetId, { + getInputFields: getCurrentInputFields, + }) const workflowStore = useWorkflowStore() const { handleRefreshWorkflowDraft } = useSnippetRefreshDraft(snippetId) const { @@ -220,6 +225,7 @@ const SnippetMain = ({ }, [reset, snippetId]) useEffect(() => { + currentInputFieldsRef.current = effectiveDraftPayload.inputFields setFields(effectiveDraftPayload.inputFields) }, [effectiveDraftPayload.inputFields, setFields, snippetId]) @@ -248,6 +254,7 @@ const SnippetMain = ({ ) => syncWorkflowDraft(...args), [syncWorkflowDraft]) const handleFieldsChange = useCallback((nextFields: SnippetInputField[]) => { + currentInputFieldsRef.current = nextFields handleSnippetFieldsChange(nextFields) }, [handleSnippetFieldsChange]) @@ -279,6 +286,7 @@ const SnippetMain = ({ ? syncedDraftPayload.input_fields as SnippetInputField[] : fields + currentInputFieldsRef.current = inputFields setLocalDraftState({ payload: { ...draftPayload, diff --git a/web/app/components/snippets/hooks/__tests__/use-nodes-sync-draft.spec.ts b/web/app/components/snippets/hooks/__tests__/use-nodes-sync-draft.spec.ts index ee5a2663134..df75ec1714b 100644 --- a/web/app/components/snippets/hooks/__tests__/use-nodes-sync-draft.spec.ts +++ b/web/app/components/snippets/hooks/__tests__/use-nodes-sync-draft.spec.ts @@ -139,6 +139,27 @@ describe('snippet/use-nodes-sync-draft', () => { expect(mockUseNodesReadOnlyByCanEdit).toHaveBeenCalledWith(true) }) + it('should use provided input_fields when the snippet store is not initialized yet', async () => { + useSnippetDetailStore.setState({ + fields: [], + }) + const inputFields = [createInputField('topic')] + const { result } = renderHook(() => useNodesSyncDraft('snippet-1', { + getInputFields: () => inputFields, + })) + + await act(async () => { + await result.current.doSyncWorkflowDraft() + }) + + expect(mockSyncDraftWorkflow).toHaveBeenCalledWith({ + params: { snippetId: 'snippet-1' }, + body: expect.objectContaining({ + input_fields: inputFields, + }), + }) + }) + it('should snapshot graph before queued draft sync executes', async () => { deferSerialCallbacks = true const { result } = renderHook(() => useNodesSyncDraft('snippet-1')) @@ -246,4 +267,22 @@ describe('snippet/use-nodes-sync-draft', () => { hash: 'draft-hash', }) }) + + it('should use provided input_fields when syncing on page close before the snippet store initializes', () => { + useSnippetDetailStore.setState({ + fields: [], + }) + const inputFields = [createInputField('topic')] + const { result } = renderHook(() => useNodesSyncDraft('snippet-1', { + getInputFields: () => inputFields, + })) + + act(() => { + result.current.syncWorkflowDraftWhenPageClose() + }) + + expect(mockPostWithKeepalive).toHaveBeenCalledWith('/api/snippets/snippet-1/workflows/draft', expect.objectContaining({ + input_fields: inputFields, + })) + }) }) diff --git a/web/app/components/snippets/hooks/use-nodes-sync-draft.ts b/web/app/components/snippets/hooks/use-nodes-sync-draft.ts index e38a0539aa6..8550a5200c2 100644 --- a/web/app/components/snippets/hooks/use-nodes-sync-draft.ts +++ b/web/app/components/snippets/hooks/use-nodes-sync-draft.ts @@ -8,6 +8,7 @@ import { useNodesReadOnlyByCanEdit } from '@/app/components/workflow/hooks/use-w import { useWorkflowStore } from '@/app/components/workflow/store' import { API_PREFIX } from '@/config' import { consoleClient } from '@/service/client' +// eslint-disable-next-line no-restricted-imports import { postWithKeepalive } from '@/service/fetch' import { useSnippetDetailStore } from '../store' import { useSnippetRefreshDraft } from './use-snippet-refresh-draft' @@ -24,6 +25,10 @@ type SyncInputFieldsDraftCallback = SyncDraftCallback & { onRefresh?: (inputFields: SnippetInputField[]) => void } +type UseNodesSyncDraftOptions = { + getInputFields?: () => SnippetInputField[] +} + const snippetDraftSyncQueues = new Map>() const enqueueSnippetDraftSync = ( @@ -43,17 +48,18 @@ const enqueueSnippetDraftSync = ( return nextTask } -export const useNodesSyncDraft = (snippetId: string) => { +export const useNodesSyncDraft = (snippetId: string, options: UseNodesSyncDraftOptions = {}) => { const store = useStoreApi() const workflowStore = useWorkflowStore() const { getNodesReadOnly } = useNodesReadOnlyByCanEdit(true) const { handleRefreshWorkflowDraft } = useSnippetRefreshDraft(snippetId) + const { getInputFields } = options const getInputFieldsSyncPayload = useCallback((inputFields?: SnippetInputField[]) => { return { - input_fields: inputFields ?? useSnippetDetailStore.getState().fields, + input_fields: inputFields ?? getInputFields?.() ?? useSnippetDetailStore.getState().fields, } - }, []) + }, [getInputFields]) const getDraftSyncPayload = useCallback((inputFields?: SnippetInputField[]) => { const {