Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>tags/1.3.0
| @@ -8,7 +8,7 @@ const PluginList = async () => { | |||
| return ( | |||
| <PluginPage | |||
| plugins={<PluginsPanel />} | |||
| marketplace={<Marketplace locale={locale} pluginTypeSwitchClassName='top-[60px]' searchBoxAutoAnimate={false} />} | |||
| marketplace={<Marketplace locale={locale} pluginTypeSwitchClassName='top-[60px]' searchBoxAutoAnimate={false} showSearchParams={false} />} | |||
| /> | |||
| ) | |||
| } | |||
| @@ -92,3 +92,17 @@ export const useSingleCategories = (translateFromOut?: TFunction) => { | |||
| 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 | |||
| } | |||
| @@ -35,9 +35,10 @@ import { | |||
| import { | |||
| getMarketplaceListCondition, | |||
| getMarketplaceListFilterType, | |||
| updateSearchParams, | |||
| } from './utils' | |||
| import { useInstalledPluginList } from '@/service/use-plugins' | |||
| import { noop } from 'lodash-es' | |||
| import { debounce, noop } from 'lodash-es' | |||
| export type MarketplaceContextValue = { | |||
| intersected: boolean | |||
| @@ -96,6 +97,7 @@ type MarketplaceContextProviderProps = { | |||
| searchParams?: SearchParams | |||
| shouldExclude?: boolean | |||
| scrollContainerId?: string | |||
| showSearchParams?: boolean | |||
| } | |||
| export function useMarketplaceContext(selector: (value: MarketplaceContextValue) => any) { | |||
| @@ -107,6 +109,7 @@ export const MarketplaceContextProvider = ({ | |||
| searchParams, | |||
| shouldExclude, | |||
| scrollContainerId, | |||
| showSearchParams, | |||
| }: MarketplaceContextProviderProps) => { | |||
| const { data, isSuccess } = useInstalledPluginList(!shouldExclude) | |||
| const exclude = useMemo(() => { | |||
| @@ -159,7 +162,10 @@ export const MarketplaceContextProvider = ({ | |||
| type: getMarketplaceListFilterType(activePluginTypeRef.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 { | |||
| if (shouldExclude && isSuccess) { | |||
| @@ -182,7 +188,31 @@ export const MarketplaceContextProvider = ({ | |||
| 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) => { | |||
| handleUpdateSearchParams(debounced) | |||
| if (debounced) { | |||
| queryPluginsWithDebounced({ | |||
| query: searchPluginTextRef.current, | |||
| @@ -207,17 +237,18 @@ export const MarketplaceContextProvider = ({ | |||
| page: pageRef.current, | |||
| }) | |||
| } | |||
| }, [exclude, queryPluginsWithDebounced, queryPlugins]) | |||
| }, [exclude, queryPluginsWithDebounced, queryPlugins, handleUpdateSearchParams]) | |||
| const handleQuery = useCallback((debounced?: boolean) => { | |||
| if (!searchPluginTextRef.current && !filterPluginTagsRef.current.length) { | |||
| handleUpdateSearchParams(debounced) | |||
| cancelQueryPluginsWithDebounced() | |||
| handleQueryMarketplaceCollectionsAndPlugins() | |||
| return | |||
| } | |||
| handleQueryPlugins(debounced) | |||
| }, [handleQueryMarketplaceCollectionsAndPlugins, handleQueryPlugins, cancelQueryPluginsWithDebounced]) | |||
| }, [handleQueryMarketplaceCollectionsAndPlugins, handleQueryPlugins, cancelQueryPluginsWithDebounced, handleUpdateSearchParams]) | |||
| const handleSearchPluginTextChange = useCallback((text: string) => { | |||
| setSearchPluginText(text) | |||
| @@ -242,11 +273,9 @@ export const MarketplaceContextProvider = ({ | |||
| activePluginTypeRef.current = type | |||
| setPage(1) | |||
| pageRef.current = 1 | |||
| }, []) | |||
| useEffect(() => { | |||
| handleQuery() | |||
| }, [activePluginType, handleQuery]) | |||
| }, [handleQuery]) | |||
| const handleSortChange = useCallback((sort: PluginsSort) => { | |||
| setSort(sort) | |||
| @@ -17,6 +17,7 @@ type MarketplaceProps = { | |||
| pluginTypeSwitchClassName?: string | |||
| intersectionContainerId?: string | |||
| scrollContainerId?: string | |||
| showSearchParams?: boolean | |||
| } | |||
| const Marketplace = async ({ | |||
| locale, | |||
| @@ -27,6 +28,7 @@ const Marketplace = async ({ | |||
| pluginTypeSwitchClassName, | |||
| intersectionContainerId, | |||
| scrollContainerId, | |||
| showSearchParams = true, | |||
| }: MarketplaceProps) => { | |||
| let marketplaceCollections: any = [] | |||
| let marketplaceCollectionPluginsMap = {} | |||
| @@ -42,6 +44,7 @@ const Marketplace = async ({ | |||
| searchParams={searchParams} | |||
| shouldExclude={shouldExclude} | |||
| scrollContainerId={scrollContainerId} | |||
| showSearchParams={showSearchParams} | |||
| > | |||
| <Description locale={locale} /> | |||
| <IntersectionLine intersectionContainerId={intersectionContainerId} /> | |||
| @@ -53,6 +56,7 @@ const Marketplace = async ({ | |||
| locale={locale} | |||
| className={pluginTypeSwitchClassName} | |||
| searchBoxAutoAnimate={searchBoxAutoAnimate} | |||
| showSearchParams={showSearchParams} | |||
| /> | |||
| <ListWrapper | |||
| locale={locale} | |||
| @@ -13,6 +13,7 @@ import { | |||
| useSearchBoxAutoAnimate, | |||
| } from './hooks' | |||
| import cn from '@/utils/classnames' | |||
| import { useCallback, useEffect } from 'react' | |||
| export const PLUGIN_TYPE_SEARCH_MAP = { | |||
| all: 'all', | |||
| @@ -26,11 +27,13 @@ type PluginTypeSwitchProps = { | |||
| locale?: string | |||
| className?: string | |||
| searchBoxAutoAnimate?: boolean | |||
| showSearchParams?: boolean | |||
| } | |||
| const PluginTypeSwitch = ({ | |||
| locale, | |||
| className, | |||
| searchBoxAutoAnimate, | |||
| showSearchParams, | |||
| }: PluginTypeSwitchProps) => { | |||
| const { t } = useMixedTranslation(locale) | |||
| const activePluginType = useMarketplaceContext(s => s.activePluginType) | |||
| @@ -70,6 +73,23 @@ const PluginTypeSwitch = ({ | |||
| }, | |||
| ] | |||
| 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 ( | |||
| <div className={cn( | |||
| 'flex shrink-0 items-center justify-center space-x-2 bg-background-body py-3', | |||
| @@ -4,6 +4,7 @@ import { PluginType } from '@/app/components/plugins/types' | |||
| import type { | |||
| CollectionsAndPluginsSearchParams, | |||
| MarketplaceCollection, | |||
| PluginsSearchParams, | |||
| } from '@/app/components/plugins/marketplace/types' | |||
| import { | |||
| MARKETPLACE_API_PREFIX, | |||
| @@ -125,3 +126,22 @@ export const getMarketplaceListFilterType = (category: string) => { | |||
| 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) | |||
| } | |||
| @@ -12,9 +12,9 @@ import { | |||
| } from 'use-context-selector' | |||
| import { useSelector as useAppContextSelector } from '@/context/app-context' | |||
| import type { FilterState } from './filter-management' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useTabSearchParams } from '@/hooks/use-tab-searchparams' | |||
| import { noop } from 'lodash-es' | |||
| import { PLUGIN_PAGE_TABS_MAP, usePluginPageTabs } from '../hooks' | |||
| export type PluginPageContextValue = { | |||
| containerRef: React.RefObject<HTMLDivElement> | |||
| @@ -53,7 +53,6 @@ export function usePluginPageContext(selector: (value: PluginPageContextValue) = | |||
| export const PluginPageContextProvider = ({ | |||
| children, | |||
| }: PluginPageContextProviderProps) => { | |||
| const { t } = useTranslation() | |||
| const containerRef = useRef<HTMLDivElement>(null) | |||
| const [filters, setFilters] = useState<FilterState>({ | |||
| categories: [], | |||
| @@ -63,16 +62,10 @@ export const PluginPageContextProvider = ({ | |||
| const [currentPluginID, setCurrentPluginID] = useState<string | undefined>() | |||
| const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) | |||
| const tabs = usePluginPageTabs() | |||
| 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({ | |||
| defaultTab: options[0].value, | |||
| }) | |||
| @@ -40,6 +40,8 @@ import { SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config' | |||
| import { LanguagesSupported } from '@/i18n/language' | |||
| import I18n from '@/context/i18n' | |||
| 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 BUNDLE_INFO_KEY = 'bundle-info' | |||
| @@ -136,40 +138,45 @@ const PluginPage = ({ | |||
| const setActiveTab = usePluginPageContext(v => v.setActiveTab) | |||
| 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({ | |||
| onFileChange: setCurrentFile, | |||
| containerRef, | |||
| enabled: activeTab === 'plugins', | |||
| enabled: isPluginsTab, | |||
| }) | |||
| const { dragging, fileUploader, fileChangeHandle, removeFile } = uploaderProps | |||
| return ( | |||
| <div | |||
| id='marketplace-container' | |||
| ref={containerRef} | |||
| 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' | |||
| : 'bg-background-body', | |||
| )} | |||
| > | |||
| <div | |||
| 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-1'> | |||
| <TabSlider | |||
| value={activeTab} | |||
| value={isPluginsTab ? PLUGIN_PAGE_TABS_MAP.plugins : PLUGIN_PAGE_TABS_MAP.marketplace} | |||
| onChange={setActiveTab} | |||
| options={options} | |||
| /> | |||
| </div> | |||
| <div className='flex shrink-0 items-center gap-1'> | |||
| { | |||
| activeTab === 'discover' && ( | |||
| isExploringMarketplace && ( | |||
| <> | |||
| <Link | |||
| href={`https://docs.dify.ai/${locale === LanguagesSupported[1] ? 'v/zh-hans/' : ''}plugins/publish-plugins/publish-to-dify-marketplace`} | |||
| @@ -215,7 +222,7 @@ const PluginPage = ({ | |||
| </div> | |||
| </div> | |||
| </div> | |||
| {activeTab === 'plugins' && ( | |||
| {isPluginsTab && ( | |||
| <> | |||
| {plugins} | |||
| {dragging && ( | |||
| @@ -246,7 +253,7 @@ const PluginPage = ({ | |||
| </> | |||
| )} | |||
| { | |||
| activeTab === 'discover' && enable_marketplace && marketplace | |||
| isExploringMarketplace && enable_marketplace && marketplace | |||
| } | |||
| {showPluginSettingModal && ( | |||