From f1d72eb5d2c51f53a8a5215bc350bffc499ae239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Wed, 29 Apr 2026 17:40:26 +0800 Subject: [PATCH 1/8] chore: allow configurable Next.js dev origins (#35683) --- web/next.config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/next.config.ts b/web/next.config.ts index db44f5b9ed..a1c2e410a1 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -5,9 +5,13 @@ import { env } from './env' const isDev = process.env.NODE_ENV === 'development' const withMDX = createMDX() +const allowedDevOrigins = process.env.NEXT_ALLOWED_DEV_ORIGINS?.split(',') + .map(origin => origin.trim()) + .filter(Boolean) const nextConfig: NextConfig = { basePath: env.NEXT_PUBLIC_BASE_PATH, + ...(allowedDevOrigins?.length ? { allowedDevOrigins } : {}), transpilePackages: ['@t3-oss/env-core', '@t3-oss/env-nextjs', 'echarts', 'zrender'], turbopack: { rules: codeInspectorPlugin({ From e751ec323eede1f61dc362e8332c89cb942bcdec Mon Sep 17 00:00:00 2001 From: Coding On Star <447357187@qq.com> Date: Wed, 29 Apr 2026 23:08:35 +0800 Subject: [PATCH 2/8] fix(publisher): enhance confirm dialog handling and improve popup interactions (#35701) Co-authored-by: CodingOnStar --- .../publisher/__tests__/index.spec.tsx | 21 +++++++++++ .../rag-pipeline-header/publisher/index.tsx | 19 ++++++++-- .../rag-pipeline-header/publisher/popup.tsx | 36 ++++++++++++++----- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx index afd7c04ed1..cc2f96aa6e 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx @@ -330,6 +330,27 @@ describe('publisher', () => { }) expect(mockSetShowPricingModal).toHaveBeenCalled() }) + + it('should keep confirm dialog mounted when first publish opens follow-up overlay', async () => { + mockPublishedAt.mockReturnValue(null) + renderWithQueryClient() + + fireEvent.click(screen.getByText('workflow.common.publish')) + + await waitFor(() => { + expect(screen.getByText('workflow.common.publishUpdate')).toBeInTheDocument() + }) + + fireEvent.click(screen.getByRole('button', { name: /workflow.common.publishUpdate/i })) + + await waitFor(() => { + expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument() + }) + + fireEvent.mouseDown(document.body) + + expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument() + }) }) }) diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx index 649b06ebca..1d2ef242f8 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx @@ -1,6 +1,8 @@ import { Button } from '@langgenius/dify-ui/button' +import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { RiArrowDownSLine } from '@remixicon/react' +import { useBoolean } from 'ahooks' import { memo, useCallback, @@ -13,13 +15,19 @@ import Popup from './popup' const Publisher = () => { const { t } = useTranslation() const [open, setOpen] = useState(false) + const [confirmVisible, { setFalse: hideConfirm, setTrue: showConfirm }] = useBoolean(false) const { handleSyncWorkflowDraft } = useNodesSyncDraft() const handleOpenChange = useCallback((newOpen: boolean) => { + if (!newOpen && confirmVisible) + return if (newOpen) handleSyncWorkflowDraft(true) setOpen(newOpen) - }, [handleSyncWorkflowDraft]) + }, [confirmVisible, handleSyncWorkflowDraft]) + const closePopover = useCallback(() => { + setOpen(false) + }, []) return ( { placement="bottom-end" sideOffset={4} alignOffset={40} - popupClassName="border-none bg-transparent shadow-none" + popupClassName={cn('border-none bg-transparent shadow-none', confirmVisible && 'hidden')} > - handleOpenChange(false)} /> + ) diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx index 31f5957029..0970d66cfc 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx @@ -41,9 +41,17 @@ import PublishAsKnowledgePipelineModal from '../../publish-as-knowledge-pipeline const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P'] type PopupProps = { onRequestClose?: () => void + confirmVisible?: boolean + onShowConfirm?: () => void + onHideConfirm?: () => void } -const Popup = ({ onRequestClose }: PopupProps) => { +const Popup = ({ + onRequestClose, + confirmVisible: controlledConfirmVisible, + onShowConfirm, + onHideConfirm, +}: PopupProps) => { const { t } = useTranslation() const { datasetId } = useParams() const { push } = useRouter() @@ -60,24 +68,32 @@ const Popup = ({ onRequestClose }: PopupProps) => { const isAllowPublishAsCustomKnowledgePipelineTemplate = useProviderContextSelector(s => s.isAllowPublishAsCustomKnowledgePipelineTemplate) const setShowPricingModal = useModalContextSelector(s => s.setShowPricingModal) const apiReferenceUrl = useDatasetApiAccessUrl() - const [confirmVisible, { setFalse: hideConfirm, setTrue: showConfirm }] = useBoolean(false) + const [localConfirmVisible, { setFalse: hideLocalConfirm, setTrue: showLocalConfirm }] = useBoolean(false) + const confirmVisible = controlledConfirmVisible ?? localConfirmVisible + const showConfirm = onShowConfirm ?? showLocalConfirm + const hideConfirm = onHideConfirm ?? hideLocalConfirm const [publishing, { setFalse: hidePublishing, setTrue: showPublishing }] = useBoolean(false) const { mutateAsync: publishAsCustomizedPipeline } = usePublishAsCustomizedPipeline() const [showPublishAsKnowledgePipelineModal, { setFalse: hidePublishAsKnowledgePipelineModal, setTrue: setShowPublishAsKnowledgePipelineModal }] = useBoolean(false) const [isPublishingAsCustomizedPipeline, { setFalse: hidePublishingAsCustomizedPipeline, setTrue: showPublishingAsCustomizedPipeline }] = useBoolean(false) const invalidPublishedPipelineInfo = useInvalid([...publishedPipelineInfoQueryKeyPrefix, pipelineId]) const invalidDatasetList = useInvalidDatasetList() + const handleHideConfirm = useCallback(() => { + hideConfirm() + onRequestClose?.() + }, [hideConfirm, onRequestClose]) const handlePublish = useCallback(async (params?: PublishWorkflowParams) => { if (publishing) return + let startedPublishing = false try { const checked = await handleCheckBeforePublish() if (checked) { if (!publishedAt && !confirmVisible) { - onRequestClose?.() showConfirm() return } + startedPublishing = true showPublishing() const res = await publishWorkflow({ url: `/rag/pipelines/${pipelineId}/workflows/publish`, @@ -114,12 +130,12 @@ const Popup = ({ onRequestClose }: PopupProps) => { toast.error(t('publishPipeline.error.message', { ns: 'datasetPipeline' })) } finally { - if (publishing) + if (startedPublishing) hidePublishing() if (confirmVisible) - hideConfirm() + handleHideConfirm() } - }, [publishing, handleCheckBeforePublish, publishedAt, confirmVisible, showPublishing, publishWorkflow, pipelineId, datasetId, showConfirm, t, workflowStore, mutateDatasetRes, invalidPublishedPipelineInfo, invalidDatasetList, hidePublishing, hideConfirm, onRequestClose]) + }, [publishing, handleCheckBeforePublish, publishedAt, confirmVisible, showPublishing, publishWorkflow, pipelineId, datasetId, showConfirm, t, workflowStore, mutateDatasetRes, invalidPublishedPipelineInfo, invalidDatasetList, hidePublishing, handleHideConfirm]) useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (e) => { e.preventDefault() if (published) @@ -163,10 +179,12 @@ const Popup = ({ onRequestClose }: PopupProps) => { }, [showPublishingAsCustomizedPipeline, publishAsCustomizedPipeline, pipelineId, t, invalidCustomizedTemplateList, hidePublishingAsCustomizedPipeline, hidePublishAsKnowledgePipelineModal, docLink]) const handleClickPublishAsKnowledgePipeline = useCallback(() => { onRequestClose?.() - if (!isAllowPublishAsCustomKnowledgePipelineTemplate) + if (!isAllowPublishAsCustomKnowledgePipelineTemplate) { setShowPricingModal() - else + } + else { setShowPublishAsKnowledgePipelineModal() + } }, [isAllowPublishAsCustomKnowledgePipelineTemplate, onRequestClose, setShowPublishAsKnowledgePipelineModal, setShowPricingModal]) return (
@@ -238,7 +256,7 @@ const Popup = ({ onRequestClose }: PopupProps) => {
- !open && hideConfirm()}> + !open && handleHideConfirm()}>
Date: Wed, 29 Apr 2026 23:54:15 +0800 Subject: [PATCH 3/8] refactor(auth): update OAuth button and settings modal for improved state management and UI consistency (#35702) Co-authored-by: CodingOnStar Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- eslint-suppressions.json | 13 - .../__tests__/add-oauth-button.spec.tsx | 166 +++++++++++- .../__tests__/api-key-modal.spec.tsx | 24 ++ .../__tests__/oauth-client-settings.spec.tsx | 244 ++++++++++++++++-- .../authorize/add-oauth-button.tsx | 38 +-- .../plugin-auth/authorize/api-key-modal.tsx | 5 +- .../authorize/oauth-client-settings.tsx | 139 ++++++---- 7 files changed, 517 insertions(+), 112 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index b23e612a0a..f8208a8265 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -3182,24 +3182,11 @@ "count": 1 } }, - "web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx": { - "ts/no-explicit-any": { - "count": 2 - } - }, "web/app/components/plugins/plugin-auth/authorize/index.tsx": { "no-restricted-imports": { "count": 1 } }, - "web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 2 - } - }, "web/app/components/plugins/plugin-auth/authorized-in-node.tsx": { "ts/no-explicit-any": { "count": 1 diff --git a/web/app/components/plugins/plugin-auth/authorize/__tests__/add-oauth-button.spec.tsx b/web/app/components/plugins/plugin-auth/authorize/__tests__/add-oauth-button.spec.tsx index f39148afc8..f00df35d0c 100644 --- a/web/app/components/plugins/plugin-auth/authorize/__tests__/add-oauth-button.spec.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/__tests__/add-oauth-button.spec.tsx @@ -1,10 +1,14 @@ -import { fireEvent, render, screen } from '@testing-library/react' +import type { OAuthClientSettingsProps } from '../oauth-client-settings' +import type { FormSchema } from '@/app/components/base/form/types' +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { AuthCategory } from '../../types' const mockGetPluginOAuthUrl = vi.fn().mockResolvedValue({ authorization_url: 'https://auth.example.com' }) const mockOpenOAuthPopup = vi.fn() +const mockWriteText = vi.fn() +const mockOAuthClientSettingsProps: OAuthClientSettingsProps[] = [] vi.mock('@/hooks/use-i18n', () => ({ useRenderI18nObject: () => (obj: Record | string) => typeof obj === 'string' ? obj : obj.en_US || '', @@ -31,11 +35,37 @@ vi.mock('../../hooks/use-credential', () => ({ })) vi.mock('../oauth-client-settings', () => ({ - default: ({ onClose }: { onClose: () => void }) => ( -
- -
- ), + default: (props: OAuthClientSettingsProps) => { + mockOAuthClientSettingsProps.push(props) + const { + open = true, + onClose, + onOpenChange, + schemas, + } = props + + if (!open) + return null + + const handleClose = () => { + onOpenChange?.(false) + onClose?.() + } + + return ( +
+ + {schemas.map(schema => ( +
+
+ {React.isValidElement(schema.label) ? schema.label : String(schema.label || '')} +
+ {String(schema.default || '')} +
+ ))} +
+ ) + }, })) vi.mock('@/app/components/base/form/types', () => ({ @@ -56,6 +86,11 @@ describe('AddOAuthButton', () => { beforeEach(async () => { vi.clearAllMocks() + mockOAuthClientSettingsProps.length = 0 + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText: mockWriteText }, + }) const mod = await import('../add-oauth-button') AddOAuthButton = mod.default }) @@ -72,6 +107,7 @@ describe('AddOAuthButton', () => { fireEvent.click(screen.getByTestId('oauth-settings-button')) expect(screen.getByTestId('oauth-settings-modal')).toBeInTheDocument() + expect(mockOAuthClientSettingsProps.at(-1)?.open).toBe(true) }) it('should close OAuth settings modal', () => { @@ -84,13 +120,37 @@ describe('AddOAuthButton', () => { }) it('should trigger OAuth flow on main button click', async () => { + const mockOnUpdate = vi.fn() + render() + + const button = screen.getByText('Use OAuth').closest('button') + if (button) + fireEvent.click(button) + + await waitFor(() => { + expect(mockOpenOAuthPopup).toHaveBeenCalledWith('https://auth.example.com', expect.any(Function)) + }) + + const handleOAuthSuccess = mockOpenOAuthPopup.mock.calls[0]?.[1] + expect(handleOAuthSuccess).toBeTypeOf('function') + if (typeof handleOAuthSuccess === 'function') + handleOAuthSuccess() + + expect(mockOnUpdate).toHaveBeenCalled() + }) + + it('should not open OAuth popup when authorization URL is missing', async () => { + mockGetPluginOAuthUrl.mockResolvedValueOnce({}) render() const button = screen.getByText('Use OAuth').closest('button') if (button) fireEvent.click(button) - expect(mockGetPluginOAuthUrl).toHaveBeenCalled() + await waitFor(() => { + expect(mockGetPluginOAuthUrl).toHaveBeenCalled() + }) + expect(mockOpenOAuthPopup).not.toHaveBeenCalled() }) it('should be disabled when disabled prop is true', () => { @@ -99,4 +159,96 @@ describe('AddOAuthButton', () => { const button = screen.getByText('Use OAuth').closest('button') expect(button).toBeDisabled() }) + + it('should open OAuth settings from setup entry when OAuth is not configured', () => { + render( + , + ) + + fireEvent.click(screen.getByText('plugin.auth.setupOAuth')) + + expect(screen.getByTestId('oauth-settings-modal')).toBeInTheDocument() + expect(mockOAuthClientSettingsProps.at(-1)?.editValues).toMatchObject({ + __oauth_client__: 'custom', + }) + }) + + it('should show custom badge when OAuth custom client is enabled', () => { + render( + , + ) + + expect(screen.getByText('plugin.auth.custom')).toBeInTheDocument() + }) + + it('should build custom OAuth schema and edit values for settings modal', () => { + const schema = [ + { + name: 'client_id', + label: { en_US: 'Client ID' }, + type: 'text-input', + required: true, + default: 'schema-client-id', + }, + ] as FormSchema[] + + render( + , + ) + + fireEvent.click(screen.getByTestId('oauth-settings-button')) + + const settingsProps = mockOAuthClientSettingsProps.at(-1) + expect(settingsProps?.editValues).toMatchObject({ + __oauth_client__: 'custom', + client_id: 'stored-client-id', + }) + expect(settingsProps?.hasOriginalClientParams).toBe(true) + expect(settingsProps?.schemas[0]).toMatchObject({ + name: '__oauth_client__', + default: 'custom', + }) + expect(settingsProps?.schemas[1]).toMatchObject({ + name: 'client_id', + default: 'stored-client-id', + show_on: [ + { + variable: '__oauth_client__', + value: 'custom', + }, + ], + }) + expect(screen.getByText('https://redirect.example.com')).toBeInTheDocument() + + fireEvent.click(within(screen.getByTestId('oauth-schema-label-client_id')).getByRole('button')) + + expect(mockWriteText).toHaveBeenCalledWith('https://redirect.example.com') + }) }) diff --git a/web/app/components/plugins/plugin-auth/authorize/__tests__/api-key-modal.spec.tsx b/web/app/components/plugins/plugin-auth/authorize/__tests__/api-key-modal.spec.tsx index 41f1aa3718..ad99f7ce8c 100644 --- a/web/app/components/plugins/plugin-auth/authorize/__tests__/api-key-modal.spec.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/__tests__/api-key-modal.spec.tsx @@ -1,5 +1,6 @@ import type { ApiKeyModalProps } from '../api-key-modal' import type { FormSchema } from '@/app/components/base/form/types' +import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -384,6 +385,29 @@ describe('ApiKeyModal', () => { expect(mockOnClose).toHaveBeenCalled() }) + it('should close on backdrop click when nested inside another dialog', async () => { + const mockOnClose = vi.fn() + render( + + + + + , + ) + + const backdrop = document.querySelector('.bg-background-overlay') + expect(backdrop).toBeInTheDocument() + + fireEvent.pointerDown(backdrop!) + fireEvent.mouseDown(backdrop!) + fireEvent.click(backdrop!) + + await waitFor(() => { + expect(screen.getByTestId('modal-open-state')).toHaveTextContent('false') + }) + expect(mockOnClose).toHaveBeenCalled() + }) + it('should render readme entrance when detail is provided', () => { const payload = { ...basePayload, detail: { name: 'Test' } as never } render() diff --git a/web/app/components/plugins/plugin-auth/authorize/__tests__/oauth-client-settings.spec.tsx b/web/app/components/plugins/plugin-auth/authorize/__tests__/oauth-client-settings.spec.tsx index 2c86820202..d99e3514db 100644 --- a/web/app/components/plugins/plugin-auth/authorize/__tests__/oauth-client-settings.spec.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/__tests__/oauth-client-settings.spec.tsx @@ -1,4 +1,8 @@ +import type { OAuthClientSettingsProps } from '../oauth-client-settings' +import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' +import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { AuthCategory } from '../../types' @@ -20,7 +24,8 @@ vi.mock('@langgenius/dify-ui/toast', () => ({ const mockSetPluginOAuthCustomClient = vi.fn().mockResolvedValue({}) const mockDeletePluginOAuthCustomClient = vi.fn().mockResolvedValue({}) const mockInvalidPluginOAuthClientSchema = vi.fn() -const mockFormValues = { isCheckValidated: true, values: { __oauth_client__: 'custom', client_id: 'test-id' } } +let mockFormValues = { isCheckValidated: true, values: { __oauth_client__: 'custom', client_id: 'test-id' } } +let mockAuthFormProps: Record | undefined vi.mock('../../hooks/use-credential', () => ({ useSetPluginOAuthCustomClientHook: () => ({ @@ -40,36 +45,19 @@ vi.mock('../../../readme-panel/store', () => ({ ReadmeShowType: { modal: 'modal' }, })) -vi.mock('@/app/components/base/modal/modal', () => ({ - default: ({ children, title, onClose: _onClose, onConfirm, onCancel, onExtraButtonClick, footerSlot }: { - children: React.ReactNode - title: string - onClose?: () => void - onConfirm?: () => void - onCancel?: () => void - onExtraButtonClick?: () => void - footerSlot?: React.ReactNode - [key: string]: unknown - }) => ( -
-
{title}
- {children} - - - - {!!footerSlot &&
{footerSlot}
} -
- ), -})) - -vi.mock('@/app/components/base/form/form-scenarios/auth', () => ({ - default: React.forwardRef((_props: Record, ref: React.Ref) => { +vi.mock('@/app/components/base/form/form-scenarios/auth', () => { + const MockAuthForm = ({ ref, ...props }: { ref?: React.Ref } & Record) => { + mockAuthFormProps = props React.useImperativeHandle(ref, () => ({ getFormValues: () => mockFormValues, })) return
- }), -})) + } + + return { + default: MockAuthForm, + } +}) vi.mock('@tanstack/react-form', () => ({ useForm: (config: Record) => ({ @@ -89,11 +77,72 @@ const defaultSchemas = [ { name: 'client_id', label: 'Client ID', type: 'text-input', required: true }, ] as never +const PopoverSettingsHarness = ({ + OAuthClientSettings, + onClose, + onPopoverClose, +}: { + OAuthClientSettings: React.FC + onClose: () => void + onPopoverClose: () => void +}) => { + const [open, setOpen] = React.useState(true) + + return ( + { + setOpen(nextOpen) + if (!nextOpen) + onPopoverClose() + }} + > + OAuth} /> + +
+ +
+
+
+ ) +} + +const ControlledSettingsHarness = ({ + OAuthClientSettings, + onClose, +}: { + OAuthClientSettings: React.FC + onClose: () => void +}) => { + const [open, setOpen] = React.useState(true) + + return ( + <> +
{String(open)}
+ + + ) +} + describe('OAuthClientSettings', () => { - let OAuthClientSettings: (typeof import('../oauth-client-settings'))['default'] + let OAuthClientSettings: React.FC beforeEach(async () => { vi.clearAllMocks() + mockFormValues = { isCheckValidated: true, values: { __oauth_client__: 'custom', client_id: 'test-id' } } + mockAuthFormProps = undefined const mod = await import('../oauth-client-settings') OAuthClientSettings = mod.default }) @@ -120,6 +169,36 @@ describe('OAuthClientSettings', () => { expect(screen.getByTestId('auth-form')).toBeInTheDocument() }) + it('should render backdrop when nested inside another dialog', () => { + render( + + + + + , + ) + + expect(document.querySelector('.bg-background-overlay')).toBeInTheDocument() + }) + + it('should pass schema defaults to auth form', () => { + render( + , + ) + + expect(mockAuthFormProps?.defaultValues).toMatchObject({ + client_id: 'default-client-id', + }) + }) + it('should call onClose when cancel clicked', () => { const mockOnClose = vi.fn() render( @@ -134,6 +213,33 @@ describe('OAuthClientSettings', () => { expect(mockOnClose).toHaveBeenCalled() }) + it('should close through controlled open state when cancel clicked', async () => { + const mockOnClose = vi.fn() + render() + + fireEvent.click(screen.getByTestId('modal-close')) + + await waitFor(() => { + expect(screen.getByTestId('modal-open-state')).toHaveTextContent('false') + }) + expect(mockOnClose).toHaveBeenCalled() + }) + + it('should close when backdrop is clicked', async () => { + const mockOnClose = vi.fn() + render() + + const backdrop = document.querySelector('.bg-background-overlay') + expect(backdrop).toBeInTheDocument() + + fireEvent.click(backdrop!) + + await waitFor(() => { + expect(screen.getByTestId('modal-open-state')).toHaveTextContent('false') + }) + expect(mockOnClose).toHaveBeenCalled() + }) + it('should save settings on save only button click', async () => { const mockOnClose = vi.fn() const mockOnUpdate = vi.fn() @@ -155,6 +261,38 @@ describe('OAuthClientSettings', () => { }) }) + it('should ignore duplicate save clicks while action is pending', async () => { + const mockOnClose = vi.fn() + let resolveSave: (value: object) => void = () => {} + mockSetPluginOAuthCustomClient.mockImplementationOnce(() => new Promise((resolve) => { + resolveSave = resolve + })) + + render( + , + ) + + fireEvent.click(screen.getByTestId('modal-cancel')) + + await waitFor(() => { + expect(mockSetPluginOAuthCustomClient).toHaveBeenCalledTimes(1) + }) + + fireEvent.click(screen.getByTestId('modal-cancel')) + + expect(mockSetPluginOAuthCustomClient).toHaveBeenCalledTimes(1) + + resolveSave({}) + + await waitFor(() => { + expect(mockOnClose).toHaveBeenCalled() + }) + }) + it('should save and authorize on confirm button click', async () => { const mockOnAuth = vi.fn().mockResolvedValue(undefined) render( @@ -172,6 +310,34 @@ describe('OAuthClientSettings', () => { }) }) + it('should remove custom client settings', async () => { + const mockOnClose = vi.fn() + const mockOnUpdate = vi.fn() + render( + , + ) + + fireEvent.click(screen.getByTestId('modal-extra')) + + await waitFor(() => { + expect(mockDeletePluginOAuthCustomClient).toHaveBeenCalled() + }) + expect(mockOnClose).toHaveBeenCalled() + expect(mockOnUpdate).toHaveBeenCalled() + expect(mockInvalidPluginOAuthClientSchema).toHaveBeenCalled() + expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ + message: 'common.api.actionSuccess', + type: 'success', + })) + }) + it('should render readme entrance when detail is provided', () => { const payload = { ...basePayload, detail: { name: 'Test' } as never } render( @@ -183,4 +349,26 @@ describe('OAuthClientSettings', () => { expect(screen.getByTestId('readme-entrance')).toBeInTheDocument() }) + + it('should stay open when clicking inside the modal from a popover', async () => { + const user = userEvent.setup() + const mockOnClose = vi.fn() + const mockOnPopoverClose = vi.fn() + + render( + , + ) + + const form = await screen.findByTestId('auth-form') + + await user.click(form) + + expect(mockOnClose).not.toHaveBeenCalled() + expect(mockOnPopoverClose).not.toHaveBeenCalled() + expect(screen.getByTestId('modal')).toBeInTheDocument() + }) }) diff --git a/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx b/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx index 44b48db7a2..41ef893db8 100644 --- a/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx @@ -3,11 +3,6 @@ import type { PluginPayload } from '../types' import type { FormSchema } from '@/app/components/base/form/types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { - RiClipboardLine, - RiEqualizer2Line, - RiInformation2Fill, -} from '@remixicon/react' import { memo, useCallback, @@ -40,10 +35,12 @@ export type AddOAuthButtonProps = { schema?: FormSchema[] is_oauth_custom_client_enabled?: boolean is_system_oauth_params_exists?: boolean - client_params?: Record + client_params?: Record redirect_uri?: string } } +type OAuthData = NonNullable + const AddOAuthButton = ({ pluginPayload, buttonVariant = 'primary', @@ -59,22 +56,27 @@ const AddOAuthButton = ({ const { t } = useTranslation() const renderI18nObject = useRenderI18nObject() const [isOAuthSettingsOpen, setIsOAuthSettingsOpen] = useState(false) + const [isOAuthSettingsMounted, setIsOAuthSettingsMounted] = useState(false) const { mutateAsync: getPluginOAuthUrl } = useGetPluginOAuthUrlHook(pluginPayload) const { data, isLoading } = useGetPluginOAuthClientSchemaHook(pluginPayload) - const mergedOAuthData = useMemo(() => { + const mergedOAuthData = useMemo(() => { if (oAuthData) return oAuthData - return data + return data || {} }, [oAuthData, data]) const { schema = [], is_oauth_custom_client_enabled, is_system_oauth_params_exists, - client_params, + client_params = {}, redirect_uri, - } = mergedOAuthData as any || {} + } = mergedOAuthData const isConfigured = is_system_oauth_params_exists || is_oauth_custom_client_enabled + const openOAuthSettings = useCallback(() => { + setIsOAuthSettingsMounted(true) + setIsOAuthSettingsOpen(true) + }, []) const handleOAuth = useCallback(async () => { const { authorization_url } = await getPluginOAuthUrl() @@ -91,7 +93,7 @@ const AddOAuthButton = ({
- +
@@ -107,7 +109,7 @@ const AddOAuthButton = ({ navigator.clipboard.writeText(redirect_uri || '') }} > - +
) @@ -232,10 +234,10 @@ const AddOAuthButton = ({ )} onClick={(e) => { e.stopPropagation() - setIsOAuthSettingsOpen(true) + openOAuthSettings() }} > - +
) @@ -244,18 +246,20 @@ const AddOAuthButton = ({ !isConfigured && ( ) } { - isOAuthSettingsOpen && ( + isOAuthSettingsMounted && ( setIsOAuthSettingsOpen(false)} disabled={disabled || isLoading} diff --git a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx index 290621141c..e01886ccde 100644 --- a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx @@ -140,7 +140,10 @@ const ApiKeyModal = ({ open={open} onOpenChange={handleOpenChange} > - +
diff --git a/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx index f52b76866a..d06ecf1d60 100644 --- a/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx @@ -4,6 +4,7 @@ import type { FormSchema, } from '@/app/components/base/form/types' import { Button } from '@langgenius/dify-ui/button' +import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { toast } from '@langgenius/dify-ui/toast' import { useForm, @@ -17,7 +18,6 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import AuthForm from '@/app/components/base/form/form-scenarios/auth' -import Modal from '@/app/components/base/modal/modal' import { ReadmeEntrance } from '../../readme-panel/entrance' import { ReadmeShowType } from '../../readme-panel/store' import { @@ -26,10 +26,12 @@ import { useSetPluginOAuthCustomClientHook, } from '../hooks/use-credential' -type OAuthClientSettingsProps = { +export type OAuthClientSettingsProps = { pluginPayload: PluginPayload + open?: boolean + onOpenChange?: (open: boolean) => void onClose?: () => void - editValues?: Record + editValues?: Record disabled?: boolean schemas: FormSchema[] onAuth?: () => Promise @@ -38,6 +40,8 @@ type OAuthClientSettingsProps = { } const OAuthClientSettings = ({ pluginPayload, + open = true, + onOpenChange, onClose, editValues, disabled, @@ -53,11 +57,16 @@ const OAuthClientSettings = ({ doingActionRef.current = value setDoingAction(value) }, []) + const handleOpenChange = useCallback((nextOpen: boolean) => { + onOpenChange?.(nextOpen) + if (!nextOpen) + onClose?.() + }, [onClose, onOpenChange]) const defaultValues = schemas.reduce((acc, schema) => { if (schema.default) acc[schema.name] = schema.default return acc - }, {} as Record) + }, {} as Record) const { mutateAsync: setPluginOAuthCustomClient } = useSetPluginOAuthCustomClientHook(pluginPayload) const invalidPluginOAuthClientSchema = useInvalidPluginOAuthClientSchemaHook(pluginPayload) const formRef = useRef(null) @@ -87,6 +96,7 @@ const OAuthClientSettings = ({ }) toast.success(t('api.actionSuccess', { ns: 'common' })) + onOpenChange?.(false) onClose?.() onUpdate?.() invalidPluginOAuthClientSchema() @@ -94,7 +104,7 @@ const OAuthClientSettings = ({ finally { handleSetDoingAction(false) } - }, [onClose, onUpdate, invalidPluginOAuthClientSchema, setPluginOAuthCustomClient, t, handleSetDoingAction]) + }, [onClose, onOpenChange, onUpdate, invalidPluginOAuthClientSchema, setPluginOAuthCustomClient, t, handleSetDoingAction]) const handleConfirmAndAuthorize = useCallback(async () => { await handleConfirm() @@ -110,6 +120,7 @@ const OAuthClientSettings = ({ handleSetDoingAction(true) await deletePluginOAuthCustomClient() toast.success(t('api.actionSuccess', { ns: 'common' })) + onOpenChange?.(false) onClose?.() onUpdate?.() invalidPluginOAuthClientSchema() @@ -117,53 +128,89 @@ const OAuthClientSettings = ({ finally { handleSetDoingAction(false) } - }, [onUpdate, invalidPluginOAuthClientSchema, deletePluginOAuthCustomClient, t, handleSetDoingAction, onClose]) + }, [onUpdate, invalidPluginOAuthClientSchema, deletePluginOAuthCustomClient, t, handleSetDoingAction, onClose, onOpenChange]) const form = useForm({ defaultValues: editValues || defaultValues, }) const __oauth_client__ = useStore(form.store, s => s.values.__oauth_client__) + const isDisabled = disabled || doingAction + return ( - - -
- ) - } - containerClassName="pt-0" - wrapperClassName="z-1002!" - clickOutsideNotClose={true} + - {pluginPayload.detail && ( - - )} - - + +
+
+ + {t('auth.oauthClientSettings', { ns: 'plugin' })} + + +
+
+ {pluginPayload.detail && ( + + )} + +
+
+
+ {__oauth_client__ === 'custom' && hasOriginalClientParams && ( + + )} +
+
+ +
+ + +
+
+
+
+
) } From 3b1458c08f6a112ec7e3b24a87aad69ef4b98178 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Thu, 30 Apr 2026 07:21:20 +0900 Subject: [PATCH 4/8] refactor: port WorkflowDraftVariableFile (#30923) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/models/workflow.py | 31 ++++--- .../workflow_draft_variable_service.py | 4 +- .../app/workflow_draft_variables_test.py | 40 +++++---- .../workflow/test_draft_var_loader_simple.py | 90 ++----------------- .../test_workflow_draft_variable_service.py | 11 ++- 5 files changed, 54 insertions(+), 122 deletions(-) diff --git a/api/models/workflow.py b/api/models/workflow.py index cb1723440b..7936c06a5a 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -1568,12 +1568,14 @@ class WorkflowDraftVariable(Base): ), ) - # Relationship to WorkflowDraftVariableFile + # WorkflowDraftVariableFile uses TypeBase while WorkflowDraftVariable uses Base, so the relationship + # must resolve the class object lazily instead of relying on string lookup across registries. variable_file: Mapped[Optional["WorkflowDraftVariableFile"]] = orm.relationship( + lambda: WorkflowDraftVariableFile, foreign_keys=[file_id], lazy="raise", uselist=False, - primaryjoin="WorkflowDraftVariableFile.id == WorkflowDraftVariable.file_id", + primaryjoin=lambda: orm.foreign(WorkflowDraftVariable.file_id) == WorkflowDraftVariableFile.id, ) # Cache for deserialized value @@ -1892,7 +1894,7 @@ class WorkflowDraftVariable(Base): return self.last_edited_at is not None -class WorkflowDraftVariableFile(Base): +class WorkflowDraftVariableFile(TypeBase): """Stores metadata about files associated with large workflow draft variables. This model acts as an intermediary between WorkflowDraftVariable and UploadFile, @@ -1906,18 +1908,7 @@ class WorkflowDraftVariableFile(Base): __tablename__ = "workflow_draft_variable_files" # Primary key - id: Mapped[str] = mapped_column( - StringUUID, - primary_key=True, - default=lambda: str(uuidv7()), - ) - - created_at: Mapped[datetime] = mapped_column( - DateTime, - nullable=False, - default=naive_utc_now, - server_default=func.current_timestamp(), - ) + id: Mapped[str] = mapped_column(StringUUID, primary_key=True, default_factory=lambda: str(uuidv7()), init=False) tenant_id: Mapped[str] = mapped_column( StringUUID, @@ -1969,15 +1960,23 @@ class WorkflowDraftVariableFile(Base): nullable=False, ) - # Relationship to UploadFile + # Rows are created with `upload_file_id`; callers should load this relationship explicitly when needed. upload_file: Mapped["UploadFile"] = orm.relationship( UploadFile, foreign_keys=[upload_file_id], lazy="raise", + init=False, uselist=False, primaryjoin=lambda: orm.foreign(WorkflowDraftVariableFile.upload_file_id) == UploadFile.id, ) + created_at: Mapped[datetime] = mapped_column( + DateTime, + nullable=False, + default_factory=naive_utc_now, + server_default=func.current_timestamp(), + ) + def is_system_variable_editable(name: str) -> bool: return name in _EDITABLE_SYSTEM_VARIABLE diff --git a/api/services/workflow_draft_variable_service.py b/api/services/workflow_draft_variable_service.py index 96f936ff9b..a55448e352 100644 --- a/api/services/workflow_draft_variable_service.py +++ b/api/services/workflow_draft_variable_service.py @@ -1083,10 +1083,9 @@ class DraftVariableSaver: mimetype=content_type, user=self._user, ) - + assert self._user.current_tenant_id # Create WorkflowDraftVariableFile record variable_file = WorkflowDraftVariableFile( - id=uuidv7(), upload_file_id=upload_file.id, size=original_size, length=original_length, @@ -1095,6 +1094,7 @@ class DraftVariableSaver: tenant_id=self._user.current_tenant_id, user_id=self._user.id, ) + variable_file.id = str(uuidv7()) engine = bind = self._session.get_bind() assert isinstance(engine, Engine) with sessionmaker(bind=engine, expire_on_commit=False).begin() as session: diff --git a/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py b/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py index 22b80b748e..62fa82e339 100644 --- a/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py +++ b/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py @@ -1,7 +1,7 @@ import uuid from collections import OrderedDict from typing import Any, NamedTuple -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest from flask_restx import marshal @@ -29,15 +29,18 @@ class TestWorkflowDraftVariableFields: def test_serialize_full_content(self): """Test that _serialize_full_content uses pre-loaded relationships.""" # Create mock objects with relationships pre-loaded - mock_variable_file = MagicMock(spec=WorkflowDraftVariableFile) - mock_variable_file.size = 100000 - mock_variable_file.length = 50 - mock_variable_file.value_type = SegmentType.OBJECT - mock_variable_file.upload_file_id = "test-upload-file-id" - - mock_variable = MagicMock(spec=WorkflowDraftVariable) - mock_variable.file_id = "test-file-id" - mock_variable.variable_file = mock_variable_file + mock_variable = WorkflowDraftVariable( + file_id="test-file-id", + variable_file=WorkflowDraftVariableFile( + size=100000, + length=50, + value_type=SegmentType.OBJECT, + upload_file_id="test-upload-file-id", + tenant_id=str(uuid.uuid4()), + app_id=str(uuid.uuid4()), + user_id=str(uuid.uuid4()), + ), + ) # Mock the file helpers with patch("controllers.console.app.workflow_draft_variable.file_helpers", autospec=True) as mock_file_helpers: @@ -84,7 +87,7 @@ class TestWorkflowDraftVariableFields: expected_without_value: OrderedDict[str, Any] = OrderedDict( { - "id": str(conv_var.id), + "id": conv_var.id, "type": conv_var.get_variable_type().value, "name": "conv_var", "description": "", @@ -117,7 +120,7 @@ class TestWorkflowDraftVariableFields: expected_without_value = OrderedDict( { - "id": str(sys_var.id), + "id": sys_var.id, "type": sys_var.get_variable_type().value, "name": "sys_var", "description": "", @@ -149,7 +152,7 @@ class TestWorkflowDraftVariableFields: expected_without_value: OrderedDict[str, Any] = OrderedDict( { - "id": str(node_var.id), + "id": node_var.id, "type": node_var.get_variable_type().value, "name": "node_var", "description": "", @@ -180,19 +183,22 @@ class TestWorkflowDraftVariableFields: node_var.id = str(uuid.uuid4()) node_var.last_edited_at = naive_utc_now() variable_file = WorkflowDraftVariableFile( - id=str(uuidv7()), upload_file_id=str(uuid.uuid4()), size=1024, length=10, value_type=SegmentType.ARRAY_STRING, + tenant_id=str(uuidv7()), + app_id=str(uuidv7()), + user_id=str(uuidv7()), ) + variable_file.id = str(uuidv7()) node_var.variable_file = variable_file node_var.file_id = variable_file.id expected_without_value: OrderedDict[str, Any] = OrderedDict( { - "id": str(node_var.id), - "type": node_var.get_variable_type().value, + "id": node_var.id, + "type": node_var.get_variable_type(), "name": "node_var", "description": "", "selector": ["test_node", "node_var"], @@ -235,7 +241,7 @@ class TestWorkflowDraftVariableList: node_var.id = str(uuid.uuid4()) node_var_dict = OrderedDict( { - "id": str(node_var.id), + "id": node_var.id, "type": node_var.get_variable_type().value, "name": "test_var", "description": "", diff --git a/api/tests/unit_tests/services/workflow/test_draft_var_loader_simple.py b/api/tests/unit_tests/services/workflow/test_draft_var_loader_simple.py index 497c26a9b3..fb5cf7bc6e 100644 --- a/api/tests/unit_tests/services/workflow/test_draft_var_loader_simple.py +++ b/api/tests/unit_tests/services/workflow/test_draft_var_loader_simple.py @@ -33,42 +33,6 @@ class TestDraftVarLoaderSimple: fallback_variables=[], ) - def test_load_offloaded_variable_string_type_unit(self, draft_var_loader): - """Test _load_offloaded_variable with string type - isolated unit test.""" - # Create mock objects - upload_file = Mock(spec=UploadFile) - upload_file.key = "storage/key/test.txt" - - variable_file = Mock(spec=WorkflowDraftVariableFile) - variable_file.value_type = SegmentType.STRING - variable_file.upload_file = upload_file - - draft_var = Mock(spec=WorkflowDraftVariable) - draft_var.id = "draft-var-id" - draft_var.node_id = "test-node-id" - draft_var.name = "test_variable" - draft_var.description = "test description" - draft_var.get_selector.return_value = ["test-node-id", "test_variable"] - draft_var.variable_file = variable_file - - test_content = "This is the full string content" - - with patch("services.workflow_draft_variable_service.storage") as mock_storage: - mock_storage.load.return_value = test_content.encode() - - # Execute the method - selector_tuple, variable = draft_var_loader._load_offloaded_variable(draft_var) - - # Verify results - assert selector_tuple == ("test-node-id", "test_variable") - assert variable.id == "draft-var-id" - assert variable.name == "test_variable" - assert variable.description == "test description" - assert variable.value == test_content - - # Verify storage was called correctly - mock_storage.load.assert_called_once_with("storage/key/test.txt") - def test_load_offloaded_variable_object_type_unit(self, draft_var_loader): """Test _load_offloaded_variable with object type - isolated unit test.""" # Create mock objects @@ -139,47 +103,6 @@ class TestDraftVarLoaderSimple: result = draft_var_loader._selector_to_tuple(selector) assert result == ("node_id", "var_name") - def test_load_offloaded_variable_number_type_unit(self, draft_var_loader): - """Test _load_offloaded_variable with number type - isolated unit test.""" - # Create mock objects - upload_file = Mock(spec=UploadFile) - upload_file.key = "storage/key/test_number.json" - - variable_file = Mock(spec=WorkflowDraftVariableFile) - variable_file.value_type = SegmentType.NUMBER - variable_file.upload_file = upload_file - - draft_var = Mock(spec=WorkflowDraftVariable) - draft_var.id = "draft-var-id" - draft_var.node_id = "test-node-id" - draft_var.name = "test_number" - draft_var.description = "test number description" - draft_var.get_selector.return_value = ["test-node-id", "test_number"] - draft_var.variable_file = variable_file - - test_number = 123.45 - test_json_content = json.dumps(test_number) - - with patch("services.workflow_draft_variable_service.storage") as mock_storage: - mock_storage.load.return_value = test_json_content.encode() - from graphon.variables.segments import FloatSegment - - mock_segment = FloatSegment(value=test_number) - draft_var.build_segment_from_serialized_value.return_value = mock_segment - - # Execute the method - selector_tuple, variable = draft_var_loader._load_offloaded_variable(draft_var) - - # Verify results - assert selector_tuple == ("test-node-id", "test_number") - assert variable.id == "draft-var-id" - assert variable.name == "test_number" - assert variable.description == "test number description" - - # Verify method calls - mock_storage.load.assert_called_once_with("storage/key/test_number.json") - draft_var.build_segment_from_serialized_value.assert_called_once_with(SegmentType.NUMBER, test_number) - def test_load_offloaded_variable_array_type_unit(self, draft_var_loader): """Test _load_offloaded_variable with array type - isolated unit test.""" # Create mock objects @@ -229,12 +152,13 @@ class TestDraftVarLoaderSimple: variable_file.value_type = SegmentType.FILE variable_file.upload_file = upload_file - draft_var = WorkflowDraftVariable() - draft_var.id = "draft-var-id" - draft_var.app_id = "app-1" - draft_var.node_id = "test-node-id" - draft_var.name = "test_file" - draft_var.description = "test file description" + draft_var = WorkflowDraftVariable( + id="draft-var-id", + app_id="app-1", + node_id="test-node-id", + name="test_file", + description="test file description", + ) draft_var._set_selector(["test-node-id", "test_file"]) draft_var.variable_file = variable_file diff --git a/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py b/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py index b14d767568..663eec6a06 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py @@ -200,7 +200,7 @@ class TestDraftVariableSaver: user=mock_user, ) - def test_draft_saver_with_small_variables(self, draft_saver, mock_session): + def test_draft_saver_with_small_variables(self, draft_saver: DraftVariableSaver, mock_session): with patch( "services.workflow_draft_variable_service.DraftVariableSaver._try_offload_large_variable", autospec=True ) as _mock_try_offload: @@ -212,18 +212,21 @@ class TestDraftVariableSaver: assert draft_var.file_id is None _mock_try_offload.return_value = None - def test_draft_saver_with_large_variables(self, draft_saver, mock_session): + def test_draft_saver_with_large_variables(self, draft_saver: DraftVariableSaver, mock_session): with patch( "services.workflow_draft_variable_service.DraftVariableSaver._try_offload_large_variable", autospec=True ) as _mock_try_offload: mock_segment = StringSegment(value="small value") mock_draft_var_file = WorkflowDraftVariableFile( - id=str(uuidv7()), + tenant_id=str(uuidv7()), + app_id=str(uuidv7()), + user_id=str(uuidv7()), size=1024, length=10, value_type=SegmentType.ARRAY_STRING, - upload_file_id=str(uuid.uuid4()), + upload_file_id=str(uuidv7()), ) + mock_draft_var_file.id = str(uuidv7()) _mock_try_offload.return_value = mock_segment, mock_draft_var_file draft_var = draft_saver._create_draft_variable(name="small_var", value=mock_segment, visible=True) From fe2f7a8920201331504a5520f8a41a421def44fb Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:29:17 +0800 Subject: [PATCH 5/8] refactor(web): migrate short tooltips to dify-ui (#35715) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- eslint-suppressions.json | 212 +----------------- .../[appId]/overview/tracing/config-popup.tsx | 13 +- .../model-parameter-trigger.spec.tsx | 22 +- .../model-parameter-trigger.tsx | 16 +- .../prompt-value-panel/index.tsx | 29 ++- web/app/components/app/log/list.tsx | 33 +-- .../app/overview/embedded/index.tsx | 33 +-- web/app/components/base/audio-btn/index.tsx | 51 +++-- .../chat/chat-with-history/header/index.tsx | 52 +++-- .../chat/embedded-chatbot/header/index.tsx | 86 ++++--- .../components/base/copy-feedback/index.tsx | 54 +++-- .../annotation-ctrl-button.tsx | 32 ++- .../new-feature-panel/feature-bar.tsx | 155 ++++++++----- .../new-feature-panel/feature-card.tsx | 15 +- .../base/file-uploader/file-list-in-log.tsx | 48 ++-- .../base/file-uploader/pdf-preview.tsx | 65 ++++-- .../base/image-uploader/image-list.tsx | 15 +- .../base/image-uploader/image-preview.tsx | 132 +++++++---- .../base/new-audio-button/index.tsx | 37 +-- web/app/components/base/qrcode/index.tsx | 47 ++-- .../components/general-chunking-options.tsx | 15 +- .../components/document-table-row.tsx | 34 ++- .../metadata/components/doc-type-selector.tsx | 23 +- .../components/query-input/textarea.tsx | 23 +- .../components/dataset-card-footer.tsx | 36 ++- .../edit-metadata-batch/edited-beacon.tsx | 17 +- .../settings/index-method/keyword-number.tsx | 15 +- .../components/explore/try-app/app/chat.tsx | 19 +- .../__tests__/feature-icon.spec.tsx | 5 +- .../model-selector/feature-icon.tsx | 126 ++++++----- .../provider-added-card/cooldown-timer.tsx | 13 +- .../model-load-balancing-configs.tsx | 53 +++-- .../provider-added-card/priority-use-tip.tsx | 19 +- .../plugins/plugin-auth/authorize/index.tsx | 16 +- .../authorized/__tests__/item.spec.tsx | 2 +- .../plugins/plugin-auth/authorized/item.tsx | 108 +++++---- .../multiple-tool-selector/index.tsx | 15 +- .../subscription-list/create/index.tsx | 78 +++++-- .../subscription-list/list-view.tsx | 15 +- .../subscription-list/selector-view.tsx | 15 +- .../components/__tests__/tool-item.spec.tsx | 21 +- .../tool-selector/components/tool-item.tsx | 19 +- .../components/plugins/plugin-page/index.tsx | 27 ++- .../nodes/_base/components/config-vision.tsx | 17 +- .../components/input-support-select-var.tsx | 19 +- .../nodes/_base/components/setting-item.tsx | 18 +- .../object-child-tree-panel/picker/field.tsx | 42 ++-- .../components/delivery-method/index.tsx | 17 +- .../components/index-method.tsx | 13 +- .../json-schema-generator/prompt-editor.tsx | 15 +- .../components/monthly-days-selector.tsx | 15 +- .../panel/debug-and-preview/index.tsx | 36 +-- .../workflow/variable-inspect/group.tsx | 32 ++- .../workflow/variable-inspect/right.tsx | 78 ++++--- .../workflow/variable-inspect/trigger.tsx | 25 ++- 55 files changed, 1264 insertions(+), 924 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index f8208a8265..ced0d27afe 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -119,11 +119,6 @@ "count": 3 } }, - "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx": { "ts/no-explicit-any": { "count": 1 @@ -526,11 +521,6 @@ "count": 2 } }, - "web/app/components/app/configuration/debug/debug-with-multiple-model/model-parameter-trigger.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx": { "ts/no-explicit-any": { "count": 8 @@ -559,11 +549,6 @@ "count": 1 } }, - "web/app/components/app/configuration/prompt-value-panel/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/app/configuration/prompt-value-panel/utils.ts": { "ts/no-explicit-any": { "count": 1 @@ -620,9 +605,6 @@ } }, "web/app/components/app/log/list.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react/set-state-in-effect": { "count": 6 }, @@ -645,7 +627,7 @@ }, "web/app/components/app/overview/embedded/index.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 }, "react/set-state-in-effect": { "count": 1 @@ -791,11 +773,6 @@ "count": 3 } }, - "web/app/components/base/audio-btn/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/audio-gallery/AudioPlayer.tsx": { "ts/no-explicit-any": { "count": 2 @@ -871,9 +848,6 @@ } }, "web/app/components/base/chat/chat-with-history/header/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 2 } @@ -991,11 +965,6 @@ "count": 7 } }, - "web/app/components/base/chat/embedded-chatbot/header/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/chat/embedded-chatbot/hooks.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 3 @@ -1037,11 +1006,6 @@ "count": 1 } }, - "web/app/components/base/copy-feedback/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/date-and-time-picker/hooks.ts": { "react/no-unnecessary-use-prefix": { "count": 2 @@ -1093,11 +1057,6 @@ "count": 1 } }, - "web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx": { "no-restricted-imports": { "count": 1 @@ -1123,15 +1082,7 @@ "count": 2 } }, - "web/app/components/base/features/new-feature-panel/feature-bar.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/features/new-feature-panel/feature-card.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 5 } @@ -1168,9 +1119,6 @@ } }, "web/app/components/base/file-uploader/file-list-in-log.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react/no-missing-key": { "count": 1 } @@ -1193,11 +1141,6 @@ "count": 2 } }, - "web/app/components/base/file-uploader/pdf-preview.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/file-uploader/store.tsx": { "react-refresh/only-export-components": { "count": 4 @@ -1593,15 +1536,7 @@ "count": 1 } }, - "web/app/components/base/image-uploader/image-list.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/image-uploader/image-preview.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -1764,9 +1699,6 @@ } }, "web/app/components/base/new-audio-button/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -1949,11 +1881,6 @@ "count": 1 } }, - "web/app/components/base/qrcode/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/radio-card/index.stories.tsx": { "ts/no-explicit-any": { "count": 1 @@ -2262,11 +2189,6 @@ "count": 1 } }, - "web/app/components/datasets/create/step-two/components/general-chunking-options.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/create/step-two/components/index.ts": { "no-barrel-files/no-barrel-files": { "count": 5 @@ -2374,11 +2296,6 @@ "count": 1 } }, - "web/app/components/datasets/documents/components/document-list/components/document-table-row.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/documents/components/document-list/components/index.ts": { "no-barrel-files/no-barrel-files": { "count": 2 @@ -2553,11 +2470,6 @@ "count": 1 } }, - "web/app/components/datasets/documents/detail/metadata/components/doc-type-selector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/documents/detail/metadata/components/metadata-field-list.tsx": { "ts/no-non-null-asserted-optional-chain": { "count": 1 @@ -2619,11 +2531,6 @@ "count": 1 } }, - "web/app/components/datasets/hit-testing/components/query-input/textarea.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/hit-testing/components/result-item-external.tsx": { "no-restricted-imports": { "count": 1 @@ -2639,21 +2546,11 @@ "count": 1 } }, - "web/app/components/datasets/list/dataset-card/components/dataset-card-footer.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/list/dataset-card/hooks/use-dataset-card-state.ts": { "react/set-state-in-effect": { "count": 1 } }, - "web/app/components/datasets/metadata/edit-metadata-batch/edited-beacon.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/metadata/edit-metadata-batch/input-combined.tsx": { "ts/no-explicit-any": { "count": 2 @@ -2722,11 +2619,6 @@ "count": 1 } }, - "web/app/components/datasets/settings/index-method/keyword-number.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/settings/summary-index-setting.tsx": { "no-restricted-imports": { "count": 1 @@ -2790,11 +2682,6 @@ "count": 1 } }, - "web/app/components/explore/try-app/app/chat.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/explore/try-app/index.tsx": { "no-restricted-imports": { "count": 1 @@ -3043,15 +2930,7 @@ "count": 2 } }, - "web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/header/account-setting/model-provider-page/provider-added-card/cooldown-timer.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react/set-state-in-effect": { "count": 2 } @@ -3067,9 +2946,6 @@ } }, "web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 5 } @@ -3085,11 +2961,6 @@ "count": 3 } }, - "web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-use-tip.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/header/account-setting/model-provider-page/utils.ts": { "no-barrel-files/no-barrel-files": { "count": 2 @@ -3182,20 +3053,12 @@ "count": 1 } }, - "web/app/components/plugins/plugin-auth/authorize/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/plugin-auth/authorized-in-node.tsx": { "ts/no-explicit-any": { "count": 1 } }, "web/app/components/plugins/plugin-auth/authorized/item.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -3290,9 +3153,6 @@ } }, "web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -3325,9 +3185,6 @@ "web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx": { "no-barrel-files/no-barrel-files": { "count": 3 - }, - "no-restricted-imports": { - "count": 1 } }, "web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx": { @@ -3363,11 +3220,6 @@ "count": 2 } }, - "web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/plugin-detail-panel/subscription-list/log-viewer.tsx": { "erasable-syntax-only/enums": { "count": 1 @@ -3376,11 +3228,6 @@ "count": 2 } }, - "web/app/components/plugins/plugin-detail-panel/subscription-list/selector-view.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx": { "no-restricted-imports": { "count": 1 @@ -3403,7 +3250,7 @@ }, "web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 } }, "web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/index.ts": { @@ -3444,11 +3291,6 @@ "count": 2 } }, - "web/app/components/plugins/plugin-page/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx": { "react/set-state-in-effect": { "count": 2 @@ -4090,11 +3932,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/_base/components/config-vision.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx": { "react/set-state-in-effect": { "count": 1 @@ -4143,9 +3980,6 @@ } }, "web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -4209,9 +4043,6 @@ } }, "web/app/components/workflow/nodes/_base/components/setting-item.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -4226,11 +4057,6 @@ "count": 8 } }, - "web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx": { "ts/no-non-null-asserted-optional-chain": { "count": 1 @@ -4544,11 +4370,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/human-input/components/delivery-method/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/human-input/components/delivery-method/method-item.tsx": { "no-restricted-imports": { "count": 1 @@ -4664,11 +4485,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/knowledge-base/components/index-method.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/hooks.tsx": { "ts/no-explicit-any": { "count": 4 @@ -4806,11 +4622,6 @@ "count": 2 } }, - "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/prompt-editor.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx": { "react-refresh/only-export-components": { "count": 2 @@ -5144,11 +4955,6 @@ "count": 7 } }, - "web/app/components/workflow/nodes/trigger-schedule/components/monthly-days-selector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/trigger-schedule/default.ts": { "regexp/no-unused-capturing-group": { "count": 2 @@ -5286,11 +5092,6 @@ "count": 12 } }, - "web/app/components/workflow/panel/debug-and-preview/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/panel/env-panel/variable-modal.tsx": { "no-restricted-imports": { "count": 1 @@ -5515,9 +5316,6 @@ } }, "web/app/components/workflow/variable-inspect/group.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 2 } @@ -5541,17 +5339,11 @@ } }, "web/app/components/workflow/variable-inspect/right.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 3 } }, "web/app/components/workflow/variable-inspect/trigger.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx index 471ab86e12..2b0f978906 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx @@ -3,12 +3,12 @@ import type { FC, JSX } from 'react' import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type' import { cn } from '@langgenius/dify-ui/cn' import { Switch } from '@langgenius/dify-ui/switch' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useBoolean } from 'ahooks' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' -import Tooltip from '@/app/components/base/tooltip' import Indicator from '@/app/components/header/indicator' import ProviderConfigModal from './provider-config-modal' import ProviderPanel from './provider-panel' @@ -338,10 +338,13 @@ const ConfigPopup: FC = ({ <> {providerAllNotConfigured ? ( - - {switchContent} + + + + {t(`${I18N_PREFIX}.disabledTip`, { ns: 'app' })} + ) : switchContent} diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/model-parameter-trigger.spec.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/model-parameter-trigger.spec.tsx index 2edc6191de..b99a2034b6 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/model-parameter-trigger.spec.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/model-parameter-trigger.spec.tsx @@ -5,6 +5,7 @@ import type { ModelProvider, } from '@/app/components/header/account-setting/model-provider-page/declarations' import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { createMockProviderContextValue } from '@/__mocks__/provider-context' import { ConfigurationMethodEnum, @@ -86,12 +87,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-name' ), })) -vi.mock('@/app/components/base/tooltip', () => ({ - default: ({ children, popupContent }: { children: ReactNode, popupContent: string }) => ( -
{children}
- ), -})) - const createModelAndParameter = (overrides: Partial = {}): ModelAndParameter => ({ id: 'model-1', model: 'gpt-3.5-turbo', @@ -385,14 +380,15 @@ describe('ModelParameterTrigger', () => { expect(screen.getByText('common.modelProvider.selectModel')).toBeInTheDocument() }) - it('should render configured model id and incompatible tooltip when model is missing from the provider list', () => { + it('should render configured model id and incompatible tooltip when model is missing from the provider list', async () => { renderComponent() expect(screen.getByText('gpt-3.5-turbo')).toBeInTheDocument() - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'common.modelProvider.selector.incompatibleTip') + await userEvent.hover(screen.getByLabelText('common.modelProvider.selector.incompatibleTip')) + expect(await screen.findByText('common.modelProvider.selector.incompatibleTip')).toBeInTheDocument() }) - it('should render configure required tooltip for no-configure status', () => { + it('should render configure required tooltip for no-configure status', async () => { const { unmount } = renderComponent() const triggerContent = capturedModalProps?.renderTrigger({ open: false, @@ -403,10 +399,11 @@ describe('ModelParameterTrigger', () => { unmount() render(<>{triggerContent}) - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'common.modelProvider.selector.configureRequired') + await userEvent.hover(screen.getByLabelText('common.modelProvider.selector.configureRequired')) + expect(await screen.findByText('common.modelProvider.selector.configureRequired')).toBeInTheDocument() }) - it('should render disabled tooltip for disabled status', () => { + it('should render disabled tooltip for disabled status', async () => { const { unmount } = renderComponent() const triggerContent = capturedModalProps?.renderTrigger({ open: false, @@ -417,7 +414,8 @@ describe('ModelParameterTrigger', () => { unmount() render(<>{triggerContent}) - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'common.modelProvider.selector.disabled') + await userEvent.hover(screen.getByLabelText('common.modelProvider.selector.disabled')) + expect(await screen.findByText('common.modelProvider.selector.disabled')).toBeInTheDocument() }) it('should apply expanded and warning styles when the trigger is open for a non-active status', () => { diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/model-parameter-trigger.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/model-parameter-trigger.tsx index a870538edc..bf889e20ea 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/model-parameter-trigger.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/model-parameter-trigger.tsx @@ -1,9 +1,9 @@ import type { FC } from 'react' import type { ModelAndParameter } from '../types' import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { memo } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { DERIVED_MODEL_STATUS_BADGE_I18N, DERIVED_MODEL_STATUS_TOOLTIP_I18N, @@ -132,8 +132,18 @@ const ModelParameterTrigger: FC = ({ { !isEmpty && !isActive && statusLabelKey && ( - - + + + )} + /> + + {t((statusTooltipKey || statusLabelKey) as 'modelProvider.selector.incompatible', { ns: 'common' })} + ) } diff --git a/web/app/components/app/configuration/prompt-value-panel/index.tsx b/web/app/components/app/configuration/prompt-value-panel/index.tsx index c3ba69bf34..c2a438b5e9 100644 --- a/web/app/components/app/configuration/prompt-value-panel/index.tsx +++ b/web/app/components/app/configuration/prompt-value-panel/index.tsx @@ -5,6 +5,7 @@ import type { VisionFile, VisionSettings } from '@/types/app' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiArrowDownSLine, RiArrowRightSLine, @@ -19,7 +20,6 @@ import FeatureBar from '@/app/components/base/features/new-feature-panel/feature import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader' import Input from '@/app/components/base/input' import Textarea from '@/app/components/base/textarea' -import Tooltip from '@/app/components/base/tooltip' import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input' import ConfigContext from '@/context/debug-configuration' import { AppModeEnum, ModelModeType } from '@/types/app' @@ -224,16 +224,23 @@ const PromptValuePanel: FC = ({
{canNotRun && ( - - + + onSend?.()} + className="w-[96px]" + > + )} {!canNotRun && ( diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index da96d41804..1633d53ccc 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -10,6 +10,7 @@ import { } from '@heroicons/react/24/outline' import { cn } from '@langgenius/dify-ui/cn' import { toast } from '@langgenius/dify-ui/toast' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiCloseLine, RiEditFill } from '@remixicon/react' import dayjs from 'dayjs' import timezone from 'dayjs/plugin/timezone' @@ -30,7 +31,6 @@ import CopyIcon from '@/app/components/base/copy-icon' import Drawer from '@/app/components/base/drawer' import Loading from '@/app/components/base/loading' import MessageLogModal from '@/app/components/base/message-log-modal' -import Tooltip from '@/app/components/base/tooltip' import { WorkflowContextProvider } from '@/app/components/workflow/context' import { useAppContext } from '@/context/app-context' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' @@ -409,10 +409,15 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
{isChatMode ? t('detail.conversationId', { ns: 'appLog' }) : t('detail.time', { ns: 'appLog' })}
{isChatMode && (
- -
{detail.id}
+ + {detail.id}
+ )} + /> + + {detail.id} +
@@ -769,18 +774,20 @@ const ConversationList: FC = ({ logs, appDetail, onRefresh }) // Annotated data needs to be highlighted const renderTdValue = (value: string | number | null, isEmptyStyle: boolean, isHighlight = false, annotation?: LogAnnotation) => { return ( - + + {value || '-'} +
+ )} + /> + {`${t('detail.annotationTip', { ns: 'appLog', user: annotation?.account?.name })} ${formatTime(annotation?.created_at || dayjs().unix(), 'MM-DD hh:mm A')}`} - )} - popupClassName={(isHighlight && !isChatMode) ? '' : 'hidden!'} - > -
- {value || '-'} -
+
) } diff --git a/web/app/components/app/overview/embedded/index.tsx b/web/app/components/app/overview/embedded/index.tsx index 029566587e..e810c7f1eb 100644 --- a/web/app/components/app/overview/embedded/index.tsx +++ b/web/app/components/app/overview/embedded/index.tsx @@ -1,5 +1,6 @@ import type { SiteInfo } from '@/models/share' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiClipboardFill, RiClipboardLine, @@ -11,7 +12,6 @@ import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import { useThemeContext } from '@/app/components/base/chat/embedded-chatbot/theme/theme-context' import Modal from '@/app/components/base/modal' -import Tooltip from '@/app/components/base/tooltip' import { IS_CE_EDITION } from '@/config' import { useAppContext } from '@/context/app-context' import { basePath } from '@/utils/var' @@ -174,21 +174,24 @@ const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, classNam
{t(`${prefixEmbedded}.${option}`, { ns: 'appOverview' })}
- + +
+ {isCopied[option] && } + {!isCopied[option] && } +
+ + )} + /> + + {(isCopied[option] ? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' }) - : t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || '' - } - > - -
- {isCopied[option] && } - {!isCopied[option] && } -
-
+ : t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''} +
diff --git a/web/app/components/base/audio-btn/index.tsx b/web/app/components/base/audio-btn/index.tsx index 47fefe19e5..3ca453213d 100644 --- a/web/app/components/base/audio-btn/index.tsx +++ b/web/app/components/base/audio-btn/index.tsx @@ -1,9 +1,9 @@ 'use client' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { t } from 'i18next' import { useState } from 'react' import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager' import Loading from '@/app/components/base/loading' -import Tooltip from '@/app/components/base/tooltip' import { useParams, usePathname } from '@/next/navigation' import s from './style.module.css' @@ -82,27 +82,34 @@ const AudioBtn = ({ return (
- - + + + + + )} + /> + + {tooltipContent} +
) diff --git a/web/app/components/base/chat/chat-with-history/header/index.tsx b/web/app/components/base/chat/chat-with-history/header/index.tsx index 95b6146afa..75cfab4228 100644 --- a/web/app/components/base/chat/chat-with-history/header/index.tsx +++ b/web/app/components/base/chat/chat-with-history/header/index.tsx @@ -9,6 +9,7 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiEditBoxLine, RiLayoutRight2Line, @@ -20,7 +21,6 @@ import ActionButton, { ActionButtonState } from '@/app/components/base/action-bu import AppIcon from '@/app/components/base/app-icon' import ViewFormDropdown from '@/app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown' import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal' -import Tooltip from '@/app/components/base/tooltip' import { useChatWithHistoryContext, } from '../context' @@ -117,31 +117,41 @@ const Header = () => {
{isSidebarCollapsed && ( - -
- - - -
+ + + + + +
+ )} + /> + + {t('chat.newChatTip', { ns: 'share' })} + )}
{currentConversationId && ( - - - - + + + + + )} + /> + + {t('chat.resetChat', { ns: 'share' })} + )} {currentConversationId && inputsForms.length > 0 && ( diff --git a/web/app/components/base/chat/embedded-chatbot/header/index.tsx b/web/app/components/base/chat/embedded-chatbot/header/index.tsx index 598e3068de..6d0cc9bc06 100644 --- a/web/app/components/base/chat/embedded-chatbot/header/index.tsx +++ b/web/app/components/base/chat/embedded-chatbot/header/index.tsx @@ -1,6 +1,7 @@ import type { FC } from 'react' import type { Theme } from '../theme/theme-context' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useSuspenseQuery } from '@tanstack/react-query' import * as React from 'react' import { useCallback, useEffect, useState } from 'react' @@ -9,7 +10,6 @@ import ActionButton from '@/app/components/base/action-button' import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown' import Divider from '@/app/components/base/divider' import DifyLogo from '@/app/components/base/logo/dify-logo' -import Tooltip from '@/app/components/base/tooltip' import { systemFeaturesQueryOptions } from '@/service/system-features' import { isClient } from '@/utils/client' import { @@ -111,26 +111,36 @@ const Header: FC = ({ )} { showToggleExpandButton && ( - - - { - expanded - ?
- :
- } - + + + { + expanded + ?
+ :
+ } + + )} + /> + + {expanded ? t('chat.collapse', { ns: 'share' }) : t('chat.expand', { ns: 'share' })} + ) } {currentConversationId && allowResetChat && ( - - -
- + + +
+ + )} + /> + + {t('chat.resetChat', { ns: 'share' })} + )} {currentConversationId && inputsForms.length > 0 && !allInputsHidden && ( @@ -158,26 +168,36 @@ const Header: FC = ({
{ showToggleExpandButton && ( - - - { - expanded - ?
- :
- } - + + + { + expanded + ?
+ :
+ } + + )} + /> + + {expanded ? t('chat.collapse', { ns: 'share' }) : t('chat.expand', { ns: 'share' })} + ) } {currentConversationId && allowResetChat && ( - - -
- + + +
+ + )} + /> + + {t('chat.resetChat', { ns: 'share' })} + )} {currentConversationId && inputsForms.length > 0 && !allInputsHidden && ( diff --git a/web/app/components/base/copy-feedback/index.tsx b/web/app/components/base/copy-feedback/index.tsx index 431b697a6a..860b88b245 100644 --- a/web/app/components/base/copy-feedback/index.tsx +++ b/web/app/components/base/copy-feedback/index.tsx @@ -1,4 +1,5 @@ 'use client' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiClipboardFill, RiClipboardLine, @@ -6,7 +7,6 @@ import { import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' -import Tooltip from '@/app/components/base/tooltip' import { useClipboard } from '@/hooks/use-clipboard' import copyStyle from './style.module.css' @@ -35,15 +35,20 @@ const CopyFeedback = ({ content }: Props) => { }, [copy, content]) return ( - - -
- {copied && } - {!copied && } -
-
+ + +
+ {copied && } + {!copied && } +
+ + )} + /> + + {safeText} +
) } @@ -65,18 +70,23 @@ export const CopyFeedbackNew = ({ content, className }: Pick -
-
-
-
+ + +
+
+
+ )} + /> + + {safeText} +
) } diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx index 3f3e68b32e..090f4d459f 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx @@ -1,11 +1,11 @@ 'use client' import type { FC } from 'react' import { toast } from '@langgenius/dify-ui/toast' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiEditLine, RiFileEditLine } from '@remixicon/react' import * as React from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' -import Tooltip from '@/app/components/base/tooltip' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { addAnnotation } from '@/service/annotation' @@ -40,17 +40,31 @@ const AnnotationCtrlButton: FC = ({ cached, query, answer, appId, message return ( <> {cached && ( - - - - + + + + + )} + /> + + {t('feature.annotation.edit', { ns: 'appDebug' })} + )} {!cached && answer && ( - - - - + + + + + )} + /> + + {t('feature.annotation.add', { ns: 'appDebug' })} + )} diff --git a/web/app/components/base/features/new-feature-panel/feature-bar.tsx b/web/app/components/base/features/new-feature-panel/feature-bar.tsx index 9b442ca52f..bf9d1c6cf0 100644 --- a/web/app/components/base/features/new-feature-panel/feature-bar.tsx +++ b/web/app/components/base/features/new-feature-panel/feature-bar.tsx @@ -1,5 +1,6 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiApps2AddLine, RiArrowRightLine, RiSparklingFill } from '@remixicon/react' import * as React from 'react' import { useMemo, useState } from 'react' @@ -7,7 +8,6 @@ import { useTranslation } from 'react-i18next' import { useFeatures } from '@/app/components/base/features/hooks' import VoiceSettings from '@/app/components/base/features/new-feature-panel/text-to-speech/voice-settings' import { Citations, ContentModeration, FolderUpload, LoveMessage, MessageFast, Microphone01, TextToAudio, VirtualAssistant } from '@/app/components/base/icons/src/vender/features' -import Tooltip from '@/app/components/base/tooltip' type Props = { isChatMode?: boolean @@ -51,86 +51,131 @@ const FeatureBar = ({
{!!features.moreLikeThis?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.moreLikeThis.title', { ns: 'appDebug' })} + )} {!!features.opening?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.conversationOpener.title', { ns: 'appDebug' })} +
)} {!!features.moderation?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.moderation.title', { ns: 'appDebug' })} +
)} {!!features.speech2text?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.speechToText.title', { ns: 'appDebug' })} + )} {!!features.text2speech?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.textToSpeech.title', { ns: 'appDebug' })} +
)} {showFileUpload && !!features.file?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.fileUpload.title', { ns: 'appDebug' })} + )} {!!features.suggested?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.suggestedQuestionsAfterAnswer.title', { ns: 'appDebug' })} +
)} {isChatMode && !!features.citation?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.citation.title', { ns: 'appDebug' })} + )} {isChatMode && !!features.annotationReply?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.annotation.title', { ns: 'appDebug' })} +
)}
diff --git a/web/app/components/base/features/new-feature-panel/feature-card.tsx b/web/app/components/base/features/new-feature-panel/feature-card.tsx index 58725a31a1..0c25a514fe 100644 --- a/web/app/components/base/features/new-feature-panel/feature-card.tsx +++ b/web/app/components/base/features/new-feature-panel/feature-card.tsx @@ -1,9 +1,9 @@ import { Switch } from '@langgenius/dify-ui/switch' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiQuestionLine, } from '@remixicon/react' import * as React from 'react' -import Tooltip from '@/app/components/base/tooltip' type Props = { icon: any @@ -41,10 +41,15 @@ const FeatureCard = ({
{title} {tooltip && ( - -
+ +
+ )} + /> + + {tooltip} +
)}
diff --git a/web/app/components/base/file-uploader/file-list-in-log.tsx b/web/app/components/base/file-uploader/file-list-in-log.tsx index 14dd171b59..9edd1ffed4 100644 --- a/web/app/components/base/file-uploader/file-list-in-log.tsx +++ b/web/app/components/base/file-uploader/file-list-in-log.tsx @@ -1,10 +1,10 @@ import type { FileEntity } from './types' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiArrowRightSLine } from '@remixicon/react' import * as React from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { SupportUploadFileTypes } from '@/app/components/workflow/types' import FileImageRender from './file-image-render' import FileTypeIcon from './file-type-icon' @@ -49,27 +49,37 @@ const FileListInLog = ({ fileList, isExpanded = false, noBorder = false, noPaddi return ( <> {isImageFile && ( - -
- -
+ + + +
+ )} + /> + + {name} +
)} {!isImageFile && ( - -
- -
+ + + +
+ )} + /> + + {name} + )} diff --git a/web/app/components/base/file-uploader/pdf-preview.tsx b/web/app/components/base/file-uploader/pdf-preview.tsx index bfd115401b..c129f9cfb3 100644 --- a/web/app/components/base/file-uploader/pdf-preview.tsx +++ b/web/app/components/base/file-uploader/pdf-preview.tsx @@ -1,4 +1,5 @@ import type { FC } from 'react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiCloseLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react' import { noop } from 'es-toolkit/function' import { t } from 'i18next' @@ -7,7 +8,6 @@ import { useState } from 'react' import { createPortal } from 'react-dom' import { useHotkeys } from 'react-hotkeys-hook' import Loading from '@/app/components/base/loading' -import Tooltip from '@/app/components/base/tooltip' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { PdfHighlighter, PdfLoader } from './pdf-highlighter-adapter' @@ -76,29 +76,50 @@ const PdfPreview: FC = ({ }}
- -
- -
+ + + +
+ )} + /> + + {t('operation.zoomOut', { ns: 'common' })} + - -
- -
+ + + +
+ )} + /> + + {t('operation.zoomIn', { ns: 'common' })} + - -
- -
+ + + + + )} + /> + + {t('operation.cancel', { ns: 'common' })} + , document.body, diff --git a/web/app/components/base/image-uploader/image-list.tsx b/web/app/components/base/image-uploader/image-list.tsx index c6cb977bb3..2e56b88f65 100644 --- a/web/app/components/base/image-uploader/image-list.tsx +++ b/web/app/components/base/image-uploader/image-list.tsx @@ -1,11 +1,11 @@ import type { FC } from 'react' import type { ImageFile } from '@/types/app' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' import ImagePreview from '@/app/components/base/image-uploader/image-preview' -import Tooltip from '@/app/components/base/tooltip' import { TransferMethod } from '@/types/app' type ImageListProps = { @@ -82,10 +82,15 @@ const ImageList: FC = ({ )} {item.progress === -1 && ( - - + + + )} + /> + + {t('imageUploader.pasteImageLinkInvalid', { ns: 'common' })} + )} diff --git a/web/app/components/base/image-uploader/image-preview.tsx b/web/app/components/base/image-uploader/image-preview.tsx index 7cc4ec8a14..354fb5ff2a 100644 --- a/web/app/components/base/image-uploader/image-preview.tsx +++ b/web/app/components/base/image-uploader/image-preview.tsx @@ -1,12 +1,12 @@ import type { FC } from 'react' import { toast } from '@langgenius/dify-ui/toast' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { noop } from 'es-toolkit/function' import { t } from 'i18next' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { useHotkeys } from 'react-hotkeys-hook' -import Tooltip from '@/app/components/base/tooltip' import { downloadUrl } from '@/utils/download' type ImagePreviewProps = { @@ -198,55 +198,97 @@ const ImagePreview: FC = ({ }} data-testid="image-preview-image" /> - -
- {isCopied - ? - : } -
+ + + {isCopied + ? + : } + + )} + /> + + {t('operation.copyImage', { ns: 'common' })} + - -
- -
+ + + + + )} + /> + + {t('operation.zoomOut', { ns: 'common' })} + - -
- -
+ + + + + )} + /> + + {t('operation.zoomIn', { ns: 'common' })} + - -
- -
+ + + + + )} + /> + + {t('operation.download', { ns: 'common' })} + - -
- -
+ + + + + )} + /> + + {t('operation.openInNewTab', { ns: 'common' })} + - -
- -
+ + + + + )} + /> + + {t('operation.cancel', { ns: 'common' })} + , document.body, diff --git a/web/app/components/base/new-audio-button/index.tsx b/web/app/components/base/new-audio-button/index.tsx index c6569ff958..56ada77df6 100644 --- a/web/app/components/base/new-audio-button/index.tsx +++ b/web/app/components/base/new-audio-button/index.tsx @@ -1,4 +1,5 @@ 'use client' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiVolumeUpLine, } from '@remixicon/react' @@ -6,7 +7,6 @@ import { t } from 'i18next' import { useState } from 'react' import ActionButton, { ActionButtonState } from '@/app/components/base/action-button' import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager' -import Tooltip from '@/app/components/base/tooltip' import { useParams, usePathname } from '@/next/navigation' type AudioBtnProps = { @@ -78,20 +78,27 @@ const AudioBtn = ({ }[audioState] return ( - - - - + + + + + +
+ )} + /> + + {tooltipContent} +
) } diff --git a/web/app/components/base/qrcode/index.tsx b/web/app/components/base/qrcode/index.tsx index ba3946b185..cfc30d752b 100644 --- a/web/app/components/base/qrcode/index.tsx +++ b/web/app/components/base/qrcode/index.tsx @@ -1,10 +1,10 @@ 'use client' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { QRCodeCanvas as QRCode } from 'qrcode.react' import * as React from 'react' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' -import Tooltip from '@/app/components/base/tooltip' import { downloadUrl } from '@/utils/download' type Props = { @@ -54,28 +54,33 @@ const ShareQRCode = ({ content }: Props) => { const safeTooltipText = tooltipText || '' return ( - -
- - - - {isShow && ( -
- -
-
{t('overview.appInfo.qrcode.scan', { ns: 'appOverview' })}
-
·
-
{t('overview.appInfo.qrcode.download', { ns: 'appOverview' })}
-
+ + + + + + {isShow && ( +
+ +
+
{t('overview.appInfo.qrcode.scan', { ns: 'appOverview' })}
+
·
+
{t('overview.appInfo.qrcode.download', { ns: 'appOverview' })}
+
+
+ )}
)} -
+ /> + + {safeTooltipText} +
) } diff --git a/web/app/components/datasets/create/step-two/components/general-chunking-options.tsx b/web/app/components/datasets/create/step-two/components/general-chunking-options.tsx index e2be96da86..f0740e9cf4 100644 --- a/web/app/components/datasets/create/step-two/components/general-chunking-options.tsx +++ b/web/app/components/datasets/create/step-two/components/general-chunking-options.tsx @@ -3,6 +3,7 @@ import type { FC } from 'react' import type { PreProcessingRule, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets' import { Button } from '@langgenius/dify-ui/button' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiAlertFill, RiSearchEyeLine, @@ -10,7 +11,6 @@ import { import { useTranslation } from 'react-i18next' import Checkbox from '@/app/components/base/checkbox' import Divider from '@/app/components/base/divider' -import Tooltip from '@/app/components/base/tooltip' import SummaryIndexSetting from '@/app/components/datasets/settings/summary-index-setting' import { IS_CE_EDITION } from '@/config' import { ChunkingMode } from '@/models/datasets' @@ -191,7 +191,18 @@ export const GeneralChunkingOptions: FC = ({ onSelect={onDocLanguageChange} disabled={currentDocForm !== ChunkingMode.qa} /> - + + + + + )} + /> + + {t('stepTwo.QATip', { ns: 'datasetCreation' })} + + {currentDocForm === ChunkingMode.qa && (
= React.memo(({
- - {doc.name} + + {doc.name} + )} + /> + + {doc.name} + {doc.summary_index_status && (
@@ -110,13 +117,20 @@ const DocumentTableRow: FC = React.memo(({
)}
- -
- -
+ + + +
+ )} + /> + + {t('list.table.rename', { ns: 'datasetDocuments' })} +
diff --git a/web/app/components/datasets/documents/detail/metadata/components/doc-type-selector.tsx b/web/app/components/datasets/documents/detail/metadata/components/doc-type-selector.tsx index c6044b261a..5dfa02aa80 100644 --- a/web/app/components/datasets/documents/detail/metadata/components/doc-type-selector.tsx +++ b/web/app/components/datasets/documents/detail/metadata/components/doc-type-selector.tsx @@ -3,9 +3,9 @@ import type { FC } from 'react' import type { DocType } from '@/models/datasets' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useTranslation } from 'react-i18next' import Radio from '@/app/components/base/radio' -import Tooltip from '@/app/components/base/tooltip' import { useMetadataMap } from '@/hooks/use-metadata' import { CUSTOMIZABLE_DOC_TYPES } from '@/models/datasets' import s from '../style.module.css' @@ -17,13 +17,20 @@ const TypeIcon: FC<{ iconName: string, className?: string }> = ({ iconName, clas const IconButton: FC<{ type: DocType, isChecked: boolean }> = ({ type, isChecked = false }) => { const metadataMap = useMetadataMap() return ( - - + + + + + )} + /> + + {metadataMap[type].text} + ) } diff --git a/web/app/components/datasets/hit-testing/components/query-input/textarea.tsx b/web/app/components/datasets/hit-testing/components/query-input/textarea.tsx index eec34630db..b30fcdcf1e 100644 --- a/web/app/components/datasets/hit-testing/components/query-input/textarea.tsx +++ b/web/app/components/datasets/hit-testing/components/query-input/textarea.tsx @@ -1,9 +1,9 @@ import type { ChangeEvent } from 'react' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import * as React from 'react' import { useTranslation } from 'react-i18next' import { Corner } from '@/app/components/base/icons/src/vender/solid/shapes' -import Tooltip from '@/app/components/base/tooltip' type TextareaProps = { text: string @@ -36,14 +36,19 @@ const Textarea = ({ /> {text.length > 200 ? ( - -
- {`${text.length}/200`} -
+ + + {`${text.length}/200`} + + )} + /> + + {t('input.countWarning', { ns: 'datasetHitTesting' })} + ) : ( diff --git a/web/app/components/datasets/list/dataset-card/components/dataset-card-footer.tsx b/web/app/components/datasets/list/dataset-card/components/dataset-card-footer.tsx index 49dcade8f3..df3b5c68d2 100644 --- a/web/app/components/datasets/list/dataset-card/components/dataset-card-footer.tsx +++ b/web/app/components/datasets/list/dataset-card/components/dataset-card-footer.tsx @@ -1,10 +1,10 @@ import type { DataSet } from '@/models/datasets' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiFileTextFill, RiRobot2Fill } from '@remixicon/react' import * as React from 'react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' const EXTERNAL_PROVIDER = 'external' @@ -39,18 +39,32 @@ const DatasetCardFooter = ({ dataset }: DatasetCardFooterProps) => { !dataset.embedding_available && 'opacity-30', )} > - -
- - {documentCount} -
+ + + + {documentCount} + + )} + /> + + {documentCountTooltip} + {!isExternalProvider && ( - -
- - {dataset.app_count} -
+ + + + {dataset.app_count} + + )} + /> + + {`${dataset.app_count} ${t('appCount', { ns: 'dataset' })}`} + )} / diff --git a/web/app/components/datasets/metadata/edit-metadata-batch/edited-beacon.tsx b/web/app/components/datasets/metadata/edit-metadata-batch/edited-beacon.tsx index 001ec74c00..25f1e19f8d 100644 --- a/web/app/components/datasets/metadata/edit-metadata-batch/edited-beacon.tsx +++ b/web/app/components/datasets/metadata/edit-metadata-batch/edited-beacon.tsx @@ -1,11 +1,11 @@ 'use client' import type { FC } from 'react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiResetLeftLine } from '@remixicon/react' import { useHover } from 'ahooks' import * as React from 'react' import { useRef } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' type Props = { onReset: () => void @@ -22,10 +22,17 @@ const EditedBeacon: FC = ({
{isHovering ? ( - -
- -
+ + + +
+ )} + /> + + {t('operation.reset', { ns: 'common' })} +
) : ( diff --git a/web/app/components/datasets/settings/index-method/keyword-number.tsx b/web/app/components/datasets/settings/index-method/keyword-number.tsx index 1f467e1798..3cbc18393f 100644 --- a/web/app/components/datasets/settings/index-method/keyword-number.tsx +++ b/web/app/components/datasets/settings/index-method/keyword-number.tsx @@ -7,10 +7,10 @@ import { NumberFieldInput, } from '@langgenius/dify-ui/number-field' import { Slider } from '@langgenius/dify-ui/slider' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' const MIN_KEYWORD_NUMBER = 0 const MAX_KEYWORD_NUMBER = 50 @@ -36,10 +36,15 @@ const KeyWordNumber = ({
{t('form.numberOfKeywords', { ns: 'datasetSettings' })}
- - + + + )} + /> + + {t('form.numberOfKeywords', { ns: 'datasetSettings' })} + = ({
{currentConversationId && ( - - - - + + + + + )} + /> + + {t('chat.resetChat', { ns: 'share' })} + )} {currentConversationId && inputsForms.length > 0 && ( diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/feature-icon.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/feature-icon.spec.tsx index 8e68ef11dc..ffdbf69780 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/feature-icon.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/feature-icon.spec.tsx @@ -1,4 +1,5 @@ -import { fireEvent, render, screen } from '@testing-library/react' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { ModelFeatureEnum, ModelFeatureTextEnum, @@ -36,7 +37,7 @@ describe('FeatureIcon', () => { for (const { feature, text } of cases) { const { container, unmount } = render() - fireEvent.mouseEnter(container.firstElementChild as HTMLElement) + await userEvent.hover(container.firstElementChild as HTMLElement) expect(await screen.findByText(`common.modelProvider.featureSupported:{"feature":"${text}"}`)) .toBeInTheDocument() unmount() diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.tsx index a2a357abb5..cc5e0154d7 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.tsx @@ -1,5 +1,6 @@ import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiFileTextLine, RiFilmAiLine, @@ -7,7 +8,6 @@ import { RiVoiceAiFill, } from '@remixicon/react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { ModelFeatureEnum, ModelFeatureTextEnum, @@ -75,19 +75,24 @@ const FeatureIcon: FC = ({ } return ( - -
- - - -
+ + + + + +
+ )} + /> + + {t('modelProvider.featureSupported', { ns: 'common', feature: ModelFeatureTextEnum.vision })} +
) } @@ -105,19 +110,24 @@ const FeatureIcon: FC = ({ } return ( - -
- - - -
+ + + + + + + )} + /> + + {t('modelProvider.featureSupported', { ns: 'common', feature: ModelFeatureTextEnum.document })} + ) } @@ -135,19 +145,24 @@ const FeatureIcon: FC = ({ } return ( - -
- - - -
+ + + + + + + )} + /> + + {t('modelProvider.featureSupported', { ns: 'common', feature: ModelFeatureTextEnum.audio })} + ) } @@ -165,19 +180,24 @@ const FeatureIcon: FC = ({ } return ( - -
- - - -
+ + + + + + + )} + /> + + {t('modelProvider.featureSupported', { ns: 'common', feature: ModelFeatureTextEnum.video })} + ) } diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/cooldown-timer.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/cooldown-timer.tsx index 47e3ccffaa..84690f3a36 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/cooldown-timer.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/cooldown-timer.tsx @@ -1,8 +1,8 @@ +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useLatest } from 'ahooks' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import SimplePieChart from '@/app/components/base/simple-pie-chart' -import Tooltip from '@/app/components/base/tooltip' type CooldownTimerProps = { secondsRemaining?: number @@ -54,8 +54,15 @@ const CooldownTimer = ({ secondsRemaining, onFinish }: CooldownTimerProps) => { return displayTime ? ( - - + + + )} + /> + + {t('modelProvider.apiKeyRateLimit', { ns: 'common', seconds: displayTime })} + ) : null diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx index 9b7b858c13..e8b99bbcea 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx @@ -10,11 +10,11 @@ import type { } from '../declarations' import { cn } from '@langgenius/dify-ui/cn' import { Switch } from '@langgenius/dify-ui/switch' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge/index' import GridMask from '@/app/components/base/grid-mask' -import Tooltip from '@/app/components/base/tooltip' import UpgradeBtn from '@/app/components/billing/upgrade-btn' import s from '@/app/components/custom/style.module.css' import { AddCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth' @@ -152,11 +152,18 @@ const ModelLoadBalancingConfigs = ({
{t('modelProvider.loadBalancing', { ns: 'common' })} - + + + + + )} + /> + + {t('modelProvider.loadBalancingInfo', { ns: 'common' })} + +
{t('modelProvider.loadBalancingDescription', { ns: 'common' })}
@@ -187,8 +194,15 @@ const ModelLoadBalancingConfigs = ({ clearCountdown(index)} /> ) : ( - - + + + )} + /> + + {t('modelProvider.apiKeyStatusNormal', { ns: 'common' })} + )} @@ -208,14 +222,21 @@ const ModelLoadBalancingConfigs = ({ {!isProviderManaged && ( <>
- - updateConfigEntry(index, () => undefined)} - data-testid={`load-balancing-remove-${config.id || index}`} - > -
- + + updateConfigEntry(index, () => undefined)} + data-testid={`load-balancing-remove-${config.id || index}`} + > +
+ + )} + /> + + {t('operation.remove', { ns: 'common' })} +
diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-use-tip.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-use-tip.tsx index 174e417dc9..ce647416ea 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-use-tip.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-use-tip.tsx @@ -1,17 +1,22 @@ +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useTranslation } from 'react-i18next' import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows' -import Tooltip from '@/app/components/base/tooltip' const PriorityUseTip = () => { const { t } = useTranslation() return ( - -
- -
+ + + +
+ )} + /> + + {t('modelProvider.priorityUsing', { ns: 'common' }) || ''} +
) } diff --git a/web/app/components/plugins/plugin-auth/authorize/index.tsx b/web/app/components/plugins/plugin-auth/authorize/index.tsx index fe82498fad..a6e53f977e 100644 --- a/web/app/components/plugins/plugin-auth/authorize/index.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/index.tsx @@ -2,12 +2,12 @@ import type { PluginPayload } from '../types' import type { AddApiKeyButtonProps } from './add-api-key-button' import type { AddOAuthButtonProps } from './add-oauth-button' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { memo, useMemo, } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import AddApiKeyButton from './add-api-key-button' import AddOAuthButton from './add-oauth-button' @@ -79,8 +79,11 @@ const Authorize = ({ if (notAllowCustomCredential) { return ( - - {Item} + + + + {t('auth.credentialUnavailable', { ns: 'plugin' })} + ) } @@ -100,8 +103,11 @@ const Authorize = ({ if (notAllowCustomCredential) { return ( - - {Item} + + + + {t('auth.credentialUnavailable', { ns: 'plugin' })} + ) } diff --git a/web/app/components/plugins/plugin-auth/authorized/__tests__/item.spec.tsx b/web/app/components/plugins/plugin-auth/authorized/__tests__/item.spec.tsx index 0225c8c8c6..39ec436ece 100644 --- a/web/app/components/plugins/plugin-auth/authorized/__tests__/item.spec.tsx +++ b/web/app/components/plugins/plugin-auth/authorized/__tests__/item.spec.tsx @@ -110,7 +110,7 @@ describe('Item Component', () => { const { container } = render() - expect(container.querySelector('[data-state]')).toBeInTheDocument() + expect(container.firstElementChild).toHaveClass('cursor-not-allowed', 'opacity-50') }) it('should not call onItemClick when disabled is true', () => { diff --git a/web/app/components/plugins/plugin-auth/authorized/item.tsx b/web/app/components/plugins/plugin-auth/authorized/item.tsx index 9193238a55..0bdb9c1a21 100644 --- a/web/app/components/plugins/plugin-auth/authorized/item.tsx +++ b/web/app/components/plugins/plugin-auth/authorized/item.tsx @@ -1,6 +1,7 @@ import type { Credential } from '../types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiCheckLine, RiDeleteBinLine, @@ -16,7 +17,6 @@ import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import Badge from '@/app/components/base/badge' import Input from '@/app/components/base/input' -import Tooltip from '@/app/components/base/tooltip' import Indicator from '@/app/components/header/indicator' import { CredentialTypeEnum } from '../types' @@ -172,55 +172,76 @@ const Item = ({ } { !disableRename && !credential.from_enterprise && !credential.not_allowed_to_use && ( - - { - e.stopPropagation() - setRenaming(true) - setRenameValue(credential.name) - }} - > - - + + { + e.stopPropagation() + setRenaming(true) + setRenameValue(credential.name) + }} + > + + + )} + /> + + {t('operation.rename', { ns: 'common' })} + ) } { !isOAuth && !disableEdit && !credential.from_enterprise && !credential.not_allowed_to_use && ( - - { - e.stopPropagation() - onEdit?.( - credential.id, - { - ...credential.credentials, - __name__: credential.name, - __credential_id__: credential.id, - }, - ) - }} - > - - + + { + e.stopPropagation() + onEdit?.( + credential.id, + { + ...credential.credentials, + __name__: credential.name, + __credential_id__: credential.id, + }, + ) + }} + > + + + )} + /> + + {t('operation.edit', { ns: 'common' })} + ) } { !disableDelete && !credential.from_enterprise && ( - - { - e.stopPropagation() - onDelete?.(credential.id) - }} - > - - + + { + e.stopPropagation() + onDelete?.(credential.id) + }} + > + + + )} + /> + + {t('operation.delete', { ns: 'common' })} + ) } @@ -232,8 +253,11 @@ const Item = ({ if (credential.not_allowed_to_use) { return ( - - {CredentialItem} + + + + {t('auth.customCredentialUnavailable', { ns: 'plugin' })} + ) } diff --git a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx index 723560832a..7972ea5f89 100644 --- a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx @@ -2,6 +2,7 @@ import type { Node } from 'reactflow' import type { ToolValue } from '@/app/components/workflow/block-selector/types' import type { NodeOutPutVar } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiAddLine, RiQuestionLine, @@ -11,7 +12,6 @@ import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import Divider from '@/app/components/base/divider' import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general' -import Tooltip from '@/app/components/base/tooltip' import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector' import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability' import { useAllMCPTools } from '@/service/use-tools' @@ -112,10 +112,15 @@ const MultipleToolSelector = ({
{label}
{required &&
*
} {tooltip && ( - -
+ +
+ )} + /> + + {tooltip} +
)} {supportCollapse && ( diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx index c30e49affc..be970b6cec 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx @@ -3,13 +3,13 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectTrigger } from '@langgenius/dify-ui/select' import { toast } from '@langgenius/dify-ui/toast' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiAddLine, RiEqualizer2Line } from '@remixicon/react' import { useBoolean } from 'ahooks' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { ActionButton, ActionButtonState } from '@/app/components/base/action-button' import Badge from '@/app/components/base/badge' -import Tooltip from '@/app/components/base/tooltip' import { openOAuthPopup } from '@/hooks/use-oauth' import { useInitiateTriggerOAuth, useTriggerOAuthConfig, useTriggerProviderInfo } from '@/service/use-triggers' import { SupportedCreationMethods } from '../../../types' @@ -86,10 +86,17 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU ), extra: ( - - - - + + + + + )} + /> + + {t('subscription.addType.options.oauth.clientSettings', { ns: 'pluginTrigger' })} + ), show: supportedMethods.includes(SupportedCreationMethods.OAUTH), @@ -102,7 +109,20 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU { value: SupportedCreationMethods.MANUAL, label: t('subscription.addType.options.manual.description', { ns: 'pluginTrigger' }), - extra: , + extra: ( + + + + + )} + /> + + {t('subscription.addType.options.manual.tip', { ns: 'pluginTrigger' })} + + + ), show: supportedMethods.includes(SupportedCreationMethods.MANUAL), }, ] @@ -196,30 +216,42 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU && (
- -
- -
+ + + +
+ )} + /> + + {t('subscription.addType.options.oauth.clientSettings', { ns: 'pluginTrigger' })} +
)} ) : ( - = MAX_COUNT ? t('subscription.maxCount', { ns: 'pluginTrigger', num: MAX_COUNT }) : t(`subscription.addType.options.${methodType!.toLowerCase() as Lowercase}.description`, { ns: 'pluginTrigger' })} - disabled={!(supportedMethods?.length === 1 || subscriptionCount >= MAX_COUNT)} - > - + = MAX_COUNT)} + render={( + = MAX_COUNT ? ActionButtonState.Disabled : ActionButtonState.Default} + > + + )} - state={subscriptionCount >= MAX_COUNT ? ActionButtonState.Disabled : ActionButtonState.Default} - > - - + /> + + {subscriptionCount >= MAX_COUNT ? t('subscription.maxCount', { ns: 'pluginTrigger', num: MAX_COUNT }) : t(`subscription.addType.options.${methodType!.toLowerCase() as Lowercase}.description`, { ns: 'pluginTrigger' })} + )} diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx index c78cda7a13..45d4c486b7 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx @@ -1,9 +1,9 @@ 'use client' import type { PluginDetail } from '@/app/components/plugins/types' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { CreateButtonType, CreateSubscriptionButton } from './create' import SubscriptionCard from './subscription-card' import { useSubscriptionList } from './use-subscription-list' @@ -30,7 +30,18 @@ export const SubscriptionListView: React.FC = ({ {t('subscription.listNum', { ns: 'pluginTrigger', num: subscriptionCount })} - + + + + + )} + /> + + {t('subscription.list.tip', { ns: 'pluginTrigger' })} + + )} = ({ {t('subscription.listNum', { ns: 'pluginTrigger', num: subscriptionCount })} - + + + + + )} + /> + + {t('subscription.list.tip', { ns: 'pluginTrigger' })} + + ({ - default: ({ - children, - popupContent, - }: { - children: React.ReactNode - popupContent: React.ReactNode - }) => ( -
- {children} -
{popupContent}
-
- ), -})) - describe('ToolItem', () => { beforeEach(() => { vi.clearAllMocks() @@ -102,7 +88,7 @@ describe('ToolItem', () => { expect(onInstall).toHaveBeenCalledTimes(2) }) - it('blocks unsupported MCP tools and still exposes error state', () => { + it('blocks unsupported MCP tools and still exposes error state', async () => { mcpAllowed = false const { rerender } = render( { />, ) - expect(screen.getByText('tool failed')).toBeInTheDocument() + await userEvent.hover(screen.getByLabelText('tool failed')) + expect(await screen.findByText('tool failed')).toBeInTheDocument() }) }) diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx index 889243d507..ba85957108 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx @@ -2,6 +2,7 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Switch } from '@langgenius/dify-ui/switch' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiDeleteBinLine, RiEqualizer2Line, @@ -13,7 +14,6 @@ import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import AppIcon from '@/app/components/base/app-icon' import { Group } from '@/app/components/base/icons/src/vender/other' -import Tooltip from '@/app/components/base/tooltip' import { ToolTipContent } from '@/app/components/base/tooltip/content' import Indicator from '@/app/components/header/indicator' import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button' @@ -167,12 +167,17 @@ const ToolItem = ({ /> )} {isError && ( - -
- -
+ + + + + )} + /> + + {errorTip} + )} diff --git a/web/app/components/plugins/plugin-page/index.tsx b/web/app/components/plugins/plugin-page/index.tsx index 61d5bfb387..d40a91e2b0 100644 --- a/web/app/components/plugins/plugin-page/index.tsx +++ b/web/app/components/plugins/plugin-page/index.tsx @@ -4,6 +4,7 @@ import type { Dependency, PluginDeclaration, PluginManifestInMarket } from '../t import type { PluginPageTab } from './context' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiBookOpenLine, RiDragDropLine, @@ -15,7 +16,6 @@ import { noop } from 'es-toolkit/function' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import TabSlider from '@/app/components/base/tab-slider' -import Tooltip from '@/app/components/base/tooltip' import ReferenceSettingModal from '@/app/components/plugins/reference-setting-modal' import { MARKETPLACE_API_PREFIX, SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config' import { useDocLink } from '@/context/i18n' @@ -218,16 +218,21 @@ const PluginPage = ({ } { canSetPermissions && ( - - + + + + + )} + /> + + {t('privilege.title', { ns: 'plugin' })} + ) } diff --git a/web/app/components/workflow/nodes/_base/components/config-vision.tsx b/web/app/components/workflow/nodes/_base/components/config-vision.tsx index d21e53368d..546cb1ac6b 100644 --- a/web/app/components/workflow/nodes/_base/components/config-vision.tsx +++ b/web/app/components/workflow/nodes/_base/components/config-vision.tsx @@ -2,11 +2,11 @@ import type { FC } from 'react' import type { ValueSelector, Var, VisionSetting } from '@/app/components/workflow/types' import { Switch } from '@langgenius/dify-ui/switch' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { produce } from 'immer' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import Field from '@/app/components/workflow/nodes/_base/components/field' import ResolutionPicker from '@/app/components/workflow/nodes/llm/components/resolution-picker' import { VarType } from '@/app/components/workflow/types' @@ -61,11 +61,16 @@ const ConfigVision: FC = ({ title={t(`${i18nPrefix}.vision`, { ns: 'workflow' })} tooltip={t('vision.description', { ns: 'appDebug' })!} operations={( - - + + + )} + /> + + {t('vision.onlySupportVisionModelTip', { ns: 'appDebug' })!} + )} > diff --git a/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx b/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx index 8a09cd9366..930f40f76f 100644 --- a/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx +++ b/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx @@ -5,6 +5,7 @@ import type { NodeOutPutVar, } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useBoolean } from 'ahooks' import { noop } from 'es-toolkit/function' import * as React from 'react' @@ -12,7 +13,6 @@ import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import PromptEditor from '@/app/components/base/prompt-editor' -import Tooltip from '@/app/components/base/tooltip' import { useStore } from '@/app/components/workflow/store' import { BlockEnum } from '@/app/components/workflow/types' @@ -119,12 +119,17 @@ const Editor: FC = ({ {readOnly &&
} {isFocus && (
- -
- -
+ + + +
+ )} + /> + + {`${t('common.insertVarTip', { ns: 'workflow' })}`} +
)} diff --git a/web/app/components/workflow/nodes/_base/components/setting-item.tsx b/web/app/components/workflow/nodes/_base/components/setting-item.tsx index feea50cd18..f8a20555dd 100644 --- a/web/app/components/workflow/nodes/_base/components/setting-item.tsx +++ b/web/app/components/workflow/nodes/_base/components/setting-item.tsx @@ -1,7 +1,7 @@ import type { ComponentProps, PropsWithChildren, ReactNode } from 'react' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { memo } from 'react' -import Tooltip from '@/app/components/base/tooltip' import Indicator from '@/app/components/header/indicator' type SettingItemProps = PropsWithChildren<{ @@ -18,10 +18,18 @@ export const SettingItem = memo(({ label, children, status, tooltip }: SettingIt
{label}
- -
- {children} -
+ + + {children} + + )} + /> + + {tooltip} + {indicator && } diff --git a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx index 717d8afbe7..5b6811bc95 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx @@ -3,10 +3,10 @@ import type { FC } from 'react' import type { Field as FieldType } from '../../../../../llm/types' import type { ValueSelector } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiMoreFill } from '@remixicon/react' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { Type } from '../../../../../llm/types' import { getFieldType } from '../../../../../llm/utils' import TreeIndentLine from '../tree-indent-line' @@ -38,24 +38,32 @@ const Field: FC = ({ return null return (
- -
!readonly && onSelect?.([...valueSelector, name])} - > -
- - {depth === MAX_DEPTH + 1 - ? ( - - ) - : (
{name}
)} + + !readonly && onSelect?.([...valueSelector, name])} + > +
+ + {depth === MAX_DEPTH + 1 + ? ( + + ) + : (
{name}
)} -
- {depth < MAX_DEPTH + 1 && ( -
{getFieldType(payload)}
+
+ {depth < MAX_DEPTH + 1 && ( +
{getFieldType(payload)}
+ )} +
)} -
+ /> + + {t('structOutput.moreFillTip', { ns: 'app' })} +
{depth <= MAX_DEPTH && payload.type === Type.object && payload.properties && ( diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/index.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/index.tsx index aa5fd77d0e..cdfda74aeb 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/index.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/index.tsx @@ -3,10 +3,10 @@ import type { Node, NodeOutPutVar, } from '@/app/components/workflow/types' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { produce } from 'immer' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { useNodesSyncDraft } from '@/app/components/workflow/hooks' import MethodItem from './method-item' import MethodSelector from './method-selector' @@ -71,9 +71,18 @@ const DeliveryMethodForm: React.FC = ({
{t(`${i18nPrefix}.deliveryMethod.title`, { ns: 'workflow' })}
- + + + + + )} + /> + + {t(`${i18nPrefix}.deliveryMethod.tooltip`, { ns: 'workflow' })} + +
{!readonly && (
diff --git a/web/app/components/workflow/nodes/knowledge-base/components/index-method.tsx b/web/app/components/workflow/nodes/knowledge-base/components/index-method.tsx index 70ff0e47eb..82ccf265ba 100644 --- a/web/app/components/workflow/nodes/knowledge-base/components/index-method.tsx +++ b/web/app/components/workflow/nodes/knowledge-base/components/index-method.tsx @@ -1,5 +1,6 @@ import { cn } from '@langgenius/dify-ui/cn' import { Slider } from '@langgenius/dify-ui/slider' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiQuestionLine } from '@remixicon/react' import { memo, @@ -11,7 +12,6 @@ import { HighQuality, } from '@/app/components/base/icons/src/vender/knowledge' import Input from '@/app/components/base/input' -import Tooltip from '@/app/components/base/tooltip' import { Field } from '@/app/components/workflow/nodes/_base/components/layout' import { ChunkStructureEnum, @@ -97,10 +97,13 @@ const IndexMethod = ({
{t('form.numberOfKeywords', { ns: 'datasetSettings' })}
- - + + } + /> + + number of keywords +
= ({
{t('nodes.llm.jsonSchema.instruction', { ns: 'workflow' })} - + + + + + )} + /> + + {t('nodes.llm.jsonSchema.promptTooltip', { ns: 'workflow' })} + +