diff --git a/web/app/components/snippets/components/snippet-header/__tests__/index.spec.tsx b/web/app/components/snippets/components/snippet-header/__tests__/index.spec.tsx
index 57216574e4..65975726ad 100644
--- a/web/app/components/snippets/components/snippet-header/__tests__/index.spec.tsx
+++ b/web/app/components/snippets/components/snippet-header/__tests__/index.spec.tsx
@@ -1,4 +1,5 @@
import type { HeaderProps } from '@/app/components/workflow/header'
+import type { SnippetDetailUIModel } from '@/models/snippet'
import { fireEvent, render, screen } from '@testing-library/react'
import SnippetHeader from '..'
@@ -23,7 +24,13 @@ vi.mock('@/app/components/workflow/header', () => ({
describe('SnippetHeader', () => {
const mockToggleInputPanel = vi.fn()
- const mockTogglePublishMenu = vi.fn()
+ const mockPublishMenuOpenChange = vi.fn()
+ const mockPublish = vi.fn()
+ const uiMeta: SnippetDetailUIModel = {
+ inputFieldCount: 1,
+ checklistCount: 2,
+ autoSavedAt: 'Auto-saved · a few seconds ago',
+ }
beforeEach(() => {
vi.clearAllMocks()
@@ -36,8 +43,12 @@ describe('SnippetHeader', () => {
,
)
@@ -53,13 +64,17 @@ describe('SnippetHeader', () => {
// Verifies forwarded callbacks still drive the snippet-specific controls.
describe('User Interactions', () => {
- it('should invoke the snippet callbacks when input and publish buttons are clicked', () => {
+ it('should invoke the snippet callbacks when input and publish trigger are clicked', () => {
render(
,
)
@@ -67,7 +82,8 @@ describe('SnippetHeader', () => {
fireEvent.click(screen.getByRole('button', { name: /snippet\.publishButton/i }))
expect(mockToggleInputPanel).toHaveBeenCalledTimes(1)
- expect(mockTogglePublishMenu).toHaveBeenCalledTimes(1)
+ expect(mockPublishMenuOpenChange).toHaveBeenCalledTimes(1)
+ expect(mockPublishMenuOpenChange.mock.calls[0][0]).toBe(true)
})
})
})
diff --git a/web/app/components/snippets/components/snippet-header/index.tsx b/web/app/components/snippets/components/snippet-header/index.tsx
index d25d9a5d17..d19a8e6967 100644
--- a/web/app/components/snippets/components/snippet-header/index.tsx
+++ b/web/app/components/snippets/components/snippet-header/index.tsx
@@ -1,6 +1,7 @@
'use client'
import type { HeaderProps } from '@/app/components/workflow/header'
+import type { SnippetDetailUIModel } from '@/models/snippet'
import {
memo,
useMemo,
@@ -13,15 +14,23 @@ import RunMode from './run-mode'
type SnippetHeaderProps = {
snippetId: string
inputFieldCount: number
+ uiMeta: SnippetDetailUIModel
+ isPublishMenuOpen: boolean
+ isPublishing: boolean
onToggleInputPanel: () => void
- onTogglePublishMenu: () => void
+ onPublishMenuOpenChange: (open: boolean) => void
+ onPublish: () => void
}
const SnippetHeader = ({
snippetId,
inputFieldCount,
+ uiMeta,
+ isPublishMenuOpen,
+ isPublishing,
onToggleInputPanel,
- onTogglePublishMenu,
+ onPublishMenuOpenChange,
+ onPublish,
}: SnippetHeaderProps) => {
const viewHistoryProps = useMemo(() => {
return {
@@ -34,7 +43,15 @@ const SnippetHeader = ({
normal: {
components: {
left:
,
- middle:
,
+ middle: (
+
+ ),
},
controls: {
showEnvButton: false,
@@ -52,7 +69,7 @@ const SnippetHeader = ({
viewHistoryProps,
},
}
- }, [inputFieldCount, onToggleInputPanel, onTogglePublishMenu, viewHistoryProps])
+ }, [inputFieldCount, isPublishMenuOpen, isPublishing, onPublish, onPublishMenuOpenChange, onToggleInputPanel, uiMeta, viewHistoryProps])
return
}
diff --git a/web/app/components/snippets/components/snippet-header/publisher.tsx b/web/app/components/snippets/components/snippet-header/publisher.tsx
index 0d4cf02611..25b79485c6 100644
--- a/web/app/components/snippets/components/snippet-header/publisher.tsx
+++ b/web/app/components/snippets/components/snippet-header/publisher.tsx
@@ -1,26 +1,50 @@
'use client'
+import type { SnippetDetailUIModel } from '@/models/snippet'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuTrigger,
+} from '@/app/components/base/ui/dropdown-menu'
+import PublishMenu from '../publish-menu'
type PublisherProps = {
- onClick: () => void
+ uiMeta: SnippetDetailUIModel
+ open: boolean
+ isPublishing: boolean
+ onOpenChange: (open: boolean) => void
+ onPublish: () => void
}
const Publisher = ({
- onClick,
+ uiMeta,
+ open,
+ isPublishing,
+ onOpenChange,
+ onPublish,
}: PublisherProps) => {
const { t } = useTranslation('snippet')
return (
-
+
+
+ {t('publishButton')}
+
+
+
+
+
+
)
}
diff --git a/web/app/components/snippets/components/snippet-main.tsx b/web/app/components/snippets/components/snippet-main.tsx
index 795d644752..b57fed1342 100644
--- a/web/app/components/snippets/components/snippet-main.tsx
+++ b/web/app/components/snippets/components/snippet-main.tsx
@@ -10,6 +10,10 @@ import {
RiTerminalWindowLine,
} from '@remixicon/react'
import {
+ useKeyPress,
+} from 'ahooks'
+import {
+ useCallback,
useEffect,
useMemo,
useState,
@@ -25,7 +29,9 @@ import Evaluation from '@/app/components/evaluation'
import { WorkflowWithInnerContext } from '@/app/components/workflow'
import { useAvailableNodesMetaData } from '@/app/components/workflow-app/hooks'
import { BlockEnum } from '@/app/components/workflow/types'
+import { getKeyboardKeyCodeBySystem } from '@/app/components/workflow/utils'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
+import { usePublishSnippetWorkflowMutation } from '@/service/use-snippet-workflows'
import { useConfigsMap } from '../hooks/use-configs-map'
import { useNodesSyncDraft } from '../hooks/use-nodes-sync-draft'
import { useSnippetRefreshDraft } from '../hooks/use-snippet-refresh-draft'
@@ -61,6 +67,7 @@ const SnippetMain = ({
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const [fields, setFields] = useState
(payload.inputFields)
+ const publishSnippetMutation = usePublishSnippetWorkflowMutation(snippetId)
const {
doSyncWorkflowDraft,
syncInputFieldsDraft,
@@ -97,8 +104,8 @@ const SnippetMain = ({
openEditor,
reset,
setInputPanelOpen,
+ setPublishMenuOpen,
toggleInputPanel,
- togglePublishMenu,
} = useSnippetDetailStore(useShallow(state => ({
editingField: state.editingField,
isEditorOpen: state.isEditorOpen,
@@ -108,8 +115,8 @@ const SnippetMain = ({
openEditor: state.openEditor,
reset: state.reset,
setInputPanelOpen: state.setInputPanelOpen,
+ setPublishMenuOpen: state.setPublishMenuOpen,
toggleInputPanel: state.toggleInputPanel,
- togglePublishMenu: state.togglePublishMenu,
})))
useEffect(() => {
@@ -166,6 +173,27 @@ const SnippetMain = ({
setInputPanelOpen(false)
}
+ const handlePublish = useCallback(async () => {
+ try {
+ await publishSnippetMutation.mutateAsync({
+ params: { snippetId },
+ })
+ setPublishMenuOpen(false)
+ toast.success(t('publishSuccess'))
+ }
+ catch (error) {
+ toast.error(error instanceof Error ? error.message : t('publishFailed'))
+ }
+ }, [publishSnippetMutation, setPublishMenuOpen, snippetId, t])
+
+ useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (e) => {
+ if (section !== 'orchestrate' || publishSnippetMutation.isPending)
+ return
+
+ e.preventDefault()
+ void handlePublish()
+ }, { exactMatch: true, useCapture: true })
+
const hooksStore = useMemo(() => {
return {
doSyncWorkflowDraft,
@@ -222,9 +250,11 @@ const SnippetMain = ({
isEditorOpen={isEditorOpen}
isInputPanelOpen={isInputPanelOpen}
isPublishMenuOpen={isPublishMenuOpen}
+ isPublishing={publishSnippetMutation.isPending}
onToggleInputPanel={handleToggleInputPanel}
- onTogglePublishMenu={togglePublishMenu}
+ onPublishMenuOpenChange={setPublishMenuOpen}
onCloseInputPanel={handleCloseInputPanel}
+ onPublish={handlePublish}
onOpenEditor={openEditor}
onCloseEditor={closeEditor}
onSubmitField={handleSubmitField}
diff --git a/web/i18n/en-US/snippet.json b/web/i18n/en-US/snippet.json
index 3bc86cfe43..fda6980f69 100644
--- a/web/i18n/en-US/snippet.json
+++ b/web/i18n/en-US/snippet.json
@@ -24,7 +24,9 @@
"panelSecondaryGroup": "Optional inputs",
"panelTitle": "Input Field",
"publishButton": "Publish",
+ "publishFailed": "Failed to publish snippet",
"publishMenuCurrentDraft": "Current draft unpublished",
+ "publishSuccess": "Snippet published",
"save": "Save",
"sectionEvaluation": "Evaluation",
"sectionOrchestrate": "Orchestrate",
diff --git a/web/i18n/zh-Hans/snippet.json b/web/i18n/zh-Hans/snippet.json
index bb4514d622..b8ed686559 100644
--- a/web/i18n/zh-Hans/snippet.json
+++ b/web/i18n/zh-Hans/snippet.json
@@ -24,7 +24,9 @@
"panelSecondaryGroup": "可选输入",
"panelTitle": "输入字段",
"publishButton": "发布",
+ "publishFailed": "发布 Snippet 失败",
"publishMenuCurrentDraft": "当前草稿未发布",
+ "publishSuccess": "Snippet 已发布",
"save": "保存",
"sectionEvaluation": "评测",
"sectionOrchestrate": "编排",