From fb2dba0cf71b9cb801fc2d1f7c6a9896d6d38d2e Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Mon, 21 Sep 2026 11:45:05 +0000 Subject: [PATCH] chore(web): enforce tooltip styling with shadcn lint (#42667) --- lint.config.ts | 26 ++++ package.json | 1 + pnpm-lock.yaml | 43 ++++++ pnpm-workspace.yaml | 1 + .../billing/billing-integration.test.tsx | 9 +- web/app/components/app/log/list.tsx | 8 +- .../chat/chat/citation/progress-tooltip.tsx | 6 +- .../base/chat/chat/citation/tooltip.tsx | 6 +- .../billing/priority-label/index.tsx | 33 ++--- .../__tests__/indexing-progress-item.spec.tsx | 16 ++- .../indexing-progress-item.tsx | 23 ++-- .../__tests__/index.spec.tsx | 33 +++++ .../workflow-log-archives-page/index.tsx | 46 +++---- .../integrations/sidebar-actions.tsx | 4 +- .../workflow/block-selector/tabs.tsx | 2 +- .../components/workflow/comment/thread.tsx | 8 +- .../workflow/header/online-users.tsx | 2 +- .../_base/__tests__/node-sections.spec.tsx | 4 +- .../nodes/_base/__tests__/node.spec.tsx | 3 +- .../agent-strategy-selector.spec.tsx | 24 +++- .../components/agent-strategy-selector.tsx | 44 +++--- .../_base/components/workflow-panel/index.tsx | 2 +- .../workflow/nodes/_base/node-sections.tsx | 23 ++-- .../__tests__/panel-output-section.spec.tsx | 22 +++ .../llm/components/panel-output-section.tsx | 32 ++--- .../workflow/nodes/trigger-webhook/panel.tsx | 6 +- web/app/components/workflow/run/node.tsx | 4 +- .../workflow/variable-inspect/listening.tsx | 2 +- web/plugins/eslint/tooltip-content.test.ts | 127 ++++++++++++++++++ 29 files changed, 409 insertions(+), 151 deletions(-) create mode 100644 web/plugins/eslint/tooltip-content.test.ts diff --git a/lint.config.ts b/lint.config.ts index e543a0a153f..fa176439c73 100644 --- a/lint.config.ts +++ b/lint.config.ts @@ -6,6 +6,27 @@ import path from 'node:path' const rootDir = import.meta.dirname const difyUiPackageJson = path.resolve(rootDir, 'packages/dify-ui/package.json') const enableTailwindCanonicalClasses = process.env.TAILWIND_CANONICAL_CLASSES === 'true' + +export const tooltipContentRules = { + 'shadcn/no-restyle': [ + 'error', + { + componentImports: ['^@langgenius/dify-ui/tooltip$'], + allow: ['*'], + contracts: [ + { + pattern: '^TooltipContent$', + allow: ['flex', 'items-center', 'gap-1', 'w-*', 'max-w-*'], + // `flex` also names a shadcn class group; allow display flex without flex sizing. + deny: ['flex-*'], + message: + '"{{className}}" is outside the TooltipContent styling contract. Use the default surface, spacing, and typography from packages/dify-ui/src/tooltip/index.tsx; callers may compose a label/shortcut row or constrain width.', + }, + ], + }, + ], +} satisfies NonNullable + const tailwindCanonicalClassesOverride = { files: [ 'web/**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}', @@ -174,6 +195,7 @@ export const lintConfig = { // Keep JavaScript plugins ordered by rule namespace. The `-js` aliases distinguish // plugins that supplement an active native Oxlint plugin with the same namespace. jsPlugins: [ + '@shadcn/lint', '@tanstack/eslint-plugin-query', 'eslint-plugin-antfu', ...(enableTailwindCanonicalClasses ? ['eslint-plugin-better-tailwindcss'] : []), @@ -808,6 +830,10 @@ export const lintConfig = { files: ['web/**/*.{jsx,tsx}', 'packages/dify-ui/**/*.{jsx,tsx}'], rules: jsxA11yRules, }, + { + files: ['web/**/*.{jsx,tsx}'], + rules: tooltipContentRules, + }, { files: ['web/**/*.stories.{js,cjs,mjs,jsx,ts,tsx}', 'web/**/*.story.{js,cjs,mjs,jsx,ts,tsx}'], rules: { diff --git a/package.json b/package.json index e252a1928b6..ab367a7d499 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@eslint/markdown": "catalog:", "@iconify-json/heroicons": "catalog:", "@iconify-json/ri": "catalog:", + "@shadcn/lint": "catalog:", "@tanstack/eslint-plugin-query": "catalog:", "@types/node": "catalog:", "@typescript-eslint/parser": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b58eef8ac09..a7abd3799a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -266,6 +266,9 @@ catalogs: '@sentry/react': specifier: 10.75.0 version: 10.75.0 + '@shadcn/lint': + specifier: 0.1.3 + version: 0.1.3 '@storybook/addon-a11y': specifier: 10.6.0 version: 10.6.0 @@ -828,6 +831,9 @@ importers: '@iconify-json/ri': specifier: 'catalog:' version: 1.2.10 + '@shadcn/lint': + specifier: 'catalog:' + version: 0.1.3(@typescript-eslint/parser@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)) '@tanstack/eslint-plugin-query': specifier: 'catalog:' version: 5.103.1(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) @@ -2482,6 +2488,10 @@ packages: resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@1.2.1': resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -4224,6 +4234,18 @@ packages: resolution: {integrity: sha512-p4xxEo/ayiMZG9tygr09YjqYAd52uq8Qi0EVoRRgUe70nQda0HVPVW519JLq0G+Th/5O5sKME9y1layLU3xlPg==} engines: {node: '>=18'} + '@shadcn/lint@0.1.3': + resolution: {integrity: sha512-eRUt1NfIrmkqWq4CJxvHWNq2O6UH0qIpiFemgJmRucUx+qxisqhBJzsZbPH31x1UU7gczTB7T6p/tQt/cAQgiw==} + engines: {node: '>=20.19'} + peerDependencies: + '@typescript-eslint/parser': '>=8.40.0' + eslint: '>=9.30.0' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + '@shikijs/core@4.4.3': resolution: {integrity: sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==} engines: {node: '>=20'} @@ -5877,6 +5899,11 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + cn@0.2.6: + resolution: {integrity: sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ==} + engines: {node: '>=20'} + hasBin: true + cn@0.3.0: resolution: {integrity: sha512-qRoU0ARrZD3Bbj1wKFftEKxPjSnzTEdFkf22NVhW+zU9D/wajhoP+qUZ5DChhhSOBCH1onV12hrmGqfu1+5Z2Q==} engines: {node: '>=20'} @@ -10420,6 +10447,10 @@ snapshots: dependencies: '@eslint/core': 1.2.1 + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/core@1.2.1': dependencies: '@types/json-schema': 7.0.15 @@ -11782,6 +11813,15 @@ snapshots: '@sentry/browser-utils': 10.75.0 '@sentry/core': 10.75.0 + '@shadcn/lint@0.1.3(@typescript-eslint/parser@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))': + dependencies: + '@eslint/core': 0.17.0 + cn: 0.2.6 + optionalDependencies: + '@typescript-eslint/parser': 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint: 10.10.0(jiti@2.7.0)(supports-color@11.0.0) + oxc-parser: 0.148.0 + '@shikijs/core@4.4.3': dependencies: '@shikijs/primitive': 4.4.3 @@ -13481,6 +13521,8 @@ snapshots: clsx@2.1.1: {} + cn@0.2.6: {} + cn@0.3.0: {} code-inspector-plugin@1.6.6(supports-color@11.0.0): @@ -18251,6 +18293,7 @@ time: '@remixicon/react@4.9.0': '2026-01-29T10:53:18.993Z' '@rgrove/parse-xml@5.0.0': '2026-08-30T03:19:42.049Z' '@sentry/react@10.75.0': '2026-09-16T14:58:08.681Z' + '@shadcn/lint@0.1.3': '2026-09-20T18:26:44.252Z' '@storybook/addon-a11y@10.6.0': '2026-09-02T13:57:15.272Z' '@storybook/addon-docs@10.6.0': '2026-09-02T13:56:43.113Z' '@storybook/addon-links@10.6.0': '2026-09-02T13:57:52.366Z' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8e3183a398d..bc8cfc5e39d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -96,6 +96,7 @@ catalog: '@remixicon/react': 4.9.0 '@rgrove/parse-xml': 5.0.0 '@sentry/react': 10.75.0 + '@shadcn/lint': 0.1.3 '@storybook/addon-a11y': 10.6.0 '@storybook/addon-docs': 10.6.0 '@storybook/addon-links': 10.6.0 diff --git a/web/__tests__/billing/billing-integration.test.tsx b/web/__tests__/billing/billing-integration.test.tsx index 30efc0f7c86..0e7d229c86f 100644 --- a/web/__tests__/billing/billing-integration.test.tsx +++ b/web/__tests__/billing/billing-integration.test.tsx @@ -652,12 +652,19 @@ describe('PriorityLabel Integration', () => { setupConsoleState() }) - it('should display "standard" priority for sandbox plan', () => { + it('opens the standard priority explanation without changing the badge', async () => { + const user = userEvent.setup() setupBilling({ billing: { subscription: { plan: 'sandbox' } } }) render() expect(screen.getByText(/plansCommon\.priority\.standard/i)).toBeInTheDocument() + await user.click( + screen.getByRole('button', { name: /plansCommon\.documentProcessingPriority$/i }), + ) + expect(await screen.findByRole('dialog')).toHaveTextContent( + /plansCommon\.documentProcessingPriorityTip/i, + ) }) it('should display "priority" for professional plan with icon', () => { diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 790f928229c..84fb93e8538 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -898,7 +898,7 @@ const ConversationList: FC = ({ logs, appDetail, onRefresh }) annotation?: LogAnnotation, ) => { return ( - + = ({ logs, appDetail, onRefresh }) } /> - - - + + + {`${t(($) => $['detail.annotationTip'], { ns: 'appLog', user: annotation?.account?.name })} ${formatTime(annotation?.created_at || dayjs().unix(), 'MM-DD hh:mm A')}`} diff --git a/web/app/components/base/chat/chat/citation/progress-tooltip.tsx b/web/app/components/base/chat/chat/citation/progress-tooltip.tsx index de5d36dc183..b941a6c7bb2 100644 --- a/web/app/components/base/chat/chat/citation/progress-tooltip.tsx +++ b/web/app/components/base/chat/chat/citation/progress-tooltip.tsx @@ -24,11 +24,7 @@ const ProgressTooltip: FC = ({ data }) => { {data} - + {t(($) => $['chat.citation.hitScore'], { ns: 'common' })} {data} diff --git a/web/app/components/base/chat/chat/citation/tooltip.tsx b/web/app/components/base/chat/chat/citation/tooltip.tsx index 3d216036d24..17cf8dbbc0c 100644 --- a/web/app/components/base/chat/chat/citation/tooltip.tsx +++ b/web/app/components/base/chat/chat/citation/tooltip.tsx @@ -18,11 +18,7 @@ const CitationTooltip: FC = ({ data, text, icon }) => { {icon} {data} - + {text} {data} diff --git a/web/app/components/billing/priority-label/index.tsx b/web/app/components/billing/priority-label/index.tsx index 6586183b350..e5be6dd8d7c 100644 --- a/web/app/components/billing/priority-label/index.tsx +++ b/web/app/components/billing/priority-label/index.tsx @@ -1,9 +1,9 @@ import { cn } from '@langgenius/dify-ui/cn' -import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiAedFill } from '@remixicon/react' import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useTranslation } from 'react-i18next' +import { Infotip } from '@/app/components/base/infotip' import { deploymentEditionAtom } from '@/features/system-features/state' import { consoleQuery } from '@/service/console' @@ -26,32 +26,27 @@ const PriorityLabel = ({ className }: PriorityLabelProps) => { const label = priority[plan] return ( - - - } +
+ + {(plan === 'professional' || plan === 'team') && ( + + )} + {t(($) => $[`plansCommon.priority.${label}`], { ns: 'billing' })} + + $['plansCommon.documentProcessingPriority'], { ns: 'billing' })} > - {(plan === 'professional' || plan === 'team') && } - {t(($) => $[`plansCommon.priority.${label}`], { ns: 'billing' })} - - -
+
{t(($) => $['plansCommon.documentProcessingPriority'], { ns: 'billing' })}:{' '} {t(($) => $[`plansCommon.priority.${label}`], { ns: 'billing' })}
{label !== 'top-priority' && ( -
+
{t(($) => $['plansCommon.documentProcessingPriorityTip'], { ns: 'billing' })}
)} - - + +
) } diff --git a/web/app/components/datasets/create/embedding-process/__tests__/indexing-progress-item.spec.tsx b/web/app/components/datasets/create/embedding-process/__tests__/indexing-progress-item.spec.tsx index cf16e383329..45f6a8b6fb4 100644 --- a/web/app/components/datasets/create/embedding-process/__tests__/indexing-progress-item.spec.tsx +++ b/web/app/components/datasets/create/embedding-process/__tests__/indexing-progress-item.spec.tsx @@ -1,5 +1,6 @@ import type { IndexingStatusResponse } from '@/models/datasets' import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { DataSourceType } from '@/models/datasets' import IndexingProgressItem from '../indexing-progress-item' @@ -85,7 +86,8 @@ describe('IndexingProgressItem', () => { expect(screen.queryByText(/%/)).not.toBeInTheDocument() }) - it('should render error icon with tooltip for error status', () => { + it('opens the full indexing error from the warning button', async () => { + const user = userEvent.setup() render( { />, ) - expect(screen.getByText('Parse failed')).toBeInTheDocument() + expect(screen.queryByText('Parse failed')).not.toBeInTheDocument() + await user.click(screen.getByRole('button', { name: 'common.error' })) + expect(await screen.findByRole('dialog')).toHaveTextContent('Parse failed') + await user.keyboard('{Escape}') + expect(screen.getByRole('button', { name: 'common.error' })).toHaveFocus() }) - it('should use the localized fallback when an error has no message', () => { + it('should use the localized fallback when an error has no message', async () => { + const user = userEvent.setup() render( { />, ) - expect(screen.getByText('common.error')).toBeInTheDocument() + await user.click(screen.getByRole('button', { name: 'common.error' })) + expect(await screen.findByRole('dialog')).toHaveTextContent('common.error') }) }) diff --git a/web/app/components/datasets/create/embedding-process/indexing-progress-item.tsx b/web/app/components/datasets/create/embedding-process/indexing-progress-item.tsx index e288d02e3b6..bcd57b541dc 100644 --- a/web/app/components/datasets/create/embedding-process/indexing-progress-item.tsx +++ b/web/app/components/datasets/create/embedding-process/indexing-progress-item.tsx @@ -1,9 +1,9 @@ import type { FC } from 'react' import type { IndexingStatusResponse } from '@/models/datasets' import { cn } from '@langgenius/dify-ui/cn' -import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' -import { RiCheckboxCircleFill, RiErrorWarningFill } from '@remixicon/react' +import { RiCheckboxCircleFill } from '@remixicon/react' import { useTranslation } from 'react-i18next' +import { Infotip } from '@/app/components/base/infotip' import NotionIcon from '@/app/components/base/notion-icon' import PriorityLabel from '@/app/components/billing/priority-label' import { DataSourceType } from '@/models/datasets' @@ -28,15 +28,16 @@ const StatusIcon: FC<{ status: string; error?: string }> = ({ status, error }) = const errorLabel = error || t(($) => $.error, { ns: 'common' }) return ( - - }> - - {errorLabel} - - - {errorLabel} - - + $.error, { ns: 'common' })} + iconVariant="warning" + iconSize="large" + className="text-text-destructive" + popupClassName="wrap-anywhere whitespace-pre-wrap" + sideOffset={4} + > + {errorLabel} + ) } diff --git a/web/app/components/header/account-setting/workflow-log-archives-page/__tests__/index.spec.tsx b/web/app/components/header/account-setting/workflow-log-archives-page/__tests__/index.spec.tsx index be77a782466..6183ab1eb93 100644 --- a/web/app/components/header/account-setting/workflow-log-archives-page/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/workflow-log-archives-page/__tests__/index.spec.tsx @@ -1,6 +1,7 @@ import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { GetWorkflowRunArchivesResponse } from '@dify/contracts/api/console/workflow-run-archives/types.gen' import { act, fireEvent, screen, waitFor, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { NuqsTestingAdapter } from 'nuqs/adapters/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { consoleQuery } from '@/service/console' @@ -219,6 +220,38 @@ describe('WorkflowLogArchivesPage', () => { ) }) + it('opens failure details independently of the retry action', async () => { + const user = userEvent.setup() + renderPage({ + ...archiveData, + months: [ + { + ...archiveMonth, + download_task: { + download_id: 'failed-export', + status: 'failed', + error: 'Storage unavailable', + year: 2025, + month: 3, + archive_bytes: 1048576, + bundle_count: 2, + created_at: '2025-03-03T00:00:00Z', + updated_at: '2025-03-03T00:00:00Z', + expires_at: '2025-03-04T00:00:00Z', + }, + }, + ], + }) + + const retry = screen.getByRole('button', { name: 'common.operation.retry 2025-03' }) + expect(retry).toBeEnabled() + await user.click(screen.getByRole('button', { name: 'common.operation.learnMore: 2025-03' })) + expect(await screen.findByRole('dialog')).toHaveTextContent( + 'appLog.archives.downloadHint.failedWithReason', + ) + expect(retry).toBeEnabled() + }) + it('should show archive content for paid workspaces', () => { // Arrange plan = 'professional' diff --git a/web/app/components/header/account-setting/workflow-log-archives-page/index.tsx b/web/app/components/header/account-setting/workflow-log-archives-page/index.tsx index c1053deaefc..4ee940a67b1 100644 --- a/web/app/components/header/account-setting/workflow-log-archives-page/index.tsx +++ b/web/app/components/header/account-setting/workflow-log-archives-page/index.tsx @@ -7,11 +7,11 @@ import type { } from '@dify/contracts/api/console/workflow-run-archives/types.gen' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { skipToken, useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useQueryState } from 'nuqs' import { useEffect, useId, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { Infotip } from '@/app/components/base/infotip' import { SkeletonRectangle } from '@/app/components/base/skeleton' import { pricingQueryParamName, @@ -408,34 +408,28 @@ function WorkflowArchiveMonthRow({ archive }: { archive: WorkflowRunArchiveMonth {formatBytes(archive.archive_bytes)} - - - {!isPreparing && ( - + +
) diff --git a/web/app/components/integrations/sidebar-actions.tsx b/web/app/components/integrations/sidebar-actions.tsx index d5552496f24..715284182ff 100644 --- a/web/app/components/integrations/sidebar-actions.tsx +++ b/web/app/components/integrations/sidebar-actions.tsx @@ -30,8 +30,6 @@ type PermissionTooltipWrapperProps = { show: boolean } -const permissionTooltipClassName = 'w-[112px] text-left' - function PermissionTooltipWrapper({ children, className, @@ -51,7 +49,7 @@ function PermissionTooltipWrapper({ return ( - + {content} diff --git a/web/app/components/workflow/block-selector/tabs.tsx b/web/app/components/workflow/block-selector/tabs.tsx index 31d660f12fc..917e25d60ce 100644 --- a/web/app/components/workflow/block-selector/tabs.tsx +++ b/web/app/components/workflow/block-selector/tabs.tsx @@ -79,7 +79,7 @@ function TabHeaderItem({ return ( - + {tab.disabledTip || fallbackDisabledTip} diff --git a/web/app/components/workflow/comment/thread.tsx b/web/app/components/workflow/comment/thread.tsx index 965f7f483b2..8173385e467 100644 --- a/web/app/components/workflow/comment/thread.tsx +++ b/web/app/components/workflow/comment/thread.tsx @@ -476,7 +476,7 @@ export const CommentThread: FC = memo( } /> - + {t(($) => $['comments.aria.deleteComment'], { ns: 'workflow' })} @@ -500,7 +500,7 @@ export const CommentThread: FC = memo( } /> - + {t(($) => $['comments.aria.resolveComment'], { ns: 'workflow' })} @@ -521,7 +521,7 @@ export const CommentThread: FC = memo( } /> - + {t(($) => $['comments.aria.previousComment'], { ns: 'workflow' })} @@ -541,7 +541,7 @@ export const CommentThread: FC = memo( } /> - + {t(($) => $['comments.aria.nextComment'], { ns: 'workflow' })} diff --git a/web/app/components/workflow/header/online-users.tsx b/web/app/components/workflow/header/online-users.tsx index 55f41835fe1..fea0dcaafef 100644 --- a/web/app/components/workflow/header/online-users.tsx +++ b/web/app/components/workflow/header/online-users.tsx @@ -158,7 +158,7 @@ const OnlineUsers = () => { ) } /> - + {displayName} {isCurrentUser && ( {currentUserSuffix} diff --git a/web/app/components/workflow/nodes/_base/__tests__/node-sections.spec.tsx b/web/app/components/workflow/nodes/_base/__tests__/node-sections.spec.tsx index 95650fdea47..033d78c189e 100644 --- a/web/app/components/workflow/nodes/_base/__tests__/node-sections.spec.tsx +++ b/web/app/components/workflow/nodes/_base/__tests__/node-sections.spec.tsx @@ -61,7 +61,9 @@ describe('node sections', () => { ) expect(screen.getByText('nodes.iteration.parallelModeUpper')).toBeInTheDocument() - await user.hover(screen.getByText('nodes.iteration.parallelModeUpper')) + await user.click( + screen.getByRole('button', { name: 'nodes.iteration.parallelModeEnableTitle' }), + ) expect(await screen.findByText('nodes.iteration.parallelModeEnableTitle')).toBeInTheDocument() expect(screen.getByText('nodes.iteration.parallelModeEnableDesc')).toBeInTheDocument() expect(screen.getByText('3/3')).toBeInTheDocument() diff --git a/web/app/components/workflow/nodes/_base/__tests__/node.spec.tsx b/web/app/components/workflow/nodes/_base/__tests__/node.spec.tsx index 64518067cf4..544e8dc9c47 100644 --- a/web/app/components/workflow/nodes/_base/__tests__/node.spec.tsx +++ b/web/app/components/workflow/nodes/_base/__tests__/node.spec.tsx @@ -180,8 +180,9 @@ describe('BaseNode', () => { , ) + expect(screen.getByText('workflow.nodes.iteration.parallelModeUpper')).toBeInTheDocument() const parallelButton = screen.getByRole('button', { - name: /workflow\.nodes\.iteration\.parallelModeUpper/, + name: /workflow\.nodes\.iteration\.parallelModeEnableTitle/, }) expect(parallelButton).toBeInTheDocument() diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy-selector.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy-selector.spec.tsx index fd0b0f4a83f..85d5f5960d3 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy-selector.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy-selector.spec.tsx @@ -310,7 +310,9 @@ describe('AgentStrategySelector', () => { />, ) - await user.hover(document.querySelector('[data-base-ui-tooltip-trigger]')!) + await user.click( + screen.getByRole('button', { name: /(?:^|\.)nodes\.agent\.pluginNotInstalled(?=$|:)/ }), + ) expect( await screen.findByText(/(?:^|\.)nodes\.agent\.pluginNotInstalled(?=$|:)/), @@ -318,6 +320,26 @@ describe('AgentStrategySelector', () => { expect( screen.getByText(/(?:^|\.)nodes\.agent\.pluginNotInstalledDesc(?=$|:)/), ).toBeInTheDocument() + expect(screen.getByRole('link', { name: /nodes\.agent\.linkToPlugin/ })).toHaveAttribute( + 'href', + '/plugins', + ) + expect(screen.getAllByRole('dialog')).toHaveLength(1) + + for (const key of ['{Enter}', ' ']) { + await user.keyboard('{Escape}') + await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()) + expect( + screen.getByRole('button', { name: /(?:^|\.)nodes\.agent\.pluginNotInstalled(?=$|:)/ }), + ).toHaveFocus() + await user.keyboard(key) + expect(await screen.findByRole('dialog')).toBeInTheDocument() + expect(screen.queryByTestId('tools-list')).not.toBeInTheDocument() + expect(screen.getByRole('link', { name: /nodes\.agent\.linkToPlugin/ })).toHaveAttribute( + 'href', + '/plugins', + ) + } }) it('renders install and switch-version actions for marketplace strategies', async () => { diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx index 53c5a390e5a..45af349512a 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx @@ -8,10 +8,10 @@ import type { } from '@/app/components/workflow/block-selector/marketplace-plugin/list' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' -import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useSuspenseQuery } from '@tanstack/react-query' import { memo, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { Infotip } from '@/app/components/base/infotip' import { SearchInput } from '@/app/components/base/search-input' import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon' import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/hooks' @@ -30,34 +30,28 @@ import { SwitchPluginVersion } from './switch-plugin-version' const DEFAULT_TAGS: ListProps['tags'] = [] -const NotFoundWarn = (props: { title: ReactNode; description: ReactNode }) => { +const NotFoundWarn = (props: { title: string; description: ReactNode }) => { const { title, description } = props const { t } = useTranslation() return ( - - -
- } - /> - -
-

{title}

-

{description}

-

- - {t(($) => $['nodes.agent.linkToPlugin'], { ns: 'workflow' })} - -

-
-
-
+ +
+

{title}

+

{description}

+

+ + {t(($) => $['nodes.agent.linkToPlugin'], { ns: 'workflow' })} + +

+
+
) } diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 48a1dc94b43..b5fbec9be63 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -618,7 +618,7 @@ const BasePanel: FC = ({ id, data, children }) => { } /> - {runThisStepLabel} + {runThisStepLabel} )} diff --git a/web/app/components/workflow/nodes/_base/node-sections.tsx b/web/app/components/workflow/nodes/_base/node-sections.tsx index 28f05b27dc5..aa1631fa030 100644 --- a/web/app/components/workflow/nodes/_base/node-sections.tsx +++ b/web/app/components/workflow/nodes/_base/node-sections.tsx @@ -2,7 +2,7 @@ import type { SelectorParam } from 'i18next' import type { ReactElement } from 'react' import type { IterationNodeType } from '@/app/components/workflow/nodes/iteration/types' import type { NodeProps } from '@/app/components/workflow/types' -import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' +import { Infotip } from '@/app/components/base/infotip' import { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types' type HeaderMetaProps = { @@ -22,19 +22,20 @@ export const NodeHeaderMeta = ({ data, hasVarValue, isLoading, loopIndex, t }: H return ( <> {data.type === BlockEnum.Iteration && (data as IterationNodeType).is_parallel && ( - - -
- {t(($) => $['nodes.iteration.parallelModeUpper'], { ns: 'workflow' })} -
-
- -
+
+ + {t(($) => $['nodes.iteration.parallelModeUpper'], { ns: 'workflow' })} + + $['nodes.iteration.parallelModeEnableTitle'], { ns: 'workflow' })} + popupClassName="w-45" + > +
{t(($) => $['nodes.iteration.parallelModeEnableTitle'], { ns: 'workflow' })}
{t(($) => $['nodes.iteration.parallelModeEnableDesc'], { ns: 'workflow' })} - - +
+
)} {!!( data._iterationLength && diff --git a/web/app/components/workflow/nodes/llm/components/__tests__/panel-output-section.spec.tsx b/web/app/components/workflow/nodes/llm/components/__tests__/panel-output-section.spec.tsx index 100feef43a5..a677dd1d918 100644 --- a/web/app/components/workflow/nodes/llm/components/__tests__/panel-output-section.spec.tsx +++ b/web/app/components/workflow/nodes/llm/components/__tests__/panel-output-section.spec.tsx @@ -121,4 +121,26 @@ describe('llm/panel-output-section', () => { await user.click(screen.getByRole('switch')) expect(handleStructureOutputEnableChange).toHaveBeenCalledWith(false) }) + it('opens the model warning with the keyboard without changing structured output', async () => { + const user = userEvent.setup() + const onEnable = vi.fn() + render( + , + ) + await user.tab() + expect(screen.getByRole('button', { name: 'app.structOutput.modelNotSupported' })).toHaveFocus() + await user.keyboard('{Enter}') + expect(await screen.findByRole('dialog')).toHaveTextContent( + 'app.structOutput.modelNotSupportedTip', + ) + expect(onEnable).not.toHaveBeenCalled() + }) }) diff --git a/web/app/components/workflow/nodes/llm/components/panel-output-section.tsx b/web/app/components/workflow/nodes/llm/components/panel-output-section.tsx index 5d081d64e27..084475919bd 100644 --- a/web/app/components/workflow/nodes/llm/components/panel-output-section.tsx +++ b/web/app/components/workflow/nodes/llm/components/panel-output-section.tsx @@ -1,7 +1,6 @@ import type { FC } from 'react' import type { LLMNodeType, StructuredOutput } from '../types' import { Switch } from '@langgenius/dify-ui/switch' -import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import * as React from 'react' import { useTranslation } from 'react-i18next' import { Infotip } from '@/app/components/base/infotip' @@ -41,23 +40,20 @@ const PanelOutputSection: FC = ({ operations={
{!isModelSupportStructuredOutput && !!inputs.structured_output_enabled && ( - - - -
- } - /> - -
- {t(($) => $['structOutput.modelNotSupported'], { ns: 'app' })} -
-
- {t(($) => $['structOutput.modelNotSupportedTip'], { ns: 'app' })} -
-
- + $['structOutput.modelNotSupported'], { ns: 'app' })} + iconVariant="warning" + iconSize="large" + className="mr-1 text-text-warning-secondary" + popupClassName="w-58" + > +
+ {t(($) => $['structOutput.modelNotSupported'], { ns: 'app' })} +
+
+ {t(($) => $['structOutput.modelNotSupportedTip'], { ns: 'app' })} +
+
)}
{t(($) => $['structOutput.structured'], { ns: 'app' })} diff --git a/web/app/components/workflow/nodes/trigger-webhook/panel.tsx b/web/app/components/workflow/nodes/trigger-webhook/panel.tsx index 9dbd64e1405..ed025378530 100644 --- a/web/app/components/workflow/nodes/trigger-webhook/panel.tsx +++ b/web/app/components/workflow/nodes/trigger-webhook/panel.tsx @@ -204,11 +204,7 @@ const Panel: FC> = ({ id, data }) => { } /> - + {debugUrlCopied ? t(($) => $[`${i18nPrefix}.debugUrlCopied`], { ns: 'workflow' }) : t(($) => $[`${i18nPrefix}.debugUrlCopy`], { ns: 'workflow' })} diff --git a/web/app/components/workflow/run/node.tsx b/web/app/components/workflow/run/node.tsx index ca36be3b9d6..52f65ec2a81 100644 --- a/web/app/components/workflow/run/node.tsx +++ b/web/app/components/workflow/run/node.tsx @@ -229,9 +229,7 @@ const NodePanel: FC = ({ } /> - -
{nodeInfo.title}
-
+ {nodeInfo.title} {!hideProcessDetail && (