mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
chore: show package installed status in marketplace page (#39146)
This commit is contained in:
parent
e9c0e9de9e
commit
f4ad6ce978
@ -13,7 +13,11 @@ import {
|
|||||||
useInvalidDataSourceListAuth,
|
useInvalidDataSourceListAuth,
|
||||||
} from '@/service/use-datasource'
|
} from '@/service/use-datasource'
|
||||||
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
||||||
import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
import {
|
||||||
|
useCheckInstalled,
|
||||||
|
useInstalledPluginList,
|
||||||
|
useInvalidateInstalledPluginList,
|
||||||
|
} from '@/service/use-plugins'
|
||||||
import { useDataSourceAuthUpdate, useMarketplaceAllPlugins } from '../hooks'
|
import { useDataSourceAuthUpdate, useMarketplaceAllPlugins } from '../hooks'
|
||||||
import DataSourcePage from '../index'
|
import DataSourcePage from '../index'
|
||||||
|
|
||||||
@ -42,6 +46,7 @@ vi.mock('@/service/use-pipeline', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/service/use-plugins', () => ({
|
vi.mock('@/service/use-plugins', () => ({
|
||||||
|
useCheckInstalled: vi.fn(),
|
||||||
useInstalledPluginList: vi.fn(),
|
useInstalledPluginList: vi.fn(),
|
||||||
useInvalidateInstalledPluginList: vi.fn(),
|
useInvalidateInstalledPluginList: vi.fn(),
|
||||||
}))
|
}))
|
||||||
@ -186,6 +191,11 @@ describe('DataSourcePage Component', () => {
|
|||||||
vi.mocked(useInstalledPluginList).mockReturnValue({
|
vi.mocked(useInstalledPluginList).mockReturnValue({
|
||||||
data: { plugins: [], total: 0 },
|
data: { plugins: [], total: 0 },
|
||||||
} as unknown as ReturnType<typeof useInstalledPluginList>)
|
} as unknown as ReturnType<typeof useInstalledPluginList>)
|
||||||
|
vi.mocked(useCheckInstalled).mockReturnValue({
|
||||||
|
data: { plugins: [] },
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
} as unknown as ReturnType<typeof useCheckInstalled>)
|
||||||
vi.mocked(usePluginsWithLatestVersion).mockImplementation(
|
vi.mocked(usePluginsWithLatestVersion).mockImplementation(
|
||||||
(plugins = []) => plugins as PluginDetail[],
|
(plugins = []) => plugins as PluginDetail[],
|
||||||
)
|
)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { PluginCategoryEnum } from '../../../types'
|
|||||||
|
|
||||||
// Mock invalidation / refresh functions
|
// Mock invalidation / refresh functions
|
||||||
const mockInvalidateInstalledPluginList = vi.fn()
|
const mockInvalidateInstalledPluginList = vi.fn()
|
||||||
|
const mockInvalidateCheckInstalled = vi.fn()
|
||||||
const mockRefetchLLMModelList = vi.fn()
|
const mockRefetchLLMModelList = vi.fn()
|
||||||
const mockRefetchEmbeddingModelList = vi.fn()
|
const mockRefetchEmbeddingModelList = vi.fn()
|
||||||
const mockRefetchRerankModelList = vi.fn()
|
const mockRefetchRerankModelList = vi.fn()
|
||||||
@ -20,6 +21,7 @@ const mockInvalidateAllTriggerPlugins = vi.fn()
|
|||||||
const mockInvalidateRAGRecommendedPlugins = vi.fn()
|
const mockInvalidateRAGRecommendedPlugins = vi.fn()
|
||||||
|
|
||||||
vi.mock('@/service/use-plugins', () => ({
|
vi.mock('@/service/use-plugins', () => ({
|
||||||
|
useInvalidateCheckInstalled: () => mockInvalidateCheckInstalled,
|
||||||
useInvalidateInstalledPluginList: () => mockInvalidateInstalledPluginList,
|
useInvalidateInstalledPluginList: () => mockInvalidateInstalledPluginList,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -80,12 +82,13 @@ describe('useRefreshPluginList', () => {
|
|||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should always invalidate installed plugin list', () => {
|
it('should always invalidate installed plugin list and installation checks', () => {
|
||||||
const { result } = renderHook(() => useRefreshPluginList())
|
const { result } = renderHook(() => useRefreshPluginList())
|
||||||
|
|
||||||
result.current.refreshPluginList()
|
result.current.refreshPluginList()
|
||||||
|
|
||||||
expect(mockInvalidateInstalledPluginList).toHaveBeenCalledTimes(1)
|
expect(mockInvalidateInstalledPluginList).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockInvalidateCheckInstalled).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should refresh tool providers for tool category manifest', () => {
|
it('should refresh tool providers for tool category manifest', () => {
|
||||||
|
|||||||
@ -7,7 +7,10 @@ import {
|
|||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { useInvalidDataSourceListAuth } from '@/service/use-datasource'
|
import { useInvalidDataSourceListAuth } from '@/service/use-datasource'
|
||||||
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
||||||
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
import {
|
||||||
|
useInvalidateCheckInstalled,
|
||||||
|
useInvalidateInstalledPluginList,
|
||||||
|
} from '@/service/use-plugins'
|
||||||
import { useInvalidateStrategyProviders } from '@/service/use-strategy'
|
import { useInvalidateStrategyProviders } from '@/service/use-strategy'
|
||||||
import {
|
import {
|
||||||
useInvalidateAllBuiltInTools,
|
useInvalidateAllBuiltInTools,
|
||||||
@ -31,6 +34,7 @@ const SYSTEM_MODEL_TYPES = [
|
|||||||
|
|
||||||
const useRefreshPluginList = () => {
|
const useRefreshPluginList = () => {
|
||||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||||
|
const invalidateCheckInstalled = useInvalidateCheckInstalled()
|
||||||
const { mutate: refetchLLMModelList } = useModelList(ModelTypeEnum.textGeneration)
|
const { mutate: refetchLLMModelList } = useModelList(ModelTypeEnum.textGeneration)
|
||||||
const { mutate: refetchEmbeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
|
const { mutate: refetchEmbeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
|
||||||
const { mutate: refetchRerankModelList } = useModelList(ModelTypeEnum.rerank)
|
const { mutate: refetchRerankModelList } = useModelList(ModelTypeEnum.rerank)
|
||||||
@ -57,6 +61,7 @@ const useRefreshPluginList = () => {
|
|||||||
) => {
|
) => {
|
||||||
// installed list
|
// installed list
|
||||||
invalidateInstalledPluginList()
|
invalidateInstalledPluginList()
|
||||||
|
invalidateCheckInstalled()
|
||||||
|
|
||||||
// tool page, tool select
|
// tool page, tool select
|
||||||
if ((manifest && PluginCategoryEnum.tool.includes(manifest.category)) || refreshAllType) {
|
if ((manifest && PluginCategoryEnum.tool.includes(manifest.category)) || refreshAllType) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { ComponentProps } from 'react'
|
import type { ComponentProps } from 'react'
|
||||||
import type { Plugin } from '@/app/components/plugins/types'
|
import type { Plugin } from '@/app/components/plugins/types'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
import { ThemeProvider } from 'next-themes'
|
import { ThemeProvider } from 'next-themes'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||||
@ -104,6 +105,17 @@ describe('CardWrapper', () => {
|
|||||||
).toBeInTheDocument()
|
).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('shows a disabled installed action and prevents another installation', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
renderCardWrapper({ showInstallButton: true, isInstalled: true })
|
||||||
|
|
||||||
|
const installedButton = screen.getByRole('button', { name: 'plugin.task.installed' })
|
||||||
|
expect(installedButton).toBeDisabled()
|
||||||
|
|
||||||
|
await user.click(installedButton)
|
||||||
|
expect(screen.queryByTestId('install-modal')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('opens marketplace detail from the detail action', () => {
|
it('opens marketplace detail from the detail action', () => {
|
||||||
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
|
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@ vi.mock('#i18n', async () => {
|
|||||||
'plugin.marketplace.noPluginFound': 'No plugins found',
|
'plugin.marketplace.noPluginFound': 'No plugins found',
|
||||||
'plugin.detailPanel.operation.install': 'Install',
|
'plugin.detailPanel.operation.install': 'Install',
|
||||||
'plugin.detailPanel.operation.detail': 'Detail',
|
'plugin.detailPanel.operation.detail': 'Detail',
|
||||||
|
'plugin.task.installed': 'Installed',
|
||||||
}
|
}
|
||||||
return translations[fullKey] || key
|
return translations[fullKey] || key
|
||||||
}),
|
}),
|
||||||
@ -50,6 +51,19 @@ vi.mock('../../state', () => ({
|
|||||||
useMarketplaceData: () => mockMarketplaceData,
|
useMarketplaceData: () => mockMarketplaceData,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const mockUseCheckInstalled = vi.hoisted(() => vi.fn())
|
||||||
|
vi.mock('@/app/components/plugins/install-plugin/hooks/use-check-installed', () => ({
|
||||||
|
default: mockUseCheckInstalled,
|
||||||
|
}))
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockUseCheckInstalled.mockReturnValue({
|
||||||
|
installedInfo: undefined,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('../../atoms', () => ({
|
vi.mock('../../atoms', () => ({
|
||||||
useMarketplaceMoreClick: () => mockMoreClick,
|
useMarketplaceMoreClick: () => mockMoreClick,
|
||||||
}))
|
}))
|
||||||
@ -991,6 +1005,64 @@ describe('CardWrapper (via List integration)', () => {
|
|||||||
expect(screen.getByText('Detail')).toBeInTheDocument()
|
expect(screen.getByText('Detail')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should render installed state for every card with an installed plugin id', () => {
|
||||||
|
const installedPlugin = createMockPlugin({
|
||||||
|
name: 'installed-plugin',
|
||||||
|
plugin_id: 'plugin-a',
|
||||||
|
})
|
||||||
|
const duplicateInstalledPlugin = createMockPlugin({
|
||||||
|
name: 'installed-plugin-duplicate',
|
||||||
|
plugin_id: 'plugin-a',
|
||||||
|
})
|
||||||
|
const uninstalledPlugin = createMockPlugin({
|
||||||
|
name: 'uninstalled-plugin',
|
||||||
|
plugin_id: 'plugin-b',
|
||||||
|
})
|
||||||
|
mockUseCheckInstalled.mockReturnValue({
|
||||||
|
installedInfo: {
|
||||||
|
'plugin-a': {
|
||||||
|
installedId: 'installation-a',
|
||||||
|
installedVersion: '1.0.0',
|
||||||
|
uniqueIdentifier: 'plugin-a@1.0.0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
render(
|
||||||
|
<List
|
||||||
|
marketplaceCollections={[]}
|
||||||
|
marketplaceCollectionPluginsMap={{}}
|
||||||
|
plugins={[installedPlugin, duplicateInstalledPlugin, uninstalledPlugin]}
|
||||||
|
showInstallButton={true}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getAllByRole('button', { name: 'Installed' })).toHaveLength(2)
|
||||||
|
expect(screen.getByRole('button', { name: 'Install' })).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should keep install available while installation status is pending', () => {
|
||||||
|
const plugin = createMockPlugin({ name: 'pending-status-plugin' })
|
||||||
|
mockUseCheckInstalled.mockReturnValue({
|
||||||
|
installedInfo: undefined,
|
||||||
|
isLoading: true,
|
||||||
|
error: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
render(
|
||||||
|
<List
|
||||||
|
marketplaceCollections={[]}
|
||||||
|
marketplaceCollectionPluginsMap={{}}
|
||||||
|
plugins={[plugin]}
|
||||||
|
showInstallButton={true}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByRole('button', { name: 'Install' })).not.toHaveAttribute('aria-disabled')
|
||||||
|
})
|
||||||
|
|
||||||
it('should call showInstallFromMarketplace when install button is clicked', () => {
|
it('should call showInstallFromMarketplace when install button is clicked', () => {
|
||||||
const plugin = createMockPlugin({ name: 'click-test-plugin' })
|
const plugin = createMockPlugin({ name: 'click-test-plugin' })
|
||||||
|
|
||||||
|
|||||||
@ -16,8 +16,13 @@ import { getPluginLinkInMarketplace } from '../utils'
|
|||||||
type CardWrapperProps = {
|
type CardWrapperProps = {
|
||||||
plugin: Plugin
|
plugin: Plugin
|
||||||
showInstallButton?: boolean
|
showInstallButton?: boolean
|
||||||
|
isInstalled?: boolean
|
||||||
}
|
}
|
||||||
const CardWrapperComponent = ({ plugin, showInstallButton }: CardWrapperProps) => {
|
const CardWrapperComponent = ({
|
||||||
|
plugin,
|
||||||
|
showInstallButton,
|
||||||
|
isInstalled = false,
|
||||||
|
}: CardWrapperProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const [
|
const [
|
||||||
@ -68,11 +73,14 @@ const CardWrapperComponent = ({ plugin, showInstallButton }: CardWrapperProps) =
|
|||||||
/>
|
/>
|
||||||
<div className="pointer-events-none absolute right-[-0.5px] bottom-[-0.5px] left-[-0.5px] z-10 flex items-center gap-2 rounded-b-xl bg-linear-to-t from-components-panel-on-panel-item-bg-hover from-[60%] to-background-gradient-mask-transparent px-4 pt-8 pb-4 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100">
|
<div className="pointer-events-none absolute right-[-0.5px] bottom-[-0.5px] left-[-0.5px] z-10 flex items-center gap-2 rounded-b-xl bg-linear-to-t from-components-panel-on-panel-item-bg-hover from-[60%] to-background-gradient-mask-transparent px-4 pt-8 pb-4 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant={isInstalled ? 'secondary' : 'primary'}
|
||||||
className="min-w-0 flex-1 shadow-md"
|
className="min-w-0 flex-1 shadow-md"
|
||||||
onClick={showInstallFromMarketplace}
|
disabled={isInstalled}
|
||||||
|
onClick={isInstalled ? undefined : showInstallFromMarketplace}
|
||||||
>
|
>
|
||||||
{t(($) => $['detailPanel.operation.install'], { ns: 'plugin' })}
|
{isInstalled
|
||||||
|
? t(($) => $['task.installed'], { ns: 'plugin' })
|
||||||
|
: t(($) => $['detailPanel.operation.install'], { ns: 'plugin' })}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="min-w-0 flex-1 gap-0.5 shadow-xs backdrop-blur-[5px]"
|
className="min-w-0 flex-1 gap-0.5 shadow-xs backdrop-blur-[5px]"
|
||||||
|
|||||||
@ -2,7 +2,10 @@
|
|||||||
import type { MarketplaceCollection, SearchParamsFromCollection } from '@dify/contracts/marketplace'
|
import type { MarketplaceCollection, SearchParamsFromCollection } from '@dify/contracts/marketplace'
|
||||||
import type { Plugin } from '../../types'
|
import type { Plugin } from '../../types'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
|
import { useMemo } from 'react'
|
||||||
import { PluginInstallPermissionProviderGuard } from '@/app/components/plugins/install-plugin/components/plugin-install-permission-provider'
|
import { PluginInstallPermissionProviderGuard } from '@/app/components/plugins/install-plugin/components/plugin-install-permission-provider'
|
||||||
|
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||||
|
import { useOptionalPluginInstallPermission } from '@/app/components/plugins/install-plugin/hooks/use-plugin-install-permission'
|
||||||
import Empty from '../empty'
|
import Empty from '../empty'
|
||||||
import CardWrapper from './card-wrapper'
|
import CardWrapper from './card-wrapper'
|
||||||
import ListWithCollection from './list-with-collection'
|
import ListWithCollection from './list-with-collection'
|
||||||
@ -27,6 +30,31 @@ const List = ({
|
|||||||
emptyClassName,
|
emptyClassName,
|
||||||
onCollectionMoreClick,
|
onCollectionMoreClick,
|
||||||
}: ListProps) => {
|
}: ListProps) => {
|
||||||
|
const { canInstallPlugin } = useOptionalPluginInstallPermission()
|
||||||
|
const pluginIds = useMemo(() => {
|
||||||
|
const ids = new Set<string>()
|
||||||
|
const addPluginId = (plugin: Plugin) => ids.add(plugin.plugin_id)
|
||||||
|
|
||||||
|
if (plugins) plugins.forEach(addPluginId)
|
||||||
|
else
|
||||||
|
Object.values(marketplaceCollectionPluginsMap).forEach((collectionPlugins) => {
|
||||||
|
collectionPlugins.forEach(addPluginId)
|
||||||
|
})
|
||||||
|
|
||||||
|
return [...ids].sort()
|
||||||
|
}, [marketplaceCollectionPluginsMap, plugins])
|
||||||
|
|
||||||
|
const shouldCheckInstalled =
|
||||||
|
!!showInstallButton && canInstallPlugin && !cardRender && pluginIds.length > 0
|
||||||
|
const { installedInfo } = useCheckInstalled({
|
||||||
|
pluginIds,
|
||||||
|
enabled: shouldCheckInstalled,
|
||||||
|
})
|
||||||
|
const installedPluginIds = useMemo(
|
||||||
|
() => new Set(Object.keys(installedInfo ?? {})),
|
||||||
|
[installedInfo],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PluginInstallPermissionProviderGuard canInstallPlugin={!!showInstallButton}>
|
<PluginInstallPermissionProviderGuard canInstallPlugin={!!showInstallButton}>
|
||||||
{!plugins && (
|
{!plugins && (
|
||||||
@ -37,6 +65,7 @@ const List = ({
|
|||||||
cardContainerClassName={cardContainerClassName}
|
cardContainerClassName={cardContainerClassName}
|
||||||
cardRender={cardRender}
|
cardRender={cardRender}
|
||||||
onCollectionMoreClick={onCollectionMoreClick}
|
onCollectionMoreClick={onCollectionMoreClick}
|
||||||
|
installedPluginIds={installedPluginIds}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{plugins && !!plugins.length && (
|
{plugins && !!plugins.length && (
|
||||||
@ -49,6 +78,7 @@ const List = ({
|
|||||||
key={`${plugin.org}/${plugin.name}`}
|
key={`${plugin.org}/${plugin.name}`}
|
||||||
plugin={plugin}
|
plugin={plugin}
|
||||||
showInstallButton={showInstallButton}
|
showInstallButton={showInstallButton}
|
||||||
|
isInstalled={installedPluginIds.has(plugin.plugin_id)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -38,18 +38,22 @@ type ListWithCollectionProps = {
|
|||||||
cardContainerClassName?: string
|
cardContainerClassName?: string
|
||||||
cardRender?: (plugin: Plugin) => React.JSX.Element | null
|
cardRender?: (plugin: Plugin) => React.JSX.Element | null
|
||||||
onCollectionMoreClick?: (searchParams?: SearchParamsFromCollection) => void
|
onCollectionMoreClick?: (searchParams?: SearchParamsFromCollection) => void
|
||||||
|
installedPluginIds?: ReadonlySet<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginCardProps = {
|
type PluginCardProps = {
|
||||||
plugin: Plugin
|
plugin: Plugin
|
||||||
showInstallButton?: boolean
|
showInstallButton?: boolean
|
||||||
cardRender?: (plugin: Plugin) => React.JSX.Element | null
|
cardRender?: (plugin: Plugin) => React.JSX.Element | null
|
||||||
|
isInstalled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const PluginCard = ({ plugin, showInstallButton, cardRender }: PluginCardProps) => {
|
const PluginCard = ({ plugin, showInstallButton, cardRender, isInstalled }: PluginCardProps) => {
|
||||||
if (cardRender) return cardRender(plugin)
|
if (cardRender) return cardRender(plugin)
|
||||||
|
|
||||||
return <CardWrapper plugin={plugin} showInstallButton={showInstallButton} />
|
return (
|
||||||
|
<CardWrapper plugin={plugin} showInstallButton={showInstallButton} isInstalled={isInstalled} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListWithCollection = ({
|
const ListWithCollection = ({
|
||||||
@ -59,6 +63,7 @@ const ListWithCollection = ({
|
|||||||
cardContainerClassName,
|
cardContainerClassName,
|
||||||
cardRender,
|
cardRender,
|
||||||
onCollectionMoreClick,
|
onCollectionMoreClick,
|
||||||
|
installedPluginIds,
|
||||||
}: ListWithCollectionProps) => {
|
}: ListWithCollectionProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const locale = useLocale()
|
const locale = useLocale()
|
||||||
@ -143,6 +148,7 @@ const ListWithCollection = ({
|
|||||||
plugin={plugin}
|
plugin={plugin}
|
||||||
showInstallButton={showInstallButton}
|
showInstallButton={showInstallButton}
|
||||||
cardRender={cardRender}
|
cardRender={cardRender}
|
||||||
|
isInstalled={installedPluginIds?.has(plugin.plugin_id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -158,6 +164,7 @@ const ListWithCollection = ({
|
|||||||
plugin={plugin}
|
plugin={plugin}
|
||||||
showInstallButton={showInstallButton}
|
showInstallButton={showInstallButton}
|
||||||
cardRender={cardRender}
|
cardRender={cardRender}
|
||||||
|
isInstalled={installedPluginIds?.has(plugin.plugin_id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user