From 0f13aabea8223713fd268d75de67599f9a74aa69 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Sun, 29 Mar 2026 16:31:38 +0800 Subject: [PATCH] feat(web): input fields in snippet --- .../snippets/__tests__/index.spec.tsx | 1 + .../__tests__/snippet-main.spec.tsx | 216 ++++++++++++++++++ .../__tests__/workflow-panel.spec.tsx | 3 +- .../snippets/components/panel/index.tsx | 57 +---- .../snippets/components/snippet-children.tsx | 12 +- .../snippets/components/snippet-main.tsx | 33 +-- .../snippets/components/workflow-panel.tsx | 19 +- .../hooks/__tests__/use-snippet-init.spec.ts | 62 +++++ .../snippets/hooks/use-nodes-sync-draft.ts | 95 +++++--- .../hooks/use-snippet-refresh-draft.ts | 4 +- web/service/use-snippets.ts | 4 +- web/types/snippet.ts | 3 +- 12 files changed, 395 insertions(+), 114 deletions(-) create mode 100644 web/app/components/snippets/components/__tests__/snippet-main.spec.tsx diff --git a/web/app/components/snippets/__tests__/index.spec.tsx b/web/app/components/snippets/__tests__/index.spec.tsx index e0a2777f2a..3c9a831035 100644 --- a/web/app/components/snippets/__tests__/index.spec.tsx +++ b/web/app/components/snippets/__tests__/index.spec.tsx @@ -22,6 +22,7 @@ vi.mock('../hooks/use-configs-map', () => ({ vi.mock('../hooks/use-nodes-sync-draft', () => ({ useNodesSyncDraft: () => ({ doSyncWorkflowDraft: vi.fn(), + syncInputFieldsDraft: vi.fn(), syncWorkflowDraftWhenPageClose: vi.fn(), }), })) diff --git a/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx b/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx new file mode 100644 index 0000000000..7550d7d045 --- /dev/null +++ b/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx @@ -0,0 +1,216 @@ +import type { WorkflowProps } from '@/app/components/workflow' +import type { SnippetDetailPayload, SnippetInputField, SnippetSection } from '@/models/snippet' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { PipelineInputVarType } from '@/models/pipeline' +import SnippetMain from '../snippet-main' + +const mockSetAppSidebarExpand = vi.fn() +const mockSyncInputFieldsDraft = vi.fn() +const mockCloseEditor = vi.fn() +const mockOpenEditor = vi.fn() +const mockReset = vi.fn() +const mockSetInputPanelOpen = vi.fn() +const mockToggleInputPanel = vi.fn() +const mockTogglePublishMenu = vi.fn() + +vi.mock('@/hooks/use-breakpoints', () => ({ + default: () => 'desktop', + MediaType: { mobile: 'mobile', desktop: 'desktop' }, +})) + +vi.mock('@/app/components/app/store', () => ({ + useStore: (selector: (state: { setAppSidebarExpand: typeof mockSetAppSidebarExpand }) => unknown) => selector({ + setAppSidebarExpand: mockSetAppSidebarExpand, + }), +})) + +vi.mock('@/app/components/snippets/store', () => ({ + useSnippetDetailStore: (selector: (state: { + editingField: SnippetInputField | null + isEditorOpen: boolean + isInputPanelOpen: boolean + isPublishMenuOpen: boolean + closeEditor: typeof mockCloseEditor + openEditor: typeof mockOpenEditor + reset: typeof mockReset + setInputPanelOpen: typeof mockSetInputPanelOpen + toggleInputPanel: typeof mockToggleInputPanel + togglePublishMenu: typeof mockTogglePublishMenu + }) => unknown) => selector({ + editingField: null, + isEditorOpen: false, + isInputPanelOpen: true, + isPublishMenuOpen: false, + closeEditor: mockCloseEditor, + openEditor: mockOpenEditor, + reset: mockReset, + setInputPanelOpen: mockSetInputPanelOpen, + toggleInputPanel: mockToggleInputPanel, + togglePublishMenu: mockTogglePublishMenu, + }), +})) + +vi.mock('@/app/components/snippets/hooks/use-configs-map', () => ({ + useConfigsMap: () => ({ + flowId: 'snippet-1', + flowType: 'snippet', + fileSettings: {}, + }), +})) + +vi.mock('@/app/components/snippets/hooks/use-nodes-sync-draft', () => ({ + useNodesSyncDraft: () => ({ + doSyncWorkflowDraft: vi.fn(), + syncInputFieldsDraft: mockSyncInputFieldsDraft, + syncWorkflowDraftWhenPageClose: vi.fn(), + }), +})) + +vi.mock('@/app/components/snippets/hooks/use-snippet-refresh-draft', () => ({ + useSnippetRefreshDraft: () => ({ + handleRefreshWorkflowDraft: vi.fn(), + }), +})) + +vi.mock('@/app/components/app-sidebar', () => ({ + default: ({ + renderHeader, + renderNavigation, + }: { + renderHeader?: (modeState: string) => React.ReactNode + renderNavigation?: (modeState: string) => React.ReactNode + }) => ( +
+
{renderHeader?.('expand')}
+
{renderNavigation?.('expand')}
+
+ ), +})) + +vi.mock('@/app/components/app-sidebar/nav-link', () => ({ + default: ({ name }: { name: string }) =>
{name}
, +})) + +vi.mock('@/app/components/app-sidebar/snippet-info', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/evaluation', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/workflow', () => ({ + WorkflowWithInnerContext: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})) + +vi.mock('@/app/components/snippets/components/snippet-children', () => ({ + default: ({ + onRemoveField, + onSubmitField, + }: { + onRemoveField: (index: number) => void + onSubmitField: (field: SnippetInputField) => void + }) => ( +
+ + +
+ ), +})) + +const payload: SnippetDetailPayload = { + snippet: { + id: 'snippet-1', + name: 'Snippet', + description: 'desc', + author: '', + updatedAt: '2026-03-29 10:00', + usage: '0', + icon: '', + iconBackground: '', + }, + graph: { + nodes: [], + edges: [], + viewport: { x: 0, y: 0, zoom: 1 }, + }, + inputFields: [ + { + type: PipelineInputVarType.textInput, + label: 'Blog URL', + variable: 'blog_url', + required: true, + }, + ], + uiMeta: { + inputFieldCount: 1, + checklistCount: 0, + autoSavedAt: '2026-03-29 10:00', + }, +} + +const renderSnippetMain = (section: SnippetSection = 'orchestrate') => { + return render( + , + ) +} + +describe('SnippetMain', () => { + beforeEach(() => { + vi.clearAllMocks() + mockSyncInputFieldsDraft.mockResolvedValue(undefined) + }) + + describe('Input Fields Sync', () => { + it('should sync draft input_fields when removing a field from the panel', async () => { + renderSnippetMain() + + fireEvent.click(screen.getByRole('button', { name: 'remove' })) + + await waitFor(() => { + expect(mockSyncInputFieldsDraft).toHaveBeenCalledWith([], { + onRefresh: expect.any(Function), + }) + }) + }) + + it('should sync draft input_fields when submitting a field from the editor', async () => { + renderSnippetMain() + + fireEvent.click(screen.getByRole('button', { name: 'submit' })) + + await waitFor(() => { + expect(mockSyncInputFieldsDraft).toHaveBeenCalledWith([ + payload.inputFields[0], + { + type: PipelineInputVarType.textInput, + label: 'New Field', + variable: 'new_field', + required: true, + }, + ], { + onRefresh: expect.any(Function), + }) + }) + }) + }) +}) diff --git a/web/app/components/snippets/components/__tests__/workflow-panel.spec.tsx b/web/app/components/snippets/components/__tests__/workflow-panel.spec.tsx index e897216e6a..7b81a98494 100644 --- a/web/app/components/snippets/components/__tests__/workflow-panel.spec.tsx +++ b/web/app/components/snippets/components/__tests__/workflow-panel.spec.tsx @@ -35,8 +35,7 @@ describe('SnippetWorkflowPanel', () => { onCloseEditor={vi.fn()} onSubmitField={vi.fn()} onRemoveField={vi.fn()} - onPrimarySortChange={vi.fn()} - onSecondarySortChange={vi.fn()} + onSortChange={vi.fn()} />, ) diff --git a/web/app/components/snippets/components/panel/index.tsx b/web/app/components/snippets/components/panel/index.tsx index c949a35f14..96de81d2ee 100644 --- a/web/app/components/snippets/components/panel/index.tsx +++ b/web/app/components/snippets/components/panel/index.tsx @@ -5,7 +5,6 @@ import type { SnippetInputField } from '@/models/snippet' import { memo, useCallback } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' -import Divider from '@/app/components/base/divider' import FieldListContainer from '@/app/components/rag-pipeline/components/panel/input-field/field-list/field-list-container' type SnippetInputFieldPanelProps = { @@ -14,8 +13,7 @@ type SnippetInputFieldPanelProps = { onAdd: () => void onEdit: (field: SnippetInputField) => void onRemove: (index: number) => void - onPrimarySortChange: (fields: SnippetInputField[]) => void - onSecondarySortChange: (fields: SnippetInputField[]) => void + onSortChange: (fields: SnippetInputField[]) => void } const toInputFields = (list: SortableItem[]) => { @@ -31,32 +29,19 @@ const SnippetInputFieldPanel = ({ onAdd, onEdit, onRemove, - onPrimarySortChange, - onSecondarySortChange, + onSortChange, }: SnippetInputFieldPanelProps) => { const { t } = useTranslation('snippet') - const primaryFields = fields.slice(0, 2) - const secondaryFields = fields.slice(2) - const handlePrimaryRemove = useCallback((index: number) => { + const handleRemove = useCallback((index: number) => { onRemove(index) }, [onRemove]) - const handleSecondaryRemove = useCallback((index: number) => { - onRemove(index + primaryFields.length) - }, [onRemove, primaryFields.length]) - - const handlePrimaryEdit = useCallback((id: string) => { - const field = primaryFields.find(item => item.variable === id) + const handleEdit = useCallback((id: string) => { + const field = fields.find(item => item.variable === id) if (field) onEdit(field) - }, [onEdit, primaryFields]) - - const handleSecondaryEdit = useCallback((id: string) => { - const field = secondaryFields.find(item => item.variable === id) - if (field) - onEdit(field) - }, [onEdit, secondaryFields]) + }, [fields, onEdit]) return (
@@ -79,37 +64,19 @@ const SnippetInputFieldPanel = ({
-
-
- {t('panelPrimaryGroup')} -
onPrimarySortChange(toInputFields(list))} - onRemoveField={handlePrimaryRemove} - onEditField={handlePrimaryEdit} - /> - -
- -
- -
- {t('panelSecondaryGroup')} -
- onSecondarySortChange(toInputFields(list))} - onRemoveField={handleSecondaryRemove} - onEditField={handleSecondaryEdit} + className="flex flex-col gap-y-1 px-4 py-4" + inputFields={fields} + onListSortChange={list => onSortChange(toInputFields(list))} + onRemoveField={handleRemove} + onEditField={handleEdit} />
diff --git a/web/app/components/snippets/components/snippet-children.tsx b/web/app/components/snippets/components/snippet-children.tsx index b812e56a71..af5da29cb0 100644 --- a/web/app/components/snippets/components/snippet-children.tsx +++ b/web/app/components/snippets/components/snippet-children.tsx @@ -22,8 +22,7 @@ type SnippetChildrenProps = { onCloseEditor: () => void onSubmitField: (field: SnippetInputField) => void onRemoveField: (index: number) => void - onPrimarySortChange: (fields: SnippetInputField[]) => void - onSecondarySortChange: (fields: SnippetInputField[]) => void + onSortChange: (fields: SnippetInputField[]) => void } const SnippetChildren = ({ @@ -41,8 +40,7 @@ const SnippetChildren = ({ onCloseEditor, onSubmitField, onRemoveField, - onPrimarySortChange, - onSecondarySortChange, + onSortChange, }: SnippetChildrenProps) => { return ( <> @@ -66,8 +64,7 @@ const SnippetChildren = ({ onCloseEditor={onCloseEditor} onSubmitField={onSubmitField} onRemoveField={onRemoveField} - onPrimarySortChange={onPrimarySortChange} - onSecondarySortChange={onSecondarySortChange} + onSortChange={onSortChange} /> {isPublishMenuOpen && ( @@ -85,8 +82,7 @@ const SnippetChildren = ({ onAdd={() => onOpenEditor()} onEdit={onOpenEditor} onRemove={onRemoveField} - onPrimarySortChange={onPrimarySortChange} - onSecondarySortChange={onSecondarySortChange} + onSortChange={onSortChange} />
diff --git a/web/app/components/snippets/components/snippet-main.tsx b/web/app/components/snippets/components/snippet-main.tsx index a5441b6df8..770404acca 100644 --- a/web/app/components/snippets/components/snippet-main.tsx +++ b/web/app/components/snippets/components/snippet-main.tsx @@ -61,6 +61,7 @@ const SnippetMain = ({ const [fields, setFields] = useState(payload.inputFields) const { doSyncWorkflowDraft, + syncInputFieldsDraft, syncWorkflowDraftWhenPageClose, } = useNodesSyncDraft(snippetId) const { handleRefreshWorkflowDraft } = useSnippetRefreshDraft(snippetId) @@ -100,19 +101,16 @@ const SnippetMain = ({ setAppSidebarExpand(isMobile ? mode : localeMode) }, [isMobile, setAppSidebarExpand]) - const primaryFields = useMemo(() => fields.slice(0, 2), [fields]) - const secondaryFields = useMemo(() => fields.slice(2), [fields]) - - const handlePrimarySortChange = (newFields: SnippetInputField[]) => { - setFields([...newFields, ...secondaryFields]) - } - - const handleSecondarySortChange = (newFields: SnippetInputField[]) => { - setFields([...primaryFields, ...newFields]) + const handleSortChange = (newFields: SnippetInputField[]) => { + setFields(newFields) } const handleRemoveField = (index: number) => { - setFields(current => current.filter((_, currentIndex) => currentIndex !== index)) + const nextFields = fields.filter((_, currentIndex) => currentIndex !== index) + setFields(nextFields) + void syncInputFieldsDraft(nextFields, { + onRefresh: setFields, + }) } const handleSubmitField = (field: SnippetInputField) => { @@ -124,10 +122,14 @@ const SnippetMain = ({ return } - if (originalVariable) - setFields(current => current.map(item => item.variable === originalVariable ? field : item)) - else - setFields(current => [...current, field]) + const nextFields = originalVariable + ? fields.map(item => item.variable === originalVariable ? field : item) + : [...fields, field] + + setFields(nextFields) + void syncInputFieldsDraft(nextFields, { + onRefresh: setFields, + }) closeEditor() } @@ -205,8 +207,7 @@ const SnippetMain = ({ onCloseEditor={closeEditor} onSubmitField={handleSubmitField} onRemoveField={handleRemoveField} - onPrimarySortChange={handlePrimarySortChange} - onSecondarySortChange={handleSecondarySortChange} + onSortChange={handleSortChange} /> )} diff --git a/web/app/components/snippets/components/workflow-panel.tsx b/web/app/components/snippets/components/workflow-panel.tsx index 47769899b8..80c41df8a0 100644 --- a/web/app/components/snippets/components/workflow-panel.tsx +++ b/web/app/components/snippets/components/workflow-panel.tsx @@ -18,8 +18,7 @@ type SnippetWorkflowPanelProps = { onCloseEditor: () => void onSubmitField: (field: SnippetInputField) => void onRemoveField: (index: number) => void - onPrimarySortChange: (fields: SnippetInputField[]) => void - onSecondarySortChange: (fields: SnippetInputField[]) => void + onSortChange: (fields: SnippetInputField[]) => void } const SnippetPanelOnLeft = ({ @@ -32,8 +31,7 @@ const SnippetPanelOnLeft = ({ onCloseEditor, onSubmitField, onRemoveField, - onPrimarySortChange, - onSecondarySortChange, + onSortChange, }: SnippetWorkflowPanelProps) => { return (
@@ -51,8 +49,7 @@ const SnippetPanelOnLeft = ({ onAdd={() => onOpenEditor()} onEdit={onOpenEditor} onRemove={onRemoveField} - onPrimarySortChange={onPrimarySortChange} - onSecondarySortChange={onSecondarySortChange} + onSortChange={onSortChange} /> )}
@@ -70,8 +67,7 @@ const SnippetWorkflowPanel = ({ onCloseEditor, onSubmitField, onRemoveField, - onPrimarySortChange, - onSecondarySortChange, + onSortChange, }: SnippetWorkflowPanelProps) => { const versionHistoryPanelProps = useMemo(() => { return { @@ -98,8 +94,7 @@ const SnippetWorkflowPanel = ({ onCloseEditor={onCloseEditor} onSubmitField={onSubmitField} onRemoveField={onRemoveField} - onPrimarySortChange={onPrimarySortChange} - onSecondarySortChange={onSecondarySortChange} + onSortChange={onSortChange} /> ), }, @@ -113,10 +108,10 @@ const SnippetWorkflowPanel = ({ onCloseEditor, onCloseInputPanel, onOpenEditor, - onPrimarySortChange, onRemoveField, - onSecondarySortChange, + onSortChange, onSubmitField, + snippetId, versionHistoryPanelProps, ]) diff --git a/web/app/components/snippets/hooks/__tests__/use-snippet-init.spec.ts b/web/app/components/snippets/hooks/__tests__/use-snippet-init.spec.ts index e53f1c13fe..beb019b6a4 100644 --- a/web/app/components/snippets/hooks/__tests__/use-snippet-init.spec.ts +++ b/web/app/components/snippets/hooks/__tests__/use-snippet-init.spec.ts @@ -84,6 +84,68 @@ describe('useSnippetInit', () => { expect(result.current.isLoading).toBe(false) }) + it('should use draft input_fields for snippet inputs', () => { + mockUseSnippetApiDetail.mockReturnValue({ + data: { + id: 'snippet-1', + name: 'Tone Rewriter', + description: 'A static snippet mock.', + type: 'node', + is_published: false, + version: '1', + use_count: 0, + icon_info: { + icon_type: null, + icon: '🪄', + icon_background: '#E0EAFF', + }, + input_fields: [ + { + label: 'Published field', + variable: 'published_field', + type: 'text-input', + required: true, + }, + ], + created_at: 1_712_300_000, + updated_at: 1_712_300_000, + author: 'Evan', + }, + error: null, + isLoading: false, + }) + mockUseSnippetDraftWorkflow.mockReturnValue({ + data: { + id: 'draft-1', + graph: {}, + features: {}, + input_fields: [ + { + label: 'Draft field', + variable: 'draft_field', + type: 'text-input', + required: true, + }, + ], + hash: 'draft-hash', + created_at: 1_712_300_000, + updated_at: 1_712_345_678, + }, + isLoading: false, + }) + + const { result } = renderHook(() => useSnippetInit('snippet-1')) + + expect(result.current.data?.inputFields).toEqual([ + { + label: 'Draft field', + variable: 'draft_field', + type: 'text-input', + required: true, + }, + ]) + }) + it('should sync draft metadata into workflow store', () => { mockUseSnippetDraftWorkflow.mockImplementation((_snippetId: string, onSuccess?: (data: { updated_at: number, hash: string }) => void) => { onSuccess?.({ 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 e216876979..169f1e6e9a 100644 --- a/web/app/components/snippets/hooks/use-nodes-sync-draft.ts +++ b/web/app/components/snippets/hooks/use-nodes-sync-draft.ts @@ -1,4 +1,6 @@ import type { SyncDraftCallback } from '@/app/components/workflow/hooks-store' +import type { SnippetInputField } from '@/models/snippet' +import type { SnippetDraftSyncPayload, SnippetWorkflow } from '@/types/snippet' import { produce } from 'immer' import { useCallback } from 'react' import { useStoreApi } from 'reactflow' @@ -18,13 +20,17 @@ const isSyncConflictError = (error: unknown): error is { bodyUsed: boolean, json && typeof error.json === 'function' } +type SyncInputFieldsDraftCallback = SyncDraftCallback & { + onRefresh?: (inputFields: SnippetInputField[]) => void +} + export const useNodesSyncDraft = (snippetId: string) => { const store = useStoreApi() const workflowStore = useWorkflowStore() const { getNodesReadOnly } = useNodesReadOnly() const { handleRefreshWorkflowDraft } = useSnippetRefreshDraft(snippetId) - const getPostParams = useCallback(() => { + const getGraphSyncPayload = useCallback(() => { const { getNodes, edges, @@ -32,8 +38,6 @@ export const useNodesSyncDraft = (snippetId: string) => { } = store.getState() const nodes = getNodes().filter(node => !node.data?._isTempNode) const [x, y, zoom] = transform - const { syncWorkflowDraftHash } = workflowStore.getState() - if (!snippetId) return null @@ -55,47 +59,39 @@ export const useNodesSyncDraft = (snippetId: string) => { }) return { - url: `/snippets/${snippetId}/workflows/draft`, - params: { - graph: { - nodes: producedNodes, - edges: producedEdges, - viewport: { x, y, zoom }, - }, - hash: syncWorkflowDraftHash, + graph: { + nodes: producedNodes, + edges: producedEdges, + viewport: { x, y, zoom }, }, } - }, [snippetId, store, workflowStore]) + }, [snippetId, store]) - const syncWorkflowDraftWhenPageClose = useCallback(() => { - if (getNodesReadOnly()) - return - - const postParams = getPostParams() - if (postParams) - postWithKeepalive(`${API_PREFIX}${postParams.url}`, postParams.params) - }, [getNodesReadOnly, getPostParams]) - - const performSync = useCallback(async ( + const syncDraft = useCallback(async ( + payload: Omit, notRefreshWhenSyncError?: boolean, callback?: SyncDraftCallback, + onRefresh?: (draftWorkflow: SnippetWorkflow) => void, ) => { if (getNodesReadOnly()) return - const postParams = getPostParams() - if (!postParams) + if (!snippetId) return const { setDraftUpdatedAt, setSyncWorkflowDraftHash, + syncWorkflowDraftHash, } = workflowStore.getState() try { const response = await consoleClient.snippets.syncDraftWorkflow({ params: { snippetId }, - body: postParams.params, + body: { + ...payload, + hash: syncWorkflowDraftHash || undefined, + }, }) setSyncWorkflowDraftHash(response.hash) @@ -106,7 +102,7 @@ export const useNodesSyncDraft = (snippetId: string) => { if (isSyncConflictError(error) && !error.bodyUsed) { error.json().then((err) => { if (err.code === 'draft_workflow_not_sync' && !notRefreshWhenSyncError) - handleRefreshWorkflowDraft() + handleRefreshWorkflowDraft(onRefresh) }) } callback?.onError?.() @@ -114,12 +110,57 @@ export const useNodesSyncDraft = (snippetId: string) => { finally { callback?.onSettled?.() } - }, [getNodesReadOnly, getPostParams, handleRefreshWorkflowDraft, snippetId, workflowStore]) + }, [getNodesReadOnly, handleRefreshWorkflowDraft, snippetId, workflowStore]) + + const syncWorkflowDraftWhenPageClose = useCallback(() => { + if (getNodesReadOnly()) + return + + const graphPayload = getGraphSyncPayload() + if (!graphPayload) + return + + const { syncWorkflowDraftHash } = workflowStore.getState() + postWithKeepalive(`${API_PREFIX}/snippets/${snippetId}/workflows/draft`, { + ...graphPayload, + hash: syncWorkflowDraftHash, + }) + }, [getGraphSyncPayload, getNodesReadOnly, snippetId, workflowStore]) + + const performSync = useCallback(async ( + notRefreshWhenSyncError?: boolean, + callback?: SyncDraftCallback, + ) => { + const graphPayload = getGraphSyncPayload() + if (!graphPayload) + return + + await syncDraft(graphPayload, notRefreshWhenSyncError, callback) + }, [getGraphSyncPayload, syncDraft]) + + const performInputFieldsSync = useCallback(async ( + inputFields: SnippetInputField[], + callback?: SyncInputFieldsDraftCallback, + ) => { + await syncDraft( + { input_fields: inputFields }, + false, + callback, + (draftWorkflow) => { + const refreshedInputFields = Array.isArray(draftWorkflow.input_fields) + ? draftWorkflow.input_fields as SnippetInputField[] + : [] + callback?.onRefresh?.(refreshedInputFields) + }, + ) + }, [syncDraft]) const doSyncWorkflowDraft = useSerialAsyncCallback(performSync, getNodesReadOnly) + const syncInputFieldsDraft = useSerialAsyncCallback(performInputFieldsSync) return { doSyncWorkflowDraft, + syncInputFieldsDraft, syncWorkflowDraftWhenPageClose, } } diff --git a/web/app/components/snippets/hooks/use-snippet-refresh-draft.ts b/web/app/components/snippets/hooks/use-snippet-refresh-draft.ts index d37a338a37..270db76a00 100644 --- a/web/app/components/snippets/hooks/use-snippet-refresh-draft.ts +++ b/web/app/components/snippets/hooks/use-snippet-refresh-draft.ts @@ -1,4 +1,5 @@ import type { WorkflowDataUpdater } from '@/app/components/workflow/types' +import type { SnippetWorkflow } from '@/types/snippet' import { useCallback } from 'react' import { useWorkflowUpdate } from '@/app/components/workflow/hooks' import { useWorkflowStore } from '@/app/components/workflow/store' @@ -8,7 +9,7 @@ export const useSnippetRefreshDraft = (snippetId: string) => { const workflowStore = useWorkflowStore() const { handleUpdateWorkflowCanvas } = useWorkflowUpdate() - const handleRefreshWorkflowDraft = useCallback(() => { + const handleRefreshWorkflowDraft = useCallback((onSuccess?: (draftWorkflow: SnippetWorkflow) => void) => { const { setDraftUpdatedAt, setIsSyncingWorkflowDraft, @@ -30,6 +31,7 @@ export const useSnippetRefreshDraft = (snippetId: string) => { } as WorkflowDataUpdater) setSyncWorkflowDraftHash(response.hash) setDraftUpdatedAt(response.updated_at) + onSuccess?.(response) }).finally(() => { setIsSyncingWorkflowDraft(false) }) diff --git a/web/service/use-snippets.ts b/web/service/use-snippets.ts index 6b5ddc780f..12df39b830 100644 --- a/web/service/use-snippets.ts +++ b/web/service/use-snippets.ts @@ -102,8 +102,8 @@ const toSnippetCanvasData = (workflow?: SnippetWorkflow): SnippetCanvasData => { } export const buildSnippetDetailPayload = (snippet: SnippetContract, workflow?: SnippetWorkflow): SnippetDetailPayload => { - const inputFields = Array.isArray(snippet.input_fields) - ? snippet.input_fields as SnippetInputFieldUIModel[] + const inputFields = Array.isArray(workflow?.input_fields) + ? workflow.input_fields as SnippetInputFieldUIModel[] : [] return { diff --git a/web/types/snippet.ts b/web/types/snippet.ts index f962b20ab5..f0651c6ebf 100644 --- a/web/types/snippet.ts +++ b/web/types/snippet.ts @@ -77,6 +77,7 @@ export type SnippetWorkflow = { id: string graph: Record features: Record + input_fields?: SnippetInputField[] hash: string created_at: number updated_at: number @@ -87,7 +88,7 @@ export type SnippetDraftSyncPayload = { hash?: string environment_variables?: Record[] conversation_variables?: Record[] - input_variables?: Record[] + input_fields?: SnippetInputField[] } export type SnippetDraftSyncResponse = {