feat(web): preserve snippet draft fields

This commit is contained in:
JzoNg 2026-06-23 11:06:22 +08:00
parent cd2c40e7c0
commit 0bfce31e25
3 changed files with 57 additions and 4 deletions

View File

@ -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<SnippetInputField[]>(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,

View File

@ -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,
}))
})
})

View File

@ -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<string, Promise<unknown>>()
const enqueueSnippetDraftSync = <Result>(
@ -43,17 +48,18 @@ const enqueueSnippetDraftSync = <Result>(
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 {