mirror of
https://github.com/langgenius/dify.git
synced 2026-09-19 02:07:44 +08:00
feat(web): embed marketplace in integration categories (#40744)
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"installModal.uploadingPackage": "جارٍ تحميل {{packageName}}...",
|
||||
"installModal.viewDetails": "عرض التفاصيل",
|
||||
"installPlugin": "تثبيت الإضافة",
|
||||
"list.emptyInstallFromMarketplace": "يمكنك تثبيت {{category}} من <marketplace>Marketplace</marketplace>.",
|
||||
"list.noAgentStrategyFound": "لم يتم العثور على استراتيجية وكيل",
|
||||
"list.noExtensionFound": "لم يتم العثور على ملحق",
|
||||
"list.noInstalled": "لم يتم تثبيت أي إضافات",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"installModal.uploadingPackage": "آپلود {{packageName}}...",
|
||||
"installModal.viewDetails": "نمایش جزئیات",
|
||||
"installPlugin": "افزونه را نصب کنید",
|
||||
"list.emptyInstallFromMarketplace": "میتوانید {{category}} را از <marketplace>Marketplace</marketplace> نصب کنید.",
|
||||
"list.noAgentStrategyFound": "هیچ استراتژی عاملی یافت نشد",
|
||||
"list.noExtensionFound": "هیچ افزونهای یافت نشد",
|
||||
"list.noInstalled": "هیچ افزونه ای نصب نشده است",
|
||||
|
||||
@@ -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 d’agent trouvée",
|
||||
"list.noExtensionFound": "Aucune extension trouvée",
|
||||
"list.noInstalled": "Aucun plugin installé",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"installModal.uploadingPackage": "{{packageName}} अपलोड हो रहा है...",
|
||||
"installModal.viewDetails": "विवरण देखें",
|
||||
"installPlugin": "प्लगइन स्थापित करें",
|
||||
"list.emptyInstallFromMarketplace": "आप <marketplace>Marketplace</marketplace> से {{category}} इंस्टॉल कर सकते हैं।",
|
||||
"list.noAgentStrategyFound": "कोई एजेंट रणनीति नहीं मिली",
|
||||
"list.noExtensionFound": "कोई एक्सटेंशन नहीं मिला",
|
||||
"list.noInstalled": "कोई प्लगइन स्थापित नहीं हैं",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"installModal.uploadingPackage": "{{packageName}}をアップロード中...",
|
||||
"installModal.viewDetails": "詳細を見る",
|
||||
"installPlugin": "プラグインをインストール",
|
||||
"list.emptyInstallFromMarketplace": "<marketplace>Marketplace</marketplace> から {{category}} をインストールできます。",
|
||||
"list.noAgentStrategyFound": "エージェント戦略が見つかりません",
|
||||
"list.noExtensionFound": "拡張機能が見つかりません",
|
||||
"list.noInstalled": "プラグインはインストールされていません",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"installModal.uploadingPackage": "{{packageName}} 업로드 중...",
|
||||
"installModal.viewDetails": "세부 정보 보기",
|
||||
"installPlugin": "플러그인 설치",
|
||||
"list.emptyInstallFromMarketplace": "<marketplace>Marketplace</marketplace>에서 {{category}}을(를) 설치할 수 있습니다.",
|
||||
"list.noAgentStrategyFound": "에이전트 전략을 찾을 수 없습니다.",
|
||||
"list.noExtensionFound": "확장을 찾을 수 없습니다.",
|
||||
"list.noInstalled": "설치된 플러그인이 없습니다.",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"installModal.uploadingPackage": "ກຳລັງອັບໂຫຼດ {{packageName}}...",
|
||||
"installModal.viewDetails": "ເບິ່ງລາຍລະອຽດ",
|
||||
"installPlugin": "ຕິດຕັ້ງການເຊື່ອມຕໍ່",
|
||||
"list.emptyInstallFromMarketplace": "ທ່ານສາມາດຕິດຕັ້ງ {{category}} ຈາກ <marketplace>Marketplace</marketplace> ໄດ້.",
|
||||
"list.noAgentStrategyFound": "ບໍ່ພົບກົນລະຍຸດຕົວແທນ",
|
||||
"list.noExtensionFound": "ບໍ່ພົບສ່ວນຂະຫຍຍ",
|
||||
"list.noInstalled": "ບໍ່ມີການເຊື່ອມຕໍ່ທີ່ຕິດຕັ້ງໄວ້",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"installModal.uploadingPackage": "Загрузка {{packageName}}...",
|
||||
"installModal.viewDetails": "Подробнее",
|
||||
"installPlugin": "Установка плагина",
|
||||
"list.emptyInstallFromMarketplace": "Вы можете установить {{category}} из <marketplace>Marketplace</marketplace>.",
|
||||
"list.noAgentStrategyFound": "Агентская стратегия не найдена",
|
||||
"list.noExtensionFound": "Расширение не найдено",
|
||||
"list.noInstalled": "Плагины не установлены",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"installModal.uploadingPackage": "กําลังอัปโหลด {{packageName}}...",
|
||||
"installModal.viewDetails": "ดูรายละเอียด",
|
||||
"installPlugin": "ติดตั้งปลั๊กอิน",
|
||||
"list.emptyInstallFromMarketplace": "คุณสามารถติดตั้ง {{category}} จาก <marketplace>Marketplace</marketplace> ได้",
|
||||
"list.noAgentStrategyFound": "ไม่พบกลยุทธ์ตัวแทน",
|
||||
"list.noExtensionFound": "ไม่พบส่วนขยาย",
|
||||
"list.noInstalled": "ไม่ได้ติดตั้งปลั๊กอิน",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"installModal.uploadingPackage": "Завантаження {{packageName}}...",
|
||||
"installModal.viewDetails": "Переглянути деталі",
|
||||
"installPlugin": "Встановити плагін",
|
||||
"list.emptyInstallFromMarketplace": "Ви можете встановити {{category}} з <marketplace>Marketplace</marketplace>.",
|
||||
"list.noAgentStrategyFound": "Стратегію агента не знайдено",
|
||||
"list.noExtensionFound": "Розширення не знайдено",
|
||||
"list.noInstalled": "Плагіни не встановлено",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"installModal.uploadingPackage": "上传 {{packageName}} 中...",
|
||||
"installModal.viewDetails": "查看详情",
|
||||
"installPlugin": "安装集成",
|
||||
"list.emptyInstallFromMarketplace": "你可以从 <marketplace>Marketplace</marketplace> 安装{{category}}。",
|
||||
"list.noAgentStrategyFound": "未找到 Agent 策略",
|
||||
"list.noExtensionFound": "未找到扩展",
|
||||
"list.noInstalled": "无已安装的集成",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"installModal.uploadingPackage": "正在上傳 {{packageName}}...",
|
||||
"installModal.viewDetails": "查看詳情",
|
||||
"installPlugin": "安裝插件",
|
||||
"list.emptyInstallFromMarketplace": "你可以從 <marketplace>Marketplace</marketplace> 安裝{{category}}。",
|
||||
"list.noAgentStrategyFound": "未找到代理策略",
|
||||
"list.noExtensionFound": "未找到擴展",
|
||||
"list.noInstalled": "未安裝插件",
|
||||
|
||||
Reference in New Issue
Block a user