Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>tags/1.3.0
| return ( | return ( | ||||
| <PluginPage | <PluginPage | ||||
| plugins={<PluginsPanel />} | plugins={<PluginsPanel />} | ||||
| marketplace={<Marketplace locale={locale} pluginTypeSwitchClassName='top-[60px]' searchBoxAutoAnimate={false} />} | |||||
| marketplace={<Marketplace locale={locale} pluginTypeSwitchClassName='top-[60px]' searchBoxAutoAnimate={false} showSearchParams={false} />} | |||||
| /> | /> | ||||
| ) | ) | ||||
| } | } |
| categoriesMap, | categoriesMap, | ||||
| } | } | ||||
| } | } | ||||
| export const PLUGIN_PAGE_TABS_MAP = { | |||||
| plugins: 'plugins', | |||||
| marketplace: 'discover', | |||||
| } | |||||
| export const usePluginPageTabs = () => { | |||||
| const { t } = useTranslation() | |||||
| const tabs = [ | |||||
| { value: PLUGIN_PAGE_TABS_MAP.plugins, text: t('common.menus.plugins') }, | |||||
| { value: PLUGIN_PAGE_TABS_MAP.marketplace, text: t('common.menus.exploreMarketplace') }, | |||||
| ] | |||||
| return tabs | |||||
| } |
| import { | import { | ||||
| getMarketplaceListCondition, | getMarketplaceListCondition, | ||||
| getMarketplaceListFilterType, | getMarketplaceListFilterType, | ||||
| updateSearchParams, | |||||
| } from './utils' | } from './utils' | ||||
| import { useInstalledPluginList } from '@/service/use-plugins' | import { useInstalledPluginList } from '@/service/use-plugins' | ||||
| import { noop } from 'lodash-es' | |||||
| import { debounce, noop } from 'lodash-es' | |||||
| export type MarketplaceContextValue = { | export type MarketplaceContextValue = { | ||||
| intersected: boolean | intersected: boolean | ||||
| searchParams?: SearchParams | searchParams?: SearchParams | ||||
| shouldExclude?: boolean | shouldExclude?: boolean | ||||
| scrollContainerId?: string | scrollContainerId?: string | ||||
| showSearchParams?: boolean | |||||
| } | } | ||||
| export function useMarketplaceContext(selector: (value: MarketplaceContextValue) => any) { | export function useMarketplaceContext(selector: (value: MarketplaceContextValue) => any) { | ||||
| searchParams, | searchParams, | ||||
| shouldExclude, | shouldExclude, | ||||
| scrollContainerId, | scrollContainerId, | ||||
| showSearchParams, | |||||
| }: MarketplaceContextProviderProps) => { | }: MarketplaceContextProviderProps) => { | ||||
| const { data, isSuccess } = useInstalledPluginList(!shouldExclude) | const { data, isSuccess } = useInstalledPluginList(!shouldExclude) | ||||
| const exclude = useMemo(() => { | const exclude = useMemo(() => { | ||||
| type: getMarketplaceListFilterType(activePluginTypeRef.current), | type: getMarketplaceListFilterType(activePluginTypeRef.current), | ||||
| page: pageRef.current, | page: pageRef.current, | ||||
| }) | }) | ||||
| history.pushState({}, '', `/${searchParams?.language ? `?language=${searchParams?.language}` : ''}`) | |||||
| const url = new URL(window.location.href) | |||||
| if (searchParams?.language) | |||||
| url.searchParams.set('language', searchParams?.language) | |||||
| history.replaceState({}, '', url) | |||||
| } | } | ||||
| else { | else { | ||||
| if (shouldExclude && isSuccess) { | if (shouldExclude && isSuccess) { | ||||
| resetPlugins() | resetPlugins() | ||||
| }, [exclude, queryMarketplaceCollectionsAndPlugins, resetPlugins]) | }, [exclude, queryMarketplaceCollectionsAndPlugins, resetPlugins]) | ||||
| const debouncedUpdateSearchParams = useMemo(() => debounce(() => { | |||||
| updateSearchParams({ | |||||
| query: searchPluginTextRef.current, | |||||
| category: activePluginTypeRef.current, | |||||
| tags: filterPluginTagsRef.current, | |||||
| }) | |||||
| }, 500), []) | |||||
| const handleUpdateSearchParams = useCallback((debounced?: boolean) => { | |||||
| if (!showSearchParams) | |||||
| return | |||||
| if (debounced) { | |||||
| debouncedUpdateSearchParams() | |||||
| } | |||||
| else { | |||||
| updateSearchParams({ | |||||
| query: searchPluginTextRef.current, | |||||
| category: activePluginTypeRef.current, | |||||
| tags: filterPluginTagsRef.current, | |||||
| }) | |||||
| } | |||||
| }, [debouncedUpdateSearchParams, showSearchParams]) | |||||
| const handleQueryPlugins = useCallback((debounced?: boolean) => { | const handleQueryPlugins = useCallback((debounced?: boolean) => { | ||||
| handleUpdateSearchParams(debounced) | |||||
| if (debounced) { | if (debounced) { | ||||
| queryPluginsWithDebounced({ | queryPluginsWithDebounced({ | ||||
| query: searchPluginTextRef.current, | query: searchPluginTextRef.current, | ||||
| page: pageRef.current, | page: pageRef.current, | ||||
| }) | }) | ||||
| } | } | ||||
| }, [exclude, queryPluginsWithDebounced, queryPlugins]) | |||||
| }, [exclude, queryPluginsWithDebounced, queryPlugins, handleUpdateSearchParams]) | |||||
| const handleQuery = useCallback((debounced?: boolean) => { | const handleQuery = useCallback((debounced?: boolean) => { | ||||
| if (!searchPluginTextRef.current && !filterPluginTagsRef.current.length) { | if (!searchPluginTextRef.current && !filterPluginTagsRef.current.length) { | ||||
| handleUpdateSearchParams(debounced) | |||||
| cancelQueryPluginsWithDebounced() | cancelQueryPluginsWithDebounced() | ||||
| handleQueryMarketplaceCollectionsAndPlugins() | handleQueryMarketplaceCollectionsAndPlugins() | ||||
| return | return | ||||
| } | } | ||||
| handleQueryPlugins(debounced) | handleQueryPlugins(debounced) | ||||
| }, [handleQueryMarketplaceCollectionsAndPlugins, handleQueryPlugins, cancelQueryPluginsWithDebounced]) | |||||
| }, [handleQueryMarketplaceCollectionsAndPlugins, handleQueryPlugins, cancelQueryPluginsWithDebounced, handleUpdateSearchParams]) | |||||
| const handleSearchPluginTextChange = useCallback((text: string) => { | const handleSearchPluginTextChange = useCallback((text: string) => { | ||||
| setSearchPluginText(text) | setSearchPluginText(text) | ||||
| activePluginTypeRef.current = type | activePluginTypeRef.current = type | ||||
| setPage(1) | setPage(1) | ||||
| pageRef.current = 1 | pageRef.current = 1 | ||||
| }, []) | |||||
| useEffect(() => { | |||||
| handleQuery() | handleQuery() | ||||
| }, [activePluginType, handleQuery]) | |||||
| }, [handleQuery]) | |||||
| const handleSortChange = useCallback((sort: PluginsSort) => { | const handleSortChange = useCallback((sort: PluginsSort) => { | ||||
| setSort(sort) | setSort(sort) |
| pluginTypeSwitchClassName?: string | pluginTypeSwitchClassName?: string | ||||
| intersectionContainerId?: string | intersectionContainerId?: string | ||||
| scrollContainerId?: string | scrollContainerId?: string | ||||
| showSearchParams?: boolean | |||||
| } | } | ||||
| const Marketplace = async ({ | const Marketplace = async ({ | ||||
| locale, | locale, | ||||
| pluginTypeSwitchClassName, | pluginTypeSwitchClassName, | ||||
| intersectionContainerId, | intersectionContainerId, | ||||
| scrollContainerId, | scrollContainerId, | ||||
| showSearchParams = true, | |||||
| }: MarketplaceProps) => { | }: MarketplaceProps) => { | ||||
| let marketplaceCollections: any = [] | let marketplaceCollections: any = [] | ||||
| let marketplaceCollectionPluginsMap = {} | let marketplaceCollectionPluginsMap = {} | ||||
| searchParams={searchParams} | searchParams={searchParams} | ||||
| shouldExclude={shouldExclude} | shouldExclude={shouldExclude} | ||||
| scrollContainerId={scrollContainerId} | scrollContainerId={scrollContainerId} | ||||
| showSearchParams={showSearchParams} | |||||
| > | > | ||||
| <Description locale={locale} /> | <Description locale={locale} /> | ||||
| <IntersectionLine intersectionContainerId={intersectionContainerId} /> | <IntersectionLine intersectionContainerId={intersectionContainerId} /> | ||||
| locale={locale} | locale={locale} | ||||
| className={pluginTypeSwitchClassName} | className={pluginTypeSwitchClassName} | ||||
| searchBoxAutoAnimate={searchBoxAutoAnimate} | searchBoxAutoAnimate={searchBoxAutoAnimate} | ||||
| showSearchParams={showSearchParams} | |||||
| /> | /> | ||||
| <ListWrapper | <ListWrapper | ||||
| locale={locale} | locale={locale} |
| useSearchBoxAutoAnimate, | useSearchBoxAutoAnimate, | ||||
| } from './hooks' | } from './hooks' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import { useCallback, useEffect } from 'react' | |||||
| export const PLUGIN_TYPE_SEARCH_MAP = { | export const PLUGIN_TYPE_SEARCH_MAP = { | ||||
| all: 'all', | all: 'all', | ||||
| locale?: string | locale?: string | ||||
| className?: string | className?: string | ||||
| searchBoxAutoAnimate?: boolean | searchBoxAutoAnimate?: boolean | ||||
| showSearchParams?: boolean | |||||
| } | } | ||||
| const PluginTypeSwitch = ({ | const PluginTypeSwitch = ({ | ||||
| locale, | locale, | ||||
| className, | className, | ||||
| searchBoxAutoAnimate, | searchBoxAutoAnimate, | ||||
| showSearchParams, | |||||
| }: PluginTypeSwitchProps) => { | }: PluginTypeSwitchProps) => { | ||||
| const { t } = useMixedTranslation(locale) | const { t } = useMixedTranslation(locale) | ||||
| const activePluginType = useMarketplaceContext(s => s.activePluginType) | const activePluginType = useMarketplaceContext(s => s.activePluginType) | ||||
| }, | }, | ||||
| ] | ] | ||||
| const handlePopState = useCallback(() => { | |||||
| if (!showSearchParams) | |||||
| return | |||||
| const url = new URL(window.location.href) | |||||
| const category = url.searchParams.get('category') || PLUGIN_TYPE_SEARCH_MAP.all | |||||
| handleActivePluginTypeChange(category) | |||||
| }, [showSearchParams, handleActivePluginTypeChange]) | |||||
| useEffect(() => { | |||||
| window.addEventListener('popstate', () => { | |||||
| handlePopState() | |||||
| }) | |||||
| return () => { | |||||
| window.removeEventListener('popstate', handlePopState) | |||||
| } | |||||
| }, [handlePopState]) | |||||
| return ( | return ( | ||||
| <div className={cn( | <div className={cn( | ||||
| 'flex shrink-0 items-center justify-center space-x-2 bg-background-body py-3', | 'flex shrink-0 items-center justify-center space-x-2 bg-background-body py-3', |
| import type { | import type { | ||||
| CollectionsAndPluginsSearchParams, | CollectionsAndPluginsSearchParams, | ||||
| MarketplaceCollection, | MarketplaceCollection, | ||||
| PluginsSearchParams, | |||||
| } from '@/app/components/plugins/marketplace/types' | } from '@/app/components/plugins/marketplace/types' | ||||
| import { | import { | ||||
| MARKETPLACE_API_PREFIX, | MARKETPLACE_API_PREFIX, | ||||
| return 'plugin' | return 'plugin' | ||||
| } | } | ||||
| export const updateSearchParams = (pluginsSearchParams: PluginsSearchParams) => { | |||||
| const { query, category, tags } = pluginsSearchParams | |||||
| const url = new URL(window.location.href) | |||||
| const categoryChanged = url.searchParams.get('category') !== category | |||||
| if (query) | |||||
| url.searchParams.set('q', query) | |||||
| else | |||||
| url.searchParams.delete('q') | |||||
| if (category) | |||||
| url.searchParams.set('category', category) | |||||
| else | |||||
| url.searchParams.delete('category') | |||||
| if (tags && tags.length) | |||||
| url.searchParams.set('tags', tags.join(',')) | |||||
| else | |||||
| url.searchParams.delete('tags') | |||||
| history[`${categoryChanged ? 'pushState' : 'replaceState'}`]({}, '', url) | |||||
| } |
| } from 'use-context-selector' | } from 'use-context-selector' | ||||
| import { useSelector as useAppContextSelector } from '@/context/app-context' | import { useSelector as useAppContextSelector } from '@/context/app-context' | ||||
| import type { FilterState } from './filter-management' | import type { FilterState } from './filter-management' | ||||
| import { useTranslation } from 'react-i18next' | |||||
| import { useTabSearchParams } from '@/hooks/use-tab-searchparams' | import { useTabSearchParams } from '@/hooks/use-tab-searchparams' | ||||
| import { noop } from 'lodash-es' | import { noop } from 'lodash-es' | ||||
| import { PLUGIN_PAGE_TABS_MAP, usePluginPageTabs } from '../hooks' | |||||
| export type PluginPageContextValue = { | export type PluginPageContextValue = { | ||||
| containerRef: React.RefObject<HTMLDivElement> | containerRef: React.RefObject<HTMLDivElement> | ||||
| export const PluginPageContextProvider = ({ | export const PluginPageContextProvider = ({ | ||||
| children, | children, | ||||
| }: PluginPageContextProviderProps) => { | }: PluginPageContextProviderProps) => { | ||||
| const { t } = useTranslation() | |||||
| const containerRef = useRef<HTMLDivElement>(null) | const containerRef = useRef<HTMLDivElement>(null) | ||||
| const [filters, setFilters] = useState<FilterState>({ | const [filters, setFilters] = useState<FilterState>({ | ||||
| categories: [], | categories: [], | ||||
| const [currentPluginID, setCurrentPluginID] = useState<string | undefined>() | const [currentPluginID, setCurrentPluginID] = useState<string | undefined>() | ||||
| const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) | const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) | ||||
| const tabs = usePluginPageTabs() | |||||
| const options = useMemo(() => { | const options = useMemo(() => { | ||||
| return [ | |||||
| { value: 'plugins', text: t('common.menus.plugins') }, | |||||
| ...( | |||||
| enable_marketplace | |||||
| ? [{ value: 'discover', text: t('common.menus.exploreMarketplace') }] | |||||
| : [] | |||||
| ), | |||||
| ] | |||||
| }, [t, enable_marketplace]) | |||||
| return enable_marketplace ? tabs : tabs.filter(tab => tab.value !== PLUGIN_PAGE_TABS_MAP.marketplace) | |||||
| }, [tabs, enable_marketplace]) | |||||
| const [activeTab, setActiveTab] = useTabSearchParams({ | const [activeTab, setActiveTab] = useTabSearchParams({ | ||||
| defaultTab: options[0].value, | defaultTab: options[0].value, | ||||
| }) | }) |
| import { LanguagesSupported } from '@/i18n/language' | import { LanguagesSupported } from '@/i18n/language' | ||||
| import I18n from '@/context/i18n' | import I18n from '@/context/i18n' | ||||
| import { noop } from 'lodash-es' | import { noop } from 'lodash-es' | ||||
| import { PLUGIN_TYPE_SEARCH_MAP } from '../marketplace/plugin-type-switch' | |||||
| import { PLUGIN_PAGE_TABS_MAP } from '../hooks' | |||||
| const PACKAGE_IDS_KEY = 'package-ids' | const PACKAGE_IDS_KEY = 'package-ids' | ||||
| const BUNDLE_INFO_KEY = 'bundle-info' | const BUNDLE_INFO_KEY = 'bundle-info' | ||||
| const setActiveTab = usePluginPageContext(v => v.setActiveTab) | const setActiveTab = usePluginPageContext(v => v.setActiveTab) | ||||
| const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) | const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) | ||||
| const isPluginsTab = useMemo(() => activeTab === PLUGIN_PAGE_TABS_MAP.plugins, [activeTab]) | |||||
| const isExploringMarketplace = useMemo(() => { | |||||
| const values = Object.values(PLUGIN_TYPE_SEARCH_MAP) | |||||
| return activeTab === PLUGIN_PAGE_TABS_MAP.marketplace || values.includes(activeTab) | |||||
| }, [activeTab]) | |||||
| const uploaderProps = useUploader({ | const uploaderProps = useUploader({ | ||||
| onFileChange: setCurrentFile, | onFileChange: setCurrentFile, | ||||
| containerRef, | containerRef, | ||||
| enabled: activeTab === 'plugins', | |||||
| enabled: isPluginsTab, | |||||
| }) | }) | ||||
| const { dragging, fileUploader, fileChangeHandle, removeFile } = uploaderProps | const { dragging, fileUploader, fileChangeHandle, removeFile } = uploaderProps | ||||
| return ( | return ( | ||||
| <div | <div | ||||
| id='marketplace-container' | id='marketplace-container' | ||||
| ref={containerRef} | ref={containerRef} | ||||
| style={{ scrollbarGutter: 'stable' }} | style={{ scrollbarGutter: 'stable' }} | ||||
| className={cn('relative flex grow flex-col overflow-y-auto border-t border-divider-subtle', activeTab === 'plugins' | |||||
| className={cn('relative flex grow flex-col overflow-y-auto border-t border-divider-subtle', isPluginsTab | |||||
| ? 'rounded-t-xl bg-components-panel-bg' | ? 'rounded-t-xl bg-components-panel-bg' | ||||
| : 'bg-background-body', | : 'bg-background-body', | ||||
| )} | )} | ||||
| > | > | ||||
| <div | <div | ||||
| className={cn( | className={cn( | ||||
| 'sticky top-0 z-10 flex min-h-[60px] items-center gap-1 self-stretch bg-components-panel-bg px-12 pb-2 pt-4', activeTab === 'discover' && 'bg-background-body', | |||||
| 'sticky top-0 z-10 flex min-h-[60px] items-center gap-1 self-stretch bg-components-panel-bg px-12 pb-2 pt-4', isExploringMarketplace && 'bg-background-body', | |||||
| )} | )} | ||||
| > | > | ||||
| <div className='flex w-full items-center justify-between'> | <div className='flex w-full items-center justify-between'> | ||||
| <div className='flex-1'> | <div className='flex-1'> | ||||
| <TabSlider | <TabSlider | ||||
| value={activeTab} | |||||
| value={isPluginsTab ? PLUGIN_PAGE_TABS_MAP.plugins : PLUGIN_PAGE_TABS_MAP.marketplace} | |||||
| onChange={setActiveTab} | onChange={setActiveTab} | ||||
| options={options} | options={options} | ||||
| /> | /> | ||||
| </div> | </div> | ||||
| <div className='flex shrink-0 items-center gap-1'> | <div className='flex shrink-0 items-center gap-1'> | ||||
| { | { | ||||
| activeTab === 'discover' && ( | |||||
| isExploringMarketplace && ( | |||||
| <> | <> | ||||
| <Link | <Link | ||||
| href={`https://docs.dify.ai/${locale === LanguagesSupported[1] ? 'v/zh-hans/' : ''}plugins/publish-plugins/publish-to-dify-marketplace`} | href={`https://docs.dify.ai/${locale === LanguagesSupported[1] ? 'v/zh-hans/' : ''}plugins/publish-plugins/publish-to-dify-marketplace`} | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| {activeTab === 'plugins' && ( | |||||
| {isPluginsTab && ( | |||||
| <> | <> | ||||
| {plugins} | {plugins} | ||||
| {dragging && ( | {dragging && ( | ||||
| </> | </> | ||||
| )} | )} | ||||
| { | { | ||||
| activeTab === 'discover' && enable_marketplace && marketplace | |||||
| isExploringMarketplace && enable_marketplace && marketplace | |||||
| } | } | ||||
| {showPluginSettingModal && ( | {showPluginSettingModal && ( |