From 66f0e1209af55ea780583466c778a3ec70ff36bf Mon Sep 17 00:00:00 2001 From: JzoNg Date: Sun, 17 Nov 2024 12:57:34 +0800 Subject: [PATCH] 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) => {