workflow tool drawer
+
@@ -818,6 +841,74 @@ describe('AppPublisher', () => {
expect(screen.getByRole('dialog', { name: 'Workflow tool drawer' })).toBeInTheDocument()
})
+ it('should show one success toast when automatically publishing a workflow tool', async () => {
+ mockAppDetail = {
+ ...mockAppDetail,
+ mode: AppModeEnum.WORKFLOW,
+ }
+ mockOnPublish.mockImplementation(async (_params, options?: { showSuccessToast?: boolean }) => {
+ if (options?.showSuccessToast !== false) mockToastSuccess('common.api.actionSuccess')
+ })
+ mockCreateWorkflowToolProvider.mockResolvedValue({})
+
+ render(
)
+
+ fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
+ fireEvent.click(screen.getByText('publisher-workflow-tool'))
+ fireEvent.click(screen.getByRole('button', { name: 'create-workflow-tool' }))
+
+ await waitFor(() => {
+ expect(mockCreateWorkflowToolProvider).toHaveBeenCalledOnce()
+ })
+ expect(mockOnPublish).toHaveBeenCalledWith(undefined, { showSuccessToast: false })
+ expect(mockToastSuccess).toHaveBeenCalledOnce()
+ })
+
+ it('should not show a success toast when workflow tool creation fails after publishing', async () => {
+ mockAppDetail = {
+ ...mockAppDetail,
+ mode: AppModeEnum.WORKFLOW,
+ }
+ mockOnPublish.mockImplementation(async (_params, options?: { showSuccessToast?: boolean }) => {
+ if (options?.showSuccessToast !== false) mockToastSuccess('common.api.actionSuccess')
+ })
+ mockCreateWorkflowToolProvider.mockRejectedValue(new Error('create failed'))
+
+ render(
)
+
+ fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
+ fireEvent.click(screen.getByText('publisher-workflow-tool'))
+ fireEvent.click(screen.getByRole('button', { name: 'create-workflow-tool' }))
+
+ await waitFor(() => {
+ expect(mockToastError).toHaveBeenCalledWith('create failed')
+ })
+ expect(mockOnPublish).toHaveBeenCalledWith(undefined, { showSuccessToast: false })
+ expect(mockToastSuccess).not.toHaveBeenCalled()
+ })
+
+ it('should not create a workflow tool when automatic publishing fails', async () => {
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
+ mockAppDetail = {
+ ...mockAppDetail,
+ mode: AppModeEnum.WORKFLOW,
+ }
+ mockOnPublish.mockRejectedValueOnce(new Error('publish failed'))
+
+ render(
)
+
+ fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
+ fireEvent.click(screen.getByText('publisher-workflow-tool'))
+ fireEvent.click(screen.getByRole('button', { name: 'create-workflow-tool' }))
+
+ await waitFor(() => {
+ expect(mockOnPublish).toHaveBeenCalledOnce()
+ })
+ expect(mockCreateWorkflowToolProvider).not.toHaveBeenCalled()
+ expect(mockToastError).toHaveBeenCalledWith('publish failed')
+ consoleWarnSpy.mockRestore()
+ })
+
it('should not open workflow tool drawer without tool.manage', () => {
mockWorkspacePermissionKeys = []
mockAppDetail = {
diff --git a/web/app/components/app/app-publisher/features-wrapper.tsx b/web/app/components/app/app-publisher/features-wrapper.tsx
index 4114aadb50a..ffb85da6b44 100644
--- a/web/app/components/app/app-publisher/features-wrapper.tsx
+++ b/web/app/components/app/app-publisher/features-wrapper.tsx
@@ -1,5 +1,6 @@
import type {
AppPublisherProps,
+ AppPublisherPublishOptions,
AppPublisherPublishParams,
} from '@/app/components/app/app-publisher/types'
import type { ConfigurationPublishConfig } from '@/app/components/app/configuration/hooks/configuration-lifecycle/types'
@@ -26,6 +27,7 @@ type Props = Omit
& {
onPublish?: (
params?: AppPublisherPublishParams,
features?: Features,
+ options?: AppPublisherPublishOptions,
) => Promise | unknown
publishedConfig: ConfigurationPublishConfig
resetAppConfig?: () => void
@@ -88,7 +90,8 @@ const FeaturesWrappedAppPublisher = (props: Props) => {
}
const handlePublish = useCallback(
- (params?: AppPublisherPublishParams) => {
+ (params?: AppPublisherPublishParams, options?: AppPublisherPublishOptions) => {
+ if (options) return props.onPublish?.(params, features, options)
return props.onPublish?.(params, features)
},
[features, props],
diff --git a/web/app/components/app/app-publisher/publisher-content/index.tsx b/web/app/components/app/app-publisher/publisher-content/index.tsx
index 945e879331b..c5b7e489db1 100644
--- a/web/app/components/app/app-publisher/publisher-content/index.tsx
+++ b/web/app/components/app/app-publisher/publisher-content/index.tsx
@@ -130,7 +130,7 @@ export function PublisherContent({
hasTriggerNode,
inputs,
onClosePublisher: closePublisher,
- onPublish: publish.handlePublish,
+ onPublish: publish.publishWorkflowTool,
onRefreshData,
outputs,
toolPublished,
diff --git a/web/app/components/app/app-publisher/publisher-content/use-publish-controller.ts b/web/app/components/app/app-publisher/publisher-content/use-publish-controller.ts
index 0fc2f7acb3d..6737b74ee17 100644
--- a/web/app/components/app/app-publisher/publisher-content/use-publish-controller.ts
+++ b/web/app/components/app/app-publisher/publisher-content/use-publish-controller.ts
@@ -1,5 +1,9 @@
import type { QueryClient } from '@tanstack/react-query'
-import type { AppPublisherProps, AppPublisherPublishParams } from '../types'
+import type {
+ AppPublisherProps,
+ AppPublisherPublishOptions,
+ AppPublisherPublishParams,
+} from '../types'
import type { CollaborationUpdate } from '@/app/components/workflow/collaboration/types/collaboration'
import { useHotkey } from '@tanstack/react-hotkeys'
import { useQueryClient } from '@tanstack/react-query'
@@ -81,43 +85,54 @@ export function usePublishController({
: publishedAt
const hasPublishedVersion = Boolean(currentPublishedAt)
+ async function publishApp(
+ params?: AppPublisherPublishParams,
+ options?: AppPublisherPublishOptions,
+ ) {
+ await onPublish?.(params, options)
+ setPublished(true)
+
+ const socket = appId ? webSocketClient.getSocket(appId) : null
+ if (appId) {
+ invalidateAppWorkflow(appId)
+ if (supportsMultiEnvironment) refreshAppDeploymentData(queryClient, appId)
+ } else {
+ console.warn('[app-publisher] missing appId, skip workflow invalidate and socket emit')
+ }
+ if (socket) {
+ const timestamp = Date.now()
+ socket.emit('collaboration_event', {
+ type: 'app_publish_update',
+ data: {
+ action: 'published',
+ timestamp,
+ },
+ timestamp,
+ })
+ } else if (appId) {
+ console.warn('[app-publisher] socket not ready, skip collaboration_event emit', { appId })
+ }
+
+ trackEvent('app_published_time', {
+ action_mode: 'app',
+ app_id: appId,
+ app_name: appName,
+ })
+ }
+
async function handlePublish(params?: AppPublisherPublishParams) {
try {
- await onPublish?.(params)
- setPublished(true)
-
- const socket = appId ? webSocketClient.getSocket(appId) : null
- if (appId) {
- invalidateAppWorkflow(appId)
- if (supportsMultiEnvironment) refreshAppDeploymentData(queryClient, appId)
- } else {
- console.warn('[app-publisher] missing appId, skip workflow invalidate and socket emit')
- }
- if (socket) {
- const timestamp = Date.now()
- socket.emit('collaboration_event', {
- type: 'app_publish_update',
- data: {
- action: 'published',
- timestamp,
- },
- timestamp,
- })
- } else if (appId) {
- console.warn('[app-publisher] socket not ready, skip collaboration_event emit', { appId })
- }
-
- trackEvent('app_published_time', {
- action_mode: 'app',
- app_id: appId,
- app_name: appName,
- })
+ await publishApp(params)
} catch (error) {
console.warn('[app-publisher] publish failed', error)
setPublished(false)
}
}
+ async function publishWorkflowTool(params?: AppPublisherPublishParams) {
+ await publishApp(params, { showSuccessToast: false })
+ }
+
async function handleRestore() {
try {
await onRestore?.()
@@ -164,6 +179,7 @@ export function usePublishController({
isWorkflowApp,
published,
publishedWorkflow,
+ publishWorkflowTool,
resetPublished: () => setPublished(false),
}
}
diff --git a/web/app/components/app/app-publisher/types.ts b/web/app/components/app/app-publisher/types.ts
index 8c8e9917cde..903d45097f0 100644
--- a/web/app/components/app/app-publisher/types.ts
+++ b/web/app/components/app/app-publisher/types.ts
@@ -4,9 +4,16 @@ import type { PublishWorkflowParams } from '@/types/workflow'
export type AppPublisherPublishParams = ModelAndParameter | PublishWorkflowParams
+export type AppPublisherPublishOptions = {
+ showSuccessToast?: boolean
+}
+
type AppPublisherPublishHandler =
- | ((params?: AppPublisherPublishParams) => Promise | unknown)
- | ((params?: unknown) => Promise | unknown)
+ | ((
+ params?: AppPublisherPublishParams,
+ options?: AppPublisherPublishOptions,
+ ) => Promise | unknown)
+ | ((params?: unknown, options?: AppPublisherPublishOptions) => Promise | unknown)
type AppPublisherRestoreHandler = () => Promise | unknown
diff --git a/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts b/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts
index 0f8720f10be..702fd85d77f 100644
--- a/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts
+++ b/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts
@@ -370,6 +370,46 @@ describe('useConfigureButton', () => {
// Mutation handlers
describe('handleCreate', () => {
+ it('should publish before creating the provider', async () => {
+ mockCreateWorkflowToolProvider.mockResolvedValue({})
+ const handlePublish = vi.fn().mockResolvedValue(undefined)
+ const { result } = renderHook(() =>
+ useConfigureButton(createDefaultOptions({ handlePublish })),
+ )
+
+ await act(async () => {
+ await result.current.handleCreate(
+ createMockRequest({ workflow_app_id: 'app-123' }) as WorkflowToolProviderRequest & {
+ workflow_app_id: string
+ },
+ )
+ })
+
+ expect(handlePublish).toHaveBeenCalledOnce()
+ expect(mockCreateWorkflowToolProvider).toHaveBeenCalledOnce()
+ expect(handlePublish.mock.invocationCallOrder[0]).toBeLessThan(
+ mockCreateWorkflowToolProvider.mock.invocationCallOrder[0]!,
+ )
+ })
+
+ it('should not create the provider when publishing fails', async () => {
+ const handlePublish = vi.fn().mockRejectedValue(new Error('Publish failed'))
+ const { result } = renderHook(() =>
+ useConfigureButton(createDefaultOptions({ handlePublish })),
+ )
+
+ await act(async () => {
+ await result.current.handleCreate(
+ createMockRequest({ workflow_app_id: 'app-123' }) as WorkflowToolProviderRequest & {
+ workflow_app_id: string
+ },
+ )
+ })
+
+ expect(mockCreateWorkflowToolProvider).not.toHaveBeenCalled()
+ expect(mockToastNotify).toHaveBeenCalledWith({ type: 'error', message: 'Publish failed' })
+ })
+
it('should create provider, invalidate caches, refresh, and notify configured', async () => {
mockCreateWorkflowToolProvider.mockResolvedValue({})
const onRefreshData = vi.fn()
@@ -439,6 +479,7 @@ describe('useConfigureButton', () => {
expect(onRefreshData).toHaveBeenCalled()
expect(mockInvalidateAllWorkflowTools).toHaveBeenCalled()
expect(mockInvalidateWorkflowToolDetailByAppID).toHaveBeenCalledWith('app-123')
+ expect(mockToastNotify).toHaveBeenCalledWith({ type: 'success', message: expect.any(String) })
expect(onConfigured).toHaveBeenCalled()
})
diff --git a/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts b/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts
index 6c8e982c624..a0d15c68ac9 100644
--- a/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts
+++ b/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts
@@ -121,7 +121,6 @@ export function useConfigureButton(options: UseConfigureButtonOptions) {
onRefreshData,
onConfigured,
} = options
-
const { t } = useTranslation()
// Data fetching via React Query
@@ -180,6 +179,7 @@ export function useConfigureButton(options: UseConfigureButtonOptions) {
// Mutation handlers (not memoized — only used in conditionally-rendered modal)
const handleCreate = async (data: WorkflowToolProviderRequest & { workflow_app_id: string }) => {
try {
+ await handlePublish()
await createWorkflowToolProvider(data)
invalidateAllWorkflowTools()
onRefreshData?.()
@@ -204,6 +204,7 @@ export function useConfigureButton(options: UseConfigureButtonOptions) {
onRefreshData?.()
invalidateAllWorkflowTools()
invalidateDetail(workflowAppId)
+ toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' }))
onConfigured?.()
} catch (e) {
toast.error((e as Error).message)
diff --git a/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx b/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx
index cafae7b2526..ea6550b541c 100644
--- a/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx
+++ b/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx
@@ -182,6 +182,23 @@ vi.mock('@/app/components/app/app-publisher', () => ({
>
publisher-publish
+