From bcd87ddc58eeb919f435046de6c24671ccd0c4b8 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Mon, 20 Apr 2026 16:24:53 +0800 Subject: [PATCH] fix: publish as evaluation --- .../app-info/app-info-detail-panel.tsx | 2 +- .../app-publisher/__tests__/index.spec.tsx | 47 ++++++++++++++--- .../components/app/app-publisher/index.tsx | 50 +++++++++++++++---- .../__tests__/features-trigger.spec.tsx | 21 ++++++++ .../workflow-header/features-trigger.tsx | 2 +- .../start-node-selection-panel.tsx | 2 +- .../hooks/use-available-nodes-meta-data.ts | 2 +- .../block-selector/all-start-blocks.tsx | 2 +- .../workflow/block-selector/blocks.tsx | 2 +- web/types/app.ts | 2 +- 10 files changed, 107 insertions(+), 25 deletions(-) diff --git a/web/app/components/app-sidebar/app-info/app-info-detail-panel.tsx b/web/app/components/app-sidebar/app-info/app-info-detail-panel.tsx index 67f8dd5d5a..b80ea5d522 100644 --- a/web/app/components/app-sidebar/app-info/app-info-detail-panel.tsx +++ b/web/app/components/app-sidebar/app-info/app-info-detail-panel.tsx @@ -126,7 +126,7 @@ const AppInfoDetailPanel = ({ secondaryOperations={secondaryOperations} /> - {appDetail.workflow_type !== AppTypeEnum.EVALUATION && ( + {appDetail.workflow_kind !== AppTypeEnum.EVALUATION && ( { it('should switch workflow type, refresh app detail, and close the popover for published apps', async () => { mockFetchAppDetailDirect.mockResolvedValueOnce({ id: 'app-1', - type: AppTypeEnum.EVALUATION, + workflow_kind: AppTypeEnum.EVALUATION, }) render( @@ -511,16 +511,49 @@ describe('AppPublisher', () => { expect(mockFetchAppDetailDirect).toHaveBeenCalledWith({ url: '/apps', id: 'app-1' }) expect(mockSetAppDetail).toHaveBeenCalledWith({ id: 'app-1', - type: AppTypeEnum.EVALUATION, + workflow_kind: AppTypeEnum.EVALUATION, + }) + }) + await waitFor(() => { + expect(screen.queryByText('publisher-summary-publish')).not.toBeInTheDocument() + }) + }) + + it('should publish an unpublished workflow as evaluation workflow through the evaluation publish endpoint', async () => { + mockOnPublish.mockResolvedValue(undefined) + mockFetchAppDetailDirect.mockResolvedValueOnce({ + id: 'app-1', + workflow_kind: AppTypeEnum.EVALUATION, + }) + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-switch-workflow-type')) + + await waitFor(() => { + expect(mockOnPublish).toHaveBeenCalledWith({ + url: '/apps/app-1/workflows/publish/evaluation', + title: '', + releaseNotes: '', + }) + expect(mockConvertWorkflowType).not.toHaveBeenCalled() + expect(mockFetchAppDetailDirect).toHaveBeenCalledWith({ url: '/apps', id: 'app-1' }) + expect(mockSetAppDetail).toHaveBeenCalledWith({ + id: 'app-1', + workflow_kind: AppTypeEnum.EVALUATION, }) }) - expect(screen.queryByText('publisher-summary-publish')).not.toBeInTheDocument() }) it('should hide access and actions sections for evaluation workflow apps', () => { mockAppDetail = { ...mockAppDetail, - type: AppTypeEnum.EVALUATION, + workflow_kind: AppTypeEnum.EVALUATION, } render( @@ -545,7 +578,7 @@ describe('AppPublisher', () => { it('should confirm before switching an evaluation workflow with associated targets to a standard workflow', async () => { mockAppDetail = { ...mockAppDetail, - type: AppTypeEnum.EVALUATION, + workflow_kind: AppTypeEnum.EVALUATION, } mockEvaluationWorkflowAssociatedTargets = { items: [ @@ -595,7 +628,7 @@ describe('AppPublisher', () => { it('should switch an evaluation workflow directly when there are no associated targets', async () => { mockAppDetail = { ...mockAppDetail, - type: AppTypeEnum.EVALUATION, + workflow_kind: AppTypeEnum.EVALUATION, } render( @@ -620,7 +653,7 @@ describe('AppPublisher', () => { it('should block switching an evaluation workflow when associated targets fail to load', async () => { mockAppDetail = { ...mockAppDetail, - type: AppTypeEnum.EVALUATION, + workflow_kind: AppTypeEnum.EVALUATION, } mockRefetchEvaluationWorkflowAssociatedTargets.mockResolvedValueOnce({ data: undefined, diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index aa0495edae..6ef09917e7 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -147,15 +147,15 @@ const AppPublisher = ({ const appURL = getPublisherAppUrl({ appBaseUrl: appBaseURL, accessToken, mode: appDetail?.mode }) const isChatApp = [AppModeEnum.CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.COMPLETION].includes(appDetail?.mode || AppModeEnum.CHAT) const workflowTypeSwitchConfig = useMemo(() => { - if (!appDetail?.workflow_type) + if (!appDetail?.workflow_kind) return WORKFLOW_TYPE_SWITCH_CONFIG.workflow - if (!isWorkflowTypeConversionTarget(appDetail?.workflow_type)) + if (!isWorkflowTypeConversionTarget(appDetail?.workflow_kind)) return undefined - return WORKFLOW_TYPE_SWITCH_CONFIG[appDetail.workflow_type] - }, [appDetail?.workflow_type]) - const isEvaluationWorkflowType = appDetail?.workflow_type === AppTypeEnum.EVALUATION + return WORKFLOW_TYPE_SWITCH_CONFIG[appDetail.workflow_kind] + }, [appDetail?.workflow_kind]) + const isEvaluationWorkflowType = appDetail?.workflow_kind === AppTypeEnum.EVALUATION const { refetch: refetchEvaluationWorkflowAssociatedTargets, isFetching: isFetchingEvaluationWorkflowAssociatedTargets, @@ -281,11 +281,42 @@ const AppPublisher = ({ } }, [appDetail, setAppDetail]) + const getWorkflowTypeSwitchPublishUrl = useCallback(() => { + if (!appDetail?.id || !workflowTypeSwitchConfig) + return undefined + + if (workflowTypeSwitchConfig.targetType === AppTypeEnum.EVALUATION) + return `/apps/${appDetail.id}/workflows/publish/evaluation` + + return `/apps/${appDetail.id}/workflows/publish` + }, [appDetail?.id, workflowTypeSwitchConfig]) + const performWorkflowTypeSwitch = useCallback(async () => { if (!appDetail?.id || !workflowTypeSwitchConfig) return false try { + if (!publishedAt) { + const publishUrl = getWorkflowTypeSwitchPublishUrl() + if (!publishUrl) + return false + + await handlePublish({ + url: publishUrl, + title: '', + releaseNotes: '', + }) + + const latestAppDetail = await fetchAppDetailDirect({ + url: '/apps', + id: appDetail.id, + }) + setAppDetail(latestAppDetail) + setShowEvaluationWorkflowSwitchConfirm(false) + setEvaluationWorkflowSwitchTargets([]) + return true + } + await convertWorkflowType({ params: { appId: appDetail.id, @@ -295,9 +326,6 @@ const AppPublisher = ({ }, }) - if (!publishedAt) - await handlePublish() - const latestAppDetail = await fetchAppDetailDirect({ url: '/apps', id: appDetail.id, @@ -314,7 +342,7 @@ const AppPublisher = ({ catch { return false } - }, [appDetail?.id, convertWorkflowType, handlePublish, publishedAt, setAppDetail, workflowTypeSwitchConfig]) + }, [appDetail?.id, convertWorkflowType, getWorkflowTypeSwitchPublishUrl, handlePublish, publishedAt, setAppDetail, workflowTypeSwitchConfig]) const handleWorkflowTypeSwitch = useCallback(async () => { if (!appDetail?.id || !workflowTypeSwitchConfig) @@ -324,7 +352,7 @@ const AppPublisher = ({ return } - if (appDetail.workflow_type === AppTypeEnum.EVALUATION && workflowTypeSwitchConfig.targetType === AppTypeEnum.WORKFLOW) { + if (appDetail.workflow_kind === AppTypeEnum.EVALUATION && workflowTypeSwitchConfig.targetType === AppTypeEnum.WORKFLOW) { const associatedTargetsResult = await refetchEvaluationWorkflowAssociatedTargets() if (associatedTargetsResult.isError) { @@ -343,7 +371,7 @@ const AppPublisher = ({ await performWorkflowTypeSwitch() }, [ appDetail?.id, - appDetail?.workflow_type, + appDetail?.workflow_kind, performWorkflowTypeSwitch, refetchEvaluationWorkflowAssociatedTargets, t, 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 41e47967b2..5f9815a72f 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 @@ -127,6 +127,9 @@ vi.mock('@/app/components/app/app-publisher', () => ({ + ) }, @@ -457,6 +460,24 @@ describe('FeaturesTrigger', () => { }) }) + it('should respect the publish url passed by the publisher', async () => { + // Arrange + const user = userEvent.setup() + renderWithToast() + + // Act + await user.click(screen.getByRole('button', { name: 'publisher-publish-evaluation' })) + + // Assert + await waitFor(() => { + expect(mockPublishWorkflow).toHaveBeenCalledWith({ + url: '/apps/app-id/workflows/publish/evaluation', + title: 'Evaluation title', + releaseNotes: 'Evaluation notes', + }) + }) + }) + it('should skip success side effects when publish mutation returns no workflow version', async () => { // Arrange const user = userEvent.setup() diff --git a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx index d1aafa5c43..a441532a7f 100644 --- a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx +++ b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx @@ -158,7 +158,7 @@ const FeaturesTrigger = () => { // Then perform the detailed validation if (await handleCheckBeforePublish()) { const res = await publishWorkflow({ - url: `/apps/${appID}/workflows/publish`, + url: publishParams?.url || `/apps/${appID}/workflows/publish`, title: publishParams?.title || '', releaseNotes: publishParams?.releaseNotes || '', }) diff --git a/web/app/components/workflow-app/components/workflow-onboarding-modal/start-node-selection-panel.tsx b/web/app/components/workflow-app/components/workflow-onboarding-modal/start-node-selection-panel.tsx index 30dce87121..e5d148f6b5 100644 --- a/web/app/components/workflow-app/components/workflow-onboarding-modal/start-node-selection-panel.tsx +++ b/web/app/components/workflow-app/components/workflow-onboarding-modal/start-node-selection-panel.tsx @@ -21,7 +21,7 @@ const StartNodeSelectionPanel: FC = ({ onSelectTrigger, }) => { const { t } = useTranslation() - const appType = useAppStore(s => s.appDetail?.workflow_type) + const appType = useAppStore(s => s.appDetail?.workflow_kind) const [showTriggerSelector, setShowTriggerSelector] = useState(false) const isEvaluationWorkflowType = isEvaluationWorkflow(appType) diff --git a/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts b/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts index 1e47b82932..b11be29e9a 100644 --- a/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts +++ b/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts @@ -18,7 +18,7 @@ import { useIsChatMode } from './use-is-chat-mode' export const useAvailableNodesMetaData = () => { const { t } = useTranslation() const isChatMode = useIsChatMode() - const appType = useAppStore(s => s.appDetail?.workflow_type) + const appType = useAppStore(s => s.appDetail?.workflow_kind) const docLink = useDocLink() const isEvaluationWorkflowType = isEvaluationWorkflow(appType) diff --git a/web/app/components/workflow/block-selector/all-start-blocks.tsx b/web/app/components/workflow/block-selector/all-start-blocks.tsx index 416c107ed3..605a3eb5b7 100644 --- a/web/app/components/workflow/block-selector/all-start-blocks.tsx +++ b/web/app/components/workflow/block-selector/all-start-blocks.tsx @@ -57,7 +57,7 @@ const AllStartBlocks = ({ const { t } = useTranslation() const [hasStartBlocksContent, setHasStartBlocksContent] = useState(false) const [hasPluginContent, setHasPluginContent] = useState(false) - const appType = useAppStore(s => s.appDetail?.workflow_type) + const appType = useAppStore(s => s.appDetail?.workflow_kind) const { data: enable_marketplace } = useSuspenseQuery({ ...systemFeaturesQueryOptions(), select: s => s.enable_marketplace, diff --git a/web/app/components/workflow/block-selector/blocks.tsx b/web/app/components/workflow/block-selector/blocks.tsx index 27e62a0661..70497ec69d 100644 --- a/web/app/components/workflow/block-selector/blocks.tsx +++ b/web/app/components/workflow/block-selector/blocks.tsx @@ -31,7 +31,7 @@ const Blocks = ({ }: BlocksProps) => { const { t } = useTranslation() const store = useStoreApi() - const appType = useAppStore(s => s.appDetail?.workflow_type) + const appType = useAppStore(s => s.appDetail?.workflow_kind) const blocksFromHooks = useBlocks() const filteredAvailableBlocksTypes = useMemo(() => { if (!isEvaluationWorkflow(appType)) diff --git a/web/types/app.ts b/web/types/app.ts index 3d11798f31..f75b252865 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -391,7 +391,7 @@ export type App = { /** whether workflow trigger has un-published draft */ has_draft_trigger?: boolean /** Type */ - workflow_type?: AppTypeEnum + workflow_kind?: AppTypeEnum } export type AppSSO = {