mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 21:28:25 +08:00
refactor(web): migrate legacy tooltip callers (#35961)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
d5ad6aedc0
commit
2bb1f0906b
@ -272,11 +272,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/app-access-control/specific-groups-or-members.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/app-publisher/features-wrapper.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 4
|
||||
@ -323,11 +318,6 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/app/configuration/config-var/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/configuration/config-var/select-var-type.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -341,11 +331,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/configuration/config/agent/agent-setting/item-panel.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/configuration/config/agent/agent-tools/index.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 9
|
||||
@ -593,11 +578,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/app/workflow-log/detail.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/workflow-log/filter.tsx": {
|
||||
"react-refresh/only-export-components": {
|
||||
"count": 1
|
||||
@ -967,11 +947,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/features/new-feature-panel/annotation-reply/config-param.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/features/new-feature-panel/annotation-reply/index.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
@ -1005,11 +980,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/features/types.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 2
|
||||
@ -2096,11 +2066,6 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/components/operations.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/components/rename-modal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@ -2116,11 +2081,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/item.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 5
|
||||
@ -2166,11 +2126,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/create-from-pipeline/steps/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 3
|
||||
@ -2196,11 +2151,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/detail/completed/display-toggle.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/detail/completed/hooks/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 5
|
||||
@ -2217,11 +2167,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/detail/completed/segment-card/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/detail/context.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -2310,7 +2255,7 @@
|
||||
},
|
||||
"web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/metadata/hooks/use-edit-dataset-metadata.ts": {
|
||||
@ -2338,7 +2283,7 @@
|
||||
},
|
||||
"web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 3
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/metadata/metadata-dataset/select-metadata-modal.tsx": {
|
||||
@ -2571,11 +2516,6 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/model-provider-page/model-auth/config-provider.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/model-provider-page/model-auth/hooks/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 6
|
||||
@ -2602,9 +2542,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/model-provider-page/model-auth/switch-credential-in-load-balancing.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
@ -2630,9 +2567,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/model-provider-page/model-parameter-modal/status-indicators.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
@ -2647,11 +2581,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 5
|
||||
@ -2900,11 +2829,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/subscription-list/types.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
@ -2915,11 +2839,6 @@
|
||||
"count": 7
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 2
|
||||
@ -2934,9 +2853,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-item/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
@ -3028,11 +2944,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/rag-pipeline/components/panel/input-field/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/index.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -3193,7 +3104,7 @@
|
||||
},
|
||||
"web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/edit-custom-collection-modal/get-schema.tsx": {
|
||||
@ -3380,11 +3291,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/tabs.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -3651,11 +3557,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/_base/components/memory-config.tsx": {
|
||||
"unicorn/prefer-number-properties": {
|
||||
"count": 1
|
||||
@ -3691,11 +3592,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/_base/components/variable/match-schema-type.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 8
|
||||
@ -4050,11 +3946,6 @@
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/iteration-start/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/iteration/default.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -4075,11 +3966,6 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/search-method-option.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/type.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
@ -4113,11 +3999,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/knowledge-retrieval/default.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -4240,11 +4121,6 @@
|
||||
"count": 7
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/loop-start/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/loop/components/condition-list/condition-input.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -4306,11 +4182,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/parameter-extractor/panel.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/parameter-extractor/types.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 2
|
||||
@ -4329,11 +4200,6 @@
|
||||
"count": 9
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/question-classifier/components/advanced-setting.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/question-classifier/components/class-item.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 1
|
||||
@ -4352,11 +4218,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/question-classifier/node.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/question-classifier/use-config.ts": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 2
|
||||
@ -4464,9 +4325,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/item.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
@ -4522,11 +4380,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/trigger-webhook/panel.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/utils.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -4637,9 +4490,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/panel/env-panel/variable-modal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react/set-state-in-effect": {
|
||||
"count": 4
|
||||
},
|
||||
@ -4870,9 +4720,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/variable-inspect/listening.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
@ -4911,26 +4758,11 @@
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/workflow-preview/components/nodes/base.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/workflow-preview/components/nodes/constants.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/workflow-preview/components/nodes/iteration-start/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/workflow-preview/components/nodes/loop-start/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/workflow-preview/components/zoom-in-out.tsx": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
|
||||
@ -99,6 +99,13 @@ See `[web/docs/overlay-migration.md](../../web/docs/overlay-migration.md)` for t
|
||||
- Never create an extra manual portal on top of our primitives — use the exported content / portal parts such as `DialogContent`, `PopoverContent`, and `DrawerPortal`. Base UI handles focus management, scroll-locking, and dismissal.
|
||||
- When a primitive needs additional presentation chrome (e.g. a custom backdrop), add it **inside** the exported component, not at call sites.
|
||||
|
||||
### Tooltip, infotip, and popover semantics
|
||||
|
||||
- Use `Tooltip` only for short, non-interactive visual labels. The trigger must already have visible text or an `aria-label`; the tooltip is not the accessible name and must not contain links, buttons, forms, or structured prose.
|
||||
- Use `Popover` for explanatory content, long text, rich layout, or anything users may need to reach on touch or with assistive technology. In `web/`, the `Infotip` wrapper is the preferred pattern for a `?` help glyph backed by `Popover`.
|
||||
- Pick a `placement` and let the primitive own spacing. Avoid per-call-site offsets unless the component API explicitly needs a measured layout exception.
|
||||
- When passing a Base UI trigger `render` prop, render a real `<button type="button">` for button-like triggers. If a Popover trigger must render a `div`, `span`, or another non-button element, pass `nativeButton={false}`.
|
||||
|
||||
## Development
|
||||
|
||||
- `pnpm -C packages/dify-ui test` — Vitest unit tests for primitives.
|
||||
|
||||
@ -90,6 +90,8 @@ const renderCloudPlanItem = ({
|
||||
)
|
||||
}
|
||||
|
||||
const getPlanButton = (name: string) => screen.getByRole('button', { name })
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
describe('Cloud Plan Payment Flow', () => {
|
||||
beforeEach(() => {
|
||||
@ -180,30 +182,30 @@ describe('Cloud Plan Payment Flow', () => {
|
||||
it('should disable sandbox button when user is on professional plan (downgrade)', () => {
|
||||
renderCloudPlanItem({ currentPlan: Plan.professional, plan: Plan.sandbox })
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
const button = getPlanButton('billing.plansCommon.startForFree')
|
||||
expect(button).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable sandbox and professional buttons when user is on team plan', () => {
|
||||
const { unmount } = renderCloudPlanItem({ currentPlan: Plan.team, plan: Plan.sandbox })
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
expect(getPlanButton('billing.plansCommon.startForFree')).toBeDisabled()
|
||||
unmount()
|
||||
|
||||
renderCloudPlanItem({ currentPlan: Plan.team, plan: Plan.professional })
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
expect(getPlanButton('billing.plansCommon.startBuilding')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should not disable current paid plan button (for invoice management)', () => {
|
||||
renderCloudPlanItem({ currentPlan: Plan.professional, plan: Plan.professional })
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
const button = getPlanButton('billing.plansCommon.currentPlan')
|
||||
expect(button).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should enable higher-tier plan buttons for upgrade', () => {
|
||||
renderCloudPlanItem({ currentPlan: Plan.sandbox, plan: Plan.team })
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
const button = getPlanButton('billing.plansCommon.getStarted')
|
||||
expect(button).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
@ -219,7 +221,7 @@ describe('Cloud Plan Payment Flow', () => {
|
||||
planRange: PlanRange.monthly,
|
||||
})
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
const button = getPlanButton('billing.plansCommon.startBuilding')
|
||||
await user.click(button)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -235,7 +237,7 @@ describe('Cloud Plan Payment Flow', () => {
|
||||
planRange: PlanRange.yearly,
|
||||
})
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
const button = getPlanButton('billing.plansCommon.getStarted')
|
||||
await user.click(button)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -247,7 +249,7 @@ describe('Cloud Plan Payment Flow', () => {
|
||||
const user = userEvent.setup()
|
||||
renderCloudPlanItem({ currentPlan: Plan.professional, plan: Plan.professional })
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
const button = getPlanButton('billing.plansCommon.currentPlan')
|
||||
await user.click(button)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -261,7 +263,7 @@ describe('Cloud Plan Payment Flow', () => {
|
||||
const user = userEvent.setup()
|
||||
renderCloudPlanItem({ currentPlan: Plan.sandbox, plan: Plan.sandbox })
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
const button = getPlanButton('billing.plansCommon.currentPlan')
|
||||
await user.click(button)
|
||||
|
||||
// Wait a tick and verify no actions were taken
|
||||
@ -279,7 +281,7 @@ describe('Cloud Plan Payment Flow', () => {
|
||||
const user = userEvent.setup()
|
||||
renderCloudPlanItem({ currentPlan: Plan.sandbox, plan: Plan.professional })
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
const button = getPlanButton('billing.plansCommon.startBuilding')
|
||||
await user.click(button)
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
'use client'
|
||||
import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control'
|
||||
import { Avatar } from '@langgenius/dify-ui/avatar'
|
||||
import { RiAlertFill, RiCloseCircleFill, RiLockLine, RiOrganizationChart } from '@remixicon/react'
|
||||
import { RiCloseCircleFill, RiLockLine, RiOrganizationChart } from '@remixicon/react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { useAppWhiteListSubjects } from '@/service/access-control'
|
||||
import useAccessControlStore from '../../../../context/access-control-store'
|
||||
import { Infotip } from '../../base/infotip'
|
||||
import Loading from '../../base/loading'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import AddMemberOrGroupDialog from './add-member-or-group-pop'
|
||||
|
||||
export default function SpecificGroupsOrMembers() {
|
||||
@ -137,9 +137,14 @@ function BaseItem({ icon, onRemove, children }: BaseItemProps) {
|
||||
|
||||
export function WebAppSSONotEnabledTip() {
|
||||
const { t } = useTranslation()
|
||||
const tip = t('accessControlDialog.webAppSSONotEnabledTip', { ns: 'app' })
|
||||
|
||||
return (
|
||||
<Tooltip asChild={false} popupContent={t('accessControlDialog.webAppSSONotEnabledTip', { ns: 'app' })}>
|
||||
<RiAlertFill className="h-4 w-4 shrink-0 text-text-warning-secondary" />
|
||||
</Tooltip>
|
||||
<Infotip
|
||||
aria-label={tip}
|
||||
iconClassName="h-4 w-4 shrink-0 text-text-warning-secondary hover:text-text-warning-secondary"
|
||||
>
|
||||
{tip}
|
||||
</Infotip>
|
||||
)
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ReactSortable } from 'react-sortablejs'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
@ -257,13 +257,9 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
||||
<div className="flex items-center">
|
||||
<div className="mr-1">{t('variableTitle', { ns: 'appDebug' })}</div>
|
||||
{!readonly && (
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[180px]">
|
||||
{t('variableTip', { ns: 'appDebug' })}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Infotip aria-label={t('variableTip', { ns: 'appDebug' })} popupClassName="w-[180px]">
|
||||
{t('variableTip', { ns: 'appDebug' })}
|
||||
</Infotip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import * as React from 'react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
@ -24,14 +24,9 @@ const ItemPanel: FC<Props> = ({
|
||||
<div className="flex items-center">
|
||||
{icon}
|
||||
<div className="mr-1 ml-3 text-sm leading-6 font-semibold text-text-secondary">{name}</div>
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[180px]">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
</Tooltip>
|
||||
<Infotip aria-label={description} popupClassName="w-[180px]">
|
||||
{description}
|
||||
</Infotip>
|
||||
</div>
|
||||
<div>
|
||||
{children}
|
||||
|
||||
@ -84,10 +84,6 @@ vi.mock('@/app/components/app/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/drawer', () => ({
|
||||
default: ({ children, isOpen, onClose }: { children: ReactNode, isOpen: boolean, onClose: () => void }) => (
|
||||
isOpen
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ReactElement, ReactNode } from 'react'
|
||||
import type { ReactElement } from 'react'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||
@ -98,15 +98,6 @@ vi.mock('../../app-access-control', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children, popupContent }: { children: ReactNode, popupContent?: ReactNode }) => (
|
||||
<div>
|
||||
{children}
|
||||
{popupContent}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const mockWindowOpen = vi.fn()
|
||||
Object.defineProperty(window, 'open', {
|
||||
writable: true,
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { RiCloseLine, RiPlayLargeLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import TooltipPlus from '@/app/components/base/tooltip'
|
||||
import { WorkflowContextProvider } from '@/app/components/workflow/context'
|
||||
import Run from '@/app/components/workflow/run'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
@ -33,19 +33,23 @@ const DetailPanel: FC<ILogDetail> = ({ runID, onClose, canReplay = false }) => {
|
||||
<div className="flex items-center bg-components-panel-bg">
|
||||
<h1 className="shrink-0 px-4 py-1 system-xl-semibold text-text-primary">{t('runDetail.workflowTitle', { ns: 'appLog' })}</h1>
|
||||
{canReplay && (
|
||||
<TooltipPlus
|
||||
popupContent={t('runDetail.testWithParams', { ns: 'appLog' })}
|
||||
popupClassName="rounded-xl"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="mr-1 flex h-6 w-6 items-center justify-center rounded-md hover:bg-state-base-hover"
|
||||
aria-label={t('runDetail.testWithParams', { ns: 'appLog' })}
|
||||
onClick={handleReplay}
|
||||
>
|
||||
<RiPlayLargeLine className="h-4 w-4 text-text-tertiary" />
|
||||
</button>
|
||||
</TooltipPlus>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
className="mr-1 flex h-6 w-6 items-center justify-center rounded-md hover:bg-state-base-hover"
|
||||
aria-label={t('runDetail.testWithParams', { ns: 'appLog' })}
|
||||
onClick={handleReplay}
|
||||
>
|
||||
<RiPlayLargeLine className="h-4 w-4 text-text-tertiary" />
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent className="rounded-xl">
|
||||
{t('runDetail.testWithParams', { ns: 'appLog' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<WorkflowContextProvider>
|
||||
|
||||
@ -296,11 +296,6 @@ vi.mock('@langgenius/dify-ui/dropdown-menu', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Tooltip uses portals - minimal mock preserving popup content as title attribute
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children, popupContent }: { children: React.ReactNode, popupContent: React.ReactNode }) => React.createElement('div', { title: popupContent }, children),
|
||||
}))
|
||||
|
||||
// AppCardTags has tag API dependencies - mock for isolated testing
|
||||
vi.mock('@/features/tag-management/components/app-card-tags', () => ({
|
||||
AppCardTags: ({ tags }: { tags?: { id: string, name: string }[] }) => {
|
||||
|
||||
@ -55,7 +55,7 @@ describe('ProgressTooltip', () => {
|
||||
|
||||
await user.hover(screen.getByTestId('progress-trigger-content'))
|
||||
|
||||
expect(screen.getByTestId('progress-tooltip-popup')).toBeInTheDocument()
|
||||
expect(await screen.findByTestId('progress-tooltip-popup')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide the tooltip popup on mouse leave', async () => {
|
||||
@ -74,7 +74,7 @@ describe('ProgressTooltip', () => {
|
||||
|
||||
await user.hover(screen.getByTestId('progress-trigger-content'))
|
||||
|
||||
expect(screen.getByTestId('progress-tooltip-popup')).toHaveTextContent(/hitScore/i)
|
||||
expect(await screen.findByTestId('progress-tooltip-popup')).toHaveTextContent(/hitScore/i)
|
||||
})
|
||||
|
||||
it('should show the data value inside the tooltip popup', async () => {
|
||||
@ -83,7 +83,7 @@ describe('ProgressTooltip', () => {
|
||||
|
||||
await user.hover(screen.getByTestId('progress-trigger-content'))
|
||||
|
||||
expect(screen.getByTestId('progress-tooltip-popup')).toHaveTextContent('0.8')
|
||||
expect(await screen.findByTestId('progress-tooltip-popup')).toHaveTextContent('0.8')
|
||||
})
|
||||
})
|
||||
|
||||
@ -126,7 +126,7 @@ describe('ProgressTooltip', () => {
|
||||
await user.unhover(screen.getByTestId('progress-trigger-content'))
|
||||
await user.hover(screen.getByTestId('progress-trigger-content'))
|
||||
|
||||
expect(screen.getByTestId('progress-tooltip-popup')).toBeInTheDocument()
|
||||
expect(await screen.findByTestId('progress-tooltip-popup')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep tooltip closed without any interaction', () => {
|
||||
|
||||
@ -41,7 +41,7 @@ describe('Tooltip', () => {
|
||||
|
||||
await user.hover(screen.getByTestId('tooltip-trigger-content'))
|
||||
|
||||
expect(screen.getByTestId('tooltip-popup')).toHaveTextContent('Word Count')
|
||||
expect(await screen.findByTestId('tooltip-popup')).toHaveTextContent('Word Count')
|
||||
})
|
||||
|
||||
it('should render the data value inside the tooltip popup', async () => {
|
||||
@ -50,7 +50,7 @@ describe('Tooltip', () => {
|
||||
|
||||
await user.hover(screen.getByTestId('tooltip-trigger-content'))
|
||||
|
||||
expect(screen.getByTestId('tooltip-popup')).toHaveTextContent('99')
|
||||
expect(await screen.findByTestId('tooltip-popup')).toHaveTextContent('99')
|
||||
})
|
||||
|
||||
it('should render a string data value inside the tooltip popup', async () => {
|
||||
@ -59,7 +59,7 @@ describe('Tooltip', () => {
|
||||
|
||||
await user.hover(screen.getByTestId('tooltip-trigger-content'))
|
||||
|
||||
expect(screen.getByTestId('tooltip-popup')).toHaveTextContent('abc1234')
|
||||
expect(await screen.findByTestId('tooltip-popup')).toHaveTextContent('abc1234')
|
||||
})
|
||||
|
||||
it('should render both text and data together inside the tooltip popup', async () => {
|
||||
@ -68,7 +68,7 @@ describe('Tooltip', () => {
|
||||
|
||||
await user.hover(screen.getByTestId('tooltip-trigger-content'))
|
||||
|
||||
const popup = screen.getByTestId('tooltip-popup')
|
||||
const popup = await screen.findByTestId('tooltip-popup')
|
||||
expect(popup).toHaveTextContent('Characters')
|
||||
expect(popup).toHaveTextContent('55')
|
||||
})
|
||||
@ -90,10 +90,10 @@ describe('Tooltip', () => {
|
||||
const user = userEvent.setup()
|
||||
const { rerender } = render(<Tooltip data={10} text="Original" icon={<span />} />)
|
||||
await user.hover(screen.getByTestId('tooltip-trigger-content'))
|
||||
expect(screen.getByTestId('tooltip-popup')).toHaveTextContent('Original')
|
||||
expect(await screen.findByTestId('tooltip-popup')).toHaveTextContent('Original')
|
||||
|
||||
rerender(<Tooltip data={10} text="Updated" icon={<span />} />)
|
||||
expect(screen.getByTestId('tooltip-popup')).toHaveTextContent('Updated')
|
||||
expect(await screen.findByTestId('tooltip-popup')).toHaveTextContent('Updated')
|
||||
})
|
||||
})
|
||||
|
||||
@ -104,7 +104,7 @@ describe('Tooltip', () => {
|
||||
|
||||
await user.hover(screen.getByTestId('tooltip-trigger-content'))
|
||||
|
||||
expect(screen.getByTestId('tooltip-popup')).toBeInTheDocument()
|
||||
expect(await screen.findByTestId('tooltip-popup')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide the tooltip popup on mouse leave', async () => {
|
||||
@ -125,7 +125,7 @@ describe('Tooltip', () => {
|
||||
await user.unhover(screen.getByTestId('tooltip-trigger-content'))
|
||||
await user.hover(screen.getByTestId('tooltip-trigger-content'))
|
||||
|
||||
expect(screen.getByTestId('tooltip-popup')).toBeInTheDocument()
|
||||
expect(await screen.findByTestId('tooltip-popup')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@langgenius/dify-ui/tooltip'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type ProgressTooltipProps = {
|
||||
@ -15,22 +14,12 @@ const ProgressTooltip: FC<ProgressTooltipProps> = ({
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<div
|
||||
data-testid="progress-trigger-content"
|
||||
className="flex grow items-center"
|
||||
onMouseEnter={() => setOpen(true)}
|
||||
onMouseLeave={() => setOpen(false)}
|
||||
/>
|
||||
)}
|
||||
data-testid="progress-trigger-content"
|
||||
className="flex grow items-center border-0 bg-transparent p-0 text-left"
|
||||
>
|
||||
<div className="mr-1 h-1.5 w-16 overflow-hidden rounded-[3px] border border-components-progress-gray-border">
|
||||
<div
|
||||
@ -45,7 +34,6 @@ const ProgressTooltip: FC<ProgressTooltipProps> = ({
|
||||
<TooltipContent
|
||||
data-testid="progress-tooltip-popup"
|
||||
placement="top-start"
|
||||
sideOffset={0}
|
||||
className="rounded-lg bg-components-tooltip-bg p-3 system-xs-medium text-text-quaternary shadow-lg"
|
||||
>
|
||||
{t('chat.citation.hitScore', { ns: 'common' })}
|
||||
|
||||
@ -1,39 +1,27 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
Tooltip as DifyTooltip,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@langgenius/dify-ui/tooltip'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
|
||||
type TooltipProps = {
|
||||
type CitationTooltipProps = {
|
||||
data: number | string
|
||||
text: string
|
||||
icon: React.ReactNode
|
||||
}
|
||||
|
||||
const Tooltip: FC<TooltipProps> = ({
|
||||
const CitationTooltip: FC<CitationTooltipProps> = ({
|
||||
data,
|
||||
text,
|
||||
icon,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<DifyTooltip
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<div
|
||||
data-testid="tooltip-trigger-content"
|
||||
className="mr-6 flex items-center"
|
||||
onMouseEnter={() => setOpen(true)}
|
||||
onMouseLeave={() => setOpen(false)}
|
||||
/>
|
||||
)}
|
||||
data-testid="tooltip-trigger-content"
|
||||
className="mr-6 flex items-center border-0 bg-transparent p-0 text-left"
|
||||
>
|
||||
{icon}
|
||||
{data}
|
||||
@ -41,15 +29,14 @@ const Tooltip: FC<TooltipProps> = ({
|
||||
<TooltipContent
|
||||
data-testid="tooltip-popup"
|
||||
placement="top-start"
|
||||
sideOffset={0}
|
||||
className="rounded-lg bg-components-tooltip-bg p-3 system-xs-medium text-text-quaternary shadow-lg"
|
||||
>
|
||||
{text}
|
||||
{' '}
|
||||
{data}
|
||||
</TooltipContent>
|
||||
</DifyTooltip>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tooltip
|
||||
export default CitationTooltip
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useClipboard } from '@/hooks/use-clipboard'
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
type Props = {
|
||||
content: string
|
||||
@ -25,14 +25,25 @@ const CopyIcon = ({ content }: Props) => {
|
||||
const safeTooltipText = tooltipText || ''
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={safeTooltipText}
|
||||
>
|
||||
<div onMouseLeave={reset}>
|
||||
{!copied
|
||||
? (<span className="mx-1 i-custom-vender-line-files-copy h-3.5 w-3.5 cursor-pointer text-text-tertiary" onClick={handleCopy} data-testid="copy-icon" />)
|
||||
: (<span className="mx-1 i-custom-vender-line-files-copy-check h-3.5 w-3.5 text-text-tertiary" data-testid="copied-icon" />)}
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
aria-label={safeTooltipText}
|
||||
className="mx-1 inline-flex h-3.5 w-3.5 cursor-pointer border-0 bg-transparent p-0 text-text-tertiary"
|
||||
onClick={handleCopy}
|
||||
onMouseLeave={reset}
|
||||
>
|
||||
{!copied
|
||||
? (<span aria-hidden className="i-custom-vender-line-files-copy h-3.5 w-3.5" data-testid="copy-icon" />)
|
||||
: (<span aria-hidden className="i-custom-vender-line-files-copy-check h-3.5 w-3.5" data-testid="copied-icon" />)}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{safeTooltipText}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import * as React from 'react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
|
||||
export const Item: FC<{ title: string, tooltip: string, children: React.JSX.Element }> = ({
|
||||
title,
|
||||
@ -12,11 +12,9 @@ export const Item: FC<{ title: string, tooltip: string, children: React.JSX.Elem
|
||||
<div>
|
||||
<div className="mb-1 flex items-center space-x-1">
|
||||
<div className="py-1 system-sm-semibold text-text-secondary">{title}</div>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className="max-w-[200px] system-sm-regular text-text-secondary">{tooltip}</div>
|
||||
}
|
||||
/>
|
||||
<Infotip aria-label={tooltip} popupClassName="max-w-[200px] system-sm-regular text-text-secondary">
|
||||
{tooltip}
|
||||
</Infotip>
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
|
||||
@ -110,8 +110,7 @@ describe('ParamConfigContent', () => {
|
||||
|
||||
const languageLabel = screen.getByText(/voice\.voiceSettings\.language/)
|
||||
expect(languageLabel)!.toBeInTheDocument()
|
||||
const tooltip = languageLabel.parentElement as HTMLElement
|
||||
expect(tooltip.querySelector('svg'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /voice\.voiceSettings\.resolutionTooltip/ }))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display language listbox button', () => {
|
||||
|
||||
@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { replace } from 'string-ts'
|
||||
import AudioBtn from '@/app/components/base/audio-btn'
|
||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
import { usePathname } from '@/next/navigation'
|
||||
import { useAppVoices } from '@/service/use-apps'
|
||||
@ -89,17 +89,16 @@ const VoiceParamConfig = ({
|
||||
<div className="mb-3">
|
||||
<div className="mb-1 flex items-center py-1 system-sm-semibold text-text-secondary">
|
||||
{t('voice.voiceSettings.language', { ns: 'appDebug' })}
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[180px]">
|
||||
{t('voice.voiceSettings.resolutionTooltip', { ns: 'appDebug' }).split('\n').map(item => (
|
||||
<div key={item}>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
<Infotip
|
||||
aria-label={t('voice.voiceSettings.resolutionTooltip', { ns: 'appDebug' })}
|
||||
popupClassName="w-[180px]"
|
||||
>
|
||||
{t('voice.voiceSettings.resolutionTooltip', { ns: 'appDebug' }).split('\n').map(item => (
|
||||
<div key={item}>
|
||||
{item}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</Infotip>
|
||||
</div>
|
||||
<Listbox
|
||||
value={languageItem}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { FileTypeIcon } from '../file-uploader'
|
||||
import { getFileAppearanceType } from '../file-uploader/utils'
|
||||
import Tooltip from '../tooltip'
|
||||
import ImageRender from './image-render'
|
||||
|
||||
const FileThumbVariants = cva(
|
||||
@ -46,43 +46,49 @@ const FileThumb = ({
|
||||
const { name, mimeType, sourceUrl } = file
|
||||
const isImage = mimeType.startsWith('image/')
|
||||
|
||||
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const handleClick = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
onClick?.(file)
|
||||
}, [onClick, file])
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={name}
|
||||
popupClassName="p-1.5 rounded-lg system-xs-medium text-text-secondary"
|
||||
position="top"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
FileThumbVariants({ size, className }),
|
||||
isImage
|
||||
? 'p-px'
|
||||
: 'rounded-md border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs hover:bg-components-panel-on-panel-item-bg-alt',
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
aria-label={name}
|
||||
className={cn(
|
||||
FileThumbVariants({ size, className }),
|
||||
'border-0 bg-transparent p-0',
|
||||
isImage
|
||||
? 'p-px'
|
||||
: 'rounded-md border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs hover:bg-components-panel-on-panel-item-bg-alt',
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{
|
||||
isImage
|
||||
? (
|
||||
<ImageRender
|
||||
sourceUrl={sourceUrl}
|
||||
name={name}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<FileTypeIcon
|
||||
type={getFileAppearanceType(name, mimeType)}
|
||||
size="sm"
|
||||
/>
|
||||
)
|
||||
}
|
||||
</button>
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{
|
||||
isImage
|
||||
? (
|
||||
<ImageRender
|
||||
sourceUrl={sourceUrl}
|
||||
name={name}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<FileTypeIcon
|
||||
type={getFileAppearanceType(name, mimeType)}
|
||||
size="sm"
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
/>
|
||||
<TooltipContent placement="top" className="rounded-lg p-1.5 system-xs-medium text-text-secondary">
|
||||
{name}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@ -41,8 +41,8 @@ describe('Label', () => {
|
||||
const tooltipText = 'Test Tooltip'
|
||||
render(<Label {...defaultProps} tooltip={tooltipText} />)
|
||||
|
||||
await user.hover(screen.getByTestId('test-input-tooltip'))
|
||||
expect(screen.getByText(tooltipText)).toBeInTheDocument()
|
||||
await user.hover(screen.getByRole('button', { name: tooltipText }))
|
||||
expect(await screen.findByText(tooltipText)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide optional text when required is true', () => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '../../tooltip'
|
||||
import { Infotip } from '../../infotip'
|
||||
|
||||
export type LabelProps = {
|
||||
htmlFor: string
|
||||
@ -33,13 +33,9 @@ const Label = ({
|
||||
{!isRequired && showOptional && <div className="ml-1 system-xs-regular text-text-tertiary">{t('label.optional', { ns: 'common' })}</div>}
|
||||
{isRequired && <div className="ml-1 system-xs-regular text-text-destructive-secondary">*</div>}
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className="w-[200px]">{tooltip}</div>
|
||||
}
|
||||
triggerClassName="ml-0.5 w-4 h-4"
|
||||
triggerTestId={`${htmlFor}-tooltip`}
|
||||
/>
|
||||
<Infotip aria-label={tooltip} className="ml-0.5 h-4 w-4" popupClassName="w-[200px]">
|
||||
{tooltip}
|
||||
</Infotip>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
'use client'
|
||||
import type { InputProps } from '../input'
|
||||
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 { useClipboard } from '@/hooks/use-clipboard'
|
||||
import ActionButton from '../action-button'
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
type InputWithCopyProps = {
|
||||
showCopyButton?: boolean
|
||||
@ -64,18 +64,24 @@ const InputWithCopy = React.forwardRef<HTMLInputElement, InputWithCopyProps>((
|
||||
onMouseLeave={reset}
|
||||
data-testid="copy-button-wrapper"
|
||||
>
|
||||
<Tooltip
|
||||
popupContent={safeTooltipText}
|
||||
>
|
||||
<ActionButton
|
||||
size="xs"
|
||||
onClick={handleCopy}
|
||||
className="hover:bg-components-button-ghost-bg-hover"
|
||||
>
|
||||
{copied
|
||||
? (<span className="i-ri-clipboard-fill h-3.5 w-3.5 text-text-tertiary" data-testid="copied-icon" />)
|
||||
: (<span className="i-ri-clipboard-line h-3.5 w-3.5 text-text-tertiary" data-testid="copy-icon" />)}
|
||||
</ActionButton>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton
|
||||
size="xs"
|
||||
aria-label={safeTooltipText}
|
||||
onClick={handleCopy}
|
||||
className="hover:bg-components-button-ghost-bg-hover"
|
||||
>
|
||||
{copied
|
||||
? (<span className="i-ri-clipboard-fill h-3.5 w-3.5 text-text-tertiary" data-testid="copied-icon" />)
|
||||
: (<span className="i-ri-clipboard-line h-3.5 w-3.5 text-text-tertiary" data-testid="copy-icon" />)}
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{safeTooltipText}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
class TooltipManager {
|
||||
private activeCloser: (() => void) | null = null
|
||||
|
||||
register(closeFn: () => void) {
|
||||
if (this.activeCloser)
|
||||
this.activeCloser()
|
||||
this.activeCloser = closeFn
|
||||
}
|
||||
|
||||
clear(closeFn: () => void) {
|
||||
if (this.activeCloser === closeFn)
|
||||
this.activeCloser = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the currently active tooltip by calling its closer function
|
||||
* and clearing the reference to it
|
||||
*/
|
||||
closeActiveTooltip() {
|
||||
if (this.activeCloser) {
|
||||
this.activeCloser()
|
||||
this.activeCloser = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const tooltipManager = new TooltipManager()
|
||||
@ -1,129 +0,0 @@
|
||||
import { tooltipManager } from '../TooltipManager'
|
||||
|
||||
describe('TooltipManager', () => {
|
||||
// Test the singleton instance directly
|
||||
let manager: typeof tooltipManager
|
||||
|
||||
beforeEach(() => {
|
||||
// Get fresh reference to the singleton
|
||||
manager = tooltipManager
|
||||
// Clean up any active tooltip by calling closeActiveTooltip
|
||||
// This ensures each test starts with a clean state
|
||||
manager.closeActiveTooltip()
|
||||
})
|
||||
|
||||
describe('register', () => {
|
||||
it('should register a close function', () => {
|
||||
const closeFn = vi.fn()
|
||||
manager.register(closeFn)
|
||||
expect(closeFn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call the existing close function when registering a new one', () => {
|
||||
const firstCloseFn = vi.fn()
|
||||
const secondCloseFn = vi.fn()
|
||||
|
||||
manager.register(firstCloseFn)
|
||||
manager.register(secondCloseFn)
|
||||
|
||||
expect(firstCloseFn).toHaveBeenCalledTimes(1)
|
||||
expect(secondCloseFn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should replace the active closer with the new one', () => {
|
||||
const firstCloseFn = vi.fn()
|
||||
const secondCloseFn = vi.fn()
|
||||
|
||||
// Register first function
|
||||
manager.register(firstCloseFn)
|
||||
|
||||
// Register second function - this should call firstCloseFn and replace it
|
||||
manager.register(secondCloseFn)
|
||||
|
||||
// Verify firstCloseFn was called during register (replacement behavior)
|
||||
expect(firstCloseFn).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Now close the active tooltip - this should call secondCloseFn
|
||||
manager.closeActiveTooltip()
|
||||
|
||||
// Verify secondCloseFn was called, not firstCloseFn
|
||||
expect(secondCloseFn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('clear', () => {
|
||||
it('should not clear if the close function does not match', () => {
|
||||
const closeFn = vi.fn()
|
||||
const otherCloseFn = vi.fn()
|
||||
|
||||
manager.register(closeFn)
|
||||
manager.clear(otherCloseFn)
|
||||
|
||||
manager.closeActiveTooltip()
|
||||
expect(closeFn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should clear the close function if it matches', () => {
|
||||
const closeFn = vi.fn()
|
||||
|
||||
manager.register(closeFn)
|
||||
manager.clear(closeFn)
|
||||
|
||||
manager.closeActiveTooltip()
|
||||
expect(closeFn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not call the close function when clearing', () => {
|
||||
const closeFn = vi.fn()
|
||||
|
||||
manager.register(closeFn)
|
||||
manager.clear(closeFn)
|
||||
|
||||
expect(closeFn).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('closeActiveTooltip', () => {
|
||||
it('should do nothing when no active closer is registered', () => {
|
||||
expect(() => manager.closeActiveTooltip()).not.toThrow()
|
||||
})
|
||||
|
||||
it('should call the active closer function', () => {
|
||||
const closeFn = vi.fn()
|
||||
manager.register(closeFn)
|
||||
|
||||
manager.closeActiveTooltip()
|
||||
|
||||
expect(closeFn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should clear the active closer after calling it', () => {
|
||||
const closeFn = vi.fn()
|
||||
manager.register(closeFn)
|
||||
|
||||
manager.closeActiveTooltip()
|
||||
manager.closeActiveTooltip()
|
||||
|
||||
expect(closeFn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should handle multiple register and close cycles', () => {
|
||||
const closeFn1 = vi.fn()
|
||||
const closeFn2 = vi.fn()
|
||||
const closeFn3 = vi.fn()
|
||||
|
||||
manager.register(closeFn1)
|
||||
manager.closeActiveTooltip()
|
||||
|
||||
manager.register(closeFn2)
|
||||
manager.closeActiveTooltip()
|
||||
|
||||
manager.register(closeFn3)
|
||||
manager.closeActiveTooltip()
|
||||
|
||||
expect(closeFn1).toHaveBeenCalledTimes(1)
|
||||
expect(closeFn2).toHaveBeenCalledTimes(1)
|
||||
expect(closeFn3).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,49 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { ToolTipContent } from '../content'
|
||||
|
||||
describe('ToolTipContent', () => {
|
||||
it('should render children correctly', () => {
|
||||
render(
|
||||
<ToolTipContent>
|
||||
<span>Tooltip body text</span>
|
||||
</ToolTipContent>,
|
||||
)
|
||||
expect(screen.getByTestId('tooltip-content')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('tooltip-content-body')).toHaveTextContent('Tooltip body text')
|
||||
expect(screen.queryByTestId('tooltip-content-title')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('tooltip-content-action')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render title when provided', () => {
|
||||
render(
|
||||
<ToolTipContent title="Tooltip Title">
|
||||
<span>Tooltip body text</span>
|
||||
</ToolTipContent>,
|
||||
)
|
||||
expect(screen.getByTestId('tooltip-content-title')).toHaveTextContent('Tooltip Title')
|
||||
})
|
||||
|
||||
it('should render action when provided', () => {
|
||||
render(
|
||||
<ToolTipContent action={<span>Action Text</span>}>
|
||||
<span>Tooltip body text</span>
|
||||
</ToolTipContent>,
|
||||
)
|
||||
expect(screen.getByTestId('tooltip-content-action')).toHaveTextContent('Action Text')
|
||||
})
|
||||
|
||||
it('should handle action click', async () => {
|
||||
const user = userEvent.setup()
|
||||
const handleActionClick = vi.fn()
|
||||
render(
|
||||
<ToolTipContent action={<span onClick={handleActionClick}>Action Text</span>}>
|
||||
<span>Tooltip body text</span>
|
||||
</ToolTipContent>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByText('Action Text'))
|
||||
expect(handleActionClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@ -1,333 +0,0 @@
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import Tooltip from '../index'
|
||||
import { tooltipManager } from '../TooltipManager'
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.clearAllTimers()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('Tooltip', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render default tooltip with question icon', () => {
|
||||
const triggerClassName = 'custom-trigger'
|
||||
const { container } = render(<Tooltip popupContent="Tooltip content" triggerClassName={triggerClassName} />)
|
||||
const trigger = container.querySelector(`.${triggerClassName}`)
|
||||
expect(trigger).not.toBeNull()
|
||||
expect(trigger?.querySelector('svg')).not.toBeNull() // question icon
|
||||
})
|
||||
|
||||
it('should render with custom children', () => {
|
||||
const { getByText } = render(
|
||||
<Tooltip popupContent="Tooltip content">
|
||||
<button>Hover me</button>
|
||||
</Tooltip>,
|
||||
)
|
||||
expect(getByText('Hover me').textContent).toBe('Hover me')
|
||||
})
|
||||
|
||||
it('should render correctly when asChild is false', () => {
|
||||
const { container } = render(
|
||||
<Tooltip popupContent="Tooltip" asChild={false} triggerClassName="custom-parent-trigger">
|
||||
<span>Trigger</span>
|
||||
</Tooltip>,
|
||||
)
|
||||
const trigger = container.querySelector('.custom-parent-trigger')
|
||||
expect(trigger).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should render with a fallback question icon when children are null', () => {
|
||||
const { container } = render(
|
||||
<Tooltip popupContent="Tooltip" triggerClassName="custom-fallback-trigger">
|
||||
{null}
|
||||
</Tooltip>,
|
||||
)
|
||||
const trigger = container.querySelector('.custom-fallback-trigger')
|
||||
expect(trigger).not.toBeNull()
|
||||
expect(trigger?.querySelector('svg')).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Disabled state', () => {
|
||||
it('should not show tooltip when disabled', () => {
|
||||
const triggerClassName = 'custom-trigger'
|
||||
const { container } = render(<Tooltip popupContent="Tooltip content" disabled triggerClassName={triggerClassName} />)
|
||||
const trigger = container.querySelector(`.${triggerClassName}`)
|
||||
act(() => {
|
||||
fireEvent.mouseEnter(trigger!)
|
||||
})
|
||||
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Trigger methods', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
it('should open on hover when triggerMethod is hover', () => {
|
||||
const triggerClassName = 'custom-trigger'
|
||||
const { container } = render(<Tooltip popupContent="Tooltip content" triggerClassName={triggerClassName} />)
|
||||
const trigger = container.querySelector(`.${triggerClassName}`)
|
||||
act(() => {
|
||||
fireEvent.mouseEnter(trigger!)
|
||||
})
|
||||
expect(screen.queryByText('Tooltip content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close on mouse leave when triggerMethod is hover and needsDelay is false', () => {
|
||||
const triggerClassName = 'custom-trigger'
|
||||
const { container } = render(<Tooltip popupContent="Tooltip content" triggerClassName={triggerClassName} needsDelay={false} />)
|
||||
const trigger = container.querySelector(`.${triggerClassName}`)
|
||||
act(() => {
|
||||
fireEvent.mouseEnter(trigger!)
|
||||
fireEvent.mouseLeave(trigger!)
|
||||
})
|
||||
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should toggle on click when triggerMethod is click', () => {
|
||||
const triggerClassName = 'custom-trigger'
|
||||
const { container } = render(<Tooltip popupContent="Tooltip content" triggerMethod="click" triggerClassName={triggerClassName} />)
|
||||
const trigger = container.querySelector(`.${triggerClassName}`)
|
||||
act(() => {
|
||||
fireEvent.click(trigger!)
|
||||
})
|
||||
expect(screen.queryByText('Tooltip content')).toBeInTheDocument()
|
||||
|
||||
// Test toggle off
|
||||
act(() => {
|
||||
fireEvent.click(trigger!)
|
||||
})
|
||||
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should do nothing on mouse enter if triggerMethod is click', () => {
|
||||
const triggerClassName = 'custom-trigger'
|
||||
const { container } = render(<Tooltip popupContent="Tooltip content" triggerMethod="click" triggerClassName={triggerClassName} />)
|
||||
const trigger = container.querySelector(`.${triggerClassName}`)
|
||||
act(() => {
|
||||
fireEvent.mouseEnter(trigger!)
|
||||
})
|
||||
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should delay closing on mouse leave when needsDelay is true', () => {
|
||||
const triggerClassName = 'custom-trigger'
|
||||
const { container } = render(<Tooltip popupContent="Tooltip content" triggerMethod="hover" needsDelay triggerClassName={triggerClassName} />)
|
||||
const trigger = container.querySelector(`.${triggerClassName}`)
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseEnter(trigger!)
|
||||
})
|
||||
expect(screen.getByText('Tooltip content')).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseLeave(trigger!)
|
||||
})
|
||||
// Shouldn't close immediately
|
||||
expect(screen.getByText('Tooltip content')).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(350)
|
||||
})
|
||||
// Should close after delay
|
||||
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not close if mouse enters popup before delay finishes', () => {
|
||||
const triggerClassName = 'custom-trigger'
|
||||
const { container } = render(<Tooltip popupContent="Tooltip content" triggerMethod="hover" needsDelay triggerClassName={triggerClassName} />)
|
||||
const trigger = container.querySelector(`.${triggerClassName}`)
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseEnter(trigger!)
|
||||
})
|
||||
|
||||
const popup = screen.getByText('Tooltip content')
|
||||
expect(popup).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseLeave(trigger!)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(150)
|
||||
// Simulate mouse entering popup area itself during the delay timeframe
|
||||
fireEvent.mouseEnter(popup)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(200) // Complete the 300ms original delay
|
||||
})
|
||||
|
||||
// Should still be open because we are hovering the popup
|
||||
expect(screen.getByText('Tooltip content')).toBeInTheDocument()
|
||||
|
||||
// Now mouse leaves popup
|
||||
act(() => {
|
||||
fireEvent.mouseLeave(popup)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(350)
|
||||
})
|
||||
// Should now close
|
||||
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should do nothing on mouse enter/leave of popup when triggerMethod is not hover', () => {
|
||||
const triggerClassName = 'custom-trigger'
|
||||
const { container } = render(<Tooltip popupContent="Tooltip content" triggerMethod="click" needsDelay triggerClassName={triggerClassName} />)
|
||||
const trigger = container.querySelector(`.${triggerClassName}`)
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(trigger!)
|
||||
})
|
||||
|
||||
const popup = screen.getByText('Tooltip content')
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseEnter(popup)
|
||||
fireEvent.mouseLeave(popup)
|
||||
vi.advanceTimersByTime(350)
|
||||
})
|
||||
|
||||
// Should still be open because click method requires another click to close, not hover leave
|
||||
expect(screen.getByText('Tooltip content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should clear close timeout if trigger is hovered again before delay finishes', () => {
|
||||
const triggerClassName = 'custom-trigger'
|
||||
const { container } = render(<Tooltip popupContent="Tooltip content" triggerMethod="hover" needsDelay triggerClassName={triggerClassName} />)
|
||||
const trigger = container.querySelector(`.${triggerClassName}`)
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseEnter(trigger!)
|
||||
})
|
||||
expect(screen.getByText('Tooltip content')).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseLeave(trigger!)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(150)
|
||||
// Re-hover trigger before it closes
|
||||
fireEvent.mouseEnter(trigger!)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(200) // Original 300ms would be up
|
||||
})
|
||||
|
||||
// Should still be open because we reset it
|
||||
expect(screen.getByText('Tooltip content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should test clear close timeout if trigger is hovered again before delay finishes and isHoverPopupRef is true', () => {
|
||||
const triggerClassName = 'custom-trigger'
|
||||
const { container } = render(<Tooltip popupContent="Tooltip content" triggerMethod="hover" needsDelay triggerClassName={triggerClassName} />)
|
||||
const trigger = container.querySelector(`.${triggerClassName}`)
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseEnter(trigger!)
|
||||
})
|
||||
|
||||
const popup = screen.getByText('Tooltip content')
|
||||
expect(popup).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseEnter(popup)
|
||||
fireEvent.mouseLeave(trigger!)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(350)
|
||||
})
|
||||
|
||||
// Should still be open because we are hovering the popup
|
||||
expect(screen.getByText('Tooltip content')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('TooltipManager', () => {
|
||||
it('should close active tooltips when triggered centrally, overriding other closes', () => {
|
||||
const triggerClassName1 = 'custom-trigger-1'
|
||||
const triggerClassName2 = 'custom-trigger-2'
|
||||
|
||||
const { container } = render(
|
||||
<div>
|
||||
<Tooltip popupContent="Tooltip content 1" triggerMethod="hover" triggerClassName={triggerClassName1} />
|
||||
<Tooltip popupContent="Tooltip content 2" triggerMethod="hover" triggerClassName={triggerClassName2} />
|
||||
</div>,
|
||||
)
|
||||
|
||||
const trigger1 = container.querySelector(`.${triggerClassName1}`)
|
||||
const trigger2 = container.querySelector(`.${triggerClassName2}`)
|
||||
|
||||
expect(trigger2).not.toBeNull()
|
||||
|
||||
// Open first tooltip
|
||||
act(() => {
|
||||
fireEvent.mouseEnter(trigger1!)
|
||||
})
|
||||
expect(screen.queryByText('Tooltip content 1')).toBeInTheDocument()
|
||||
|
||||
// TooltipManager should keep track of it
|
||||
// Next, immediately open the second one without leaving first (e.g., via TooltipManager)
|
||||
// TooltipManager registers the newest one and closes the old one when doing full external operations, but internally the manager allows direct closing
|
||||
|
||||
act(() => {
|
||||
tooltipManager.closeActiveTooltip()
|
||||
})
|
||||
|
||||
expect(screen.queryByText('Tooltip content 1')).not.toBeInTheDocument()
|
||||
|
||||
// Safe to call again
|
||||
expect(() => tooltipManager.closeActiveTooltip()).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Styling and positioning', () => {
|
||||
it('should apply custom trigger className', () => {
|
||||
const triggerClassName = 'custom-trigger'
|
||||
const { container } = render(<Tooltip popupContent="Tooltip content" triggerClassName={triggerClassName} />)
|
||||
const trigger = container.querySelector(`.${triggerClassName}`)
|
||||
expect(trigger?.className).toContain('custom-trigger')
|
||||
})
|
||||
|
||||
it('should pass triggerTestId to the fallback icon wrapper', () => {
|
||||
render(<Tooltip popupContent="Tooltip content" triggerTestId="test-tooltip-icon" />)
|
||||
expect(screen.getByTestId('test-tooltip-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply custom popup className', async () => {
|
||||
const triggerClassName = 'custom-trigger'
|
||||
const { container } = render(<Tooltip popupContent="Tooltip content" triggerClassName={triggerClassName} popupClassName="custom-popup" />)
|
||||
const trigger = container.querySelector(`.${triggerClassName}`)
|
||||
act(() => {
|
||||
fireEvent.mouseEnter(trigger!)
|
||||
})
|
||||
expect((await screen.findByText('Tooltip content'))?.className).toContain('custom-popup')
|
||||
})
|
||||
|
||||
it('should apply noDecoration when specified', async () => {
|
||||
const triggerClassName = 'custom-trigger'
|
||||
const { container } = render(
|
||||
<Tooltip
|
||||
popupContent="Tooltip content"
|
||||
triggerClassName={triggerClassName}
|
||||
noDecoration
|
||||
/>,
|
||||
)
|
||||
const trigger = container.querySelector(`.${triggerClassName}`)
|
||||
act(() => {
|
||||
fireEvent.mouseEnter(trigger!)
|
||||
})
|
||||
expect((await screen.findByText('Tooltip content'))?.className).not.toContain('bg-components-panel-bg')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,22 +0,0 @@
|
||||
import type { FC, PropsWithChildren, ReactNode } from 'react'
|
||||
|
||||
type ToolTipContentProps = {
|
||||
title?: ReactNode
|
||||
action?: ReactNode
|
||||
} & PropsWithChildren
|
||||
|
||||
export const ToolTipContent: FC<ToolTipContentProps> = ({
|
||||
title,
|
||||
action,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-[180px]" data-testid="tooltip-content">
|
||||
{!!title && (
|
||||
<div className="mb-1.5 font-semibold text-text-secondary" data-testid="tooltip-content-title">{title}</div>
|
||||
)}
|
||||
<div className="mb-1.5 text-text-tertiary" data-testid="tooltip-content-body">{children}</div>
|
||||
{!!action && <div className="cursor-pointer text-text-accent" data-testid="tooltip-content-action">{action}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import Tooltip from '.'
|
||||
|
||||
const TooltipGrid = () => {
|
||||
return (
|
||||
<div className="flex w-full max-w-xl flex-col gap-6 rounded-2xl border border-divider-subtle bg-components-panel-bg p-6">
|
||||
<div className="text-xs tracking-[0.18em] text-text-tertiary uppercase">Hover tooltips</div>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Tooltip popupContent="Helpful hint explaining the setting.">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-divider-subtle bg-background-default px-3 py-1 text-xs font-medium text-text-secondary hover:bg-state-base-hover"
|
||||
>
|
||||
Hover me
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip popupContent="Placement can vary." position="right">
|
||||
<span className="rounded-md bg-background-default px-3 py-1 text-xs text-text-secondary">
|
||||
Right tooltip
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="text-xs tracking-[0.18em] text-text-tertiary uppercase">Click tooltips</div>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Tooltip popupContent="Click again to close." triggerMethod="click" position="bottom-start">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-divider-subtle bg-background-default px-3 py-1 text-xs font-medium text-text-secondary hover:bg-state-base-hover"
|
||||
>
|
||||
Click trigger
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip popupContent="Decoration disabled" triggerMethod="click" noDecoration>
|
||||
<span className="rounded-md border border-dashed border-divider-regular px-3 py-1 text-xs text-text-secondary">
|
||||
Plain content
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Feedback/Tooltip',
|
||||
component: TooltipGrid,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Portal-based tooltip component supporting hover and click triggers, custom placements, and decorated content.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof TooltipGrid>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Playground: Story = {}
|
||||
@ -1,231 +0,0 @@
|
||||
'use client'
|
||||
import type { Placement } from '@langgenius/dify-ui/popover'
|
||||
/**
|
||||
* @deprecated Use `@langgenius/dify-ui/tooltip` instead.
|
||||
* This component will be removed after migration is complete.
|
||||
* See: https://github.com/langgenius/dify/issues/32767
|
||||
*/
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@langgenius/dify-ui/popover'
|
||||
import { RiQuestionLine } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { tooltipManager } from './TooltipManager'
|
||||
|
||||
type TooltipOffset = number | {
|
||||
mainAxis?: number
|
||||
crossAxis?: number
|
||||
}
|
||||
|
||||
type TooltipProps = {
|
||||
position?: Placement
|
||||
triggerMethod?: 'hover' | 'click'
|
||||
triggerClassName?: string
|
||||
triggerTestId?: string
|
||||
disabled?: boolean
|
||||
popupContent?: React.ReactNode
|
||||
children?: React.ReactNode
|
||||
popupClassName?: string
|
||||
portalContentClassName?: string
|
||||
noDecoration?: boolean
|
||||
offset?: TooltipOffset
|
||||
needsDelay?: boolean
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Tooltip: FC<TooltipProps> = ({
|
||||
position = 'top',
|
||||
triggerMethod = 'hover',
|
||||
triggerClassName,
|
||||
triggerTestId,
|
||||
disabled = false,
|
||||
popupContent,
|
||||
children,
|
||||
popupClassName,
|
||||
portalContentClassName,
|
||||
noDecoration,
|
||||
offset,
|
||||
asChild = true,
|
||||
needsDelay = true,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const resolvedOffset = offset ?? 8
|
||||
const sideOffset = typeof resolvedOffset === 'number' ? resolvedOffset : (resolvedOffset.mainAxis ?? 0)
|
||||
const alignOffset = typeof resolvedOffset === 'number' ? 0 : (resolvedOffset.crossAxis ?? 0)
|
||||
const [isHoverPopup, {
|
||||
setTrue: setHoverPopup,
|
||||
setFalse: setNotHoverPopup,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const isHoverPopupRef = useRef(isHoverPopup)
|
||||
useEffect(() => {
|
||||
isHoverPopupRef.current = isHoverPopup
|
||||
}, [isHoverPopup])
|
||||
|
||||
const [isHoverTrigger, {
|
||||
setTrue: setHoverTrigger,
|
||||
setFalse: setNotHoverTrigger,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const isHoverTriggerRef = useRef(isHoverTrigger)
|
||||
useEffect(() => {
|
||||
isHoverTriggerRef.current = isHoverTrigger
|
||||
}, [isHoverTrigger])
|
||||
|
||||
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const clearCloseTimeout = useCallback(() => {
|
||||
if (closeTimeoutRef.current) {
|
||||
clearTimeout(closeTimeoutRef.current)
|
||||
closeTimeoutRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearCloseTimeout()
|
||||
}
|
||||
}, [clearCloseTimeout])
|
||||
|
||||
const close = () => setOpen(false)
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
if (disabled) {
|
||||
setOpen(false)
|
||||
return
|
||||
}
|
||||
if (triggerMethod === 'click')
|
||||
setOpen(nextOpen)
|
||||
else if (!nextOpen)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleLeave = (isTrigger: boolean) => {
|
||||
if (isTrigger)
|
||||
setNotHoverTrigger()
|
||||
else
|
||||
setNotHoverPopup()
|
||||
|
||||
// give time to move to the popup
|
||||
if (needsDelay) {
|
||||
clearCloseTimeout()
|
||||
closeTimeoutRef.current = setTimeout(() => {
|
||||
closeTimeoutRef.current = null
|
||||
if (!isHoverPopupRef.current && !isHoverTriggerRef.current) {
|
||||
setOpen(false)
|
||||
tooltipManager.clear(close)
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
else {
|
||||
clearCloseTimeout()
|
||||
setOpen(false)
|
||||
tooltipManager.clear(close)
|
||||
}
|
||||
}
|
||||
const handleTriggerMouseEnter = () => {
|
||||
if (triggerMethod === 'hover') {
|
||||
clearCloseTimeout()
|
||||
setHoverTrigger()
|
||||
tooltipManager.register(close)
|
||||
setOpen(true)
|
||||
}
|
||||
}
|
||||
const handleTriggerMouseLeave = () => {
|
||||
if (triggerMethod === 'hover')
|
||||
handleLeave(true)
|
||||
}
|
||||
const handlePopupMouseEnter = () => {
|
||||
if (triggerMethod === 'hover') {
|
||||
clearCloseTimeout()
|
||||
setHoverPopup()
|
||||
}
|
||||
}
|
||||
const handlePopupMouseLeave = () => {
|
||||
if (triggerMethod === 'hover')
|
||||
handleLeave(false)
|
||||
}
|
||||
|
||||
const fallbackTrigger = (
|
||||
<div data-testid={triggerTestId} className={triggerClassName || 'h-3.5 w-3.5 shrink-0 p-px'}>
|
||||
<RiQuestionLine className="h-full w-full text-text-quaternary hover:text-text-tertiary" />
|
||||
</div>
|
||||
)
|
||||
const triggerContent = children || fallbackTrigger
|
||||
const childElement = React.isValidElement<React.HTMLAttributes<HTMLElement>>(triggerContent)
|
||||
? triggerContent
|
||||
: fallbackTrigger
|
||||
const nativeButton = typeof childElement.type !== 'string' || childElement.type === 'button'
|
||||
|
||||
const renderAsChildTrigger = () => {
|
||||
const childProps = childElement.props
|
||||
return React.cloneElement(childElement, {
|
||||
onMouseEnter: (event: React.MouseEvent<HTMLElement>) => {
|
||||
childProps.onMouseEnter?.(event)
|
||||
handleTriggerMouseEnter()
|
||||
},
|
||||
onMouseLeave: (event: React.MouseEvent<HTMLElement>) => {
|
||||
childProps.onMouseLeave?.(event)
|
||||
handleTriggerMouseLeave()
|
||||
},
|
||||
})
|
||||
}
|
||||
const effectiveOpen = !disabled && open
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={effectiveOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
{asChild
|
||||
? (
|
||||
<PopoverTrigger
|
||||
nativeButton={nativeButton}
|
||||
disabled={disabled}
|
||||
render={renderAsChildTrigger()}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<PopoverTrigger
|
||||
nativeButton={false}
|
||||
disabled={disabled}
|
||||
render={(
|
||||
<div
|
||||
className={triggerClassName}
|
||||
onMouseEnter={handleTriggerMouseEnter}
|
||||
onMouseLeave={handleTriggerMouseLeave}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{triggerContent}
|
||||
</PopoverTrigger>
|
||||
)}
|
||||
{effectiveOpen && !!popupContent && (
|
||||
<PopoverContent
|
||||
placement={position}
|
||||
sideOffset={sideOffset}
|
||||
alignOffset={alignOffset}
|
||||
className={portalContentClassName}
|
||||
popupClassName={cn(
|
||||
noDecoration
|
||||
? 'border-0 bg-transparent p-0 shadow-none'
|
||||
: 'relative max-w-[300px] rounded-md border-0 bg-components-panel-bg px-3 py-2 text-left system-xs-regular wrap-break-word text-text-tertiary shadow-lg',
|
||||
popupClassName,
|
||||
)}
|
||||
popupProps={{
|
||||
onMouseEnter: handlePopupMouseEnter,
|
||||
onMouseLeave: handlePopupMouseLeave,
|
||||
}}
|
||||
>
|
||||
{popupContent}
|
||||
</PopoverContent>
|
||||
)}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Tooltip)
|
||||
@ -241,7 +241,7 @@ describe('CloudPlanItem', () => {
|
||||
)
|
||||
|
||||
// Sandbox viewed from a higher plan is disabled, but let's verify no API calls
|
||||
const button = screen.getByRole('button')
|
||||
const button = screen.getByRole('button', { name: 'billing.plansCommon.startForFree' })
|
||||
fireEvent.click(button)
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import { Plan } from '../../../../../type'
|
||||
import List from '../index'
|
||||
@ -12,11 +13,13 @@ describe('CloudPlanItem/List', () => {
|
||||
expect(screen.getByText('billing.plansCommon.startNodes.limited:{"count":2}')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show professional monthly quotas and tooltips', () => {
|
||||
it('should show professional monthly quotas and tooltips', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<List plan={Plan.professional} />)
|
||||
|
||||
expect(screen.getByText('billing.plansCommon.messageRequest.titlePerMonth:{"count":5000}')).toBeInTheDocument()
|
||||
expect(screen.getByText('billing.plansCommon.vectorSpaceTooltip')).toBeInTheDocument()
|
||||
await user.hover(screen.getByRole('button', { name: 'billing.plansCommon.vectorSpaceTooltip' }))
|
||||
expect(await screen.findByText('billing.plansCommon.vectorSpaceTooltip')).toBeInTheDocument()
|
||||
expect(screen.getByText('billing.plansCommon.workflowExecution.faster')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import Item from '../index'
|
||||
|
||||
describe('Item', () => {
|
||||
@ -20,14 +21,16 @@ describe('Item', () => {
|
||||
|
||||
// Toggling the optional tooltip indicator
|
||||
describe('Tooltip behavior', () => {
|
||||
it('should render tooltip content when tooltip text is provided', () => {
|
||||
it('should render tooltip content when tooltip text is provided', async () => {
|
||||
const user = userEvent.setup()
|
||||
const label = 'Workspace seats'
|
||||
const tooltip = 'Seats define how many teammates can join the workspace.'
|
||||
|
||||
const { container } = render(<Item label={label} tooltip={tooltip} />)
|
||||
|
||||
expect(screen.getByText(label)).toBeInTheDocument()
|
||||
expect(screen.getByText(tooltip)).toBeInTheDocument()
|
||||
await user.hover(screen.getByRole('button', { name: tooltip }))
|
||||
expect(await screen.findByText(tooltip)).toBeInTheDocument()
|
||||
expect(container.querySelector('.group')).not.toBeNull()
|
||||
})
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
describe('Tooltip', () => {
|
||||
@ -8,12 +9,14 @@ describe('Tooltip', () => {
|
||||
|
||||
// Rendering the info tooltip container
|
||||
describe('Rendering', () => {
|
||||
it('should render the content panel when provide with text', () => {
|
||||
it('should render the content panel when hovered', async () => {
|
||||
const user = userEvent.setup()
|
||||
const content = 'Usage resets on the first day of every month.'
|
||||
|
||||
render(<Tooltip content={content} />)
|
||||
await user.hover(screen.getByRole('button', { name: content }))
|
||||
|
||||
expect(() => screen.getByText(content)).not.toThrow()
|
||||
expect(await screen.findByText(content)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { RiInfoI } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
|
||||
@ -11,14 +12,20 @@ const Tooltip = ({
|
||||
if (!content)
|
||||
return null
|
||||
return (
|
||||
<div className="group relative z-10 size-[18px] overflow-visible">
|
||||
<div className="absolute right-0 bottom-0 -z-10 hidden w-[260px] bg-saas-dify-blue-static px-5 py-[18px] system-xs-regular text-text-primary-on-surface group-hover:block">
|
||||
{content}
|
||||
</div>
|
||||
<div className="flex h-full w-full items-center justify-center rounded-sm bg-state-base-hover transition-all duration-500 ease-in-out group-hover:rounded-none group-hover:bg-saas-dify-blue-static">
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
openOnHover
|
||||
delay={0}
|
||||
closeDelay={0}
|
||||
aria-label={content}
|
||||
className="group relative z-10 flex size-[18px] items-center justify-center rounded-sm border-0 bg-state-base-hover p-0 transition-[border-radius,background-color] duration-500 ease-in-out hover:rounded-none hover:bg-saas-dify-blue-static"
|
||||
>
|
||||
<RiInfoI className="size-3.5 text-text-tertiary group-hover:text-text-primary-on-surface" data-testid="tooltip-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent placement="top-end" popupClassName="w-[260px] rounded-none border-0 bg-saas-dify-blue-static px-5 py-[18px] system-xs-regular text-text-primary-on-surface shadow-none">
|
||||
{content}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -16,15 +16,16 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useBoolean, useDebounceFn } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
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 { IS_CE_EDITION } from '@/config'
|
||||
import { DataSourceType, DocumentActionType } from '@/models/datasets'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
@ -205,11 +206,12 @@ const Operations = ({ embeddingAvailable, datasetId, detail, selectedIds, onSele
|
||||
<>
|
||||
{archived
|
||||
? (
|
||||
<Tooltip popupContent={t('list.action.enableWarning', { ns: 'datasetDocuments' })} popupClassName="!font-semibold">
|
||||
<div>
|
||||
<Switch checked={false} onCheckedChange={noop} disabled={true} size="md" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Popover>
|
||||
<PopoverTrigger nativeButton={false} openOnHover render={<div><Switch checked={false} onCheckedChange={noop} disabled={true} size="md" /></div>} />
|
||||
<PopoverContent popupClassName="px-3 py-2 font-semibold system-xs-regular text-text-tertiary">
|
||||
{t('list.action.enableWarning', { ns: 'datasetDocuments' })}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
: <Switch checked={enabled} onCheckedChange={v => handleSwitch(v ? 'enable' : 'disable')} size="md" />}
|
||||
<Divider className="!mr-2 !ml-4 !h-3" type="vertical" />
|
||||
@ -217,16 +219,24 @@ const Operations = ({ embeddingAvailable, datasetId, detail, selectedIds, onSele
|
||||
)}
|
||||
{embeddingAvailable && (
|
||||
<>
|
||||
<Tooltip popupContent={t('list.action.settings', { ns: 'datasetDocuments' })} popupClassName="text-text-secondary system-xs-medium" needsDelay={false}>
|
||||
<button
|
||||
type="button"
|
||||
className={cn('mr-2 cursor-pointer rounded-lg', !isListScene
|
||||
? 'border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg p-2 shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px] hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover'
|
||||
: 'p-0.5 hover:bg-state-base-hover')}
|
||||
onClick={() => router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}
|
||||
>
|
||||
<span aria-hidden className="i-ri-equalizer-2-line h-4 w-4 text-components-button-secondary-text" />
|
||||
</button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('list.action.settings', { ns: 'datasetDocuments' })}
|
||||
className={cn('mr-2 cursor-pointer rounded-lg', !isListScene
|
||||
? 'border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg p-2 shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px] hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover'
|
||||
: 'p-0.5 hover:bg-state-base-hover')}
|
||||
onClick={() => router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}
|
||||
>
|
||||
<span aria-hidden className="i-ri-equalizer-2-line h-4 w-4 text-components-button-secondary-text" />
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent className="system-xs-medium text-text-secondary">
|
||||
{t('list.action.settings', { ns: 'datasetDocuments' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenu open={isOperationsMenuOpen} onOpenChange={setIsOperationsMenuOpen}>
|
||||
<DropdownMenuTrigger
|
||||
|
||||
@ -15,12 +15,6 @@ vi.mock('@/app/components/base/radio/ui', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children, popupContent }: { children: React.ReactNode, popupContent: string }) => (
|
||||
<div data-testid="tooltip" title={popupContent}>{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../file-icon', () => ({
|
||||
default: () => <span data-testid="file-icon" />,
|
||||
}))
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import type { Placement } from '@floating-ui/react'
|
||||
import type { OnlineDriveFile } from '@/models/pipeline'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import Radio from '@/app/components/base/radio/ui'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
import FileIcon from './file-icon'
|
||||
|
||||
@ -33,14 +31,7 @@ const Item = ({
|
||||
const isBucket = type === 'bucket'
|
||||
const isFolder = type === 'folder'
|
||||
|
||||
const Wrapper = disabled ? Tooltip : React.Fragment
|
||||
const wrapperProps = disabled
|
||||
? {
|
||||
popupContent: t('onlineDrive.notSupportedFileType', { ns: 'datasetPipeline' }),
|
||||
position: 'top-end' as Placement,
|
||||
offset: { mainAxis: 4, crossAxis: -104 },
|
||||
}
|
||||
: {}
|
||||
const disabledTip = t('onlineDrive.notSupportedFileType', { ns: 'datasetPipeline' })
|
||||
|
||||
const handleSelect = useCallback((e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
@ -80,27 +71,44 @@ const Item = ({
|
||||
onCheck={handleSelect}
|
||||
/>
|
||||
)}
|
||||
<Wrapper
|
||||
{...wrapperProps}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex grow items-center gap-x-1 overflow-hidden py-0.5',
|
||||
disabled && 'opacity-30',
|
||||
{disabled
|
||||
? (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
openOnHover
|
||||
aria-label={disabledTip}
|
||||
className="flex grow items-center gap-x-1 overflow-hidden border-0 bg-transparent p-0 py-0.5 text-left opacity-30"
|
||||
>
|
||||
<FileIcon type={type} fileName={name} className="shrink-0 transform-gpu" />
|
||||
<span
|
||||
className="grow truncate system-sm-medium text-text-secondary"
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
{!isFolder && typeof size === 'number' && (
|
||||
<span className="shrink-0 system-xs-regular text-text-tertiary">{formatFileSize(size)}</span>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent placement="top-end" popupClassName="px-3 py-2 system-xs-regular text-text-tertiary">
|
||||
{disabledTip}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
: (
|
||||
<div className="flex grow items-center gap-x-1 overflow-hidden py-0.5">
|
||||
<FileIcon type={type} fileName={name} className="shrink-0 transform-gpu" />
|
||||
<span
|
||||
className="grow truncate system-sm-medium text-text-secondary"
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
{!isFolder && typeof size === 'number' && (
|
||||
<span className="shrink-0 system-xs-regular text-text-tertiary">{formatFileSize(size)}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<FileIcon type={type} fileName={name} className="shrink-0 transform-gpu" />
|
||||
<span
|
||||
className="grow truncate system-sm-medium text-text-secondary"
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
{!isFolder && typeof size === 'number' && (
|
||||
<span className="shrink-0 system-xs-regular text-text-tertiary">{formatFileSize(size)}</span>
|
||||
)}
|
||||
</div>
|
||||
</Wrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import type { InitialDocumentDetail } from '@/models/pipeline'
|
||||
import type { RETRIEVE_METHOD } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import {
|
||||
RiAedFill,
|
||||
RiArrowRightLine,
|
||||
@ -17,7 +18,6 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import NotionIcon from '@/app/components/base/notion-icon'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import PriorityLabel from '@/app/components/billing/priority-label'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
@ -203,15 +203,18 @@ const EmbeddingProcess = ({
|
||||
<div className="shrink-0 text-xs text-text-secondary">{`${getSourcePercent(indexingStatusDetail)}%`}</div>
|
||||
)}
|
||||
{indexingStatusDetail.indexing_status === 'error' && (
|
||||
<Tooltip
|
||||
popupClassName="px-4 py-[14px] max-w-60 body-xs-regular text-text-secondary border-[0.5px] border-components-panel-border rounded-xl"
|
||||
offset={4}
|
||||
popupContent={indexingStatusDetail.error}
|
||||
>
|
||||
<span>
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
openOnHover
|
||||
aria-label={indexingStatusDetail.error}
|
||||
className="inline-flex border-0 bg-transparent p-0"
|
||||
>
|
||||
<RiErrorWarningFill className="size-4 shrink-0 text-text-destructive" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent popupClassName="max-w-60 rounded-xl border-[0.5px] border-components-panel-border px-4 py-[14px] body-xs-regular text-text-secondary">
|
||||
{indexingStatusDetail.error}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
{indexingStatusDetail.indexing_status === 'completed' && (
|
||||
<RiCheckboxCircleFill className="size-4 shrink-0 text-text-success" />
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import type { FC } from 'react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { RiLineHeight } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Collapse } from '@/app/components/base/icons/src/vender/knowledge'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type DisplayToggleProps = {
|
||||
isCollapsed: boolean
|
||||
@ -15,25 +15,30 @@ const DisplayToggle: FC<DisplayToggleProps> = ({
|
||||
toggleCollapsed,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const label = isCollapsed ? t('segment.expandChunks', { ns: 'datasetDocuments' }) : t('segment.collapseChunks', { ns: 'datasetDocuments' })
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={isCollapsed ? t('segment.expandChunks', { ns: 'datasetDocuments' }) : t('segment.collapseChunks', { ns: 'datasetDocuments' })}
|
||||
popupClassName="text-text-secondary system-xs-medium border-[0.5px] border-components-panel-border"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-center rounded-lg border-[0.5px] border-components-button-secondary-border
|
||||
bg-components-button-secondary-bg p-2 shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px]"
|
||||
onClick={toggleCollapsed}
|
||||
>
|
||||
{
|
||||
isCollapsed
|
||||
? <RiLineHeight className="h-4 w-4 text-components-button-secondary-text" />
|
||||
: <Collapse className="h-4 w-4 text-components-button-secondary-text" />
|
||||
}
|
||||
</button>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
aria-label={label}
|
||||
className="flex items-center justify-center rounded-lg border-[0.5px] border-components-button-secondary-border
|
||||
bg-components-button-secondary-bg p-2 shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px]"
|
||||
onClick={toggleCollapsed}
|
||||
>
|
||||
{
|
||||
isCollapsed
|
||||
? <RiLineHeight className="h-4 w-4 text-components-button-secondary-text" />
|
||||
: <Collapse className="h-4 w-4 text-components-button-secondary-text" />
|
||||
}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent className="border-[0.5px] border-components-panel-border system-xs-medium text-text-secondary">
|
||||
{label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@ -10,13 +10,13 @@ import {
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
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, RiEditLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import ImageList from '@/app/components/datasets/common/image-list'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
@ -182,35 +182,43 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
||||
>
|
||||
{!archived && (
|
||||
<>
|
||||
<Tooltip
|
||||
popupContent="Edit"
|
||||
popupClassName="text-text-secondary system-xs-medium"
|
||||
>
|
||||
<div
|
||||
data-testid="segment-edit-button"
|
||||
className="flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClickEdit?.()
|
||||
}}
|
||||
>
|
||||
<RiEditLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Edit"
|
||||
data-testid="segment-edit-button"
|
||||
className="flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClickEdit?.()
|
||||
}}
|
||||
>
|
||||
<RiEditLine className="h-4 w-4 text-text-tertiary" />
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent className="system-xs-medium text-text-secondary">Edit</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
popupContent="Delete"
|
||||
popupClassName="text-text-secondary system-xs-medium"
|
||||
>
|
||||
<div
|
||||
data-testid="segment-delete-button"
|
||||
className="group/delete flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-lg hover:bg-state-destructive-hover"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowModal(true)
|
||||
}}
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4 text-text-tertiary group-hover/delete:text-text-destructive" />
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Delete"
|
||||
data-testid="segment-delete-button"
|
||||
className="group/delete flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-lg hover:bg-state-destructive-hover"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowModal(true)
|
||||
}}
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4 text-text-tertiary group-hover/delete:text-text-destructive" />
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent className="system-xs-medium text-text-secondary">Delete</TooltipContent>
|
||||
</Tooltip>
|
||||
<Divider type="vertical" className="h-3.5 bg-divider-regular" />
|
||||
</>
|
||||
|
||||
@ -3,7 +3,6 @@ import type { FC } from 'react'
|
||||
import type { BuiltInMetadataItem, MetadataItemInBatchEdit, MetadataItemWithEdit } from '../types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiQuestionLine } from '@remixicon/react'
|
||||
import { produce } from 'immer'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
@ -11,8 +10,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { useCreateMetaData } from '@/service/knowledge/use-metadata'
|
||||
import Checkbox from '../../../base/checkbox'
|
||||
import { Infotip } from '../../../base/infotip'
|
||||
import Modal from '../../../base/modal'
|
||||
import Tooltip from '../../../base/tooltip'
|
||||
import AddMetadataButton from '../add-metadata-button'
|
||||
import useCheckMetadataName from '../hooks/use-check-metadata-name'
|
||||
import SelectMetadataModal from '../metadata-dataset/select-metadata-modal'
|
||||
@ -115,11 +114,14 @@ const EditMetadataBatchModal: FC<Props> = ({ datasetId, documentNum, list, onSav
|
||||
<div className="flex items-center select-none">
|
||||
<Checkbox checked={isApplyToAllSelectDocument} onCheck={() => setIsApplyToAllSelectDocument(!isApplyToAllSelectDocument)} id="apply-to-all" />
|
||||
<div className="mr-1 ml-2 system-xs-medium text-text-secondary">{t(`${i18nPrefix}.applyToAllSelectDocument`, { ns: 'dataset' })}</div>
|
||||
<Tooltip popupContent={<div className="max-w-[240px]">{t(`${i18nPrefix}.applyToAllSelectDocumentTip`, { ns: 'dataset' })}</div>}>
|
||||
<div className="cursor-pointer p-px">
|
||||
<RiQuestionLine className="size-3.5 text-text-tertiary" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Infotip
|
||||
aria-label={t(`${i18nPrefix}.applyToAllSelectDocumentTip`, { ns: 'dataset' })}
|
||||
className="p-px"
|
||||
iconClassName="size-3.5 text-text-tertiary"
|
||||
popupClassName="max-w-[240px]"
|
||||
>
|
||||
{t(`${i18nPrefix}.applyToAllSelectDocumentTip`, { ns: 'dataset' })}
|
||||
</Infotip>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button onClick={onHide}>
|
||||
|
||||
@ -20,9 +20,9 @@ import * as React from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import CreateModal from '@/app/components/datasets/metadata/metadata-dataset/create-metadata-modal'
|
||||
import { getIcon } from '../utils/get-icon'
|
||||
import Field from './field'
|
||||
@ -215,7 +215,9 @@ const DatasetMetadataDrawer: FC<Props> = ({
|
||||
onCheckedChange={onIsBuiltInEnabledChange}
|
||||
/>
|
||||
<div className="mr-0.5 ml-2 system-sm-semibold text-text-secondary">{t(`${i18nPrefix}.builtIn`, { ns: 'dataset' })}</div>
|
||||
<Tooltip popupContent={<div className="max-w-[100px]">{t(`${i18nPrefix}.builtInDescription`, { ns: 'dataset' })}</div>} />
|
||||
<Infotip aria-label={t(`${i18nPrefix}.builtInDescription`, { ns: 'dataset' })} popupClassName="max-w-[100px]">
|
||||
{t(`${i18nPrefix}.builtInDescription`, { ns: 'dataset' })}
|
||||
</Infotip>
|
||||
</div>
|
||||
|
||||
<div className="mt-1 space-y-1">
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { CustomModel, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import SwitchCredentialInLoadBalancing from '../switch-credential-in-load-balancing'
|
||||
|
||||
@ -105,7 +106,8 @@ describe('SwitchCredentialInLoadBalancing', () => {
|
||||
expect(mockSetCustomModelCredential).toHaveBeenCalledWith(mockCredentials[0])
|
||||
})
|
||||
|
||||
it('should show tooltip when empty and custom credentials not allowed', () => {
|
||||
it('should show tooltip when empty and custom credentials not allowed', async () => {
|
||||
const user = userEvent.setup()
|
||||
const restrictedProvider = { ...mockProvider, allow_custom_token: false }
|
||||
render(
|
||||
<SwitchCredentialInLoadBalancing
|
||||
@ -117,8 +119,8 @@ describe('SwitchCredentialInLoadBalancing', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.mouseEnter(screen.getByText(/auth.credentialUnavailableInButton/))
|
||||
expect(screen.getByText('plugin.auth.credentialUnavailable'))!.toBeInTheDocument()
|
||||
await user.hover(screen.getByRole('button', { name: /auth.credentialUnavailableInButton/ }))
|
||||
expect(await screen.findByText('plugin.auth.credentialUnavailable'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Empty credentials with allowed custom: no tooltip but still shows unavailable text
|
||||
|
||||
@ -5,6 +5,7 @@ import type {
|
||||
import {
|
||||
Button,
|
||||
} from '@langgenius/dify-ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import {
|
||||
RiEqualizer2Line,
|
||||
} from '@remixicon/react'
|
||||
@ -13,7 +14,6 @@ import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import Authorized from './authorized'
|
||||
import { useCredentialStatus } from './hooks'
|
||||
@ -53,11 +53,11 @@ const ConfigProvider = ({
|
||||
)
|
||||
if (notAllowCustomCredential && !hasCredential) {
|
||||
return (
|
||||
<Tooltip
|
||||
asChild
|
||||
popupContent={t('auth.credentialUnavailable', { ns: 'plugin' })}
|
||||
>
|
||||
{Item}
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={Item} />
|
||||
<TooltipContent>
|
||||
{t('auth.credentialUnavailable', { ns: 'plugin' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import type {
|
||||
} from '../declarations'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import {
|
||||
memo,
|
||||
@ -13,7 +14,6 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { ConfigurationMethodEnum, ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Authorized from './authorized'
|
||||
@ -89,11 +89,11 @@ const SwitchCredentialInLoadBalancing = ({
|
||||
)
|
||||
if (empty && notAllowCustomCredential) {
|
||||
return (
|
||||
<Tooltip
|
||||
asChild
|
||||
popupContent={t('auth.credentialUnavailable', { ns: 'plugin' })}
|
||||
>
|
||||
{Item}
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={Item} />
|
||||
<TooltipContent>
|
||||
{t('auth.credentialUnavailable', { ns: 'plugin' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@ -21,10 +21,10 @@ describe('StatusIndicators', () => {
|
||||
installedPlugins = [{ name: 'demo-plugin', plugin_unique_identifier: 'demo@1.0.0' }]
|
||||
})
|
||||
|
||||
const getTooltipTrigger = (container: HTMLElement) => {
|
||||
const trigger = container.querySelector('[role="button"][aria-haspopup="dialog"]')
|
||||
const getPopoverTrigger = (name: string) => {
|
||||
const trigger = screen.getByRole('button', { name })
|
||||
expect(trigger).toBeInTheDocument()
|
||||
return trigger as HTMLElement
|
||||
return trigger
|
||||
}
|
||||
|
||||
it('should render nothing when model is available and enabled', () => {
|
||||
@ -43,7 +43,7 @@ describe('StatusIndicators', () => {
|
||||
|
||||
it('should render deprecated tooltip when provider model is disabled and in model list', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { container } = render(
|
||||
render(
|
||||
<StatusIndicators
|
||||
needsConfiguration={false}
|
||||
modelProvider={true}
|
||||
@ -54,14 +54,14 @@ describe('StatusIndicators', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.hover(getTooltipTrigger(container))
|
||||
await user.hover(getPopoverTrigger('nodes.agent.modelSelectorTooltips.deprecated'))
|
||||
|
||||
expect(await screen.findByText('nodes.agent.modelSelectorTooltips.deprecated')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render model-not-support tooltip when disabled model is not in model list and has no pluginInfo', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { container } = render(
|
||||
render(
|
||||
<StatusIndicators
|
||||
needsConfiguration={false}
|
||||
modelProvider={true}
|
||||
@ -72,7 +72,7 @@ describe('StatusIndicators', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.hover(getTooltipTrigger(container))
|
||||
await user.hover(getPopoverTrigger('nodes.agent.modelNotSupport.title'))
|
||||
|
||||
expect(await screen.findByText('nodes.agent.modelNotSupport.title')).toBeInTheDocument()
|
||||
})
|
||||
@ -125,7 +125,7 @@ describe('StatusIndicators', () => {
|
||||
|
||||
it('should render marketplace warning tooltip when provider is unavailable', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { container } = render(
|
||||
render(
|
||||
<StatusIndicators
|
||||
needsConfiguration={false}
|
||||
modelProvider={false}
|
||||
@ -136,7 +136,7 @@ describe('StatusIndicators', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.hover(getTooltipTrigger(container))
|
||||
await user.hover(getPopoverTrigger('nodes.agent.modelNotInMarketplace.title'))
|
||||
|
||||
expect(await screen.findByText('nodes.agent.modelNotInMarketplace.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { RiErrorWarningFill } from '@remixicon/react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { SwitchPluginVersion } from '@/app/components/workflow/nodes/_base/components/switch-plugin-version'
|
||||
import Link from '@/next/link'
|
||||
import { useInstalledPluginList } from '@/service/use-plugins'
|
||||
@ -13,6 +14,28 @@ type StatusIndicatorsProps = {
|
||||
t: any
|
||||
}
|
||||
|
||||
type StatusPopoverProps = {
|
||||
ariaLabel: string
|
||||
content: ReactNode
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const StatusPopover = ({ ariaLabel, content, children }: StatusPopoverProps) => (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
openOnHover
|
||||
aria-label={ariaLabel}
|
||||
className="inline-flex border-0 bg-transparent p-0"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent placement="top" popupClassName="rounded-md px-3 py-2 system-xs-regular text-text-tertiary">
|
||||
{content}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
|
||||
const StatusIndicators = ({ needsConfiguration, modelProvider, inModelList, disabled, pluginInfo, t }: StatusIndicatorsProps) => {
|
||||
const { data: pluginList } = useInstalledPluginList()
|
||||
const renderTooltipContent = (title: string, description?: string, linkText?: string, linkHref?: string) => {
|
||||
@ -48,27 +71,26 @@ const StatusIndicators = ({ needsConfiguration, modelProvider, inModelList, disa
|
||||
<>
|
||||
{inModelList
|
||||
? (
|
||||
<Tooltip
|
||||
popupContent={t('nodes.agent.modelSelectorTooltips.deprecated', { ns: 'workflow' })}
|
||||
asChild={false}
|
||||
needsDelay={false}
|
||||
<StatusPopover
|
||||
ariaLabel={t('nodes.agent.modelSelectorTooltips.deprecated', { ns: 'workflow' })}
|
||||
content={t('nodes.agent.modelSelectorTooltips.deprecated', { ns: 'workflow' })}
|
||||
>
|
||||
<RiErrorWarningFill className="h-4 w-4 text-text-destructive" />
|
||||
</Tooltip>
|
||||
</StatusPopover>
|
||||
)
|
||||
: !pluginInfo
|
||||
? (
|
||||
<Tooltip
|
||||
popupContent={renderTooltipContent(
|
||||
<StatusPopover
|
||||
ariaLabel={t('nodes.agent.modelNotSupport.title', { ns: 'workflow' })}
|
||||
content={renderTooltipContent(
|
||||
t('nodes.agent.modelNotSupport.title', { ns: 'workflow' }),
|
||||
t('nodes.agent.modelNotSupport.desc', { ns: 'workflow' }),
|
||||
t('nodes.agent.linkToPlugin', { ns: 'workflow' }),
|
||||
'/plugins',
|
||||
)}
|
||||
asChild={false}
|
||||
>
|
||||
<RiErrorWarningFill className="h-4 w-4 text-text-destructive" />
|
||||
</Tooltip>
|
||||
</StatusPopover>
|
||||
)
|
||||
: (
|
||||
<SwitchPluginVersion
|
||||
@ -82,17 +104,17 @@ const StatusIndicators = ({ needsConfiguration, modelProvider, inModelList, disa
|
||||
</>
|
||||
)}
|
||||
{!modelProvider && !pluginInfo && (
|
||||
<Tooltip
|
||||
popupContent={renderTooltipContent(
|
||||
<StatusPopover
|
||||
ariaLabel={t('nodes.agent.modelNotInMarketplace.title', { ns: 'workflow' })}
|
||||
content={renderTooltipContent(
|
||||
t('nodes.agent.modelNotInMarketplace.title', { ns: 'workflow' }),
|
||||
t('nodes.agent.modelNotInMarketplace.desc', { ns: 'workflow' }),
|
||||
t('nodes.agent.linkToPlugin', { ns: 'workflow' }),
|
||||
'/plugins',
|
||||
)}
|
||||
asChild={false}
|
||||
>
|
||||
<RiErrorWarningFill className="h-4 w-4 text-text-destructive" />
|
||||
</Tooltip>
|
||||
</StatusPopover>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@ -42,10 +42,6 @@ vi.mock('../feature-icon', () => ({
|
||||
default: ({ feature }: { feature: string }) => <span data-testid="feature-icon">{feature}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
const mockCredentialPanelState = vi.hoisted(() => vi.fn())
|
||||
vi.mock('../../provider-added-card/use-credential-panel-state', () => ({
|
||||
useCredentialPanelState: mockCredentialPanelState,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { ModelItem, ModelProvider } from '../declarations'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
@ -7,7 +8,6 @@ import { memo, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext, useProviderContextSelector } from '@/context/provider-context'
|
||||
@ -102,14 +102,12 @@ const ModelListItem = ({ model, provider, isConfigurable, onChange, onModifyLoad
|
||||
{
|
||||
model.deprecated
|
||||
? (
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<span className="font-semibold">{t('modelProvider.modelHasBeenDeprecated', { ns: 'common' })}</span>
|
||||
}
|
||||
offset={{ mainAxis: 4 }}
|
||||
>
|
||||
<Switch checked={false} disabled size="md" />
|
||||
</Tooltip>
|
||||
<Popover>
|
||||
<PopoverTrigger nativeButton={false} openOnHover render={<span><Switch checked={false} disabled size="md" /></span>} />
|
||||
<PopoverContent popupClassName="px-3 py-2 font-semibold system-xs-regular text-text-tertiary">
|
||||
{t('modelProvider.modelHasBeenDeprecated', { ns: 'common' })}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
: (isCurrentWorkspaceManager && (
|
||||
<Switch
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEditLine,
|
||||
@ -10,7 +11,6 @@ import {
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { DeleteConfirm } from './delete-confirm'
|
||||
import { EditModal } from './edit'
|
||||
|
||||
@ -65,19 +65,26 @@ const SubscriptionCard = ({ data, pluginDetail }: Props) => {
|
||||
</div>
|
||||
|
||||
<div className="mt-1 flex items-center justify-between">
|
||||
<Tooltip
|
||||
disabled={!data.endpoint}
|
||||
popupContent={data.endpoint && (
|
||||
<div className="max-w-[320px] break-all">
|
||||
{data.endpoint}
|
||||
</div>
|
||||
)}
|
||||
position="left"
|
||||
>
|
||||
<div className="flex-1 truncate system-xs-regular text-text-tertiary">
|
||||
{data.endpoint}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{data.endpoint
|
||||
? (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
openOnHover
|
||||
aria-label={data.endpoint}
|
||||
className="flex-1 truncate border-0 bg-transparent p-0 text-left system-xs-regular text-text-tertiary"
|
||||
>
|
||||
{data.endpoint}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent placement="left" popupClassName="max-w-[320px] break-all px-3 py-2 system-xs-regular text-text-tertiary">
|
||||
{data.endpoint}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
: (
|
||||
<div className="flex-1 truncate system-xs-regular text-text-tertiary">
|
||||
{data.endpoint}
|
||||
</div>
|
||||
)}
|
||||
<div className="mx-2 text-xs text-text-tertiary opacity-30">·</div>
|
||||
<div className="shrink-0 system-xs-regular text-text-tertiary">
|
||||
{data.workflows_in_use > 0 ? t('subscription.list.item.usedByNum', { ns: 'pluginTrigger', num: data.workflows_in_use }) : t('subscription.list.item.noUsed', { ns: 'pluginTrigger' })}
|
||||
|
||||
@ -54,10 +54,6 @@ vi.mock('@langgenius/dify-ui/switch', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useLanguage: () => 'en_US',
|
||||
}))
|
||||
@ -233,7 +229,7 @@ describe('ReasoningConfigForm', () => {
|
||||
it('should open schema modal for object fields and support app selection', () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
const { container } = render(
|
||||
render(
|
||||
<ReasoningConfigForm
|
||||
value={{
|
||||
app: {
|
||||
@ -265,7 +261,7 @@ describe('ReasoningConfigForm', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(container.querySelector('div.ml-0\\.5.cursor-pointer')!)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'workflow.nodes.agent.clickToViewParameterSchema' }))
|
||||
expect(screen.getByTestId('schema-modal')).toHaveTextContent('Config')
|
||||
fireEvent.click(screen.getByTestId('close-schema'))
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import type {
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import {
|
||||
RiArrowRightUpLine,
|
||||
RiBracesLine,
|
||||
@ -16,9 +17,8 @@ import {
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import Input from '@/app/components/base/input'
|
||||
// eslint-disable-next-line no-restricted-imports -- legacy tooltip migration is handled separately from this change
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { AppSelector } from '@/app/components/plugins/plugin-detail-panel/app-selector'
|
||||
@ -127,17 +127,16 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
} = schema
|
||||
const auto = value[variable]?.auto
|
||||
const fieldTitle = getFieldTitle(label, language)
|
||||
const tooltipContent = (tooltip && (
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[200px]">
|
||||
{tooltip[language] || tooltip.en_US}
|
||||
</div>
|
||||
)}
|
||||
triggerClassName="ml-0.5 w-4 h-4"
|
||||
asChild={false}
|
||||
/>
|
||||
))
|
||||
const tooltipText = tooltip?.[language] || tooltip?.en_US
|
||||
const tooltipContent = tooltipText && (
|
||||
<Infotip
|
||||
aria-label={tooltipText}
|
||||
className="ml-0.5 h-4 w-4"
|
||||
popupClassName="w-[200px]"
|
||||
>
|
||||
{tooltipText}
|
||||
</Infotip>
|
||||
)
|
||||
const varInput = value[variable]!.value
|
||||
const {
|
||||
isString,
|
||||
@ -173,20 +172,22 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
<span className="mx-1 system-xs-regular text-text-quaternary">·</span>
|
||||
<span className="system-xs-regular text-text-tertiary">{resolveTargetVarType(type)}</span>
|
||||
{isShowJSONEditor && (
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="system-xs-medium text-text-secondary">
|
||||
{t('nodes.agent.clickToViewParameterSchema', { ns: 'workflow' })}
|
||||
</div>
|
||||
)}
|
||||
asChild={false}
|
||||
>
|
||||
<div
|
||||
className="ml-0.5 cursor-pointer rounded-sm p-px text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary"
|
||||
onClick={() => showSchema(input_schema as SchemaRoot, fieldTitle!)}
|
||||
>
|
||||
<RiBracesLine className="size-3.5" />
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('nodes.agent.clickToViewParameterSchema', { ns: 'workflow' })}
|
||||
className="ml-0.5 cursor-pointer rounded-sm border-0 bg-transparent p-px text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary"
|
||||
onClick={() => showSchema(input_schema as SchemaRoot, fieldTitle!)}
|
||||
>
|
||||
<RiBracesLine className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent className="system-xs-medium text-text-secondary">
|
||||
{t('nodes.agent.clickToViewParameterSchema', { ns: 'workflow' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEqualizer2Line,
|
||||
@ -14,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 { 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'
|
||||
import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
|
||||
@ -144,11 +143,14 @@ const ToolItem = ({
|
||||
className="-mt-1"
|
||||
uniqueIdentifier={installInfo}
|
||||
tooltip={(
|
||||
<ToolTipContent
|
||||
title={t('detailPanel.toolSelector.unsupportedTitle', { ns: 'plugin' })}
|
||||
>
|
||||
{`${t('detailPanel.toolSelector.unsupportedContent', { ns: 'plugin' })} ${t('detailPanel.toolSelector.unsupportedContent2', { ns: 'plugin' })}`}
|
||||
</ToolTipContent>
|
||||
<div className="w-[180px]" data-testid="tooltip-content">
|
||||
<div className="mb-1.5 font-semibold text-text-secondary" data-testid="tooltip-content-title">
|
||||
{t('detailPanel.toolSelector.unsupportedTitle', { ns: 'plugin' })}
|
||||
</div>
|
||||
<div className="mb-1.5 text-text-tertiary" data-testid="tooltip-content-body">
|
||||
{`${t('detailPanel.toolSelector.unsupportedContent', { ns: 'plugin' })} ${t('detailPanel.toolSelector.unsupportedContent2', { ns: 'plugin' })}`}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
onChange={() => {
|
||||
onInstall?.()
|
||||
@ -167,18 +169,18 @@ const ToolItem = ({
|
||||
/>
|
||||
)}
|
||||
{isError && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<div aria-label={typeof errorTip === 'string' ? errorTip : undefined}>
|
||||
<RiErrorWarningFill className="h-4 w-4 text-text-destructive" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
openOnHover
|
||||
aria-label={typeof errorTip === 'string' ? errorTip : t('detailPanel.toolSelector.unsupportedTitle', { ns: 'plugin' })}
|
||||
className="inline-flex border-0 bg-transparent p-0"
|
||||
>
|
||||
<RiErrorWarningFill className="h-4 w-4 text-text-destructive" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent popupClassName="px-3 py-2 system-xs-regular text-text-tertiary">
|
||||
{errorTip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import type { PluginDetail } from '../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import {
|
||||
RiArrowRightUpLine,
|
||||
RiBugLine,
|
||||
@ -13,7 +14,6 @@ import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
|
||||
import { API_PREFIX } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
@ -124,12 +124,18 @@ const PluginItem: FC<Props> = ({
|
||||
<Title title={title} />
|
||||
{verified && <Verified className="ml-0.5 h-4 w-4" text={t('marketplace.verifiedTip', { ns: 'plugin' })} />}
|
||||
{!isDifyVersionCompatible && (
|
||||
<Tooltip popupContent={
|
||||
t('difyVersionNotCompatible', { ns: 'plugin', minimalDifyVersion: declarationMeta.minimum_dify_version })
|
||||
}
|
||||
>
|
||||
<RiErrorWarningLine color="red" className="ml-0.5 h-4 w-4 shrink-0 text-text-accent" />
|
||||
</Tooltip>
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
openOnHover
|
||||
aria-label={t('difyVersionNotCompatible', { ns: 'plugin', minimalDifyVersion: declarationMeta.minimum_dify_version })}
|
||||
className="ml-0.5 inline-flex h-4 w-4 shrink-0 border-0 bg-transparent p-0"
|
||||
>
|
||||
<RiErrorWarningLine color="red" className="h-4 w-4 text-text-accent" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent popupClassName="px-3 py-2 system-xs-regular text-text-tertiary">
|
||||
{t('difyVersionNotCompatible', { ns: 'plugin', minimalDifyVersion: declarationMeta.minimum_dify_version })}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
<Badge
|
||||
className="ml-1 shrink-0"
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNodes } from 'reactflow'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
|
||||
import { useNodesSyncDraft } from '@/app/components/workflow/hooks'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
@ -137,10 +137,12 @@ const InputFieldPanel = () => {
|
||||
<span className="system-sm-semibold-uppercase text-text-secondary">
|
||||
{t('inputFieldPanel.uniqueInputs.title', { ns: 'datasetPipeline' })}
|
||||
</span>
|
||||
<Tooltip
|
||||
popupContent={t('inputFieldPanel.uniqueInputs.tooltip', { ns: 'datasetPipeline' })}
|
||||
<Infotip
|
||||
aria-label={t('inputFieldPanel.uniqueInputs.tooltip', { ns: 'datasetPipeline' })}
|
||||
popupClassName="max-w-[240px]"
|
||||
/>
|
||||
>
|
||||
{t('inputFieldPanel.uniqueInputs.tooltip', { ns: 'datasetPipeline' })}
|
||||
</Infotip>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-1 py-1">
|
||||
{
|
||||
|
||||
@ -6,9 +6,9 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Drawer from '@/app/components/base/drawer-plus'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Radio from '@/app/components/base/radio/ui'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { AuthHeaderPrefix, AuthType } from '@/app/components/tools/types'
|
||||
|
||||
type Props = {
|
||||
@ -123,14 +123,13 @@ const ConfigCredential: FC<Props> = ({
|
||||
<div>
|
||||
<div className="flex items-center py-2 system-sm-medium text-text-primary">
|
||||
{t('createTool.authMethod.key', { ns: 'tools' })}
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[261px] text-text-tertiary">
|
||||
{t('createTool.authMethod.keyTooltip', { ns: 'tools' })}
|
||||
</div>
|
||||
)}
|
||||
triggerClassName="ml-0.5 w-4 h-4"
|
||||
/>
|
||||
<Infotip
|
||||
aria-label={t('createTool.authMethod.keyTooltip', { ns: 'tools' })}
|
||||
className="ml-0.5 h-4 w-4"
|
||||
popupClassName="w-[261px] text-text-tertiary"
|
||||
>
|
||||
{t('createTool.authMethod.keyTooltip', { ns: 'tools' })}
|
||||
</Infotip>
|
||||
</div>
|
||||
<Input
|
||||
value={tempCredential.api_key_header}
|
||||
@ -153,14 +152,13 @@ const ConfigCredential: FC<Props> = ({
|
||||
<div>
|
||||
<div className="flex items-center py-2 system-sm-medium text-text-primary">
|
||||
{t('createTool.authMethod.queryParam', { ns: 'tools' })}
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[261px] text-text-tertiary">
|
||||
{t('createTool.authMethod.queryParamTooltip', { ns: 'tools' })}
|
||||
</div>
|
||||
)}
|
||||
triggerClassName="ml-0.5 w-4 h-4"
|
||||
/>
|
||||
<Infotip
|
||||
aria-label={t('createTool.authMethod.queryParamTooltip', { ns: 'tools' })}
|
||||
className="ml-0.5 h-4 w-4"
|
||||
popupClassName="w-[261px] text-text-tertiary"
|
||||
>
|
||||
{t('createTool.authMethod.queryParamTooltip', { ns: 'tools' })}
|
||||
</Infotip>
|
||||
</div>
|
||||
<Input
|
||||
value={tempCredential.api_key_query_param}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { WorkflowToolDrawerPayload } from '../index'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
@ -28,21 +27,6 @@ vi.mock('@/app/components/tools/labels/selector', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({
|
||||
children,
|
||||
popupContent,
|
||||
}: {
|
||||
children?: ReactNode
|
||||
popupContent?: ReactNode
|
||||
}) => (
|
||||
<div>
|
||||
{children}
|
||||
{popupContent}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../confirm-modal', () => ({
|
||||
default: ({ show, onClose, onConfirm }: { show: boolean, onClose: () => void, onConfirm: () => void }) => (
|
||||
show
|
||||
|
||||
@ -23,21 +23,6 @@ const {
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({
|
||||
children,
|
||||
popupContent,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
popupContent: React.ReactNode
|
||||
}) => (
|
||||
<div>
|
||||
<span>{popupContent}</span>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useFeaturedToolsRecommendations: () => ({
|
||||
plugins: [],
|
||||
@ -121,11 +106,13 @@ describe('Tabs', () => {
|
||||
filterElem: <div>filter</div>,
|
||||
}
|
||||
|
||||
it('should render start content and disabled tab tooltip text', () => {
|
||||
it('should render start content and disabled tab tooltip text', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<Tabs {...baseProps} />)
|
||||
|
||||
expect(screen.getByText('start-content'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.tabs.startDisabledTip'))!.toBeInTheDocument()
|
||||
await user.hover(screen.getByText('Blocks'))
|
||||
expect(await screen.findByText('workflow.tabs.startDisabledTip'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should switch tabs through click handlers and render tools content with normalized icons', () => {
|
||||
|
||||
@ -6,10 +6,10 @@ import type {
|
||||
ToolWithProvider,
|
||||
} from '../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { memo, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
import { useFeaturedToolsRecommendations } from '@/service/use-plugins'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools } from '@/service/use-tools'
|
||||
@ -129,19 +129,22 @@ const TabHeaderItem = ({
|
||||
|
||||
if (tab.disabled) {
|
||||
return (
|
||||
<Tooltip
|
||||
key={tab.key}
|
||||
position="top"
|
||||
popupClassName="max-w-[200px]"
|
||||
popupContent={disabledTip}
|
||||
>
|
||||
<div
|
||||
className={className}
|
||||
aria-disabled={tab.disabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{tab.name}
|
||||
</div>
|
||||
<Tooltip key={tab.key}>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
className={className}
|
||||
aria-disabled={tab.disabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent placement="top" className="max-w-[200px]">
|
||||
{disabledTip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,22 +1,23 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { RiAlertFill } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
const McpToolNotSupportTooltip: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const tip = t('detailPanel.toolSelector.unsupportedMCPTool', { ns: 'plugin' })
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[256px]">
|
||||
{t('detailPanel.toolSelector.unsupportedMCPTool', { ns: 'plugin' })}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<RiAlertFill className="size-4 text-text-warning-secondary" />
|
||||
</Tooltip>
|
||||
<Popover>
|
||||
<PopoverTrigger openOnHover aria-label={tip} className="inline-flex border-0 bg-transparent p-0">
|
||||
<RiAlertFill className="size-4 text-text-warning-secondary" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent popupClassName="w-[256px] px-3 py-2 system-xs-regular text-text-tertiary">
|
||||
{tip}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
export default React.memo(McpToolNotSupportTooltip)
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { RiArrowLeftRightLine, RiExternalLinkLine } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { Badge as Badge2, BadgeState } from '@/app/components/base/badge/index'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
import { pluginManifestToCardPluginProps } from '@/app/components/plugins/install-plugin/utils'
|
||||
import PluginMutationModel from '@/app/components/plugins/plugin-mutation-model'
|
||||
@ -67,76 +67,91 @@ export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => {
|
||||
if (!uniqueIdentifier || !pluginId)
|
||||
return null
|
||||
|
||||
const content = (
|
||||
<div className={cn('flex w-fit items-center justify-center', className)} onClick={e => e.stopPropagation()}>
|
||||
{isShowUpdateModal && pluginDetail && (
|
||||
<PluginMutationModel
|
||||
onCancel={hideUpdateModal}
|
||||
plugin={pluginManifestToCardPluginProps({
|
||||
...pluginDetail.declaration,
|
||||
icon: icon!,
|
||||
})}
|
||||
mutation={mutation}
|
||||
mutate={install}
|
||||
confirmButtonText={t('nodes.agent.installPlugin.install', { ns: 'workflow' })}
|
||||
cancelButtonText={t('nodes.agent.installPlugin.cancel', { ns: 'workflow' })}
|
||||
modelTitle={t('nodes.agent.installPlugin.title', { ns: 'workflow' })}
|
||||
description={t('nodes.agent.installPlugin.desc', { ns: 'workflow' })}
|
||||
cardTitleLeft={(
|
||||
<>
|
||||
<Badge2 className="mx-1" size="s" state={BadgeState.Warning}>
|
||||
{`${pluginDetail.version} -> ${target!.version}`}
|
||||
</Badge2>
|
||||
</>
|
||||
)}
|
||||
modalBottomLeft={(
|
||||
<Link
|
||||
className="flex items-center justify-center gap-1"
|
||||
href={getMarketplaceUrl(`/plugins/${pluginDetail.declaration.author}/${pluginDetail.declaration.name}`)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="system-xs-regular text-xs text-text-accent">
|
||||
{t('nodes.agent.installPlugin.changelog', { ns: 'workflow' })}
|
||||
</span>
|
||||
<RiExternalLinkLine className="size-3 text-text-accent" />
|
||||
</Link>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{pluginDetail && (
|
||||
<PluginVersionPicker
|
||||
isShow={isShow}
|
||||
onShowChange={setIsShow}
|
||||
pluginID={pluginId}
|
||||
currentVersion={pluginDetail.version}
|
||||
onSelect={(state) => {
|
||||
setTarget({
|
||||
pluginUniqueIden: state.unique_identifier,
|
||||
version: state.version,
|
||||
})
|
||||
showUpdateModal()
|
||||
}}
|
||||
trigger={(
|
||||
<Badge
|
||||
className={cn(
|
||||
'mx-1 flex hover:bg-state-base-hover',
|
||||
isShow && 'bg-state-base-hover',
|
||||
)}
|
||||
uppercase={true}
|
||||
text={(
|
||||
<>
|
||||
<div>{pluginDetail.version}</div>
|
||||
<RiArrowLeftRightLine className="ml-1 h-3 w-3 text-text-tertiary" />
|
||||
</>
|
||||
)}
|
||||
hasRedCornerMark={true}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
if (!tooltip || isShow || isShowUpdateModal)
|
||||
return content
|
||||
|
||||
return (
|
||||
<Tooltip popupContent={!isShow && !isShowUpdateModal && tooltip} triggerMethod="hover">
|
||||
<div className={cn('flex w-fit items-center justify-center', className)} onClick={e => e.stopPropagation()}>
|
||||
{isShowUpdateModal && pluginDetail && (
|
||||
<PluginMutationModel
|
||||
onCancel={hideUpdateModal}
|
||||
plugin={pluginManifestToCardPluginProps({
|
||||
...pluginDetail.declaration,
|
||||
icon: icon!,
|
||||
})}
|
||||
mutation={mutation}
|
||||
mutate={install}
|
||||
confirmButtonText={t('nodes.agent.installPlugin.install', { ns: 'workflow' })}
|
||||
cancelButtonText={t('nodes.agent.installPlugin.cancel', { ns: 'workflow' })}
|
||||
modelTitle={t('nodes.agent.installPlugin.title', { ns: 'workflow' })}
|
||||
description={t('nodes.agent.installPlugin.desc', { ns: 'workflow' })}
|
||||
cardTitleLeft={(
|
||||
<>
|
||||
<Badge2 className="mx-1" size="s" state={BadgeState.Warning}>
|
||||
{`${pluginDetail.version} -> ${target!.version}`}
|
||||
</Badge2>
|
||||
</>
|
||||
)}
|
||||
modalBottomLeft={(
|
||||
<Link
|
||||
className="flex items-center justify-center gap-1"
|
||||
href={getMarketplaceUrl(`/plugins/${pluginDetail.declaration.author}/${pluginDetail.declaration.name}`)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="system-xs-regular text-xs text-text-accent">
|
||||
{t('nodes.agent.installPlugin.changelog', { ns: 'workflow' })}
|
||||
</span>
|
||||
<RiExternalLinkLine className="size-3 text-text-accent" />
|
||||
</Link>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{pluginDetail && (
|
||||
<PluginVersionPicker
|
||||
isShow={isShow}
|
||||
onShowChange={setIsShow}
|
||||
pluginID={pluginId}
|
||||
currentVersion={pluginDetail.version}
|
||||
onSelect={(state) => {
|
||||
setTarget({
|
||||
pluginUniqueIden: state.unique_identifier,
|
||||
version: state.version,
|
||||
})
|
||||
showUpdateModal()
|
||||
}}
|
||||
trigger={(
|
||||
<Badge
|
||||
className={cn(
|
||||
'mx-1 flex hover:bg-state-base-hover',
|
||||
isShow && 'bg-state-base-hover',
|
||||
)}
|
||||
uppercase={true}
|
||||
text={(
|
||||
<>
|
||||
<div>{pluginDetail.version}</div>
|
||||
<RiArrowLeftRightLine className="ml-1 h-3 w-3 text-text-tertiary" />
|
||||
</>
|
||||
)}
|
||||
hasRedCornerMark={true}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
openOnHover
|
||||
nativeButton={false}
|
||||
aria-label={typeof tooltip === 'string' ? tooltip : t('nodes.agent.installPlugin.title', { ns: 'workflow' })}
|
||||
render={content}
|
||||
/>
|
||||
<PopoverContent popupClassName="px-3 py-2 system-xs-regular text-text-tertiary">
|
||||
{tooltip}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { RiHome5Fill } from '@remixicon/react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { NodeSourceHandle } from '@/app/components/workflow/nodes/_base/components/node-handle'
|
||||
|
||||
const IterationStartNode = ({ id, data }: NodeProps) => {
|
||||
@ -10,10 +10,14 @@ const IterationStartNode = ({ id, data }: NodeProps) => {
|
||||
|
||||
return (
|
||||
<div className="nodrag group mt-1 flex h-11 w-11 items-center justify-center rounded-2xl border border-workflow-block-border bg-workflow-block-bg shadow-xs">
|
||||
<Tooltip popupContent={t('blocks.iteration-start', { ns: 'workflow' })} asChild={false}>
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500">
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
aria-label={t('blocks.iteration-start', { ns: 'workflow' })}
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500 p-0"
|
||||
>
|
||||
<RiHome5Fill className="h-3 w-3 text-text-primary-on-surface" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('blocks.iteration-start', { ns: 'workflow' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
<NodeSourceHandle
|
||||
id={id}
|
||||
@ -30,10 +34,14 @@ export const IterationStartNodeDumb = () => {
|
||||
|
||||
return (
|
||||
<div className="nodrag relative top-[21px] left-[17px] z-11 flex h-11 w-11 items-center justify-center rounded-2xl border border-workflow-block-border bg-workflow-block-bg">
|
||||
<Tooltip popupContent={t('blocks.iteration-start', { ns: 'workflow' })} asChild={false}>
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500">
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
aria-label={t('blocks.iteration-start', { ns: 'workflow' })}
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500 p-0"
|
||||
>
|
||||
<RiHome5Fill className="h-3 w-3 text-text-primary-on-surface" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('blocks.iteration-start', { ns: 'workflow' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -17,7 +17,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import WeightedScoreComponent from '@/app/components/app/configuration/dataset-config/params-config/weighted-score'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import { DEFAULT_WEIGHTED_SCORE } from '@/models/datasets'
|
||||
import {
|
||||
HybridSearchModeEnum,
|
||||
@ -174,10 +174,13 @@ const SearchMethodOption = ({
|
||||
disabled={readonly}
|
||||
/>
|
||||
{t('modelProvider.rerankModel.key', { ns: 'common' })}
|
||||
<Tooltip
|
||||
triggerClassName="ml-0.5 shrink-0 w-3.5 h-3.5"
|
||||
popupContent={t('modelProvider.rerankModel.tip', { ns: 'common' })}
|
||||
/>
|
||||
<Infotip
|
||||
aria-label={t('modelProvider.rerankModel.tip', { ns: 'common' })}
|
||||
className="ml-0.5 h-3.5 w-3.5 shrink-0"
|
||||
iconClassName="h-3.5 w-3.5"
|
||||
>
|
||||
{t('modelProvider.rerankModel.tip', { ns: 'common' })}
|
||||
</Infotip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
import Collapse from '@/app/components/workflow/nodes/_base/components/collapse'
|
||||
import { MetadataFilteringModeEnum } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
@ -46,13 +46,9 @@ const MetadataFilter = ({
|
||||
<div className="mr-0.5 system-sm-semibold-uppercase text-text-secondary">
|
||||
{t('nodes.knowledgeRetrieval.metadata.title', { ns: 'workflow' })}
|
||||
</div>
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[200px]">
|
||||
{t('nodes.knowledgeRetrieval.metadata.tip', { ns: 'workflow' })}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Infotip aria-label={t('nodes.knowledgeRetrieval.metadata.tip', { ns: 'workflow' })} popupClassName="w-[200px]">
|
||||
{t('nodes.knowledgeRetrieval.metadata.tip', { ns: 'workflow' })}
|
||||
</Infotip>
|
||||
{collapseIcon}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { RiHome5Fill } from '@remixicon/react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { NodeSourceHandle } from '@/app/components/workflow/nodes/_base/components/node-handle'
|
||||
|
||||
const LoopStartNode = ({ id, data }: NodeProps) => {
|
||||
@ -10,10 +10,14 @@ const LoopStartNode = ({ id, data }: NodeProps) => {
|
||||
|
||||
return (
|
||||
<div className="nodrag group mt-1 flex h-11 w-11 items-center justify-center rounded-2xl border border-workflow-block-border bg-workflow-block-bg">
|
||||
<Tooltip popupContent={t('blocks.loop-start', { ns: 'workflow' })} asChild={false}>
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500">
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
aria-label={t('blocks.loop-start', { ns: 'workflow' })}
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500 p-0"
|
||||
>
|
||||
<RiHome5Fill className="h-3 w-3 text-text-primary-on-surface" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('blocks.loop-start', { ns: 'workflow' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
<NodeSourceHandle
|
||||
id={id}
|
||||
@ -30,10 +34,14 @@ export const LoopStartNodeDumb = () => {
|
||||
|
||||
return (
|
||||
<div className="nodrag relative top-[21px] left-[17px] z-11 flex h-11 w-11 items-center justify-center rounded-2xl border border-workflow-block-border bg-workflow-block-bg">
|
||||
<Tooltip popupContent={t('blocks.loop-start', { ns: 'workflow' })} asChild={false}>
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500">
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
aria-label={t('blocks.loop-start', { ns: 'workflow' })}
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500 p-0"
|
||||
>
|
||||
<RiHome5Fill className="h-3 w-3 text-text-primary-on-surface" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('blocks.loop-start', { ns: 'workflow' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -3,7 +3,7 @@ import type { ParameterExtractorNodeType } from './types'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
@ -131,14 +131,14 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
|
||||
title={(
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="uppercase">{t(`${i18nPrefix}.instruction`, { ns: 'workflow' })}</span>
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[120px]">
|
||||
{t(`${i18nPrefix}.instructionTip`, { ns: 'workflow' })}
|
||||
</div>
|
||||
)}
|
||||
triggerClassName="w-3.5 h-3.5 ml-0.5"
|
||||
/>
|
||||
<Infotip
|
||||
aria-label={t(`${i18nPrefix}.instructionTip`, { ns: 'workflow' })}
|
||||
className="ml-0.5 h-3.5 w-3.5"
|
||||
iconClassName="h-3.5 w-3.5"
|
||||
popupClassName="w-[120px]"
|
||||
>
|
||||
{t(`${i18nPrefix}.instructionTip`, { ns: 'workflow' })}
|
||||
</Infotip>
|
||||
</div>
|
||||
)}
|
||||
value={inputs.instruction}
|
||||
|
||||
@ -344,8 +344,8 @@ describe('question-classifier path', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByText(`${longName.slice(0, 50)}...`)).toBeInTheDocument()
|
||||
await user.hover(screen.getByText(`${longName.slice(0, 50)}...`))
|
||||
expect(screen.getByText(longName)).toBeInTheDocument()
|
||||
await user.hover(screen.getByRole('button', { name: longName }))
|
||||
expect(await screen.findByText(longName)).toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<Node
|
||||
|
||||
@ -1,25 +1,10 @@
|
||||
import type { QuestionClassifierNodeType, Topic } from '../types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import Node from '../node'
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
children,
|
||||
popupContent,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
popupContent: React.ReactNode
|
||||
}) => (
|
||||
<div>
|
||||
{children}
|
||||
{popupContent}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useTextGenerationCurrentProviderAndModelAndModelList: vi.fn(),
|
||||
}))
|
||||
@ -101,7 +86,8 @@ describe('question-classifier/node', () => {
|
||||
expect(screen.getByText('handle-topic-2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('returns nothing when neither model nor classes are configured and truncates long class names', () => {
|
||||
it('returns nothing when neither model nor classes are configured and truncates long class names', async () => {
|
||||
const user = userEvent.setup()
|
||||
const longName = 'L'.repeat(60)
|
||||
const { container, rerender } = render(
|
||||
<Node
|
||||
@ -119,7 +105,8 @@ describe('question-classifier/node', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByText(`${longName.slice(0, 50)}...`)).toBeInTheDocument()
|
||||
expect(screen.getByText(longName)).toBeInTheDocument()
|
||||
await user.hover(screen.getByRole('button', { name: longName }))
|
||||
expect(await screen.findByText(longName)).toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<Node
|
||||
|
||||
@ -3,7 +3,7 @@ import type { FC } from 'react'
|
||||
import type { Memory, Node, NodeOutPutVar } from '@/app/components/workflow/types'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
|
||||
import MemoryConfig from '../../_base/components/memory-config'
|
||||
|
||||
@ -48,14 +48,14 @@ const AdvancedSetting: FC<Props> = ({
|
||||
title={(
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="uppercase">{t(`${i18nPrefix}.instruction`, { ns: 'workflow' })}</span>
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[120px]">
|
||||
{t(`${i18nPrefix}.instructionTip`, { ns: 'workflow' })}
|
||||
</div>
|
||||
)}
|
||||
triggerClassName="w-3.5 h-3.5 ml-0.5"
|
||||
/>
|
||||
<Infotip
|
||||
aria-label={t(`${i18nPrefix}.instructionTip`, { ns: 'workflow' })}
|
||||
className="ml-0.5 h-3.5 w-3.5"
|
||||
iconClassName="h-3.5 w-3.5"
|
||||
popupClassName="w-[120px]"
|
||||
>
|
||||
{t(`${i18nPrefix}.instructionTip`, { ns: 'workflow' })}
|
||||
</Infotip>
|
||||
</div>
|
||||
)}
|
||||
value={instruction}
|
||||
|
||||
@ -2,9 +2,9 @@ import type { TFunction } from 'i18next'
|
||||
import type { FC } from 'react'
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import type { QuestionClassifierNodeType } from './types'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import {
|
||||
useTextGenerationCurrentProviderAndModelAndModelList,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
@ -47,15 +47,18 @@ const TruncatedClassItem: FC<TruncatedClassItemProps> = ({ topic, index, nodeId,
|
||||
</div>
|
||||
{shouldShowTooltip
|
||||
? (
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="max-w-[300px] wrap-break-word">
|
||||
<ReadonlyInputWithSelectVar value={topic.name} nodeId={nodeId} />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</Tooltip>
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
openOnHover
|
||||
aria-label={topic.name}
|
||||
className="w-full border-0 bg-transparent p-0 text-left"
|
||||
>
|
||||
{content}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent popupClassName="max-w-[300px] px-3 py-2 system-xs-regular wrap-break-word text-text-tertiary">
|
||||
<ReadonlyInputWithSelectVar value={topic.name} nodeId={nodeId} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
: content}
|
||||
</div>
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
RiBracesLine,
|
||||
} from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { SchemaModal } from '@/app/components/plugins/plugin-detail-panel/tool-selector/components'
|
||||
@ -57,15 +57,13 @@ const TriggerFormItem: FC<Props> = ({
|
||||
<div className="ml-1 system-xs-regular text-text-destructive-secondary">*</div>
|
||||
)}
|
||||
{!showDescription && tooltip && (
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[200px]">
|
||||
{tooltip[language] || tooltip.en_US}
|
||||
</div>
|
||||
)}
|
||||
triggerClassName="ml-1 w-4 h-4"
|
||||
asChild={false}
|
||||
/>
|
||||
<Infotip
|
||||
aria-label={tooltip[language] || tooltip.en_US}
|
||||
className="ml-1 h-4 w-4"
|
||||
popupClassName="w-[200px]"
|
||||
>
|
||||
{tooltip[language] || tooltip.en_US}
|
||||
</Infotip>
|
||||
)}
|
||||
{showSchemaButton && (
|
||||
<>
|
||||
|
||||
@ -94,10 +94,6 @@ vi.mock('@/app/components/base/input-with-copy', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/field', () => ({
|
||||
default: ({ title, children }: { title: string, children: React.ReactNode }) => (
|
||||
<div>
|
||||
|
||||
@ -11,12 +11,12 @@ import {
|
||||
} from '@langgenius/dify-ui/number-field'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import InputWithCopy from '@/app/components/base/input-with-copy'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import OutputVars from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
@ -118,32 +118,38 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
||||
</div>
|
||||
{inputs.webhook_debug_url && (
|
||||
<div className="space-y-2">
|
||||
<Tooltip
|
||||
popupContent={debugUrlCopied ? t(`${i18nPrefix}.debugUrlCopied`, { ns: 'workflow' }) : t(`${i18nPrefix}.debugUrlCopy`, { ns: 'workflow' })}
|
||||
popupClassName="system-xs-regular text-text-primary bg-components-tooltip-bg border border-components-panel-border shadow-lg backdrop-blur-xs rounded-md px-1.5 py-1"
|
||||
position="top"
|
||||
offset={{ mainAxis: -20 }}
|
||||
needsDelay={true}
|
||||
>
|
||||
<div
|
||||
className="flex cursor-pointer gap-1.5 rounded-lg px-1 py-1.5 transition-colors"
|
||||
style={{ width: '368px', height: '38px' }}
|
||||
onClick={() => {
|
||||
copy(inputs.webhook_debug_url || '')
|
||||
setDebugUrlCopied(true)
|
||||
setTimeout(() => setDebugUrlCopied(false), 2000)
|
||||
}}
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t(`${i18nPrefix}.debugUrlCopy`, { ns: 'workflow' })}
|
||||
className="flex cursor-pointer gap-1.5 rounded-lg px-1 py-1.5 text-left transition-colors"
|
||||
style={{ width: '368px', height: '38px' }}
|
||||
onClick={() => {
|
||||
copy(inputs.webhook_debug_url || '')
|
||||
setDebugUrlCopied(true)
|
||||
setTimeout(() => setDebugUrlCopied(false), 2000)
|
||||
}}
|
||||
>
|
||||
<span className="mt-0.5 w-0.5 bg-divider-regular" style={{ height: '28px' }} />
|
||||
<span className="flex-1" style={{ width: '352px', height: '32px' }}>
|
||||
<span className="block text-xs leading-4 text-text-tertiary">
|
||||
{t(`${i18nPrefix}.debugUrlTitle`, { ns: 'workflow' })}
|
||||
</span>
|
||||
<span className="block truncate text-xs leading-4 text-text-primary">
|
||||
{inputs.webhook_debug_url}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent
|
||||
placement="top"
|
||||
className="rounded-md border border-components-panel-border bg-components-tooltip-bg px-1.5 py-1 system-xs-regular text-text-primary shadow-lg backdrop-blur-xs"
|
||||
>
|
||||
<div className="mt-0.5 w-0.5 bg-divider-regular" style={{ height: '28px' }}></div>
|
||||
<div className="flex-1" style={{ width: '352px', height: '32px' }}>
|
||||
<div className="text-xs leading-4 text-text-tertiary">
|
||||
{t(`${i18nPrefix}.debugUrlTitle`, { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className="truncate text-xs leading-4 text-text-primary">
|
||||
{inputs.webhook_debug_url}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{debugUrlCopied ? t(`${i18nPrefix}.debugUrlCopied`, { ns: 'workflow' }) : t(`${i18nPrefix}.debugUrlCopy`, { ns: 'workflow' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{isPrivateOrLocalAddress(inputs.webhook_debug_url) && (
|
||||
<div className="mt-1 px-0 py-[2px] system-xs-regular text-text-warning">
|
||||
|
||||
@ -7,8 +7,8 @@ import * as React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var'
|
||||
|
||||
@ -129,14 +129,14 @@ const VariableModal = ({
|
||||
onClick={() => setType('secret')}
|
||||
>
|
||||
<span>Secret</span>
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[240px]">
|
||||
{t('env.modal.secretTip', { ns: 'workflow' })}
|
||||
</div>
|
||||
)}
|
||||
triggerClassName="ml-0.5 w-3.5 h-3.5"
|
||||
/>
|
||||
<Infotip
|
||||
aria-label={t('env.modal.secretTip', { ns: 'workflow' })}
|
||||
className="ml-0.5 h-3.5 w-3.5"
|
||||
iconClassName="h-3.5 w-3.5"
|
||||
popupClassName="w-[240px]"
|
||||
>
|
||||
{t('env.modal.secretTip', { ns: 'workflow' })}
|
||||
</Infotip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -17,7 +17,7 @@ import {
|
||||
RiLoader2Line,
|
||||
RiPauseCircleFill,
|
||||
} from '@remixicon/react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
|
||||
@ -68,16 +68,6 @@ const NodePanel: FC<Props> = ({
|
||||
return
|
||||
doSetCollapseState(state)
|
||||
}, [hideProcessDetail])
|
||||
const titleRef = useRef<HTMLDivElement>(null)
|
||||
const [isTooltipOpen, setIsTooltipOpen] = useState(false)
|
||||
const handleTooltipOpenChange = useCallback((open: boolean) => {
|
||||
if (open) {
|
||||
const el = titleRef.current
|
||||
if (!el || el.scrollWidth <= el.clientWidth)
|
||||
return
|
||||
}
|
||||
setIsTooltipOpen(open)
|
||||
}, [])
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
|
||||
@ -142,11 +132,10 @@ const NodePanel: FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
<BlockIcon size={inMessage ? 'xs' : 'sm'} className={cn('mr-2 shrink-0', inMessage && 'mr-1!')} type={nodeInfo.node_type} toolIcon={nodeInfo.extras?.icon || nodeInfo.extras} />
|
||||
<Tooltip open={isTooltipOpen} onOpenChange={handleTooltipOpenChange}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<div
|
||||
ref={titleRef}
|
||||
className={cn(
|
||||
'min-w-0 grow truncate system-xs-semibold-uppercase text-text-secondary',
|
||||
hideInfo && 'text-xs!',
|
||||
|
||||
@ -4,12 +4,12 @@ import type { Node } from 'reactflow'
|
||||
import type { ScheduleTriggerNodeType } from '@/app/components/workflow/nodes/trigger-schedule/types'
|
||||
import type { WebhookTriggerNodeType } from '@/app/components/workflow/nodes/trigger-webhook/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { useGetToolIcon } from '@/app/components/workflow/hooks/use-tool-icon'
|
||||
import { getNextExecutionTime } from '@/app/components/workflow/nodes/trigger-schedule/utils/execution-time-calculator'
|
||||
@ -179,28 +179,32 @@ const Listening: FC<ListeningProps> = ({
|
||||
<div className="shrink-0 system-xs-regular whitespace-pre-line text-text-tertiary">
|
||||
{t('nodes.triggerWebhook.debugUrlTitle', { ns: 'workflow' })}
|
||||
</div>
|
||||
<Tooltip
|
||||
popupContent={debugUrlCopied
|
||||
? t('nodes.triggerWebhook.debugUrlCopied', { ns: 'workflow' })
|
||||
: t('nodes.triggerWebhook.debugUrlCopy', { ns: 'workflow' })}
|
||||
popupClassName="system-xs-regular text-text-primary bg-components-tooltip-bg border border-components-panel-border shadow-lg backdrop-blur-xs rounded-md px-1.5 py-1"
|
||||
position="top"
|
||||
offset={{ mainAxis: -4 }}
|
||||
needsDelay={true}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('nodes.triggerWebhook.debugUrlCopy', { ns: 'workflow' }) || ''}
|
||||
className={`inline-flex items-center rounded-md border border-divider-regular bg-components-badge-white-to-dark px-1.5 py-[2px] font-mono text-[13px] leading-[18px] text-text-secondary transition-colors hover:bg-components-panel-on-panel-item-bg-hover focus:outline-hidden focus-visible:outline-2 focus-visible:outline-components-panel-border focus-visible:outline-solid ${debugUrlCopied ? 'bg-components-panel-on-panel-item-bg-hover text-text-primary' : ''}`}
|
||||
onClick={() => {
|
||||
copy(webhookDebugUrl)
|
||||
setDebugUrlCopied(true)
|
||||
}}
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('nodes.triggerWebhook.debugUrlCopy', { ns: 'workflow' }) || ''}
|
||||
className={`inline-flex items-center rounded-md border border-divider-regular bg-components-badge-white-to-dark px-1.5 py-[2px] font-mono text-[13px] leading-[18px] text-text-secondary transition-colors hover:bg-components-panel-on-panel-item-bg-hover focus:outline-hidden focus-visible:outline-2 focus-visible:outline-components-panel-border focus-visible:outline-solid ${debugUrlCopied ? 'bg-components-panel-on-panel-item-bg-hover text-text-primary' : ''}`}
|
||||
onClick={() => {
|
||||
copy(webhookDebugUrl)
|
||||
setDebugUrlCopied(true)
|
||||
}}
|
||||
>
|
||||
<span className="whitespace-nowrap text-text-primary">
|
||||
{webhookDebugUrl}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent
|
||||
placement="top"
|
||||
className="rounded-md border border-components-panel-border bg-components-tooltip-bg px-1.5 py-1 system-xs-regular text-text-primary shadow-lg backdrop-blur-xs"
|
||||
>
|
||||
<span className="whitespace-nowrap text-text-primary">
|
||||
{webhookDebugUrl}
|
||||
</span>
|
||||
</button>
|
||||
{debugUrlCopied
|
||||
? t('nodes.triggerWebhook.debugUrlCopied', { ns: 'workflow' })
|
||||
: t('nodes.triggerWebhook.debugUrlCopy', { ns: 'workflow' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -6,12 +6,12 @@ import type {
|
||||
NodeProps,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import {
|
||||
cloneElement,
|
||||
memo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import {
|
||||
BlockEnum,
|
||||
@ -91,19 +91,21 @@ const BaseCard = ({
|
||||
</div>
|
||||
{
|
||||
data.type === BlockEnum.Iteration && (data as IterationNodeType).is_parallel && (
|
||||
<Tooltip popupContent={(
|
||||
<div className="w-[180px]">
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
openOnHover
|
||||
aria-label={t('nodes.iteration.parallelModeEnableTitle', { ns: 'workflow' })}
|
||||
className="ml-1 flex items-center justify-center rounded-[5px] border border-text-warning bg-transparent px-[5px] py-[3px] system-2xs-medium-uppercase text-text-warning"
|
||||
>
|
||||
{t('nodes.iteration.parallelModeUpper', { ns: 'workflow' })}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent popupClassName="w-[180px] px-3 py-2 system-xs-regular text-text-tertiary">
|
||||
<div className="font-extrabold">
|
||||
{t('nodes.iteration.parallelModeEnableTitle', { ns: 'workflow' })}
|
||||
</div>
|
||||
{t('nodes.iteration.parallelModeEnableDesc', { ns: 'workflow' })}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="ml-1 flex items-center justify-center rounded-[5px] border border-text-warning px-[5px] py-[3px] system-2xs-medium-uppercase text-text-warning">
|
||||
{t('nodes.iteration.parallelModeUpper', { ns: 'workflow' })}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { RiHome5Fill } from '@remixicon/react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { NodeSourceHandle } from '../../node-handle'
|
||||
|
||||
const IterationStartNode = ({ id, data }: NodeProps) => {
|
||||
@ -10,10 +10,14 @@ const IterationStartNode = ({ id, data }: NodeProps) => {
|
||||
|
||||
return (
|
||||
<div className="nodrag group mt-1 flex h-11 w-11 items-center justify-center rounded-2xl border border-workflow-block-border bg-workflow-block-bg shadow-xs">
|
||||
<Tooltip popupContent={t('blocks.iteration-start', { ns: 'workflow' })} asChild={false}>
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500">
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
aria-label={t('blocks.iteration-start', { ns: 'workflow' })}
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500 p-0"
|
||||
>
|
||||
<RiHome5Fill className="h-3 w-3 text-text-primary-on-surface" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('blocks.iteration-start', { ns: 'workflow' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
<NodeSourceHandle
|
||||
id={id}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { RiHome5Fill } from '@remixicon/react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { NodeSourceHandle } from '../../node-handle'
|
||||
|
||||
const LoopStartNode = ({ id, data }: NodeProps) => {
|
||||
@ -10,10 +10,14 @@ const LoopStartNode = ({ id, data }: NodeProps) => {
|
||||
|
||||
return (
|
||||
<div className="nodrag group mt-1 flex h-11 w-11 items-center justify-center rounded-2xl border border-workflow-block-border bg-workflow-block-bg">
|
||||
<Tooltip popupContent={t('blocks.loop-start', { ns: 'workflow' })} asChild={false}>
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500">
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
aria-label={t('blocks.loop-start', { ns: 'workflow' })}
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500 p-0"
|
||||
>
|
||||
<RiHome5Fill className="h-3 w-3 text-text-primary-on-surface" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('blocks.loop-start', { ns: 'workflow' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
<NodeSourceHandle
|
||||
id={id}
|
||||
|
||||
@ -7,7 +7,6 @@ This document tracks the Dify-web migration away from legacy overlay APIs.
|
||||
## Scope
|
||||
|
||||
- Deprecated imports:
|
||||
- `@/app/components/base/tooltip`
|
||||
- `@/app/components/base/modal`
|
||||
- `@/app/components/base/dialog`
|
||||
- `@/app/components/base/drawer`
|
||||
@ -36,6 +35,8 @@ This document tracks the Dify-web migration away from legacy overlay APIs.
|
||||
1. Business/UI features outside `app/components/base/**`
|
||||
- Migrate old calls to semantic primitives from `@langgenius/dify-ui/*`.
|
||||
- Keep deprecated imports out of newly touched files.
|
||||
- Use `@langgenius/dify-ui/tooltip` only for short, non-interactive labels where the trigger already has its own accessible name.
|
||||
- Use `@langgenius/dify-ui/popover` or the web `Infotip` wrapper for explanatory, long-form, structured, or interactive content.
|
||||
1. Legacy base components
|
||||
- Migrate legacy base callers gradually.
|
||||
- Keep deprecated imports out of newly touched files.
|
||||
@ -75,6 +76,9 @@ back to `z-9999`.
|
||||
parent legacy overlay should be migrated instead.
|
||||
- When migrating a legacy overlay that has a high z-index, remove the z-index
|
||||
entirely — the new primitive's default `z-1002` handles it.
|
||||
- When using Base UI trigger `render`, render a real `button` for button-like
|
||||
triggers. If the trigger must render a non-button element, the primitive must
|
||||
explicitly opt out of the native button behavior where that API is available.
|
||||
|
||||
### Post-migration cleanup
|
||||
|
||||
|
||||
@ -45,13 +45,6 @@ export const WEB_RESTRICTED_IMPORT_PATTERNS = [
|
||||
]
|
||||
|
||||
export const OVERLAY_RESTRICTED_IMPORT_PATTERNS = [
|
||||
{
|
||||
group: [
|
||||
'**/base/tooltip',
|
||||
'**/base/tooltip/index',
|
||||
],
|
||||
message: 'Deprecated: use @langgenius/dify-ui/tooltip instead. See issue #32767.',
|
||||
},
|
||||
{
|
||||
group: [
|
||||
'**/base/modal',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user