fix: can not create app from learn dify in app list page (#39187)

This commit is contained in:
Joel 2026-07-17 15:20:31 +08:00 committed by GitHub
parent 83f40b85f4
commit 38045078dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 162 additions and 6 deletions

View File

@ -1,7 +1,9 @@
import type { ReactNode } from 'react'
import type { App } from '@/models/explore'
import type { TryAppSelection } from '@/types/try-app'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { useContextSelector } from 'use-context-selector'
import AppListContext from '@/context/app-list-context'
@ -132,7 +134,13 @@ vi.mock('@/next/navigation', () => ({
}))
vi.mock('../list', () => {
const MockList = () => {
const MockList = ({
onCreateLearnDify,
onTryLearnDify,
}: {
onCreateLearnDify?: (app: App) => void
onTryLearnDify?: (params: TryAppSelection) => void
}) => {
const setShowTryAppPanel = useContextSelector(AppListContext, (ctx) => ctx.setShowTryAppPanel)
return React.createElement(
'div',
@ -150,6 +158,19 @@ vi.mock('../list', () => {
},
'Open Preview',
),
React.createElement(
'button',
{
type: 'button',
onClick: () => onTryLearnDify?.({ appId: mockTemplateApp.app_id, app: mockTemplateApp }),
},
'Preview Learn Dify template',
),
React.createElement(
'button',
{ type: 'button', onClick: () => onCreateLearnDify?.(mockTemplateApp) },
'Create Learn Dify template',
),
)
}
@ -365,6 +386,24 @@ describe('Apps', () => {
})
})
it('should open the template preview from Learn Dify', async () => {
const user = userEvent.setup()
renderWithClient(<Apps />)
await user.click(screen.getByRole('button', { name: 'Preview Learn Dify template' }))
expect(await screen.findByTestId('try-app-panel')).toBeInTheDocument()
})
it('should open the create modal from Learn Dify', async () => {
const user = userEvent.setup()
renderWithClient(<Apps />)
await user.click(screen.getByRole('button', { name: 'Create Learn Dify template' }))
expect(await screen.findByTestId('create-app-modal')).toBeInTheDocument()
})
it('should track template preview creation after confirming a pending import', async () => {
mockHandleImportDSL.mockImplementation(
async (_payload: unknown, options: { onPending?: () => void }) => {

View File

@ -1,4 +1,6 @@
import type { GetSystemFeaturesResponse } from '@dify/contracts/api/console/system-features/types.gen'
import type { App } from '@/models/explore'
import type { TryAppSelection } from '@/types/try-app'
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
@ -30,6 +32,36 @@ const mockUseWorkflowOnlineUsers = vi.hoisted(() =>
})),
)
const mockLearnDifyApp = vi.hoisted(
() =>
({
app_id: 'learn-dify-template',
app: {
id: 'learn-dify-template-source',
mode: 'chat',
icon_type: 'emoji',
icon: '🤖',
icon_background: '#fff',
icon_url: '',
name: 'Learn Dify Template',
description: 'Learn how to build with Dify',
use_icon_as_answer_icon: false,
},
description: 'Learn how to build with Dify',
copyright: '',
privacy_policy: null,
custom_disclaimer: null,
categories: ['Assistant'],
position: 1,
is_listed: true,
install_count: 0,
installed: false,
editable: false,
is_agent: false,
can_trial: true,
}) satisfies App,
)
const mockReplace = vi.fn()
const mockRouter = { replace: mockReplace }
let mockSearchParams = new URLSearchParams('')
@ -436,7 +468,33 @@ vi.mock('../empty', () => ({
}))
vi.mock('@/app/components/explore/learn-dify', () => ({
default: ({ title }: { title?: string }) => React.createElement('section', null, title),
default: ({
title,
onCreate,
onTry,
}: {
title?: string
onCreate?: (app: App) => void
onTry?: (params: TryAppSelection) => void
}) =>
React.createElement(
'section',
null,
title,
React.createElement(
'button',
{
type: 'button',
onClick: () => onTry?.({ appId: mockLearnDifyApp.app_id, app: mockLearnDifyApp }),
},
'Preview Learn Dify template',
),
React.createElement(
'button',
{ type: 'button', onClick: () => onCreate?.(mockLearnDifyApp) },
'Create Learn Dify template',
),
),
}))
const intersectionCallbacks: IntersectionObserverCallback[] = []
@ -462,6 +520,8 @@ beforeAll(() => {
// Render helper wrapping with shared nuqs testing helper plus a seeded
// systemFeatures cache so List can resolve its useSuspenseQuery.
type RenderListOptions = {
onCreateLearnDify?: (app: App) => void
onTryLearnDify?: (params: TryAppSelection) => void
systemFeatures?: Partial<GetSystemFeaturesResponse>
}
@ -472,7 +532,7 @@ const renderList = (searchParams = '', options: RenderListOptions = {}) => {
})
return renderWithNuqs(
<SystemFeaturesWrapper>
<List />
<List onCreateLearnDify={options.onCreateLearnDify} onTryLearnDify={options.onTryLearnDify} />
</SystemFeaturesWrapper>,
{ searchParams },
)
@ -768,6 +828,28 @@ describe('List', () => {
expect(screen.getByTestId('create-dsl-modal'))!.toBeInTheDocument()
})
it('should forward Learn Dify template interactions', async () => {
const user = userEvent.setup()
const onCreateLearnDify = vi.fn()
const onTryLearnDify = vi.fn()
mockAppData = { pages: [{ data: [], total: 0 }] }
renderList('', {
onCreateLearnDify,
onTryLearnDify,
systemFeatures: { enable_learn_app: true },
})
await user.click(screen.getByRole('button', { name: 'Preview Learn Dify template' }))
expect(onTryLearnDify).toHaveBeenCalledWith({
appId: mockLearnDifyApp.app_id,
app: mockLearnDifyApp,
})
await user.click(screen.getByRole('button', { name: 'Create Learn Dify template' }))
expect(onCreateLearnDify).toHaveBeenCalledWith(mockLearnDifyApp)
})
it('should pass workflow app ids to online users hook', () => {
renderList()

View File

@ -1,6 +1,8 @@
'use client'
import type { ReactNode } from 'react'
import type { App } from '@/models/explore'
import type { TryAppSelection } from '@/types/try-app'
import { useTranslation } from 'react-i18next'
import LearnDify from '@/app/components/explore/learn-dify'
import FirstEmptyActionCard from './action-card'
@ -20,12 +22,21 @@ type EmptyCreateAction = {
type Props = {
onCreateBlank: () => void
onCreateLearnDify?: (app: App) => void
onCreateTemplate: () => void
onImportDSL: () => void
onTryLearnDify?: (params: TryAppSelection) => void
showLearnDify: boolean
}
function FirstEmptyState({ onCreateBlank, onCreateTemplate, onImportDSL, showLearnDify }: Props) {
function FirstEmptyState({
onCreateBlank,
onCreateLearnDify,
onCreateTemplate,
onImportDSL,
onTryLearnDify,
showLearnDify,
}: Props) {
const { t } = useTranslation()
const actions: EmptyCreateAction[] = [
@ -109,9 +120,12 @@ function FirstEmptyState({ onCreateBlank, onCreateTemplate, onImportDSL, showLea
</div>
{showLearnDify && (
<LearnDify
canCreate
className="px-4 pt-2 pb-0 [&_div.grid]:gap-3 [&>div]:mx-0 [&>div]:rounded-t-2xl [&>div]:rounded-b-none [&>div]:px-5 [&>div]:pt-4 [&>div]:pb-5"
dismissible={false}
itemLimit={4}
onCreate={onCreateLearnDify}
onTry={onTryLearnDify}
showDescription
title={t(($) => $['firstEmpty.learnDifyTitle'], { ns: 'app' })}
/>

View File

@ -1,5 +1,6 @@
'use client'
import type { CreateAppModalProps } from '../explore/create-app-modal'
import type { App } from '@/models/explore'
import type { TryAppSelection } from '@/types/try-app'
import type { TrackCreateAppParams } from '@/utils/create-app-tracking'
import { useAtomValue } from 'jotai'
@ -60,6 +61,16 @@ const Apps = () => {
}
const [isShowCreateModal, setIsShowCreateModal] = useState(false)
const handleTryLearnDify = (params: TryAppSelection) => {
setShowTryAppPanel(true, params)
}
const handleCreateLearnDify = (app: App) => {
if (!canCreateApp) return
setCurrentTryAppParams({ appId: app.app_id, app })
setIsShowCreateModal(true)
}
const handleShowFromTryApp = useCallback(() => {
if (!canCreateApp) return
@ -190,7 +201,11 @@ const Apps = () => {
}}
>
<div className="relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body">
<List controlRefreshList={controlRefreshList} />
<List
controlRefreshList={controlRefreshList}
onCreateLearnDify={handleCreateLearnDify}
onTryLearnDify={handleTryLearnDify}
/>
{isShowTryAppPanel && (
<TryApp
appId={currentTryAppParams?.appId || ''}

View File

@ -1,6 +1,8 @@
'use client'
import type { GetAppsData } from '@dify/contracts/api/console/apps/types.gen'
import type { App } from '@/models/explore'
import type { TryAppSelection } from '@/types/try-app'
import { cn } from '@langgenius/dify-ui/cn'
import {
keepPreviousData,
@ -42,9 +44,11 @@ type AppListSortBy = NonNullable<AppListQuery['sort_by']>
type Props = Readonly<{
controlRefreshList?: number
onCreateLearnDify?: (app: App) => void
onTryLearnDify?: (params: TryAppSelection) => void
}>
function List({ controlRefreshList = 0 }: Props) {
function List({ controlRefreshList = 0, onCreateLearnDify, onTryLearnDify }: Props) {
const { t } = useTranslation()
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
@ -270,8 +274,10 @@ function List({ controlRefreshList = 0 }: Props) {
{showFirstEmptyState ? (
<FirstEmptyState
onCreateBlank={openCreateBlankModal}
onCreateLearnDify={onCreateLearnDify}
onCreateTemplate={openCreateTemplateDialog}
onImportDSL={openCreateFromDSLModal}
onTryLearnDify={onTryLearnDify}
showLearnDify={systemFeatures.enable_learn_app}
/>
) : (