From 24ea21db25b8e254de8fef286bd33b245e487766 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Sat, 9 May 2026 20:18:39 +0800 Subject: [PATCH] refactor(web): converge overlay layering on dify-ui z-50 (#35976) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- eslint-suppressions.json | 42 -- packages/dify-ui/AGENTS.md | 2 +- packages/dify-ui/README.md | 14 +- packages/dify-ui/src/alert-dialog/index.tsx | 4 +- .../src/autocomplete/__tests__/index.spec.tsx | 2 +- packages/dify-ui/src/autocomplete/index.tsx | 2 +- .../src/combobox/__tests__/index.spec.tsx | 2 +- packages/dify-ui/src/combobox/index.tsx | 2 +- packages/dify-ui/src/context-menu/index.tsx | 2 +- packages/dify-ui/src/dialog/index.tsx | 8 +- .../src/drawer/__tests__/index.spec.tsx | 2 +- packages/dify-ui/src/drawer/index.tsx | 6 +- packages/dify-ui/src/dropdown-menu/index.tsx | 2 +- packages/dify-ui/src/overlay-shared.ts | 2 +- packages/dify-ui/src/popover/index.tsx | 2 +- packages/dify-ui/src/preview-card/index.tsx | 2 +- packages/dify-ui/src/select/index.tsx | 2 +- .../src/toast/__tests__/index.spec.tsx | 2 +- packages/dify-ui/src/toast/index.tsx | 2 +- packages/dify-ui/src/tooltip/index.tsx | 2 +- web/AGENTS.md | 6 +- .../annotation/add-annotation-modal/index.tsx | 115 +++-- .../edit-annotation-modal/index.tsx | 192 ++++---- .../view-annotation-modal/index.tsx | 159 ++++--- .../inputs-form/__tests__/content.spec.tsx | 2 +- .../inputs-form/__tests__/content.spec.tsx | 2 +- .../base/drawer-plus/__tests__/index.spec.tsx | 446 ------------------ .../base/drawer-plus/index.stories.tsx | 124 ----- web/app/components/base/drawer-plus/index.tsx | 106 ----- .../pricing/plans/cloud-plan-item/index.tsx | 1 - .../__tests__/index.spec.tsx | 2 +- .../__tests__/config-credentials.spec.tsx | 21 +- .../config-credentials.tsx | 341 +++++++------ .../get-schema.tsx | 147 +++--- .../edit-custom-collection-modal/index.tsx | 412 ++++++++-------- .../edit-custom-collection-modal/test-api.tsx | 160 ++++--- .../__tests__/config-credentials.spec.tsx | 12 +- .../setting/build-in/config-credentials.tsx | 143 +++--- web/docs/lint.md | 4 +- web/docs/overlay-migration.md | 91 ---- web/docs/overlay.md | 44 ++ web/eslint.config.mjs | 2 +- web/eslint.constants.mjs | 16 - web/service/tools.ts | 2 +- 44 files changed, 1052 insertions(+), 1602 deletions(-) delete mode 100644 web/app/components/base/drawer-plus/__tests__/index.spec.tsx delete mode 100644 web/app/components/base/drawer-plus/index.stories.tsx delete mode 100644 web/app/components/base/drawer-plus/index.tsx delete mode 100644 web/docs/overlay-migration.md create mode 100644 web/docs/overlay.md diff --git a/eslint-suppressions.json b/eslint-suppressions.json index f7ff4f8d6d..e49483f63c 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -192,11 +192,6 @@ "count": 1 } }, - "web/app/components/app/annotation/add-annotation-modal/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/app/annotation/batch-add-annotation-modal/index.tsx": { "erasable-syntax-only/enums": { "count": 1 @@ -222,11 +217,6 @@ "count": 1 } }, - "web/app/components/app/annotation/edit-annotation-modal/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/app/annotation/header-opts/index.tsx": { "ts/no-explicit-any": { "count": 1 @@ -249,9 +239,6 @@ "erasable-syntax-only/enums": { "count": 1 }, - "no-restricted-imports": { - "count": 1 - }, "react/set-state-in-effect": { "count": 5 }, @@ -844,16 +831,6 @@ "count": 1 } }, - "web/app/components/base/drawer-plus/index.stories.tsx": { - "react/component-hook-factories": { - "count": 1 - } - }, - "web/app/components/base/drawer-plus/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/error-boundary/index.tsx": { "react-refresh/only-export-components": { "count": 3 @@ -2879,20 +2856,7 @@ "count": 2 } }, - "web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/components/tools/edit-custom-collection-modal/get-schema.tsx": { - "ts/no-explicit-any": { - "count": 1 - } - }, "web/app/components/tools/edit-custom-collection-modal/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react/set-state-in-effect": { "count": 4 }, @@ -2901,9 +2865,6 @@ } }, "web/app/components/tools/edit-custom-collection-modal/test-api.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -2944,9 +2905,6 @@ } }, "web/app/components/tools/setting/build-in/config-credentials.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 3 } diff --git a/packages/dify-ui/AGENTS.md b/packages/dify-ui/AGENTS.md index bdc2160702..9524394214 100644 --- a/packages/dify-ui/AGENTS.md +++ b/packages/dify-ui/AGENTS.md @@ -75,7 +75,7 @@ Composition rules: - Keep Base UI primitive semantics visible in the public API. Export compound parts such as `ComboboxInputGroup`, `ComboboxInput`, `ComboboxContent`, `ComboboxList`, `ComboboxItem`, and `ComboboxItemIndicator` instead of wrapping them into one business component. - For `Combobox` multiple selection, follow the official chips pattern: `ComboboxInputGroup` contains `ComboboxChips`, `ComboboxValue` renders `ComboboxChip` items, and `ComboboxInput` remains inside the chips row. Chips should wrap and let the input group grow vertically instead of forcing horizontal overflow. -- Content primitives must own their Base UI `Portal` and use `z-1002` on `Positioner`, matching the overlay contract in `README.md`. +- Content primitives must own their Base UI `Portal` and use `z-50` on `Positioner`, matching the overlay contract in `README.md`. Toast owns `z-60`. - Use `w-(--anchor-width)` with viewport-aware max-width for `Autocomplete` and `Combobox` popups. Do not add `min-w-(--anchor-width)` when it would defeat available-width clamping. [Autocomplete docs]: https://base-ui.com/react/components/autocomplete.md#usage-guidelines diff --git a/packages/dify-ui/README.md b/packages/dify-ui/README.md index c78faede89..010fb3e56d 100644 --- a/packages/dify-ui/README.md +++ b/packages/dify-ui/README.md @@ -84,18 +84,18 @@ Equivalent: any root element with `isolation: isolate` in CSS. Without it, overl Every overlay primitive uses a single, shared z-index. Do **not** override it at call sites. -| Layer | z-index | Where | -| ------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------- | -| Overlays (Dialog, AlertDialog, Autocomplete, Combobox, Drawer, Popover, DropdownMenu, ContextMenu, Select, Tooltip) | `z-1002` | Positioner / Backdrop | -| Toast viewport | `z-1003` | One layer above overlays so notifications are never hidden under a dialog. | +| Layer | z-index | Where | +| ------------------------------------------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------------------- | +| Overlays (Dialog, AlertDialog, Autocomplete, Combobox, Drawer, Popover, DropdownMenu, ContextMenu, Select, Tooltip) | `z-50` | Positioner / Backdrop | +| Toast viewport | `z-60` | One layer above overlays so notifications are never hidden under a dialog. | -Rationale: during Dify's migration from legacy `base/modal` / `base/dialog` / `base/drawer` / `base/drawer-plus` overlays to this package, new and old overlays coexist in the DOM. `z-1002` sits above any common legacy layer, eliminating per-call-site z-index hacks. Among themselves, new primitives share the same z-index and **rely on DOM order** for stacking — the portal mounted later wins. +Rationale: Dify UI owns the normal application overlay layer. Overlay primitives share `z-50` and **rely on DOM order** for stacking — the portal mounted later wins. Toast owns `z-60` so notifications remain visible above dialogs, popovers, and other portalled surfaces without falling back to `z-9999`. -See `[web/docs/overlay-migration.md](../../web/docs/overlay-migration.md)` for the Dify-web migration history. Once the legacy overlays are gone, the values in this table can drop back to `z-50` / `z-51`. +See `[web/docs/overlay.md](../../web/docs/overlay.md)` for the web app overlay best practices. ### Rules -- Never add `z-1003` / `z-9999` / etc. overrides on primitives from this package. If something is getting clipped, the **parent** overlay (typically a legacy one) is the problem and should be migrated. +- Never add ad hoc `z-*` overrides on primitives from this package. If something is getting clipped, fix the parent overlay structure instead of raising the child primitive. - Never create an extra manual portal on top of our primitives — use the exported content / portal parts such as `DialogContent`, `PopoverContent`, and `DrawerPortal`. Base UI handles focus management, scroll-locking, and dismissal. - When a primitive needs additional presentation chrome (e.g. a custom backdrop), add it **inside** the exported component, not at call sites. diff --git a/packages/dify-ui/src/alert-dialog/index.tsx b/packages/dify-ui/src/alert-dialog/index.tsx index 7b432c87dc..81299ef932 100644 --- a/packages/dify-ui/src/alert-dialog/index.tsx +++ b/packages/dify-ui/src/alert-dialog/index.tsx @@ -29,14 +29,14 @@ export function AlertDialogContent({ { await expect.element(screen.getByRole('group', { name: 'autocomplete positioner' })).toHaveAttribute('data-side', 'bottom') await expect.element(screen.getByRole('group', { name: 'autocomplete positioner' })).toHaveAttribute('data-align', 'start') - await expect.element(screen.getByRole('group', { name: 'autocomplete positioner' })).toHaveClass('z-1002') + await expect.element(screen.getByRole('group', { name: 'autocomplete positioner' })).toHaveClass('z-50') await expect.element(screen.getByRole('dialog', { name: 'autocomplete popup' })).toHaveClass('rounded-xl') await expect.element(screen.getByRole('dialog', { name: 'autocomplete popup' })).toHaveClass('w-(--anchor-width)') await expect.element(screen.getByRole('listbox', { name: 'autocomplete list' })).toHaveClass('scroll-py-1') diff --git a/packages/dify-ui/src/autocomplete/index.tsx b/packages/dify-ui/src/autocomplete/index.tsx index 16c4b19673..4c8893b376 100644 --- a/packages/dify-ui/src/autocomplete/index.tsx +++ b/packages/dify-ui/src/autocomplete/index.tsx @@ -261,7 +261,7 @@ export function AutocompleteContent({ align={align} sideOffset={sideOffset} alignOffset={alignOffset} - className={cn('z-1002 outline-hidden', className)} + className={cn('z-50 outline-hidden', className)} {...positionerProps} > { await expect.element(screen.getByRole('group', { name: 'combobox positioner' })).toHaveAttribute('data-side', 'bottom') await expect.element(screen.getByRole('group', { name: 'combobox positioner' })).toHaveAttribute('data-align', 'start') - await expect.element(screen.getByRole('group', { name: 'combobox positioner' })).toHaveClass('z-1002') + await expect.element(screen.getByRole('group', { name: 'combobox positioner' })).toHaveClass('z-50') await expect.element(screen.getByRole('dialog', { name: 'combobox popup' })).toHaveClass('rounded-xl') await expect.element(screen.getByRole('dialog', { name: 'combobox popup' })).toHaveClass('w-(--anchor-width)') await expect.element(screen.getByRole('listbox', { name: 'combobox list' })).toHaveClass('scroll-py-1') diff --git a/packages/dify-ui/src/combobox/index.tsx b/packages/dify-ui/src/combobox/index.tsx index c4f03241f6..eb43b911c7 100644 --- a/packages/dify-ui/src/combobox/index.tsx +++ b/packages/dify-ui/src/combobox/index.tsx @@ -323,7 +323,7 @@ export function ComboboxContent({ align={align} sideOffset={sideOffset} alignOffset={alignOffset} - className={cn('z-1002 outline-hidden', className)} + className={cn('z-50 outline-hidden', className)} {...positionerProps} > { expect(screen.container).not.toContainElement(dialog) await expect.element(dialog).toHaveTextContent('Workspace controls') await expect.element(screen.getByText('Configure the current workspace.')).toBeInTheDocument() - await expect.element(screen.getByTestId('drawer-backdrop')).toHaveClass('z-1002') + await expect.element(screen.getByTestId('drawer-backdrop')).toHaveClass('z-50') asHTMLElement(screen.getByRole('button', { name: 'Close drawer' }).element()).click() diff --git a/packages/dify-ui/src/drawer/index.tsx b/packages/dify-ui/src/drawer/index.tsx index c63bc8174e..a2ad6dcdaf 100644 --- a/packages/dify-ui/src/drawer/index.tsx +++ b/packages/dify-ui/src/drawer/index.tsx @@ -32,7 +32,7 @@ export function DrawerBackdrop({ return ( ) @@ -60,7 +60,7 @@ export function DrawerPopup({ return ( { await expect.element(screen.getByText('Saved')).toBeInTheDocument() await expect.element(screen.getByText('Your changes are available now.')).toBeInTheDocument() await expect.element(screen.getByRole('region', { name: 'Notifications' })).toHaveAttribute('aria-live', 'polite') - await expect.element(screen.getByRole('region', { name: 'Notifications' })).toHaveClass('z-1003') + await expect.element(screen.getByRole('region', { name: 'Notifications' })).toHaveClass('z-60') expect(screen.getByRole('region', { name: 'Notifications' }).element().firstElementChild).toHaveClass('top-4') expect(screen.getByRole('dialog').element()).not.toHaveClass('outline-hidden') expect(document.body.querySelector('[aria-hidden="true"].i-ri-checkbox-circle-fill')).toBeInTheDocument() diff --git a/packages/dify-ui/src/toast/index.tsx b/packages/dify-ui/src/toast/index.tsx index a479621563..7d4e867faf 100644 --- a/packages/dify-ui/src/toast/index.tsx +++ b/packages/dify-ui/src/toast/index.tsx @@ -222,7 +222,7 @@ function ToastViewport() {
= ({ onHide() } } + if (!isShow) + return null + return (
- - -
- )} - foot={ - ( -
- {isAnnotationFull && ( -
- -
- )} -
-
- setIsCreateNext(!isCreateNext)} /> -
{t('addModal.createNext', { ns: 'appAnnotation' })}
-
-
- - -
-
-
- - ) - } + open + modal + disablePointerDismissal + swipeDirection="right" + onOpenChange={(open) => { + if (!open) + onHide() + }} > + + + + + +
+
+ + {t('addModal.title', { ns: 'appAnnotation' })} + + +
+
+
+
+ + +
+
+
+ {isAnnotationFull && ( +
+ +
+ )} +
+
+ setIsCreateNext(!isCreateNext)} /> +
{t('addModal.createNext', { ns: 'appAnnotation' })}
+
+
+ + +
+
+
+
+
+
+
) diff --git a/web/app/components/app/annotation/edit-annotation-modal/index.tsx b/web/app/components/app/annotation/edit-annotation-modal/index.tsx index 8e690eca9b..0f6ee0dd1b 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/index.tsx @@ -8,11 +8,20 @@ import { AlertDialogContent, AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' +import { + Drawer, + DrawerBackdrop, + DrawerCloseButton, + DrawerContent, + DrawerPopup, + DrawerPortal, + DrawerTitle, + DrawerViewport, +} from '@langgenius/dify-ui/drawer' import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import Drawer from '@/app/components/base/drawer-plus' import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication' import AnnotationFull from '@/app/components/billing/annotation-full' import { useProviderContext } from '@/context/provider-context' @@ -90,92 +99,115 @@ const EditAnnotationModal: FC = ({ } } const [showModal, setShowModal] = useState(false) + if (!isShow) + return null return (
-
- handleSave(EditItemType.Query, editedContent)} - /> - handleSave(EditItemType.Answer, editedContent)} - /> - !open && setShowModal(false)}> - -
- - {t('feature.annotation.removeConfirm', { ns: 'appDebug' })} - + open + modal + disablePointerDismissal + swipeDirection="right" + onOpenChange={(open) => { + if (!open) + onHide() + }} + > + + + + + +
+
+ + {t('editModal.title', { ns: 'appAnnotation' })} + +
- - - {t('operation.cancel', { ns: 'common' })} - - { - onRemove() - setShowModal(false) - onHide() - }} - > - {t('operation.confirm', { ns: 'common' })} - - - - -
-
- )} - foot={( -
- {isAnnotationFull && ( -
- -
- )} - - { - annotationId - ? ( -
-
setShowModal(true)} - > - -
{t('editModal.removeThisCache', { ns: 'appAnnotation' })}
-
- {!!createdAt && ( -
- {t('editModal.createdAt', { ns: 'appAnnotation' })} -  - {formatTime(createdAt, t('dateTimeFormat', { ns: 'appLog' }) as string)} +
+
+
+ handleSave(EditItemType.Query, editedContent)} + /> + handleSave(EditItemType.Answer, editedContent)} + /> + !open && setShowModal(false)}> + +
+ + {t('feature.annotation.removeConfirm', { ns: 'appDebug' })} +
- )} + + + {t('operation.cancel', { ns: 'common' })} + + { + onRemove() + setShowModal(false) + onHide() + }} + > + {t('operation.confirm', { ns: 'common' })} + + +
+
+
+
+
+ {isAnnotationFull && ( +
+
- ) - : undefined - } -
- )} - /> + )} + + { + annotationId + ? ( +
+
setShowModal(true)} + > + +
{t('editModal.removeThisCache', { ns: 'appAnnotation' })}
+
+ {!!createdAt && ( +
+ {t('editModal.createdAt', { ns: 'appAnnotation' })} +  + {formatTime(createdAt, t('dateTimeFormat', { ns: 'appLog' }) as string)} +
+ )} +
+ ) + : undefined + } +
+ + + + +
) diff --git a/web/app/components/app/annotation/view-annotation-modal/index.tsx b/web/app/components/app/annotation/view-annotation-modal/index.tsx index c9f7e8a78f..712fb29c2e 100644 --- a/web/app/components/app/annotation/view-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/view-annotation-modal/index.tsx @@ -10,11 +10,20 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { cn } from '@langgenius/dify-ui/cn' +import { + Drawer, + DrawerBackdrop, + DrawerCloseButton, + DrawerContent, + DrawerPopup, + DrawerPortal, + DrawerTitle, + DrawerViewport, +} from '@langgenius/dify-ui/drawer' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge' -import Drawer from '@/app/components/base/drawer-plus' import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication' import Pagination from '@/app/components/base/pagination' import TabSlider from '@/app/components/base/tab-slider-plain' @@ -198,75 +207,97 @@ const ViewAnnotationModal: FC = ({
) + if (!isShow) + return null + return (
setActiveTab(v as TabType)} - options={tabs} - noBorderBottom - itemClassName="pb-3.5!" - /> - )} - body={( -
-
- {activeTab === TabType.annotation ? annotationTab : hitHistoryTab} -
- !open && setShowModal(false)}> - -
- - {t('feature.annotation.removeConfirm', { ns: 'appDebug' })} - + open + modal + disablePointerDismissal + swipeDirection="right" + onOpenChange={(open) => { + if (!open) + onHide() + }} + > + + + + + +
+
+ } className="min-w-0"> + setActiveTab(v as TabType)} + options={tabs} + noBorderBottom + itemClassName="pb-3.5!" + /> + + +
- - - {t('operation.cancel', { ns: 'common' })} - - { - await onRemove() - setShowModal(false) - onHide() - }} - > - {t('operation.confirm', { ns: 'common' })} - - - - -
- )} - foot={id - ? ( -
-
setShowModal(true)} - > - -
{t('editModal.removeThisCache', { ns: 'appAnnotation' })}
+
+
+ {activeTab === TabType.annotation ? annotationTab : hitHistoryTab} +
+ !open && setShowModal(false)}> + +
+ + {t('feature.annotation.removeConfirm', { ns: 'appDebug' })} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + { + await onRemove() + setShowModal(false) + onHide() + }} + > + {t('operation.confirm', { ns: 'common' })} + + +
+
-
- {t('editModal.createdAt', { ns: 'appAnnotation' })} + {id && ( +
+
setShowModal(true)} + > + +
{t('editModal.removeThisCache', { ns: 'appAnnotation' })}
+
+
+ {t('editModal.createdAt', { ns: 'appAnnotation' })}   - {formatTime(createdAt, t('dateTimeFormat', { ns: 'appLog' }) as string)} -
-
- ) - : undefined} - /> + {formatTime(createdAt, t('dateTimeFormat', { ns: 'appLog' }) as string)} +
+
+ )} + + + + +
) diff --git a/web/app/components/base/chat/chat-with-history/inputs-form/__tests__/content.spec.tsx b/web/app/components/base/chat/chat-with-history/inputs-form/__tests__/content.spec.tsx index 8d3c7002d7..a53bdc3b93 100644 --- a/web/app/components/base/chat/chat-with-history/inputs-form/__tests__/content.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/inputs-form/__tests__/content.spec.tsx @@ -258,7 +258,7 @@ describe('InputsFormContent', () => { renderWithContext(, context) await user.click(screen.getByText('B')) - expect(screen.getByText('A').closest('.z-1002')).not.toBeNull() + expect(screen.getByText('A').closest('.z-50')).not.toBeNull() }) it('handles select input with existing value (value not in options -> shows placeholder)', () => { diff --git a/web/app/components/base/chat/embedded-chatbot/inputs-form/__tests__/content.spec.tsx b/web/app/components/base/chat/embedded-chatbot/inputs-form/__tests__/content.spec.tsx index 47c273d163..d552af8cec 100644 --- a/web/app/components/base/chat/embedded-chatbot/inputs-form/__tests__/content.spec.tsx +++ b/web/app/components/base/chat/embedded-chatbot/inputs-form/__tests__/content.spec.tsx @@ -208,7 +208,7 @@ describe('InputsFormContent', () => { await user.click(selectTrigger) - expect(screen.getByText('Option 1').closest('.z-1002')).not.toBeNull() + expect(screen.getByText('Option 1').closest('.z-50')).not.toBeNull() }) it('should handle single file upload change', async () => { diff --git a/web/app/components/base/drawer-plus/__tests__/index.spec.tsx b/web/app/components/base/drawer-plus/__tests__/index.spec.tsx deleted file mode 100644 index b3a7d2cd2b..0000000000 --- a/web/app/components/base/drawer-plus/__tests__/index.spec.tsx +++ /dev/null @@ -1,446 +0,0 @@ -import { fireEvent, render, screen } from '@testing-library/react' -import * as React from 'react' -import DrawerPlus from '..' - -vi.mock('@/hooks/use-breakpoints', () => ({ - default: () => 'desktop', - MediaType: { mobile: 'mobile', desktop: 'desktop', tablet: 'tablet' }, -})) - -describe('DrawerPlus', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('Rendering', () => { - it('should not render when isShow is false', () => { - render( - {}} - title="Test Drawer" - body={
Content
} - />, - ) - - expect(screen.queryByRole('dialog')).not.toBeInTheDocument() - }) - - it('should render when isShow is true', () => { - const bodyContent =
Body Content
- render( - {}} - title="Test Drawer" - body={bodyContent} - />, - ) - - expect(screen.getByRole('dialog')).toBeInTheDocument() - expect(screen.getByText('Test Drawer')).toBeInTheDocument() - expect(screen.getByText('Body Content')).toBeInTheDocument() - }) - - it('should render footer when provided', () => { - const footerContent =
Footer Content
- render( - {}} - title="Test Drawer" - body={
Body
} - foot={footerContent} - />, - ) - - expect(screen.getByText('Footer Content')).toBeInTheDocument() - }) - - it('should render JSX element as title', () => { - const titleElement =

Custom Title

- render( - {}} - title={titleElement} - body={
Body
} - />, - ) - - expect(screen.getByTestId('custom-title')).toBeInTheDocument() - }) - - it('should render titleDescription when provided', () => { - render( - {}} - title="Test Drawer" - titleDescription="Description text" - body={
Body
} - />, - ) - - expect(screen.getByText('Description text')).toBeInTheDocument() - }) - - it('should not render titleDescription when not provided', () => { - render( - {}} - title="Test Drawer" - body={
Body
} - />, - ) - - expect(screen.queryByText(/Description/)).not.toBeInTheDocument() - }) - - it('should render JSX element as titleDescription', () => { - const descElement = Custom Description - render( - {}} - title="Test" - titleDescription={descElement} - body={
Body
} - />, - ) - - expect(screen.getByTestId('custom-desc')).toBeInTheDocument() - }) - }) - - describe('Props - Display Options', () => { - it('should apply default maxWidthClassName', () => { - render( - {}} - title="Test" - body={
Body
} - />, - ) - const innerPanel = screen.getByText('Test').closest('.bg-components-panel-bg') - const outerPanel = innerPanel?.parentElement - expect(outerPanel?.className).toContain('max-w-[640px]!') - }) - - it('should apply custom maxWidthClassName', () => { - render( - {}} - title="Test" - body={
Body
} - maxWidthClassName="max-w-[800px]!" - />, - ) - - const innerPanel = screen.getByText('Test').closest('.bg-components-panel-bg') - const outerPanel = innerPanel?.parentElement - expect(outerPanel?.className).toContain('max-w-[800px]!') - }) - - it('should apply custom panelClassName', () => { - render( - {}} - title="Test" - body={
Body
} - panelClassName="custom-panel" - />, - ) - - const innerPanel = screen.getByText('Test').closest('.bg-components-panel-bg') - const outerPanel = innerPanel?.parentElement - expect(outerPanel?.className).toContain('custom-panel') - }) - - it('should apply custom dialogClassName', () => { - render( - {}} - title="Test" - body={
Body
} - dialogClassName="custom-dialog" - />, - ) - - expect(document.querySelector('.custom-dialog')).toBeInTheDocument() - }) - - it('should apply custom contentClassName', () => { - render( - {}} - title="Test" - body={
Body
} - contentClassName="custom-content" - />, - ) - const title = screen.getByText('Test') - const header = title.closest('.shrink-0.border-b.border-divider-subtle') - const content = header?.parentElement - expect(content?.className).toContain('custom-content') - }) - - it('should apply custom headerClassName', () => { - render( - {}} - title="Test" - body={
Body
} - headerClassName="custom-header" - />, - ) - - const title = screen.getByText('Test') - const header = title.closest('.shrink-0.border-b.border-divider-subtle') - expect(header?.className).toContain('custom-header') - }) - - it('should apply custom height', () => { - render( - {}} - title="Test" - body={
Body
} - height="500px" - />, - ) - - const title = screen.getByText('Test') - const header = title.closest('.shrink-0.border-b.border-divider-subtle') - const content = header?.parentElement - expect(content?.getAttribute('style')).toContain('height: 500px') - }) - - it('should use default height', () => { - render( - {}} - title="Test" - body={
Body
} - />, - ) - - const title = screen.getByText('Test') - const header = title.closest('.shrink-0.border-b.border-divider-subtle') - const content = header?.parentElement - expect(content?.getAttribute('style')).toContain('calc(100vh - 72px)') - }) - }) - - describe('Event Handlers', () => { - it('should call onHide when close button is clicked', () => { - const handleHide = vi.fn() - render( - Body
} - />, - ) - - const title = screen.getByText('Test') - const headerRight = title.nextElementSibling // .flex items-center - const closeDiv = headerRight?.querySelector('.cursor-pointer') as HTMLElement - - fireEvent.click(closeDiv) - expect(handleHide).toHaveBeenCalledTimes(1) - }) - }) - - describe('Complex Content', () => { - it('should render complex JSX elements in body', () => { - const complexBody = ( -
-

Header

-

Paragraph

- -
- ) - - render( - {}} - title="Test" - body={complexBody} - />, - ) - - expect(screen.getByText('Header')).toBeInTheDocument() - expect(screen.getByText('Paragraph')).toBeInTheDocument() - expect(screen.getByRole('button', { name: 'Action Button' })).toBeInTheDocument() - }) - - it('should render complex footer', () => { - const complexFooter = ( -
- - -
- ) - - render( - {}} - title="Test" - body={
Body
} - foot={complexFooter} - />, - ) - - expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument() - expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument() - }) - }) - - describe('Edge Cases', () => { - it('should handle empty title', () => { - render( - {}} - title="" - body={
Body
} - />, - ) - - expect(screen.getByRole('dialog')).toBeInTheDocument() - }) - - it('should handle undefined titleDescription', () => { - render( - {}} - title="Test" - titleDescription={undefined} - body={
Body
} - />, - ) - - expect(screen.getByRole('dialog')).toBeInTheDocument() - }) - - it('should handle rapid isShow toggle', () => { - const { rerender } = render( - {}} - title="Test" - body={
Body
} - />, - ) - - expect(screen.getByRole('dialog')).toBeInTheDocument() - - rerender( - {}} - title="Test" - body={
Body
} - />, - ) - - expect(screen.queryByRole('dialog')).not.toBeInTheDocument() - - rerender( - {}} - title="Test" - body={
Body
} - />, - ) - - expect(screen.getByRole('dialog')).toBeInTheDocument() - }) - - it('should handle special characters in title', () => { - const specialTitle = 'Test <> & " \' | Drawer' - render( - {}} - title={specialTitle} - body={
Body
} - />, - ) - - expect(screen.getByText(specialTitle)).toBeInTheDocument() - }) - - it('should handle empty body content', () => { - render( - {}} - title="Test" - body={
} - />, - ) - - expect(screen.getByRole('dialog')).toBeInTheDocument() - }) - - it('should apply both custom maxWidth and panel classNames', () => { - render( - {}} - title="Test" - body={
Body
} - maxWidthClassName="max-w-[500px]!" - panelClassName="custom-style" - />, - ) - - const innerPanel = screen.getByText('Test').closest('.bg-components-panel-bg') - const outerPanel = innerPanel?.parentElement - expect(outerPanel?.className).toContain('max-w-[500px]!') - expect(outerPanel?.className).toContain('custom-style') - }) - }) - - describe('Memoization', () => { - it('should be memoized and not re-render on parent changes', () => { - const { rerender } = render( - {}} - title="Test" - body={
Body
} - />, - ) - - const dialog = screen.getByRole('dialog') - - rerender( - {}} - title="Test" - body={
Body
} - />, - ) - - expect(dialog).toBeInTheDocument() - }) - }) -}) diff --git a/web/app/components/base/drawer-plus/index.stories.tsx b/web/app/components/base/drawer-plus/index.stories.tsx deleted file mode 100644 index 4bdfef2ab3..0000000000 --- a/web/app/components/base/drawer-plus/index.stories.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/nextjs-vite' -import { useState } from 'react' -import { fn } from 'storybook/test' -import DrawerPlus from '.' - -const meta = { - title: 'Base/Feedback/DrawerPlus', - component: DrawerPlus, - parameters: { - layout: 'fullscreen', - docs: { - description: { - component: 'Enhanced drawer built atop the base drawer component. Provides header/foot slots, mask control, and mobile breakpoints.', - }, - }, - }, - tags: ['autodocs'], -} satisfies Meta - -export default meta -type Story = StoryObj - -type DrawerPlusProps = React.ComponentProps - -const storyBodyElement: React.JSX.Element = ( -
-

- DrawerPlus allows rich content with sticky header/footer and responsive masking on mobile. Great for editing flows or showing execution logs. -

-
- Body content scrolls if it exceeds the allotted height. -
-
-) - -const DrawerPlusDemo = (props: Partial) => { - const [open, setOpen] = useState(false) - - const { - body, - title, - foot, - isShow: _isShow, - onHide: _onHide, - ...rest - } = props - - const resolvedBody: React.JSX.Element = body ?? storyBodyElement - - return ( -
- - - } - isShow={open} - onHide={() => setOpen(false)} - title={title ?? 'Workflow execution details'} - body={resolvedBody} - foot={foot} - /> -
- ) -} - -export const Playground: Story = { - render: args => , - args: { - isShow: false, - onHide: fn(), - title: 'Edit configuration', - body: storyBodyElement, - }, -} - -export const WithFooter: Story = { - render: (args) => { - const FooterDemo = () => { - const [open, setOpen] = useState(false) - return ( -
- - - setOpen(false)} - title={args.title ?? 'Workflow execution details'} - body={args.body ?? ( -
-

Populate the body with scrollable content. Footer stays pinned.

-
- )} - foot={( -
- - -
- )} - /> -
- ) - } - return - }, - args: { - isShow: false, - onHide: fn(), - title: 'Edit configuration!', - body: storyBodyElement, - }, -} diff --git a/web/app/components/base/drawer-plus/index.tsx b/web/app/components/base/drawer-plus/index.tsx deleted file mode 100644 index 261022669b..0000000000 --- a/web/app/components/base/drawer-plus/index.tsx +++ /dev/null @@ -1,106 +0,0 @@ -'use client' -import type { FC } from 'react' -import { cn } from '@langgenius/dify-ui/cn' -import { RiCloseLine } from '@remixicon/react' -import * as React from 'react' -import { useRef } from 'react' -import Drawer from '@/app/components/base/drawer' -import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' - -type Props = { - isShow: boolean - onHide: () => void - dialogClassName?: string - dialogBackdropClassName?: string - panelClassName?: string - maxWidthClassName?: string - contentClassName?: string - headerClassName?: string - height?: number | string - title: string | React.JSX.Element - titleDescription?: string | React.JSX.Element - body: React.JSX.Element - foot?: React.JSX.Element - isShowMask?: boolean - clickOutsideNotOpen?: boolean - positionCenter?: boolean -} - -const DrawerPlus: FC = ({ - isShow, - onHide, - dialogClassName = '', - dialogBackdropClassName = '', - panelClassName = '', - maxWidthClassName = 'max-w-[640px]!', - height = 'calc(100vh - 72px)', - contentClassName, - headerClassName, - title, - titleDescription, - body, - foot, - isShowMask, - clickOutsideNotOpen = true, - positionCenter, -}) => { - const ref = useRef(null) - const media = useBreakpoints() - const isMobile = media === MediaType.mobile - - if (!isShow) - return null - - return ( - // clickOutsideNotOpen to fix confirm modal click cause drawer close - -
-
-
-
- {title} -
-
-
- -
-
-
- {titleDescription && ( -
- {titleDescription} -
- )} -
-
- {body} -
- {foot && ( -
- {foot} -
- )} -
-
- ) -} -export default React.memo(DrawerPlus) diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx index d3dc47b29f..e6546a469c 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx @@ -201,7 +201,6 @@ const CloudPlanItem: FC = ({ open={showEducationPricingConfirm} onOpenChange={setShowEducationPricingConfirm} > - {showEducationPricingConfirm &&
}
diff --git a/web/app/components/header/account-setting/members-page/edit-workspace-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/edit-workspace-modal/__tests__/index.spec.tsx index 206a7c0148..db7b69f7cb 100644 --- a/web/app/components/header/account-setting/members-page/edit-workspace-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/members-page/edit-workspace-modal/__tests__/index.spec.tsx @@ -57,7 +57,7 @@ describe('EditWorkspaceModal', () => { it('should render on the dify-ui overlay layer', async () => { renderModal() - expect(await screen.findByRole('dialog')).toHaveClass('z-1002') + expect(await screen.findByRole('dialog')).toHaveClass('z-50') }) it('should let user edit workspace name', async () => { diff --git a/web/app/components/tools/edit-custom-collection-modal/__tests__/config-credentials.spec.tsx b/web/app/components/tools/edit-custom-collection-modal/__tests__/config-credentials.spec.tsx index ec4866b212..98a7b10c76 100644 --- a/web/app/components/tools/edit-custom-collection-modal/__tests__/config-credentials.spec.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/__tests__/config-credentials.spec.tsx @@ -1,5 +1,5 @@ import type { Credential } from '@/app/components/tools/types' -import { act, fireEvent, render, screen } from '@testing-library/react' +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { AuthHeaderPrefix, AuthType } from '@/app/components/tools/types' import ConfigCredential from '../config-credentials' @@ -82,6 +82,25 @@ describe('ConfigCredential', () => { expect(mockOnChange).not.toHaveBeenCalled() }) + it('should call onHide when Escape is pressed', async () => { + await act(async () => { + render( + , + ) + }) + + fireEvent.keyDown(document, { key: 'Escape' }) + + await waitFor(() => { + expect(mockOnHide).toHaveBeenCalledTimes(1) + }) + expect(mockOnChange).not.toHaveBeenCalled() + }) + it('should call both onChange and onHide when save is pressed', async () => { await act(async () => { render( diff --git a/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx b/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx index 9ed7c45165..dde11b56f3 100644 --- a/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx @@ -1,11 +1,19 @@ 'use client' -import type { FC } from 'react' import type { Credential } from '@/app/components/tools/types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import * as React from 'react' +import { + Drawer, + DrawerBackdrop, + DrawerCloseButton, + DrawerContent, + DrawerPopup, + DrawerPortal, + DrawerTitle, + DrawerViewport, +} from '@langgenius/dify-ui/drawer' +import { useState } from 'react' import { useTranslation } from 'react-i18next' -import Drawer from '@/app/components/base/drawer-plus' import { Infotip } from '@/app/components/base/infotip' import Input from '@/app/components/base/input' import Radio from '@/app/components/base/radio/ui' @@ -25,175 +33,200 @@ type ItemProps = { onClick: (value: AuthType | AuthHeaderPrefix) => void } -const SelectItem: FC = ({ text, value, isChecked, onClick }) => { +function SelectItem({ text, value, isChecked, onClick }: ItemProps) { return ( -
onClick(value)} >
{text}
-
+ ) } -const ConfigCredential: FC = ({ +export default function ConfigCredential({ positionCenter, credential, onChange, onHide, -}) => { +}: Props) { const { t } = useTranslation() - const [tempCredential, setTempCredential] = React.useState(credential) + const [tempCredential, setTempCredential] = useState(credential) return ( -
-
-
{t('createTool.authMethod.type', { ns: 'tools' })}
-
- setTempCredential({ - auth_type: value as AuthType, - })} - /> - setTempCredential({ - auth_type: value as AuthType, - api_key_header: tempCredential.api_key_header || 'Authorization', - api_key_value: tempCredential.api_key_value || '', - api_key_header_prefix: tempCredential.api_key_header_prefix || AuthHeaderPrefix.custom, - })} - /> - setTempCredential({ - auth_type: value as AuthType, - api_key_query_param: tempCredential.api_key_query_param || 'key', - api_key_value: tempCredential.api_key_value || '', - })} - /> + open + modal + disablePointerDismissal + swipeDirection="right" + onOpenChange={(open) => { + if (!open) + onHide() + }} + > + + + + + +
+
+ + {t('createTool.authMethod.title', { ns: 'tools' })} + + +
-
- {tempCredential.auth_type === AuthType.apiKeyHeader && ( - <> -
-
{t('createTool.authHeaderPrefix.title', { ns: 'tools' })}
-
- setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })} - /> - setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })} - /> - setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })} - /> +
+
+
+
{t('createTool.authMethod.type', { ns: 'tools' })}
+
+ setTempCredential({ + auth_type: value as AuthType, + })} + /> + setTempCredential({ + auth_type: value as AuthType, + api_key_header: tempCredential.api_key_header || 'Authorization', + api_key_value: tempCredential.api_key_value || '', + api_key_header_prefix: tempCredential.api_key_header_prefix || AuthHeaderPrefix.custom, + })} + /> + setTempCredential({ + auth_type: value as AuthType, + api_key_query_param: tempCredential.api_key_query_param || 'key', + api_key_value: tempCredential.api_key_value || '', + })} + /> +
+ {tempCredential.auth_type === AuthType.apiKeyHeader && ( + <> +
+
{t('createTool.authHeaderPrefix.title', { ns: 'tools' })}
+
+ setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })} + /> + setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })} + /> + setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })} + /> +
+
+
+
+ {t('createTool.authMethod.key', { ns: 'tools' })} + + {t('createTool.authMethod.keyTooltip', { ns: 'tools' })} + +
+ setTempCredential({ ...tempCredential, api_key_header: e.target.value })} + placeholder={t('createTool.authMethod.types.apiKeyPlaceholder', { ns: 'tools' })!} + /> +
+
+
{t('createTool.authMethod.value', { ns: 'tools' })}
+ setTempCredential({ ...tempCredential, api_key_value: e.target.value })} + placeholder={t('createTool.authMethod.types.apiValuePlaceholder', { ns: 'tools' })!} + /> +
+ + )} + {tempCredential.auth_type === AuthType.apiKeyQuery && ( + <> +
+
+ {t('createTool.authMethod.queryParam', { ns: 'tools' })} + + {t('createTool.authMethod.queryParamTooltip', { ns: 'tools' })} + +
+ setTempCredential({ ...tempCredential, api_key_query_param: e.target.value })} + placeholder={t('createTool.authMethod.types.queryParamPlaceholder', { ns: 'tools' })!} + /> +
+
+
{t('createTool.authMethod.value', { ns: 'tools' })}
+ setTempCredential({ ...tempCredential, api_key_value: e.target.value })} + placeholder={t('createTool.authMethod.types.apiValuePlaceholder', { ns: 'tools' })!} + /> +
+ + )}
-
-
- {t('createTool.authMethod.key', { ns: 'tools' })} - - {t('createTool.authMethod.keyTooltip', { ns: 'tools' })} - -
- setTempCredential({ ...tempCredential, api_key_header: e.target.value })} - placeholder={t('createTool.authMethod.types.apiKeyPlaceholder', { ns: 'tools' })!} - /> -
-
-
{t('createTool.authMethod.value', { ns: 'tools' })}
- setTempCredential({ ...tempCredential, api_key_value: e.target.value })} - placeholder={t('createTool.authMethod.types.apiValuePlaceholder', { ns: 'tools' })!} - /> -
- - )} - {tempCredential.auth_type === AuthType.apiKeyQuery && ( - <> -
-
- {t('createTool.authMethod.queryParam', { ns: 'tools' })} - - {t('createTool.authMethod.queryParamTooltip', { ns: 'tools' })} - -
- setTempCredential({ ...tempCredential, api_key_query_param: e.target.value })} - placeholder={t('createTool.authMethod.types.queryParamPlaceholder', { ns: 'tools' })!} - /> -
-
-
{t('createTool.authMethod.value', { ns: 'tools' })}
- setTempCredential({ ...tempCredential, api_key_value: e.target.value })} - placeholder={t('createTool.authMethod.types.apiValuePlaceholder', { ns: 'tools' })!} - /> -
- - )} - -
- -
- - -
-
- )} - /> +
+
+ + +
+ + + + + ) } -export default React.memo(ConfigCredential) diff --git a/web/app/components/tools/edit-custom-collection-modal/get-schema.tsx b/web/app/components/tools/edit-custom-collection-modal/get-schema.tsx index 2bb5059870..b07d21ce14 100644 --- a/web/app/components/tools/edit-custom-collection-modal/get-schema.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/get-schema.tsx @@ -1,12 +1,13 @@ 'use client' import type { FC } from 'react' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import { - RiAddLine, - RiArrowDownSLine, -} from '@remixicon/react' -import { useClickAway } from 'ahooks' + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@langgenius/dify-ui/dropdown-menu' +import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' @@ -32,7 +33,7 @@ const GetSchema: FC = ({ } setIsParsing(true) try { - const { schema } = await importSchemaFromURL(importUrl) as any + const { schema } = await importSchemaFromURL(importUrl) setImportUrl('') onChange(schema) } @@ -42,79 +43,79 @@ const GetSchema: FC = ({ } } - const importURLRef = React.useRef(null) - useClickAway(() => { - setShowImportFromUrl(false) - }, importURLRef) - const [showExamples, setShowExamples] = useState(false) - const showExamplesRef = React.useRef(null) - useClickAway(() => { - setShowExamples(false) - }, showExamplesRef) return ( -
-
- - {showImportFromUrl && ( -
-
- setImportUrl(e.target.value)} - /> - -
-
- )} -
-
- - {showExamples && ( -
- {examples.map(item => ( -
{ - onChange(item.content) - setShowExamples(false) - }} - className="cursor-pointer rounded-lg px-3 py-1.5 system-sm-regular leading-5 whitespace-nowrap text-text-secondary hover:bg-components-panel-on-panel-item-bg-hover" - > - {t(`createTool.exampleOptions.${item.key}`, { ns: 'tools' })} -
- ))} +
+ setImportUrl(e.target.value)} + /> +
- )} - -
+ + + + + )} + > + {t('createTool.examples', { ns: 'tools' })} + + + + {examples.map(item => ( + { + onChange(item.content) + setShowExamples(false) + }} + className="system-sm-regular whitespace-nowrap text-text-secondary" + > + {t(`createTool.exampleOptions.${item.key}`, { ns: 'tools' })} + + ))} + +
) } diff --git a/web/app/components/tools/edit-custom-collection-modal/index.tsx b/web/app/components/tools/edit-custom-collection-modal/index.tsx index 03899941ba..0a0f017a36 100644 --- a/web/app/components/tools/edit-custom-collection-modal/index.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/index.tsx @@ -3,6 +3,16 @@ import type { FC } from 'react' import type { Credential, CustomCollectionBackend, CustomParamSchema, Emoji } from '../types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { + Drawer, + DrawerBackdrop, + DrawerCloseButton, + DrawerContent, + DrawerPopup, + DrawerPortal, + DrawerTitle, + DrawerViewport, +} from '@langgenius/dify-ui/drawer' import { toast } from '@langgenius/dify-ui/toast' import { RiSettings2Line } from '@remixicon/react' import { useDebounce, useGetState } from 'ahooks' @@ -11,7 +21,6 @@ import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' -import Drawer from '@/app/components/base/drawer-plus' import EmojiPicker from '@/app/components/base/emoji-picker' import Input from '@/app/components/base/input' import Textarea from '@/app/components/base/textarea' @@ -191,199 +200,224 @@ const EditCustomCollectionModal: FC = ({ return ( <> -
-
-
- {t('createTool.name', { ns: 'tools' })} - {' '} - * -
-
- { setShowEmojiPicker(true) }} className="cursor-pointer" icon={emoji.content} background={emoji.background} /> - { - const newCollection = produce(customCollection, (draft) => { - draft.provider = e.target.value - }) - setCustomCollection(newCollection) - }} - /> -
-
- - {/* Schema */} -
-
-
-
- {t('createTool.schema', { ns: 'tools' })} - * -
-
- -
{t('createTool.viewSchemaSpec', { ns: 'tools' })}
- -
+ open + modal + disablePointerDismissal + swipeDirection="right" + onOpenChange={(open) => { + if (!open) + onHide() + }} + > + + + + + +
+
+ + {t(`createTool.${isAdd ? 'title' : 'editTitle'}`, { ns: 'tools' })} + +
- -
-