mirror of https://github.com/langgenius/dify.git
refactor(web): migrate explore app lists from useSWR to TanStack Query (#30076)
This commit is contained in:
parent
1e3823e605
commit
18d69775ef
|
|
@ -8,7 +8,6 @@ import { useRouter } from 'next/navigation'
|
|||
import * as React from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import AppTypeSelector from '@/app/components/app/type-selector'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
|
|
@ -24,7 +23,8 @@ import ExploreContext from '@/context/explore-context'
|
|||
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
|
||||
import { DSLImportMode } from '@/models/app'
|
||||
import { importDSL } from '@/service/apps'
|
||||
import { fetchAppDetail, fetchAppList } from '@/service/explore'
|
||||
import { fetchAppDetail } from '@/service/explore'
|
||||
import { exploreAppListInitialData, useExploreAppList } from '@/service/use-explore'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
|
@ -70,21 +70,8 @@ const Apps = ({
|
|||
})
|
||||
|
||||
const {
|
||||
data: { categories, allList },
|
||||
} = useSWR(
|
||||
['/explore/apps'],
|
||||
() =>
|
||||
fetchAppList().then(({ categories, recommended_apps }) => ({
|
||||
categories,
|
||||
allList: recommended_apps.sort((a, b) => a.position - b.position),
|
||||
})),
|
||||
{
|
||||
fallbackData: {
|
||||
categories: [],
|
||||
allList: [],
|
||||
},
|
||||
},
|
||||
)
|
||||
data: { categories, allList } = exploreAppListInitialData,
|
||||
} = useExploreAppList()
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
const filteredByCategory = allList.filter((item) => {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import AppList from './index'
|
|||
const allCategoriesEn = 'explore.apps.allCategories:{"lng":"en"}'
|
||||
let mockTabValue = allCategoriesEn
|
||||
const mockSetTab = vi.fn()
|
||||
let mockSWRData: { categories: string[], allList: App[] } = { categories: [], allList: [] }
|
||||
let mockExploreData: { categories: string[], allList: App[] } = { categories: [], allList: [] }
|
||||
const mockHandleImportDSL = vi.fn()
|
||||
const mockHandleImportDSLConfirm = vi.fn()
|
||||
|
||||
|
|
@ -33,9 +33,9 @@ vi.mock('ahooks', async () => {
|
|||
}
|
||||
})
|
||||
|
||||
vi.mock('swr', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({ data: mockSWRData }),
|
||||
vi.mock('@/service/use-explore', () => ({
|
||||
exploreAppListInitialData: { categories: [], allList: [] },
|
||||
useExploreAppList: () => ({ data: mockExploreData }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/explore', () => ({
|
||||
|
|
@ -135,14 +135,14 @@ describe('AppList', () => {
|
|||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockTabValue = allCategoriesEn
|
||||
mockSWRData = { categories: [], allList: [] }
|
||||
mockExploreData = { categories: [], allList: [] }
|
||||
})
|
||||
|
||||
// Rendering: show loading when categories are not ready.
|
||||
describe('Rendering', () => {
|
||||
it('should render loading when categories are empty', () => {
|
||||
// Arrange
|
||||
mockSWRData = { categories: [], allList: [] }
|
||||
mockExploreData = { categories: [], allList: [] }
|
||||
|
||||
// Act
|
||||
renderWithContext()
|
||||
|
|
@ -153,7 +153,7 @@ describe('AppList', () => {
|
|||
|
||||
it('should render app cards when data is available', () => {
|
||||
// Arrange
|
||||
mockSWRData = {
|
||||
mockExploreData = {
|
||||
categories: ['Writing', 'Translate'],
|
||||
allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Beta' }, category: 'Translate' })],
|
||||
}
|
||||
|
|
@ -172,7 +172,7 @@ describe('AppList', () => {
|
|||
it('should filter apps by selected category', () => {
|
||||
// Arrange
|
||||
mockTabValue = 'Writing'
|
||||
mockSWRData = {
|
||||
mockExploreData = {
|
||||
categories: ['Writing', 'Translate'],
|
||||
allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Beta' }, category: 'Translate' })],
|
||||
}
|
||||
|
|
@ -190,7 +190,7 @@ describe('AppList', () => {
|
|||
describe('User Interactions', () => {
|
||||
it('should filter apps by search keywords', async () => {
|
||||
// Arrange
|
||||
mockSWRData = {
|
||||
mockExploreData = {
|
||||
categories: ['Writing'],
|
||||
allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Gamma' } })],
|
||||
}
|
||||
|
|
@ -210,7 +210,7 @@ describe('AppList', () => {
|
|||
it('should handle create flow and confirm DSL when pending', async () => {
|
||||
// Arrange
|
||||
const onSuccess = vi.fn()
|
||||
mockSWRData = {
|
||||
mockExploreData = {
|
||||
categories: ['Writing'],
|
||||
allList: [createApp()],
|
||||
};
|
||||
|
|
@ -246,7 +246,7 @@ describe('AppList', () => {
|
|||
describe('Edge Cases', () => {
|
||||
it('should reset search results when clear icon is clicked', async () => {
|
||||
// Arrange
|
||||
mockSWRData = {
|
||||
mockExploreData = {
|
||||
categories: ['Writing'],
|
||||
allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Gamma' } })],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { useDebounceFn } from 'ahooks'
|
|||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal'
|
||||
import Input from '@/app/components/base/input'
|
||||
|
|
@ -20,7 +19,8 @@ import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
|
|||
import {
|
||||
DSLImportMode,
|
||||
} from '@/models/app'
|
||||
import { fetchAppDetail, fetchAppList } from '@/service/explore'
|
||||
import { fetchAppDetail } from '@/service/explore'
|
||||
import { exploreAppListInitialData, useExploreAppList } from '@/service/use-explore'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import s from './style.module.css'
|
||||
|
||||
|
|
@ -58,21 +58,8 @@ const Apps = ({
|
|||
})
|
||||
|
||||
const {
|
||||
data: { categories, allList },
|
||||
} = useSWR(
|
||||
['/explore/apps'],
|
||||
() =>
|
||||
fetchAppList().then(({ categories, recommended_apps }) => ({
|
||||
categories,
|
||||
allList: recommended_apps.sort((a, b) => a.position - b.position),
|
||||
})),
|
||||
{
|
||||
fallbackData: {
|
||||
categories: [],
|
||||
allList: [],
|
||||
},
|
||||
},
|
||||
)
|
||||
data: { categories, allList } = exploreAppListInitialData,
|
||||
} = useExploreAppList()
|
||||
|
||||
const filteredList = allList.filter(item => currCategory === allCategoriesEn || item.category === currCategory)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { HeaderProps } from '@/app/components/workflow/header'
|
||||
import type { App } from '@/types/app'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import WorkflowHeader from './index'
|
||||
|
||||
|
|
@ -9,8 +9,47 @@ const mockSetCurrentLogItem = vi.fn()
|
|||
const mockSetShowMessageLogModal = vi.fn()
|
||||
const mockResetWorkflowVersionHistory = vi.fn()
|
||||
|
||||
const createMockApp = (overrides: Partial<App> = {}): App => ({
|
||||
id: 'app-id',
|
||||
name: 'Workflow App',
|
||||
description: 'Workflow app description',
|
||||
author_name: 'Workflow app author',
|
||||
icon_type: 'emoji',
|
||||
icon: 'app-icon',
|
||||
icon_background: '#FFFFFF',
|
||||
icon_url: null,
|
||||
use_icon_as_answer_icon: false,
|
||||
mode: AppModeEnum.COMPLETION,
|
||||
enable_site: true,
|
||||
enable_api: true,
|
||||
api_rpm: 60,
|
||||
api_rph: 3600,
|
||||
is_demo: false,
|
||||
model_config: {} as App['model_config'],
|
||||
app_model_config: {} as App['app_model_config'],
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
site: {
|
||||
access_token: 'token',
|
||||
app_base_url: 'https://example.com',
|
||||
} as App['site'],
|
||||
api_base_url: 'https://api.example.com',
|
||||
tags: [],
|
||||
access_mode: 'public_access' as App['access_mode'],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
let appDetail: App
|
||||
|
||||
const mockAppStore = (overrides: Partial<App> = {}) => {
|
||||
appDetail = createMockApp(overrides)
|
||||
mockUseAppStoreSelector.mockImplementation(selector => selector({
|
||||
appDetail,
|
||||
setCurrentLogItem: mockSetCurrentLogItem,
|
||||
setShowMessageLogModal: mockSetShowMessageLogModal,
|
||||
}))
|
||||
}
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
__esModule: true,
|
||||
useStore: (selector: (state: { appDetail?: App, setCurrentLogItem: typeof mockSetCurrentLogItem, setShowMessageLogModal: typeof mockSetShowMessageLogModal }) => unknown) => mockUseAppStoreSelector(selector),
|
||||
|
|
@ -60,13 +99,7 @@ vi.mock('@/service/use-workflow', () => ({
|
|||
describe('WorkflowHeader', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
appDetail = { id: 'app-id', mode: AppModeEnum.COMPLETION } as unknown as App
|
||||
|
||||
mockUseAppStoreSelector.mockImplementation(selector => selector({
|
||||
appDetail,
|
||||
setCurrentLogItem: mockSetCurrentLogItem,
|
||||
setShowMessageLogModal: mockSetShowMessageLogModal,
|
||||
}))
|
||||
mockAppStore()
|
||||
})
|
||||
|
||||
// Verifies the wrapper renders the workflow header shell.
|
||||
|
|
@ -84,12 +117,7 @@ describe('WorkflowHeader', () => {
|
|||
describe('Props', () => {
|
||||
it('should configure preview mode when app is in advanced chat mode', () => {
|
||||
// Arrange
|
||||
appDetail = { id: 'app-id', mode: AppModeEnum.ADVANCED_CHAT } as unknown as App
|
||||
mockUseAppStoreSelector.mockImplementation(selector => selector({
|
||||
appDetail,
|
||||
setCurrentLogItem: mockSetCurrentLogItem,
|
||||
setShowMessageLogModal: mockSetShowMessageLogModal,
|
||||
}))
|
||||
mockAppStore({ mode: AppModeEnum.ADVANCED_CHAT })
|
||||
|
||||
// Act
|
||||
render(<WorkflowHeader />)
|
||||
|
|
@ -104,12 +132,7 @@ describe('WorkflowHeader', () => {
|
|||
|
||||
it('should configure run mode when app is not in advanced chat mode', () => {
|
||||
// Arrange
|
||||
appDetail = { id: 'app-id', mode: AppModeEnum.COMPLETION } as unknown as App
|
||||
mockUseAppStoreSelector.mockImplementation(selector => selector({
|
||||
appDetail,
|
||||
setCurrentLogItem: mockSetCurrentLogItem,
|
||||
setShowMessageLogModal: mockSetShowMessageLogModal,
|
||||
}))
|
||||
mockAppStore({ mode: AppModeEnum.COMPLETION })
|
||||
|
||||
// Act
|
||||
render(<WorkflowHeader />)
|
||||
|
|
@ -130,7 +153,7 @@ describe('WorkflowHeader', () => {
|
|||
render(<WorkflowHeader />)
|
||||
|
||||
// Act
|
||||
screen.getByRole('button', { name: 'clear-history' }).click()
|
||||
fireEvent.click(screen.getByRole('button', { name: /clear-history/i }))
|
||||
|
||||
// Assert
|
||||
expect(mockSetCurrentLogItem).toHaveBeenCalledWith()
|
||||
|
|
@ -145,7 +168,7 @@ describe('WorkflowHeader', () => {
|
|||
render(<WorkflowHeader />)
|
||||
|
||||
// Assert
|
||||
screen.getByRole('button', { name: 'restore-settled' }).click()
|
||||
fireEvent.click(screen.getByRole('button', { name: /restore-settled/i }))
|
||||
expect(mockResetWorkflowVersionHistory).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { AccessMode } from '@/models/access-control'
|
||||
import type { App, AppCategory } from '@/models/explore'
|
||||
import { del, get, patch, post } from './base'
|
||||
import { del, get, patch } from './base'
|
||||
|
||||
export const fetchAppList = () => {
|
||||
return get<{
|
||||
|
|
@ -17,14 +17,6 @@ export const fetchInstalledAppList = (app_id?: string | null) => {
|
|||
return get(`/installed-apps${app_id ? `?app_id=${app_id}` : ''}`)
|
||||
}
|
||||
|
||||
export const installApp = (id: string) => {
|
||||
return post('/installed-apps', {
|
||||
body: {
|
||||
app_id: id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const uninstallApp = (id: string) => {
|
||||
return del(`/installed-apps/${id}`)
|
||||
}
|
||||
|
|
@ -37,10 +29,6 @@ export const updatePinStatus = (id: string, isPinned: boolean) => {
|
|||
})
|
||||
}
|
||||
|
||||
export const getToolProviders = () => {
|
||||
return get('/workspaces/current/tool-providers')
|
||||
}
|
||||
|
||||
export const getAppAccessModeByAppId = (appId: string) => {
|
||||
return get<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,36 @@
|
|||
import type { App, AppCategory } from '@/models/explore'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { fetchInstalledAppList, getAppAccessModeByAppId, uninstallApp, updatePinStatus } from './explore'
|
||||
import { fetchAppList, fetchInstalledAppList, getAppAccessModeByAppId, uninstallApp, updatePinStatus } from './explore'
|
||||
import { fetchAppMeta, fetchAppParams } from './share'
|
||||
|
||||
const NAME_SPACE = 'explore'
|
||||
|
||||
type ExploreAppListData = {
|
||||
categories: AppCategory[]
|
||||
allList: App[]
|
||||
}
|
||||
|
||||
export const exploreAppListInitialData: ExploreAppListData = {
|
||||
categories: [],
|
||||
allList: [],
|
||||
}
|
||||
|
||||
export const useExploreAppList = () => {
|
||||
return useQuery<ExploreAppListData>({
|
||||
queryKey: [NAME_SPACE, 'appList'],
|
||||
queryFn: async () => {
|
||||
const { categories, recommended_apps } = await fetchAppList()
|
||||
return {
|
||||
categories,
|
||||
allList: [...recommended_apps].sort((a, b) => a.position - b.position),
|
||||
}
|
||||
},
|
||||
placeholderData: exploreAppListInitialData,
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetInstalledApps = () => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'installedApps'],
|
||||
|
|
|
|||
Loading…
Reference in New Issue