From 3a7f09a2504b5f5378477a1b009a612524e2e5fa Mon Sep 17 00:00:00 2001 From: JzoNg Date: Tue, 28 Apr 2026 15:42:31 +0800 Subject: [PATCH] fix(web): snippet publish check --- .../__tests__/snippet-main.spec.tsx | 8 ++ .../__tests__/use-snippet-publish.spec.ts | 32 +++++++- .../components/hooks/use-snippet-publish.ts | 8 +- .../snippets/components/snippet-main.tsx | 79 +++++++++++++++---- 4 files changed, 111 insertions(+), 16 deletions(-) diff --git a/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx b/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx index 05ed901d0b..b9a9ec067f 100644 --- a/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx +++ b/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx @@ -23,6 +23,7 @@ const mockHandleRun = vi.fn() const mockHandleStartWorkflowRun = vi.fn() const mockHandleStopRun = vi.fn() const mockHandleWorkflowStartRunInWorkflow = vi.fn() +const mockHandleCheckBeforePublish = vi.fn() const mockInspectVarsCrud = { hasNodeInspectVars: vi.fn(), hasSetInspectVar: vi.fn(), @@ -81,6 +82,12 @@ vi.mock('@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars', () => }), })) +vi.mock('@/app/components/workflow/hooks/use-checklist', () => ({ + useChecklistBeforePublish: () => ({ + handleCheckBeforePublish: mockHandleCheckBeforePublish, + }), +})) + vi.mock('@/app/components/snippets/hooks/use-inspect-vars-crud', () => ({ useInspectVarsCrud: () => mockInspectVarsCrud, })) @@ -208,6 +215,7 @@ describe('SnippetMain', () => { vi.clearAllMocks() mockSyncInputFieldsDraft.mockResolvedValue(undefined) mockPublishSnippetMutateAsync.mockResolvedValue({ created_at: 1_744_000_000 }) + mockHandleCheckBeforePublish.mockResolvedValue(true) capturedHooksStore = undefined snippetDetailStoreState = { editingField: null, diff --git a/web/app/components/snippets/components/hooks/__tests__/use-snippet-publish.spec.ts b/web/app/components/snippets/components/hooks/__tests__/use-snippet-publish.spec.ts index 9f13053d2d..82f33f2e44 100644 --- a/web/app/components/snippets/components/hooks/__tests__/use-snippet-publish.spec.ts +++ b/web/app/components/snippets/components/hooks/__tests__/use-snippet-publish.spec.ts @@ -7,6 +7,7 @@ const mockSetPublishMenuOpen = vi.fn() const mockUseKeyPress = vi.fn() const mockSetPublishedAt = vi.fn() const mockSetQueryData = vi.fn() +const mockHandleCheckBeforePublish = vi.fn<() => Promise>() let isPublishMenuOpen = false let isPending = false @@ -44,6 +45,12 @@ vi.mock('@/app/components/workflow/store', () => ({ }), })) +vi.mock('@/app/components/workflow/hooks/use-checklist', () => ({ + useChecklistBeforePublish: () => ({ + handleCheckBeforePublish: mockHandleCheckBeforePublish, + }), +})) + vi.mock('../../../store', () => ({ useSnippetDetailStore: (selector: (state: { isPublishMenuOpen: boolean @@ -60,6 +67,7 @@ describe('useSnippetPublish', () => { isPublishMenuOpen = false isPending = false shortcutHandler = undefined + mockHandleCheckBeforePublish.mockResolvedValue(true) mockMutateAsync.mockResolvedValue({ created_at: 1_712_345_678 }) mockUseKeyPress.mockImplementation((_key, handler) => { shortcutHandler = handler @@ -76,17 +84,39 @@ describe('useSnippetPublish', () => { await result.current.handlePublish() }) + expect(mockHandleCheckBeforePublish).toHaveBeenCalledTimes(1) expect(mockMutateAsync).toHaveBeenCalledWith({ params: { snippetId: 'snippet-1' }, }) expect(mockSetQueryData).toHaveBeenCalledTimes(1) - const updateSnippetDetail = mockSetQueryData.mock.calls[0][1] as (old: { is_published: boolean }) => { is_published: boolean } + const setQueryDataCall = mockSetQueryData.mock.calls[0] + expect(setQueryDataCall).toBeDefined() + const updateSnippetDetail = setQueryDataCall![1] as (old: { is_published: boolean }) => { is_published: boolean } expect(updateSnippetDetail({ is_published: false })).toEqual({ is_published: true }) expect(mockSetPublishedAt).toHaveBeenCalledWith(1_712_345_678) expect(mockSetPublishMenuOpen).toHaveBeenCalledWith(false) expect(toast.success).toHaveBeenCalledWith('snippet.publishSuccess') }) + it('should not publish the snippet when checklist validation fails', async () => { + mockHandleCheckBeforePublish.mockResolvedValue(false) + + const { result } = renderHook(() => useSnippetPublish({ + snippetId: 'snippet-1', + })) + + await act(async () => { + await result.current.handlePublish() + }) + + expect(mockHandleCheckBeforePublish).toHaveBeenCalledTimes(1) + expect(mockMutateAsync).not.toHaveBeenCalled() + expect(mockSetQueryData).not.toHaveBeenCalled() + expect(mockSetPublishedAt).not.toHaveBeenCalled() + expect(mockSetPublishMenuOpen).not.toHaveBeenCalled() + expect(toast.success).not.toHaveBeenCalled() + }) + it('should surface publish errors through toast feedback', async () => { mockMutateAsync.mockRejectedValue(new Error('publish failed')) diff --git a/web/app/components/snippets/components/hooks/use-snippet-publish.ts b/web/app/components/snippets/components/hooks/use-snippet-publish.ts index bb803ccbac..3133b9aaa2 100644 --- a/web/app/components/snippets/components/hooks/use-snippet-publish.ts +++ b/web/app/components/snippets/components/hooks/use-snippet-publish.ts @@ -5,6 +5,7 @@ import { useKeyPress } from 'ahooks' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useShallow } from 'zustand/react/shallow' +import { useChecklistBeforePublish } from '@/app/components/workflow/hooks/use-checklist' import { useWorkflowStore } from '@/app/components/workflow/store' import { getKeyboardKeyCodeBySystem } from '@/app/components/workflow/utils' import { consoleQuery } from '@/service/client' @@ -22,6 +23,7 @@ export const useSnippetPublish = ({ const workflowStore = useWorkflowStore() const queryClient = useQueryClient() const publishSnippetMutation = usePublishSnippetWorkflowMutation(snippetId) + const { handleCheckBeforePublish } = useChecklistBeforePublish() const { isPublishMenuOpen, setPublishMenuOpen, @@ -32,6 +34,10 @@ export const useSnippetPublish = ({ const handlePublish = useCallback(async () => { try { + const canPublish = await handleCheckBeforePublish() + if (!canPublish) + return + const publishedWorkflow = await publishSnippetMutation.mutateAsync({ params: { snippetId }, }) @@ -50,7 +56,7 @@ export const useSnippetPublish = ({ catch (error) { toast.error(error instanceof Error ? error.message : t('publishFailed')) } - }, [publishSnippetMutation, queryClient, setPublishMenuOpen, snippetId, t, workflowStore]) + }, [handleCheckBeforePublish, publishSnippetMutation, queryClient, setPublishMenuOpen, snippetId, t, workflowStore]) useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (event) => { if (publishSnippetMutation.isPending) diff --git a/web/app/components/snippets/components/snippet-main.tsx b/web/app/components/snippets/components/snippet-main.tsx index 290b587420..324f523ede 100644 --- a/web/app/components/snippets/components/snippet-main.tsx +++ b/web/app/components/snippets/components/snippet-main.tsx @@ -2,7 +2,7 @@ import type { WorkflowProps } from '@/app/components/workflow' import type { Shape as HooksStoreShape } from '@/app/components/workflow/hooks-store' -import type { SnippetDetailPayload } from '@/models/snippet' +import type { SnippetDetailPayload, SnippetDetailUIModel, SnippetInputField } from '@/models/snippet' import { useEffect, useMemo, @@ -28,6 +28,69 @@ type SnippetMainProps = { snippetId: string } & Pick +type SnippetMainContentProps = { + snippetId: string + fields: SnippetInputField[] + uiMeta: SnippetDetailUIModel + editingField: SnippetInputField | null + isEditorOpen: boolean + isInputPanelOpen: boolean + onToggleInputPanel: () => void + onCloseInputPanel: () => void + onOpenEditor: (field?: SnippetInputField | null) => void + onCloseEditor: () => void + onSubmitField: (field: SnippetInputField) => void + onRemoveField: (index: number) => void + onSortChange: (fields: SnippetInputField[]) => void +} + +const SnippetMainContent = ({ + snippetId, + fields, + uiMeta, + editingField, + isEditorOpen, + isInputPanelOpen, + onToggleInputPanel, + onCloseInputPanel, + onOpenEditor, + onCloseEditor, + onSubmitField, + onRemoveField, + onSortChange, +}: SnippetMainContentProps) => { + const { + handlePublish, + isPublishMenuOpen, + isPublishing, + setPublishMenuOpen, + } = useSnippetPublish({ + snippetId, + }) + + return ( + + ) +} + const SnippetMain = ({ payload, snippetId, @@ -109,14 +172,6 @@ const SnippetMain = ({ } = useSnippetInputFieldActions({ snippetId, }) - const { - handlePublish, - isPublishMenuOpen, - isPublishing, - setPublishMenuOpen, - } = useSnippetPublish({ - snippetId, - }) const { handleStartWorkflowRun, handleWorkflowStartRunInWorkflow, @@ -200,19 +255,15 @@ const SnippetMain = ({ viewport={viewport ?? graph.viewport} hooksStore={hooksStore as unknown as Partial} > -