From 38045078dc4b1441269d4c9ed232d5ff55dbcf0f Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 17 Jul 2026 15:20:31 +0800 Subject: [PATCH] fix: can not create app from learn dify in app list page (#39187) --- .../components/apps/__tests__/index.spec.tsx | 41 ++++++++- .../components/apps/__tests__/list.spec.tsx | 86 ++++++++++++++++++- .../apps/first-empty-state/index.tsx | 16 +++- web/app/components/apps/index.tsx | 17 +++- web/app/components/apps/list.tsx | 8 +- 5 files changed, 162 insertions(+), 6 deletions(-) diff --git a/web/app/components/apps/__tests__/index.spec.tsx b/web/app/components/apps/__tests__/index.spec.tsx index b20eabdf0c8..a05cf35a4b6 100644 --- a/web/app/components/apps/__tests__/index.spec.tsx +++ b/web/app/components/apps/__tests__/index.spec.tsx @@ -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() + + 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() + + 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 }) => { diff --git a/web/app/components/apps/__tests__/list.spec.tsx b/web/app/components/apps/__tests__/list.spec.tsx index 0ef8d00eae8..eba651204ca 100644 --- a/web/app/components/apps/__tests__/list.spec.tsx +++ b/web/app/components/apps/__tests__/list.spec.tsx @@ -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 } @@ -472,7 +532,7 @@ const renderList = (searchParams = '', options: RenderListOptions = {}) => { }) return renderWithNuqs( - + , { 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() diff --git a/web/app/components/apps/first-empty-state/index.tsx b/web/app/components/apps/first-empty-state/index.tsx index fba20dc17cb..54accf7d4c9 100644 --- a/web/app/components/apps/first-empty-state/index.tsx +++ b/web/app/components/apps/first-empty-state/index.tsx @@ -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 {showLearnDify && ( $['firstEmpty.learnDifyTitle'], { ns: 'app' })} /> diff --git a/web/app/components/apps/index.tsx b/web/app/components/apps/index.tsx index 4aed098a7ae..07376c80d18 100644 --- a/web/app/components/apps/index.tsx +++ b/web/app/components/apps/index.tsx @@ -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 = () => { }} >
- + {isShowTryAppPanel && ( 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 ? ( ) : (