fix: publish as evaluation

This commit is contained in:
JzoNg 2026-04-20 16:24:53 +08:00
parent 7c8a87af05
commit bcd87ddc58
10 changed files with 107 additions and 25 deletions

View File

@ -126,7 +126,7 @@ const AppInfoDetailPanel = ({
secondaryOperations={secondaryOperations}
/>
</div>
{appDetail.workflow_type !== AppTypeEnum.EVALUATION && (
{appDetail.workflow_kind !== AppTypeEnum.EVALUATION && (
<CardView
appId={appDetail.id}
isInPanel={true}

View File

@ -491,7 +491,7 @@ describe('AppPublisher', () => {
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(
<AppPublisher
onPublish={mockOnPublish}
/>,
)
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,

View File

@ -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,

View File

@ -127,6 +127,9 @@ vi.mock('@/app/components/app/app-publisher', () => ({
<button type="button" onClick={() => { Promise.resolve(props.onPublish?.({ title: 'Test title', releaseNotes: 'Test notes' })).catch(() => undefined) }}>
publisher-publish-with-params
</button>
<button type="button" onClick={() => { Promise.resolve(props.onPublish?.({ url: '/apps/app-id/workflows/publish/evaluation', title: 'Evaluation title', releaseNotes: 'Evaluation notes' })).catch(() => undefined) }}>
publisher-publish-evaluation
</button>
</div>
)
},
@ -457,6 +460,24 @@ describe('FeaturesTrigger', () => {
})
})
it('should respect the publish url passed by the publisher', async () => {
// Arrange
const user = userEvent.setup()
renderWithToast(<FeaturesTrigger />)
// 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()

View File

@ -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 || '',
})

View File

@ -21,7 +21,7 @@ const StartNodeSelectionPanel: FC<StartNodeSelectionPanelProps> = ({
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)

View File

@ -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)

View File

@ -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,

View File

@ -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))

View File

@ -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 = {