mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 10:56:13 +08:00
refactor(web): move app quota refresh out of provider context (#41906)
This commit is contained in:
parent
40f0b8a2a0
commit
3be0cabf0a
@ -20,7 +20,6 @@ export const baseProviderContextValue: ProviderContextState = {
|
||||
isFetchedPlanInfo: false,
|
||||
enableBilling: false,
|
||||
enableSkill: false,
|
||||
onPlanInfoChanged: noop,
|
||||
enableReplaceWebAppLogo: false,
|
||||
modelLoadBalancingEnabled: false,
|
||||
enableEducationPlan: false,
|
||||
@ -38,7 +37,6 @@ export const createMockProviderContextValue = (
|
||||
return {
|
||||
...merged,
|
||||
refreshModelProviders: merged.refreshModelProviders ?? noop,
|
||||
onPlanInfoChanged: merged.onPlanInfoChanged ?? noop,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { useAppInfoActions } from '../use-app-info-actions'
|
||||
|
||||
const toastMocks = vi.hoisted(() => {
|
||||
@ -16,7 +17,6 @@ const toastMocks = vi.hoisted(() => {
|
||||
}
|
||||
})
|
||||
const mockReplace = vi.fn()
|
||||
const mockOnPlanInfoChanged = vi.fn()
|
||||
const mockInvalidateQueries = vi.fn()
|
||||
const mockSetAppDetail = vi.fn()
|
||||
const mockUpdateAppInfo = vi.fn()
|
||||
@ -47,10 +47,6 @@ vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({ replace: mockReplace }),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({ onPlanInfoChanged: mockOnPlanInfoChanged }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: (selector: (state: Record<string, unknown>) => unknown) =>
|
||||
selector({
|
||||
@ -87,6 +83,13 @@ vi.mock('@tanstack/react-query', () => ({
|
||||
useSuspenseQuery: () => ({
|
||||
data: { rbac_enabled: true },
|
||||
}),
|
||||
useMutation: ({ mutationKey }: { mutationKey: unknown }) => ({
|
||||
mutateAsync:
|
||||
JSON.stringify(mutationKey) ===
|
||||
JSON.stringify(consoleQuery.apps.byAppId.copy.post.mutationOptions().mutationKey)
|
||||
? mockCopyApp
|
||||
: mockDeleteApp,
|
||||
}),
|
||||
useQueryClient: () => ({
|
||||
invalidateQueries: mockInvalidateQueries,
|
||||
setQueryData: mockSetQueryData,
|
||||
@ -95,8 +98,6 @@ vi.mock('@tanstack/react-query', () => ({
|
||||
|
||||
vi.mock('@/service/apps', () => ({
|
||||
updateAppInfo: (...args: unknown[]) => mockUpdateAppInfo(...args),
|
||||
copyApp: (...args: unknown[]) => mockCopyApp(...args),
|
||||
deleteApp: (...args: unknown[]) => mockDeleteApp(...args),
|
||||
fetchAppDetail: (...args: unknown[]) => mockFetchAppDetail(...args),
|
||||
}))
|
||||
|
||||
@ -306,29 +307,50 @@ describe('useAppInfoActions', () => {
|
||||
})
|
||||
|
||||
describe('onCopy', () => {
|
||||
it('should copy app and redirect on success', async () => {
|
||||
const newApp = { id: 'app-2', name: 'Copy', mode: 'chat' }
|
||||
mockCopyApp.mockResolvedValue(newApp)
|
||||
it.each(['completed', 'pending'] as const)(
|
||||
'should redirect only when the copy is completed (%s)',
|
||||
async (status) => {
|
||||
const newApp = { id: 'app-2', name: 'Copy', mode: 'chat' }
|
||||
mockCopyApp.mockResolvedValue(
|
||||
status === 'completed'
|
||||
? newApp
|
||||
: {
|
||||
id: 'import-1',
|
||||
status: 'pending',
|
||||
current_dsl_version: '1.0.0',
|
||||
},
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useAppInfoActions({}))
|
||||
const { result } = renderHook(() => useAppInfoActions({}))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.onCopy({
|
||||
name: 'Copy',
|
||||
icon_type: 'emoji',
|
||||
icon: '🤖',
|
||||
icon_background: '#fff',
|
||||
await act(async () => {
|
||||
await result.current.onCopy({
|
||||
name: 'Copy',
|
||||
icon_type: 'emoji',
|
||||
icon: '🤖',
|
||||
icon_background: '#fff',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
expect(mockCopyApp).toHaveBeenCalled()
|
||||
expect(mockInvalidateQueries).toHaveBeenCalledTimes(3)
|
||||
expect(toastMocks.call).toHaveBeenCalledWith({
|
||||
type: 'success',
|
||||
message: 'app.newApp.appCreated',
|
||||
})
|
||||
expect(mockOnPlanInfoChanged).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockCopyApp).toHaveBeenCalledWith({
|
||||
params: { app_id: 'app-1' },
|
||||
body: { name: 'Copy', icon_type: 'emoji', icon: '🤖', icon_background: '#fff' },
|
||||
})
|
||||
if (status === 'completed') {
|
||||
expect(toastMocks.call).toHaveBeenCalledWith({
|
||||
type: 'success',
|
||||
message: 'app.newApp.appCreated',
|
||||
})
|
||||
expect(getRedirection).toHaveBeenCalledWith(newApp, mockReplace, { isRbacEnabled: true })
|
||||
} else {
|
||||
expect(toastMocks.call).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'app.newApp.appCreateFailed',
|
||||
})
|
||||
expect(getRedirection).not.toHaveBeenCalled()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it('should notify error on copy failure', async () => {
|
||||
mockCopyApp.mockRejectedValue(new Error('fail'))
|
||||
@ -507,12 +529,11 @@ describe('useAppInfoActions', () => {
|
||||
await result.current.onConfirmDelete()
|
||||
})
|
||||
|
||||
expect(mockDeleteApp).toHaveBeenCalledWith('app-1')
|
||||
expect(mockDeleteApp).toHaveBeenCalledWith({ params: { app_id: 'app-1' } })
|
||||
expect(mockMarkAppDeletionStarted).toHaveBeenCalledWith('app-1')
|
||||
expect(mockMarkAppDeletionSucceeded).toHaveBeenCalledWith('app-1')
|
||||
expect(mockMarkAppDeletionFailed).not.toHaveBeenCalled()
|
||||
expect(toastMocks.call).toHaveBeenCalledWith({ type: 'success', message: 'app.appDeleted' })
|
||||
expect(mockInvalidateQueries).toHaveBeenCalledTimes(3)
|
||||
expect(mockReplace).toHaveBeenCalledWith('/apps')
|
||||
expect(mockSetAppDetail).toHaveBeenCalledWith()
|
||||
})
|
||||
|
||||
@ -7,12 +7,11 @@ import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-moda
|
||||
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
||||
import type { App } from '@/types/app'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useExportAppDsl, useExportWorkflowAppDsl } from '@/app/components/app/use-export-app-dsl'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import {
|
||||
@ -20,7 +19,7 @@ import {
|
||||
markAppDeletionStarted,
|
||||
markAppDeletionSucceeded,
|
||||
} from '@/service/app-deletion'
|
||||
import { copyApp, deleteApp, fetchAppDetail, updateAppInfo } from '@/service/apps'
|
||||
import { fetchAppDetail, updateAppInfo } from '@/service/apps'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
@ -94,7 +93,10 @@ export function useAppInfoActions({ resetKey }: UseAppInfoActionsParams) {
|
||||
const { t } = useTranslation()
|
||||
const { replace } = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const { onPlanInfoChanged } = useProviderContext()
|
||||
const { mutateAsync: copyApp } = useMutation(
|
||||
consoleQuery.apps.byAppId.copy.post.mutationOptions(),
|
||||
)
|
||||
const { mutateAsync: deleteApp } = useMutation(consoleQuery.apps.byAppId.delete.mutationOptions())
|
||||
const appDetail = useAppStore((state) => state.appDetail)
|
||||
const setAppDetail = useAppStore((state) => state.setAppDetail)
|
||||
const { exportAppDsl, isExporting: isAppDslExporting } = useExportAppDsl()
|
||||
@ -251,22 +253,21 @@ export function useAppInfoActions({ resetKey }: UseAppInfoActionsParams) {
|
||||
if (!appDetail) return
|
||||
try {
|
||||
const newApp = await copyApp({
|
||||
appID: appDetail.id,
|
||||
name,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
mode: appDetail.mode,
|
||||
params: { app_id: appDetail.id },
|
||||
body: { name, icon_type, icon, icon_background },
|
||||
})
|
||||
if (!('mode' in newApp)) {
|
||||
toast(
|
||||
t(($) => $['newApp.appCreateFailed'], { ns: 'app' }),
|
||||
{ type: 'error' },
|
||||
)
|
||||
return
|
||||
}
|
||||
closeModal()
|
||||
toast(
|
||||
t(($) => $['newApp.appCreated'], { ns: 'app' }),
|
||||
{ type: 'success' },
|
||||
)
|
||||
void queryClient.invalidateQueries({ queryKey: consoleQuery.apps.get.key() })
|
||||
void queryClient.invalidateQueries({ queryKey: consoleQuery.apps.starred.get.key() })
|
||||
void queryClient.invalidateQueries({ queryKey: consoleQuery.apps.recent.get.key() })
|
||||
onPlanInfoChanged()
|
||||
getRedirection(newApp, replace, { isRbacEnabled })
|
||||
} catch {
|
||||
toast(
|
||||
@ -275,7 +276,7 @@ export function useAppInfoActions({ resetKey }: UseAppInfoActionsParams) {
|
||||
)
|
||||
}
|
||||
},
|
||||
[appDetail, closeModal, isRbacEnabled, onPlanInfoChanged, queryClient, replace, t],
|
||||
[appDetail, closeModal, copyApp, isRbacEnabled, replace, t],
|
||||
)
|
||||
|
||||
const onExport = useCallback(
|
||||
@ -313,16 +314,12 @@ export function useAppInfoActions({ resetKey }: UseAppInfoActionsParams) {
|
||||
if (!appDetail) return
|
||||
markAppDeletionStarted(appDetail.id)
|
||||
try {
|
||||
await deleteApp(appDetail.id)
|
||||
await deleteApp({ params: { app_id: appDetail.id } })
|
||||
markAppDeletionSucceeded(appDetail.id)
|
||||
toast(
|
||||
t(($) => $.appDeleted, { ns: 'app' }),
|
||||
{ type: 'success' },
|
||||
)
|
||||
void queryClient.invalidateQueries({ queryKey: consoleQuery.apps.get.key() })
|
||||
void queryClient.invalidateQueries({ queryKey: consoleQuery.apps.starred.get.key() })
|
||||
void queryClient.invalidateQueries({ queryKey: consoleQuery.apps.recent.get.key() })
|
||||
onPlanInfoChanged()
|
||||
setAppDetail()
|
||||
replace('/apps')
|
||||
} catch (e: unknown) {
|
||||
@ -333,7 +330,7 @@ export function useAppInfoActions({ resetKey }: UseAppInfoActionsParams) {
|
||||
)
|
||||
}
|
||||
closeModal()
|
||||
}, [appDetail, closeModal, onPlanInfoChanged, queryClient, replace, setAppDetail, t])
|
||||
}, [appDetail, closeModal, deleteApp, replace, setAppDetail, t])
|
||||
|
||||
return {
|
||||
appDetail,
|
||||
|
||||
@ -125,7 +125,6 @@ function createMockProviderContext(
|
||||
ttsDefaultModel: null,
|
||||
agentThoughtDefaultModel: null,
|
||||
updateModelList: vi.fn(),
|
||||
onPlanInfoChanged: vi.fn(),
|
||||
refreshModelProviders: vi.fn(),
|
||||
...overrides,
|
||||
} as ProviderContextState
|
||||
|
||||
@ -5,14 +5,14 @@ import CreateAppTemplateDialog from '../index'
|
||||
vi.mock('../app-list', () => ({
|
||||
default: function MockAppList({
|
||||
onCreateFromBlank,
|
||||
onSuccess,
|
||||
onClose,
|
||||
}: {
|
||||
onCreateFromBlank?: () => void
|
||||
onSuccess: () => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
return (
|
||||
<div role="region" aria-label="App list">
|
||||
<button type="button" onClick={onSuccess}>
|
||||
<button type="button" onClick={onClose}>
|
||||
Success
|
||||
</button>
|
||||
{onCreateFromBlank && (
|
||||
@ -28,7 +28,6 @@ vi.mock('../app-list', () => ({
|
||||
describe('CreateAppTemplateDialog', () => {
|
||||
const defaultProps = {
|
||||
show: false,
|
||||
onSuccess: vi.fn(),
|
||||
onClose: vi.fn(),
|
||||
onCreateFromBlank: vi.fn(),
|
||||
}
|
||||
@ -103,21 +102,12 @@ describe('CreateAppTemplateDialog', () => {
|
||||
expect(screen.getByRole('button', { name: 'Success' }))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call both onSuccess and onClose when app list success is triggered', () => {
|
||||
const mockOnSuccess = vi.fn()
|
||||
it('should close when an app is created from the list', () => {
|
||||
const mockOnClose = vi.fn()
|
||||
render(
|
||||
<CreateAppTemplateDialog
|
||||
{...defaultProps}
|
||||
show={true}
|
||||
onSuccess={mockOnSuccess}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
)
|
||||
render(<CreateAppTemplateDialog {...defaultProps} show={true} onClose={mockOnClose} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Success' }))
|
||||
|
||||
expect(mockOnSuccess).toHaveBeenCalledTimes(1)
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@ -143,7 +133,6 @@ describe('CreateAppTemplateDialog', () => {
|
||||
render(
|
||||
<CreateAppTemplateDialog
|
||||
show={true}
|
||||
onSuccess={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
// onCreateFromBlank is undefined
|
||||
/>,
|
||||
@ -154,12 +143,7 @@ describe('CreateAppTemplateDialog', () => {
|
||||
it('should handle undefined props gracefully', () => {
|
||||
expect(() => {
|
||||
render(
|
||||
<CreateAppTemplateDialog
|
||||
show={true}
|
||||
onSuccess={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
onCreateFromBlank={undefined}
|
||||
/>,
|
||||
<CreateAppTemplateDialog show={true} onClose={vi.fn()} onCreateFromBlank={undefined} />,
|
||||
)
|
||||
}).not.toThrow()
|
||||
})
|
||||
@ -193,7 +177,6 @@ describe('CreateAppTemplateDialog', () => {
|
||||
it('should work with all required props only', () => {
|
||||
const requiredProps = {
|
||||
show: true,
|
||||
onSuccess: vi.fn(),
|
||||
onClose: vi.fn(),
|
||||
}
|
||||
|
||||
|
||||
@ -247,7 +247,7 @@ describe('Apps', () => {
|
||||
})
|
||||
|
||||
it('renders template cards when data is available', () => {
|
||||
render(<Apps />)
|
||||
render(<Apps onClose={vi.fn()} />)
|
||||
|
||||
expect(screen.getAllByTestId('app-card')).toHaveLength(6)
|
||||
expect(screen.getByText('Alpha'))!.toBeInTheDocument()
|
||||
@ -255,7 +255,7 @@ describe('Apps', () => {
|
||||
})
|
||||
|
||||
it('opens create modal when a template card is clicked', () => {
|
||||
render(<Apps />)
|
||||
render(<Apps onClose={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getAllByTestId('app-card')[0]!)
|
||||
expect(screen.getByTestId('create-from-template-modal'))!.toBeInTheDocument()
|
||||
@ -264,7 +264,7 @@ describe('Apps', () => {
|
||||
it('passes app.create_and_management permission to template cards even when user is not a workspace editor', () => {
|
||||
mockWorkspacePermissionKeys = ['app.create_and_management']
|
||||
|
||||
render(<Apps />)
|
||||
render(<Apps onClose={vi.fn()} />)
|
||||
|
||||
expect(screen.getAllByTestId('app-card')[0]).toHaveAttribute('data-can-create', 'true')
|
||||
})
|
||||
@ -272,7 +272,7 @@ describe('Apps', () => {
|
||||
it('does not allow template creation when app.create_and_management permission is missing', () => {
|
||||
mockWorkspacePermissionKeys = []
|
||||
|
||||
render(<Apps />)
|
||||
render(<Apps onClose={vi.fn()} />)
|
||||
|
||||
expect(screen.getAllByTestId('app-card')[0]).toHaveAttribute('data-can-create', 'false')
|
||||
})
|
||||
@ -283,14 +283,14 @@ describe('Apps', () => {
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<Apps />)
|
||||
render(<Apps onClose={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText('app.newApp.noTemplateFound'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('app.newApp.noTemplateFoundTip'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('filters templates by keyword and selected app type', async () => {
|
||||
render(<Apps />)
|
||||
render(<Apps onClose={vi.fn()} />)
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('app.newAppFromTemplate.searchAllTemplate'), {
|
||||
target: { value: 'Bravo' },
|
||||
@ -314,9 +314,9 @@ describe('Apps', () => {
|
||||
})
|
||||
|
||||
it('creates an app from a template and redirects after import succeeds', async () => {
|
||||
const onSuccess = vi.fn()
|
||||
const onClose = vi.fn()
|
||||
|
||||
render(<Apps onSuccess={onSuccess} />)
|
||||
render(<Apps onClose={onClose} />)
|
||||
|
||||
fireEvent.click(screen.getAllByTestId('app-card')[0]!)
|
||||
fireEvent.click(screen.getByTestId('confirm-create'))
|
||||
@ -337,7 +337,7 @@ describe('Apps', () => {
|
||||
templateId: 'Alpha',
|
||||
})
|
||||
expect(mockToastSuccess).toHaveBeenCalledWith('app.newApp.appCreated')
|
||||
expect(onSuccess).toHaveBeenCalled()
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
expect(mockHandleCheckPluginDependencies).toHaveBeenCalledWith('created-app-id')
|
||||
expect(mockGetRedirection).toHaveBeenCalledWith(
|
||||
{
|
||||
@ -361,7 +361,7 @@ describe('Apps', () => {
|
||||
app_mode: AppModeEnum.WORKFLOW,
|
||||
})
|
||||
|
||||
render(<Apps />)
|
||||
render(<Apps onClose={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getAllByTestId('app-card')[0]!)
|
||||
fireEvent.click(screen.getByTestId('confirm-create'))
|
||||
@ -387,7 +387,7 @@ describe('Apps', () => {
|
||||
it('shows an error toast when importing the template fails', async () => {
|
||||
mockImportDSL.mockRejectedValueOnce(new Error('failed'))
|
||||
|
||||
render(<Apps />)
|
||||
render(<Apps onClose={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getAllByTestId('app-card')[0]!)
|
||||
fireEvent.click(screen.getByTestId('confirm-create'))
|
||||
@ -400,7 +400,7 @@ describe('Apps', () => {
|
||||
it('forwards the create-from-blank action from the sidebar', () => {
|
||||
const onCreateFromBlank = vi.fn()
|
||||
|
||||
render(<Apps onCreateFromBlank={onCreateFromBlank} />)
|
||||
render(<Apps onClose={vi.fn()} onCreateFromBlank={onCreateFromBlank} />)
|
||||
|
||||
fireEvent.click(screen.getByText('app.newApp.startFromBlank'))
|
||||
|
||||
@ -413,7 +413,7 @@ describe('Apps', () => {
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
render(<Apps />)
|
||||
render(<Apps onClose={vi.fn()} />)
|
||||
|
||||
expect(screen.getByRole('status'))!.toBeInTheDocument()
|
||||
})
|
||||
@ -424,13 +424,13 @@ describe('Apps', () => {
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<Apps />)
|
||||
render(<Apps onClose={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText('app.newApp.noTemplateFound'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should filter templates by category and the remaining app modes', async () => {
|
||||
render(<Apps />)
|
||||
render(<Apps onClose={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getByText('Cat C'))
|
||||
expect(screen.queryByText('Alpha')).not.toBeInTheDocument()
|
||||
@ -473,7 +473,7 @@ describe('Apps', () => {
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<Apps />)
|
||||
render(<Apps onClose={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText('Cat A'))!.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'v' })).not.toBeInTheDocument()
|
||||
@ -481,7 +481,7 @@ describe('Apps', () => {
|
||||
})
|
||||
|
||||
it('should clear the search, hide the sidebar during search, and close the modal when requested', async () => {
|
||||
render(<Apps />)
|
||||
render(<Apps onClose={vi.fn()} />)
|
||||
|
||||
const searchInput = screen.getByPlaceholderText('app.newAppFromTemplate.searchAllTemplate')
|
||||
fireEvent.change(searchInput, {
|
||||
@ -510,7 +510,7 @@ describe('Apps', () => {
|
||||
|
||||
it('clears an active search immediately and returns focus to the searchbox', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<Apps />)
|
||||
render(<Apps onClose={vi.fn()} />)
|
||||
|
||||
const searchInput = screen.getByRole('searchbox', {
|
||||
name: 'app.newAppFromTemplate.searchAllTemplate',
|
||||
|
||||
@ -32,7 +32,7 @@ import AppCard from '../app-card'
|
||||
import Sidebar, { AppCategories, AppCategoryLabel } from './sidebar'
|
||||
|
||||
type AppsProps = {
|
||||
onSuccess?: () => void
|
||||
onClose: () => void
|
||||
onCreateFromBlank?: () => void
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ type AppsProps = {
|
||||
// CREATE = 'create',
|
||||
// }
|
||||
|
||||
const Apps = ({ onSuccess, onCreateFromBlank }: AppsProps) => {
|
||||
const Apps = ({ onClose, onCreateFromBlank }: AppsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const { data: currentUserId } = useSuspenseQuery({
|
||||
@ -151,7 +151,7 @@ const Apps = ({ onSuccess, onCreateFromBlank }: AppsProps) => {
|
||||
|
||||
setIsShowCreateModal(false)
|
||||
toast.success(t(($) => $['newApp.appCreated'], { ns: 'app' }))
|
||||
if (onSuccess) onSuccess()
|
||||
onClose()
|
||||
await handleCheckPluginDependencies(app.app_id)
|
||||
getRedirection(
|
||||
{ id: app.app_id, mode: app.app_mode, permission_keys: app.permission_keys },
|
||||
|
||||
@ -5,17 +5,11 @@ import AppList from './app-list'
|
||||
|
||||
type CreateAppDialogProps = {
|
||||
show: boolean
|
||||
onSuccess: () => void
|
||||
onClose: () => void
|
||||
onCreateFromBlank?: () => void
|
||||
}
|
||||
|
||||
const CreateAppTemplateDialog = ({
|
||||
show,
|
||||
onSuccess,
|
||||
onClose,
|
||||
onCreateFromBlank,
|
||||
}: CreateAppDialogProps) => {
|
||||
const CreateAppTemplateDialog = ({ show, onClose, onCreateFromBlank }: CreateAppDialogProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
@ -24,13 +18,7 @@ const CreateAppTemplateDialog = ({
|
||||
title={t(($) => $['newApp.startFromTemplate'], { ns: 'app' })}
|
||||
onClose={onClose}
|
||||
>
|
||||
<AppList
|
||||
onCreateFromBlank={onCreateFromBlank}
|
||||
onSuccess={() => {
|
||||
onSuccess()
|
||||
onClose()
|
||||
}}
|
||||
/>
|
||||
<AppList onCreateFromBlank={onCreateFromBlank} onClose={onClose} />
|
||||
</CreateAppDialogShell>
|
||||
)
|
||||
}
|
||||
|
||||
@ -125,18 +125,16 @@ const defaultPlanUsage = {
|
||||
|
||||
const renderModal = () => {
|
||||
const onClose = vi.fn()
|
||||
const onSuccess = vi.fn()
|
||||
const onCreateFromTemplate = vi.fn()
|
||||
render(
|
||||
<CreateAppModal
|
||||
show
|
||||
onClose={onClose}
|
||||
onSuccess={onSuccess}
|
||||
onCreateFromTemplate={onCreateFromTemplate}
|
||||
defaultAppMode={AppModeEnum.ADVANCED_CHAT}
|
||||
/>,
|
||||
)
|
||||
return { onClose, onSuccess, onCreateFromTemplate }
|
||||
return { onClose, onCreateFromTemplate }
|
||||
}
|
||||
|
||||
describe('CreateAppModal', () => {
|
||||
@ -168,7 +166,7 @@ describe('CreateAppModal', () => {
|
||||
maintainer: 'user-1',
|
||||
}
|
||||
mockCreateApp.mockResolvedValue(mockApp as App)
|
||||
const { onClose, onSuccess } = renderModal()
|
||||
const { onClose } = renderModal()
|
||||
|
||||
const nameInput = screen.getByPlaceholderText('app.newApp.appNamePlaceholder')
|
||||
fireEvent.change(nameInput, { target: { value: 'My App' } })
|
||||
@ -190,8 +188,7 @@ describe('CreateAppModal', () => {
|
||||
appMode: AppModeEnum.ADVANCED_CHAT,
|
||||
})
|
||||
expect(mockToastSuccess).toHaveBeenCalledWith('app.newApp.appCreated')
|
||||
expect(onSuccess).toHaveBeenCalled()
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
await waitFor(() =>
|
||||
expect(mockGetRedirection).toHaveBeenCalledWith(mockApp, mockPush, {
|
||||
currentUserId: 'user-1',
|
||||
|
||||
@ -34,7 +34,6 @@ import AppIconPicker from '../../base/app-icon-picker'
|
||||
import { CreateAppDialogShell } from '../create-app-dialog-shell'
|
||||
|
||||
type CreateAppProps = {
|
||||
onSuccess: () => void
|
||||
onClose: () => void
|
||||
onCreateFromTemplate?: () => void
|
||||
defaultAppMode?: AppModeEnum
|
||||
@ -50,7 +49,7 @@ const shouldExpandBeginnerAppTypes = (appMode?: AppModeEnum) => {
|
||||
)
|
||||
}
|
||||
|
||||
function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }: CreateAppProps) {
|
||||
function CreateApp({ onClose, onCreateFromTemplate, defaultAppMode }: CreateAppProps) {
|
||||
const { t } = useTranslation()
|
||||
const { push } = useRouter()
|
||||
const nameInputId = useId()
|
||||
@ -120,7 +119,6 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
}
|
||||
|
||||
toast.success(t(($) => $['newApp.appCreated'], { ns: 'app' }))
|
||||
onSuccess()
|
||||
onClose()
|
||||
getRedirection(app, push, {
|
||||
currentUserId,
|
||||
@ -146,7 +144,6 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
appMode,
|
||||
appIcon,
|
||||
description,
|
||||
onSuccess,
|
||||
onClose,
|
||||
push,
|
||||
workspacePermissionKeys,
|
||||
@ -420,7 +417,6 @@ type CreateAppDialogProps = CreateAppProps & {
|
||||
const CreateAppModal = ({
|
||||
show,
|
||||
onClose,
|
||||
onSuccess,
|
||||
onCreateFromTemplate,
|
||||
defaultAppMode,
|
||||
}: CreateAppDialogProps) => {
|
||||
@ -435,7 +431,6 @@ const CreateAppModal = ({
|
||||
>
|
||||
<CreateApp
|
||||
onClose={onClose}
|
||||
onSuccess={onSuccess}
|
||||
onCreateFromTemplate={onCreateFromTemplate}
|
||||
defaultAppMode={defaultAppMode}
|
||||
/>
|
||||
|
||||
@ -2,7 +2,6 @@ import type { DeploymentEdition } from '@dify/contracts/api/console/system-featu
|
||||
import type { RenderOptions } from '@testing-library/react'
|
||||
import type { MockedFunction } from 'vite-plus/test'
|
||||
import { fireEvent, screen } from '@testing-library/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { defaultPlan } from '@/app/components/billing/config'
|
||||
import { useProviderContext as actualUseProviderContext } from '@/context/provider-context'
|
||||
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
@ -51,7 +50,6 @@ const defaultProviderContext = {
|
||||
isFetchedPlanInfo: false,
|
||||
enableBilling: false,
|
||||
enableSkill: false,
|
||||
onPlanInfoChanged: noop,
|
||||
enableReplaceWebAppLogo: false,
|
||||
modelLoadBalancingEnabled: false,
|
||||
enableEducationPlan: false,
|
||||
|
||||
@ -185,14 +185,6 @@ vi.mock('@/context/permission-state', async () => {
|
||||
return createPermissionStateModuleMock(() => mockConsoleState)
|
||||
})
|
||||
|
||||
// Mock provider context
|
||||
const mockOnPlanInfoChanged = vi.fn()
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
onPlanInfoChanged: mockOnPlanInfoChanged,
|
||||
}),
|
||||
}))
|
||||
|
||||
// systemFeatures is seeded into the QueryClient via the local render helper.
|
||||
|
||||
vi.mock('@/service/apps', () => ({
|
||||
@ -1163,25 +1155,6 @@ describe('AppCard', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onPlanInfoChanged after successful duplication', async () => {
|
||||
render(<AppCard app={mockApp} />)
|
||||
|
||||
fireEvent.click(getOperationsTrigger())
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('app.duplicate'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('duplicate-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('confirm-duplicate-modal'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnPlanInfoChanged).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle copy failure', async () => {
|
||||
mockCopyApp.mockRejectedValueOnce(new Error('Copy failed'))
|
||||
|
||||
|
||||
@ -131,12 +131,6 @@ vi.mock('@/context/permission-state', async () => {
|
||||
workspacePermissionKeys: mockWorkspacePermissionKeys,
|
||||
}))
|
||||
})
|
||||
const mockOnPlanInfoChanged = vi.fn()
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
onPlanInfoChanged: mockOnPlanInfoChanged,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useMembers: () => ({
|
||||
@ -288,11 +282,9 @@ vi.mock('@/next/dynamic', () => ({
|
||||
return function MockCreateFromDSLModal({
|
||||
show,
|
||||
onClose,
|
||||
onSuccess,
|
||||
}: {
|
||||
show: boolean
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
}) {
|
||||
if (!show) return null
|
||||
return React.createElement(
|
||||
@ -305,7 +297,7 @@ vi.mock('@/next/dynamic', () => ({
|
||||
),
|
||||
React.createElement(
|
||||
'button',
|
||||
{ onClick: onSuccess, 'data-testid': 'success-dsl-modal' },
|
||||
{ onClick: onClose, 'data-testid': 'success-dsl-modal' },
|
||||
'Success',
|
||||
),
|
||||
)
|
||||
@ -315,12 +307,10 @@ vi.mock('@/next/dynamic', () => ({
|
||||
return function MockCreateAppModal({
|
||||
show,
|
||||
onClose,
|
||||
onSuccess,
|
||||
onCreateFromTemplate,
|
||||
}: {
|
||||
show: boolean
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
onCreateFromTemplate: () => void
|
||||
}) {
|
||||
if (!show) return null
|
||||
@ -334,7 +324,7 @@ vi.mock('@/next/dynamic', () => ({
|
||||
),
|
||||
React.createElement(
|
||||
'button',
|
||||
{ onClick: onSuccess, 'data-testid': 'success-create-modal' },
|
||||
{ onClick: onClose, 'data-testid': 'success-create-modal' },
|
||||
'Success',
|
||||
),
|
||||
React.createElement(
|
||||
@ -349,12 +339,10 @@ vi.mock('@/next/dynamic', () => ({
|
||||
return function MockCreateAppTemplateDialog({
|
||||
show,
|
||||
onClose,
|
||||
onSuccess,
|
||||
onCreateFromBlank,
|
||||
}: {
|
||||
show: boolean
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
onCreateFromBlank: () => void
|
||||
}) {
|
||||
if (!show) return null
|
||||
@ -368,7 +356,7 @@ vi.mock('@/next/dynamic', () => ({
|
||||
),
|
||||
React.createElement(
|
||||
'button',
|
||||
{ onClick: onSuccess, 'data-testid': 'success-template-dialog' },
|
||||
{ onClick: onClose, 'data-testid': 'success-template-dialog' },
|
||||
'Success',
|
||||
),
|
||||
React.createElement(
|
||||
|
||||
@ -51,7 +51,6 @@ import {
|
||||
useStepByStepTourControlledDropdown,
|
||||
} from '@/app/components/step-by-step-tour/dropdown-menu'
|
||||
import { workspacePermissionKeysAtom } from '@/context/permission-state'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||
@ -276,7 +275,6 @@ export function AppCardInteractions({
|
||||
})
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const isRbacEnabled = systemFeatures.rbac_enabled
|
||||
const { onPlanInfoChanged } = useProviderContext()
|
||||
const { push } = useRouter()
|
||||
const { mutate: copyApp } = useMutation(consoleQuery.apps.byAppId.copy.post.mutationOptions())
|
||||
const { mutateAsync: updateApp } = useMutation(consoleQuery.apps.byAppId.put.mutationOptions())
|
||||
@ -329,7 +327,6 @@ export function AppCardInteractions({
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t(($) => $.appDeleted, { ns: 'app' }))
|
||||
onPlanInfoChanged()
|
||||
setActiveDialog(null)
|
||||
setConfirmDeleteInput('')
|
||||
},
|
||||
@ -345,7 +342,7 @@ export function AppCardInteractions({
|
||||
const message = error instanceof Error ? error.message : ''
|
||||
toast.error(`${t(($) => $.appDeleteFailed, { ns: 'app' })}${message ? `: ${message}` : ''}`)
|
||||
}
|
||||
}, [app.id, deleteApp, onPlanInfoChanged, t])
|
||||
}, [app.id, deleteApp, t])
|
||||
|
||||
const onDeleteDialogOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
@ -461,7 +458,6 @@ export function AppCardInteractions({
|
||||
|
||||
setActiveDialog(null)
|
||||
toast.success(t(($) => $['newApp.appCreated'], { ns: 'app' }))
|
||||
onPlanInfoChanged()
|
||||
getRedirection(newApp, push, {
|
||||
currentUserId,
|
||||
resourceMaintainer: newApp.maintainer ?? undefined,
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
import type { AppListUrlQuery } from './query-params'
|
||||
import { zPostAppsBody } from '@dify/contracts/api/console/apps/zod.gen'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import dynamic from '@/next/dynamic'
|
||||
|
||||
type AppListCategory = AppListUrlQuery['category']
|
||||
@ -38,40 +37,24 @@ export function AppListCreationModals({
|
||||
onOpenBlank: () => void
|
||||
onOpenTemplate: () => void
|
||||
}) {
|
||||
const { onPlanInfoChanged } = useProviderContext()
|
||||
|
||||
if (!canCreateApp) return null
|
||||
const defaultAppModeResult = zPostAppsBody.shape.mode.safeParse(category)
|
||||
|
||||
return (
|
||||
<>
|
||||
{dialog?.type === 'dsl' && (
|
||||
<CreateFromDSLModal
|
||||
show
|
||||
onClose={onClose}
|
||||
onSuccess={() => {
|
||||
onClose()
|
||||
onPlanInfoChanged()
|
||||
}}
|
||||
droppedFile={dialog.droppedFile}
|
||||
/>
|
||||
<CreateFromDSLModal show onClose={onClose} droppedFile={dialog.droppedFile} />
|
||||
)}
|
||||
{dialog?.type === 'blank' && (
|
||||
<CreateAppModal
|
||||
show
|
||||
onClose={onClose}
|
||||
onSuccess={onPlanInfoChanged}
|
||||
onCreateFromTemplate={onOpenTemplate}
|
||||
defaultAppMode={defaultAppModeResult.success ? defaultAppModeResult.data : undefined}
|
||||
/>
|
||||
)}
|
||||
{dialog?.type === 'template' && (
|
||||
<CreateAppTemplateDialog
|
||||
show
|
||||
onClose={onClose}
|
||||
onSuccess={onPlanInfoChanged}
|
||||
onCreateFromBlank={onOpenBlank}
|
||||
/>
|
||||
<CreateAppTemplateDialog show onClose={onClose} onCreateFromBlank={onOpenBlank} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@ -218,7 +218,6 @@ vi.mock('@/context/provider-context', () => ({
|
||||
hasSettedApiKey: true,
|
||||
plan: { type: 'free' },
|
||||
enableBilling: false,
|
||||
onPlanInfoChanged: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
|
||||
@ -60,11 +60,6 @@ export const ProviderContextProvider = ({ children }: ProviderContextProviderPro
|
||||
queryClient.invalidateQueries({ queryKey: commonQueryKeys.modelProviderDetails }),
|
||||
]).then(() => undefined)
|
||||
|
||||
const refreshFeatures = () =>
|
||||
queryClient
|
||||
.invalidateQueries({ queryKey: consoleQuery.features.get.key() })
|
||||
.then(() => undefined)
|
||||
|
||||
// #region Zendesk conversation fields
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.PLAN && plan.type) {
|
||||
@ -99,7 +94,6 @@ export const ProviderContextProvider = ({ children }: ProviderContextProviderPro
|
||||
isFetchedPlanInfo,
|
||||
enableBilling,
|
||||
enableSkill,
|
||||
onPlanInfoChanged: refreshFeatures,
|
||||
enableReplaceWebAppLogo,
|
||||
modelLoadBalancingEnabled,
|
||||
enableEducationPlan,
|
||||
|
||||
@ -8,7 +8,6 @@ import type {
|
||||
import type { UsagePlanInfo, UsageResetInfo } from '@/app/components/billing/type'
|
||||
import type { Model } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { RETRIEVE_METHOD } from '@/types/app'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { createContext, useContext, useContextSelector } from 'use-context-selector'
|
||||
import { defaultPlan } from '@/app/components/billing/config'
|
||||
|
||||
@ -31,7 +30,6 @@ export type ProviderContextState = {
|
||||
isFetchedPlanInfo: boolean
|
||||
enableBilling: boolean
|
||||
enableSkill: boolean
|
||||
onPlanInfoChanged: () => void
|
||||
enableReplaceWebAppLogo: boolean
|
||||
modelLoadBalancingEnabled: boolean
|
||||
enableEducationPlan: boolean
|
||||
@ -55,7 +53,6 @@ export const baseProviderContextValue: ProviderContextState = {
|
||||
isFetchedPlanInfo: false,
|
||||
enableBilling: false,
|
||||
enableSkill: false,
|
||||
onPlanInfoChanged: noop,
|
||||
enableReplaceWebAppLogo: false,
|
||||
modelLoadBalancingEnabled: false,
|
||||
enableEducationPlan: false,
|
||||
|
||||
@ -85,28 +85,6 @@ export const updateAppInfo = ({
|
||||
return put<AppDetailResponse>(`apps/${appID}`, { body })
|
||||
}
|
||||
|
||||
export const copyApp = ({
|
||||
appID,
|
||||
name,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
mode,
|
||||
description,
|
||||
}: {
|
||||
appID: string
|
||||
name: string
|
||||
icon_type: AppIconType
|
||||
icon: string
|
||||
icon_background?: string | null
|
||||
mode: AppModeEnum
|
||||
description?: string
|
||||
}): Promise<AppDetailResponse> => {
|
||||
return post<AppDetailResponse>(`apps/${appID}/copy`, {
|
||||
body: { name, icon_type, icon, icon_background, mode, description },
|
||||
})
|
||||
}
|
||||
|
||||
export const exportAppConfig = ({
|
||||
appID,
|
||||
include = false,
|
||||
|
||||
@ -778,39 +778,51 @@ describe('consoleQuery app mutation defaults', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should invalidate app lists without blocking a directly completed import', async () => {
|
||||
const consoleQuery = await loadConsoleQuery()
|
||||
const queryClient = new QueryClient()
|
||||
const invalidateQueries = vi
|
||||
.spyOn(queryClient, 'invalidateQueries')
|
||||
.mockImplementation(() => new Promise(() => {}))
|
||||
const mutationOptions = consoleQuery.apps.imports.post.mutationOptions()
|
||||
it.each([undefined, 'existing-app'])(
|
||||
'should refresh completed import data without blocking (overwrite: %s)',
|
||||
async (appId) => {
|
||||
const consoleQuery = await loadConsoleQuery()
|
||||
const queryClient = new QueryClient()
|
||||
const invalidateQueries = vi
|
||||
.spyOn(queryClient, 'invalidateQueries')
|
||||
.mockImplementation(() => new Promise(() => {}))
|
||||
const mutationOptions = consoleQuery.apps.imports.post.mutationOptions()
|
||||
|
||||
const result = mutationOptions.onSuccess?.(
|
||||
{
|
||||
id: 'import-1',
|
||||
status: 'completed',
|
||||
app_id: 'app-1',
|
||||
current_dsl_version: '',
|
||||
imported_dsl_version: '',
|
||||
error: '',
|
||||
},
|
||||
{ body: { mode: 'yaml-content', yaml_content: 'app: demo' } },
|
||||
undefined,
|
||||
createMutationContext(queryClient),
|
||||
)
|
||||
const result = mutationOptions.onSuccess?.(
|
||||
{
|
||||
id: 'import-1',
|
||||
status: 'completed',
|
||||
app_id: 'app-1',
|
||||
current_dsl_version: '',
|
||||
imported_dsl_version: '',
|
||||
error: '',
|
||||
},
|
||||
{ body: { mode: 'yaml-content', yaml_content: 'app: demo', app_id: appId } },
|
||||
undefined,
|
||||
createMutationContext(queryClient),
|
||||
)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.apps.get.key(),
|
||||
})
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.apps.starred.get.key(),
|
||||
})
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.apps.recent.get.key(),
|
||||
})
|
||||
})
|
||||
if (appId) {
|
||||
expect(invalidateQueries).not.toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.features.get.key(),
|
||||
})
|
||||
} else {
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.features.get.key(),
|
||||
})
|
||||
}
|
||||
expect(result).toBeUndefined()
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.apps.get.key(),
|
||||
})
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.apps.starred.get.key(),
|
||||
})
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.apps.recent.get.key(),
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
it('should keep app lists intact while an import awaits confirmation', async () => {
|
||||
const consoleQuery = await loadConsoleQuery()
|
||||
@ -873,16 +885,20 @@ describe('consoleQuery app mutation defaults', () => {
|
||||
expect(synchronized).toBe(true)
|
||||
})
|
||||
|
||||
it('should keep a delete mutation pending until every app list synchronizes', async () => {
|
||||
it('should wait for deleted app lists to synchronize without waiting for quota', async () => {
|
||||
const consoleQuery = await loadConsoleQuery()
|
||||
const queryClient = new QueryClient()
|
||||
const invalidationResolvers: Array<() => void> = []
|
||||
const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries').mockImplementation(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
const invalidateQueries = vi
|
||||
.spyOn(queryClient, 'invalidateQueries')
|
||||
.mockImplementation((filters) => {
|
||||
if (JSON.stringify(filters?.queryKey) === JSON.stringify(consoleQuery.features.get.key()))
|
||||
return new Promise<void>(() => {})
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
invalidationResolvers.push(resolve)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
const mutationOptions = consoleQuery.apps.byAppId.delete.mutationOptions()
|
||||
|
||||
const synchronization = mutationOptions.onSuccess?.(
|
||||
@ -907,7 +923,8 @@ describe('consoleQuery app mutation defaults', () => {
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.apps.recent.get.key(),
|
||||
})
|
||||
expect(invalidateQueries).toHaveBeenCalledTimes(3)
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: consoleQuery.features.get.key() })
|
||||
expect(invalidateQueries).toHaveBeenCalledTimes(4)
|
||||
|
||||
invalidationResolvers.forEach((resolve) => resolve())
|
||||
await synchronization
|
||||
|
||||
@ -583,6 +583,7 @@ export const consoleQuery: RouterUtils<typeof consoleClient> = createTanstackQue
|
||||
post: {
|
||||
mutationOptions: {
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
void context.client.invalidateQueries({ queryKey: consoleQuery.features.get.key() })
|
||||
void context.client.invalidateQueries({ queryKey: consoleQuery.apps.get.key() })
|
||||
void context.client.invalidateQueries({
|
||||
queryKey: consoleQuery.apps.starred.get.key(),
|
||||
@ -596,9 +597,14 @@ export const consoleQuery: RouterUtils<typeof consoleClient> = createTanstackQue
|
||||
imports: {
|
||||
post: {
|
||||
mutationOptions: {
|
||||
onSuccess: (data, _variables, _onMutateResult, context) => {
|
||||
onSuccess: (data, variables, _onMutateResult, context) => {
|
||||
if (data.status !== 'completed' && data.status !== 'completed-with-warnings') return
|
||||
|
||||
if (!variables.body.app_id) {
|
||||
void context.client.invalidateQueries({
|
||||
queryKey: consoleQuery.features.get.key(),
|
||||
})
|
||||
}
|
||||
void context.client.invalidateQueries({ queryKey: consoleQuery.apps.get.key() })
|
||||
void context.client.invalidateQueries({
|
||||
queryKey: consoleQuery.apps.starred.get.key(),
|
||||
@ -617,6 +623,9 @@ export const consoleQuery: RouterUtils<typeof consoleClient> = createTanstackQue
|
||||
if (data.status !== 'completed' && data.status !== 'completed-with-warnings')
|
||||
return
|
||||
|
||||
void context.client.invalidateQueries({
|
||||
queryKey: consoleQuery.features.get.key(),
|
||||
})
|
||||
void context.client.invalidateQueries({
|
||||
queryKey: consoleQuery.apps.get.key(),
|
||||
})
|
||||
@ -651,8 +660,9 @@ export const consoleQuery: RouterUtils<typeof consoleClient> = createTanstackQue
|
||||
},
|
||||
delete: {
|
||||
mutationOptions: {
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) =>
|
||||
Promise.all([
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
void context.client.invalidateQueries({ queryKey: consoleQuery.features.get.key() })
|
||||
return Promise.all([
|
||||
context.client.invalidateQueries({ queryKey: consoleQuery.apps.get.key() }),
|
||||
context.client.invalidateQueries({
|
||||
queryKey: consoleQuery.apps.starred.get.key(),
|
||||
@ -660,7 +670,8 @@ export const consoleQuery: RouterUtils<typeof consoleClient> = createTanstackQue
|
||||
context.client.invalidateQueries({
|
||||
queryKey: consoleQuery.apps.recent.get.key(),
|
||||
}),
|
||||
]),
|
||||
])
|
||||
},
|
||||
},
|
||||
},
|
||||
put: {
|
||||
@ -732,7 +743,12 @@ export const consoleQuery: RouterUtils<typeof consoleClient> = createTanstackQue
|
||||
copy: {
|
||||
post: {
|
||||
mutationOptions: {
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
onSuccess: (data, _variables, _onMutateResult, context) => {
|
||||
if (!('mode' in data)) return
|
||||
|
||||
void context.client.invalidateQueries({
|
||||
queryKey: consoleQuery.features.get.key(),
|
||||
})
|
||||
void context.client.invalidateQueries({ queryKey: consoleQuery.apps.get.key() })
|
||||
void context.client.invalidateQueries({
|
||||
queryKey: consoleQuery.apps.starred.get.key(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user