mirror of
https://github.com/langgenius/dify.git
synced 2026-03-06 07:35:14 +00:00
feat(web): add base AlertDialog with app-card migration example (#32933)
Signed-off-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -222,6 +222,7 @@ mise.toml
|
||||
|
||||
# AI Assistant
|
||||
.roo/
|
||||
/.claude/worktrees/
|
||||
api/.env.backup
|
||||
/clickzetta
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import AppCard from '@/app/components/apps/app-card'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||
import { exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
let mockIsCurrentWorkspaceEditor = true
|
||||
@@ -26,6 +26,8 @@ let mockSystemFeatures = {
|
||||
const mockRouterPush = vi.fn()
|
||||
const mockNotify = vi.fn()
|
||||
const mockOnPlanInfoChanged = vi.fn()
|
||||
const mockDeleteAppMutation = vi.fn().mockResolvedValue(undefined)
|
||||
let mockDeleteMutationPending = false
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
@@ -117,6 +119,13 @@ vi.mock('@/service/tag', () => ({
|
||||
fetchTagList: vi.fn().mockResolvedValue([]),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
useDeleteAppMutation: () => ({
|
||||
mutateAsync: mockDeleteAppMutation,
|
||||
isPending: mockDeleteMutationPending,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/apps', () => ({
|
||||
deleteApp: vi.fn().mockResolvedValue({}),
|
||||
updateAppInfo: vi.fn().mockResolvedValue({}),
|
||||
@@ -270,6 +279,7 @@ const renderAppCard = (app?: Partial<App>) => {
|
||||
describe('App Card Operations Flow', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockDeleteMutationPending = false
|
||||
mockIsCurrentWorkspaceEditor = true
|
||||
mockSystemFeatures = {
|
||||
branding: { enabled: false },
|
||||
@@ -341,7 +351,7 @@ describe('App Card Operations Flow', () => {
|
||||
fireEvent.click(confirmBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteApp).toHaveBeenCalledWith('app-to-delete')
|
||||
expect(mockDeleteAppMutation).toHaveBeenCalledWith('app-to-delete')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,10 @@ vi.mock('@/service/use-apps', () => ({
|
||||
error: mockError,
|
||||
refetch: mockRefetch,
|
||||
}),
|
||||
useDeleteAppMutation: () => ({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-pay', () => ({
|
||||
|
||||
@@ -91,6 +91,10 @@ vi.mock('@/service/use-apps', () => ({
|
||||
error: null,
|
||||
refetch: mockRefetch,
|
||||
}),
|
||||
useDeleteAppMutation: () => ({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-pay', () => ({
|
||||
|
||||
@@ -63,6 +63,15 @@ vi.mock('@/service/apps', () => ({
|
||||
exportAppConfig: vi.fn(() => Promise.resolve({ data: 'yaml: content' })),
|
||||
}))
|
||||
|
||||
const mockDeleteAppMutation = vi.fn(() => Promise.resolve())
|
||||
let mockDeleteMutationPending = false
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
useDeleteAppMutation: () => ({
|
||||
mutateAsync: mockDeleteAppMutation,
|
||||
isPending: mockDeleteMutationPending,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
fetchWorkflowDraft: vi.fn(() => Promise.resolve({ environment_variables: [] })),
|
||||
}))
|
||||
@@ -146,13 +155,6 @@ vi.mock('next/dynamic', () => ({
|
||||
return React.createElement('div', { 'data-testid': 'switch-modal' }, React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-switch-modal' }, 'Close'), React.createElement('button', { 'onClick': onSuccess, 'data-testid': 'confirm-switch-modal' }, 'Switch'))
|
||||
}
|
||||
}
|
||||
if (fnString.includes('base/confirm')) {
|
||||
return function MockConfirm({ isShow, onCancel, onConfirm }: { isShow: boolean, onCancel: () => void, onConfirm: () => void }) {
|
||||
if (!isShow)
|
||||
return null
|
||||
return React.createElement('div', { 'data-testid': 'confirm-dialog' }, React.createElement('button', { 'onClick': onCancel, 'data-testid': 'cancel-confirm' }, 'Cancel'), React.createElement('button', { 'onClick': onConfirm, 'data-testid': 'confirm-confirm' }, 'Confirm'))
|
||||
}
|
||||
}
|
||||
if (fnString.includes('dsl-export-confirm-modal')) {
|
||||
return function MockDSLExportModal({ onClose, onConfirm }: { onClose?: () => void, onConfirm?: (withSecrets: boolean) => void }) {
|
||||
return React.createElement('div', { 'data-testid': 'dsl-export-modal' }, React.createElement('button', { 'onClick': () => onClose?.(), 'data-testid': 'close-dsl-export' }, 'Close'), React.createElement('button', { 'onClick': () => onConfirm?.(true), 'data-testid': 'confirm-dsl-export' }, 'Export with secrets'), React.createElement('button', { 'onClick': () => onConfirm?.(false), 'data-testid': 'confirm-dsl-export-no-secrets' }, 'Export without secrets'))
|
||||
@@ -235,6 +237,7 @@ describe('AppCard', () => {
|
||||
vi.clearAllMocks()
|
||||
mockOpenAsyncWindow.mockReset()
|
||||
mockWebappAuthEnabled = false
|
||||
mockDeleteMutationPending = false
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
@@ -461,35 +464,19 @@ describe('AppCard', () => {
|
||||
render(<AppCard app={mockApp} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
|
||||
await waitFor(() => {
|
||||
const deleteButton = screen.getByText('common.operation.delete')
|
||||
fireEvent.click(deleteButton)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
|
||||
})
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'common.operation.delete' }))
|
||||
expect(await screen.findByRole('alertdialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close confirm dialog when cancel is clicked', async () => {
|
||||
render(<AppCard app={mockApp} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'common.operation.delete' }))
|
||||
expect(await screen.findByRole('alertdialog')).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
await waitFor(() => {
|
||||
const deleteButton = screen.getByText('common.operation.delete')
|
||||
fireEvent.click(deleteButton)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('cancel-confirm'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -554,59 +541,41 @@ describe('AppCard', () => {
|
||||
|
||||
// Open popover and click delete
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('common.operation.delete'))
|
||||
})
|
||||
|
||||
// Confirm delete
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('confirm-confirm'))
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'common.operation.delete' }))
|
||||
expect(await screen.findByRole('alertdialog')).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(appsService.deleteApp).toHaveBeenCalled()
|
||||
expect(mockDeleteAppMutation).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onRefresh after successful delete', async () => {
|
||||
it('should not call onRefresh after successful delete', async () => {
|
||||
render(<AppCard app={mockApp} onRefresh={mockOnRefresh} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('common.operation.delete'))
|
||||
})
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'common.operation.delete' }))
|
||||
expect(await screen.findByRole('alertdialog')).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('confirm-confirm'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnRefresh).toHaveBeenCalled()
|
||||
expect(mockDeleteAppMutation).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockOnRefresh).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle delete failure', async () => {
|
||||
(appsService.deleteApp as Mock).mockRejectedValueOnce(new Error('Delete failed'))
|
||||
;(mockDeleteAppMutation as Mock).mockRejectedValueOnce(new Error('Delete failed'))
|
||||
|
||||
render(<AppCard app={mockApp} onRefresh={mockOnRefresh} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('common.operation.delete'))
|
||||
})
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'common.operation.delete' }))
|
||||
expect(await screen.findByRole('alertdialog')).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('confirm-confirm'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(appsService.deleteApp).toHaveBeenCalled()
|
||||
expect(mockDeleteAppMutation).toHaveBeenCalled()
|
||||
expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: expect.stringContaining('Delete failed') })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -106,6 +106,10 @@ vi.mock('@/service/use-apps', () => ({
|
||||
error: mockServiceState.error,
|
||||
refetch: mockRefetch,
|
||||
}),
|
||||
useDeleteAppMutation: () => ({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/tag', () => ({
|
||||
|
||||
@@ -20,6 +20,15 @@ import CustomPopover from '@/app/components/base/popover'
|
||||
import TagSelector from '@/app/components/base/tag-management/selector'
|
||||
import Toast, { ToastContext } from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
AlertDialogCancelButton,
|
||||
AlertDialogConfirmButton,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
} from '@/app/components/base/ui/alert-dialog'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
@@ -27,8 +36,9 @@ import { useProviderContext } from '@/context/provider-context'
|
||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { useGetUserCanAccessApp } from '@/service/access-control'
|
||||
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||
import { copyApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||
import { fetchInstalledAppList } from '@/service/explore'
|
||||
import { useDeleteAppMutation } from '@/service/use-apps'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
@@ -46,9 +56,6 @@ const DuplicateAppModal = dynamic(() => import('@/app/components/app/duplicate-m
|
||||
const SwitchAppModal = dynamic(() => import('@/app/components/app/switch-app-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const Confirm = dynamic(() => import('@/app/components/base/confirm'), {
|
||||
ssr: false,
|
||||
})
|
||||
const DSLExportConfirmModal = dynamic(() => import('@/app/components/workflow/dsl-export-confirm-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
@@ -76,13 +83,12 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const [showAccessControl, setShowAccessControl] = useState(false)
|
||||
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
||||
const { mutateAsync: mutateDeleteApp, isPending: isDeleting } = useDeleteAppMutation()
|
||||
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
try {
|
||||
await deleteApp(app.id)
|
||||
await mutateDeleteApp(app.id)
|
||||
notify({ type: 'success', message: t('appDeleted', { ns: 'app' }) })
|
||||
if (onRefresh)
|
||||
onRefresh()
|
||||
onPlanInfoChanged()
|
||||
}
|
||||
catch (e: any) {
|
||||
@@ -91,8 +97,17 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
message: `${t('appDeleteFailed', { ns: 'app' })}${'message' in e ? `: ${e.message}` : ''}`,
|
||||
})
|
||||
}
|
||||
setShowConfirmDelete(false)
|
||||
}, [app.id, notify, onPlanInfoChanged, onRefresh, t])
|
||||
finally {
|
||||
setShowConfirmDelete(false)
|
||||
}
|
||||
}, [app.id, mutateDeleteApp, notify, onPlanInfoChanged, t])
|
||||
|
||||
const onDeleteDialogOpenChange = useCallback((open: boolean) => {
|
||||
if (isDeleting)
|
||||
return
|
||||
|
||||
setShowConfirmDelete(open)
|
||||
}, [isDeleting])
|
||||
|
||||
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
|
||||
name,
|
||||
@@ -438,7 +453,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
<div
|
||||
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-md"
|
||||
>
|
||||
<RiMoreFill className="h-4 w-4 text-text-tertiary" />
|
||||
<span className="sr-only">{t('operation.more', { ns: 'common' })}</span>
|
||||
<RiMoreFill aria-hidden className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
)}
|
||||
btnClassName={open =>
|
||||
@@ -495,15 +511,26 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
onSuccess={onSwitch}
|
||||
/>
|
||||
)}
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('deleteAppConfirmTitle', { ns: 'app' })}
|
||||
content={t('deleteAppConfirmContent', { ns: 'app' })}
|
||||
isShow={showConfirmDelete}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
/>
|
||||
)}
|
||||
<AlertDialog open={showConfirmDelete} onOpenChange={onDeleteDialogOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<div className="flex flex-col gap-2 px-6 pb-4 pt-6">
|
||||
<AlertDialogTitle className="text-text-primary title-2xl-semi-bold">
|
||||
{t('deleteAppConfirmTitle', { ns: 'app' })}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="w-full whitespace-pre-wrap break-words text-text-tertiary system-md-regular">
|
||||
{t('deleteAppConfirmContent', { ns: 'app' })}
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton disabled={isDeleting}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton loading={isDeleting} disabled={isDeleting} onClick={onConfirmDelete}>
|
||||
{t('operation.confirm', { ns: 'common' })}
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
{secretEnvList.length > 0 && (
|
||||
<DSLExportConfirmModal
|
||||
envList={secretEnvList}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @deprecated Use `@/app/components/base/ui/alert-dialog` instead.
|
||||
* See issue #32767 for migration details.
|
||||
*/
|
||||
|
||||
import * as React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
@@ -5,6 +10,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '../button'
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
/** @deprecated Use `@/app/components/base/ui/alert-dialog` instead. */
|
||||
export type IConfirm = {
|
||||
className?: string
|
||||
isShow: boolean
|
||||
|
||||
145
web/app/components/base/ui/alert-dialog/__tests__/index.spec.tsx
Normal file
145
web/app/components/base/ui/alert-dialog/__tests__/index.spec.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
AlertDialogCancelButton,
|
||||
AlertDialogClose,
|
||||
AlertDialogConfirmButton,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '../index'
|
||||
|
||||
describe('AlertDialog wrapper', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render alert dialog content when dialog is open', () => {
|
||||
render(
|
||||
<AlertDialog open>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
|
||||
<AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>,
|
||||
)
|
||||
|
||||
const dialog = screen.getByRole('alertdialog')
|
||||
expect(dialog).toHaveTextContent('Confirm Delete')
|
||||
expect(dialog).toHaveTextContent('This action cannot be undone.')
|
||||
})
|
||||
|
||||
it('should not render content when dialog is closed', () => {
|
||||
render(
|
||||
<AlertDialog open={false}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>Hidden Title</AlertDialogTitle>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>,
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should apply custom className to popup', () => {
|
||||
render(
|
||||
<AlertDialog open>
|
||||
<AlertDialogContent className="custom-class">
|
||||
<AlertDialogTitle>Title</AlertDialogTitle>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>,
|
||||
)
|
||||
|
||||
const dialog = screen.getByRole('alertdialog')
|
||||
expect(dialog).toHaveClass('custom-class')
|
||||
})
|
||||
|
||||
it('should not render a close button by default', () => {
|
||||
render(
|
||||
<AlertDialog open>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>Title</AlertDialogTitle>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>,
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should open and close dialog when trigger and close are clicked', async () => {
|
||||
render(
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger>Open Dialog</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>Action Required</AlertDialogTitle>
|
||||
<AlertDialogDescription>Please confirm the action.</AlertDialogDescription>
|
||||
<AlertDialogClose>Cancel</AlertDialogClose>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>,
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open Dialog' }))
|
||||
expect(await screen.findByRole('alertdialog')).toHaveTextContent('Action Required')
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Composition Helpers', () => {
|
||||
it('should render actions wrapper and default confirm button styles', () => {
|
||||
render(
|
||||
<AlertDialog open>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>Action Required</AlertDialogTitle>
|
||||
<AlertDialogActions data-testid="actions" className="custom-actions">
|
||||
<AlertDialogConfirmButton>Confirm</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('actions')).toHaveClass('flex', 'items-start', 'justify-end', 'gap-2', 'self-stretch', 'p-6', 'custom-actions')
|
||||
const confirmButton = screen.getByRole('button', { name: 'Confirm' })
|
||||
expect(confirmButton).toHaveClass('btn-primary')
|
||||
expect(confirmButton).toHaveClass('btn-destructive')
|
||||
})
|
||||
|
||||
it('should keep dialog open after confirm click and close via cancel helper', async () => {
|
||||
const onConfirm = vi.fn()
|
||||
|
||||
render(
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger>Open Dialog</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>Action Required</AlertDialogTitle>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton>Cancel</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton onClick={onConfirm}>Confirm</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open Dialog' }))
|
||||
expect(await screen.findByRole('alertdialog')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Confirm' }))
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1)
|
||||
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
106
web/app/components/base/ui/alert-dialog/index.tsx
Normal file
106
web/app/components/base/ui/alert-dialog/index.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
'use client'
|
||||
|
||||
import type { ButtonProps } from '@/app/components/base/button'
|
||||
import { AlertDialog as BaseAlertDialog } from '@base-ui/react/alert-dialog'
|
||||
import * as React from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
// z-index strategy (relies on root `isolation: isolate` in layout.tsx):
|
||||
// All overlay primitives (Tooltip / Popover / Dropdown / Select / Dialog / AlertDialog) — z-50
|
||||
// Overlays share the same z-index; DOM order handles stacking when multiple are open.
|
||||
// This ensures overlays inside an AlertDialog (e.g. a Tooltip on a dialog button) render
|
||||
// above the dialog backdrop instead of being clipped by it.
|
||||
// Toast — z-[99], always on top (defined in toast component)
|
||||
|
||||
export const AlertDialog = BaseAlertDialog.Root
|
||||
export const AlertDialogTrigger = BaseAlertDialog.Trigger
|
||||
export const AlertDialogTitle = BaseAlertDialog.Title
|
||||
export const AlertDialogDescription = BaseAlertDialog.Description
|
||||
export const AlertDialogClose = BaseAlertDialog.Close
|
||||
|
||||
type AlertDialogContentProps = {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
overlayClassName?: string
|
||||
popupProps?: Omit<React.ComponentPropsWithoutRef<typeof BaseAlertDialog.Popup>, 'children' | 'className'>
|
||||
backdropProps?: Omit<React.ComponentPropsWithoutRef<typeof BaseAlertDialog.Backdrop>, 'className'>
|
||||
}
|
||||
|
||||
export function AlertDialogContent({
|
||||
children,
|
||||
className,
|
||||
overlayClassName,
|
||||
popupProps,
|
||||
backdropProps,
|
||||
}: AlertDialogContentProps) {
|
||||
return (
|
||||
<BaseAlertDialog.Portal>
|
||||
<BaseAlertDialog.Backdrop
|
||||
{...backdropProps}
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-background-overlay',
|
||||
'transition-opacity duration-150 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none',
|
||||
overlayClassName,
|
||||
)}
|
||||
/>
|
||||
<BaseAlertDialog.Popup
|
||||
{...popupProps}
|
||||
className={cn(
|
||||
'fixed left-1/2 top-1/2 z-50 max-h-[calc(100vh-2rem)] w-[480px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg',
|
||||
'transition-[transform,scale,opacity] duration-150 data-[ending-style]:scale-95 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</BaseAlertDialog.Popup>
|
||||
</BaseAlertDialog.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
type AlertDialogActionsProps = React.ComponentPropsWithoutRef<'div'>
|
||||
|
||||
export function AlertDialogActions({ className, ...props }: AlertDialogActionsProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn('flex items-start justify-end gap-2 self-stretch p-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type AlertDialogCancelButtonProps = Omit<ButtonProps, 'children'> & {
|
||||
children: React.ReactNode
|
||||
closeProps?: Omit<React.ComponentPropsWithoutRef<typeof BaseAlertDialog.Close>, 'children' | 'render'>
|
||||
}
|
||||
|
||||
export function AlertDialogCancelButton({
|
||||
children,
|
||||
closeProps,
|
||||
...buttonProps
|
||||
}: AlertDialogCancelButtonProps) {
|
||||
return (
|
||||
<BaseAlertDialog.Close
|
||||
{...closeProps}
|
||||
render={<Button {...buttonProps} />}
|
||||
>
|
||||
{children}
|
||||
</BaseAlertDialog.Close>
|
||||
)
|
||||
}
|
||||
|
||||
type AlertDialogConfirmButtonProps = ButtonProps
|
||||
|
||||
export function AlertDialogConfirmButton({
|
||||
variant = 'primary',
|
||||
destructive = true,
|
||||
...props
|
||||
}: AlertDialogConfirmButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
variant={variant}
|
||||
destructive={destructive}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
14
web/contract/console/apps.ts
Normal file
14
web/contract/console/apps.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { type } from '@orpc/contract'
|
||||
import { base } from '../base'
|
||||
|
||||
export const appDeleteContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}',
|
||||
method: 'DELETE',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
appId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { InferContractRouterInputs } from '@orpc/contract'
|
||||
import { appDeleteContract } from './console/apps'
|
||||
import { bindPartnerStackContract, invoicesContract } from './console/billing'
|
||||
import {
|
||||
exploreAppDetailContract,
|
||||
@@ -42,6 +43,9 @@ export type MarketPlaceInputs = InferContractRouterInputs<typeof marketplaceRout
|
||||
|
||||
export const consoleRouterContract = {
|
||||
systemFeatures: systemFeaturesContract,
|
||||
apps: {
|
||||
deleteApp: appDeleteContract,
|
||||
},
|
||||
explore: {
|
||||
apps: exploreAppsContract,
|
||||
appDetail: exploreAppDetailContract,
|
||||
|
||||
@@ -8,12 +8,14 @@ This document tracks the migration away from legacy overlay APIs.
|
||||
- `@/app/components/base/portal-to-follow-elem`
|
||||
- `@/app/components/base/tooltip`
|
||||
- `@/app/components/base/modal`
|
||||
- `@/app/components/base/confirm`
|
||||
- `@/app/components/base/select` (including `custom` / `pure`)
|
||||
- Replacement primitives:
|
||||
- `@/app/components/base/ui/tooltip`
|
||||
- `@/app/components/base/ui/dropdown-menu`
|
||||
- `@/app/components/base/ui/popover`
|
||||
- `@/app/components/base/ui/dialog`
|
||||
- `@/app/components/base/ui/alert-dialog`
|
||||
- `@/app/components/base/ui/select`
|
||||
- Tracking issue: https://github.com/langgenius/dify/issues/32767
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
},
|
||||
"app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
@@ -325,7 +325,7 @@
|
||||
},
|
||||
"app/components/app-sidebar/dataset-info/dropdown.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@@ -359,6 +359,11 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/app/annotation/batch-action.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/app/annotation/batch-add-annotation-modal/csv-downloader.spec.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
@@ -391,6 +396,11 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/app/annotation/clear-all-annotations-confirm-modal/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
@@ -408,6 +418,9 @@
|
||||
}
|
||||
},
|
||||
"app/components/app/annotation/edit-annotation-modal/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
}
|
||||
@@ -451,12 +464,20 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/app/annotation/remove-annotation-confirm-modal/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/app/annotation/view-annotation-modal/hit-history-no-data.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/app/annotation/view-annotation-modal/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 5
|
||||
},
|
||||
@@ -494,6 +515,9 @@
|
||||
}
|
||||
},
|
||||
"app/components/app/app-publisher/features-wrapper.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 4
|
||||
}
|
||||
@@ -602,7 +626,7 @@
|
||||
},
|
||||
"app/components/app/configuration/config-var/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/app/configuration/config-var/input-type-icon.tsx": {
|
||||
@@ -737,7 +761,7 @@
|
||||
},
|
||||
"app/components/app/configuration/config/automatic/get-automatic-res.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
},
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 4
|
||||
@@ -788,7 +812,7 @@
|
||||
},
|
||||
"app/components/app/configuration/config/code-generator/get-code-generator-res.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
},
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 4
|
||||
@@ -999,6 +1023,9 @@
|
||||
}
|
||||
},
|
||||
"app/components/app/configuration/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
},
|
||||
@@ -1189,7 +1216,7 @@
|
||||
},
|
||||
"app/components/app/overview/app-card.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
},
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 3
|
||||
@@ -1255,7 +1282,7 @@
|
||||
},
|
||||
"app/components/app/switch-app-modal/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
},
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
@@ -1527,13 +1554,16 @@
|
||||
}
|
||||
},
|
||||
"app/components/base/chat/chat-with-history/header-in-mobile.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/base/chat/chat-with-history/header/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
@@ -1585,6 +1615,9 @@
|
||||
}
|
||||
},
|
||||
"app/components/base/chat/chat-with-history/sidebar/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 2
|
||||
}
|
||||
@@ -2833,7 +2866,7 @@
|
||||
},
|
||||
"app/components/base/tag-management/tag-item-editor.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/base/tag-management/tag-remove-modal.tsx": {
|
||||
@@ -3174,7 +3207,7 @@
|
||||
},
|
||||
"app/components/datasets/create-from-pipeline/list/template-card/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/datasets/create-from-pipeline/list/template-card/operations.tsx": {
|
||||
@@ -3406,7 +3439,7 @@
|
||||
},
|
||||
"app/components/datasets/documents/components/operations.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/datasets/documents/components/rename-modal.tsx": {
|
||||
@@ -3675,6 +3708,9 @@
|
||||
}
|
||||
},
|
||||
"app/components/datasets/documents/detail/completed/common/batch-action.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 2
|
||||
}
|
||||
@@ -3768,7 +3804,7 @@
|
||||
},
|
||||
"app/components/datasets/documents/detail/completed/segment-card/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 3
|
||||
@@ -3860,7 +3896,7 @@
|
||||
},
|
||||
"app/components/datasets/external-api/external-api-modal/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 3
|
||||
},
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
@@ -3875,6 +3911,9 @@
|
||||
}
|
||||
},
|
||||
"app/components/datasets/external-api/external-knowledge-api-card/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
}
|
||||
@@ -4024,6 +4063,11 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"app/components/datasets/list/dataset-card/components/dataset-card-modals.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/datasets/list/dataset-card/components/description.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
@@ -4105,7 +4149,7 @@
|
||||
},
|
||||
"app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 3
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 5
|
||||
@@ -4258,7 +4302,7 @@
|
||||
},
|
||||
"app/components/develop/secret-key/secret-key-modal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/explore/banner/banner-item.tsx": {
|
||||
@@ -4306,6 +4350,11 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/explore/sidebar/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/explore/sidebar/no-apps/index.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 3
|
||||
@@ -4401,6 +4450,11 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/header/account-setting/api-based-extension-page/item.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/header/account-setting/api-based-extension-page/modal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@@ -4412,6 +4466,9 @@
|
||||
}
|
||||
},
|
||||
"app/components/header/account-setting/data-source-page-new/card.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 4
|
||||
},
|
||||
@@ -4636,7 +4693,7 @@
|
||||
},
|
||||
"app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 3
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 2
|
||||
@@ -4704,7 +4761,7 @@
|
||||
},
|
||||
"app/components/header/account-setting/model-provider-page/model-modal/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 6
|
||||
@@ -4861,7 +4918,7 @@
|
||||
},
|
||||
"app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
},
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
@@ -5190,7 +5247,7 @@
|
||||
},
|
||||
"app/components/plugins/plugin-auth/authorized/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 3
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 2
|
||||
@@ -5293,6 +5350,11 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/detail-header/components/header-modals.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/detail-header/components/plugin-source-badge.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@@ -5308,7 +5370,7 @@
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/endpoint-card.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 5
|
||||
@@ -5427,6 +5489,9 @@
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
},
|
||||
@@ -5556,7 +5621,7 @@
|
||||
},
|
||||
"app/components/plugins/plugin-item/action.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-item/index.tsx": {
|
||||
@@ -5755,6 +5820,9 @@
|
||||
}
|
||||
},
|
||||
"app/components/rag-pipeline/components/conversion.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 2
|
||||
}
|
||||
@@ -5913,6 +5981,9 @@
|
||||
}
|
||||
},
|
||||
"app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 8
|
||||
}
|
||||
@@ -6144,7 +6215,7 @@
|
||||
},
|
||||
"app/components/tools/mcp/detail/content.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 12
|
||||
@@ -6195,7 +6266,7 @@
|
||||
},
|
||||
"app/components/tools/mcp/mcp-service-card.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 3
|
||||
@@ -6210,6 +6281,9 @@
|
||||
}
|
||||
},
|
||||
"app/components/tools/mcp/provider-card.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 7
|
||||
},
|
||||
@@ -6246,6 +6320,9 @@
|
||||
}
|
||||
},
|
||||
"app/components/tools/provider/detail.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 10
|
||||
}
|
||||
@@ -7034,6 +7111,11 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/workflow/nodes/_base/components/remove-effect-var-confirm.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/workflow/nodes/_base/components/retry/retry-on-node.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
@@ -9572,6 +9654,9 @@
|
||||
}
|
||||
},
|
||||
"hooks/use-pay.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 4
|
||||
},
|
||||
@@ -9629,6 +9714,17 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"lib/utils.ts": {
|
||||
"import/consistent-type-specifier-style": {
|
||||
"count": 1
|
||||
},
|
||||
"perfectionist/sort-named-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"style/quotes": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"models/common.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
|
||||
@@ -189,6 +189,12 @@ export default antfu(
|
||||
'**/base/select/pure',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/select instead. See issue #32767.',
|
||||
}, {
|
||||
group: [
|
||||
'**/base/confirm',
|
||||
'**/base/confirm/index',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/alert-dialog instead. See issue #32767.',
|
||||
}],
|
||||
}],
|
||||
},
|
||||
|
||||
@@ -14,9 +14,11 @@ import type { App } from '@/types/app'
|
||||
import {
|
||||
keepPreviousData,
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import { consoleClient, consoleQuery } from '@/service/client'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { get, post } from './base'
|
||||
import { useInvalid } from './use-base'
|
||||
@@ -135,6 +137,29 @@ export const useInvalidateAppList = () => {
|
||||
}
|
||||
}
|
||||
|
||||
export const useDeleteAppMutation = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.apps.deleteApp.mutationKey(),
|
||||
mutationFn: (appId: string) => {
|
||||
return consoleClient.apps.deleteApp({
|
||||
params: { appId },
|
||||
})
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [NAME_SPACE, 'list'],
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: useAppFullListKey,
|
||||
}),
|
||||
])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const useAppStatisticsQuery = <T>(metric: string, appId: string, params?: DateRangeParams) => {
|
||||
return useQuery<T>({
|
||||
queryKey: [NAME_SPACE, 'statistics', metric, appId, params],
|
||||
|
||||
Reference in New Issue
Block a user