| @@ -17,9 +17,9 @@ import Loading from '@/app/components/base/loading' | |||
| import ProviderCard from '@/app/components/plugins/provider-card' | |||
| import List from '@/app/components/plugins/marketplace/list' | |||
| import type { Plugin } from '@/app/components/plugins/types' | |||
| import { MARKETPLACE_URL_PREFIX } from '@/config' | |||
| import cn from '@/utils/classnames' | |||
| import { getLocaleOnClient } from '@/i18n' | |||
| import { getMarketplaceUrl } from '@/utils/var' | |||
| type InstallFromMarketplaceProps = { | |||
| providers: ModelProvider[] | |||
| @@ -55,7 +55,7 @@ const InstallFromMarketplace = ({ | |||
| </div> | |||
| <div className='mb-2 flex items-center pt-2'> | |||
| <span className='system-sm-regular pr-1 text-text-tertiary'>{t('common.modelProvider.discoverMore')}</span> | |||
| <Link target="_blank" href={`${MARKETPLACE_URL_PREFIX}${theme ? `?theme=${theme}` : ''}`} className='system-sm-medium inline-flex items-center text-text-accent'> | |||
| <Link target="_blank" href={getMarketplaceUrl('', { theme })} className='system-sm-medium inline-flex items-center text-text-accent'> | |||
| {t('plugin.marketplace.difyMarketplace')} | |||
| <RiArrowRightUpLine className='h-4 w-4' /> | |||
| </Link> | |||
| @@ -15,6 +15,7 @@ import { renderI18nObject } from '@/i18n' | |||
| import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks' | |||
| import Partner from '../base/badges/partner' | |||
| import Verified from '../base/badges/verified' | |||
| import { RiAlertFill } from '@remixicon/react' | |||
| export type Props = { | |||
| className?: string | |||
| @@ -28,6 +29,7 @@ export type Props = { | |||
| isLoading?: boolean | |||
| loadingFileName?: string | |||
| locale?: string | |||
| limitedInstall?: boolean | |||
| } | |||
| const Card = ({ | |||
| @@ -42,6 +44,7 @@ const Card = ({ | |||
| isLoading = false, | |||
| loadingFileName, | |||
| locale: localeFromProps, | |||
| limitedInstall = false, | |||
| }: Props) => { | |||
| const defaultLocale = useGetLanguage() | |||
| const locale = localeFromProps ? getLanguage(localeFromProps) : defaultLocale | |||
| @@ -54,7 +57,7 @@ const Card = ({ | |||
| obj ? renderI18nObject(obj, locale) : '' | |||
| const isPartner = badges.includes('partner') | |||
| const wrapClassName = cn('hover-bg-components-panel-on-panel-item-bg relative rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 pb-3 shadow-xs', className) | |||
| const wrapClassName = cn('hover-bg-components-panel-on-panel-item-bg relative overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs', className) | |||
| if (isLoading) { | |||
| return ( | |||
| <Placeholder | |||
| @@ -66,30 +69,39 @@ const Card = ({ | |||
| return ( | |||
| <div className={wrapClassName}> | |||
| {!hideCornerMark && <CornerMark text={cornerMark} />} | |||
| {/* Header */} | |||
| <div className="flex"> | |||
| <Icon src={icon} installed={installed} installFailed={installFailed} /> | |||
| <div className="ml-3 w-0 grow"> | |||
| <div className="flex h-5 items-center"> | |||
| <Title title={getLocalizedText(label)} /> | |||
| {isPartner && <Partner className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.partnerTip')} />} | |||
| {verified && <Verified className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.verifiedTip')} />} | |||
| {titleLeft} {/* This can be version badge */} | |||
| <div className={cn('p-4 pb-3', limitedInstall && 'pb-1')}> | |||
| {!hideCornerMark && <CornerMark text={cornerMark} />} | |||
| {/* Header */} | |||
| <div className="flex"> | |||
| <Icon src={icon} installed={installed} installFailed={installFailed} /> | |||
| <div className="ml-3 w-0 grow"> | |||
| <div className="flex h-5 items-center"> | |||
| <Title title={getLocalizedText(label)} /> | |||
| {isPartner && <Partner className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.partnerTip')} />} | |||
| {verified && <Verified className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.verifiedTip')} />} | |||
| {titleLeft} {/* This can be version badge */} | |||
| </div> | |||
| <OrgInfo | |||
| className="mt-0.5" | |||
| orgName={org} | |||
| packageName={name} | |||
| /> | |||
| </div> | |||
| <OrgInfo | |||
| className="mt-0.5" | |||
| orgName={org} | |||
| packageName={name} | |||
| /> | |||
| </div> | |||
| <Description | |||
| className="mt-3" | |||
| text={getLocalizedText(brief)} | |||
| descriptionLineRows={descriptionLineRows} | |||
| /> | |||
| {footer && <div>{footer}</div>} | |||
| </div> | |||
| <Description | |||
| className="mt-3" | |||
| text={getLocalizedText(brief)} | |||
| descriptionLineRows={descriptionLineRows} | |||
| /> | |||
| {footer && <div>{footer}</div>} | |||
| {limitedInstall | |||
| && <div className='relative flex h-8 items-center gap-x-2 px-3 after:absolute after:bottom-0 after:left-0 after:right-0 after:top-0 after:bg-toast-warning-bg after:opacity-40'> | |||
| <RiAlertFill className='h-3 w-3 shrink-0 text-text-warning-secondary' /> | |||
| <p className='system-xs-regular z-10 grow text-text-secondary'> | |||
| {t('plugin.installModal.installWarning')} | |||
| </p> | |||
| </div>} | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -0,0 +1,46 @@ | |||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| import type { SystemFeatures } from '@/types/feature' | |||
| import { InstallationScope } from '@/types/feature' | |||
| import type { Plugin, PluginManifestInMarket } from '../../types' | |||
| type PluginProps = (Plugin | PluginManifestInMarket) & { from: 'github' | 'marketplace' | 'package' } | |||
| export function pluginInstallLimit(plugin: PluginProps, systemFeatures: SystemFeatures) { | |||
| if (systemFeatures.plugin_installation_permission.restrict_to_marketplace_only) { | |||
| if (plugin.from === 'github' || plugin.from === 'package') | |||
| return { canInstall: false } | |||
| } | |||
| if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.ALL) { | |||
| return { | |||
| canInstall: true, | |||
| } | |||
| } | |||
| if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.NONE) { | |||
| return { | |||
| canInstall: false, | |||
| } | |||
| } | |||
| const verification = plugin.verification || {} | |||
| if (!plugin.verification || !plugin.verification.authorized_category) | |||
| verification.authorized_category = 'langgenius' | |||
| if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.OFFICIAL_ONLY) { | |||
| return { | |||
| canInstall: verification.authorized_category === 'langgenius', | |||
| } | |||
| } | |||
| if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.OFFICIAL_AND_PARTNER) { | |||
| return { | |||
| canInstall: verification.authorized_category === 'langgenius' || verification.authorized_category === 'partner', | |||
| } | |||
| } | |||
| return { | |||
| canInstall: true, | |||
| } | |||
| } | |||
| export default function usePluginInstallLimit(plugin: PluginProps) { | |||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | |||
| return pluginInstallLimit(plugin, systemFeatures) | |||
| } | |||
| @@ -39,7 +39,7 @@ const Item: FC<Props> = ({ | |||
| plugin_id: data.unique_identifier, | |||
| } | |||
| onFetchedPayload(payload) | |||
| setPayload(payload) | |||
| setPayload({ ...payload, from: dependency.type }) | |||
| } | |||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||
| }, [data]) | |||
| @@ -8,6 +8,7 @@ import useGetIcon from '../../base/use-get-icon' | |||
| import { MARKETPLACE_API_PREFIX } from '@/config' | |||
| import Version from '../../base/version' | |||
| import type { VersionProps } from '../../../types' | |||
| import usePluginInstallLimit from '../../hooks/use-install-plugin-limit' | |||
| type Props = { | |||
| checked: boolean | |||
| @@ -29,9 +30,11 @@ const LoadedItem: FC<Props> = ({ | |||
| ...particleVersionInfo, | |||
| toInstallVersion: payload.version, | |||
| } | |||
| const { canInstall } = usePluginInstallLimit(payload) | |||
| return ( | |||
| <div className='flex items-center space-x-2'> | |||
| <Checkbox | |||
| disabled={!canInstall} | |||
| className='shrink-0' | |||
| checked={checked} | |||
| onCheck={() => onCheckedChange(payload)} | |||
| @@ -43,6 +46,7 @@ const LoadedItem: FC<Props> = ({ | |||
| icon: isFromMarketPlace ? `${MARKETPLACE_API_PREFIX}/plugins/${payload.org}/${payload.name}/icon` : getIconUrl(payload.icon), | |||
| }} | |||
| titleLeft={payload.version ? <Version {...versionInfo} /> : null} | |||
| limitedInstall={!canInstall} | |||
| /> | |||
| </div> | |||
| ) | |||
| @@ -29,7 +29,7 @@ const PackageItem: FC<Props> = ({ | |||
| const plugin = pluginManifestToCardPluginProps(payload.value.manifest) | |||
| return ( | |||
| <LoadedItem | |||
| payload={plugin} | |||
| payload={{ ...plugin, from: payload.type }} | |||
| checked={checked} | |||
| onCheckedChange={onCheckedChange} | |||
| isFromMarketPlace={isFromMarketPlace} | |||
| @@ -1,5 +1,6 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import type { ForwardRefRenderFunction } from 'react' | |||
| import { useImperativeHandle } from 'react' | |||
| import React, { useCallback, useEffect, useMemo, useState } from 'react' | |||
| import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../types' | |||
| import MarketplaceItem from '../item/marketplace-item' | |||
| @@ -9,22 +10,34 @@ import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use | |||
| import produce from 'immer' | |||
| import PackageItem from '../item/package-item' | |||
| import LoadingError from '../../base/loading-error' | |||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| import { pluginInstallLimit } from '../../hooks/use-install-plugin-limit' | |||
| type Props = { | |||
| allPlugins: Dependency[] | |||
| selectedPlugins: Plugin[] | |||
| onSelect: (plugin: Plugin, selectedIndex: number) => void | |||
| onSelect: (plugin: Plugin, selectedIndex: number, allCanInstallPluginsLength: number) => void | |||
| onSelectAll: (plugins: Plugin[], selectedIndexes: number[]) => void | |||
| onDeSelectAll: () => void | |||
| onLoadedAllPlugin: (installedInfo: Record<string, VersionInfo>) => void | |||
| isFromMarketPlace?: boolean | |||
| } | |||
| const InstallByDSLList: FC<Props> = ({ | |||
| export type ExposeRefs = { | |||
| selectAllPlugins: () => void | |||
| deSelectAllPlugins: () => void | |||
| } | |||
| const InstallByDSLList: ForwardRefRenderFunction<ExposeRefs, Props> = ({ | |||
| allPlugins, | |||
| selectedPlugins, | |||
| onSelect, | |||
| onSelectAll, | |||
| onDeSelectAll, | |||
| onLoadedAllPlugin, | |||
| isFromMarketPlace, | |||
| }) => { | |||
| }, ref) => { | |||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | |||
| // DSL has id, to get plugin info to show more info | |||
| const { isLoading: isFetchingMarketplaceDataById, data: infoGetById, error: infoByIdError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map((d) => { | |||
| const dependecy = (d as GitHubItemAndMarketPlaceDependency).value | |||
| @@ -97,7 +110,8 @@ const InstallByDSLList: FC<Props> = ({ | |||
| const sortedList = allPlugins.filter(d => d.type === 'marketplace').map((d) => { | |||
| const p = d as GitHubItemAndMarketPlaceDependency | |||
| const id = p.value.marketplace_plugin_unique_identifier?.split(':')[0] | |||
| return infoGetById.data.list.find(item => item.plugin.plugin_id === id)?.plugin | |||
| const retPluginInfo = infoGetById.data.list.find(item => item.plugin.plugin_id === id)?.plugin | |||
| return { ...retPluginInfo, from: d.type } as Plugin | |||
| }) | |||
| const payloads = sortedList | |||
| const failedIndex: number[] = [] | |||
| @@ -106,7 +120,7 @@ const InstallByDSLList: FC<Props> = ({ | |||
| if (payloads[i]) { | |||
| draft[index] = { | |||
| ...payloads[i], | |||
| version: payloads[i].version || payloads[i].latest_version, | |||
| version: payloads[i]!.version || payloads[i]!.latest_version, | |||
| } | |||
| } | |||
| else { failedIndex.push(index) } | |||
| @@ -181,9 +195,35 @@ const InstallByDSLList: FC<Props> = ({ | |||
| const handleSelect = useCallback((index: number) => { | |||
| return () => { | |||
| onSelect(plugins[index]!, index) | |||
| const canSelectPlugins = plugins.filter((p) => { | |||
| const { canInstall } = pluginInstallLimit(p!, systemFeatures) | |||
| return canInstall | |||
| }) | |||
| onSelect(plugins[index]!, index, canSelectPlugins.length) | |||
| } | |||
| }, [onSelect, plugins]) | |||
| }, [onSelect, plugins, systemFeatures]) | |||
| useImperativeHandle(ref, () => ({ | |||
| selectAllPlugins: () => { | |||
| const selectedIndexes: number[] = [] | |||
| const selectedPlugins: Plugin[] = [] | |||
| allPlugins.forEach((d, index) => { | |||
| const p = plugins[index] | |||
| if (!p) | |||
| return | |||
| const { canInstall } = pluginInstallLimit(p, systemFeatures) | |||
| if (canInstall) { | |||
| selectedIndexes.push(index) | |||
| selectedPlugins.push(p) | |||
| } | |||
| }) | |||
| onSelectAll(selectedPlugins, selectedIndexes) | |||
| }, | |||
| deSelectAllPlugins: () => { | |||
| onDeSelectAll() | |||
| }, | |||
| })) | |||
| return ( | |||
| <> | |||
| {allPlugins.map((d, index) => { | |||
| @@ -211,7 +251,7 @@ const InstallByDSLList: FC<Props> = ({ | |||
| key={index} | |||
| checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)} | |||
| onCheckedChange={handleSelect(index)} | |||
| payload={plugin} | |||
| payload={{ ...plugin, from: d.type } as Plugin} | |||
| version={(d as GitHubItemAndMarketPlaceDependency).value.version! || plugin?.version || ''} | |||
| versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)} | |||
| /> | |||
| @@ -234,4 +274,4 @@ const InstallByDSLList: FC<Props> = ({ | |||
| </> | |||
| ) | |||
| } | |||
| export default React.memo(InstallByDSLList) | |||
| export default React.forwardRef(InstallByDSLList) | |||
| @@ -1,15 +1,18 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import { useRef } from 'react' | |||
| import React, { useCallback, useState } from 'react' | |||
| import type { Dependency, InstallStatusResponse, Plugin, VersionInfo } from '../../../types' | |||
| import Button from '@/app/components/base/button' | |||
| import { RiLoader2Line } from '@remixicon/react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import type { ExposeRefs } from './install-multi' | |||
| import InstallMulti from './install-multi' | |||
| import { useInstallOrUpdate } from '@/service/use-plugins' | |||
| import useRefreshPluginList from '../../hooks/use-refresh-plugin-list' | |||
| import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-permission' | |||
| import { useMittContextSelector } from '@/context/mitt-context' | |||
| import Checkbox from '@/app/components/base/checkbox' | |||
| const i18nPrefix = 'plugin.installModal' | |||
| type Props = { | |||
| @@ -34,18 +37,8 @@ const Install: FC<Props> = ({ | |||
| const [selectedPlugins, setSelectedPlugins] = React.useState<Plugin[]>([]) | |||
| const [selectedIndexes, setSelectedIndexes] = React.useState<number[]>([]) | |||
| const selectedPluginsNum = selectedPlugins.length | |||
| const installMultiRef = useRef<ExposeRefs>(null) | |||
| const { refreshPluginList } = useRefreshPluginList() | |||
| const handleSelect = (plugin: Plugin, selectedIndex: number) => { | |||
| const isSelected = !!selectedPlugins.find(p => p.plugin_id === plugin.plugin_id) | |||
| let nextSelectedPlugins | |||
| if (isSelected) | |||
| nextSelectedPlugins = selectedPlugins.filter(p => p.plugin_id !== plugin.plugin_id) | |||
| else | |||
| nextSelectedPlugins = [...selectedPlugins, plugin] | |||
| setSelectedPlugins(nextSelectedPlugins) | |||
| const nextSelectedIndexes = isSelected ? selectedIndexes.filter(i => i !== selectedIndex) : [...selectedIndexes, selectedIndex] | |||
| setSelectedIndexes(nextSelectedIndexes) | |||
| } | |||
| const [canInstall, setCanInstall] = React.useState(false) | |||
| const [installedInfo, setInstalledInfo] = useState<Record<string, VersionInfo> | undefined>(undefined) | |||
| @@ -81,6 +74,51 @@ const Install: FC<Props> = ({ | |||
| installedInfo: installedInfo!, | |||
| }) | |||
| } | |||
| const [isSelectAll, setIsSelectAll] = useState(false) | |||
| const [isIndeterminate, setIsIndeterminate] = useState(false) | |||
| const handleClickSelectAll = useCallback(() => { | |||
| if (isSelectAll) | |||
| installMultiRef.current?.deSelectAllPlugins() | |||
| else | |||
| installMultiRef.current?.selectAllPlugins() | |||
| }, [isSelectAll]) | |||
| const handleSelectAll = useCallback((plugins: Plugin[], selectedIndexes: number[]) => { | |||
| setSelectedPlugins(plugins) | |||
| setSelectedIndexes(selectedIndexes) | |||
| setIsSelectAll(true) | |||
| setIsIndeterminate(false) | |||
| }, []) | |||
| const handleDeSelectAll = useCallback(() => { | |||
| setSelectedPlugins([]) | |||
| setSelectedIndexes([]) | |||
| setIsSelectAll(false) | |||
| setIsIndeterminate(false) | |||
| }, []) | |||
| const handleSelect = useCallback((plugin: Plugin, selectedIndex: number, allPluginsLength: number) => { | |||
| const isSelected = !!selectedPlugins.find(p => p.plugin_id === plugin.plugin_id) | |||
| let nextSelectedPlugins | |||
| if (isSelected) | |||
| nextSelectedPlugins = selectedPlugins.filter(p => p.plugin_id !== plugin.plugin_id) | |||
| else | |||
| nextSelectedPlugins = [...selectedPlugins, plugin] | |||
| setSelectedPlugins(nextSelectedPlugins) | |||
| const nextSelectedIndexes = isSelected ? selectedIndexes.filter(i => i !== selectedIndex) : [...selectedIndexes, selectedIndex] | |||
| setSelectedIndexes(nextSelectedIndexes) | |||
| if (nextSelectedPlugins.length === 0) { | |||
| setIsSelectAll(false) | |||
| setIsIndeterminate(false) | |||
| } | |||
| else if (nextSelectedPlugins.length === allPluginsLength) { | |||
| setIsSelectAll(true) | |||
| setIsIndeterminate(false) | |||
| } | |||
| else { | |||
| setIsIndeterminate(true) | |||
| setIsSelectAll(false) | |||
| } | |||
| }, [selectedPlugins, selectedIndexes]) | |||
| const { canInstallPluginFromMarketplace } = useCanInstallPluginFromMarketplace() | |||
| return ( | |||
| <> | |||
| @@ -90,9 +128,12 @@ const Install: FC<Props> = ({ | |||
| </div> | |||
| <div className='w-full space-y-1 rounded-2xl bg-background-section-burn p-2'> | |||
| <InstallMulti | |||
| ref={installMultiRef} | |||
| allPlugins={allPlugins} | |||
| selectedPlugins={selectedPlugins} | |||
| onSelect={handleSelect} | |||
| onSelectAll={handleSelectAll} | |||
| onDeSelectAll={handleDeSelectAll} | |||
| onLoadedAllPlugin={handleLoadedAllPlugin} | |||
| isFromMarketPlace={isFromMarketPlace} | |||
| /> | |||
| @@ -100,21 +141,29 @@ const Install: FC<Props> = ({ | |||
| </div> | |||
| {/* Action Buttons */} | |||
| {!isHideButton && ( | |||
| <div className='flex items-center justify-end gap-2 self-stretch p-6 pt-5'> | |||
| {!canInstall && ( | |||
| <Button variant='secondary' className='min-w-[72px]' onClick={onCancel}> | |||
| {t('common.operation.cancel')} | |||
| <div className='flex items-center justify-between gap-2 self-stretch p-6 pt-5'> | |||
| <div className='px-2'> | |||
| {canInstall && <div className='flex items-center gap-x-2' onClick={handleClickSelectAll}> | |||
| <Checkbox checked={isSelectAll} indeterminate={isIndeterminate} /> | |||
| <p className='system-sm-medium cursor-pointer text-text-secondary'>{isSelectAll ? t('common.operation.deSelectAll') : t('common.operation.selectAll')}</p> | |||
| </div>} | |||
| </div> | |||
| <div className='flex items-center justify-end gap-2 self-stretch'> | |||
| {!canInstall && ( | |||
| <Button variant='secondary' className='min-w-[72px]' onClick={onCancel}> | |||
| {t('common.operation.cancel')} | |||
| </Button> | |||
| )} | |||
| <Button | |||
| variant='primary' | |||
| className='flex min-w-[72px] space-x-0.5' | |||
| disabled={!canInstall || isInstalling || selectedPlugins.length === 0 || !canInstallPluginFromMarketplace} | |||
| onClick={handleInstall} | |||
| > | |||
| {isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />} | |||
| <span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span> | |||
| </Button> | |||
| )} | |||
| <Button | |||
| variant='primary' | |||
| className='flex min-w-[72px] space-x-0.5' | |||
| disabled={!canInstall || isInstalling || selectedPlugins.length === 0 || !canInstallPluginFromMarketplace} | |||
| onClick={handleInstall} | |||
| > | |||
| {isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />} | |||
| <span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span> | |||
| </Button> | |||
| </div> | |||
| </div> | |||
| )} | |||
| @@ -124,7 +124,7 @@ const Installed: FC<Props> = ({ | |||
| /> | |||
| </p> | |||
| {!isDifyVersionCompatible && ( | |||
| <p className='system-md-regular flex items-center gap-1 text-text-secondary text-text-warning'> | |||
| <p className='system-md-regular flex items-center gap-1 text-text-warning'> | |||
| {t('plugin.difyVersionNotCompatible', { minimalDifyVersion: payload.meta.minimum_dify_version })} | |||
| </p> | |||
| )} | |||
| @@ -15,6 +15,7 @@ import Version from '../../base/version' | |||
| import { usePluginTaskList } from '@/service/use-plugins' | |||
| import { gte } from 'semver' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import useInstallPluginLimit from '../../hooks/use-install-plugin-limit' | |||
| const i18nPrefix = 'plugin.installModal' | |||
| @@ -124,15 +125,16 @@ const Installed: FC<Props> = ({ | |||
| const isDifyVersionCompatible = useMemo(() => { | |||
| if (!pluginDeclaration || !langeniusVersionInfo.current_version) return true | |||
| return gte(langeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0') | |||
| }, [langeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version]) | |||
| }, [langeniusVersionInfo.current_version, pluginDeclaration]) | |||
| const { canInstall } = useInstallPluginLimit({ ...payload, from: 'marketplace' }) | |||
| return ( | |||
| <> | |||
| <div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'> | |||
| <div className='system-md-regular text-text-secondary'> | |||
| <p>{t(`${i18nPrefix}.readyToInstall`)}</p> | |||
| {!isDifyVersionCompatible && ( | |||
| <p className='system-md-regular text-text-secondary text-text-warning'> | |||
| <p className='system-md-regular text-text-warning'> | |||
| {t('plugin.difyVersionNotCompatible', { minimalDifyVersion: pluginDeclaration?.manifest.meta.minimum_dify_version })} | |||
| </p> | |||
| )} | |||
| @@ -146,6 +148,7 @@ const Installed: FC<Props> = ({ | |||
| installedVersion={installedVersion} | |||
| toInstallVersion={toInstallVersion} | |||
| />} | |||
| limitedInstall={!canInstall} | |||
| /> | |||
| </div> | |||
| </div> | |||
| @@ -159,7 +162,7 @@ const Installed: FC<Props> = ({ | |||
| <Button | |||
| variant='primary' | |||
| className='flex min-w-[72px] space-x-0.5' | |||
| disabled={isInstalling || isLoading} | |||
| disabled={isInstalling || isLoading || !canInstall} | |||
| onClick={handleInstall} | |||
| > | |||
| {isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />} | |||
| @@ -1,5 +1,6 @@ | |||
| import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../types' | |||
| import type { GitHubUrlInfo } from '@/app/components/plugins/types' | |||
| import { isEmpty } from 'lodash-es' | |||
| export const pluginManifestToCardPluginProps = (pluginManifest: PluginDeclaration): Plugin => { | |||
| return { | |||
| @@ -47,6 +48,7 @@ export const pluginManifestInMarketToPluginProps = (pluginManifest: PluginManife | |||
| }, | |||
| tags: [], | |||
| badges: pluginManifest.badges, | |||
| verification: isEmpty(pluginManifest.verification) ? { authorized_category: 'langgenius' } : pluginManifest.verification, | |||
| } | |||
| } | |||
| @@ -56,7 +56,7 @@ const CardWrapper = ({ | |||
| > | |||
| {t('plugin.detailPanel.operation.install')} | |||
| </Button> | |||
| <a href={`${getPluginLinkInMarketplace(plugin)}?language=${localeFromLocale}${theme ? `&theme=${theme}` : ''}`} target='_blank' className='block w-[calc(50%-4px)] flex-1 shrink-0'> | |||
| <a href={getPluginLinkInMarketplace(plugin, { language: localeFromLocale, theme })} target='_blank' className='block w-[calc(50%-4px)] flex-1 shrink-0'> | |||
| <Button | |||
| className='w-full gap-0.5' | |||
| > | |||
| @@ -8,8 +8,8 @@ import type { | |||
| } from '@/app/components/plugins/marketplace/types' | |||
| import { | |||
| MARKETPLACE_API_PREFIX, | |||
| MARKETPLACE_URL_PREFIX, | |||
| } from '@/config' | |||
| import { getMarketplaceUrl } from '@/utils/var' | |||
| export const getPluginIconInMarketplace = (plugin: Plugin) => { | |||
| if (plugin.type === 'bundle') | |||
| @@ -32,10 +32,10 @@ export const getFormattedPlugin = (bundle: any) => { | |||
| } | |||
| } | |||
| export const getPluginLinkInMarketplace = (plugin: Plugin) => { | |||
| export const getPluginLinkInMarketplace = (plugin: Plugin, params?: Record<string, string | undefined>) => { | |||
| if (plugin.type === 'bundle') | |||
| return `${MARKETPLACE_URL_PREFIX}/bundles/${plugin.org}/${plugin.name}` | |||
| return `${MARKETPLACE_URL_PREFIX}/plugins/${plugin.org}/${plugin.name}` | |||
| return getMarketplaceUrl(`/bundles/${plugin.org}/${plugin.name}`, params) | |||
| return getMarketplaceUrl(`/plugins/${plugin.org}/${plugin.name}`, params) | |||
| } | |||
| export const getMarketplacePluginsByCollectionId = async (collectionId: string, query?: CollectionsAndPluginsSearchParams) => { | |||
| @@ -33,8 +33,9 @@ import { useGetLanguage } from '@/context/i18n' | |||
| import { useModalContext } from '@/context/modal-context' | |||
| import { useProviderContext } from '@/context/provider-context' | |||
| import { useInvalidateAllToolProviders } from '@/service/use-tools' | |||
| import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' | |||
| import { API_PREFIX } from '@/config' | |||
| import cn from '@/utils/classnames' | |||
| import { getMarketplaceUrl } from '@/utils/var' | |||
| const i18nPrefix = 'plugin.action' | |||
| @@ -87,7 +88,7 @@ const DetailHeader = ({ | |||
| if (isFromGitHub) | |||
| return `https://github.com/${meta!.repo}` | |||
| if (isFromMarketplace) | |||
| return `${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}${theme ? `?theme=${theme}` : ''}` | |||
| return getMarketplaceUrl(`/plugins/${author}/${name}`, { theme }) | |||
| return '' | |||
| }, [author, isFromGitHub, isFromMarketplace, meta, name, theme]) | |||
| @@ -21,13 +21,14 @@ import OrgInfo from '../card/base/org-info' | |||
| import Title from '../card/base/title' | |||
| import Action from './action' | |||
| import cn from '@/utils/classnames' | |||
| import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' | |||
| import { API_PREFIX } from '@/config' | |||
| import { useSingleCategories } from '../hooks' | |||
| import { useRenderI18nObject } from '@/hooks/use-i18n' | |||
| import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import { gte } from 'semver' | |||
| import Tooltip from '@/app/components/base/tooltip' | |||
| import { getMarketplaceUrl } from '@/utils/var' | |||
| type Props = { | |||
| className?: string | |||
| @@ -166,7 +167,7 @@ const PluginItem: FC<Props> = ({ | |||
| } | |||
| {source === PluginSource.marketplace | |||
| && <> | |||
| <a href={`${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}${theme ? `?theme=${theme}` : ''}`} target='_blank' className='flex items-center gap-0.5'> | |||
| <a href={getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })} target='_blank' className='flex items-center gap-0.5'> | |||
| <div className='system-2xs-medium-uppercase text-text-tertiary'>{t('plugin.from')} <span className='text-text-secondary'>marketplace</span></div> | |||
| <RiArrowRightUpLine className='h-3 w-3 text-text-tertiary' /> | |||
| </a> | |||
| @@ -1,4 +1,5 @@ | |||
| import React, { useMemo, useRef, useState } from 'react' | |||
| 'use client' | |||
| import React, { useEffect, useMemo, useRef, useState } from 'react' | |||
| import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' | |||
| import { FileZip } from '@/app/components/base/icons/src/vender/solid/files' | |||
| import { Github } from '@/app/components/base/icons/src/vender/solid/general' | |||
| @@ -14,12 +15,18 @@ import { noop } from 'lodash-es' | |||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| import Button from '@/app/components/base/button' | |||
| type InstallMethod = { | |||
| icon: React.FC<{ className?: string }> | |||
| text: string | |||
| action: string | |||
| } | |||
| const Empty = () => { | |||
| const { t } = useTranslation() | |||
| const fileInputRef = useRef<HTMLInputElement>(null) | |||
| const [selectedAction, setSelectedAction] = useState<string | null>(null) | |||
| const [selectedFile, setSelectedFile] = useState<File | null>(null) | |||
| const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) | |||
| const { enable_marketplace, plugin_installation_permission } = useGlobalPublicStore(s => s.systemFeatures) | |||
| const setActiveTab = usePluginPageContext(v => v.setActiveTab) | |||
| const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { | |||
| @@ -39,6 +46,22 @@ const Empty = () => { | |||
| return t('plugin.list.notFound') | |||
| }, [pluginList?.plugins.length, t, filters.categories.length, filters.tags.length, filters.searchQuery]) | |||
| const [installMethods, setInstallMethods] = useState<InstallMethod[]>([]) | |||
| useEffect(() => { | |||
| const methods = [] | |||
| if (enable_marketplace) | |||
| methods.push({ icon: MagicBox, text: t('plugin.source.marketplace'), action: 'marketplace' }) | |||
| if (plugin_installation_permission.restrict_to_marketplace_only) { | |||
| setInstallMethods(methods) | |||
| } | |||
| else { | |||
| methods.push({ icon: Github, text: t('plugin.source.github'), action: 'github' }) | |||
| methods.push({ icon: FileZip, text: t('plugin.source.local'), action: 'local' }) | |||
| setInstallMethods(methods) | |||
| } | |||
| }, [plugin_installation_permission, enable_marketplace, t]) | |||
| return ( | |||
| <div className='relative z-0 w-full grow'> | |||
| {/* skeleton */} | |||
| @@ -71,15 +94,7 @@ const Empty = () => { | |||
| accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS} | |||
| /> | |||
| <div className='flex w-full flex-col gap-y-1'> | |||
| {[ | |||
| ...( | |||
| (enable_marketplace) | |||
| ? [{ icon: MagicBox, text: t('plugin.list.source.marketplace'), action: 'marketplace' }] | |||
| : [] | |||
| ), | |||
| { icon: Github, text: t('plugin.list.source.github'), action: 'github' }, | |||
| { icon: FileZip, text: t('plugin.list.source.local'), action: 'local' }, | |||
| ].map(({ icon: Icon, text, action }) => ( | |||
| {installMethods.map(({ icon: Icon, text, action }) => ( | |||
| <Button | |||
| key={action} | |||
| className='justify-start gap-x-0.5 px-3' | |||
| @@ -136,7 +136,7 @@ const PluginPage = ({ | |||
| const options = usePluginPageContext(v => v.options) | |||
| const activeTab = usePluginPageContext(v => v.activeTab) | |||
| const setActiveTab = usePluginPageContext(v => v.setActiveTab) | |||
| const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) | |||
| const { enable_marketplace, branding } = useGlobalPublicStore(s => s.systemFeatures) | |||
| const isPluginsTab = useMemo(() => activeTab === PLUGIN_PAGE_TABS_MAP.plugins, [activeTab]) | |||
| const isExploringMarketplace = useMemo(() => { | |||
| @@ -225,7 +225,7 @@ const PluginPage = ({ | |||
| ) | |||
| } | |||
| { | |||
| canSetPermissions && ( | |||
| canSetPermissions && !branding.enabled && ( | |||
| <Tooltip | |||
| popupContent={t('plugin.privilege.title')} | |||
| > | |||
| @@ -1,6 +1,6 @@ | |||
| 'use client' | |||
| import { useRef, useState } from 'react' | |||
| import { useEffect, useRef, useState } from 'react' | |||
| import { RiAddLine, RiArrowDownSLine } from '@remixicon/react' | |||
| import Button from '@/app/components/base/button' | |||
| import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' | |||
| @@ -22,6 +22,13 @@ import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| type Props = { | |||
| onSwitchToMarketplaceTab: () => void | |||
| } | |||
| type InstallMethod = { | |||
| icon: React.FC<{ className?: string }> | |||
| text: string | |||
| action: string | |||
| } | |||
| const InstallPluginDropdown = ({ | |||
| onSwitchToMarketplaceTab, | |||
| }: Props) => { | |||
| @@ -30,7 +37,7 @@ const InstallPluginDropdown = ({ | |||
| const [isMenuOpen, setIsMenuOpen] = useState(false) | |||
| const [selectedAction, setSelectedAction] = useState<string | null>(null) | |||
| const [selectedFile, setSelectedFile] = useState<File | null>(null) | |||
| const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) | |||
| const { enable_marketplace, plugin_installation_permission } = useGlobalPublicStore(s => s.systemFeatures) | |||
| const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { | |||
| const file = event.target.files?.[0] | |||
| @@ -54,6 +61,22 @@ const InstallPluginDropdown = ({ | |||
| // console.log(res) | |||
| // } | |||
| const [installMethods, setInstallMethods] = useState<InstallMethod[]>([]) | |||
| useEffect(() => { | |||
| const methods = [] | |||
| if (enable_marketplace) | |||
| methods.push({ icon: MagicBox, text: t('plugin.source.marketplace'), action: 'marketplace' }) | |||
| if (plugin_installation_permission.restrict_to_marketplace_only) { | |||
| setInstallMethods(methods) | |||
| } | |||
| else { | |||
| methods.push({ icon: Github, text: t('plugin.source.github'), action: 'github' }) | |||
| methods.push({ icon: FileZip, text: t('plugin.source.local'), action: 'local' }) | |||
| setInstallMethods(methods) | |||
| } | |||
| }, [plugin_installation_permission, enable_marketplace, t]) | |||
| return ( | |||
| <PortalToFollowElem | |||
| open={isMenuOpen} | |||
| @@ -84,15 +107,7 @@ const InstallPluginDropdown = ({ | |||
| accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS} | |||
| /> | |||
| <div className='w-full'> | |||
| {[ | |||
| ...( | |||
| (enable_marketplace) | |||
| ? [{ icon: MagicBox, text: t('plugin.source.marketplace'), action: 'marketplace' }] | |||
| : [] | |||
| ), | |||
| { icon: Github, text: t('plugin.source.github'), action: 'github' }, | |||
| { icon: FileZip, text: t('plugin.source.local'), action: 'local' }, | |||
| ].map(({ icon: Icon, text, action }) => ( | |||
| {installMethods.map(({ icon: Icon, text, action }) => ( | |||
| <div | |||
| key={action} | |||
| className='flex w-full !cursor-pointer items-center gap-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover' | |||
| @@ -94,7 +94,11 @@ export type PluginManifestInMarket = { | |||
| introduction: string | |||
| verified: boolean | |||
| install_count: number | |||
| badges: string[] | |||
| badges: string[], | |||
| verification: { | |||
| authorized_category: 'langgenius' | 'partner' | 'community' | |||
| }, | |||
| from: Dependency['type'] | |||
| } | |||
| export type PluginDetail = { | |||
| @@ -145,7 +149,11 @@ export type Plugin = { | |||
| settings: CredentialFormSchemaBase[] | |||
| } | |||
| tags: { name: string }[] | |||
| badges: string[] | |||
| badges: string[], | |||
| verification: { | |||
| authorized_category: 'langgenius' | 'partner' | 'community' | |||
| }, | |||
| from: Dependency['type'] | |||
| } | |||
| export enum PermissionType { | |||
| @@ -12,7 +12,7 @@ import { useMarketplace } from './hooks' | |||
| import List from '@/app/components/plugins/marketplace/list' | |||
| import Loading from '@/app/components/base/loading' | |||
| import { getLocaleOnClient } from '@/i18n' | |||
| import { MARKETPLACE_URL_PREFIX } from '@/config' | |||
| import { getMarketplaceUrl } from '@/utils/var' | |||
| type MarketplaceProps = { | |||
| searchPluginText: string | |||
| @@ -84,7 +84,7 @@ const Marketplace = ({ | |||
| </span> | |||
| {t('common.operation.in')} | |||
| <a | |||
| href={`${MARKETPLACE_URL_PREFIX}?language=${locale}&q=${searchPluginText}&tags=${filterPluginTags.join(',')}${theme ? `&theme=${theme}` : ''}`} | |||
| href={getMarketplaceUrl('', { language: locale, q: searchPluginText, tags: filterPluginTags.join(','), theme })} | |||
| className='system-sm-medium ml-1 flex items-center text-text-accent' | |||
| target='_blank' | |||
| > | |||
| @@ -12,9 +12,9 @@ import { | |||
| PortalToFollowElemTrigger, | |||
| } from '@/app/components/base/portal-to-follow-elem' | |||
| import cn from '@/utils/classnames' | |||
| import { MARKETPLACE_URL_PREFIX } from '@/config' | |||
| import { useDownloadPlugin } from '@/service/use-plugins' | |||
| import { downloadFile } from '@/utils/format' | |||
| import { getMarketplaceUrl } from '@/utils/var' | |||
| type Props = { | |||
| open: boolean | |||
| @@ -80,7 +80,7 @@ const OperationDropdown: FC<Props> = ({ | |||
| <PortalToFollowElemContent className='z-[9999]'> | |||
| <div className='w-[112px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'> | |||
| <div onClick={handleDownload} className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.download')}</div> | |||
| <a href={`${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}${theme ? `?theme=${theme}` : ''}`} target='_blank' className='system-md-regular block cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.viewDetails')}</a> | |||
| <a href={getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })} target='_blank' className='system-md-regular block cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.viewDetails')}</a> | |||
| </div> | |||
| </PortalToFollowElemContent> | |||
| </PortalToFollowElem> | |||
| @@ -1,30 +0,0 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import classNames from '@/utils/classnames' | |||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| import { useTheme } from 'next-themes' | |||
| type LoginLogoProps = { | |||
| className?: string | |||
| } | |||
| const LoginLogo: FC<LoginLogoProps> = ({ | |||
| className, | |||
| }) => { | |||
| const { systemFeatures } = useGlobalPublicStore() | |||
| const { theme } = useTheme() | |||
| let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png` | |||
| if (systemFeatures.branding.enabled) | |||
| src = systemFeatures.branding.login_page_logo | |||
| return ( | |||
| <img | |||
| src={src} | |||
| className={classNames('block w-auto h-10', className)} | |||
| alt='logo' | |||
| /> | |||
| ) | |||
| } | |||
| export default LoginLogo | |||
| @@ -64,6 +64,8 @@ const translation = { | |||
| skip: 'Skip', | |||
| format: 'Format', | |||
| more: 'More', | |||
| selectAll: 'Select All', | |||
| deSelectAll: 'Deselect All', | |||
| }, | |||
| errorMsg: { | |||
| fieldRequired: '{{field}} is required', | |||
| @@ -154,6 +154,7 @@ const translation = { | |||
| next: 'Next', | |||
| pluginLoadError: 'Plugin load error', | |||
| pluginLoadErrorDesc: 'This plugin will not be installed', | |||
| installWarning: 'This plugin is not allowed to be installed.', | |||
| }, | |||
| installFromGitHub: { | |||
| installPlugin: 'Install plugin from GitHub', | |||
| @@ -64,6 +64,8 @@ const translation = { | |||
| in: '中', | |||
| format: 'フォーマット', | |||
| more: 'もっと', | |||
| selectAll: 'すべて選択', | |||
| deSelectAll: 'すべて選択解除', | |||
| }, | |||
| errorMsg: { | |||
| fieldRequired: '{{field}}は必要です', | |||
| @@ -137,6 +137,7 @@ const translation = { | |||
| installPlugin: 'プラグインをインストールする', | |||
| back: '戻る', | |||
| uploadingPackage: '{{packageName}}をアップロード中...', | |||
| installWarning: 'このプラグインはインストールを許可されていません。', | |||
| }, | |||
| installFromGitHub: { | |||
| installedSuccessfully: 'インストールに成功しました', | |||
| @@ -64,6 +64,8 @@ const translation = { | |||
| skip: '跳过', | |||
| format: '格式化', | |||
| more: '更多', | |||
| selectAll: '全选', | |||
| deSelectAll: '取消全选', | |||
| }, | |||
| errorMsg: { | |||
| fieldRequired: '{{field}} 为必填项', | |||
| @@ -154,6 +154,7 @@ const translation = { | |||
| next: '下一步', | |||
| pluginLoadError: '插件加载错误', | |||
| pluginLoadErrorDesc: '此插件将不会被安装', | |||
| installWarning: '此插件不允许安装。', | |||
| }, | |||
| installFromGitHub: { | |||
| installPlugin: '从 GitHub 安装插件', | |||
| @@ -13,12 +13,23 @@ export enum LicenseStatus { | |||
| LOST = 'lost', | |||
| } | |||
| export enum InstallationScope { | |||
| ALL = 'all', | |||
| NONE = 'none', | |||
| OFFICIAL_ONLY = 'official_only', | |||
| OFFICIAL_AND_PARTNER = 'official_and_specific_partners', | |||
| } | |||
| type License = { | |||
| status: LicenseStatus | |||
| expired_at: string | null | |||
| } | |||
| export type SystemFeatures = { | |||
| plugin_installation_permission: { | |||
| plugin_installation_scope: InstallationScope, | |||
| restrict_to_marketplace_only: boolean | |||
| }, | |||
| sso_enforced_for_signin: boolean | |||
| sso_enforced_for_signin_protocol: SSOProtocol | '' | |||
| sso_enforced_for_web: boolean | |||
| @@ -50,6 +61,10 @@ export type SystemFeatures = { | |||
| } | |||
| export const defaultSystemFeatures: SystemFeatures = { | |||
| plugin_installation_permission: { | |||
| plugin_installation_scope: InstallationScope.ALL, | |||
| restrict_to_marketplace_only: false, | |||
| }, | |||
| sso_enforced_for_signin: false, | |||
| sso_enforced_for_signin_protocol: '', | |||
| sso_enforced_for_web: false, | |||
| @@ -1,4 +1,4 @@ | |||
| import { MAX_VAR_KEY_LENGTH, VAR_ITEM_TEMPLATE, VAR_ITEM_TEMPLATE_IN_WORKFLOW, getMaxVarNameLength } from '@/config' | |||
| import { MARKETPLACE_URL_PREFIX, MAX_VAR_KEY_LENGTH, VAR_ITEM_TEMPLATE, VAR_ITEM_TEMPLATE_IN_WORKFLOW, getMaxVarNameLength } from '@/config' | |||
| import { | |||
| CONTEXT_PLACEHOLDER_TEXT, | |||
| HISTORY_PLACEHOLDER_TEXT, | |||
| @@ -108,3 +108,15 @@ export const getVars = (value: string) => { | |||
| // Set the value of basePath | |||
| // example: /dify | |||
| export const basePath = '' | |||
| export function getMarketplaceUrl(path: string, params?: Record<string, string | undefined>) { | |||
| const searchParams = new URLSearchParams({ source: encodeURIComponent(window.location.origin) }) | |||
| if (params) { | |||
| Object.keys(params).forEach((key) => { | |||
| const value = params[key] | |||
| if (value !== undefined && value !== null) | |||
| searchParams.append(key, value) | |||
| }) | |||
| } | |||
| return `${MARKETPLACE_URL_PREFIX}${path}?${searchParams.toString()}` | |||
| } | |||