diff --git a/web/app/components/app-sidebar/app-info/__tests__/app-info-detail-panel.spec.tsx b/web/app/components/app-sidebar/app-info/__tests__/app-info-detail-panel.spec.tsx
index 3082eb3789..44966f0ebe 100644
--- a/web/app/components/app-sidebar/app-info/__tests__/app-info-detail-panel.spec.tsx
+++ b/web/app/components/app-sidebar/app-info/__tests__/app-info-detail-panel.spec.tsx
@@ -11,22 +11,26 @@ vi.mock('../../../base/app-icon', () => ({
),
}))
-vi.mock('@/app/components/base/content-dialog', () => ({
- default: ({ show, onClose, children, className }: {
- show: boolean
- onClose: () => void
+vi.mock('@/app/components/base/ui/dialog', () => ({
+ Dialog: ({ open, onOpenChange, children }: {
+ open: boolean
+ onOpenChange: (open: boolean) => void
children: React.ReactNode
- className?: string
}) => (
- show
+ open
? (
-
-
+
+
{children}
)
: null
),
+ DialogPortal: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ DialogBackdrop: () =>
,
+ DialogPopup: ({ children, className }: { children: React.ReactNode, className?: string }) => (
+
{children}
+ ),
}))
vi.mock('@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view', () => ({
@@ -96,12 +100,12 @@ describe('AppInfoDetailPanel', () => {
describe('Rendering', () => {
it('should not render when show is false', () => {
render(
)
- expect(screen.queryByTestId('content-dialog')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('detail-drawer')).not.toBeInTheDocument()
})
it('should render dialog when show is true', () => {
render(
)
- expect(screen.getByTestId('content-dialog')).toBeInTheDocument()
+ expect(screen.getByTestId('detail-drawer')).toBeInTheDocument()
})
it('should display app name', () => {
diff --git a/web/app/components/app-sidebar/app-info/app-info-detail-panel.tsx b/web/app/components/app-sidebar/app-info/app-info-detail-panel.tsx
index 4aacc0cdb1..93e7b4c586 100644
--- a/web/app/components/app-sidebar/app-info/app-info-detail-panel.tsx
+++ b/web/app/components/app-sidebar/app-info/app-info-detail-panel.tsx
@@ -1,20 +1,17 @@
import type { Operation } from './app-operations'
import type { AppInfoModalType } from './use-app-info-actions'
import type { App, AppSSO } from '@/types/app'
-import {
- RiDeleteBinLine,
- RiEditLine,
- RiExchange2Line,
- RiFileCopy2Line,
- RiFileDownloadLine,
- RiFileUploadLine,
-} from '@remixicon/react'
import * as React from 'react'
-import { useMemo } from 'react'
+import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view'
import Button from '@/app/components/base/button'
-import ContentDialog from '@/app/components/base/content-dialog'
+import {
+ Dialog,
+ DialogBackdrop,
+ DialogPopup,
+ DialogPortal,
+} from '@/app/components/base/ui/dialog'
import { AppModeEnum } from '@/types/app'
import AppIcon from '../../base/app-icon'
import { getAppModeLabel } from './app-mode-labels'
@@ -37,23 +34,28 @@ const AppInfoDetailPanel = ({
}: AppInfoDetailPanelProps) => {
const { t } = useTranslation()
+ const handleOpenChange = useCallback((open: boolean) => {
+ if (!open)
+ onClose()
+ }, [onClose])
+
const primaryOperations = useMemo
(() => [
{
id: 'edit',
title: t('editApp', { ns: 'app' }),
- icon: ,
+ icon: ,
onClick: () => openModal('edit'),
},
{
id: 'duplicate',
title: t('duplicate', { ns: 'app' }),
- icon: ,
+ icon: ,
onClick: () => openModal('duplicate'),
},
{
id: 'export',
title: t('export', { ns: 'app' }),
- icon: ,
+ icon: ,
onClick: exportCheck,
},
], [t, openModal, exportCheck])
@@ -63,7 +65,7 @@ const AppInfoDetailPanel = ({
? [{
id: 'import',
title: t('common.importDSL', { ns: 'workflow' }),
- icon: ,
+ icon: ,
onClick: () => openModal('importDSL'),
}]
: [],
@@ -77,7 +79,7 @@ const AppInfoDetailPanel = ({
{
id: 'delete',
title: t('operation.delete', { ns: 'common' }),
- icon: ,
+ icon: ,
onClick: () => openModal('delete'),
},
], [appDetail.mode, t, openModal])
@@ -88,63 +90,64 @@ const AppInfoDetailPanel = ({
return {
id: 'switch',
title: t('switch', { ns: 'app' }),
- icon: ,
+ icon: ,
onClick: () => openModal('switch'),
}
}, [appDetail.mode, t, openModal])
return (
-
-
-
-
-
-
{appDetail.name}
-
- {getAppModeLabel(appDetail.mode, t)}
+
- {appDetail.description && (
-
- {appDetail.description}
-
- )}
-
-
-
- {switchOperation && (
-
-
-
- )}
-
+
+ {switchOperation && (
+
+
+
+ )}
+
+
+
)
}
diff --git a/web/app/components/app-sidebar/app-info/app-operations.tsx b/web/app/components/app-sidebar/app-info/app-operations.tsx
index e3cf233fea..b2a9bc97c8 100644
--- a/web/app/components/app-sidebar/app-info/app-operations.tsx
+++ b/web/app/components/app-sidebar/app-info/app-operations.tsx
@@ -1,8 +1,8 @@
import type { JSX } from 'react'
-import { RiMoreLine } from '@remixicon/react'
import { cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
+import { cn } from '@/utils/classnames'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
export type Operation = {
@@ -133,8 +133,8 @@ const AppOperations = ({
className="gap-px"
tabIndex={-1}
>
- {cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })}
-
+ {cloneElement(operation.icon, { className: cn(operation.icon.props.className, 'h-3.5 w-3.5 text-components-button-secondary-text') })}
+
{operation.title}
@@ -146,8 +146,8 @@ const AppOperations = ({
className="gap-px"
tabIndex={-1}
>
-
-
+
+
{t('operation.more', { ns: 'common' })}
@@ -162,8 +162,8 @@ const AppOperations = ({
className="gap-px"
onClick={operation.onClick}
>
- {cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })}
-
+ {cloneElement(operation.icon, { className: cn(operation.icon.props.className, 'h-3.5 w-3.5 text-components-button-secondary-text') })}
+
{operation.title}
@@ -181,8 +181,8 @@ const AppOperations = ({
variant="secondary"
className="gap-px"
>
-
-
+
+
{t('operation.more', { ns: 'common' })}
@@ -199,8 +199,8 @@ const AppOperations = ({
className="flex h-8 cursor-pointer items-center gap-x-1 rounded-lg p-1.5 hover:bg-state-base-hover"
onClick={item.onClick}
>
- {cloneElement(item.icon, { className: 'h-4 w-4 text-text-tertiary' })}
- {item.title}
+ {cloneElement(item.icon, { className: cn(item.icon.props.className, 'h-4 w-4 text-text-tertiary') })}
+ {item.title}
))}
diff --git a/web/app/components/base/content-dialog/__tests__/index.spec.tsx b/web/app/components/base/content-dialog/__tests__/index.spec.tsx
deleted file mode 100644
index e987d306a1..0000000000
--- a/web/app/components/base/content-dialog/__tests__/index.spec.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { render, screen } from '@testing-library/react'
-import userEvent from '@testing-library/user-event'
-import ContentDialog from '../index'
-
-describe('ContentDialog', () => {
- it('renders children when show is true', async () => {
- render(
-
- Dialog body
- ,
- )
-
- await screen.findByText('Dialog body')
- expect(screen.getByText('Dialog body')).toBeInTheDocument()
-
- const backdrop = document.querySelector('.bg-app-detail-overlay-bg')
- expect(backdrop).toBeTruthy()
- })
-
- it('does not render children when show is false', () => {
- render(
-
- Hidden content
- ,
- )
-
- expect(screen.queryByText('Hidden content')).toBeNull()
- expect(document.querySelector('.bg-app-detail-overlay-bg')).toBeNull()
- })
-
- it('calls onClose when backdrop is clicked', async () => {
- const onClose = vi.fn()
- render(
-
- Body
- ,
- )
-
- const user = userEvent.setup()
- const backdrop = document.querySelector('.bg-app-detail-overlay-bg') as HTMLElement | null
- expect(backdrop).toBeTruthy()
-
- await user.click(backdrop!)
- expect(onClose).toHaveBeenCalledTimes(1)
- })
-
- it('applies provided className to the content panel', () => {
- render(
-
- Panel content
- ,
- )
-
- const contentPanel = document.querySelector('.bg-app-detail-bg') as HTMLElement | null
- expect(contentPanel).toBeTruthy()
- expect(contentPanel?.className).toContain('my-panel-class')
- expect(screen.getByText('Panel content')).toBeInTheDocument()
- })
-})
diff --git a/web/app/components/base/content-dialog/index.stories.tsx b/web/app/components/base/content-dialog/index.stories.tsx
deleted file mode 100644
index 8ddd5c667d..0000000000
--- a/web/app/components/base/content-dialog/index.stories.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/nextjs-vite'
-import { useEffect, useState } from 'react'
-import ContentDialog from '.'
-
-type Props = React.ComponentProps
-
-const meta = {
- title: 'Base/Feedback/ContentDialog',
- component: ContentDialog,
- parameters: {
- layout: 'fullscreen',
- docs: {
- description: {
- component: 'Sliding panel overlay used in the app detail view. Includes dimmed backdrop and animated entrance/exit transitions.',
- },
- },
- },
- tags: ['autodocs'],
- argTypes: {
- className: {
- control: 'text',
- description: 'Additional classes applied to the sliding panel container.',
- },
- show: {
- control: 'boolean',
- description: 'Controls visibility of the dialog.',
- },
- onClose: {
- control: false,
- description: 'Invoked when the overlay/backdrop is clicked.',
- },
- children: {
- control: false,
- table: { disable: true },
- },
- },
- args: {
- show: false,
- children: null,
- },
-} satisfies Meta
-
-export default meta
-type Story = StoryObj
-
-const DemoWrapper = (props: Props) => {
- const [open, setOpen] = useState(props.show)
-
- useEffect(() => {
- setOpen(props.show)
- }, [props.show])
-
- return (
-
-
-
-
-
-
{
- props.onClose?.()
- setOpen(false)
- }}
- >
-
-
Plan summary
-
- Use this area to present rich content for the selected run, configuration details, or
- any supporting context.
-
-
- Scrollable placeholder content. Add domain-specific information, activity logs, or
- editors in the real application.
-
-
-
-
-
-
-
-
- )
-}
-
-export const Default: Story = {
- args: {
- children: null,
- },
- render: args => ,
-}
-
-export const NarrowPanel: Story = {
- render: args => ,
- args: {
- className: 'max-w-[420px]',
- children: null,
- },
- parameters: {
- docs: {
- description: {
- story: 'Applies a custom width class to show the dialog as a narrower information panel.',
- },
- },
- },
-}
diff --git a/web/app/components/base/content-dialog/index.tsx b/web/app/components/base/content-dialog/index.tsx
deleted file mode 100644
index e12365b691..0000000000
--- a/web/app/components/base/content-dialog/index.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { ReactNode } from 'react'
-import { Transition, TransitionChild } from '@headlessui/react'
-import { cn } from '@/utils/classnames'
-
-type ContentDialogProps = {
- className?: string
- show: boolean
- onClose?: () => void
- children: ReactNode
-}
-
-const ContentDialog = ({
- className,
- show,
- onClose,
- children,
-}: ContentDialogProps) => {
- return (
-
-
-
-
-
-
-
- {children}
-
-
-
- )
-}
-
-export default ContentDialog
diff --git a/web/app/components/base/ui/dialog/index.tsx b/web/app/components/base/ui/dialog/index.tsx
index 7b58be4cc5..3eef536142 100644
--- a/web/app/components/base/ui/dialog/index.tsx
+++ b/web/app/components/base/ui/dialog/index.tsx
@@ -42,6 +42,37 @@ export function DialogCloseButton({
)
}
+export function DialogBackdrop({
+ className,
+ ...props
+}: React.ComponentPropsWithoutRef) {
+ return (
+
+ )
+}
+
+export function DialogPopup({
+ className,
+ children,
+ ...props
+}: React.ComponentPropsWithoutRef) {
+ return (
+
+ {children}
+
+ )
+}
+
type DialogContentProps = {
children: React.ReactNode
className?: string
@@ -57,24 +88,16 @@ export function DialogContent({
}: DialogContentProps) {
return (
-
+
-
{children}
-
+
)
}
diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json
index d9098f426b..01f05b3e14 100644
--- a/web/eslint-suppressions.json
+++ b/web/eslint-suppressions.json
@@ -314,11 +314,6 @@
"count": 1
}
},
- "app/components/app-sidebar/app-info/app-info-detail-panel.tsx": {
- "tailwindcss/enforce-consistent-class-order": {
- "count": 5
- }
- },
"app/components/app-sidebar/app-info/app-info-trigger.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 2
@@ -330,9 +325,6 @@
},
"react/set-state-in-effect": {
"count": 4
- },
- "tailwindcss/enforce-consistent-class-order": {
- "count": 5
}
},
"app/components/app-sidebar/app-sidebar-dropdown.tsx": {
@@ -1996,16 +1988,6 @@
"count": 6
}
},
- "app/components/base/content-dialog/index.stories.tsx": {
- "react/set-state-in-effect": {
- "count": 1
- }
- },
- "app/components/base/content-dialog/index.tsx": {
- "tailwindcss/enforce-consistent-class-order": {
- "count": 2
- }
- },
"app/components/base/copy-feedback/index.tsx": {
"no-restricted-imports": {
"count": 1