From 1dc2d7f4a2caed4911899c0c9d354bf4cdcec5f2 Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 15 Nov 2024 16:47:56 +0800 Subject: [PATCH 01/11] chore: fix jump url --- .../workflow/block-selector/market-place-plugin/list.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/app/components/workflow/block-selector/market-place-plugin/list.tsx b/web/app/components/workflow/block-selector/market-place-plugin/list.tsx index 2e7e13be9e..540b7d924f 100644 --- a/web/app/components/workflow/block-selector/market-place-plugin/list.tsx +++ b/web/app/components/workflow/block-selector/market-place-plugin/list.tsx @@ -26,7 +26,7 @@ const List = ({ const { t } = useTranslation() const hasFilter = !searchText const hasRes = list.length > 0 - const urlWithSearchText = `${marketplaceUrlPrefix}/marketplace?q=${searchText}&tags=${tags.join(',')}` + const urlWithSearchText = `${marketplaceUrlPrefix}/?q=${searchText}&tags=${tags.join(',')}` const nextToStickyELemRef = useRef(null) const { handleScroll, scrollPosition } = useStickyScroll({ @@ -65,7 +65,7 @@ const List = ({ return ( {t('plugin.findMoreInMarketplace')} From f0e48859268120efb811d2232e40bbeb78f0cf62 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Fri, 15 Nov 2024 16:21:51 +0800 Subject: [PATCH 02/11] tooltip of endpoints --- .../plugin-detail-panel/endpoint-list.tsx | 26 +++++++++++++++++-- web/i18n/en-US/plugin.ts | 2 ++ web/i18n/zh-Hans/plugin.ts | 16 +++++++----- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx index b5f5d2768e..0dfc65404f 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx @@ -1,7 +1,11 @@ import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useBoolean } from 'ahooks' -import { RiAddLine } from '@remixicon/react' +import { + RiAddLine, + RiApps2AddLine, + RiBookOpenLine, +} from '@remixicon/react' import EndpointModal from './endpoint-modal' import EndpointCard from './endpoint-card' import { NAME_FIELD } from './utils' @@ -61,8 +65,26 @@ const EndpointList = ({ showTopBorder }: Props) => {
{t('plugin.detailPanel.endpoints')} TODO
+
+
+ +
+
{t('plugin.detailPanel.endpointsTip')}
+ {/* TODO endpoints doc link */} + +
+ + {t('plugin.detailPanel.endpointsDocLink')} +
+
+
} /> diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts index 16389b2129..e2f3ae7cf5 100644 --- a/web/i18n/en-US/plugin.ts +++ b/web/i18n/en-US/plugin.ts @@ -50,6 +50,8 @@ const translation = { }, actionNum: '{{num}} ACTIONS INCLUDED', endpoints: 'Endpoints', + endpointsTip: 'This plugin provides specific functionalities via endpoints, and you can configure multiple endpoint sets for current workspace.', + endpointsDocLink: 'View the document', endpointsEmpty: 'Click the \'+\' button to add an endpoint', endpointDisableTip: 'Disable Endpoint', endpointDisableContent: 'Would you like to disable {{name}}? ', diff --git a/web/i18n/zh-Hans/plugin.ts b/web/i18n/zh-Hans/plugin.ts index f4b1aeb53b..c537656e54 100644 --- a/web/i18n/zh-Hans/plugin.ts +++ b/web/i18n/zh-Hans/plugin.ts @@ -49,14 +49,16 @@ const translation = { remove: '移除', }, actionNum: '{{num}} ACTIONS 已包含', - endpoints: 'Endpoints', - endpointsEmpty: '点击 \'+\' 按钮添加 Endpoint', - endpointDisableTip: '停用 Endpoint', - endpointDisableContent: '是否要停用 {{name}} 的 Endpoint ?', - endpointDeleteTip: '移除 Endpoint', + endpoints: 'API 端点', + endpointsTip: '此插件通过 API 端点提供特定功能,您可以为当前工作区配置多个 API 端点集。', + endpointsDocLink: '查看文档', + endpointsEmpty: '点击 \'+\' 按钮添加 API 端点', + endpointDisableTip: '停用 API 端点', + endpointDisableContent: '是否要停用 {{name}} 的 API 端点 ?', + endpointDeleteTip: '移除 API 端点', endpointDeleteContent: '是否要移除 {{name}} ?', - endpointModalTitle: '设置 Endpoint', - endpointModalDesc: '配置表单后,工作区内的所有成员都可以在编排应用时使用此端点。', + endpointModalTitle: '设置 API 端点', + endpointModalDesc: '配置表单后,工作区内的所有成员都可以在编排应用时使用此 API 端点。', serviceOk: '服务正常', disabled: '停用', modelNum: '{{num}} 模型已包含', From 66f0e1209af55ea780583466c778a3ec70ff36bf Mon Sep 17 00:00:00 2001 From: JzoNg Date: Sun, 17 Nov 2024 12:57:34 +0800 Subject: [PATCH 03/11] switch version --- web/app/components/base/badge.tsx | 2 +- .../plugin-detail-panel/detail-header.tsx | 49 ++++++-- .../plugin-detail-panel/endpoint-list.tsx | 1 + web/app/components/plugins/types.ts | 14 +++ .../update-plugin/plugin-version-picker.tsx | 118 ++++++++++++++++++ web/hooks/use-timestamp.ts | 6 +- web/i18n/en-US/plugin.ts | 1 + web/i18n/zh-Hans/plugin.ts | 1 + web/service/use-plugins.ts | 16 ++- 9 files changed, 196 insertions(+), 12 deletions(-) create mode 100644 web/app/components/plugins/update-plugin/plugin-version-picker.tsx diff --git a/web/app/components/base/badge.tsx b/web/app/components/base/badge.tsx index c520fe02c9..c44057b9a4 100644 --- a/web/app/components/base/badge.tsx +++ b/web/app/components/base/badge.tsx @@ -3,7 +3,7 @@ import cn from '@/utils/classnames' type BadgeProps = { className?: string - text: string + text: string | React.ReactNode uppercase?: boolean hasRedCornerMark?: boolean } diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx index 97e61b66d8..024d5c2804 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx @@ -1,7 +1,8 @@ -import React, { useCallback, useMemo } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useBoolean } from 'ahooks' import { + RiArrowLeftRightLine, RiBugLine, RiCloseLine, RiHardDrive3Line, @@ -15,7 +16,9 @@ import Title from '../card/base/title' import OrgInfo from '../card/base/org-info' import { useGitHubReleases } from '../install-plugin/hooks' import { compareVersion, getLatestVersion } from '@/utils/semver' -import OperationDropdown from './operation-dropdown' +import PluginVersionPicker from '@/app/components/plugins/update-plugin/plugin-version-picker' +import UpdateFromMarketplace from '@/app/components/plugins/update-plugin/from-market-place' +import OperationDropdown from '@/app/components/plugins/plugin-detail-panel/operation-dropdown' import PluginInfo from '@/app/components/plugins/plugin-page/plugin-info' import ActionButton from '@/app/components/base/action-button' import Button from '@/app/components/base/button' @@ -28,7 +31,6 @@ import { Github } from '@/app/components/base/icons/src/public/common' import { uninstallPlugin } from '@/service/plugins' import { useGetLanguage } from '@/context/i18n' import { useModalContext } from '@/context/modal-context' -import UpdateFromMarketplace from '@/app/components/plugins/update-plugin/from-market-place' import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' import cn from '@/utils/classnames' @@ -58,11 +60,17 @@ const DetailHeader = ({ latest_unique_identifier, latest_version, meta, + plugin_id, } = detail const { author, name, label, description, icon, verified } = detail.declaration const isFromGitHub = source === PluginSource.github const isFromMarketplace = source === PluginSource.marketplace + const [isShow, setIsShow] = useState(false) + const [targetVersion, setTargetVersion] = useState({ + version: latest_version, + unique_identifier: latest_unique_identifier, + }) const hasNewVersion = useMemo(() => { if (isFromGitHub) return latest_version !== version @@ -167,10 +175,33 @@ const DetailHeader = ({
{verified && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />} - <Badge - className='mx-1' - text={version} - hasRedCornerMark={hasNewVersion} + <PluginVersionPicker + disabled={!isFromMarketplace || !hasNewVersion} + isShow={isShow} + onShowChange={setIsShow} + pluginID={plugin_id} + currentVersion={version} + onSelect={(state) => { + setTargetVersion(state) + handleUpdate() + }} + trigger={ + <Badge + className={cn( + 'mx-1', + isShow && 'bg-state-base-hover', + (isShow || isFromMarketplace) && 'hover:bg-state-base-hover', + )} + uppercase={false} + text={ + <> + <div>{version}</div> + {isFromMarketplace && <RiArrowLeftRightLine className='ml-1 w-3 h-3 text-text-tertiary' />} + </> + } + hasRedCornerMark={hasNewVersion} + /> + } /> {hasNewVersion && ( <Button variant='secondary-accent' size='small' className='!h-5' onClick={handleUpdate}>{t('plugin.detailPanel.operation.update')}</Button> @@ -254,8 +285,8 @@ const DetailHeader = ({ payload: detail.declaration, }, targetPackageInfo: { - id: latest_unique_identifier, - version: latest_version, + id: targetVersion.unique_identifier, + version: targetVersion.version, }, }} onCancel={hideUpdateModal} diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx index 0dfc65404f..812cdb8e20 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx @@ -65,6 +65,7 @@ const EndpointList = ({ showTopBorder }: Props) => { <div className='flex items-center gap-0.5'> {t('plugin.detailPanel.endpoints')} <Tooltip + position='right' needsDelay popupClassName='w-[240px] p-4 rounded-xl bg-components-panel-bg-blur border-[0.5px] border-components-panel-border' popupContent={ diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index 34049171ea..4ef98321e2 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -322,3 +322,17 @@ export type Dependency = { plugin_unique_identifier?: string } } + +export type Version = { + plugin_org: string + plugin_name: string + version: string + file_name: string + checksum: string + created_at: string + unique_identifier: string +} + +export type VersionListResponse = { + versions: Version[] +} diff --git a/web/app/components/plugins/update-plugin/plugin-version-picker.tsx b/web/app/components/plugins/update-plugin/plugin-version-picker.tsx new file mode 100644 index 0000000000..62f8f85233 --- /dev/null +++ b/web/app/components/plugins/update-plugin/plugin-version-picker.tsx @@ -0,0 +1,118 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import Badge from '@/app/components/base/badge' +import type { + OffsetOptions, + Placement, +} from '@floating-ui/react' +import { useVersionListOfPlugin } from '@/service/use-plugins' +import useTimestamp from '@/hooks/use-timestamp' +import cn from '@/utils/classnames' + +type Props = { + disabled?: boolean + isShow: boolean + onShowChange: (isShow: boolean) => void + pluginID: string + currentVersion: string + trigger: React.ReactNode + placement?: Placement + offset?: OffsetOptions + onSelect: ({ + version, + unique_identifier, + }: { + version: string + unique_identifier: string + }) => void +} + +const PluginVersionPicker: FC<Props> = ({ + disabled = false, + isShow, + onShowChange, + pluginID, + currentVersion, + trigger, + placement = 'bottom-start', + offset = { + mainAxis: 4, + crossAxis: -16, + }, + onSelect, +}) => { + const { t } = useTranslation() + const format = t('appLog.dateTimeFormat').split(' ')[0] + const { formatDate } = useTimestamp() + + const handleTriggerClick = () => { + if (disabled) return + onShowChange(true) + } + + const { data: res } = useVersionListOfPlugin(pluginID) + + const handleSelect = useCallback(({ version, unique_identifier }: { + version: string + unique_identifier: string + }) => { + if (currentVersion === version) + return + onSelect({ version, unique_identifier }) + onShowChange(false) + }, [currentVersion, onSelect]) + + return ( + <PortalToFollowElem + placement={placement} + offset={offset} + open={isShow} + onOpenChange={onShowChange} + > + <PortalToFollowElemTrigger + className={cn('inline-flex items-center cursor-pointer', disabled && 'cursor-default')} + onClick={handleTriggerClick} + > + {trigger} + </PortalToFollowElemTrigger> + + <PortalToFollowElemContent className='z-[1000]'> + <div className="relative w-[209px] p-1 rounded-xl bg-components-panel-bg-blur border-[0.5px] border-components-panel-border shadow-lg"> + <div className='px-3 pt-1 pb-0.5 text-text-tertiary system-xs-medium-uppercase'> + {t('plugin.detailPanel.switchVersion')} + </div> + <div className='relative'> + {res?.data.versions.map(version => ( + <div + key={version.unique_identifier} + className={cn( + 'h-7 px-3 py-1 flex items-center gap-1 rounded-lg hover:bg-state-base-hover cursor-pointer', + currentVersion === version.version && 'opacity-30 cursor-default hover:bg-transparent', + )} + onClick={() => handleSelect({ + version: version.version, + unique_identifier: version.unique_identifier, + })} + > + <div className='grow flex items-center'> + <div className='text-text-secondary system-sm-medium'>{version.version}</div> + {currentVersion === version.version && <Badge className='ml-1' text='CURRENT'/>} + </div> + <div className='shrink-0 text-text-tertiary system-xs-regular'>{formatDate(version.created_at, format)}</div> + </div> + ))} + </div> + </div> + </PortalToFollowElemContent> + </PortalToFollowElem> + ) +} + +export default React.memo(PluginVersionPicker) diff --git a/web/hooks/use-timestamp.ts b/web/hooks/use-timestamp.ts index 05cc48eaad..5242eb565a 100644 --- a/web/hooks/use-timestamp.ts +++ b/web/hooks/use-timestamp.ts @@ -15,7 +15,11 @@ const useTimestamp = () => { return dayjs.unix(value).tz(timezone).format(format) }, [timezone]) - return { formatTime } + const formatDate = useCallback((value: string, format: string) => { + return dayjs(value).tz(timezone).format(format) + }, [timezone]) + + return { formatTime, formatDate } } export default useTimestamp diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts index e2f3ae7cf5..0e8e6dfccd 100644 --- a/web/i18n/en-US/plugin.ts +++ b/web/i18n/en-US/plugin.ts @@ -33,6 +33,7 @@ const translation = { local: 'Local Package File', }, detailPanel: { + switchVersion: 'Switch Version', categoryTip: { marketplace: 'Installed from Marketplace', github: 'Installed from Github', diff --git a/web/i18n/zh-Hans/plugin.ts b/web/i18n/zh-Hans/plugin.ts index c537656e54..c1ad4e0d67 100644 --- a/web/i18n/zh-Hans/plugin.ts +++ b/web/i18n/zh-Hans/plugin.ts @@ -33,6 +33,7 @@ const translation = { local: '本地插件', }, detailPanel: { + switchVersion: '切换版本', categoryTip: { marketplace: '从 Marketplace 安装', github: '从 Github 安装', diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 8ab970db13..8c45f35301 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -7,13 +7,14 @@ import type { Permissions, PluginTask, PluginsFromMarketplaceResponse, + VersionListResponse, uploadGitHubResponse, } from '@/app/components/plugins/types' import { TaskStatus } from '@/app/components/plugins/types' import type { PluginsSearchParams, } from '@/app/components/plugins/marketplace/types' -import { get, post, postMarketplace } from './base' +import { get, getMarketplace, post, postMarketplace } from './base' import { useMutation, useQuery, @@ -52,6 +53,19 @@ export const useInstallPackageFromMarketPlace = () => { }) } +export const useVersionListOfPlugin = (pluginID: string) => { + return useQuery<{ data: VersionListResponse }>({ + queryKey: [NAME_SPACE, 'versions', pluginID], + queryFn: () => getMarketplace<{ data: VersionListResponse }>(`/plugins/${pluginID}/versions`, { params: { page: 1, page_size: 100 } }), + }) +} +export const useInvalidateVersionListOfPlugin = () => { + const queryClient = useQueryClient() + return (pluginID: string) => { + queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'versions', pluginID] }) + } +} + export const useInstallPackageFromLocal = () => { return useMutation({ mutationFn: (uniqueIdentifier: string) => { From afdfc8c60984ee123de163dd79c9ee7a763fbb41 Mon Sep 17 00:00:00 2001 From: JzoNg <jzongcode@gmail.com> Date: Mon, 18 Nov 2024 11:02:37 +0800 Subject: [PATCH 04/11] chore: enabled for useQuery --- .../plugin-detail-panel/app-selector/app-inputs-panel.tsx | 2 +- web/service/use-workflow.ts | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel.tsx index 54fbfc75a9..d293be3aad 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel.tsx @@ -33,7 +33,7 @@ const AppInputsPanel = ({ const isBasicApp = appDetail.mode !== 'advanced-chat' && appDetail.mode !== 'workflow' const { data: fileUploadConfig } = useFileUploadConfig() const { data: currentApp, isFetching: isAppLoading } = useAppDetail(appDetail.id) - const { data: currentWorkflow, isFetching: isWorkflowLoading } = useAppWorkflow(isBasicApp ? 'empty' : appDetail.id) + const { data: currentWorkflow, isFetching: isWorkflowLoading } = useAppWorkflow(isBasicApp ? '' : appDetail.id) const isLoading = isAppLoading || isWorkflowLoading const basicAppFileConfig = useMemo(() => { diff --git a/web/service/use-workflow.ts b/web/service/use-workflow.ts index 9413854726..2b8e81b700 100644 --- a/web/service/use-workflow.ts +++ b/web/service/use-workflow.ts @@ -8,11 +8,8 @@ const NAME_SPACE = 'workflow' export const useAppWorkflow = (appID: string) => { return useQuery<FetchWorkflowDraftResponse>({ + enabled: !!appID, queryKey: [NAME_SPACE, 'publish', appID], - queryFn: () => { - if (appID === 'empty') - return Promise.resolve({} as unknown as FetchWorkflowDraftResponse) - return get<FetchWorkflowDraftResponse>(`/apps/${appID}/workflows/publish`) - }, + queryFn: () => get<FetchWorkflowDraftResponse>(`/apps/${appID}/workflows/publish`), }) } From 812fbab57b99f9aa9bf2cd8161419b66d1b57161 Mon Sep 17 00:00:00 2001 From: AkaraChen <akarachen@outlook.com> Date: Mon, 18 Nov 2024 11:19:58 +0800 Subject: [PATCH 05/11] fix: dataset details --- web/context/dataset-detail.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/web/context/dataset-detail.ts b/web/context/dataset-detail.ts index de046ce7a0..63c858b240 100644 --- a/web/context/dataset-detail.ts +++ b/web/context/dataset-detail.ts @@ -1,8 +1,7 @@ -import { createContext, useContext } from 'use-context-selector' import type { DataSet } from '@/models/datasets' -const DatasetDetailContext = createContext<{ indexingTechnique?: string; dataset?: DataSet; mutateDatasetRes?: () => void }>({}) - -export const useDatasetDetailContext = () => useContext(DatasetDetailContext) +const [, useDatasetDetailContext, DatasetDetailContext] = createSelectorCtx<{ indexingTechnique?: string; dataset?: DataSet; mutateDatasetRes?: () => void }>({ + defaultValue: {}, +}) export default DatasetDetailContext From 87ca20c04792590dfebe612aa38b2088c2bb9e04 Mon Sep 17 00:00:00 2001 From: twwu <twwu@dify.ai> Date: Mon, 18 Nov 2024 11:54:51 +0800 Subject: [PATCH 06/11] feat: add version checking for GitHub releases and improve error handling --- .../plugins/install-plugin/hooks.ts | 18 +++++++++- .../plugin-detail-panel/detail-header.tsx | 36 +++++++++---------- .../components/plugins/plugin-item/action.tsx | 28 ++++++++------- .../components/plugins/plugin-item/index.tsx | 2 +- 4 files changed, 52 insertions(+), 32 deletions(-) diff --git a/web/app/components/plugins/install-plugin/hooks.ts b/web/app/components/plugins/install-plugin/hooks.ts index 8ad26985cd..50ed9ab085 100644 --- a/web/app/components/plugins/install-plugin/hooks.ts +++ b/web/app/components/plugins/install-plugin/hooks.ts @@ -2,6 +2,8 @@ import Toast from '@/app/components/base/toast' import { uploadGitHub } from '@/service/plugins' import { Octokit } from '@octokit/core' import { GITHUB_ACCESS_TOKEN } from '@/config' +import { compareVersion, getLatestVersion } from '@/utils/semver' +import type { GitHubRepoReleaseResponse } from '../types' export const useGitHubReleases = () => { const fetchReleases = async (owner: string, repo: string) => { @@ -37,7 +39,21 @@ export const useGitHubReleases = () => { } } - return { fetchReleases } + const checkForUpdates = (fetchedReleases: GitHubRepoReleaseResponse[], currentVersion: string) => { + if (fetchedReleases.length === 0) throw new Error('No releases found') + const versions = fetchedReleases.map(release => release.tag_name) + const latestVersion = getLatestVersion(versions) + let res = false + try { + res = compareVersion(latestVersion, currentVersion) === 1 + } + catch { + throw new Error('Failed to compare versions, please check the version format.') + } + return res + } + + return { fetchReleases, checkForUpdates } } export const useGitHubUpload = () => { diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx index 024d5c2804..c28a091518 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx @@ -15,7 +15,6 @@ import Icon from '../card/base/card-icon' import Title from '../card/base/title' import OrgInfo from '../card/base/org-info' import { useGitHubReleases } from '../install-plugin/hooks' -import { compareVersion, getLatestVersion } from '@/utils/semver' import PluginVersionPicker from '@/app/components/plugins/update-plugin/plugin-version-picker' import UpdateFromMarketplace from '@/app/components/plugins/update-plugin/from-market-place' import OperationDropdown from '@/app/components/plugins/plugin-detail-panel/operation-dropdown' @@ -49,7 +48,7 @@ const DetailHeader = ({ }: Props) => { const { t } = useTranslation() const locale = useGetLanguage() - const { fetchReleases } = useGitHubReleases() + const { checkForUpdates, fetchReleases } = useGitHubReleases() const { setShowUpdatePluginModal } = useModalContext() const { @@ -72,14 +71,11 @@ const DetailHeader = ({ unique_identifier: latest_unique_identifier, }) const hasNewVersion = useMemo(() => { - if (isFromGitHub) - return latest_version !== version - if (isFromMarketplace) return !!latest_version && latest_version !== version return false - }, [isFromGitHub, isFromMarketplace, latest_version, version]) + }, [isFromMarketplace, latest_version, version]) const [isShowUpdateModal, { setTrue: showUpdateModal, @@ -94,11 +90,7 @@ const DetailHeader = ({ try { const fetchedReleases = await fetchReleases(author, name) - if (fetchedReleases.length === 0) - return - const versions = fetchedReleases.map(release => release.tag_name) - const latestVersion = getLatestVersion(versions) - if (compareVersion(latestVersion, version) === 1) { + if (checkForUpdates(fetchedReleases, meta!.version)) { setShowUpdatePluginModal({ onSaveCallback: () => { onUpdate() @@ -107,7 +99,7 @@ const DetailHeader = ({ type: PluginSource.github, github: { originalPackageInfo: { - id: installation_id, + id: detail.plugin_unique_identifier, repo: meta!.repo, version: meta!.version, package: meta!.package, @@ -124,11 +116,19 @@ const DetailHeader = ({ }) } } - catch { - Toast.notify({ - type: 'error', - message: 'Failed to compare versions', - }) + catch (error) { + if (error instanceof Error) { + Toast.notify({ + type: 'error', + message: error.message, + }) + } + else { + Toast.notify({ + type: 'error', + message: 'Failed to compare versions', + }) + } } } @@ -203,7 +203,7 @@ const DetailHeader = ({ /> } /> - {hasNewVersion && ( + {(hasNewVersion || isFromGitHub) && ( <Button variant='secondary-accent' size='small' className='!h-5' onClick={handleUpdate}>{t('plugin.detailPanel.operation.update')}</Button> )} </div> diff --git a/web/app/components/plugins/plugin-item/action.tsx b/web/app/components/plugins/plugin-item/action.tsx index 52c8c10309..773e42fdc9 100644 --- a/web/app/components/plugins/plugin-item/action.tsx +++ b/web/app/components/plugins/plugin-item/action.tsx @@ -11,7 +11,6 @@ import Tooltip from '../../base/tooltip' import Confirm from '../../base/confirm' import { uninstallPlugin } from '@/service/plugins' import { useGitHubReleases } from '../install-plugin/hooks' -import { compareVersion, getLatestVersion } from '@/utils/semver' import Toast from '@/app/components/base/toast' import { useModalContext } from '@/context/modal-context' import { useInvalidateInstalledPluginList } from '@/service/use-plugins' @@ -50,18 +49,14 @@ const Action: FC<Props> = ({ setTrue: showDeleting, setFalse: hideDeleting, }] = useBoolean(false) - const { fetchReleases } = useGitHubReleases() + const { checkForUpdates, fetchReleases } = useGitHubReleases() const { setShowUpdatePluginModal } = useModalContext() const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const handleFetchNewVersion = async () => { try { const fetchedReleases = await fetchReleases(author, pluginName) - if (fetchedReleases.length === 0) - return - const versions = fetchedReleases.map(release => release.tag_name) - const latestVersion = getLatestVersion(versions) - if (compareVersion(latestVersion, meta!.version) === 1) { + if (checkForUpdates(fetchedReleases, meta!.version)) { setShowUpdatePluginModal({ onSaveCallback: () => { invalidateInstalledPluginList() @@ -87,11 +82,19 @@ const Action: FC<Props> = ({ }) } } - catch { - Toast.notify({ - type: 'error', - message: 'Failed to compare versions', - }) + catch (error) { + if (error instanceof Error) { + Toast.notify({ + type: 'error', + message: error.message, + }) + } + else { + Toast.notify({ + type: 'error', + message: 'Failed to compare versions', + }) + } } } @@ -108,6 +111,7 @@ const Action: FC<Props> = ({ hideDeleteConfirm() onDelete() } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [installationId, onDelete]) return ( <div className='flex space-x-1'> diff --git a/web/app/components/plugins/plugin-item/index.tsx b/web/app/components/plugins/plugin-item/index.tsx index 4ac2c80d7d..eb833e0781 100644 --- a/web/app/components/plugins/plugin-item/index.tsx +++ b/web/app/components/plugins/plugin-item/index.tsx @@ -82,7 +82,7 @@ const PluginItem: FC<Props> = ({ <div className="flex items-center h-5"> <Title title={label[locale]} /> {verified && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />} - <Badge className='ml-1' text={plugin.version} /> + <Badge className='ml-1' text={source === PluginSource.github ? plugin.meta!.version : plugin.version} /> </div> <div className='flex items-center justify-between'> <Description text={description[locale]} descriptionLineRows={1}></Description> From d3fe6fd303edfdc4dfebd5d666ab5e3f3ac17b26 Mon Sep 17 00:00:00 2001 From: twwu <twwu@dify.ai> Date: Mon, 18 Nov 2024 11:57:31 +0800 Subject: [PATCH 07/11] fix: display version from GitHub metadata if available --- .../components/plugins/plugin-detail-panel/detail-header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx index c28a091518..810c815bad 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx @@ -195,7 +195,7 @@ const DetailHeader = ({ uppercase={false} text={ <> - <div>{version}</div> + <div>{isFromGitHub ? meta!.version : version}</div> {isFromMarketplace && <RiArrowLeftRightLine className='ml-1 w-3 h-3 text-text-tertiary' />} </> } From 07174cf52c2da8500448b0e825e1b9bed40f4601 Mon Sep 17 00:00:00 2001 From: AkaraChen <akarachen@outlook.com> Date: Mon, 18 Nov 2024 15:41:31 +0800 Subject: [PATCH 08/11] Revert "fix: dataset details" This reverts commit 812fbab57b99f9aa9bf2cd8161419b66d1b57161. --- web/context/dataset-detail.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web/context/dataset-detail.ts b/web/context/dataset-detail.ts index 63c858b240..de046ce7a0 100644 --- a/web/context/dataset-detail.ts +++ b/web/context/dataset-detail.ts @@ -1,7 +1,8 @@ +import { createContext, useContext } from 'use-context-selector' import type { DataSet } from '@/models/datasets' -const [, useDatasetDetailContext, DatasetDetailContext] = createSelectorCtx<{ indexingTechnique?: string; dataset?: DataSet; mutateDatasetRes?: () => void }>({ - defaultValue: {}, -}) +const DatasetDetailContext = createContext<{ indexingTechnique?: string; dataset?: DataSet; mutateDatasetRes?: () => void }>({}) + +export const useDatasetDetailContext = () => useContext(DatasetDetailContext) export default DatasetDetailContext From 87b23a1fac901a5678f1a55f28a7f9a5263cb962 Mon Sep 17 00:00:00 2001 From: twwu <twwu@dify.ai> Date: Tue, 19 Nov 2024 11:20:17 +0800 Subject: [PATCH 09/11] feat: refactor GitHub releases fetching and update handling with improved error notifications --- .../plugins/install-plugin/hooks.ts | 58 ++++++++++-------- .../plugin-detail-panel/detail-header.tsx | 59 +++++++------------ .../components/plugins/plugin-item/action.tsx | 59 +++++++------------ .../repos/[owner]/[repo]/releases/route.ts | 36 +++++++++++ web/package.json | 1 + web/pnpm-lock.yaml | 3 + 6 files changed, 114 insertions(+), 102 deletions(-) create mode 100644 web/app/repos/[owner]/[repo]/releases/route.ts diff --git a/web/app/components/plugins/install-plugin/hooks.ts b/web/app/components/plugins/install-plugin/hooks.ts index 50ed9ab085..b2a5af4a2f 100644 --- a/web/app/components/plugins/install-plugin/hooks.ts +++ b/web/app/components/plugins/install-plugin/hooks.ts @@ -1,26 +1,16 @@ -import Toast from '@/app/components/base/toast' +import Toast, { type IToastProps } from '@/app/components/base/toast' import { uploadGitHub } from '@/service/plugins' -import { Octokit } from '@octokit/core' -import { GITHUB_ACCESS_TOKEN } from '@/config' import { compareVersion, getLatestVersion } from '@/utils/semver' import type { GitHubRepoReleaseResponse } from '../types' export const useGitHubReleases = () => { const fetchReleases = async (owner: string, repo: string) => { try { - const octokit = new Octokit({ - auth: GITHUB_ACCESS_TOKEN, - }) - const res = await octokit.request('GET /repos/{owner}/{repo}/releases', { - owner, - repo, - headers: { - 'X-GitHub-Api-Version': '2022-11-28', - }, - }) - if (res.status !== 200) throw new Error('Failed to fetch releases') + const res = await fetch(`/repos/${owner}/${repo}/releases`) + const bodyJson = await res.json() + if (bodyJson.status !== 200) throw new Error(bodyJson.data.message) - const formattedReleases = res.data.map((release: any) => ({ + const formattedReleases = bodyJson.data.map((release: any) => ({ tag_name: release.tag_name, assets: release.assets.map((asset: any) => ({ browser_download_url: asset.browser_download_url, @@ -31,26 +21,46 @@ export const useGitHubReleases = () => { return formattedReleases } catch (error) { - Toast.notify({ - type: 'error', - message: 'Failed to fetch repository releases', - }) + if (error instanceof Error) { + Toast.notify({ + type: 'error', + message: error.message, + }) + } + else { + Toast.notify({ + type: 'error', + message: 'Failed to fetch repository releases', + }) + } return [] } } const checkForUpdates = (fetchedReleases: GitHubRepoReleaseResponse[], currentVersion: string) => { - if (fetchedReleases.length === 0) throw new Error('No releases found') + let needUpdate = false + const toastProps: IToastProps = { + type: 'info', + message: 'No new version available', + } + if (fetchedReleases.length === 0) { + toastProps.type = 'error' + toastProps.message = 'Input releases is empty' + return { needUpdate, toastProps } + } const versions = fetchedReleases.map(release => release.tag_name) const latestVersion = getLatestVersion(versions) - let res = false try { - res = compareVersion(latestVersion, currentVersion) === 1 + needUpdate = compareVersion(latestVersion, currentVersion) === 1 + if (needUpdate) + toastProps.message = `New version available: ${latestVersion}` } catch { - throw new Error('Failed to compare versions, please check the version format.') + needUpdate = false + toastProps.type = 'error' + toastProps.message = 'Fail to compare versions, please check the version format' } - return res + return { needUpdate, toastProps } } return { fetchReleases, checkForUpdates } diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx index 810c815bad..5e1b0770f7 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx @@ -88,47 +88,28 @@ const DetailHeader = ({ return } - try { - const fetchedReleases = await fetchReleases(author, name) - if (checkForUpdates(fetchedReleases, meta!.version)) { - setShowUpdatePluginModal({ - onSaveCallback: () => { - onUpdate() - }, - payload: { - type: PluginSource.github, - github: { - originalPackageInfo: { - id: detail.plugin_unique_identifier, - repo: meta!.repo, - version: meta!.version, - package: meta!.package, - releases: fetchedReleases, - }, + const fetchedReleases = await fetchReleases(author, name) + if (fetchedReleases.length === 0) return + const { needUpdate, toastProps } = checkForUpdates(fetchedReleases, meta!.version) + Toast.notify(toastProps) + if (needUpdate) { + setShowUpdatePluginModal({ + onSaveCallback: () => { + onUpdate() + }, + payload: { + type: PluginSource.github, + github: { + originalPackageInfo: { + id: detail.plugin_unique_identifier, + repo: meta!.repo, + version: meta!.version, + package: meta!.package, + releases: fetchedReleases, }, }, - }) - } - else { - Toast.notify({ - type: 'info', - message: 'No new version available', - }) - } - } - catch (error) { - if (error instanceof Error) { - Toast.notify({ - type: 'error', - message: error.message, - }) - } - else { - Toast.notify({ - type: 'error', - message: 'Failed to compare versions', - }) - } + }, + }) } } diff --git a/web/app/components/plugins/plugin-item/action.tsx b/web/app/components/plugins/plugin-item/action.tsx index 773e42fdc9..a387727b4f 100644 --- a/web/app/components/plugins/plugin-item/action.tsx +++ b/web/app/components/plugins/plugin-item/action.tsx @@ -54,47 +54,28 @@ const Action: FC<Props> = ({ const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const handleFetchNewVersion = async () => { - try { - const fetchedReleases = await fetchReleases(author, pluginName) - if (checkForUpdates(fetchedReleases, meta!.version)) { - setShowUpdatePluginModal({ - onSaveCallback: () => { - invalidateInstalledPluginList() - }, - payload: { - type: PluginSource.github, - github: { - originalPackageInfo: { - id: pluginUniqueIdentifier, - repo: meta!.repo, - version: meta!.version, - package: meta!.package, - releases: fetchedReleases, - }, + const fetchedReleases = await fetchReleases(author, pluginName) + if (fetchReleases.length === 0) return + const { needUpdate, toastProps } = checkForUpdates(fetchedReleases, meta!.version) + Toast.notify(toastProps) + if (needUpdate) { + setShowUpdatePluginModal({ + onSaveCallback: () => { + invalidateInstalledPluginList() + }, + payload: { + type: PluginSource.github, + github: { + originalPackageInfo: { + id: pluginUniqueIdentifier, + repo: meta!.repo, + version: meta!.version, + package: meta!.package, + releases: fetchedReleases, }, }, - }) - } - else { - Toast.notify({ - type: 'info', - message: 'No new version available', - }) - } - } - catch (error) { - if (error instanceof Error) { - Toast.notify({ - type: 'error', - message: error.message, - }) - } - else { - Toast.notify({ - type: 'error', - message: 'Failed to compare versions', - }) - } + }, + }) } } diff --git a/web/app/repos/[owner]/[repo]/releases/route.ts b/web/app/repos/[owner]/[repo]/releases/route.ts new file mode 100644 index 0000000000..29b604d94b --- /dev/null +++ b/web/app/repos/[owner]/[repo]/releases/route.ts @@ -0,0 +1,36 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { Octokit } from '@octokit/core' +import { RequestError } from '@octokit/request-error' +import { GITHUB_ACCESS_TOKEN } from '@/config' + +type Params = { + owner: string, + repo: string, +} + +const octokit = new Octokit({ + auth: GITHUB_ACCESS_TOKEN, +}) + +export async function GET( + request: NextRequest, + { params }: { params: Promise<Params> }, +) { + const { owner, repo } = (await params) + try { + const releasesRes = await octokit.request('GET /repos/{owner}/{repo}/releases', { + owner, + repo, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }) + return NextResponse.json(releasesRes) + } + catch (error) { + if (error instanceof RequestError) + return NextResponse.json(error.response) + else + throw error + } +} diff --git a/web/package.json b/web/package.json index bb60bedd5c..2d415f7f10 100644 --- a/web/package.json +++ b/web/package.json @@ -38,6 +38,7 @@ "@monaco-editor/react": "^4.6.0", "@next/mdx": "^14.0.4", "@octokit/core": "^6.1.2", + "@octokit/request-error": "^6.1.5", "@remixicon/react": "^4.3.0", "@sentry/react": "^7.54.0", "@sentry/utils": "^7.54.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 2c43d1fe44..68f9d2e3d6 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -55,6 +55,9 @@ importers: '@octokit/core': specifier: ^6.1.2 version: 6.1.2 + '@octokit/request-error': + specifier: ^6.1.5 + version: 6.1.5 '@remixicon/react': specifier: ^4.3.0 version: 4.3.0(react@18.2.0) From c5c06c18f1a9a32476277fd6e7909a9ff6429b14 Mon Sep 17 00:00:00 2001 From: Joel <iamjoel007@gmail.com> Date: Tue, 19 Nov 2024 15:05:15 +0800 Subject: [PATCH 10/11] feat: can upload and parse bundle --- .../(commonLayout)/plugins/test/card/page.tsx | 2 +- .../install-plugin/install-bundle/index.tsx | 34 +++------ .../install-bundle/ready-to-install.tsx | 48 +++++++++++++ .../install-bundle/steps/installed.tsx | 4 +- .../install-from-local-package/index.tsx | 71 +++++++++---------- .../ready-to-install.tsx | 68 ++++++++++++++++++ .../steps/uploading.tsx | 21 ++++-- .../plugins/plugin-page/empty/index.tsx | 7 +- .../components/plugins/plugin-page/index.tsx | 3 +- .../plugin-page/install-plugin-dropdown.tsx | 5 +- web/config/index.ts | 2 + web/service/plugins.ts | 6 +- 12 files changed, 188 insertions(+), 83 deletions(-) create mode 100644 web/app/components/plugins/install-plugin/install-bundle/ready-to-install.tsx create mode 100644 web/app/components/plugins/install-plugin/install-from-local-package/ready-to-install.tsx diff --git a/web/app/(commonLayout)/plugins/test/card/page.tsx b/web/app/(commonLayout)/plugins/test/card/page.tsx index 534789c00c..02e80d8563 100644 --- a/web/app/(commonLayout)/plugins/test/card/page.tsx +++ b/web/app/(commonLayout)/plugins/test/card/page.tsx @@ -47,7 +47,7 @@ const PluginList = () => { { type: 'marketplace', value: { - plugin_unique_identifier: 'langgenius/openai:0.0.1@f88fdb98d104466db16a425bfe3af8c1bcad45047a40fb802d98a989ac57a5a3', + plugin_unique_identifier: 'langgenius/openai:0.0.2@7baee9635a07573ea192621ebfdacb39db466fa691e75255beaf48bf41d44375', }, }, ]} /> diff --git a/web/app/components/plugins/install-plugin/install-bundle/index.tsx b/web/app/components/plugins/install-plugin/install-bundle/index.tsx index 6a7397fa6c..89323de3a0 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/index.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/index.tsx @@ -3,9 +3,8 @@ import type { FC } from 'react' import Modal from '@/app/components/base/modal' import React, { useCallback, useState } from 'react' import { InstallStep } from '../../types' -import type { Dependency, InstallStatusResponse, Plugin } from '../../types' -import Install from './steps/install' -import Installed from './steps/installed' +import type { Dependency } from '../../types' +import ReadyToInstall from './ready-to-install' import { useTranslation } from 'react-i18next' const i18nPrefix = 'plugin.installModal' @@ -30,8 +29,7 @@ const InstallBundle: FC<Props> = ({ }) => { const { t } = useTranslation() const [step, setStep] = useState<InstallStep>(installType === InstallType.fromMarketplace ? InstallStep.readyToInstall : InstallStep.uploading) - const [installedPlugins, setInstalledPlugins] = useState<Plugin[]>([]) - const [installStatus, setInstallStatus] = useState<InstallStatusResponse[]>([]) + const getTitle = useCallback(() => { if (step === InstallStep.uploadFailed) return t(`${i18nPrefix}.uploadFailed`) @@ -43,12 +41,6 @@ const InstallBundle: FC<Props> = ({ return t(`${i18nPrefix}.installPlugin`) }, [step, t]) - const handleInstalled = useCallback((plugins: Plugin[], installStatus: InstallStatusResponse[]) => { - setInstallStatus(installStatus) - setInstalledPlugins(plugins) - setStep(InstallStep.installed) - }, []) - return ( <Modal isShow={true} @@ -61,20 +53,12 @@ const InstallBundle: FC<Props> = ({ {getTitle()} </div> </div> - {step === InstallStep.readyToInstall && ( - <Install - fromDSLPayload={fromDSLPayload} - onCancel={onClose} - onInstalled={handleInstalled} - /> - )} - {step === InstallStep.installed && ( - <Installed - list={installedPlugins} - installStatus={installStatus} - onCancel={onClose} - /> - )} + <ReadyToInstall + step={step} + onStepChange={setStep} + dependencies={fromDSLPayload} + onClose={onClose} + /> </Modal> ) } diff --git a/web/app/components/plugins/install-plugin/install-bundle/ready-to-install.tsx b/web/app/components/plugins/install-plugin/install-bundle/ready-to-install.tsx new file mode 100644 index 0000000000..e9f89cb5c6 --- /dev/null +++ b/web/app/components/plugins/install-plugin/install-bundle/ready-to-install.tsx @@ -0,0 +1,48 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback, useState } from 'react' +import { InstallStep } from '../../types' +import Install from './steps/install' +import Installed from './steps/installed' +import type { Dependency, InstallStatusResponse, Plugin } from '../../types' + +type Props = { + step: InstallStep + onStepChange: (step: InstallStep) => void, + dependencies: Dependency[] + onClose: () => void +} + +const ReadyToInstall: FC<Props> = ({ + step, + onStepChange, + dependencies, + onClose, +}) => { + const [installedPlugins, setInstalledPlugins] = useState<Plugin[]>([]) + const [installStatus, setInstallStatus] = useState<InstallStatusResponse[]>([]) + const handleInstalled = useCallback((plugins: Plugin[], installStatus: InstallStatusResponse[]) => { + setInstallStatus(installStatus) + setInstalledPlugins(plugins) + onStepChange(InstallStep.installed) + }, [onStepChange]) + return ( + <> + {step === InstallStep.readyToInstall && ( + <Install + fromDSLPayload={dependencies} + onCancel={onClose} + onInstalled={handleInstalled} + /> + )} + {step === InstallStep.installed && ( + <Installed + list={installedPlugins} + installStatus={installStatus} + onCancel={onClose} + /> + )} + </> + ) +} +export default React.memo(ReadyToInstall) diff --git a/web/app/components/plugins/install-plugin/install-bundle/steps/installed.tsx b/web/app/components/plugins/install-plugin/install-bundle/steps/installed.tsx index 736930d88c..6b339bdd8c 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/steps/installed.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/steps/installed.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import React from 'react' -import type { Plugin } from '../../../types' +import type { InstallStatusResponse, Plugin } from '../../../types' import Card from '@/app/components/plugins/card' import Button from '@/app/components/base/button' import { useTranslation } from 'react-i18next' @@ -11,7 +11,7 @@ import { MARKETPLACE_API_PREFIX } from '@/config' type Props = { list: Plugin[] - installStatus: { success: boolean, isFromMarketPlace: boolean }[] + installStatus: InstallStatusResponse[] onCancel: () => void } diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx index 86f31c36f2..7cfaafb668 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx @@ -2,14 +2,13 @@ import React, { useCallback, useState } from 'react' import Modal from '@/app/components/base/modal' -import type { PluginDeclaration } from '../../types' +import type { Dependency, PluginDeclaration } from '../../types' import { InstallStep } from '../../types' import Uploading from './steps/uploading' -import Install from './steps/install' -import Installed from '../base/installed' import { useTranslation } from 'react-i18next' import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon' -import { useInvalidateInstalledPluginList } from '@/service/use-plugins' +import ReadyToInstallPackage from './ready-to-install' +import ReadyToInstallBundle from '../install-bundle/ready-to-install' const i18nPrefix = 'plugin.installModal' @@ -29,7 +28,8 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({ const [uniqueIdentifier, setUniqueIdentifier] = useState<string | null>(null) const [manifest, setManifest] = useState<PluginDeclaration | null>(null) const [errorMsg, setErrorMsg] = useState<string | null>(null) - const invalidateInstalledPluginList = useInvalidateInstalledPluginList() + const isBundle = file.name.endsWith('.bundle') + const [dependencies, setDependencies] = useState<Dependency[]>([]) const getTitle = useCallback(() => { if (step === InstallStep.uploadFailed) @@ -44,7 +44,7 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({ const { getIconUrl } = useGetIcon() - const handleUploaded = useCallback(async (result: { + const handlePackageUploaded = useCallback(async (result: { uniqueIdentifier: string manifest: PluginDeclaration }) => { @@ -61,22 +61,16 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({ setStep(InstallStep.readyToInstall) }, [getIconUrl]) + const handleBundleUploaded = useCallback((result: Dependency[]) => { + setDependencies(result) + setStep(InstallStep.readyToInstall) + }, []) + const handleUploadFail = useCallback((errorMsg: string) => { setErrorMsg(errorMsg) setStep(InstallStep.uploadFailed) }, []) - const handleInstalled = useCallback(() => { - invalidateInstalledPluginList() - setStep(InstallStep.installed) - }, [invalidateInstalledPluginList]) - - const handleFailed = useCallback((errorMsg?: string) => { - setStep(InstallStep.installFailed) - if (errorMsg) - setErrorMsg(errorMsg) - }, []) - return ( <Modal isShow={true} @@ -91,33 +85,32 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({ </div> {step === InstallStep.uploading && ( <Uploading + isBundle={isBundle} file={file} onCancel={onClose} - onUploaded={handleUploaded} + onPackageUploaded={handlePackageUploaded} + onBundleUploaded={handleBundleUploaded} onFailed={handleUploadFail} /> )} - { - step === InstallStep.readyToInstall && ( - <Install - uniqueIdentifier={uniqueIdentifier!} - payload={manifest!} - onCancel={onClose} - onInstalled={handleInstalled} - onFailed={handleFailed} - /> - ) - } - { - ([InstallStep.uploadFailed, InstallStep.installed, InstallStep.installFailed].includes(step)) && ( - <Installed - payload={manifest} - isFailed={[InstallStep.uploadFailed, InstallStep.installFailed].includes(step)} - errMsg={errorMsg} - onCancel={onClose} - /> - ) - } + {isBundle ? ( + <ReadyToInstallBundle + step={step} + onStepChange={setStep} + onClose={onClose} + dependencies={dependencies} + /> + ) : ( + <ReadyToInstallPackage + step={step} + onStepChange={setStep} + onClose={onClose} + uniqueIdentifier={uniqueIdentifier} + manifest={manifest} + errorMsg={errorMsg} + onError={setErrorMsg} + /> + )} </Modal> ) } diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/ready-to-install.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/ready-to-install.tsx new file mode 100644 index 0000000000..9f81b1e918 --- /dev/null +++ b/web/app/components/plugins/install-plugin/install-from-local-package/ready-to-install.tsx @@ -0,0 +1,68 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback } from 'react' +import type { PluginDeclaration } from '../../types' +import { InstallStep } from '../../types' +import Install from './steps/install' +import Installed from '../base/installed' +import { useInvalidateInstalledPluginList } from '@/service/use-plugins' + +type Props = { + step: InstallStep + onStepChange: (step: InstallStep) => void, + onClose: () => void + uniqueIdentifier: string | null, + manifest: PluginDeclaration | null, + errorMsg: string | null, + onError: (errorMsg: string) => void, +} + +const ReadyToInstall: FC<Props> = ({ + step, + onStepChange, + onClose, + uniqueIdentifier, + manifest, + errorMsg, + onError, +}) => { + const invalidateInstalledPluginList = useInvalidateInstalledPluginList() + + const handleInstalled = useCallback(() => { + invalidateInstalledPluginList() + onStepChange(InstallStep.installed) + }, [invalidateInstalledPluginList, onStepChange]) + + const handleFailed = useCallback((errorMsg?: string) => { + onStepChange(InstallStep.installFailed) + if (errorMsg) + onError(errorMsg) + }, [onError, onStepChange]) + + return ( + <> + { + step === InstallStep.readyToInstall && ( + <Install + uniqueIdentifier={uniqueIdentifier!} + payload={manifest!} + onCancel={onClose} + onInstalled={handleInstalled} + onFailed={handleFailed} + /> + ) + } + { + ([InstallStep.uploadFailed, InstallStep.installed, InstallStep.installFailed].includes(step)) && ( + <Installed + payload={manifest} + isFailed={[InstallStep.uploadFailed, InstallStep.installFailed].includes(step)} + errMsg={errorMsg} + onCancel={onClose} + /> + ) + } + </> + ) +} +export default React.memo(ReadyToInstall) diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.tsx index 499326c63e..61e762ce60 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.tsx @@ -3,34 +3,37 @@ import type { FC } from 'react' import React from 'react' import { RiLoader2Line } from '@remixicon/react' import Card from '../../../card' -import type { PluginDeclaration } from '../../../types' +import type { Dependency, PluginDeclaration } from '../../../types' import Button from '@/app/components/base/button' import { useTranslation } from 'react-i18next' -import { uploadPackageFile } from '@/service/plugins' +import { uploadFile } from '@/service/plugins' const i18nPrefix = 'plugin.installModal' type Props = { + isBundle: boolean file: File onCancel: () => void - onUploaded: (result: { + onPackageUploaded: (result: { uniqueIdentifier: string manifest: PluginDeclaration }) => void + onBundleUploaded: (result: Dependency[]) => void onFailed: (errorMsg: string) => void } const Uploading: FC<Props> = ({ + isBundle, file, onCancel, - onUploaded, + onPackageUploaded, + onBundleUploaded, onFailed, }) => { const { t } = useTranslation() const fileName = file.name const handleUpload = async () => { try { - const res = await uploadPackageFile(file) - onUploaded(res) + await uploadFile(file, isBundle) } catch (e: any) { if (e.response?.message) { @@ -38,7 +41,11 @@ const Uploading: FC<Props> = ({ } else { // Why it would into this branch? const res = e.response - onUploaded({ + if (isBundle) { + onBundleUploaded(res) + return + } + onPackageUploaded({ uniqueIdentifier: res.unique_identifier, manifest: res.manifest, }) diff --git a/web/app/components/plugins/plugin-page/empty/index.tsx b/web/app/components/plugins/plugin-page/empty/index.tsx index 06dc41492f..408f724df7 100644 --- a/web/app/components/plugins/plugin-page/empty/index.tsx +++ b/web/app/components/plugins/plugin-page/empty/index.tsx @@ -10,6 +10,7 @@ import { useSelector as useAppContextSelector } from '@/context/app-context' import Line from '../../marketplace/empty/line' import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' import { useTranslation } from 'react-i18next' +import { SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config' const Empty = () => { const { t } = useTranslation() @@ -42,11 +43,11 @@ const Empty = () => { {/* skeleton */} <div className='h-full w-full px-12 absolute top-0 grid grid-cols-2 gap-2 overflow-hidden z-10'> {Array.from({ length: 20 }).fill(0).map((_, i) => ( - <div key={i} className='h-[100px] bg-components-card-bg rounded-xl'/> + <div key={i} className='h-[100px] bg-components-card-bg rounded-xl' /> ))} </div> {/* mask */} - <div className='h-full w-full absolute z-20 bg-gradient-to-b from-background-gradient-mask-transparent to-white'/> + <div className='h-full w-full absolute z-20 bg-gradient-to-b from-background-gradient-mask-transparent to-white' /> <div className='flex items-center justify-center h-full relative z-30'> <div className='flex flex-col items-center gap-y-3'> <div className='relative -z-10 flex items-center justify-center w-[52px] h-[52px] rounded-xl @@ -66,7 +67,7 @@ const Empty = () => { ref={fileInputRef} style={{ display: 'none' }} onChange={handleFileChange} - accept='.difypkg' + accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS} /> <div className='w-full flex flex-col gap-y-1'> {[ diff --git a/web/app/components/plugins/plugin-page/index.tsx b/web/app/components/plugins/plugin-page/index.tsx index d089d37543..e743e248bc 100644 --- a/web/app/components/plugins/plugin-page/index.tsx +++ b/web/app/components/plugins/plugin-page/index.tsx @@ -32,6 +32,7 @@ import type { PluginDeclaration, PluginManifestInMarket } from '../types' import { sleep } from '@/utils' import { fetchManifestFromMarketPlace } from '@/service/plugins' import { marketplaceApiPrefix } from '@/config' +import { SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config' const PACKAGE_IDS_KEY = 'package-ids' @@ -186,7 +187,7 @@ const PluginPage = ({ className="hidden" type="file" id="fileUploader" - accept='.difypkg' + accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS} onChange={fileChangeHandle ?? (() => { })} /> </> diff --git a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx index 826aba334b..e8d68e1bbf 100644 --- a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx +++ b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx @@ -17,6 +17,7 @@ import { import { useSelector as useAppContextSelector } from '@/context/app-context' import { useInvalidateInstalledPluginList } from '@/service/use-plugins' import { useTranslation } from 'react-i18next' +import { SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config' type Props = { onSwitchToMarketplaceTab: () => void @@ -81,7 +82,7 @@ const InstallPluginDropdown = ({ ref={fileInputRef} style={{ display: 'none' }} onChange={handleFileChange} - accept='.difypkg' + accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS} /> <div className='w-full'> {[ @@ -126,7 +127,7 @@ const InstallPluginDropdown = ({ && (<InstallFromLocalPackage file={selectedFile} onClose={() => setSelectedAction(null)} - onSuccess={() => {}} + onSuccess={() => { }} /> ) } diff --git a/web/config/index.ts b/web/config/index.ts index 1de973f9a2..6e8c4a630c 100644 --- a/web/config/index.ts +++ b/web/config/index.ts @@ -273,3 +273,5 @@ export const TEXT_GENERATION_TIMEOUT_MS = textGenerationTimeoutMs export const DISABLE_UPLOAD_IMAGE_AS_ICON = process.env.NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON === 'true' export const GITHUB_ACCESS_TOKEN = process.env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN || globalThis.document?.body?.getAttribute('data-public-github-access-token') || '' + +export const SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS = '.difypkg,.bundle' diff --git a/web/service/plugins.ts b/web/service/plugins.ts index 46a90b885c..28bf6c44f4 100644 --- a/web/service/plugins.ts +++ b/web/service/plugins.ts @@ -16,13 +16,13 @@ import type { MarketplaceCollectionsResponse, } from '@/app/components/plugins/marketplace/types' -export const uploadPackageFile = async (file: File) => { +export const uploadFile = async (file: File, isBundle: boolean) => { const formData = new FormData() - formData.append('pkg', file) + formData.append(isBundle ? 'bundle' : 'pkg', file) return upload({ xhr: new XMLHttpRequest(), data: formData, - }, false, '/workspaces/current/plugin/upload/pkg') + }, false, `/workspaces/current/plugin/upload/${isBundle ? 'bundle' : 'pkg'}`) } export const updateFromMarketPlace = async (body: Record<string, string>) => { From 43d7a538dc1606bbeb5dd97f50c92d66d18eb8d7 Mon Sep 17 00:00:00 2001 From: StyleZhang <jasonapring2015@outlook.com> Date: Tue, 19 Nov 2024 15:08:39 +0800 Subject: [PATCH 11/11] feat: marketplace list url support search & tags --- web/app/components/plugins/constants.ts | 25 +++++ web/app/components/plugins/hooks.ts | 100 +++--------------- .../plugins/marketplace/context.tsx | 33 +++++- .../components/plugins/marketplace/index.tsx | 5 +- .../components/plugins/marketplace/types.ts | 7 ++ web/app/components/plugins/utils.ts | 13 +++ .../components/tools/marketplace/index.tsx | 2 +- 7 files changed, 96 insertions(+), 89 deletions(-) create mode 100644 web/app/components/plugins/constants.ts create mode 100644 web/app/components/plugins/utils.ts diff --git a/web/app/components/plugins/constants.ts b/web/app/components/plugins/constants.ts new file mode 100644 index 0000000000..d4f683eb9b --- /dev/null +++ b/web/app/components/plugins/constants.ts @@ -0,0 +1,25 @@ +export const tagKeys = [ + 'search', + 'image', + 'videos', + 'weather', + 'finance', + 'design', + 'travel', + 'social', + 'news', + 'medical', + 'productivity', + 'education', + 'business', + 'entertainment', + 'utilities', + 'other', +] + +export const categoryKeys = [ + 'model', + 'tool', + 'extension', + 'bundle', +] diff --git a/web/app/components/plugins/hooks.ts b/web/app/components/plugins/hooks.ts index 81488fed30..9b2bf61f10 100644 --- a/web/app/components/plugins/hooks.ts +++ b/web/app/components/plugins/hooks.ts @@ -1,5 +1,9 @@ import { useTranslation } from 'react-i18next' import type { TFunction } from 'i18next' +import { + categoryKeys, + tagKeys, +} from './constants' type Tag = { name: string @@ -10,72 +14,12 @@ export const useTags = (translateFromOut?: TFunction) => { const { t: translation } = useTranslation() const t = translateFromOut || translation - const tags = [ - { - name: 'search', - label: t('pluginTags.tags.search'), - }, - { - name: 'image', - label: t('pluginTags.tags.image'), - }, - { - name: 'videos', - label: t('pluginTags.tags.videos'), - }, - { - name: 'weather', - label: t('pluginTags.tags.weather'), - }, - { - name: 'finance', - label: t('pluginTags.tags.finance'), - }, - { - name: 'design', - label: t('pluginTags.tags.design'), - }, - { - name: 'travel', - label: t('pluginTags.tags.travel'), - }, - { - name: 'social', - label: t('pluginTags.tags.social'), - }, - { - name: 'news', - label: t('pluginTags.tags.news'), - }, - { - name: 'medical', - label: t('pluginTags.tags.medical'), - }, - { - name: 'productivity', - label: t('pluginTags.tags.productivity'), - }, - { - name: 'education', - label: t('pluginTags.tags.education'), - }, - { - name: 'business', - label: t('pluginTags.tags.business'), - }, - { - name: 'entertainment', - label: t('pluginTags.tags.entertainment'), - }, - { - name: 'utilities', - label: t('pluginTags.tags.utilities'), - }, - { - name: 'other', - label: t('pluginTags.tags.other'), - }, - ] + const tags = tagKeys.map((tag) => { + return { + name: tag, + label: t(`pluginTags.tags.${tag}`), + } + }) const tagsMap = tags.reduce((acc, tag) => { acc[tag.name] = tag @@ -97,24 +41,12 @@ export const useCategories = (translateFromOut?: TFunction) => { const { t: translation } = useTranslation() const t = translateFromOut || translation - const categories = [ - { - name: 'model', - label: t('plugin.category.models'), - }, - { - name: 'tool', - label: t('plugin.category.tools'), - }, - { - name: 'extension', - label: t('plugin.category.extensions'), - }, - { - name: 'bundle', - label: t('plugin.category.bundles'), - }, - ] + const categories = categoryKeys.map((category) => { + return { + name: category, + label: t(`plugin.category.${category}s`), + } + }) const categoriesMap = categories.reduce((acc, category) => { acc[category.name] = category diff --git a/web/app/components/plugins/marketplace/context.tsx b/web/app/components/plugins/marketplace/context.tsx index 6dabfdf736..31fe2ced5e 100644 --- a/web/app/components/plugins/marketplace/context.tsx +++ b/web/app/components/plugins/marketplace/context.tsx @@ -5,6 +5,7 @@ import type { } from 'react' import { useCallback, + useEffect, useRef, useState, } from 'react' @@ -14,9 +15,14 @@ import { } from 'use-context-selector' import { PLUGIN_TYPE_SEARCH_MAP } from './plugin-type-switch' import type { Plugin } from '../types' +import { + getValidCategoryKeys, + getValidTagKeys, +} from '../utils' import type { MarketplaceCollection, PluginsSort, + SearchParams, } from './types' import { DEFAULT_SORT } from './constants' import { @@ -66,6 +72,7 @@ export const MarketplaceContext = createContext<MarketplaceContextValue>({ type MarketplaceContextProviderProps = { children: ReactNode + searchParams?: SearchParams } export function useMarketplaceContext(selector: (value: MarketplaceContextValue) => any) { @@ -74,13 +81,19 @@ export function useMarketplaceContext(selector: (value: MarketplaceContextValue) export const MarketplaceContextProvider = ({ children, + searchParams, }: MarketplaceContextProviderProps) => { + const queryFromSearchParams = searchParams?.q || '' + const tagsFromSearchParams = searchParams?.tags ? getValidTagKeys(searchParams.tags.split(',')) : [] + const hasValidTags = !!tagsFromSearchParams.length + const hasValidCategory = getValidCategoryKeys(searchParams?.category) + const categoryFromSearchParams = hasValidCategory || PLUGIN_TYPE_SEARCH_MAP.all const [intersected, setIntersected] = useState(true) - const [searchPluginText, setSearchPluginText] = useState('') + const [searchPluginText, setSearchPluginText] = useState(queryFromSearchParams) const searchPluginTextRef = useRef(searchPluginText) - const [filterPluginTags, setFilterPluginTags] = useState<string[]>([]) + const [filterPluginTags, setFilterPluginTags] = useState<string[]>(tagsFromSearchParams) const filterPluginTagsRef = useRef(filterPluginTags) - const [activePluginType, setActivePluginType] = useState(PLUGIN_TYPE_SEARCH_MAP.all) + const [activePluginType, setActivePluginType] = useState(categoryFromSearchParams) const activePluginTypeRef = useRef(activePluginType) const [sort, setSort] = useState(DEFAULT_SORT) const sortRef = useRef(sort) @@ -100,6 +113,20 @@ export const MarketplaceContextProvider = ({ isLoading: isPluginsLoading, } = useMarketplacePlugins() + useEffect(() => { + if (queryFromSearchParams || hasValidTags || hasValidCategory) { + queryPlugins({ + query: queryFromSearchParams, + category: hasValidCategory, + tags: hasValidTags ? tagsFromSearchParams : [], + sortBy: sortRef.current.sortBy, + sortOrder: sortRef.current.sortOrder, + }) + history.pushState({}, '', `/${searchParams?.language ? `?language=${searchParams?.language}` : ''}`) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [queryPlugins]) + const handleSearchPluginTextChange = useCallback((text: string) => { setSearchPluginText(text) searchPluginTextRef.current = text diff --git a/web/app/components/plugins/marketplace/index.tsx b/web/app/components/plugins/marketplace/index.tsx index 10e623710e..0ffe94a048 100644 --- a/web/app/components/plugins/marketplace/index.tsx +++ b/web/app/components/plugins/marketplace/index.tsx @@ -4,22 +4,25 @@ import IntersectionLine from './intersection-line' import SearchBoxWrapper from './search-box/search-box-wrapper' import PluginTypeSwitch from './plugin-type-switch' import ListWrapper from './list/list-wrapper' +import type { SearchParams } from './types' import { getMarketplaceCollectionsAndPlugins } from './utils' import { TanstackQueryIniter } from '@/context/query-client' type MarketplaceProps = { locale?: string showInstallButton?: boolean + searchParams?: SearchParams } const Marketplace = async ({ locale, showInstallButton = true, + searchParams, }: MarketplaceProps) => { const { marketplaceCollections, marketplaceCollectionPluginsMap } = await getMarketplaceCollectionsAndPlugins() return ( <TanstackQueryIniter> - <MarketplaceContextProvider> + <MarketplaceContextProvider searchParams={searchParams}> <Description locale={locale} /> <IntersectionLine /> <SearchBoxWrapper locale={locale} /> diff --git a/web/app/components/plugins/marketplace/types.ts b/web/app/components/plugins/marketplace/types.ts index 1fe8d5aa37..e2c4315f3d 100644 --- a/web/app/components/plugins/marketplace/types.ts +++ b/web/app/components/plugins/marketplace/types.ts @@ -36,3 +36,10 @@ export type PluginsSort = { export type CollectionsAndPluginsSearchParams = { category?: string } + +export type SearchParams = { + language?: string + q?: string + tags?: string + category?: string +} diff --git a/web/app/components/plugins/utils.ts b/web/app/components/plugins/utils.ts new file mode 100644 index 0000000000..a87ee021eb --- /dev/null +++ b/web/app/components/plugins/utils.ts @@ -0,0 +1,13 @@ +import { + categoryKeys, + tagKeys, +} from './constants' + +export const getValidTagKeys = (tags: string[]) => { + return tags.filter(tag => tagKeys.includes(tag)) +} + +export const getValidCategoryKeys = (category?: string) => { + const currentCategory = categoryKeys.find(key => key === category) + return currentCategory ? `${currentCategory}s` : '' +} diff --git a/web/app/components/tools/marketplace/index.tsx b/web/app/components/tools/marketplace/index.tsx index c487a912d2..71cac1f781 100644 --- a/web/app/components/tools/marketplace/index.tsx +++ b/web/app/components/tools/marketplace/index.tsx @@ -57,7 +57,7 @@ const Marketplace = ({ </span> {t('common.operation.in')} <a - href={`${MARKETPLACE_URL_PREFIX}?language=${locale}`} + href={`${MARKETPLACE_URL_PREFIX}?language=${locale}&q=${searchPluginText}&tags=${filterPluginTags.join(',')}`} className='flex items-center ml-1 system-sm-medium text-text-accent' target='_blank' >