switch version

This commit is contained in:
JzoNg 2024-11-17 12:57:34 +08:00
parent f0e4885926
commit 66f0e1209a
9 changed files with 196 additions and 12 deletions

View File

@ -3,7 +3,7 @@ import cn from '@/utils/classnames'
type BadgeProps = {
className?: string
text: string
text: string | React.ReactNode
uppercase?: boolean
hasRedCornerMark?: boolean
}

View File

@ -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 = ({
<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='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}

View File

@ -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={

View File

@ -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[]
}

View File

@ -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)

View File

@ -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

View File

@ -33,6 +33,7 @@ const translation = {
local: 'Local Package File',
},
detailPanel: {
switchVersion: 'Switch Version',
categoryTip: {
marketplace: 'Installed from Marketplace',
github: 'Installed from Github',

View File

@ -33,6 +33,7 @@ const translation = {
local: '本地插件',
},
detailPanel: {
switchVersion: '切换版本',
categoryTip: {
marketplace: '从 Marketplace 安装',
github: '从 Github 安装',

View File

@ -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) => {