diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 9f9c7dc889..f87754ce6a 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2987,22 +2987,6 @@ "count": 1 } }, - "web/app/components/workflow/block-selector/featured-tools.tsx": { - "react/set-state-in-effect": { - "count": 2 - }, - "ts/no-explicit-any": { - "count": 2 - } - }, - "web/app/components/workflow/block-selector/featured-triggers.tsx": { - "react/set-state-in-effect": { - "count": 2 - }, - "ts/no-explicit-any": { - "count": 2 - } - }, "web/app/components/workflow/block-selector/hooks.ts": { "react/set-state-in-effect": { "count": 1 @@ -3038,29 +3022,6 @@ "count": 1 } }, - "web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx": { - "ts/no-explicit-any": { - "count": 1 - } - }, - "web/app/components/workflow/block-selector/tool/tool.tsx": { - "react/set-state-in-effect": { - "count": 2 - } - }, - "web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx": { - "ts/no-explicit-any": { - "count": 1 - } - }, - "web/app/components/workflow/block-selector/trigger-plugin/item.tsx": { - "react/set-state-in-effect": { - "count": 2 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, "web/app/components/workflow/block-selector/types.ts": { "erasable-syntax-only/enums": { "count": 4 diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/index.spec.tsx index 4119bca6ae..b7af6a14a3 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/index.spec.tsx @@ -118,6 +118,23 @@ describe('ModelSelector', () => { expect(triggerButton).toHaveAttribute('aria-expanded', 'false') }) + it('should use the default model settings popup width when the trigger is narrow', () => { + renderWithQueryClient( +
+ +
, + ) + + fireEvent.click(screen.getByRole('combobox')) + + expect( + Array.from(document.body.querySelectorAll('[class]')).some(element => + element.className.includes('w-[432px]') + && element.className.includes('max-w-[432px]'), + ), + ).toBe(true) + }) + it('should not open popup when readonly', () => { renderWithQueryClient() diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx index e198853ddd..a3db35302c 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx @@ -1,6 +1,7 @@ import type { ReactElement, ReactNode } from 'react' import type { DefaultModel, Model, ModelItem } from '../../declarations' import { Combobox } from '@langgenius/dify-ui/combobox' +import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card' import { fireEvent, render, screen } from '@testing-library/react' import { ConfigurationMethodEnum, @@ -106,6 +107,11 @@ const makeProvider = (overrides: Record = {}) => ({ ...overrides, }) +const previewCardProps = () => ({ + previewCardHandle: createPreviewCardHandle(), + onPreviewCardClose: vi.fn(), +}) + const createComboboxNode = ( node: ReactElement, onValueChange = vi.fn(), @@ -152,7 +158,7 @@ describe('PopupItem', () => { }) const { container } = renderWithCombobox( - , + , ) expect(container.textContent).toBe('') @@ -160,7 +166,7 @@ describe('PopupItem', () => { it('should select the combobox value when clicking an active model', () => { const onValueChange = vi.fn() - renderWithCombobox(, onValueChange) + renderWithCombobox(, onValueChange) fireEvent.click(screen.getByText('GPT-4')) @@ -170,10 +176,27 @@ describe('PopupItem', () => { ) }) + it('should close the shared preview before pressing an active model', () => { + const onPreviewCardClose = vi.fn() + renderWithCombobox( + , + ) + + fireEvent.pointerDown(screen.getByText('GPT-4')) + + expect(onPreviewCardClose).toHaveBeenCalledTimes(1) + }) + it('should not select the combobox value when model is not active', () => { const onValueChange = vi.fn() renderWithCombobox( , @@ -188,7 +211,7 @@ describe('PopupItem', () => { it('should open model modal when clicking add on unconfigured model', () => { const onValueChange = vi.fn() const { rerender } = renderWithCombobox( - , + , onValueChange, ) @@ -206,6 +229,7 @@ describe('PopupItem', () => { rerender(createComboboxNode( { const defaultModel: DefaultModel = { provider: 'openai', model: 'gpt-4' } renderWithCombobox( { renderWithCombobox( { }) it('should toggle collapsed state when clicking provider header', () => { - renderWithCombobox() + renderWithCombobox() expect(screen.getByText('GPT-4'))!.toBeInTheDocument() @@ -266,7 +292,7 @@ describe('PopupItem', () => { }) it('should show credential name when using custom provider', () => { - renderWithCombobox() + renderWithCombobox() expect(screen.getByText('my-api-key'))!.toBeInTheDocument() }) @@ -283,7 +309,7 @@ describe('PopupItem', () => { credits: 200, }) - renderWithCombobox() + renderWithCombobox() expect(screen.getByText('stale-key'))!.toBeInTheDocument() expect(document.querySelector('.bg-components-badge-status-light-error-bg')).not.toBeNull() @@ -309,7 +335,7 @@ describe('PopupItem', () => { credits: 0, }) - renderWithCombobox() + renderWithCombobox() expect(screen.getByText(/modelProvider\.selector\.configureRequired/))!.toBeInTheDocument() }) @@ -331,7 +357,7 @@ describe('PopupItem', () => { credits: 200, }) - renderWithCombobox() + renderWithCombobox() expect(screen.getByText(/modelProvider\.selector\.aiCredits/))!.toBeInTheDocument() }) @@ -356,7 +382,7 @@ describe('PopupItem', () => { credits: 0, }) - renderWithCombobox() + renderWithCombobox() expect(screen.getByText(/modelProvider\.selector\.creditsExhausted/))!.toBeInTheDocument() }) @@ -364,7 +390,7 @@ describe('PopupItem', () => { it('should close the dropdown through dropdown content callbacks', () => { const onHide = vi.fn() - renderWithCombobox() + renderWithCombobox() fireEvent.click(screen.getByRole('button', { name: /my-api-key/ })) fireEvent.click(screen.getByRole('button', { name: 'close dropdown' })) diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx index debd06d7cd..79ea3dc9c3 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx @@ -130,7 +130,7 @@ function ModelSelector({ ['handle']> type PopupItemProps = { defaultModel?: DefaultModel model: Model + previewCardHandle: PreviewCardHandle + onPreviewCardClose: () => void onHide: () => void } function PopupItem({ defaultModel, model, + previewCardHandle, + onPreviewCardClose, onHide, }: PopupItemProps) { const [collapsed, setCollapsed] = useState(false) @@ -167,7 +176,11 @@ function PopupItem({ ) const itemRender = modelItem.status === ModelStatusEnum.noConfigure ? ( -
+
{rowContent}
+ + {({ payload }) => ( + + )} + ) } +type ModelSelectorPreviewCardProps = { + capabilitiesLabel: string + language: string + payload?: ModelSelectorPreviewPayload +} + +function ModelSelectorPreviewCard({ + capabilitiesLabel, + language, + payload, +}: ModelSelectorPreviewCardProps) { + if (!payload) + return null + + const { provider, modelItem } = payload + + return ( + +
+
+ +
{modelItem.label[language] || modelItem.label.en_US}
+
+
+ {!!modelItem.model_type && ( + + {modelTypeFormat(modelItem.model_type)} + + )} + {!!modelItem.model_properties.mode && ( + + {(modelItem.model_properties.mode as string).toLocaleUpperCase()} + + )} + {!!modelItem.model_properties.context_size && ( + + {sizeFormat(modelItem.model_properties.context_size as number)} + + )} +
+ {[ModelTypeEnum.textGeneration, ModelTypeEnum.textEmbedding, ModelTypeEnum.rerank].includes(modelItem.model_type as ModelTypeEnum) + && modelItem.features?.some(feature => [ModelFeatureEnum.vision, ModelFeatureEnum.audio, ModelFeatureEnum.video, ModelFeatureEnum.document].includes(feature)) + && ( +
+
{capabilitiesLabel}
+
+ {modelItem.features?.map(feature => ( + + ))} +
+
+ )} +
+
+ ) +} + export default Popup diff --git a/web/app/components/workflow/block-selector/blocks.tsx b/web/app/components/workflow/block-selector/blocks.tsx index 8258e4d450..40fcc073f6 100644 --- a/web/app/components/workflow/block-selector/blocks.tsx +++ b/web/app/components/workflow/block-selector/blocks.tsx @@ -1,6 +1,7 @@ import type { NodeDefault } from '../types' import type { BlockClassificationEnum } from './types' import { + createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger, @@ -25,6 +26,10 @@ type BlocksProps = { availableBlocksTypes?: BlockEnum[] blocks?: NodeDefault[] } +type BlockPreviewPayload = { + block: NodeDefault +} + const Blocks = ({ searchText, onSelect, @@ -34,6 +39,7 @@ const Blocks = ({ const { t } = useTranslation() const store = useStoreApi() const blocksFromHooks = useBlocks() + const previewCardHandle = useMemo(() => createPreviewCardHandle(), []) // Use external blocks if provided, otherwise fallback to hook-based blocks const blocks = blocksFromProps || blocksFromHooks.map(block => ({ @@ -101,51 +107,38 @@ const Blocks = ({ // hover/focus-only activation is a11y-safe. See // packages/dify-ui/AGENTS.md → Overlay Primitive Selection. filteredList.map(block => ( - - onSelect(block.metaData.type)} - > - -
{block.metaData.title}
- { - block.metaData.type === BlockEnum.LoopEnd && ( - - ) - } -
- )} - /> - -
+ onSelect(block.metaData.type)} + > -
{block.metaData.title}
-
{block.metaData.description}
+
{block.metaData.title}
+ { + block.metaData.type === BlockEnum.LoopEnd && ( + + ) + }
-
- + )} + /> )) } ) - }, [groups, onSelect, t, store]) + }, [groups, onSelect, previewCardHandle, t, store]) return (
@@ -157,8 +150,43 @@ const Blocks = ({ { !isEmpty && BLOCK_CLASSIFICATIONS.map(renderGroup) } + + {({ payload }) => ( + + )} +
) } +type BlockPreviewCardProps = { + payload?: BlockPreviewPayload +} + +function BlockPreviewCard({ + payload, +}: BlockPreviewCardProps) { + if (!payload) + return null + + const { block } = payload + + return ( + +
+ +
{block.metaData.title}
+
{block.metaData.description}
+
+
+ ) +} + export default memo(Blocks) diff --git a/web/app/components/workflow/block-selector/featured-tools.tsx b/web/app/components/workflow/block-selector/featured-tools.tsx index 0cdeebcb79..4b1dc8b138 100644 --- a/web/app/components/workflow/block-selector/featured-tools.tsx +++ b/web/app/components/workflow/block-selector/featured-tools.tsx @@ -1,9 +1,10 @@ 'use client' +import type { TFunction } from 'i18next' import type { ToolWithProvider } from '../types' import type { ToolDefaultValue, ToolValue } from './types' import type { Plugin } from '@/app/components/plugins/types' import type { Locale } from '@/i18n-config' -import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' +import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' import { RiMoreLine } from '@remixicon/react' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -33,6 +34,11 @@ type FeaturedToolsProps = { isLoading?: boolean onInstallSuccess?: () => void } +type FeaturedToolPreviewPayload = { + plugin: Plugin + label: string + description: string +} const STORAGE_KEY = 'workflow_tools_featured_collapsed' @@ -46,7 +52,9 @@ const FeaturedTools = ({ }: FeaturedToolsProps) => { const { t } = useTranslation() const language = useGetLanguage() + const previewCardHandle = useMemo(() => createPreviewCardHandle(), []) const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) + const [visibleCountPlugins, setVisibleCountPlugins] = useState(plugins) const [isCollapsed, setIsCollapsed] = useState(() => { if (isServer) return false @@ -54,23 +62,16 @@ const FeaturedTools = ({ return stored === 'true' }) - useEffect(() => { - if (isServer) - return - const stored = window.localStorage.getItem(STORAGE_KEY) - if (stored !== null) - setIsCollapsed(stored === 'true') - }, []) - useEffect(() => { if (isServer) return window.localStorage.setItem(STORAGE_KEY, String(isCollapsed)) }, [isCollapsed]) - useEffect(() => { + if (visibleCountPlugins !== plugins) { + setVisibleCountPlugins(plugins) setVisibleCount(INITIAL_VISIBLE_COUNT) - }, [plugins]) + } const limitedPlugins = useMemo( () => plugins.slice(0, MAX_RECOMMENDED_COUNT), @@ -174,10 +175,11 @@ const FeaturedTools = ({ key={plugin.plugin_id} plugin={plugin} language={language} + previewCardHandle={previewCardHandle} onInstallSuccess={async () => { await onInstallSuccess?.() }} - t={t as any} + t={t} /> ))} @@ -214,6 +216,11 @@ const FeaturedTools = ({ )} )} + + {({ payload }) => ( + + )} + ) } @@ -221,13 +228,15 @@ const FeaturedTools = ({ type FeaturedToolUninstalledItemProps = { plugin: Plugin language: Locale + previewCardHandle: ReturnType> onInstallSuccess?: () => Promise | void - t: (key: string, options?: Record) => string + t: TFunction } function FeaturedToolUninstalledItem({ plugin, language, + previewCardHandle, onInstallSuccess, t, }: FeaturedToolUninstalledItemProps) { @@ -296,16 +305,13 @@ function FeaturedToolUninstalledItem({ // Preview is supplementary: icon / label / brief are all reachable from // the InstallFromMarketplace modal that opens on click, so hover/focus-only // activation is a11y-safe. See packages/dify-ui/AGENTS.md → Overlay Primitive Selection. - - - -
- -
{label}
-
{description}
-
-
-
+ ) : row} {isInstallModalOpen && ( @@ -325,4 +331,25 @@ function FeaturedToolUninstalledItem({ ) } +type FeaturedToolPreviewCardProps = { + payload?: FeaturedToolPreviewPayload +} + +function FeaturedToolPreviewCard({ + payload, +}: FeaturedToolPreviewCardProps) { + if (!payload) + return null + + return ( + +
+ +
{payload.label}
+
{payload.description}
+
+
+ ) +} + export default FeaturedTools diff --git a/web/app/components/workflow/block-selector/featured-triggers.tsx b/web/app/components/workflow/block-selector/featured-triggers.tsx index 3d3cdee2b7..842541d88d 100644 --- a/web/app/components/workflow/block-selector/featured-triggers.tsx +++ b/web/app/components/workflow/block-selector/featured-triggers.tsx @@ -1,8 +1,10 @@ 'use client' +import type { TFunction } from 'i18next' +import type { TriggerPluginActionPreviewPayload } from './trigger-plugin/action-item' import type { TriggerDefaultValue, TriggerWithProvider } from './types' import type { Plugin } from '@/app/components/plugins/types' import type { Locale } from '@/i18n-config' -import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' +import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' import { RiMoreLine } from '@remixicon/react' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -17,6 +19,7 @@ import { formatNumber } from '@/utils/format' import { getMarketplaceUrl } from '@/utils/var' import BlockIcon from '../block-icon' import { BlockEnum } from '../types' +import { TriggerPluginActionPreviewCard } from './trigger-plugin/action-item' import TriggerPluginItem from './trigger-plugin/item' const MAX_RECOMMENDED_COUNT = 15 @@ -29,6 +32,11 @@ type FeaturedTriggersProps = { isLoading?: boolean onInstallSuccess?: () => void | Promise } +type FeaturedTriggerPreviewPayload = { + plugin: Plugin + label: string + description: string +} const STORAGE_KEY = 'workflow_triggers_featured_collapsed' @@ -41,7 +49,10 @@ const FeaturedTriggers = ({ }: FeaturedTriggersProps) => { const { t } = useTranslation() const language = useGetLanguage() + const previewCardHandle = useMemo(() => createPreviewCardHandle(), []) + const triggerActionPreviewCardHandle = useMemo(() => createPreviewCardHandle(), []) const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) + const [visibleCountPlugins, setVisibleCountPlugins] = useState(plugins) const [isCollapsed, setIsCollapsed] = useState(() => { if (isServer) return false @@ -49,23 +60,16 @@ const FeaturedTriggers = ({ return stored === 'true' }) - useEffect(() => { - if (isServer) - return - const stored = window.localStorage.getItem(STORAGE_KEY) - if (stored !== null) - setIsCollapsed(stored === 'true') - }, []) - useEffect(() => { if (isServer) return window.localStorage.setItem(STORAGE_KEY, String(isCollapsed)) }, [isCollapsed]) - useEffect(() => { + if (visibleCountPlugins !== plugins) { + setVisibleCountPlugins(plugins) setVisibleCount(INITIAL_VISIBLE_COUNT) - }, [plugins]) + } const limitedPlugins = useMemo( () => plugins.slice(0, MAX_RECOMMENDED_COUNT), @@ -156,6 +160,7 @@ const FeaturedTriggers = ({ key={provider.id} payload={provider} hasSearchText={false} + previewCardHandle={triggerActionPreviewCardHandle} onSelect={onSelect} /> ))} @@ -169,10 +174,11 @@ const FeaturedTriggers = ({ key={plugin.plugin_id} plugin={plugin} language={language} + previewCardHandle={previewCardHandle} onInstallSuccess={async () => { await onInstallSuccess?.() }} - t={t as any} + t={t} /> ))} @@ -209,6 +215,16 @@ const FeaturedTriggers = ({ )} )} + + {({ payload }) => ( + + )} + + + {({ payload }) => ( + + )} + ) } @@ -216,13 +232,15 @@ const FeaturedTriggers = ({ type FeaturedTriggerUninstalledItemProps = { plugin: Plugin language: Locale + previewCardHandle: ReturnType> onInstallSuccess?: () => Promise | void - t: (key: string, options?: Record) => string + t: TFunction } function FeaturedTriggerUninstalledItem({ plugin, language, + previewCardHandle, onInstallSuccess, t, }: FeaturedTriggerUninstalledItemProps) { @@ -291,16 +309,13 @@ function FeaturedTriggerUninstalledItem({ // Preview is supplementary: icon / label / brief are all reachable from // the InstallFromMarketplace modal that opens on click, so hover/focus-only // activation is a11y-safe. See packages/dify-ui/AGENTS.md → Overlay Primitive Selection. - - - -
- -
{label}
-
{description}
-
-
-
+ ) : row} {isInstallModalOpen && ( @@ -320,4 +335,25 @@ function FeaturedTriggerUninstalledItem({ ) } +type FeaturedTriggerPreviewCardProps = { + payload?: FeaturedTriggerPreviewPayload +} + +function FeaturedTriggerPreviewCard({ + payload, +}: FeaturedTriggerPreviewCardProps) { + if (!payload) + return null + + return ( + +
+ +
{payload.label}
+
{payload.description}
+
+
+ ) +} + export default FeaturedTriggers diff --git a/web/app/components/workflow/block-selector/rag-tool-recommendations/list.tsx b/web/app/components/workflow/block-selector/rag-tool-recommendations/list.tsx index d5873ddc1f..3e536b91b5 100644 --- a/web/app/components/workflow/block-selector/rag-tool-recommendations/list.tsx +++ b/web/app/components/workflow/block-selector/rag-tool-recommendations/list.tsx @@ -1,11 +1,14 @@ import type { BlockEnum, ToolWithProvider } from '../../types' +import type { ToolActionPreviewPayload } from '../tool/action-item' import type { ToolDefaultValue } from '../types' import type { Plugin } from '@/app/components/plugins/types' import type { OnSelectBlock } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' +import { createPreviewCardHandle, PreviewCard } from '@langgenius/dify-ui/preview-card' import { useCallback, useMemo, useRef } from 'react' import { useGetLanguage } from '@/context/i18n' import { groupItems } from '../index-bar' +import { ToolActionPreviewCard } from '../tool/action-item' import ToolListFlatView from '../tool/tool-list-flat-view/list' import ToolListTreeView from '../tool/tool-list-tree-view/list' import { ViewType } from '../view-type-select' @@ -27,6 +30,7 @@ const List = ({ className, }: ListProps) => { const language = useGetLanguage() + const previewCardHandle = useMemo(() => createPreviewCardHandle(), []) const isFlatView = viewType === ViewType.flat const { letters, groups: withLetterAndGroupViewToolsData } = groupItems(tools, tool => tool.label[language]![0]!) @@ -58,7 +62,7 @@ const List = ({ return result }, [withLetterAndGroupViewToolsData, letters]) - const toolRefs = useRef({}) + const toolRefsRef = useRef>({}) const handleSelect = useCallback((type: BlockEnum, tool: ToolDefaultValue) => { onSelect(type, tool) @@ -70,9 +74,10 @@ const List = ({ isFlatView ? ( ) )} + + {({ payload }) => ( + + )} + { unInstalledPlugins.map((item) => { return ( diff --git a/web/app/components/workflow/block-selector/start-blocks.tsx b/web/app/components/workflow/block-selector/start-blocks.tsx index efc1e5c1b9..9ed4aa4008 100644 --- a/web/app/components/workflow/block-selector/start-blocks.tsx +++ b/web/app/components/workflow/block-selector/start-blocks.tsx @@ -1,6 +1,7 @@ import type { BlockEnum, CommonNodeType } from '../types' import type { TriggerDefaultValue } from './types' import { + createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger, @@ -25,6 +26,9 @@ type StartBlocksProps = { onContentStateChange?: (hasContent: boolean) => void hideUserInput?: boolean } +type StartBlockPreviewPayload = { + block: typeof START_BLOCKS[number] +} const StartBlocks = ({ searchText, @@ -35,6 +39,7 @@ const StartBlocks = ({ }: StartBlocksProps) => { const { t } = useTranslation() const nodes = useNodes() + const previewCardHandle = useMemo(() => createPreviewCardHandle(), []) // const nodeMetaData = useNodeMetaData() const filteredBlocks = useMemo(() => { @@ -74,54 +79,31 @@ const StartBlocks = ({ // the start node, so hover/focus-only activation is a11y-safe. See // packages/dify-ui/AGENTS.md → Overlay Primitive Selection. const renderBlock = useCallback((block: typeof START_BLOCKS[number]) => ( - - onSelect(block.type)} - > - -
- {t(`blocks.${block.type}`, { ns: 'workflow' })} - {block.type === BlockEnumValues.Start && ( - {t('blocks.originalStartNode', { ns: 'workflow' })} - )} -
- - )} - /> - -
+ onSelect(block.type)} + > -
- {block.type === BlockEnumValues.TriggerWebhook - ? t('customWebhook', { ns: 'workflow' }) - : t(`blocks.${block.type}`, { ns: 'workflow' })} +
+ {t(`blocks.${block.type}`, { ns: 'workflow' })} + {block.type === BlockEnumValues.Start && ( + {t('blocks.originalStartNode', { ns: 'workflow' })} + )}
-
- {t(`blocksAbout.${block.type}`, { ns: 'workflow' })} -
- {(block.type === BlockEnumValues.TriggerWebhook || block.type === BlockEnumValues.TriggerSchedule) && ( -
- {t('author', { ns: 'tools' })} - {' '} - {t('difyTeam', { ns: 'workflow' })} -
- )}
- - - ), [onSelect, t]) + )} + /> + ), [onSelect, previewCardHandle, t]) if (isEmpty) return null @@ -140,8 +122,58 @@ const StartBlocks = ({
))} + + {({ payload }) => ( + + )} + ) } +type StartBlockPreviewCardProps = { + payload?: StartBlockPreviewPayload + t: ReturnType['t'] +} + +function StartBlockPreviewCard({ + payload, + t, +}: StartBlockPreviewCardProps) { + if (!payload) + return null + + const { block } = payload + + return ( + +
+ +
+ {block.type === BlockEnumValues.TriggerWebhook + ? t('customWebhook', { ns: 'workflow' }) + : t(`blocks.${block.type}`, { ns: 'workflow' })} +
+
+ {t(`blocksAbout.${block.type}`, { ns: 'workflow' })} +
+ {(block.type === BlockEnumValues.TriggerWebhook || block.type === BlockEnumValues.TriggerSchedule) && ( +
+ {t('author', { ns: 'tools' })} + {' '} + {t('difyTeam', { ns: 'workflow' })} +
+ )} +
+
+ ) +} + export default memo(StartBlocks) diff --git a/web/app/components/workflow/block-selector/tool/__tests__/tool.spec.tsx b/web/app/components/workflow/block-selector/tool/__tests__/tool.spec.tsx index d9fad38854..b0cb229626 100644 --- a/web/app/components/workflow/block-selector/tool/__tests__/tool.spec.tsx +++ b/web/app/components/workflow/block-selector/tool/__tests__/tool.spec.tsx @@ -1,3 +1,4 @@ +import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { trackEvent } from '@/app/components/base/amplitude' @@ -51,6 +52,7 @@ describe('Tool', () => { createTool('tool-b', 'Tool B'), ], })} + previewCardHandle={createPreviewCardHandle()} viewType={ViewType.flat} hasSearchText={false} onSelect={onSelect} @@ -82,6 +84,7 @@ describe('Tool', () => { type: CollectionType.workflow, tools: [createTool('workflow-tool', 'Workflow Tool')], })} + previewCardHandle={createPreviewCardHandle()} viewType={ViewType.flat} hasSearchText={false} onSelect={onSelect} diff --git a/web/app/components/workflow/block-selector/tool/action-item.tsx b/web/app/components/workflow/block-selector/tool/action-item.tsx index 343f8482df..05aa05c162 100644 --- a/web/app/components/workflow/block-selector/tool/action-item.tsx +++ b/web/app/components/workflow/block-selector/tool/action-item.tsx @@ -1,10 +1,10 @@ 'use client' -import type { FC } from 'react' +import type { ComponentProps, FC } from 'react' import type { ToolWithProvider } from '../../types' import type { ToolDefaultValue } from '../types' import type { Tool } from '@/app/components/tools/types' import { cn } from '@langgenius/dify-ui/cn' -import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' +import { PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' import * as React from 'react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -27,14 +27,25 @@ const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => { type Props = { provider: ToolWithProvider payload: Tool + previewCardHandle: PreviewCardHandle disabled?: boolean isAdded?: boolean onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void } +export type ToolActionPreviewPayload = { + providerIcon: ToolWithProvider['icon'] + payload: Tool + language: ReturnType +} + +type PreviewCardHandle = NonNullable['handle']> +export type ToolActionPreviewCardHandle = PreviewCardHandle + const ToolItem: FC = ({ provider, payload, + previewCardHandle, onSelect, disabled, isAdded, @@ -107,21 +118,45 @@ const ToolItem: FC = ({ // reachable from the node inspector after the row is clicked to add the tool, // so hover/focus-only activation is a11y-safe. See // packages/dify-ui/AGENTS.md → Overlay Primitive Selection. - - - -
- -
{payload.label[language]}
-
{payload.description[language]}
-
-
-
+ ) } + +type ToolActionPreviewCardProps = { + payload?: ToolActionPreviewPayload +} + +export function ToolActionPreviewCard({ + payload, +}: ToolActionPreviewCardProps) { + if (!payload) + return null + + return ( + +
+ +
{payload.payload.label[payload.language]}
+
{payload.payload.description[payload.language]}
+
+
+ ) +} + export default React.memo(ToolItem) diff --git a/web/app/components/workflow/block-selector/tool/tool-list-flat-view/__tests__/list.spec.tsx b/web/app/components/workflow/block-selector/tool/tool-list-flat-view/__tests__/list.spec.tsx index ecb5dfe0a6..38bac7efcb 100644 --- a/web/app/components/workflow/block-selector/tool/tool-list-flat-view/__tests__/list.spec.tsx +++ b/web/app/components/workflow/block-selector/tool/tool-list-flat-view/__tests__/list.spec.tsx @@ -1,3 +1,4 @@ +import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card' import { render, screen } from '@testing-library/react' import { useGetLanguage } from '@/context/i18n' import useTheme from '@/hooks/use-theme' @@ -37,6 +38,7 @@ describe('ToolListFlatView', () => { render( void letters: string[] - toolRefs: any + toolRefs: RefObject> selectedTools?: ToolValue[] } const ToolViewFlatView: FC = ({ letters, payload, + previewCardHandle, isShowLetterIndex, indexBar, hasSearchText, @@ -55,6 +58,7 @@ const ToolViewFlatView: FC = ({ > { toolList={[createToolProvider({ label: { en_US: 'Provider Alpha', zh_Hans: 'Provider Alpha' }, })]} + previewCardHandle={createPreviewCardHandle()} hasSearchText={false} onSelect={vi.fn()} />, diff --git a/web/app/components/workflow/block-selector/tool/tool-list-tree-view/__tests__/list.spec.tsx b/web/app/components/workflow/block-selector/tool/tool-list-tree-view/__tests__/list.spec.tsx index 7b3c083e85..66b5a043ab 100644 --- a/web/app/components/workflow/block-selector/tool/tool-list-tree-view/__tests__/list.spec.tsx +++ b/web/app/components/workflow/block-selector/tool/tool-list-tree-view/__tests__/list.spec.tsx @@ -1,3 +1,4 @@ +import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card' import { render, screen } from '@testing-library/react' import { useGetLanguage } from '@/context/i18n' import useTheme from '@/hooks/use-theme' @@ -43,6 +44,7 @@ describe('ToolListTreeView', () => { label: { en_US: 'Custom Provider', zh_Hans: 'Custom Provider' }, })], }} + previewCardHandle={createPreviewCardHandle()} hasSearchText={false} onSelect={vi.fn()} />, diff --git a/web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx b/web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx index 97049e5f79..aa4ce9abe2 100644 --- a/web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx +++ b/web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import type { BlockEnum, ToolWithProvider } from '../../../types' import type { ToolDefaultValue, ToolValue } from '../../types' +import type { ToolActionPreviewCardHandle } from '../action-item' import * as React from 'react' import { ViewType } from '../../view-type-select' import Tool from '../tool' @@ -9,6 +10,7 @@ import Tool from '../tool' type Props = { groupName: string toolList: ToolWithProvider[] + previewCardHandle: ToolActionPreviewCardHandle hasSearchText: boolean onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void canNotSelectMultiple?: boolean @@ -19,6 +21,7 @@ type Props = { const Item: FC = ({ groupName, toolList, + previewCardHandle, hasSearchText, onSelect, canNotSelectMultiple, @@ -35,6 +38,7 @@ const Item: FC = ({ + previewCardHandle: ToolActionPreviewCardHandle hasSearchText: boolean onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void canNotSelectMultiple?: boolean @@ -19,6 +21,7 @@ type Props = { const ToolListTreeView: FC = ({ payload, + previewCardHandle, hasSearchText, onSelect, canNotSelectMultiple, @@ -49,6 +52,7 @@ const ToolListTreeView: FC = ({ key={groupName} groupName={getI18nGroupName(groupName)} toolList={payload[groupName]!} + previewCardHandle={previewCardHandle} hasSearchText={hasSearchText} onSelect={onSelect} canNotSelectMultiple={canNotSelectMultiple} diff --git a/web/app/components/workflow/block-selector/tool/tool.tsx b/web/app/components/workflow/block-selector/tool/tool.tsx index 9a25d10208..ebcfc3b8d1 100644 --- a/web/app/components/workflow/block-selector/tool/tool.tsx +++ b/web/app/components/workflow/block-selector/tool/tool.tsx @@ -3,11 +3,12 @@ import type { FC } from 'react' import type { Tool as ToolType } from '../../../tools/types' import type { ToolWithProvider } from '../../types' import type { ToolDefaultValue, ToolValue } from '../types' +import type { ToolActionPreviewCardHandle } from './action-item' import { cn } from '@langgenius/dify-ui/cn' import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' import { useHover } from 'ahooks' import * as React from 'react' -import { useCallback, useEffect, useMemo, useRef } from 'react' +import { useCallback, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { Mcp } from '@/app/components/base/icons/src/vender/other' import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability' @@ -33,6 +34,7 @@ const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => { type Props = { className?: string payload: ToolWithProvider + previewCardHandle: ToolActionPreviewCardHandle viewType: ViewType hasSearchText: boolean onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void @@ -45,6 +47,7 @@ type Props = { const Tool: FC = ({ className, payload, + previewCardHandle, viewType, hasSearchText, onSelect, @@ -59,7 +62,8 @@ const Tool: FC = ({ const notShowProvider = payload.type === CollectionType.workflow const actions = payload.tools const hasAction = !notShowProvider - const [isFold, setFold] = React.useState(true) + const [isFold, setIsFold] = React.useState(true) + const [isFoldHasSearchText, setIsFoldHasSearchText] = React.useState(hasSearchText) const ref = useRef(null) const isHovering = useHover(ref) const isMCPTool = payload.type === CollectionType.mcp @@ -146,14 +150,10 @@ const Tool: FC = ({ ) }, [actions, getIsDisabled, isAllSelected, isHovering, language, onSelectMultiple, payload.id, payload.is_team_authorization, payload.name, payload.type, selectedToolsNum, t, totalToolsNum]) - useEffect(() => { - if (hasSearchText && isFold) { - setFold(false) - return - } - if (!hasSearchText && !isFold) - setFold(true) - }, [hasSearchText]) + if (isFoldHasSearchText !== hasSearchText) { + setIsFoldHasSearchText(hasSearchText) + setIsFold(!hasSearchText) + } const FoldIcon = isFold ? RiArrowRightSLine : RiArrowDownSLine @@ -181,7 +181,7 @@ const Tool: FC = ({ className="group/item flex w-full cursor-pointer items-center justify-between rounded-lg pr-1 pl-3 select-none hover:bg-state-base-hover" onClick={() => { if (hasAction) { - setFold(!isFold) + setIsFold(!isFold) return } @@ -240,6 +240,7 @@ const Tool: FC = ({ key={action.name} provider={payload} payload={action} + previewCardHandle={previewCardHandle} onSelect={onSelect} disabled={getIsDisabled(action) || isShowCanNotChooseMCPTip} isAdded={getIsDisabled(action)} diff --git a/web/app/components/workflow/block-selector/tools.tsx b/web/app/components/workflow/block-selector/tools.tsx index cabc079750..e2c4a88048 100644 --- a/web/app/components/workflow/block-selector/tools.tsx +++ b/web/app/components/workflow/block-selector/tools.tsx @@ -1,10 +1,13 @@ import type { BlockEnum, ToolWithProvider } from '../types' +import type { ToolActionPreviewPayload } from './tool/action-item' import type { ToolDefaultValue, ToolTypeEnum, ToolValue } from './types' import { cn } from '@langgenius/dify-ui/cn' +import { createPreviewCardHandle, PreviewCard } from '@langgenius/dify-ui/preview-card' import { memo, useMemo, useRef } from 'react' import Empty from '@/app/components/tools/provider/empty' import { useGetLanguage } from '@/context/i18n' import IndexBar, { groupItems } from './index-bar' +import { ToolActionPreviewCard } from './tool/action-item' import ToolListFlatView from './tool/tool-list-flat-view/list' import ToolListTreeView from './tool/tool-list-tree-view/list' import { ViewType } from './view-type-select' @@ -35,8 +38,8 @@ const Tools = ({ indexBarClassName, selectedTools, }: ToolsProps) => { - // const tools: any = [] const language = useGetLanguage() + const previewCardHandle = useMemo(() => createPreviewCardHandle(), []) const isFlatView = viewType === ViewType.flat const isShowLetterIndex = isFlatView && tools.length > 10 @@ -85,7 +88,7 @@ const Tools = ({ return result }, [withLetterAndGroupViewToolsData, letters]) - const toolRefs = useRef({}) + const toolRefsRef = useRef>({}) return (
@@ -98,21 +101,23 @@ const Tools = ({ isFlatView ? ( } + indexBar={} /> ) : ( ) )} + + {({ payload }) => ( + + )} +
) } diff --git a/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx b/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx index 38c4c2b0f5..d2829363e7 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx @@ -1,9 +1,9 @@ 'use client' -import type { FC } from 'react' +import type { ComponentProps, FC } from 'react' import type { TriggerDefaultValue, TriggerWithProvider } from '../types' import type { Event } from '@/app/components/tools/types' import { cn } from '@langgenius/dify-ui/cn' -import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' +import { PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' import * as React from 'react' import { useTranslation } from 'react-i18next' import { useGetLanguage } from '@/context/i18n' @@ -13,14 +13,25 @@ import { BlockEnum } from '../../types' type Props = { provider: TriggerWithProvider payload: Event + previewCardHandle: TriggerPluginActionPreviewCardHandle disabled?: boolean isAdded?: boolean onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void } +export type TriggerPluginActionPreviewPayload = { + provider: TriggerWithProvider + payload: Event + language: ReturnType +} + +type PreviewCardHandle = NonNullable['handle']> +export type TriggerPluginActionPreviewCardHandle = PreviewCardHandle + const TriggerPluginActionItem: FC = ({ provider, payload, + previewCardHandle, onSelect, disabled, isAdded, @@ -37,7 +48,7 @@ const TriggerPluginActionItem: FC = ({ return const params: Record = {} if (payload.parameters) { - payload.parameters.forEach((item: any) => { + payload.parameters.forEach((item) => { params[item.name] = '' }) } @@ -73,21 +84,41 @@ const TriggerPluginActionItem: FC = ({ // reachable from the node inspector after the row is clicked to add the trigger, // so hover/focus-only activation is a11y-safe. See // packages/dify-ui/AGENTS.md → Overlay Primitive Selection. - - - -
- -
{payload.label[language]}
-
{payload.description[language]}
-
-
-
+ ) } + +type TriggerPluginActionPreviewCardProps = { + payload?: TriggerPluginActionPreviewPayload +} + +export function TriggerPluginActionPreviewCard({ + payload, +}: TriggerPluginActionPreviewCardProps) { + if (!payload) + return null + + return ( + +
+ +
{payload.payload.label[payload.language]}
+
{payload.payload.description[payload.language]}
+
+
+ ) +} + export default React.memo(TriggerPluginActionItem) diff --git a/web/app/components/workflow/block-selector/trigger-plugin/item.tsx b/web/app/components/workflow/block-selector/trigger-plugin/item.tsx index 9e6c315506..68946fb04c 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/item.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/item.tsx @@ -1,10 +1,11 @@ 'use client' import type { FC } from 'react' +import type { TriggerPluginActionPreviewCardHandle } from './action-item' import type { TriggerDefaultValue, TriggerWithProvider } from '@/app/components/workflow/block-selector/types' import { cn } from '@langgenius/dify-ui/cn' import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' import * as React from 'react' -import { useEffect, useMemo, useRef } from 'react' +import { useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { CollectionType } from '@/app/components/tools/types' import BlockIcon from '@/app/components/workflow/block-icon' @@ -27,6 +28,7 @@ type Props = { className?: string payload: TriggerWithProvider hasSearchText: boolean + previewCardHandle: TriggerPluginActionPreviewCardHandle onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void } @@ -34,6 +36,7 @@ const TriggerPluginItem: FC = ({ className, payload, hasSearchText, + previewCardHandle, onSelect, }) => { const { t } = useTranslation() @@ -42,17 +45,14 @@ const TriggerPluginItem: FC = ({ const notShowProvider = payload.type === CollectionType.workflow const actions = payload.events const hasAction = !notShowProvider - const [isFold, setFold] = React.useState(true) + const [isFold, setIsFold] = React.useState(true) + const [isFoldHasSearchText, setIsFoldHasSearchText] = React.useState(hasSearchText) const ref = useRef(null) - useEffect(() => { - if (hasSearchText && isFold) { - setFold(false) - return - } - if (!hasSearchText && !isFold) - setFold(true) - }, [hasSearchText]) + if (isFoldHasSearchText !== hasSearchText) { + setIsFoldHasSearchText(hasSearchText) + setIsFold(!hasSearchText) + } const FoldIcon = isFold ? RiArrowRightSLine : RiArrowDownSLine @@ -97,14 +97,14 @@ const TriggerPluginItem: FC = ({ className="group/item flex w-full cursor-pointer items-center justify-between rounded-lg pr-1 pl-3 select-none hover:bg-state-base-hover" onClick={() => { if (hasAction) { - setFold(!isFold) + setIsFold(!isFold) return } const event = actions[0] const params: Record = {} if (event!.parameters) { - event!.parameters.forEach((item: any) => { + event!.parameters.forEach((item) => { params[item.name] = '' }) } @@ -150,6 +150,7 @@ const TriggerPluginItem: FC = ({ key={action.name} provider={providerWithResolvedIcon} payload={action} + previewCardHandle={previewCardHandle} onSelect={onSelect} disabled={false} isAdded={false} diff --git a/web/app/components/workflow/block-selector/trigger-plugin/list.tsx b/web/app/components/workflow/block-selector/trigger-plugin/list.tsx index 126583be73..2d2752c4f6 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/list.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/list.tsx @@ -1,9 +1,12 @@ 'use client' import type { BlockEnum } from '../../types' import type { TriggerDefaultValue, TriggerWithProvider } from '../types' +import type { TriggerPluginActionPreviewPayload } from './action-item' +import { createPreviewCardHandle, PreviewCard } from '@langgenius/dify-ui/preview-card' import { memo, useEffect, useMemo } from 'react' import { useGetLanguage } from '@/context/i18n' import { useAllTriggerPlugins } from '@/service/use-triggers' +import { TriggerPluginActionPreviewCard } from './action-item' import TriggerPluginItem from './item' type TriggerPluginListProps = { @@ -20,6 +23,7 @@ const TriggerPluginList = ({ }: TriggerPluginListProps) => { const { data: triggerPluginsData } = useAllTriggerPlugins() const language = useGetLanguage() + const previewCardHandle = useMemo(() => createPreviewCardHandle(), []) const normalizedSearch = searchText.trim().toLowerCase() const triggerPlugins = useMemo(() => { @@ -96,8 +100,14 @@ const TriggerPluginList = ({ payload={plugin} onSelect={onSelect} hasSearchText={!!searchText} + previewCardHandle={previewCardHandle} /> ))} + + {({ payload }) => ( + + )} + ) }