mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
chore(web): enforce tooltip styling with shadcn lint (#42667)
This commit is contained in:
29 files changed
+409
-151
No files matched your search
@@ -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<OxlintConfig['rules']>
|
||||
|
||||
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: {
|
||||
|
||||
@@ -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:",
|
||||
|
||||
Generated
+43
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(<PriorityLabel />)
|
||||
|
||||
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', () => {
|
||||
|
||||
@@ -898,7 +898,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
||||
annotation?: LogAnnotation,
|
||||
) => {
|
||||
return (
|
||||
<Tooltip>
|
||||
<Tooltip disabled={!isHighlight || isChatMode}>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<div
|
||||
@@ -912,9 +912,9 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<TooltipContent className={isHighlight && !isChatMode ? '' : 'hidden!'}>
|
||||
<span className="inline-flex items-center text-xs text-text-tertiary">
|
||||
<RiEditFill className="mr-1 size-3" />
|
||||
<TooltipContent className="flex items-center gap-1">
|
||||
<RiEditFill aria-hidden className="size-3 shrink-0" />
|
||||
<span>
|
||||
{`${t(($) => $['detail.annotationTip'], { ns: 'appLog', user: annotation?.account?.name })} ${formatTime(annotation?.created_at || dayjs().unix(), 'MM-DD hh:mm A')}`}
|
||||
</span>
|
||||
</TooltipContent>
|
||||
|
||||
@@ -24,11 +24,7 @@ const ProgressTooltip: FC<ProgressTooltipProps> = ({ data }) => {
|
||||
</div>
|
||||
{data}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
data-testid="progress-tooltip-popup"
|
||||
placement="top-start"
|
||||
className="p-3 text-text-quaternary"
|
||||
>
|
||||
<TooltipContent data-testid="progress-tooltip-popup" placement="top-start">
|
||||
{t(($) => $['chat.citation.hitScore'], { ns: 'common' })} {data}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -18,11 +18,7 @@ const CitationTooltip: FC<CitationTooltipProps> = ({ data, text, icon }) => {
|
||||
{icon}
|
||||
{data}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
data-testid="tooltip-popup"
|
||||
placement="top-start"
|
||||
className="p-3 text-text-quaternary"
|
||||
>
|
||||
<TooltipContent data-testid="tooltip-popup" placement="top-start">
|
||||
{text} {data}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -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 (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<div
|
||||
className={cn(
|
||||
'ml-1 inline-flex h-4.5 shrink-0 items-center rounded-[5px] border border-text-accent-secondary bg-components-badge-bg-dimm px-1.25 system-2xs-medium text-text-accent-secondary',
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
}
|
||||
<div className={cn('ml-1 inline-flex shrink-0 items-center gap-1', className)}>
|
||||
<span className="inline-flex h-4.5 items-center rounded-[5px] border border-text-accent-secondary bg-components-badge-bg-dimm px-1.25 system-2xs-medium text-text-accent-secondary">
|
||||
{(plan === 'professional' || plan === 'team') && (
|
||||
<RiAedFill aria-hidden className="mr-0.5 size-3" />
|
||||
)}
|
||||
{t(($) => $[`plansCommon.priority.${label}`], { ns: 'billing' })}
|
||||
</span>
|
||||
<Infotip
|
||||
aria-label={t(($) => $['plansCommon.documentProcessingPriority'], { ns: 'billing' })}
|
||||
>
|
||||
{(plan === 'professional' || plan === 'team') && <RiAedFill className="mr-0.5 size-3" />}
|
||||
<span>{t(($) => $[`plansCommon.priority.${label}`], { ns: 'billing' })}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="mb-1 text-xs font-semibold text-text-primary">
|
||||
<div className="font-semibold text-text-primary">
|
||||
{t(($) => $['plansCommon.documentProcessingPriority'], { ns: 'billing' })}:{' '}
|
||||
{t(($) => $[`plansCommon.priority.${label}`], { ns: 'billing' })}
|
||||
</div>
|
||||
{label !== 'top-priority' && (
|
||||
<div className="text-xs text-text-secondary">
|
||||
<div className="mt-1">
|
||||
{t(($) => $['plansCommon.documentProcessingPriorityTip'], { ns: 'billing' })}
|
||||
</div>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Infotip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+12
-4
@@ -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(
|
||||
<IndexingProgressItem
|
||||
detail={makeDetail({ indexing_status: 'error', error: 'Parse failed' })}
|
||||
@@ -93,10 +95,15 @@ describe('IndexingProgressItem', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
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(
|
||||
<IndexingProgressItem
|
||||
detail={makeDetail({ indexing_status: 'error', error: null })}
|
||||
@@ -104,6 +111,7 @@ describe('IndexingProgressItem', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('common.error')).toBeInTheDocument()
|
||||
await user.click(screen.getByRole('button', { name: 'common.error' }))
|
||||
expect(await screen.findByRole('dialog')).toHaveTextContent('common.error')
|
||||
})
|
||||
})
|
||||
@@ -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 (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span />}>
|
||||
<RiErrorWarningFill aria-hidden className="size-4 shrink-0 text-text-destructive" />
|
||||
<span className="sr-only">{errorLabel}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={4} className="max-w-60 rounded-xl px-4 py-3.5 body-xs-regular">
|
||||
{errorLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Infotip
|
||||
aria-label={t(($) => $.error, { ns: 'common' })}
|
||||
iconVariant="warning"
|
||||
iconSize="large"
|
||||
className="text-text-destructive"
|
||||
popupClassName="wrap-anywhere whitespace-pre-wrap"
|
||||
sideOffset={4}
|
||||
>
|
||||
{errorLabel}
|
||||
</Infotip>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+33
@@ -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'
|
||||
|
||||
@@ -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)}
|
||||
</td>
|
||||
<td className="px-2 py-3 text-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
loading={isPreparing}
|
||||
className="px-2"
|
||||
aria-labelledby={`${downloadActionLabelId} ${archiveMonthLabelId}`}
|
||||
onClick={onAction}
|
||||
>
|
||||
{!isPreparing && (
|
||||
<span className={cn(buttonIconClassName, 'size-3.5')} aria-hidden="true" />
|
||||
)}
|
||||
<span id={downloadActionLabelId}>{buttonContent}</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent
|
||||
placement="top"
|
||||
className={cn(
|
||||
'max-w-65 text-center text-text-tertiary',
|
||||
isFailed && 'max-w-75 text-start wrap-anywhere whitespace-pre-wrap',
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
loading={isPreparing}
|
||||
className="px-2"
|
||||
aria-labelledby={`${downloadActionLabelId} ${archiveMonthLabelId}`}
|
||||
onClick={onAction}
|
||||
>
|
||||
{!isPreparing && (
|
||||
<span className={cn(buttonIconClassName, 'size-3.5')} aria-hidden="true" />
|
||||
)}
|
||||
<span id={downloadActionLabelId}>{buttonContent}</span>
|
||||
</Button>
|
||||
<Infotip
|
||||
aria-label={`${t(($) => $['operation.learnMore'], { ns: 'common' })}: ${archiveMonth}`}
|
||||
iconVariant={isFailed ? 'warning' : 'information'}
|
||||
popupClassName="wrap-anywhere whitespace-pre-wrap"
|
||||
>
|
||||
{downloadHint}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Infotip>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={trigger} />
|
||||
<TooltipContent placement={placement} sideOffset={8} className={permissionTooltipClassName}>
|
||||
<TooltipContent placement={placement} sideOffset={8}>
|
||||
{content}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -79,7 +79,7 @@ function TabHeaderItem({
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={tabElement} />
|
||||
<TooltipContent placement="top" className="max-w-57.5 rounded-xl px-4 py-3.5">
|
||||
<TooltipContent placement="top" className="max-w-57.5">
|
||||
{tab.disabledTip || fallbackDisabledTip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -476,7 +476,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent placement="top" className="px-2">
|
||||
<TooltipContent placement="top">
|
||||
{t(($) => $['comments.aria.deleteComment'], { ns: 'workflow' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -500,7 +500,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent placement="top" className="px-2">
|
||||
<TooltipContent placement="top">
|
||||
{t(($) => $['comments.aria.resolveComment'], { ns: 'workflow' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -521,7 +521,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent placement="top" className="px-2">
|
||||
<TooltipContent placement="top">
|
||||
{t(($) => $['comments.aria.previousComment'], { ns: 'workflow' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -541,7 +541,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent placement="top" className="px-2">
|
||||
<TooltipContent placement="top">
|
||||
{t(($) => $['comments.aria.nextComment'], { ns: 'workflow' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -158,7 +158,7 @@ const OnlineUsers = () => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
<TooltipContent placement="bottom" sideOffset={4} className="max-w-55 px-3">
|
||||
<TooltipContent placement="bottom" sideOffset={4} className="max-w-55">
|
||||
{displayName}
|
||||
{isCurrentUser && (
|
||||
<span className="ml-1 text-text-quaternary">{currentUserSuffix}</span>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -180,8 +180,9 @@ describe('BaseNode', () => {
|
||||
</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()
|
||||
|
||||
+23
-1
@@ -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 () => {
|
||||
|
||||
@@ -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 (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<div>
|
||||
<span
|
||||
className="i-ri-error-warning-fill size-4 text-text-destructive"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<TooltipContent className="w-45">
|
||||
<div className="space-y-1 text-xs">
|
||||
<h3 className="font-semibold text-text-primary">{title}</h3>
|
||||
<p className="tracking-tight text-text-secondary">{description}</p>
|
||||
<p>
|
||||
<Link href="/plugins" className="tracking-tight text-text-accent">
|
||||
{t(($) => $['nodes.agent.linkToPlugin'], { ns: 'workflow' })}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Infotip
|
||||
aria-label={title}
|
||||
iconVariant="warning"
|
||||
iconSize="large"
|
||||
className="text-text-destructive"
|
||||
popupClassName="w-45"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-semibold text-text-primary">{title}</h3>
|
||||
<p>{description}</p>
|
||||
<p>
|
||||
<Link href="/plugins" className="text-text-accent">
|
||||
{t(($) => $['nodes.agent.linkToPlugin'], { ns: 'workflow' })}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</Infotip>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -618,7 +618,7 @@ const BasePanel: FC<BasePanelProps> = ({ id, data, children }) => {
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<TooltipContent className="mr-1">{runThisStepLabel}</TooltipContent>
|
||||
<TooltipContent>{runThisStepLabel}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<HelpLink nodeType={nodeMetaType} />
|
||||
|
||||
@@ -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 && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="ml-1 flex items-center justify-center rounded-[5px] border border-text-warning px-1.25 py-0.75 system-2xs-medium-uppercase text-text-warning">
|
||||
{t(($) => $['nodes.iteration.parallelModeUpper'], { ns: 'workflow' })}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="w-45">
|
||||
<div className="font-extrabold">
|
||||
<div className="ml-1 flex items-center gap-1">
|
||||
<span className="rounded-[5px] border border-text-warning px-1.25 py-0.75 system-2xs-medium-uppercase text-text-warning">
|
||||
{t(($) => $['nodes.iteration.parallelModeUpper'], { ns: 'workflow' })}
|
||||
</span>
|
||||
<Infotip
|
||||
aria-label={t(($) => $['nodes.iteration.parallelModeEnableTitle'], { ns: 'workflow' })}
|
||||
popupClassName="w-45"
|
||||
>
|
||||
<div className="font-semibold text-text-primary">
|
||||
{t(($) => $['nodes.iteration.parallelModeEnableTitle'], { ns: 'workflow' })}
|
||||
</div>
|
||||
{t(($) => $['nodes.iteration.parallelModeEnableDesc'], { ns: 'workflow' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Infotip>
|
||||
</div>
|
||||
)}
|
||||
{!!(
|
||||
data._iterationLength &&
|
||||
|
||||
+22
@@ -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(
|
||||
<PanelOutputSection
|
||||
readOnly={false}
|
||||
inputs={createInputs({ structured_output_enabled: true })}
|
||||
isModelSupportStructuredOutput={false}
|
||||
structuredOutputCollapsed={false}
|
||||
setStructuredOutputCollapsed={vi.fn()}
|
||||
handleStructureOutputEnableChange={onEnable}
|
||||
handleStructureOutputChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -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<Props> = ({
|
||||
operations={
|
||||
<div className="mr-4 flex shrink-0 items-center">
|
||||
{!isModelSupportStructuredOutput && !!inputs.structured_output_enabled && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<div>
|
||||
<span className="mr-1 i-ri-alert-fill size-4 text-text-warning-secondary" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<TooltipContent className="w-58 rounded-xl px-4 py-3.5">
|
||||
<div className="title-xs-semi-bold text-text-primary">
|
||||
{t(($) => $['structOutput.modelNotSupported'], { ns: 'app' })}
|
||||
</div>
|
||||
<div className="mt-1 body-xs-regular text-text-secondary">
|
||||
{t(($) => $['structOutput.modelNotSupportedTip'], { ns: 'app' })}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Infotip
|
||||
aria-label={t(($) => $['structOutput.modelNotSupported'], { ns: 'app' })}
|
||||
iconVariant="warning"
|
||||
iconSize="large"
|
||||
className="mr-1 text-text-warning-secondary"
|
||||
popupClassName="w-58"
|
||||
>
|
||||
<div className="title-xs-semi-bold text-text-primary">
|
||||
{t(($) => $['structOutput.modelNotSupported'], { ns: 'app' })}
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
{t(($) => $['structOutput.modelNotSupportedTip'], { ns: 'app' })}
|
||||
</div>
|
||||
</Infotip>
|
||||
)}
|
||||
<div className="mr-0.5 system-xs-medium-uppercase text-text-tertiary">
|
||||
{t(($) => $['structOutput.structured'], { ns: 'app' })}
|
||||
|
||||
@@ -204,11 +204,7 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({ id, data }) => {
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent
|
||||
placement="bottom"
|
||||
sideOffset={4}
|
||||
className="py-1 system-xs-regular text-text-primary"
|
||||
>
|
||||
<TooltipContent placement="bottom" sideOffset={4}>
|
||||
{debugUrlCopied
|
||||
? t(($) => $[`${i18nPrefix}.debugUrlCopied`], { ns: 'workflow' })
|
||||
: t(($) => $[`${i18nPrefix}.debugUrlCopy`], { ns: 'workflow' })}
|
||||
|
||||
@@ -229,9 +229,7 @@ const NodePanel: FC<Props> = ({
|
||||
</Header>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<div className="max-w-xs">{nodeInfo.title}</div>
|
||||
</TooltipContent>
|
||||
<TooltipContent>{nodeInfo.title}</TooltipContent>
|
||||
</Tooltip>
|
||||
{!hideProcessDetail && (
|
||||
<div id={detailsId} hidden={collapseState} className="px-1 pb-1">
|
||||
|
||||
@@ -202,7 +202,7 @@ const Listening: FC<ListeningProps> = ({ onStop, message }) => {
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent placement="top" className="py-1 system-xs-regular text-text-primary">
|
||||
<TooltipContent placement="top">
|
||||
{debugUrlCopied
|
||||
? t(($) => $['nodes.triggerWebhook.debugUrlCopied'], { ns: 'workflow' })
|
||||
: t(($) => $['nodes.triggerWebhook.debugUrlCopy'], { ns: 'workflow' })}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { expect, it } from 'vite-plus/test'
|
||||
import { tooltipContentRules } from '../../../lint.config'
|
||||
|
||||
function lintFixture(source: string) {
|
||||
const require = createRequire(import.meta.url)
|
||||
const viteRequire = createRequire(require.resolve('vite-plus/package.json'))
|
||||
const oxlintBin = join(dirname(dirname(viteRequire.resolve('oxlint'))), 'bin', 'oxlint')
|
||||
const directory = mkdtempSync(join(tmpdir(), 'dify-tooltip-contract-'))
|
||||
try {
|
||||
writeFileSync(
|
||||
join(directory, '.oxlintrc.json'),
|
||||
JSON.stringify({
|
||||
categories: { correctness: 'off' },
|
||||
jsPlugins: [require.resolve('@shadcn/lint')],
|
||||
rules: tooltipContentRules,
|
||||
}),
|
||||
)
|
||||
writeFileSync(join(directory, 'fixture.tsx'), source)
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[oxlintBin, '--config', join(directory, '.oxlintrc.json'), '--format', 'json', 'fixture.tsx'],
|
||||
{ cwd: directory, encoding: 'utf8' },
|
||||
)
|
||||
expect(result.error).toBeUndefined()
|
||||
expect([0, 1], result.stderr + result.stdout).toContain(result.status)
|
||||
return JSON.parse(result.stdout).diagnostics as Array<{ message: string }>
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
it('allows shortcut row composition and width constraints without restricting other components', () => {
|
||||
const diagnostics = lintFixture(`
|
||||
import { TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { PopoverContent } from '@langgenius/dify-ui/popover'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
const examples = <>
|
||||
<TooltipContent className="flex items-center gap-1 max-w-60" />
|
||||
<TooltipContent className="w-58 max-w-65" />
|
||||
<TooltipContent className="flex items-center gap-1 max-w-none" />
|
||||
<TooltipContent />
|
||||
<TooltipTrigger className="border-0 bg-transparent" />
|
||||
<PopoverContent className="shadow-none" />
|
||||
<Infotip popupClassName="shadow-none" />
|
||||
<div className="shadow-none" />
|
||||
</>
|
||||
`)
|
||||
expect(diagnostics).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects surface overrides through aliases, variants, important modifiers, and arbitrary properties', () => {
|
||||
const classes = [
|
||||
'p-1.5',
|
||||
'overflow-clip',
|
||||
'text-start',
|
||||
'wrap-break-word',
|
||||
'transition-opacity',
|
||||
'data-ending-style:opacity-0',
|
||||
'origin-(--transform-origin)',
|
||||
'shadow-[0px_12px_16px_-4px_var(--color-shadow-shadow-5),0px_4px_6px_-2px_var(--color-shadow-shadow-1)]',
|
||||
'rounded-lg',
|
||||
'system-xs-medium',
|
||||
'text-text-secondary',
|
||||
'bg-components-tooltip-bg',
|
||||
'border-[0.5px]',
|
||||
'border-components-panel-border',
|
||||
'border-x-0',
|
||||
'dark:shadow-none!',
|
||||
'backdrop-blur-[5px]',
|
||||
'[box-shadow:none]',
|
||||
'[background:red]',
|
||||
]
|
||||
const diagnostics = lintFixture(`
|
||||
import { TooltipContent as Hint } from '@langgenius/dify-ui/tooltip'
|
||||
const example = <Hint className="${classes.join(' ')}" />
|
||||
`)
|
||||
expect(diagnostics).toHaveLength(classes.length)
|
||||
for (const className of classes)
|
||||
expect(diagnostics.some(({ message }) => message.includes(`"${className}"`))).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects caller typography, padding, alignment, visibility, and positioning policies', () => {
|
||||
const classes = [
|
||||
'rounded-xl',
|
||||
'px-4',
|
||||
'py-3.5',
|
||||
'body-xs-regular',
|
||||
'text-left',
|
||||
'text-text-primary',
|
||||
'mr-1',
|
||||
'hidden!',
|
||||
'z-50',
|
||||
'flex-1',
|
||||
'items-start',
|
||||
'gap-2',
|
||||
'truncate',
|
||||
'whitespace-nowrap',
|
||||
]
|
||||
const diagnostics = lintFixture(`
|
||||
import { TooltipContent } from '@langgenius/dify-ui/tooltip'
|
||||
const example = <TooltipContent className="${classes.join(' ')}" />
|
||||
`)
|
||||
expect(diagnostics).toHaveLength(classes.length)
|
||||
for (const className of classes)
|
||||
expect(diagnostics.some(({ message }) => message.includes(`"${className}"`))).toBe(true)
|
||||
})
|
||||
|
||||
it('checks readable class helpers, local values, and props spreads', () => {
|
||||
const diagnostics = lintFixture(`
|
||||
import { TooltipContent } from '@langgenius/dify-ui/tooltip'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
const classes = 'shadow-none'
|
||||
const props = { className: 'border-0' }
|
||||
const example = <>
|
||||
<TooltipContent className={cn('max-w-60', classes)} />
|
||||
<TooltipContent {...props} />
|
||||
</>
|
||||
`)
|
||||
expect(diagnostics).toHaveLength(2)
|
||||
expect(diagnostics.some(({ message }) => message.includes('"shadow-none"'))).toBe(true)
|
||||
expect(diagnostics.some(({ message }) => message.includes('"border-0"'))).toBe(true)
|
||||
})
|
||||
Reference in new issue
Block a user