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.name}
+
+ {getAppModeLabel(appDetail.mode, t)} +
+
+ {appDetail.description && ( +
+ {appDetail.description} +
+ )} +
-
- {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