feat(web): embed marketplace in integration categories (#40744)

This commit is contained in:
Jingyi
2026-08-14 01:55:51 +00:00
committed by GitHub
parent bf044cf19c
commit e3b3165e9a
37 changed files with 677 additions and 315 deletions
@@ -126,9 +126,9 @@ describe('PluginCategoryPage', () => {
it.each([
[PluginCategoryEnum.tool, true],
[PluginCategoryEnum.trigger, true],
[PluginCategoryEnum.agent, true],
[PluginCategoryEnum.extension, true],
[PluginCategoryEnum.trigger, false],
[PluginCategoryEnum.agent, false],
[PluginCategoryEnum.extension, false],
])('sets drop install availability for %s', (category, enabled) => {
render(<PluginCategoryPage category={category} />)
@@ -43,11 +43,7 @@ const PluginCategoryPageContent = ({
...systemFeaturesQueryOptions(),
select: (s) => s.plugin_installation_permission,
})
const supportsDropInstall =
category === PluginCategoryEnum.tool ||
category === PluginCategoryEnum.trigger ||
category === PluginCategoryEnum.agent ||
category === PluginCategoryEnum.extension
const supportsDropInstall = category === PluginCategoryEnum.tool
const canDropLocalPackage =
canInstall && supportsDropInstall && !pluginInstallationPermission.restrict_to_marketplace_only
@@ -0,0 +1,43 @@
import type { ReactNode } from 'react'
import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { PluginCategoryEnum } from '../../types'
import CategoryEmptyState from '../category-empty-state'
import { getCategoryMarketplaceId } from '../category-marketplace'
vi.mock('react-i18next', async () => {
const { createReactI18nextMock } = await import('@/test/i18n-mock')
return {
...createReactI18nextMock(),
Trans: ({ components }: { components: { marketplace: ReactNode } }) => (
<>You can install integrations from the {components.marketplace}.</>
),
}
})
describe('CategoryEmptyState', () => {
it.each([
[PluginCategoryEnum.trigger, 'plugin.list.noTriggerFound'],
[PluginCategoryEnum.agent, 'plugin.list.noAgentStrategyFound'],
[PluginCategoryEnum.extension, 'plugin.list.noExtensionFound'],
] as const)(
'renders the compact %s empty state with an embedded marketplace link',
(category, label) => {
render(<CategoryEmptyState category={category} showMarketplaceLink />)
expect(screen.getByText(label)).toBeInTheDocument()
expect(screen.getByText(/You can install integrations from the/)).toBeInTheDocument()
expect(
screen.getByRole('link', { name: 'plugin.marketplace.difyMarketplace' }),
).toHaveAttribute('href', `#${getCategoryMarketplaceId(category)}`)
},
)
it('removes the middle install entry when marketplace is disabled', () => {
render(<CategoryEmptyState category={PluginCategoryEnum.trigger} showMarketplaceLink={false} />)
expect(screen.queryByRole('link')).not.toBeInTheDocument()
expect(screen.queryByText('plugin.source.github')).not.toBeInTheDocument()
expect(screen.queryByText('plugin.source.local')).not.toBeInTheDocument()
})
})
@@ -0,0 +1,184 @@
import type { PluginCategoryEnum } from '../../types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { PluginCategoryEnum as Category } from '../../types'
import { getCategoryMarketplaceId } from '../category-marketplace'
import CategoryMarketplacePanel from '../category-marketplace-panel'
const mocks = vi.hoisted(() => ({
canInstallPlugin: true,
fetchNextPage: vi.fn(),
installedPluginIds: ['installed/plugin'] as string[] | undefined,
installedPluginIdsError: false,
installedPluginIdsRefetch: vi.fn(),
searchParams: undefined as unknown,
}))
vi.mock('@tanstack/react-query', () => ({
useQuery: () => ({
data: mocks.installedPluginIds,
isError: mocks.installedPluginIdsError,
isFetching: false,
isPending: false,
refetch: mocks.installedPluginIdsRefetch,
}),
}))
vi.mock('next-themes', () => ({
useTheme: () => ({ theme: 'light' }),
}))
vi.mock('@/service/client', () => ({
consoleQuery: {
workspaces: {
current: {
plugin: {
installedIds: {
get: {
queryOptions: () => ({}),
},
},
},
},
},
},
}))
vi.mock('@/app/components/plugins/marketplace/query', () => ({
useMarketplacePlugins: (searchParams: unknown) => {
mocks.searchParams = searchParams
return {
data: {
pages: [
{
plugins: [
{ name: 'Installed plugin', plugin_id: 'installed/plugin', type: 'plugin' },
{ name: 'Calendar', plugin_id: 'langgenius/calendar', type: 'plugin' },
],
},
],
},
fetchNextPage: mocks.fetchNextPage,
hasNextPage: true,
isFetchingNextPage: false,
isPending: false,
}
},
}))
vi.mock('@/app/components/plugins/plugin-page/use-reference-setting', () => ({
usePluginSettingsAccess: () => ({ canInstallPlugin: mocks.canInstallPlugin }),
}))
vi.mock('@/app/components/plugins/marketplace/utils', () => ({
getMarketplaceCategoryUrl: (category: PluginCategoryEnum) =>
`https://marketplace.test/plugins/${category}`,
}))
vi.mock('@/app/components/plugins/marketplace/list', () => ({
default: ({
cardRender,
plugins,
showInstallButton,
}: {
cardRender: (plugin: { name: string; plugin_id: string; type: string }) => React.ReactNode
plugins: { name: string; plugin_id: string; type: string }[]
showInstallButton: boolean
}) => (
<div data-can-install={showInstallButton ? 'true' : 'false'} data-testid="marketplace-list">
{plugins.map((plugin) => (
<div key={plugin.plugin_id}>{cardRender(plugin)}</div>
))}
</div>
),
}))
vi.mock('@/app/components/plugins/provider-card', () => ({
default: ({ payload }: { payload: { name: string } }) => <div>{payload.name}</div>,
}))
describe('CategoryMarketplacePanel', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.canInstallPlugin = true
mocks.installedPluginIds = ['installed/plugin']
mocks.installedPluginIdsError = false
mocks.searchParams = undefined
})
it.each([Category.trigger, Category.agent, Category.extension] as const)(
'queries and links to the scoped %s marketplace',
async (category) => {
render(<CategoryMarketplacePanel category={category} searchText="calendar" />)
await waitFor(() => {
expect(mocks.searchParams).toEqual({
category,
exclude: ['installed/plugin'],
page_size: 30,
query: 'calendar',
sort_by: 'install_count',
sort_order: 'DESC',
type: 'plugin',
})
})
expect(
screen.getByRole('link', { name: /plugin\.marketplace\.difyMarketplace/ }),
).toHaveAttribute('href', `https://marketplace.test/plugins/${category}`)
expect(document.getElementById(getCategoryMarketplaceId(category))).toBeInTheDocument()
expect(screen.getByText('Calendar')).toBeInTheDocument()
expect(screen.queryByText('Installed plugin')).not.toBeInTheDocument()
},
)
it('passes selected tags to the trigger marketplace search', async () => {
render(
<CategoryMarketplacePanel
category={Category.trigger}
searchText="calendar"
tags={['search']}
/>,
)
await waitFor(() => {
expect(mocks.searchParams).toMatchObject({
category: Category.trigger,
tags: ['search'],
})
})
})
it('keeps browsing available but hides install actions without permission', () => {
mocks.canInstallPlugin = false
render(<CategoryMarketplacePanel category={Category.trigger} searchText="" />)
expect(screen.getByTestId('marketplace-list')).toHaveAttribute('data-can-install', 'false')
expect(screen.getByText('Calendar')).toBeInTheDocument()
})
it('shows a retry action when installed plugin IDs cannot be loaded', () => {
mocks.installedPluginIds = undefined
mocks.installedPluginIdsError = true
render(<CategoryMarketplacePanel category={Category.trigger} searchText="" />)
expect(screen.getByRole('alert')).toHaveTextContent('common.errorBoundary.title')
fireEvent.click(screen.getByRole('button', { name: 'common.operation.retry' }))
expect(mocks.installedPluginIdsRefetch).toHaveBeenCalledTimes(1)
expect(mocks.searchParams).toBeUndefined()
})
it('supports collapsing and loading the next marketplace page', () => {
render(<CategoryMarketplacePanel category={Category.extension} searchText="" />)
const toggle = screen.getByRole('button', { name: 'plugin.list.source.marketplace' })
fireEvent.click(toggle)
expect(toggle).toHaveAttribute('aria-expanded', 'false')
expect(screen.queryByTestId('marketplace-list')).not.toBeInTheDocument()
fireEvent.click(toggle)
fireEvent.click(screen.getByRole('button', { name: 'workflow.common.loadMore' }))
expect(mocks.fetchNextPage).toHaveBeenCalledTimes(1)
})
})
@@ -17,6 +17,9 @@ const mockState = vi.hoisted(() => ({
},
currentPluginID: undefined as string | undefined,
}))
const mockSystemFeatures = vi.hoisted(() => ({
enableMarketplace: true,
}))
const mockSetFilters = vi.fn()
const mockSetCurrentPluginID = vi.fn()
@@ -31,7 +34,7 @@ const mockDisconnect = vi.fn()
vi.mock('@tanstack/react-query', () => ({
queryOptions: (options: unknown) => options,
useSuspenseQuery: () => ({ data: true }),
useSuspenseQuery: () => ({ data: mockSystemFeatures.enableMarketplace }),
}))
vi.mock('@/i18n-config', () => ({
renderI18nObject: (value: Record<string, string>, locale: string) => value[locale] || '',
@@ -192,6 +195,41 @@ vi.mock('@/app/components/tools/marketplace', () => ({
),
}))
vi.mock('../category-marketplace-panel', () => ({
default: ({
category,
searchText,
tags,
}: {
category: PluginCategoryEnum
searchText: string
tags: string[]
}) => (
<div
data-category={category}
data-search-text={searchText}
data-tags={tags.join(',')}
data-testid="category-marketplace"
/>
),
}))
vi.mock('../category-empty-state', () => ({
default: ({
category,
showMarketplaceLink,
}: {
category: PluginCategoryEnum
showMarketplaceLink: boolean
}) => (
<div
data-category={category}
data-show-marketplace-link={showMarketplaceLink ? 'true' : 'false'}
data-testid="category-empty-state"
/>
),
}))
vi.mock('@/app/components/tools/provider/detail', () => ({
default: ({ collection, onHide }: { collection: Collection; onHide: () => void }) => (
<div data-testid="builtin-tool-detail">
@@ -270,6 +308,7 @@ const createBuiltinTool = (id: string, label: string, labels: string[] = []): Co
describe('PluginsPanel', () => {
beforeEach(() => {
mockSystemFeatures.enableMarketplace = true
vi.clearAllMocks()
vi.useFakeTimers()
intersectionObserverCallbacks.length = 0
@@ -543,6 +582,49 @@ describe('PluginsPanel', () => {
).toBeTruthy()
})
it.each([PluginCategoryEnum.trigger, PluginCategoryEnum.agent, PluginCategoryEnum.extension])(
'keeps the scoped %s marketplace below installed plugins',
(category) => {
mockState.filters.searchQuery = 'calendar'
mockPluginListWithLatestVersion.mockReturnValue([
createPlugin(`${category}-calendar`, `${category} Calendar`, [], category),
])
render(<PluginsPanel contentInset="compact" fixedCategory={category} />)
const marketplace = screen.getByTestId('category-marketplace')
expect(marketplace).toHaveAttribute('data-category', category)
expect(marketplace).toHaveAttribute('data-search-text', 'calendar')
expect(
screen.getByTestId('plugin-list').compareDocumentPosition(marketplace) &
Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy()
},
)
it('passes tag filters to the trigger marketplace only', () => {
mockState.filters.tags = ['search']
render(<PluginsPanel contentInset="compact" fixedCategory={PluginCategoryEnum.trigger} />)
expect(screen.getByTestId('category-marketplace')).toHaveAttribute('data-tags', 'search')
})
it.each([PluginCategoryEnum.trigger, PluginCategoryEnum.agent, PluginCategoryEnum.extension])(
'hides the scoped %s marketplace when marketplace is disabled',
(category) => {
mockSystemFeatures.enableMarketplace = false
render(<PluginsPanel contentInset="compact" fixedCategory={category} />)
expect(screen.queryByTestId('category-marketplace')).not.toBeInTheDocument()
expect(screen.getByTestId('category-empty-state')).toHaveAttribute(
'data-show-marketplace-link',
'false',
)
},
)
it('uses the Figma trigger toolbar frame and renders the toolbar action', () => {
render(
<PluginsPanel
@@ -598,13 +680,13 @@ describe('PluginsPanel', () => {
)
expect(screen.getByTestId('filter-management')).toHaveAttribute('data-hide-tag-filter', 'true')
expect(screen.getByText('update setting')).toBeInTheDocument()
expect(screen.getByTestId('empty-state')).toHaveAttribute(
'data-variant',
'integrationsAgentStrategy',
expect(screen.getByTestId('category-empty-state')).toHaveAttribute(
'data-category',
PluginCategoryEnum.agent,
)
})
it('passes install permission to the integration category empty state', () => {
it('keeps the integration category empty state browsable without install permission', () => {
render(
<PluginsPanel
canInstall={false}
@@ -613,7 +695,11 @@ describe('PluginsPanel', () => {
/>,
)
expect(screen.getByTestId('empty-state')).toHaveAttribute('data-can-install', 'false')
expect(screen.getByTestId('category-empty-state')).toHaveAttribute(
'data-show-marketplace-link',
'true',
)
expect(screen.getByTestId('category-marketplace')).toBeInTheDocument()
})
it('uses the Figma extension toolbar frame and renders the extension empty state', () => {
@@ -642,13 +728,13 @@ describe('PluginsPanel', () => {
)
expect(screen.getByTestId('filter-management')).toHaveAttribute('data-hide-tag-filter', 'true')
expect(screen.getByText('update setting')).toBeInTheDocument()
expect(screen.getByTestId('empty-state')).toHaveAttribute(
'data-variant',
'integrationsExtension',
expect(screen.getByTestId('category-empty-state')).toHaveAttribute(
'data-category',
PluginCategoryEnum.extension,
)
})
it('passes the marketplace action to the empty state', () => {
it('uses the embedded marketplace link instead of the external marketplace action', () => {
const onSwitchToMarketplace = vi.fn()
render(
@@ -659,7 +745,11 @@ describe('PluginsPanel', () => {
/>,
)
expect(screen.getByTestId('empty-state')).toHaveAttribute('data-has-marketplace-action', 'true')
expect(screen.getByTestId('category-empty-state')).toHaveAttribute(
'data-show-marketplace-link',
'true',
)
expect(onSwitchToMarketplace).not.toHaveBeenCalled()
})
it('ignores hidden tag filters within the fixed extension integrations category', () => {
@@ -0,0 +1,76 @@
import type { EmbeddedMarketplaceCategory } from './category-marketplace'
import { Trans, useTranslation } from 'react-i18next'
import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/target-registry'
import { getCategoryMarketplaceId } from './category-marketplace'
const categoryConfig = {
trigger: {
categoryKey: 'category.triggers',
iconClassName: 'i-custom-vender-integrations-trigger-active',
textKey: 'list.noTriggerFound',
tourTarget: STEP_BY_STEP_TOUR_TARGETS.integrationTriggerGrid,
},
'agent-strategy': {
categoryKey: 'category.agents',
iconClassName: 'i-custom-vender-integrations-agent-strategy-active',
textKey: 'list.noAgentStrategyFound',
tourTarget: STEP_BY_STEP_TOUR_TARGETS.integrationAgentStrategyEmpty,
},
extension: {
categoryKey: 'category.extensions',
iconClassName: 'i-custom-vender-integrations-extension-active',
textKey: 'list.noExtensionFound',
tourTarget: STEP_BY_STEP_TOUR_TARGETS.integrationExtensionGrid,
},
} as const
type Category = keyof typeof categoryConfig
const CategoryEmptyState = ({
category,
showMarketplaceLink,
}: {
category: EmbeddedMarketplaceCategory
showMarketplaceLink: boolean
}) => {
const { t } = useTranslation()
const config = categoryConfig[category as Category]
if (!config) return null
return (
<div
className="mb-2 rounded-[10px] bg-workflow-process-bg p-4"
data-step-by-step-tour-target={config.tourTarget}
>
<div className="flex size-10 items-center justify-center rounded-[10px] border-[0.5px] border-components-card-border bg-components-card-bg shadow-lg backdrop-blur-sm">
<span aria-hidden className={`${config.iconClassName} size-5 text-text-primary`} />
</div>
<div className="mt-2 system-sm-medium text-text-secondary">
{t(($) => $[config.textKey], { ns: 'plugin' })}
</div>
{showMarketplaceLink && (
<p className="mt-1 system-xs-regular text-text-tertiary">
<Trans
components={{
marketplace: (
<a
aria-label={t(($) => $['marketplace.difyMarketplace'], { ns: 'plugin' })}
className="system-xs-medium text-text-accent hover:underline"
href={`#${getCategoryMarketplaceId(category)}`}
>
{t(($) => $['marketplace.difyMarketplace'], { ns: 'plugin' })}
</a>
),
}}
i18nKey={($) => $['list.emptyInstallFromMarketplace']}
ns="plugin"
values={{ category: t(($) => $[config.categoryKey], { ns: 'plugin' }) }}
/>
</p>
)}
</div>
)
}
export default CategoryEmptyState
@@ -0,0 +1,158 @@
'use client'
import type { PluginsSearchParams } from '@dify/contracts/marketplace'
import type { Plugin } from '../types'
import type { EmbeddedMarketplaceCategory } from './category-marketplace'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useQuery } from '@tanstack/react-query'
import { useTheme } from 'next-themes'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import Loading from '@/app/components/base/loading'
import List from '@/app/components/plugins/marketplace/list'
import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/query'
import { getMarketplaceCategoryUrl } from '@/app/components/plugins/marketplace/utils'
import { usePluginSettingsAccess } from '@/app/components/plugins/plugin-page/use-reference-setting'
import ProviderCard from '@/app/components/plugins/provider-card'
import Link from '@/next/link'
import { consoleQuery } from '@/service/client'
import { getCategoryMarketplaceId } from './category-marketplace'
const MARKETPLACE_PAGE_SIZE = 30
const CategoryMarketplacePanel = ({
category,
searchText,
tags = [],
}: {
category: EmbeddedMarketplaceCategory
searchText: string
tags?: string[]
}) => {
const { t } = useTranslation()
const { theme } = useTheme()
const { canInstallPlugin } = usePluginSettingsAccess()
const [collapsed, setCollapsed] = useState(false)
const {
data: installedPluginIds,
isError: hasInstalledPluginIdsError,
isFetching: isFetchingInstalledPluginIds,
isPending: isLoadingInstalledPluginIds,
refetch: refetchInstalledPluginIds,
} = useQuery({
...consoleQuery.workspaces.current.plugin.installedIds.get.queryOptions({
input: { query: { category } },
}),
select: (data) => data.plugin_ids,
})
const hasLoadedInstalledPluginIds = installedPluginIds !== undefined
const marketplaceSearchParams = useMemo<PluginsSearchParams | undefined>(() => {
if (!hasLoadedInstalledPluginIds || collapsed) return undefined
return {
category,
exclude: installedPluginIds ?? [],
page_size: MARKETPLACE_PAGE_SIZE,
query: searchText,
sort_by: 'install_count',
sort_order: 'DESC',
...(tags.length ? { tags } : {}),
type: 'plugin',
}
}, [category, collapsed, hasLoadedInstalledPluginIds, installedPluginIds, searchText, tags])
const { data, isPending, isFetchingNextPage, fetchNextPage, hasNextPage } =
useMarketplacePlugins(marketplaceSearchParams)
const plugins = useMemo(
() =>
data?.pages
.flatMap((page) => page.plugins)
.filter((plugin) => !installedPluginIds?.includes(plugin.plugin_id)),
[data, installedPluginIds],
)
const marketplaceLink = getMarketplaceCategoryUrl(category, { theme })
const showInstalledPluginIdsError = hasInstalledPluginIdsError && !hasLoadedInstalledPluginIds
const showLoading = isLoadingInstalledPluginIds || !!(marketplaceSearchParams && isPending)
const cardRender = useCallback((plugin: Plugin) => {
if (plugin.type === 'bundle') return null
return <ProviderCard key={plugin.plugin_id} className="h-36.5" payload={plugin} />
}, [])
return (
<section
className="flex scroll-mt-4 flex-col gap-2 pb-2"
id={getCategoryMarketplaceId(category)}
>
<Divider className="my-2! h-px" />
<div className="flex items-center justify-between gap-3">
<button
type="button"
aria-expanded={!collapsed}
className="flex cursor-pointer items-center gap-1 border-0 bg-transparent p-0 text-left system-md-semibold text-text-primary"
onClick={() => setCollapsed((value) => !value)}
>
<span
aria-hidden
className={cn('i-ri-arrow-down-s-line size-4', collapsed && '-rotate-90')}
/>
{t(($) => $['list.source.marketplace'], { ns: 'plugin' })}
</button>
<div className="flex items-center gap-1">
<span className="system-sm-regular text-text-tertiary">
{t(($) => $['modelProvider.discoverMore'], { ns: 'common' })}
</span>
<Link
className="inline-flex items-center system-sm-medium text-text-accent"
href={marketplaceLink}
rel="noopener noreferrer"
target="_blank"
>
{t(($) => $['marketplace.difyMarketplace'], { ns: 'plugin' })}
<span aria-hidden className="i-ri-arrow-right-up-line size-4" />
</Link>
</div>
</div>
{!collapsed && (
<div>
{showLoading && <Loading type="area" />}
{showInstalledPluginIdsError && (
<div className="flex flex-col items-center gap-2 py-4">
<span className="system-sm-regular text-text-tertiary" role="alert">
{t(($) => $['errorBoundary.title'], { ns: 'common' })}
</span>
<Button
loading={isFetchingInstalledPluginIds}
onClick={() => refetchInstalledPluginIds()}
>
{t(($) => $['operation.retry'], { ns: 'common' })}
</Button>
</div>
)}
{!showLoading && !showInstalledPluginIdsError && (
<List
cardContainerClassName="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
emptyClassName="h-auto"
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
plugins={plugins ?? []}
showInstallButton={canInstallPlugin}
cardRender={cardRender}
/>
)}
{!showLoading && hasNextPage && (
<div className="flex justify-center py-4">
<Button loading={isFetchingNextPage} onClick={() => fetchNextPage()}>
{t(($) => $['common.loadMore'], { ns: 'workflow' })}
</Button>
</div>
)}
</div>
)}
</section>
)
}
export default CategoryMarketplacePanel
@@ -0,0 +1,13 @@
import { PluginCategoryEnum } from '../types'
export type EmbeddedMarketplaceCategory = 'agent-strategy' | 'extension' | 'trigger'
export const isEmbeddedMarketplaceCategory = (
category?: EmbeddedMarketplaceCategory | PluginCategoryEnum,
): category is EmbeddedMarketplaceCategory =>
category === PluginCategoryEnum.trigger ||
category === PluginCategoryEnum.agent ||
category === PluginCategoryEnum.extension
export const getCategoryMarketplaceId = (category: EmbeddedMarketplaceCategory) =>
`plugin-category-marketplace-${category}`
@@ -4,10 +4,6 @@ import type { FilterState } from '../../filter-management'
import { zPluginInstallationScope } from '@dify/contracts/api/console/system-features/zod.gen'
import { act, fireEvent, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
getStepByStepTourTargetSelector,
STEP_BY_STEP_TOUR_TARGETS,
} from '@/app/components/step-by-step-tour/target-registry'
import { renderWithConsoleQuery } from '@/test/console/query-data'
// ==================== Imports (after mocks) ====================
import Empty from '../index'
@@ -158,116 +154,6 @@ describe('Empty Component', () => {
const lines = screen.getAllByTestId('line-component')
expect(lines).toHaveLength(4)
})
it('should render the Figma trigger empty layout variant', async () => {
// Arrange & Act
const { container } = render(<Empty contentInset="compact" variant="integrationsTrigger" />)
await flushEffects()
// Assert
expect(screen.getByText('plugin.list.noTriggerFound')).toBeInTheDocument()
expect(screen.getByText('plugin.installModal.dropIntegrationToInstall')).toBeInTheDocument()
expect(container.querySelector('.i-ri-drag-drop-line')).toBeInTheDocument()
expect(container.firstElementChild).toHaveClass('bg-components-panel-bg')
expect(
container.querySelector('.i-custom-vender-integrations-trigger-active'),
).toBeInTheDocument()
expect(
container.querySelector('.i-custom-vender-integrations-trigger'),
).not.toBeInTheDocument()
const buttons = screen.getAllByRole('button')
buttons.forEach((button) => expect(button).toHaveClass('h-8', 'w-full', 'justify-start'))
})
it('should anchor the trigger tour target to the empty state content instead of the grow root', async () => {
const { container } = render(<Empty contentInset="compact" variant="integrationsTrigger" />)
await flushEffects()
const selector = getStepByStepTourTargetSelector(
STEP_BY_STEP_TOUR_TARGETS.integrationTriggerGrid,
)
const target = document.querySelector<HTMLElement>(selector)
expect(container.firstElementChild).not.toHaveAttribute('data-step-by-step-tour-target')
expect(target).toContainElement(screen.getByText('plugin.list.noTriggerFound'))
expect(target).toContainElement(screen.getByText('plugin.source.marketplace'))
expect(target).not.toContainElement(
screen.getByText('plugin.installModal.dropIntegrationToInstall'),
)
})
it('should render the Figma agent strategy empty layout at the shared center position', async () => {
// Arrange & Act
const { container } = render(
<Empty contentInset="compact" variant="integrationsAgentStrategy" />,
)
await flushEffects()
// Assert
expect(screen.getByText('plugin.list.noAgentStrategyFound')).toBeInTheDocument()
expect(screen.getByText('plugin.installModal.dropIntegrationToInstall')).toBeInTheDocument()
expect(container.querySelector('.i-ri-drag-drop-line')).toBeInTheDocument()
expect(container.firstElementChild).toHaveClass('bg-components-panel-bg')
expect(container.querySelector('.items-center')).toBeInTheDocument()
expect(container.querySelector('.-translate-y-7')).not.toBeInTheDocument()
expect(
container.querySelector('.i-custom-vender-integrations-agent-strategy-active'),
).toHaveClass('size-6', 'shrink-0')
})
it('should anchor the agent strategy tour target to the empty state content instead of the grow root', async () => {
const { container } = render(
<Empty contentInset="compact" variant="integrationsAgentStrategy" />,
)
await flushEffects()
const selector = getStepByStepTourTargetSelector(
STEP_BY_STEP_TOUR_TARGETS.integrationAgentStrategyEmpty,
)
const target = document.querySelector<HTMLElement>(selector)
expect(container.firstElementChild).not.toHaveAttribute('data-step-by-step-tour-target')
expect(target).toContainElement(screen.getByText('plugin.list.noAgentStrategyFound'))
expect(target).toContainElement(screen.getByText('plugin.source.marketplace'))
expect(target).not.toContainElement(
screen.getByText('plugin.installModal.dropIntegrationToInstall'),
)
})
it('should render the Figma extension empty layout with extension copy', async () => {
// Arrange & Act
const { container } = render(<Empty contentInset="compact" variant="integrationsExtension" />)
await flushEffects()
// Assert
expect(screen.getByText('plugin.list.noExtensionFound')).toBeInTheDocument()
expect(screen.getByText('plugin.installModal.dropIntegrationToInstall')).toBeInTheDocument()
expect(container.querySelector('.i-ri-drag-drop-line')).toBeInTheDocument()
expect(container.querySelector('.i-custom-vender-integrations-extension-active')).toHaveClass(
'size-6',
'shrink-0',
)
})
it('should anchor the extension tour target to the empty state content instead of the grow root', async () => {
const { container } = render(<Empty contentInset="compact" variant="integrationsExtension" />)
await flushEffects()
const selector = getStepByStepTourTargetSelector(
STEP_BY_STEP_TOUR_TARGETS.integrationExtensionGrid,
)
const target = document.querySelector<HTMLElement>(selector)
expect(container.firstElementChild).not.toHaveAttribute('data-step-by-step-tour-target')
expect(target).toContainElement(screen.getByText('plugin.list.noExtensionFound'))
expect(target).toContainElement(screen.getByText('plugin.source.marketplace'))
expect(target).not.toContainElement(
screen.getByText('plugin.installModal.dropIntegrationToInstall'),
)
})
})
// ==================== Text Display Tests (useMemo) ====================
@@ -414,9 +300,9 @@ describe('Empty Component', () => {
expect(buttons).toHaveLength(0)
})
it('should render no install methods or drop hint when install permission is unavailable', async () => {
it('should render no install methods when install permission is unavailable', async () => {
// Act
render(<Empty canInstall={false} contentInset="compact" variant="integrationsTrigger" />)
render(<Empty canInstall={false} contentInset="compact" />)
await flushEffects()
// Assert
@@ -424,9 +310,6 @@ describe('Empty Component', () => {
expect(screen.queryByText('plugin.source.marketplace')).not.toBeInTheDocument()
expect(screen.queryByText('plugin.source.github')).not.toBeInTheDocument()
expect(screen.queryByText('plugin.source.local')).not.toBeInTheDocument()
expect(
screen.queryByText('plugin.installModal.dropIntegrationToInstall'),
).not.toBeInTheDocument()
})
})
@@ -447,9 +330,7 @@ describe('Empty Component', () => {
it('should use the provided marketplace action when marketplace button is clicked', async () => {
// Arrange
const onSwitchToMarketplace = vi.fn()
render(
<Empty onSwitchToMarketplace={onSwitchToMarketplace} variant="integrationsExtension" />,
)
render(<Empty onSwitchToMarketplace={onSwitchToMarketplace} />)
await flushEffects()
// Act
@@ -14,7 +14,6 @@ import { Github } from '@/app/components/base/icons/src/vender/solid/general'
import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
import InstallFromGitHub from '@/app/components/plugins/install-plugin/install-from-github'
import InstallFromLocalPackage from '@/app/components/plugins/install-plugin/install-from-local-package'
import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/target-registry'
import { SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { useInstalledPluginList } from '@/service/use-plugins'
@@ -24,45 +23,18 @@ import {
pluginPageContentInsetClassNames,
} from '../content-inset'
import { usePluginPageContext } from '../context'
import {
DropHintInstallSourceIcon,
GithubInstallSourceIcon,
LocalPackageInstallSourceIcon,
MarketplaceInstallSourceIcon,
} from '../install-source-icons'
type InstallMethod = {
icon: React.ComponentType<{ className?: string }>
integrationIcon: React.ComponentType
text: string
action: string
}
const TriggerEmptyIcon = () => (
<span aria-hidden className="i-custom-vender-integrations-trigger-active size-6 shrink-0" />
)
const AgentStrategyEmptyIcon = () => (
<span
aria-hidden
className="i-custom-vender-integrations-agent-strategy-active size-6 shrink-0"
/>
)
const ExtensionEmptyIcon = () => (
<span aria-hidden className="i-custom-vender-integrations-extension-active size-6 shrink-0" />
)
type EmptyProps = {
canInstall?: boolean
contentInset?: PluginPageContentInset
installContextCategory?: PluginCategoryEnum
onSwitchToMarketplace?: () => void
variant?:
| 'default'
| 'integrationsAgentStrategy'
| 'integrationsExtension'
| 'integrationsTrigger'
}
const Empty = ({
@@ -70,7 +42,6 @@ const Empty = ({
contentInset = 'default',
installContextCategory,
onSwitchToMarketplace,
variant = 'default',
}: EmptyProps) => {
const { t } = useTranslation()
const fileInputRef = useRef<HTMLInputElement>(null)
@@ -121,7 +92,6 @@ const Empty = ({
if (enable_marketplace)
methods.push({
icon: MagicBox,
integrationIcon: MarketplaceInstallSourceIcon,
text: t(($) => $['source.marketplace'], { ns: 'plugin' }),
action: 'marketplace',
})
@@ -130,47 +100,21 @@ const Empty = ({
methods.push({
icon: Github,
integrationIcon: GithubInstallSourceIcon,
text: t(($) => $['source.github'], { ns: 'plugin' }),
action: 'github',
})
methods.push({
icon: FileZip,
integrationIcon: LocalPackageInstallSourceIcon,
text: t(($) => $['source.local'], { ns: 'plugin' }),
action: 'local',
})
return methods
}, [canInstall, plugin_installation_permission, enable_marketplace, t])
const contentPaddingClassName = pluginPageContentInsetClassNames[contentInset]
const canInstallLocalPackage =
canInstall && !plugin_installation_permission.restrict_to_marketplace_only
const isIntegrationsTrigger = variant === 'integrationsTrigger'
const isIntegrationsAgentStrategy = variant === 'integrationsAgentStrategy'
const isIntegrationsExtension = variant === 'integrationsExtension'
const isIntegrationsCategory =
isIntegrationsTrigger || isIntegrationsAgentStrategy || isIntegrationsExtension
const supportsDropInstall = isIntegrationsCategory
const showDropInstallTip = supportsDropInstall && canInstallLocalPackage
const contentFrameClassName = cn(
pluginPageContentFrameClassNames[contentInset],
contentPaddingClassName,
)
const emptyText = isIntegrationsTrigger
? t(($) => $['list.noTriggerFound'], { ns: 'plugin' })
: isIntegrationsAgentStrategy
? t(($) => $['list.noAgentStrategyFound'], { ns: 'plugin' })
: isIntegrationsExtension
? t(($) => $['list.noExtensionFound'], { ns: 'plugin' })
: text
const emptyTarget = isIntegrationsTrigger
? STEP_BY_STEP_TOUR_TARGETS.integrationTriggerGrid
: isIntegrationsAgentStrategy
? STEP_BY_STEP_TOUR_TARGETS.integrationAgentStrategyEmpty
: isIntegrationsExtension
? STEP_BY_STEP_TOUR_TARGETS.integrationExtensionGrid
: undefined
const placeholderItemCount = isIntegrationsCategory ? 14 : 20
return (
<div className="relative z-0 w-full grow bg-components-panel-bg">
@@ -181,80 +125,26 @@ const Empty = ({
contentFrameClassName,
)}
>
{Array.from({ length: placeholderItemCount }, (_, i) => (
<div
key={i}
className={cn(
isIntegrationsCategory
? 'h-24 rounded-lg bg-background-section-burn/30'
: 'h-24 rounded-xl bg-components-card-bg',
)}
/>
{Array.from({ length: 20 }, (_, i) => (
<div key={i} className="h-24 rounded-xl bg-components-card-bg" />
))}
</div>
<div
aria-hidden
className="pointer-events-none absolute inset-0 z-20 bg-linear-to-b from-components-panel-bg-transparent to-components-panel-bg"
/>
<div
className={cn(
'relative z-30 flex h-full',
showDropInstallTip ? 'flex-col' : 'items-center justify-center',
)}
>
<div
className={cn(
'flex items-center justify-center',
showDropInstallTip ? 'min-h-0 flex-1' : 'h-full w-full',
)}
>
<div
className={cn(
'flex flex-col items-center',
isIntegrationsCategory ? 'gap-y-6' : 'gap-y-3',
)}
data-step-by-step-tour-target={emptyTarget}
>
<div className="relative z-30 flex h-full items-center justify-center">
<div className="flex h-full w-full items-center justify-center">
<div className="flex flex-col items-center gap-y-3">
<div className="flex flex-col items-center gap-y-3">
<div
className={cn(
'relative -z-10 flex items-center justify-center border-dashed bg-components-card-bg backdrop-blur-md',
isIntegrationsCategory
? 'size-14 rounded-xl border border-divider-regular'
: 'size-14 rounded-xl border border-divider-deep shadow-xl shadow-shadow-shadow-5',
)}
>
{isIntegrationsCategory ? (
<span className="text-text-tertiary">
{isIntegrationsAgentStrategy ? (
<AgentStrategyEmptyIcon />
) : isIntegrationsExtension ? (
<ExtensionEmptyIcon />
) : (
<TriggerEmptyIcon />
)}
</span>
) : (
<Group className="size-5 text-text-tertiary" />
)}
{!isIntegrationsCategory && (
<>
<Line className="absolute top-1/2 -right-px -translate-y-1/2" />
<Line className="absolute top-1/2 -left-px -translate-y-1/2" />
<Line className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-90" />
<Line className="absolute top-full left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-90" />
</>
)}
</div>
<div
className={cn(
isIntegrationsCategory
? 'system-sm-regular text-text-tertiary'
: 'system-md-regular text-text-tertiary',
)}
>
{emptyText}
<div className="relative -z-10 flex size-14 items-center justify-center rounded-xl border border-dashed border-divider-deep bg-components-card-bg shadow-xl shadow-shadow-shadow-5 backdrop-blur-md">
<Group className="size-5 text-text-tertiary" />
<Line className="absolute top-1/2 -right-px -translate-y-1/2" />
<Line className="absolute top-1/2 -left-px -translate-y-1/2" />
<Line className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-90" />
<Line className="absolute top-full left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-90" />
</div>
<div className="system-md-regular text-text-tertiary">{text}</div>
</div>
<div className="flex w-59 flex-col">
<input
@@ -265,41 +155,27 @@ const Empty = ({
accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS}
/>
<div className="flex w-full flex-col gap-y-1">
{installMethods.map(
({ icon: Icon, integrationIcon: IntegrationIcon, text, action }) => (
<Button
key={action}
variant="secondary"
title={text}
className="h-8 w-full justify-start py-2 system-sm-medium"
onClick={() => {
if (action === 'local') fileInputRef.current?.click()
else if (action === 'marketplace')
onSwitchToMarketplace ? onSwitchToMarketplace() : setActiveTab('discover')
else setSelectedAction(action)
}}
>
{isIntegrationsCategory ? (
<IntegrationIcon />
) : (
<Icon className="size-4 text-components-button-secondary-text" />
)}
<span className="min-w-0 flex-1 truncate text-left">{text}</span>
</Button>
),
)}
{installMethods.map(({ icon: Icon, text, action }) => (
<Button
key={action}
variant="secondary"
title={text}
className="h-8 w-full justify-start py-2 system-sm-medium"
onClick={() => {
if (action === 'local') fileInputRef.current?.click()
else if (action === 'marketplace')
onSwitchToMarketplace ? onSwitchToMarketplace() : setActiveTab('discover')
else setSelectedAction(action)
}}
>
<Icon className="size-4 text-components-button-secondary-text" />
<span className="min-w-0 flex-1 truncate text-left">{text}</span>
</Button>
))}
</div>
</div>
</div>
</div>
{showDropInstallTip && (
<div className="flex shrink-0 items-center justify-center gap-2 px-6 py-4 text-text-quaternary">
<DropHintInstallSourceIcon />
<span className="system-xs-regular">
{t(($) => $['installModal.dropIntegrationToInstall'], { ns: 'plugin' })}
</span>
</div>
)}
{selectedAction === 'github' && (
<InstallFromGitHub
installContextCategory={installContextCategory}
@@ -1,7 +1,3 @@
'use client'
import { cn } from '@langgenius/dify-ui/cn'
export const MarketplaceInstallSourceIcon = () => (
<span aria-hidden className="i-custom-vender-plugin-box-sparkle-fill size-4 shrink-0" />
)
@@ -13,7 +9,3 @@ export const GithubInstallSourceIcon = () => (
export const LocalPackageInstallSourceIcon = () => (
<span aria-hidden className="i-custom-vender-solid-files-file-zip size-4 shrink-0" />
)
export const DropHintInstallSourceIcon = ({ className }: { className?: string }) => (
<span aria-hidden className={cn('i-ri-drag-drop-line size-4 shrink-0', className)} />
)
@@ -1,5 +1,6 @@
import type { RefObject } from 'react'
import type { PluginDetail } from '../types'
import type { EmbeddedMarketplaceCategory } from './category-marketplace'
import type { PluginPageContentInset } from './content-inset'
import type { Collection } from '@/app/components/tools/types'
import { Button } from '@langgenius/dify-ui/button'
@@ -16,12 +17,16 @@ import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import IntegrationsToolProviderCard from '@/app/components/integrations/tool-provider-card'
import { BuiltinMarketplacePanel } from '@/app/components/tools/marketplace/builtin-marketplace-panel'
import CategoryEmptyState from './category-empty-state'
import CategoryMarketplacePanel from './category-marketplace-panel'
import List from './list'
type PluginsPanelResultsProps = {
autoLoadNextPage: boolean
canDeletePlugin: boolean
canUpdatePlugin: boolean
categoryEmptyState?: EmbeddedMarketplaceCategory
categoryMarketplace?: EmbeddedMarketplaceCategory
containerRef: RefObject<HTMLDivElement | null>
contentFrameClassName: string
contentInset: PluginPageContentInset
@@ -33,13 +38,14 @@ type PluginsPanelResultsProps = {
hasToolMarketplacePanel: boolean
hasVisibleBuiltinTools: boolean
hasVisiblePlugins: boolean
isAgentStrategyIntegrationPage: boolean
hasEmbeddedMarketplace: boolean
isFetching: boolean
isLastPage: boolean
keywords: string
loadNextPage: () => void
scrollAreaLabel?: string
setCurrentBuiltinToolID: (id: string) => void
showCategoryEmptyState: boolean
tagFilterValue: string[]
}
@@ -47,6 +53,8 @@ const PluginsPanelResults = ({
autoLoadNextPage,
canDeletePlugin,
canUpdatePlugin,
categoryEmptyState,
categoryMarketplace,
containerRef,
contentFrameClassName,
contentInset,
@@ -58,13 +66,14 @@ const PluginsPanelResults = ({
hasToolMarketplacePanel,
hasVisibleBuiltinTools,
hasVisiblePlugins,
isAgentStrategyIntegrationPage,
hasEmbeddedMarketplace,
isFetching,
isLastPage,
keywords,
loadNextPage,
scrollAreaLabel,
setCurrentBuiltinToolID,
showCategoryEmptyState,
tagFilterValue,
}: PluginsPanelResultsProps) => {
const { t } = useTranslation()
@@ -119,8 +128,14 @@ const PluginsPanelResults = ({
role={scrollAreaLabel ? 'region' : undefined}
>
<ScrollAreaContent
className={cn('flex min-h-full flex-col', isAgentStrategyIntegrationPage && 'pt-2')}
className={cn('flex min-h-full flex-col', hasEmbeddedMarketplace && 'pt-1')}
>
{showCategoryEmptyState && categoryEmptyState && (
<CategoryEmptyState
category={categoryEmptyState}
showMarketplaceLink={!!categoryMarketplace}
/>
)}
{(hasVisiblePlugins || hasVisibleBuiltinTools) && (
<List
pluginList={filteredList}
@@ -170,6 +185,13 @@ const PluginsPanelResults = ({
tagFilterValue={tagFilterValue}
/>
)}
{categoryMarketplace && (
<CategoryMarketplacePanel
category={categoryMarketplace}
searchText={keywords}
tags={categoryMarketplace === 'trigger' ? tagFilterValue : []}
/>
)}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
@@ -23,6 +23,7 @@ import {
} from '@/service/use-plugins'
import { usePluginsWithLatestVersion } from '../hooks'
import { PluginCategoryEnum } from '../types'
import { isEmbeddedMarketplaceCategory } from './category-marketplace'
import { pluginPageContentFrameClassNames, pluginPageContentInsetClassNames } from './content-inset'
import { usePluginPageContext } from './context'
import Empty from './empty'
@@ -87,6 +88,7 @@ const PluginsPanel = ({
const isTriggerIntegrationPage = fixedCategory === PluginCategoryEnum.trigger
const isAgentStrategyIntegrationPage = fixedCategory === PluginCategoryEnum.agent
const isExtensionIntegrationPage = fixedCategory === PluginCategoryEnum.extension
const hasEmbeddedMarketplace = isEmbeddedMarketplaceCategory(fixedCategory)
const isIntegrationCategoryPage =
isToolIntegrationPage ||
isTriggerIntegrationPage ||
@@ -197,18 +199,13 @@ const PluginsPanel = ({
const handleHide = () => setCurrentPluginID(undefined)
const handleBuiltinToolHide = () => setCurrentBuiltinToolID(undefined)
const hasToolMarketplacePanel = enableMarketplace && isToolIntegrationPage
const categoryMarketplace =
enableMarketplace && hasEmbeddedMarketplace ? fixedCategory : undefined
const contentPaddingClassName = pluginPageContentInsetClassNames[contentInset]
const contentFrameClassName = cn(
pluginPageContentFrameClassNames[contentInset],
contentPaddingClassName,
)
const emptyVariant = isTriggerIntegrationPage
? 'integrationsTrigger'
: isAgentStrategyIntegrationPage
? 'integrationsAgentStrategy'
: isExtensionIntegrationPage
? 'integrationsExtension'
: 'default'
const scrollAreaLabel = isTriggerIntegrationPage
? t(($) => $['categorySingle.trigger'], { ns: 'plugin' })
: isAgentStrategyIntegrationPage
@@ -256,7 +253,10 @@ const PluginsPanel = ({
{isPluginListLoading && <PluginListSkeleton contentFrameClassName={contentFrameClassName} />}
{!isPluginListLoading && (
<>
{hasVisiblePlugins || hasVisibleBuiltinTools || hasToolMarketplacePanel ? (
{hasVisiblePlugins ||
hasVisibleBuiltinTools ||
hasToolMarketplacePanel ||
hasEmbeddedMarketplace ? (
<PluginsPanelResults
autoLoadNextPage={isIntegrationCategoryPage}
containerRef={containerRef}
@@ -278,7 +278,7 @@ const PluginsPanel = ({
hasToolMarketplacePanel={hasToolMarketplacePanel}
hasVisibleBuiltinTools={hasVisibleBuiltinTools}
hasVisiblePlugins={hasVisiblePlugins}
isAgentStrategyIntegrationPage={isAgentStrategyIntegrationPage}
hasEmbeddedMarketplace={hasEmbeddedMarketplace}
isFetching={isFetching}
isLastPage={isLastPage}
keywords={filters.searchQuery}
@@ -288,6 +288,14 @@ const PluginsPanel = ({
tagFilterValue={filters.tags}
canDeletePlugin={canDeletePlugin}
canUpdatePlugin={canUpdatePlugin}
categoryEmptyState={hasEmbeddedMarketplace ? fixedCategory : undefined}
categoryMarketplace={categoryMarketplace}
showCategoryEmptyState={
hasEmbeddedMarketplace &&
!isFilteringCategory &&
!hasVisiblePlugins &&
!hasVisibleBuiltinTools
}
/>
) : isIntegrationCategorySearchEmpty ? (
<div className={cn('min-h-0 grow bg-components-panel-bg', contentFrameClassName)} />
@@ -297,7 +305,6 @@ const PluginsPanel = ({
contentInset={contentInset}
onSwitchToMarketplace={onSwitchToMarketplace}
installContextCategory={fixedCategory}
variant={emptyVariant}
/>
)}
</>
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "جارٍ تحميل {{packageName}}...",
"installModal.viewDetails": "عرض التفاصيل",
"installPlugin": "تثبيت الإضافة",
"list.emptyInstallFromMarketplace": "يمكنك تثبيت {{category}} من <marketplace>Marketplace</marketplace>.",
"list.noAgentStrategyFound": "لم يتم العثور على استراتيجية وكيل",
"list.noExtensionFound": "لم يتم العثور على ملحق",
"list.noInstalled": "لم يتم تثبيت أي إضافات",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "Das Hochladen von {{packageName}}...",
"installModal.viewDetails": "Details anzeigen",
"installPlugin": "Plugin installieren",
"list.emptyInstallFromMarketplace": "Du kannst {{category}} über den <marketplace>Marketplace</marketplace> installieren.",
"list.noAgentStrategyFound": "Keine Agenten-Strategie gefunden",
"list.noExtensionFound": "Keine Erweiterung gefunden",
"list.noInstalled": "Keine Plugins installiert",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "Uploading {{packageName}}...",
"installModal.viewDetails": "View Details",
"installPlugin": "Install integration",
"list.emptyInstallFromMarketplace": "You can install {{category}} from the <marketplace>Marketplace</marketplace>.",
"list.noAgentStrategyFound": "No Agent Strategy found",
"list.noExtensionFound": "No Extension found",
"list.noInstalled": "No integrations installed",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "Subiendo {{packageName}}...",
"installModal.viewDetails": "Ver detalles",
"installPlugin": "Instalar plugin",
"list.emptyInstallFromMarketplace": "Puedes instalar {{category}} desde el <marketplace>Marketplace</marketplace>.",
"list.noAgentStrategyFound": "No se encontró ninguna estrategia de agente",
"list.noExtensionFound": "No se encontró ninguna extensión",
"list.noInstalled": "No hay plugins instalados",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "آپلود {{packageName}}...",
"installModal.viewDetails": "نمایش جزئیات",
"installPlugin": "افزونه را نصب کنید",
"list.emptyInstallFromMarketplace": "می‌توانید {{category}} را از <marketplace>Marketplace</marketplace> نصب کنید.",
"list.noAgentStrategyFound": "هیچ استراتژی عاملی یافت نشد",
"list.noExtensionFound": "هیچ افزونه‌ای یافت نشد",
"list.noInstalled": "هیچ افزونه ای نصب نشده است",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "Téléchargement de {{packageName}}...",
"installModal.viewDetails": "Voir les détails",
"installPlugin": "Installer le plugin",
"list.emptyInstallFromMarketplace": "Vous pouvez installer {{category}} depuis le <marketplace>Marketplace</marketplace>.",
"list.noAgentStrategyFound": "Aucune stratégie dagent trouvée",
"list.noExtensionFound": "Aucune extension trouvée",
"list.noInstalled": "Aucun plugin installé",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "{{packageName}} अपलोड हो रहा है...",
"installModal.viewDetails": "विवरण देखें",
"installPlugin": "प्लगइन स्थापित करें",
"list.emptyInstallFromMarketplace": "आप <marketplace>Marketplace</marketplace> से {{category}} इंस्टॉल कर सकते हैं।",
"list.noAgentStrategyFound": "कोई एजेंट रणनीति नहीं मिली",
"list.noExtensionFound": "कोई एक्सटेंशन नहीं मिला",
"list.noInstalled": "कोई प्लगइन स्थापित नहीं हैं",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "Mengunggah {{packageName}}...",
"installModal.viewDetails": "Lihat Detail",
"installPlugin": "Instal plugin",
"list.emptyInstallFromMarketplace": "Anda dapat menginstal {{category}} dari <marketplace>Marketplace</marketplace>.",
"list.noAgentStrategyFound": "Tidak ada Strategi Agen yang ditemukan",
"list.noExtensionFound": "Tidak ada Ekstensi yang ditemukan",
"list.noInstalled": "Tidak ada plugin yang diinstal",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "Caricamento di {{packageName}}...",
"installModal.viewDetails": "Visualizza dettagli",
"installPlugin": "Installa il plugin",
"list.emptyInstallFromMarketplace": "Puoi installare {{category}} dal <marketplace>Marketplace</marketplace>.",
"list.noAgentStrategyFound": "Nessuna strategia agente trovata",
"list.noExtensionFound": "Nessuna estensione trovata",
"list.noInstalled": "Nessun plug-in installato",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "{{packageName}}をアップロード中...",
"installModal.viewDetails": "詳細を見る",
"installPlugin": "プラグインをインストール",
"list.emptyInstallFromMarketplace": "<marketplace>Marketplace</marketplace> から {{category}} をインストールできます。",
"list.noAgentStrategyFound": "エージェント戦略が見つかりません",
"list.noExtensionFound": "拡張機能が見つかりません",
"list.noInstalled": "プラグインはインストールされていません",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "{{packageName}} 업로드 중...",
"installModal.viewDetails": "세부 정보 보기",
"installPlugin": "플러그인 설치",
"list.emptyInstallFromMarketplace": "<marketplace>Marketplace</marketplace>에서 {{category}}을(를) 설치할 수 있습니다.",
"list.noAgentStrategyFound": "에이전트 전략을 찾을 수 없습니다.",
"list.noExtensionFound": "확장을 찾을 수 없습니다.",
"list.noInstalled": "설치된 플러그인이 없습니다.",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "ກຳລັງອັບໂຫຼດ {{packageName}}...",
"installModal.viewDetails": "ເບິ່ງລາຍລະອຽດ",
"installPlugin": "ຕິດຕັ້ງການເຊື່ອມຕໍ່",
"list.emptyInstallFromMarketplace": "ທ່ານສາມາດຕິດຕັ້ງ {{category}} ຈາກ <marketplace>Marketplace</marketplace> ໄດ້.",
"list.noAgentStrategyFound": "ບໍ່ພົບກົນລະຍຸດຕົວແທນ",
"list.noExtensionFound": "ບໍ່ພົບສ່ວນຂະຫຍຍ",
"list.noInstalled": "ບໍ່ມີການເຊື່ອມຕໍ່ທີ່ຕິດຕັ້ງໄວ້",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "Uploading {{packageName}}...",
"installModal.viewDetails": "Details bekijken",
"installPlugin": "Plugin installeren",
"list.emptyInstallFromMarketplace": "Je kunt {{category}} installeren via de <marketplace>Marketplace</marketplace>.",
"list.noAgentStrategyFound": "Geen Agent Strategy gevonden",
"list.noExtensionFound": "Geen Extension gevonden",
"list.noInstalled": "No plugins installed",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "Przesyłanie {{packageName}}...",
"installModal.viewDetails": "Pokaż szczegóły",
"installPlugin": "Zainstaluj wtyczkę",
"list.emptyInstallFromMarketplace": "Możesz zainstalować {{category}} z <marketplace>Marketplace</marketplace>.",
"list.noAgentStrategyFound": "Nie znaleziono strategii agenta",
"list.noExtensionFound": "Nie znaleziono rozszerzenia",
"list.noInstalled": "Brak zainstalowanych wtyczek",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "Carregando {{packageName}} ...",
"installModal.viewDetails": "Ver detalhes",
"installPlugin": "Instale o plugin",
"list.emptyInstallFromMarketplace": "Você pode instalar {{category}} do <marketplace>Marketplace</marketplace>.",
"list.noAgentStrategyFound": "Nenhuma Estratégia do Agente encontrada",
"list.noExtensionFound": "Nenhuma Extensão encontrada",
"list.noInstalled": "Nenhum plug-in instalado",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "Încărcarea {{packageName}}...",
"installModal.viewDetails": "Vezi detalii",
"installPlugin": "Instalează pluginul",
"list.emptyInstallFromMarketplace": "Poți instala {{category}} din <marketplace>Marketplace</marketplace>.",
"list.noAgentStrategyFound": "Nu s-a găsit nicio strategie de agent",
"list.noExtensionFound": "Nu s-a găsit nicio extensie",
"list.noInstalled": "Nu sunt instalate plugin-uri",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "Загрузка {{packageName}}...",
"installModal.viewDetails": "Подробнее",
"installPlugin": "Установка плагина",
"list.emptyInstallFromMarketplace": "Вы можете установить {{category}} из <marketplace>Marketplace</marketplace>.",
"list.noAgentStrategyFound": "Агентская стратегия не найдена",
"list.noExtensionFound": "Расширение не найдено",
"list.noInstalled": "Плагины не установлены",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "Nalagam {{packageName}}...",
"installModal.viewDetails": "Ogled podrobnosti",
"installPlugin": "Namestite vtičnik",
"list.emptyInstallFromMarketplace": "{{category}} lahko namestite iz <marketplace>Marketplace</marketplace>.",
"list.noAgentStrategyFound": "Nobena strategija agenta ni bila najdena",
"list.noExtensionFound": "Nobena razširitev ni bila najdena",
"list.noInstalled": "Nobeni vtičniki niso nameščeni.",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "กําลังอัปโหลด {{packageName}}...",
"installModal.viewDetails": "ดูรายละเอียด",
"installPlugin": "ติดตั้งปลั๊กอิน",
"list.emptyInstallFromMarketplace": "คุณสามารถติดตั้ง {{category}} จาก <marketplace>Marketplace</marketplace> ได้",
"list.noAgentStrategyFound": "ไม่พบกลยุทธ์ตัวแทน",
"list.noExtensionFound": "ไม่พบส่วนขยาย",
"list.noInstalled": "ไม่ได้ติดตั้งปลั๊กอิน",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "{{packageName}} yükleniyor...",
"installModal.viewDetails": "Ayrıntıları Görüntüle",
"installPlugin": "Eklentiyi yükle",
"list.emptyInstallFromMarketplace": "{{category}} öğesini <marketplace>Marketplace</marketplace> üzerinden yükleyebilirsiniz.",
"list.noAgentStrategyFound": "Ajan Stratejisi bulunamadı",
"list.noExtensionFound": "Uzantı bulunamadı",
"list.noInstalled": "Yüklü eklenti yok",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "Завантаження {{packageName}}...",
"installModal.viewDetails": "Переглянути деталі",
"installPlugin": "Встановити плагін",
"list.emptyInstallFromMarketplace": "Ви можете встановити {{category}} з <marketplace>Marketplace</marketplace>.",
"list.noAgentStrategyFound": "Стратегію агента не знайдено",
"list.noExtensionFound": "Розширення не знайдено",
"list.noInstalled": "Плагіни не встановлено",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "Tải lên {{packageName}}...",
"installModal.viewDetails": "Xem chi tiết",
"installPlugin": "Cài đặt plugin",
"list.emptyInstallFromMarketplace": "Bạn có thể cài đặt {{category}} từ <marketplace>Marketplace</marketplace>.",
"list.noAgentStrategyFound": "Không tìm thấy Chiến lược đại lý",
"list.noExtensionFound": "Không tìm thấy Phần mở rộng",
"list.noInstalled": "Không có plugin nào được cài đặt",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "上传 {{packageName}} 中...",
"installModal.viewDetails": "查看详情",
"installPlugin": "安装集成",
"list.emptyInstallFromMarketplace": "你可以从 <marketplace>Marketplace</marketplace> 安装{{category}}。",
"list.noAgentStrategyFound": "未找到 Agent 策略",
"list.noExtensionFound": "未找到扩展",
"list.noInstalled": "无已安装的集成",
+1
View File
@@ -212,6 +212,7 @@
"installModal.uploadingPackage": "正在上傳 {{packageName}}...",
"installModal.viewDetails": "查看詳情",
"installPlugin": "安裝插件",
"list.emptyInstallFromMarketplace": "你可以從 <marketplace>Marketplace</marketplace> 安裝{{category}}。",
"list.noAgentStrategyFound": "未找到代理策略",
"list.noExtensionFound": "未找到擴展",
"list.noInstalled": "未安裝插件",