merge feat/plugins
commit
5d7c527702
@ -0,0 +1,67 @@
|
||||
'use client'
|
||||
import { toolNeko } from '@/app/components/plugins/card/card-mock'
|
||||
import { PluginSource } from '@/app/components/plugins/types'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import React from 'react'
|
||||
|
||||
const UpdatePlugin = () => {
|
||||
const { setShowUpdatePluginModal } = useModalContext()
|
||||
const handleUpdateFromMarketPlace = () => {
|
||||
setShowUpdatePluginModal({
|
||||
payload: {
|
||||
type: PluginSource.marketplace,
|
||||
marketPlace: {
|
||||
originalPackageInfo: {
|
||||
id: 'langgenius/neko:0.0.1@9e57d693739287c0efdc96847d7ed959ca93f70aa704471f2eb7ed3313821824',
|
||||
payload: toolNeko as any,
|
||||
},
|
||||
targetPackageInfo: {
|
||||
id: 'target_xxx',
|
||||
version: '1.2.3',
|
||||
},
|
||||
},
|
||||
},
|
||||
onCancelCallback: () => {
|
||||
console.log('canceled')
|
||||
},
|
||||
onSaveCallback: () => {
|
||||
console.log('saved')
|
||||
},
|
||||
})
|
||||
}
|
||||
const handleUpdateFromGithub = () => {
|
||||
setShowUpdatePluginModal({
|
||||
payload: {
|
||||
type: PluginSource.github,
|
||||
github: {
|
||||
originalPackageInfo: {
|
||||
id: '111',
|
||||
repo: 'aaa/bbb',
|
||||
version: 'xxx',
|
||||
url: 'aaa/bbb',
|
||||
currVersion: '1.2.3',
|
||||
currPackage: 'pack1',
|
||||
} as any,
|
||||
},
|
||||
},
|
||||
onCancelCallback: () => {
|
||||
console.log('canceled')
|
||||
},
|
||||
onSaveCallback: () => {
|
||||
console.log('saved')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>更新组件</div>
|
||||
<div className='flex space-x-1'>
|
||||
<div className='underline cursor-pointer' onClick={handleUpdateFromMarketPlace}>从 Marketplace</div>
|
||||
<div className='underline cursor-pointer' onClick={handleUpdateFromGithub}>从 GitHub</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(UpdatePlugin)
|
||||
@ -0,0 +1,89 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { TFunction } from 'i18next'
|
||||
|
||||
type Tag = {
|
||||
name: string
|
||||
label: string
|
||||
}
|
||||
|
||||
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 tagsMap = tags.reduce((acc, tag) => {
|
||||
acc[tag.name] = tag
|
||||
return acc
|
||||
}, {} as Record<string, Tag>)
|
||||
|
||||
return {
|
||||
tags,
|
||||
tagsMap,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { uploadGitHub } from '@/service/plugins'
|
||||
import { Octokit } from '@octokit/core'
|
||||
import { GITHUB_ACCESS_TOKEN } from '@/config'
|
||||
|
||||
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 formattedReleases = res.data.map((release: any) => ({
|
||||
tag_name: release.tag_name,
|
||||
assets: release.assets.map((asset: any) => ({
|
||||
browser_download_url: asset.browser_download_url,
|
||||
name: asset.name,
|
||||
})),
|
||||
}))
|
||||
|
||||
return formattedReleases
|
||||
}
|
||||
catch (error) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Failed to fetch repository releases',
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
return { fetchReleases }
|
||||
}
|
||||
|
||||
export const useGitHubUpload = () => {
|
||||
const handleUpload = async (
|
||||
repoUrl: string,
|
||||
selectedVersion: string,
|
||||
selectedPackage: string,
|
||||
onSuccess?: (GitHubPackage: { manifest: any; unique_identifier: string }) => void,
|
||||
) => {
|
||||
try {
|
||||
const response = await uploadGitHub(repoUrl, selectedVersion, selectedPackage)
|
||||
const GitHubPackage = {
|
||||
manifest: response.manifest,
|
||||
unique_identifier: response.unique_identifier,
|
||||
}
|
||||
if (onSuccess) onSuccess(GitHubPackage)
|
||||
return GitHubPackage
|
||||
}
|
||||
catch (error) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Error uploading package',
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return { handleUpload }
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
import React from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type InstalledProps = {
|
||||
repoUrl: string
|
||||
selectedVersion: string
|
||||
selectedPackage: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const InfoRow = ({ label, value }: { label: string; value: string }) => (
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='flex-shrink-0 w-[72px] items-center gap-2'>
|
||||
<div className='text-text-tertiary system-sm-medium truncate'>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-grow overflow-hidden'>
|
||||
<div className='text-text-secondary text-ellipsis system-sm-medium'>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const Installed: React.FC<InstalledProps> = ({ repoUrl, selectedVersion, selectedPackage, onClose }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<div className='text-text-secondary system-md-regular'>The plugin has been installed successfully.</div>
|
||||
<div className='flex w-full p-4 flex-col justify-center items-start gap-2 rounded-2xl bg-background-section-burn'>
|
||||
{[
|
||||
{ label: t('plugin.installModal.labels.repository'), value: repoUrl },
|
||||
{ label: t('plugin.installModal.labels.version'), value: selectedVersion },
|
||||
{ label: t('plugin.installModal.labels.package'), value: selectedPackage },
|
||||
].map(({ label, value }) => (
|
||||
<InfoRow key={label} label={label} value={value} />
|
||||
))}
|
||||
</div>
|
||||
<div className='flex justify-end items-center gap-2 self-stretch mt-4'>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('plugin.installModal.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Installed
|
||||
@ -0,0 +1,119 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../../types'
|
||||
import Card from '../../../card'
|
||||
import Badge, { BadgeState } from '@/app/components/base/badge/index'
|
||||
import { pluginManifestToCardPluginProps } from '../../utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { installPackageFromGitHub, uninstallPlugin } from '@/service/plugins'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import { usePluginTasksStore } from '@/app/components/plugins/plugin-page/store'
|
||||
import checkTaskStatus from '../../base/check-task-status'
|
||||
import { parseGitHubUrl } from '../../utils'
|
||||
|
||||
type LoadedProps = {
|
||||
updatePayload: UpdateFromGitHubPayload
|
||||
uniqueIdentifier: string
|
||||
payload: PluginDeclaration
|
||||
repoUrl: string
|
||||
selectedVersion: string
|
||||
selectedPackage: string
|
||||
onBack: () => void
|
||||
onInstalled: () => void
|
||||
onFailed: (message?: string) => void
|
||||
}
|
||||
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
|
||||
const Loaded: React.FC<LoadedProps> = ({
|
||||
updatePayload,
|
||||
uniqueIdentifier,
|
||||
payload,
|
||||
repoUrl,
|
||||
selectedVersion,
|
||||
selectedPackage,
|
||||
onBack,
|
||||
onInstalled,
|
||||
onFailed,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isInstalling, setIsInstalling] = React.useState(false)
|
||||
const setPluginTasksWithPolling = usePluginTasksStore(s => s.setPluginTasksWithPolling)
|
||||
const { check } = checkTaskStatus()
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (isInstalling) return
|
||||
setIsInstalling(true)
|
||||
|
||||
try {
|
||||
const { owner, repo } = parseGitHubUrl(repoUrl)
|
||||
const { all_installed: isInstalled, task_id: taskId } = await installPackageFromGitHub(
|
||||
`${owner}/${repo}`,
|
||||
selectedVersion,
|
||||
selectedPackage,
|
||||
uniqueIdentifier,
|
||||
)
|
||||
|
||||
if (updatePayload && isInstalled)
|
||||
await uninstallPlugin(updatePayload.originalPackageInfo.id)
|
||||
|
||||
if (isInstalled) {
|
||||
onInstalled()
|
||||
return
|
||||
}
|
||||
|
||||
setPluginTasksWithPolling()
|
||||
await check({
|
||||
taskId,
|
||||
pluginUniqueIdentifier: uniqueIdentifier,
|
||||
})
|
||||
|
||||
onInstalled()
|
||||
}
|
||||
catch (e) {
|
||||
if (typeof e === 'string') {
|
||||
onFailed(e)
|
||||
return
|
||||
}
|
||||
onFailed()
|
||||
}
|
||||
finally {
|
||||
setIsInstalling(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='text-text-secondary system-md-regular'>
|
||||
<p>{t(`${i18nPrefix}.readyToInstall`)}</p>
|
||||
</div>
|
||||
<div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap rounded-2xl bg-background-section-burn'>
|
||||
<Card
|
||||
className='w-full'
|
||||
payload={pluginManifestToCardPluginProps(payload)}
|
||||
titleLeft={<Badge className='mx-1' size="s" state={BadgeState.Default}>{payload.version}</Badge>}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex justify-end items-center gap-2 self-stretch mt-4'>
|
||||
{!isInstalling && (
|
||||
<Button variant='secondary' className='min-w-[72px]' onClick={onBack}>
|
||||
{t('plugin.installModal.back')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px] flex space-x-0.5'
|
||||
onClick={handleInstall}
|
||||
disabled={isInstalling}
|
||||
>
|
||||
{isInstalling && <RiLoader2Line className='w-4 h-4 animate-spin-slow' />}
|
||||
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loaded
|
||||
@ -0,0 +1,126 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../../types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useGitHubUpload } from '../../hooks'
|
||||
|
||||
const i18nPrefix = 'plugin.installFromGitHub'
|
||||
|
||||
type SelectPackageProps = {
|
||||
updatePayload: UpdateFromGitHubPayload
|
||||
repoUrl: string
|
||||
selectedVersion: string
|
||||
versions: Item[]
|
||||
onSelectVersion: (item: Item) => void
|
||||
selectedPackage: string
|
||||
packages: Item[]
|
||||
onSelectPackage: (item: Item) => void
|
||||
onUploaded: (result: {
|
||||
uniqueIdentifier: string
|
||||
manifest: PluginDeclaration
|
||||
}) => void
|
||||
onFailed: (errorMsg: string) => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
const SelectPackage: React.FC<SelectPackageProps> = ({
|
||||
updatePayload,
|
||||
repoUrl,
|
||||
selectedVersion,
|
||||
versions,
|
||||
onSelectVersion,
|
||||
selectedPackage,
|
||||
packages,
|
||||
onSelectPackage,
|
||||
onUploaded,
|
||||
onFailed,
|
||||
onBack,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isEdit = Boolean(updatePayload)
|
||||
const [isUploading, setIsUploading] = React.useState(false)
|
||||
const { handleUpload } = useGitHubUpload()
|
||||
|
||||
const handleUploadPackage = async () => {
|
||||
if (isUploading) return
|
||||
setIsUploading(true)
|
||||
|
||||
try {
|
||||
const repo = repoUrl.replace('https://github.com/', '')
|
||||
await handleUpload(repo, selectedVersion, selectedPackage, (GitHubPackage) => {
|
||||
onUploaded({
|
||||
uniqueIdentifier: GitHubPackage.unique_identifier,
|
||||
manifest: GitHubPackage.manifest,
|
||||
})
|
||||
})
|
||||
}
|
||||
catch (e: any) {
|
||||
if (e.response?.message)
|
||||
onFailed(e.response?.message)
|
||||
else
|
||||
onFailed(t(`${i18nPrefix}.uploadFailed`))
|
||||
}
|
||||
finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor='version'
|
||||
className='flex flex-col justify-center items-start self-stretch text-text-secondary'
|
||||
>
|
||||
<span className='system-sm-semibold'>{t(`${i18nPrefix}.selectVersion`)}</span>
|
||||
</label>
|
||||
<PortalSelect
|
||||
value={selectedVersion}
|
||||
onSelect={onSelectVersion}
|
||||
items={versions}
|
||||
installedValue={updatePayload?.originalPackageInfo.version}
|
||||
placeholder={t(`${i18nPrefix}.selectVersionPlaceholder`) || ''}
|
||||
popupClassName='w-[512px] z-[1001]'
|
||||
/>
|
||||
<label
|
||||
htmlFor='package'
|
||||
className='flex flex-col justify-center items-start self-stretch text-text-secondary'
|
||||
>
|
||||
<span className='system-sm-semibold'>{t(`${i18nPrefix}.selectPackage`)}</span>
|
||||
</label>
|
||||
<PortalSelect
|
||||
value={selectedPackage}
|
||||
onSelect={onSelectPackage}
|
||||
items={packages}
|
||||
readonly={!selectedVersion}
|
||||
placeholder={t(`${i18nPrefix}.selectPackagePlaceholder`) || ''}
|
||||
popupClassName='w-[512px] z-[1001]'
|
||||
/>
|
||||
<div className='flex justify-end items-center gap-2 self-stretch mt-4'>
|
||||
{!isEdit
|
||||
&& <Button
|
||||
variant='secondary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onBack}
|
||||
disabled={isUploading}
|
||||
>
|
||||
{t('plugin.installModal.back')}
|
||||
</Button>
|
||||
}
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
onClick={handleUploadPackage}
|
||||
disabled={!selectedVersion || !selectedPackage || isUploading}
|
||||
>
|
||||
{t('plugin.installModal.next')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectPackage
|
||||
@ -1,53 +0,0 @@
|
||||
import React from 'react'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type SetPackageProps = {
|
||||
selectedPackage: string
|
||||
packages: Item[]
|
||||
onSelect: (item: Item) => void
|
||||
onInstall: () => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
const SetPackage: React.FC<SetPackageProps> = ({ selectedPackage, packages, onSelect, onInstall, onBack }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor='package'
|
||||
className='flex flex-col justify-center items-start self-stretch text-text-secondary'
|
||||
>
|
||||
<span className='system-sm-semibold'>{t('plugin.installFromGitHub.selectPackage')}</span>
|
||||
</label>
|
||||
<PortalSelect
|
||||
value={selectedPackage}
|
||||
onSelect={onSelect}
|
||||
items={packages}
|
||||
placeholder={t('plugin.installFromGitHub.selectPackagePlaceholder') || ''}
|
||||
popupClassName='w-[432px] z-[1001]'
|
||||
/>
|
||||
<div className='flex justify-end items-center gap-2 self-stretch mt-4'>
|
||||
<Button
|
||||
variant='secondary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onBack}
|
||||
>
|
||||
{t('plugin.installModal.back')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onInstall}
|
||||
disabled={!selectedPackage}
|
||||
>
|
||||
{t('plugin.installModal.install')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SetPackage
|
||||
@ -1,53 +0,0 @@
|
||||
import React from 'react'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type SetVersionProps = {
|
||||
selectedVersion: string
|
||||
versions: Item[]
|
||||
onSelect: (item: Item) => void
|
||||
onNext: () => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
const SetVersion: React.FC<SetVersionProps> = ({ selectedVersion, versions, onSelect, onNext, onBack }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor='version'
|
||||
className='flex flex-col justify-center items-start self-stretch text-text-secondary'
|
||||
>
|
||||
<span className='system-sm-semibold'>{t('plugin.installFromGitHub.selectVersion')}</span>
|
||||
</label>
|
||||
<PortalSelect
|
||||
value={selectedVersion}
|
||||
onSelect={onSelect}
|
||||
items={versions}
|
||||
placeholder={t('plugin.installFromGitHub.selectVersionPlaceholder') || ''}
|
||||
popupClassName='w-[432px] z-[1001]'
|
||||
/>
|
||||
<div className='flex justify-end items-center gap-2 self-stretch mt-4'>
|
||||
<Button
|
||||
variant='secondary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onBack}
|
||||
>
|
||||
{t('plugin.installModal.back')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onNext}
|
||||
disabled={!selectedVersion}
|
||||
>
|
||||
{t('plugin.installModal.next')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SetVersion
|
||||
@ -0,0 +1,118 @@
|
||||
import React, { 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'
|
||||
import InstallFromGitHub from '@/app/components/plugins/install-plugin/install-from-github'
|
||||
import InstallFromLocalPackage from '@/app/components/plugins/install-plugin/install-from-local-package'
|
||||
import { usePluginPageContext } from '../context'
|
||||
import { Group } from '@/app/components/base/icons/src/vender/other'
|
||||
import { useSelector as useAppContextSelector } from '@/context/app-context'
|
||||
import Line from '../../marketplace/empty/line'
|
||||
import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
|
||||
const Empty = () => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [selectedAction, setSelectedAction] = useState<string | null>(null)
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
|
||||
const setActiveTab = usePluginPageContext(v => v.setActiveTab)
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (file) {
|
||||
setSelectedFile(file)
|
||||
setSelectedAction('local')
|
||||
}
|
||||
}
|
||||
const filters = usePluginPageContext(v => v.filters)
|
||||
const { data: pluginList } = useInstalledPluginList()
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
|
||||
const text = useMemo(() => {
|
||||
if (pluginList?.plugins.length === 0)
|
||||
return 'No plugins installed'
|
||||
if (filters.categories.length > 0 || filters.tags.length > 0 || filters.searchQuery)
|
||||
return 'No plugins found'
|
||||
}, [pluginList, filters])
|
||||
|
||||
return (
|
||||
<div className='grow w-full relative z-0'>
|
||||
{/* 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>
|
||||
{/* mask */}
|
||||
<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
|
||||
bg-components-card-bg border-[1px] border-dashed border-divider-deep shadow-xl shadow-shadow-shadow-5'>
|
||||
<Group className='text-text-tertiary w-5 h-5' />
|
||||
<Line className='absolute -right-[1px] top-1/2 -translate-y-1/2' />
|
||||
<Line className='absolute -left-[1px] top-1/2 -translate-y-1/2' />
|
||||
<Line className='absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-90' />
|
||||
<Line className='absolute top-full left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-90' />
|
||||
</div>
|
||||
<div className='text-text-tertiary text-sm font-normal'>
|
||||
{text}
|
||||
</div>
|
||||
<div className='flex flex-col w-[240px]'>
|
||||
<input
|
||||
type='file'
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileChange}
|
||||
accept='.difypkg'
|
||||
/>
|
||||
<div className='w-full flex flex-col gap-y-1'>
|
||||
{[
|
||||
...(
|
||||
(enable_marketplace || true)
|
||||
? [{ icon: MagicBox, text: 'Marketplace', action: 'marketplace' }]
|
||||
: []
|
||||
),
|
||||
{ icon: Github, text: 'GitHub', action: 'github' },
|
||||
{ icon: FileZip, text: 'Local Package File', action: 'local' },
|
||||
].map(({ icon: Icon, text, action }) => (
|
||||
<div
|
||||
key={action}
|
||||
className='flex items-center px-3 py-2 gap-x-1 rounded-lg bg-components-button-secondary-bg
|
||||
hover:bg-state-base-hover cursor-pointer border-[0.5px] shadow-shadow-shadow-3 shadow-xs'
|
||||
onClick={() => {
|
||||
if (action === 'local')
|
||||
fileInputRef.current?.click()
|
||||
else if (action === 'marketplace')
|
||||
setActiveTab('discover')
|
||||
else
|
||||
setSelectedAction(action)
|
||||
}}
|
||||
>
|
||||
<Icon className="w-4 h-4 text-text-tertiary" />
|
||||
<span className='text-text-secondary system-md-regular'>{`Install from ${text}`}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{selectedAction === 'github' && <InstallFromGitHub
|
||||
onSuccess={() => { invalidateInstalledPluginList() }}
|
||||
onClose={() => setSelectedAction(null)}
|
||||
/>}
|
||||
{selectedAction === 'local' && selectedFile
|
||||
&& (<InstallFromLocalPackage
|
||||
file={selectedFile}
|
||||
onClose={() => setSelectedAction(null)}
|
||||
onSuccess={() => { }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Empty.displayName = 'Empty'
|
||||
|
||||
export default React.memo(Empty)
|
||||
@ -1,37 +0,0 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useRequest } from 'ahooks'
|
||||
import type { PluginTask } from '../types'
|
||||
import { fetchPluginTasks } from '@/service/plugins'
|
||||
|
||||
export const usePluginTasks = () => {
|
||||
const [pluginTasks, setPluginTasks] = useState<PluginTask[]>([])
|
||||
|
||||
const handleUpdatePluginTasks = async (callback: (tasks: PluginTask[]) => void) => {
|
||||
const { tasks } = await fetchPluginTasks()
|
||||
setPluginTasks(tasks)
|
||||
callback(tasks)
|
||||
}
|
||||
|
||||
const { run, cancel } = useRequest(handleUpdatePluginTasks, {
|
||||
manual: true,
|
||||
pollingInterval: 3000,
|
||||
pollingErrorRetryCount: 2,
|
||||
})
|
||||
|
||||
const checkHasPluginTasks = useCallback((tasks: PluginTask[]) => {
|
||||
if (!tasks.length)
|
||||
cancel()
|
||||
}, [cancel])
|
||||
|
||||
useEffect(() => {
|
||||
run(checkHasPluginTasks)
|
||||
}, [run, checkHasPluginTasks])
|
||||
|
||||
return {
|
||||
pluginTasks,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
RiCheckboxCircleFill,
|
||||
RiErrorWarningFill,
|
||||
RiInstallLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Button from '@/app/components/base/button'
|
||||
// import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||
import { useMemo } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const InstallInfo = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const status = 'error'
|
||||
const statusError = useMemo(() => status === 'error', [status])
|
||||
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 79,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
<Tooltip popupContent='Installing 1/3 plugins...'>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex items-center justify-center w-8 h-8 rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs hover:bg-components-button-secondary-bg-hover',
|
||||
statusError && 'border-components-button-destructive-secondary-border-hover bg-state-destructive-hover hover:bg-state-destructive-hover-alt',
|
||||
)}
|
||||
>
|
||||
<RiInstallLine
|
||||
className={cn(
|
||||
'w-4 h-4 text-components-button-secondary-text',
|
||||
statusError && 'text-components-button-destructive-secondary-text',
|
||||
)}
|
||||
/>
|
||||
<div className='absolute -right-1 -top-1'>
|
||||
{/* <ProgressCircle
|
||||
percentage={33}
|
||||
circleFillColor='fill-components-progress-brand-bg'
|
||||
sectorFillColor='fill-components-progress-error-bg'
|
||||
circleStrokeColor='stroke-components-progress-error-bg'
|
||||
/> */}
|
||||
<RiCheckboxCircleFill className='w-3.5 h-3.5 text-text-success' />
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-10'>
|
||||
<div className='p-1 pb-2 w-[320px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
|
||||
<div className='flex items-center px-2 pt-1 h-7 system-sm-semibold-uppercase'>3 plugins failed to install</div>
|
||||
<div className='flex items-center p-1 pl-2 h-8 rounded-lg hover:bg-state-base-hover'>
|
||||
<div className='relative flex items-center justify-center mr-2 w-6 h-6 rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge'>
|
||||
<RiErrorWarningFill className='absolute -right-0.5 -bottom-0.5 w-3 h-3 text-text-destructive' />
|
||||
</div>
|
||||
<div className='grow system-md-regular text-text-secondary truncate'>
|
||||
DuckDuckGo Search
|
||||
</div>
|
||||
<Button
|
||||
size='small'
|
||||
variant='ghost-accent'
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InstallInfo
|
||||
@ -0,0 +1,40 @@
|
||||
import { create } from 'zustand'
|
||||
import type { PluginTask } from '../types'
|
||||
import { fetchPluginTasks } from '@/service/plugins'
|
||||
|
||||
type PluginTasksStore = {
|
||||
pluginTasks: PluginTask[]
|
||||
setPluginTasks: (tasks: PluginTask[]) => void
|
||||
setPluginTasksWithPolling: () => void
|
||||
}
|
||||
|
||||
let pluginTasksTimer: NodeJS.Timeout | null = null
|
||||
|
||||
export const usePluginTasksStore = create<PluginTasksStore>(set => ({
|
||||
pluginTasks: [],
|
||||
setPluginTasks: (tasks: PluginTask[]) => set({ pluginTasks: tasks }),
|
||||
setPluginTasksWithPolling: async () => {
|
||||
if (pluginTasksTimer) {
|
||||
clearTimeout(pluginTasksTimer)
|
||||
pluginTasksTimer = null
|
||||
}
|
||||
const handleUpdatePluginTasks = async () => {
|
||||
const { tasks } = await fetchPluginTasks()
|
||||
set({ pluginTasks: tasks })
|
||||
|
||||
if (tasks.length && !tasks.every(task => task.status === 'success')) {
|
||||
pluginTasksTimer = setTimeout(() => {
|
||||
handleUpdatePluginTasks()
|
||||
}, 5000)
|
||||
}
|
||||
else {
|
||||
if (pluginTasksTimer) {
|
||||
clearTimeout(pluginTasksTimer)
|
||||
pluginTasksTimer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleUpdatePluginTasks()
|
||||
},
|
||||
}))
|
||||
@ -0,0 +1,26 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { UpdateFromGitHubPayload } from '../types'
|
||||
import InstallFromGitHub from '../install-plugin/install-from-github'
|
||||
|
||||
type Props = {
|
||||
payload: UpdateFromGitHubPayload
|
||||
onSave: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const FromGitHub: FC<Props> = ({
|
||||
payload,
|
||||
onSave,
|
||||
onCancel,
|
||||
}) => {
|
||||
return (
|
||||
<InstallFromGitHub
|
||||
updatePayload={payload}
|
||||
onClose={onCancel}
|
||||
onSuccess={onSave}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(FromGitHub)
|
||||
@ -0,0 +1,153 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { RiInformation2Line } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Card from '@/app/components/plugins/card'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Badge, { BadgeState } from '@/app/components/base/badge/index'
|
||||
import type { UpdateFromMarketPlacePayload } from '../types'
|
||||
import { pluginManifestToCardPluginProps } from '@/app/components/plugins/install-plugin/utils'
|
||||
import useGetIcon from '../install-plugin/base/use-get-icon'
|
||||
import { updateFromMarketPlace } from '@/service/plugins'
|
||||
import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status'
|
||||
import { usePluginTasksStore } from '@/app/components/plugins/plugin-page/store'
|
||||
|
||||
const i18nPrefix = 'plugin.upgrade'
|
||||
|
||||
type Props = {
|
||||
payload: UpdateFromMarketPlacePayload
|
||||
onSave: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
enum UploadStep {
|
||||
notStarted = 'notStarted',
|
||||
upgrading = 'upgrading',
|
||||
installed = 'installed',
|
||||
}
|
||||
|
||||
const UpdatePluginModal: FC<Props> = ({
|
||||
payload,
|
||||
onSave,
|
||||
onCancel,
|
||||
}) => {
|
||||
const {
|
||||
originalPackageInfo,
|
||||
targetPackageInfo,
|
||||
} = payload
|
||||
const { t } = useTranslation()
|
||||
const { getIconUrl } = useGetIcon()
|
||||
const [icon, setIcon] = useState<string>(originalPackageInfo.payload.icon)
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const icon = await getIconUrl(originalPackageInfo.payload.icon)
|
||||
setIcon(icon)
|
||||
})()
|
||||
}, [originalPackageInfo, getIconUrl])
|
||||
const {
|
||||
check,
|
||||
stop,
|
||||
} = checkTaskStatus()
|
||||
const handleCancel = () => {
|
||||
stop()
|
||||
onCancel()
|
||||
}
|
||||
|
||||
const [uploadStep, setUploadStep] = useState<UploadStep>(UploadStep.notStarted)
|
||||
const setPluginTasksWithPolling = usePluginTasksStore(s => s.setPluginTasksWithPolling)
|
||||
|
||||
const configBtnText = useMemo(() => {
|
||||
return ({
|
||||
[UploadStep.notStarted]: t(`${i18nPrefix}.upgrade`),
|
||||
[UploadStep.upgrading]: t(`${i18nPrefix}.upgrading`),
|
||||
[UploadStep.installed]: t(`${i18nPrefix}.close`),
|
||||
})[uploadStep]
|
||||
}, [t, uploadStep])
|
||||
|
||||
const handleConfirm = useCallback(async () => {
|
||||
if (uploadStep === UploadStep.notStarted) {
|
||||
setUploadStep(UploadStep.upgrading)
|
||||
const {
|
||||
all_installed: isInstalled,
|
||||
task_id: taskId,
|
||||
} = await updateFromMarketPlace({
|
||||
original_plugin_unique_identifier: originalPackageInfo.id,
|
||||
new_plugin_unique_identifier: targetPackageInfo.id,
|
||||
})
|
||||
if (isInstalled) {
|
||||
onSave()
|
||||
return
|
||||
}
|
||||
setPluginTasksWithPolling()
|
||||
await check({
|
||||
taskId,
|
||||
pluginUniqueIdentifier: targetPackageInfo.id,
|
||||
})
|
||||
onSave()
|
||||
}
|
||||
if (uploadStep === UploadStep.installed) {
|
||||
onSave()
|
||||
onCancel()
|
||||
}
|
||||
}, [onCancel, onSave, uploadStep, check, originalPackageInfo.id, setPluginTasksWithPolling, targetPackageInfo.id])
|
||||
const usedInAppInfo = useMemo(() => {
|
||||
return (
|
||||
<div className='flex px-0.5 justify-center items-center gap-0.5'>
|
||||
<div className='text-text-warning system-xs-medium'>{t(`${i18nPrefix}.usedInApps`, { num: 3 })}</div>
|
||||
{/* show the used apps */}
|
||||
<RiInformation2Line className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
)
|
||||
}, [t])
|
||||
return (
|
||||
<Modal
|
||||
isShow={true}
|
||||
onClose={onCancel}
|
||||
className='min-w-[560px]'
|
||||
closable
|
||||
title={t(`${i18nPrefix}.${uploadStep === UploadStep.installed ? 'successfulTitle' : 'title'}`)}
|
||||
>
|
||||
<div className='mt-3 mb-2 text-text-secondary system-md-regular'>
|
||||
{t(`${i18nPrefix}.description`)}
|
||||
</div>
|
||||
<div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap rounded-2xl bg-background-section-burn'>
|
||||
<Card
|
||||
installed={uploadStep === UploadStep.installed}
|
||||
payload={pluginManifestToCardPluginProps({
|
||||
...originalPackageInfo.payload,
|
||||
icon: icon!,
|
||||
})}
|
||||
className='w-full'
|
||||
titleLeft={
|
||||
<>
|
||||
<Badge className='mx-1' size="s" state={BadgeState.Warning}>
|
||||
{`${originalPackageInfo.payload.version} -> ${targetPackageInfo.version}`}
|
||||
</Badge>
|
||||
{false && usedInAppInfo}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex pt-5 justify-end items-center gap-2 self-stretch'>
|
||||
{uploadStep === UploadStep.notStarted && (
|
||||
<Button
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='primary'
|
||||
loading={uploadStep === UploadStep.upgrading}
|
||||
onClick={handleConfirm}
|
||||
disabled={uploadStep === UploadStep.upgrading}
|
||||
>
|
||||
{configBtnText}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(UpdatePluginModal)
|
||||
@ -1,97 +1,33 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { RiInformation2Line } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Card from '@/app/components/plugins/card'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Badge, { BadgeState } from '@/app/components/base/badge/index'
|
||||
import { toolNotion } from '@/app/components/plugins/card/card-mock'
|
||||
import React from 'react'
|
||||
import type { UpdatePluginModalType } from '../types'
|
||||
import { PluginSource } from '../types'
|
||||
import UpdateFromGitHub from './from-github'
|
||||
import UpdateFromMarketplace from './from-market-place'
|
||||
|
||||
const i18nPrefix = 'plugin.upgrade'
|
||||
|
||||
type Props = {
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
enum UploadStep {
|
||||
notStarted = 'notStarted',
|
||||
upgrading = 'upgrading',
|
||||
installed = 'installed',
|
||||
}
|
||||
|
||||
const UpdatePluginModal: FC<Props> = ({
|
||||
onHide,
|
||||
const UpdatePlugin: FC<UpdatePluginModalType> = ({
|
||||
type,
|
||||
marketPlace,
|
||||
github,
|
||||
onCancel,
|
||||
onSave,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [uploadStep, setUploadStep] = useState<UploadStep>(UploadStep.notStarted)
|
||||
const configBtnText = useMemo(() => {
|
||||
return ({
|
||||
[UploadStep.notStarted]: t(`${i18nPrefix}.upgrade`),
|
||||
[UploadStep.upgrading]: t(`${i18nPrefix}.upgrading`),
|
||||
[UploadStep.installed]: t(`${i18nPrefix}.close`),
|
||||
})[uploadStep]
|
||||
}, [uploadStep])
|
||||
const handleConfirm = useCallback(() => {
|
||||
if (uploadStep === UploadStep.notStarted) {
|
||||
setUploadStep(UploadStep.upgrading)
|
||||
setTimeout(() => {
|
||||
setUploadStep(UploadStep.installed)
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
if (uploadStep === UploadStep.installed)
|
||||
onHide()
|
||||
}, [uploadStep])
|
||||
if (type === PluginSource.github) {
|
||||
return (
|
||||
<UpdateFromGitHub
|
||||
payload={github!}
|
||||
onSave={onSave}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
isShow={true}
|
||||
onClose={onHide}
|
||||
className='min-w-[560px]'
|
||||
closable
|
||||
title={t(`${i18nPrefix}.${uploadStep === UploadStep.installed ? 'successfulTitle' : 'title'}`)}
|
||||
>
|
||||
<div className='mt-3 mb-2 text-text-secondary system-md-regular'>
|
||||
{t(`${i18nPrefix}.description`)}
|
||||
</div>
|
||||
<div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap rounded-2xl bg-background-section-burn'>
|
||||
<Card
|
||||
installed={uploadStep === UploadStep.installed}
|
||||
payload={toolNotion as any}
|
||||
className='w-full'
|
||||
titleLeft={
|
||||
<>
|
||||
<Badge className='mx-1' size="s" state={BadgeState.Warning}>
|
||||
{'1.2.0 -> 1.3.2'}
|
||||
</Badge>
|
||||
<div className='flex px-0.5 justify-center items-center gap-0.5'>
|
||||
<div className='text-text-warning system-xs-medium'>{t(`${i18nPrefix}.usedInApps`, { num: 3 })}</div>
|
||||
{/* show the used apps */}
|
||||
<RiInformation2Line className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex pt-5 justify-end items-center gap-2 self-stretch'>
|
||||
{uploadStep === UploadStep.notStarted && (
|
||||
<Button
|
||||
onClick={onHide}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='primary'
|
||||
loading={uploadStep === UploadStep.upgrading}
|
||||
onClick={handleConfirm}
|
||||
disabled={uploadStep === UploadStep.upgrading}
|
||||
>
|
||||
{configBtnText}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<UpdateFromMarketplace
|
||||
payload={marketPlace!}
|
||||
onSave={onSave}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(UpdatePluginModal)
|
||||
export default React.memo(UpdatePlugin)
|
||||
|
||||
@ -0,0 +1,361 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDebounce, useGetState } from 'ahooks'
|
||||
import produce from 'immer'
|
||||
import { LinkExternal02, Settings01 } from '../../base/icons/src/vender/line/general'
|
||||
import type { Credential, CustomCollectionBackend, CustomParamSchema, Emoji } from '../types'
|
||||
import { AuthHeaderPrefix, AuthType } from '../types'
|
||||
import GetSchema from './get-schema'
|
||||
import ConfigCredentials from './config-credentials'
|
||||
import TestApi from './test-api'
|
||||
import cn from '@/utils/classnames'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import EmojiPicker from '@/app/components/base/emoji-picker'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { parseParamsSchema } from '@/service/tools'
|
||||
import LabelSelector from '@/app/components/tools/labels/selector'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Modal from '../../base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
const fieldNameClassNames = 'py-2 leading-5 text-sm font-medium text-gray-900'
|
||||
type Props = {
|
||||
positionLeft?: boolean
|
||||
payload: any
|
||||
onHide: () => void
|
||||
onAdd?: (payload: CustomCollectionBackend) => void
|
||||
onRemove?: () => void
|
||||
onEdit?: (payload: CustomCollectionBackend) => void
|
||||
}
|
||||
// Add and Edit
|
||||
const EditCustomCollectionModal: FC<Props> = ({
|
||||
payload,
|
||||
onHide,
|
||||
onAdd,
|
||||
onEdit,
|
||||
onRemove,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isAdd = !payload
|
||||
const isEdit = !!payload
|
||||
|
||||
const [editFirst, setEditFirst] = useState(!isAdd)
|
||||
const [paramsSchemas, setParamsSchemas] = useState<CustomParamSchema[]>(payload?.tools || [])
|
||||
const [customCollection, setCustomCollection, getCustomCollection] = useGetState<CustomCollectionBackend>(isAdd
|
||||
? {
|
||||
provider: '',
|
||||
credentials: {
|
||||
auth_type: AuthType.none,
|
||||
api_key_header: 'Authorization',
|
||||
api_key_header_prefix: AuthHeaderPrefix.basic,
|
||||
},
|
||||
icon: {
|
||||
content: '🕵️',
|
||||
background: '#FEF7C3',
|
||||
},
|
||||
schema_type: '',
|
||||
schema: '',
|
||||
}
|
||||
: payload)
|
||||
|
||||
const originalProvider = isEdit ? payload.provider : ''
|
||||
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
||||
const emoji = customCollection.icon
|
||||
const setEmoji = (emoji: Emoji) => {
|
||||
const newCollection = produce(customCollection, (draft) => {
|
||||
draft.icon = emoji
|
||||
})
|
||||
setCustomCollection(newCollection)
|
||||
}
|
||||
const schema = customCollection.schema
|
||||
const debouncedSchema = useDebounce(schema, { wait: 500 })
|
||||
const setSchema = (schema: any) => {
|
||||
const newCollection = produce(customCollection, (draft) => {
|
||||
draft.schema = schema
|
||||
})
|
||||
setCustomCollection(newCollection)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!debouncedSchema)
|
||||
return
|
||||
if (isEdit && editFirst) {
|
||||
setEditFirst(false)
|
||||
return
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
const { parameters_schema, schema_type } = await parseParamsSchema(debouncedSchema)
|
||||
const customCollection = getCustomCollection()
|
||||
const newCollection = produce(customCollection, (draft) => {
|
||||
draft.schema_type = schema_type
|
||||
})
|
||||
setCustomCollection(newCollection)
|
||||
setParamsSchemas(parameters_schema)
|
||||
}
|
||||
catch (e) {
|
||||
const customCollection = getCustomCollection()
|
||||
const newCollection = produce(customCollection, (draft) => {
|
||||
draft.schema_type = ''
|
||||
})
|
||||
setCustomCollection(newCollection)
|
||||
setParamsSchemas([])
|
||||
}
|
||||
})()
|
||||
}, [debouncedSchema])
|
||||
|
||||
const [credentialsModalShow, setCredentialsModalShow] = useState(false)
|
||||
const credential = customCollection.credentials
|
||||
const setCredential = (credential: Credential) => {
|
||||
const newCollection = produce(customCollection, (draft) => {
|
||||
draft.credentials = credential
|
||||
})
|
||||
setCustomCollection(newCollection)
|
||||
}
|
||||
|
||||
const [currTool, setCurrTool] = useState<CustomParamSchema | null>(null)
|
||||
const [isShowTestApi, setIsShowTestApi] = useState(false)
|
||||
|
||||
const [labels, setLabels] = useState<string[]>(payload?.labels || [])
|
||||
const handleLabelSelect = (value: string[]) => {
|
||||
setLabels(value)
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
// const postData = clone(customCollection)
|
||||
const postData = produce(customCollection, (draft) => {
|
||||
delete draft.tools
|
||||
|
||||
if (draft.credentials.auth_type === AuthType.none) {
|
||||
delete draft.credentials.api_key_header
|
||||
delete draft.credentials.api_key_header_prefix
|
||||
delete draft.credentials.api_key_value
|
||||
}
|
||||
|
||||
draft.labels = labels
|
||||
})
|
||||
|
||||
let errorMessage = ''
|
||||
if (!postData.provider)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: t('tools.createTool.name') })
|
||||
|
||||
if (!postData.schema)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: t('tools.createTool.schema') })
|
||||
|
||||
if (errorMessage) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isAdd) {
|
||||
onAdd?.(postData)
|
||||
return
|
||||
}
|
||||
|
||||
onEdit?.({
|
||||
...postData,
|
||||
original_provider: originalProvider,
|
||||
})
|
||||
}
|
||||
|
||||
const getPath = (url: string) => {
|
||||
if (!url)
|
||||
return ''
|
||||
|
||||
try {
|
||||
const path = decodeURI(new URL(url).pathname)
|
||||
return path || ''
|
||||
}
|
||||
catch (e) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isShow
|
||||
onClose={onHide}
|
||||
closable
|
||||
className='!p-0 !max-w-[630px] !h-[calc(100vh-16px)]'
|
||||
>
|
||||
<div className='flex flex-col h-full'>
|
||||
<div className='ml-6 mt-6 text-base font-semibold text-gray-900'>
|
||||
{t('tools.createTool.title')}
|
||||
</div>
|
||||
<div className='grow h-0 overflow-y-auto px-6 py-3 space-y-4'>
|
||||
<div>
|
||||
<div className={fieldNameClassNames}>{t('tools.createTool.name')} <span className='ml-1 text-red-500'>*</span></div>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.content} background={emoji.background} />
|
||||
<Input
|
||||
className='h-10 grow' placeholder={t('tools.createTool.toolNamePlaceHolder')!}
|
||||
value={customCollection.provider}
|
||||
onChange={(e) => {
|
||||
const newCollection = produce(customCollection, (draft) => {
|
||||
draft.provider = e.target.value
|
||||
})
|
||||
setCustomCollection(newCollection)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Schema */}
|
||||
<div className='select-none'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='flex items-center'>
|
||||
<div className={fieldNameClassNames}>{t('tools.createTool.schema')}<span className='ml-1 text-red-500'>*</span></div>
|
||||
<div className='mx-2 w-px h-3 bg-black/5'></div>
|
||||
<a
|
||||
href="https://swagger.io/specification/"
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
className='flex items-center h-[18px] space-x-1 text-[#155EEF]'
|
||||
>
|
||||
<div className='text-xs font-normal'>{t('tools.createTool.viewSchemaSpec')}</div>
|
||||
<LinkExternal02 className='w-3 h-3' />
|
||||
</a>
|
||||
</div>
|
||||
<GetSchema onChange={setSchema} />
|
||||
|
||||
</div>
|
||||
<Textarea
|
||||
className='h-[240px] resize-none'
|
||||
value={schema}
|
||||
onChange={e => setSchema(e.target.value)}
|
||||
placeholder={t('tools.createTool.schemaPlaceHolder')!}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Available Tools */}
|
||||
<div>
|
||||
<div className={fieldNameClassNames}>{t('tools.createTool.availableTools.title')}</div>
|
||||
<div className='rounded-lg border border-gray-200 w-full overflow-x-auto'>
|
||||
<table className='w-full leading-[18px] text-xs text-gray-700 font-normal'>
|
||||
<thead className='text-gray-500 uppercase'>
|
||||
<tr className={cn(paramsSchemas.length > 0 && 'border-b', 'border-gray-200')}>
|
||||
<th className="p-2 pl-3 font-medium">{t('tools.createTool.availableTools.name')}</th>
|
||||
<th className="p-2 pl-3 font-medium w-[236px]">{t('tools.createTool.availableTools.description')}</th>
|
||||
<th className="p-2 pl-3 font-medium">{t('tools.createTool.availableTools.method')}</th>
|
||||
<th className="p-2 pl-3 font-medium">{t('tools.createTool.availableTools.path')}</th>
|
||||
<th className="p-2 pl-3 font-medium w-[54px]">{t('tools.createTool.availableTools.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paramsSchemas.map((item, index) => (
|
||||
<tr key={index} className='border-b last:border-0 border-gray-200'>
|
||||
<td className="p-2 pl-3">{item.operation_id}</td>
|
||||
<td className="p-2 pl-3 text-gray-500 w-[236px]">{item.summary}</td>
|
||||
<td className="p-2 pl-3">{item.method}</td>
|
||||
<td className="p-2 pl-3">{getPath(item.server_url)}</td>
|
||||
<td className="p-2 pl-3 w-[62px]">
|
||||
<Button
|
||||
size='small'
|
||||
onClick={() => {
|
||||
setCurrTool(item)
|
||||
setIsShowTestApi(true)
|
||||
}}
|
||||
>
|
||||
{t('tools.createTool.availableTools.test')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Authorization method */}
|
||||
<div>
|
||||
<div className={fieldNameClassNames}>{t('tools.createTool.authMethod.title')}</div>
|
||||
<div className='flex items-center h-9 justify-between px-2.5 bg-gray-100 rounded-lg cursor-pointer' onClick={() => setCredentialsModalShow(true)}>
|
||||
<div className='text-sm font-normal text-gray-900'>{t(`tools.createTool.authMethod.types.${credential.auth_type}`)}</div>
|
||||
<Settings01 className='w-4 h-4 text-gray-700 opacity-60' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Labels */}
|
||||
<div>
|
||||
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.toolInput.label')}</div>
|
||||
<LabelSelector value={labels} onChange={handleLabelSelect} />
|
||||
</div>
|
||||
|
||||
{/* Privacy Policy */}
|
||||
<div>
|
||||
<div className={fieldNameClassNames}>{t('tools.createTool.privacyPolicy')}</div>
|
||||
<Input
|
||||
value={customCollection.privacy_policy}
|
||||
onChange={(e) => {
|
||||
const newCollection = produce(customCollection, (draft) => {
|
||||
draft.privacy_policy = e.target.value
|
||||
})
|
||||
setCustomCollection(newCollection)
|
||||
}}
|
||||
className='h-10 grow' placeholder={t('tools.createTool.privacyPolicyPlaceholder') || ''} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={fieldNameClassNames}>{t('tools.createTool.customDisclaimer')}</div>
|
||||
<Input
|
||||
value={customCollection.custom_disclaimer}
|
||||
onChange={(e) => {
|
||||
const newCollection = produce(customCollection, (draft) => {
|
||||
draft.custom_disclaimer = e.target.value
|
||||
})
|
||||
setCustomCollection(newCollection)
|
||||
}}
|
||||
className='h-10 grow' placeholder={t('tools.createTool.customDisclaimerPlaceholder') || ''} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className={cn(isEdit ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-gray-50 border-t border-black/5')} >
|
||||
{
|
||||
isEdit && (
|
||||
<Button onClick={onRemove} className='text-red-500 border-red-50 hover:border-red-500'>{t('common.operation.delete')}</Button>
|
||||
)
|
||||
}
|
||||
<div className='flex space-x-2 '>
|
||||
<Button onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
<Button variant='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
{showEmojiPicker && <EmojiPicker
|
||||
onSelect={(icon, icon_background) => {
|
||||
setEmoji({ content: icon, background: icon_background })
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
/>}
|
||||
{credentialsModalShow && (
|
||||
<ConfigCredentials
|
||||
positionCenter={isAdd}
|
||||
credential={credential}
|
||||
onChange={setCredential}
|
||||
onHide={() => setCredentialsModalShow(false)}
|
||||
/>)
|
||||
}
|
||||
{isShowTestApi && (
|
||||
<TestApi
|
||||
positionCenter={isAdd}
|
||||
tool={currTool as CustomParamSchema}
|
||||
customCollection={customCollection}
|
||||
onHide={() => setIsShowTestApi(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(EditCustomCollectionModal)
|
||||
@ -1,6 +1,4 @@
|
||||
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
export type Label = {
|
||||
name: string
|
||||
icon: string
|
||||
label: TypeWithI18N
|
||||
label: string
|
||||
}
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
import { create } from 'zustand'
|
||||
import type { Label } from './constant'
|
||||
|
||||
type State = {
|
||||
labelList: Label[]
|
||||
}
|
||||
|
||||
type Action = {
|
||||
setLabelList: (labelList?: Label[]) => void
|
||||
}
|
||||
|
||||
export const useStore = create<State & Action>(set => ({
|
||||
labelList: [],
|
||||
setLabelList: labelList => set(() => ({ labelList })),
|
||||
}))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue