mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 01:48:04 +08:00
feat(web): add base AlertDialog with app-card migration example (#32933)
Signed-off-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user