mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 10:40:07 +08:00
fix: plugins related wip
This commit is contained in:
parent
bd4a979f62
commit
468aebd632
@ -1,6 +1,7 @@
|
|||||||
import { Button, Chip, Skeleton, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@heroui/react'
|
|
||||||
import type { InstalledPlugin } from '@renderer/types/plugin'
|
import type { InstalledPlugin } from '@renderer/types/plugin'
|
||||||
import { Trash2 } from 'lucide-react'
|
import type { TableProps } from 'antd'
|
||||||
|
import { Button, Skeleton, Table as AntTable, Tag } from 'antd'
|
||||||
|
import { Dot, Trash2 } from 'lucide-react'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -33,10 +34,10 @@ export const InstalledPluginsList: FC<InstalledPluginsListProps> = ({ plugins, o
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
<Skeleton className="h-12 w-full rounded-lg" />
|
<Skeleton.Input active className="w-full" size={'large'} style={{ width: '100%' }} />
|
||||||
<Skeleton className="h-12 w-full rounded-lg" />
|
<Skeleton.Input active className="w-full" size={'large'} style={{ width: '100%' }} />
|
||||||
<Skeleton className="h-12 w-full rounded-lg" />
|
<Skeleton.Input active className="w-full" size={'large'} style={{ width: '100%' }} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -50,50 +51,59 @@ export const InstalledPluginsList: FC<InstalledPluginsListProps> = ({ plugins, o
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const columns: TableProps<InstalledPlugin>['columns'] = [
|
||||||
<Table aria-label="Installed plugins table" removeWrapper>
|
{
|
||||||
<TableHeader>
|
title: t('plugins.name'),
|
||||||
<TableColumn>{t('plugins.name')}</TableColumn>
|
dataIndex: 'name',
|
||||||
<TableColumn>{t('plugins.type')}</TableColumn>
|
key: 'name',
|
||||||
<TableColumn>{t('plugins.category')}</TableColumn>
|
render: (_: any, plugin: InstalledPlugin) => (
|
||||||
<TableColumn align="end">{t('plugins.actions')}</TableColumn>
|
<div className="flex flex-col">
|
||||||
</TableHeader>
|
<span className="font-semibold text-small">{plugin.metadata.name}</span>
|
||||||
<TableBody>
|
{plugin.metadata.description && (
|
||||||
{plugins.map((plugin) => (
|
<span className="line-clamp-1 text-default-400 text-tiny">{plugin.metadata.description}</span>
|
||||||
<TableRow key={plugin.filename}>
|
)}
|
||||||
<TableCell>
|
</div>
|
||||||
<div className="flex flex-col">
|
)
|
||||||
<span className="font-semibold text-small">{plugin.metadata.name}</span>
|
},
|
||||||
{plugin.metadata.description && (
|
{
|
||||||
<span className="line-clamp-1 text-default-400 text-tiny">{plugin.metadata.description}</span>
|
title: t('plugins.type'),
|
||||||
)}
|
dataIndex: 'type',
|
||||||
</div>
|
key: 'type',
|
||||||
</TableCell>
|
render: (type: string) => <Tag color={type === 'agent' ? 'magenta' : 'purple'}>{type}</Tag>
|
||||||
<TableCell>
|
},
|
||||||
<Chip size="sm" variant="flat" color={plugin.type === 'agent' ? 'primary' : 'secondary'}>
|
{
|
||||||
{plugin.type}
|
title: t('plugins.category'),
|
||||||
</Chip>
|
dataIndex: 'category',
|
||||||
</TableCell>
|
key: 'category',
|
||||||
<TableCell>
|
render: (_: any, plugin: InstalledPlugin) => (
|
||||||
<Chip size="sm" variant="dot">
|
<Tag
|
||||||
{plugin.metadata.category}
|
icon={<Dot size={14} strokeWidth={8} />}
|
||||||
</Chip>
|
style={{
|
||||||
</TableCell>
|
display: 'flex',
|
||||||
<TableCell>
|
flexDirection: 'row',
|
||||||
<Button
|
alignItems: 'center',
|
||||||
size="sm"
|
gap: '2px'
|
||||||
color="danger"
|
}}>
|
||||||
variant="light"
|
{plugin.metadata.category}
|
||||||
isIconOnly
|
</Tag>
|
||||||
onPress={() => handleUninstall(plugin)}
|
)
|
||||||
isLoading={uninstallingPlugin === plugin.filename}
|
},
|
||||||
isDisabled={loading}>
|
{
|
||||||
<Trash2 className="h-4 w-4" />
|
title: t('plugins.actions'),
|
||||||
</Button>
|
key: 'actions',
|
||||||
</TableCell>
|
align: 'right' as const,
|
||||||
</TableRow>
|
render: (_: any, plugin: InstalledPlugin) => (
|
||||||
))}
|
<Button
|
||||||
</TableBody>
|
danger
|
||||||
</Table>
|
type="text"
|
||||||
)
|
onClick={() => handleUninstall(plugin)}
|
||||||
|
loading={uninstallingPlugin === plugin.filename}
|
||||||
|
disabled={loading}
|
||||||
|
icon={<Trash2 className="h-4 w-4" />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return <AntTable columns={columns} dataSource={plugins} />
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,6 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Chip,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalContent,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
Spinner,
|
|
||||||
Textarea
|
|
||||||
} from '@heroui/react'
|
|
||||||
import type { PluginMetadata } from '@renderer/types/plugin'
|
import type { PluginMetadata } from '@renderer/types/plugin'
|
||||||
import { Download, Edit, Save, Trash2, X } from 'lucide-react'
|
import { Button as AntButton, Input, Modal as AntdModal, Spin, Tag } from 'antd'
|
||||||
|
import { Dot, Download, Edit, Save, Trash2, X } from 'lucide-react'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
@ -121,200 +111,203 @@ export const PluginDetailModal: FC<PluginDetailModalProps> = ({
|
|||||||
if (!plugin) return null
|
if (!plugin) return null
|
||||||
|
|
||||||
const modalContent = (
|
const modalContent = (
|
||||||
<Modal
|
<AntdModal
|
||||||
isOpen={isOpen}
|
centered
|
||||||
onClose={onClose}
|
open={isOpen}
|
||||||
size="2xl"
|
onCancel={onClose}
|
||||||
scrollBehavior="inside"
|
styles={{
|
||||||
classNames={{
|
body: {
|
||||||
wrapper: 'z-[9999]'
|
maxHeight: '60vh',
|
||||||
}}>
|
overflowY: 'auto'
|
||||||
<ModalContent>
|
}
|
||||||
<ModalHeader className="flex flex-col gap-1">
|
}}
|
||||||
|
style={{
|
||||||
|
width: '70%'
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h2 className="font-bold text-xl">{plugin.name}</h2>
|
<h2 className="font-bold text-xl">{plugin.name}</h2>
|
||||||
<Chip size="sm" variant="solid" color={plugin.type === 'agent' ? 'primary' : 'secondary'}>
|
<Tag color={plugin.type === 'agent' ? 'magenta' : 'purple'}>{plugin.type}</Tag>
|
||||||
{plugin.type}
|
|
||||||
</Chip>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Chip size="sm" variant="dot" color="default">
|
<Tag
|
||||||
|
icon={<Dot size={14} strokeWidth={8} />}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '2px'
|
||||||
|
}}>
|
||||||
{plugin.category}
|
{plugin.category}
|
||||||
</Chip>
|
</Tag>
|
||||||
{plugin.version && (
|
{plugin.version && <Tag>v{plugin.version}</Tag>}
|
||||||
<Chip size="sm" variant="bordered">
|
|
||||||
v{plugin.version}
|
|
||||||
</Chip>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</ModalHeader>
|
</div>
|
||||||
|
}
|
||||||
<ModalBody>
|
footer={
|
||||||
{/* Description */}
|
<div className="flex flex-row justify-end gap-4">
|
||||||
{plugin.description && (
|
<AntButton type="text" onClick={onClose}>
|
||||||
<div className="mb-4">
|
{t('common.close')}
|
||||||
<h3 className="mb-2 font-semibold text-small">Description</h3>
|
</AntButton>
|
||||||
<p className="text-default-600 text-small">{plugin.description}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Author */}
|
|
||||||
{plugin.author && (
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="mb-2 font-semibold text-small">Author</h3>
|
|
||||||
<p className="text-default-600 text-small">{plugin.author}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Tools (for agents) */}
|
|
||||||
{plugin.tools && plugin.tools.length > 0 && (
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="mb-2 font-semibold text-small">Tools</h3>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{plugin.tools.map((tool) => (
|
|
||||||
<Chip key={tool} size="sm" variant="flat">
|
|
||||||
{tool}
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Allowed Tools (for commands) */}
|
|
||||||
{plugin.allowed_tools && plugin.allowed_tools.length > 0 && (
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="mb-2 font-semibold text-small">Allowed Tools</h3>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{plugin.allowed_tools.map((tool) => (
|
|
||||||
<Chip key={tool} size="sm" variant="flat">
|
|
||||||
{tool}
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Tags */}
|
|
||||||
{plugin.tags && plugin.tags.length > 0 && (
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="mb-2 font-semibold text-small">Tags</h3>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{plugin.tags.map((tag) => (
|
|
||||||
<Chip key={tag} size="sm" variant="bordered">
|
|
||||||
{tag}
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Metadata */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="mb-2 font-semibold text-small">Metadata</h3>
|
|
||||||
<div className="space-y-1 text-small">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-default-500">File:</span>
|
|
||||||
<span className="font-mono text-default-600 text-tiny">{plugin.filename}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-default-500">Size:</span>
|
|
||||||
<span className="text-default-600">{(plugin.size / 1024).toFixed(2)} KB</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-default-500">Source:</span>
|
|
||||||
<span className="font-mono text-default-600 text-tiny">{plugin.sourcePath}</span>
|
|
||||||
</div>
|
|
||||||
{plugin.installedAt && (
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-default-500">Installed:</span>
|
|
||||||
<span className="text-default-600">{new Date(plugin.installedAt).toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<div className="mb-2 flex items-center justify-between">
|
|
||||||
<h3 className="font-semibold text-small">Content</h3>
|
|
||||||
{installed && !contentLoading && !contentError && (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{isEditing ? (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="flat"
|
|
||||||
color="danger"
|
|
||||||
startContent={<X className="h-3 w-3" />}
|
|
||||||
onPress={handleCancelEdit}
|
|
||||||
isDisabled={saving}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
color="primary"
|
|
||||||
startContent={saving ? <Spinner size="sm" color="current" /> : <Save className="h-3 w-3" />}
|
|
||||||
onPress={handleSave}
|
|
||||||
isDisabled={saving}>
|
|
||||||
{saving ? 'Saving...' : 'Save'}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Button size="sm" variant="flat" startContent={<Edit className="h-3 w-3" />} onPress={handleEdit}>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{contentLoading ? (
|
|
||||||
<div className="flex items-center justify-center py-4">
|
|
||||||
<Spinner size="sm" />
|
|
||||||
</div>
|
|
||||||
) : contentError ? (
|
|
||||||
<div className="rounded-md bg-danger-50 p-3 text-danger text-small">{contentError}</div>
|
|
||||||
) : isEditing ? (
|
|
||||||
<Textarea
|
|
||||||
value={editedContent}
|
|
||||||
onValueChange={setEditedContent}
|
|
||||||
minRows={20}
|
|
||||||
classNames={{
|
|
||||||
input: 'font-mono text-tiny'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<pre className="max-h-96 overflow-auto whitespace-pre-wrap rounded-md bg-default-100 p-3 font-mono text-tiny">
|
|
||||||
{content}
|
|
||||||
</pre>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button variant="light" onPress={onClose}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
{installed ? (
|
{installed ? (
|
||||||
<Button
|
<AntButton
|
||||||
color="danger"
|
danger
|
||||||
variant="flat"
|
variant="filled"
|
||||||
startContent={loading ? <Spinner size="sm" color="current" /> : <Trash2 className="h-4 w-4" />}
|
icon={loading ? <Spin size="small" /> : <Trash2 className="h-4 w-4" />}
|
||||||
onPress={onUninstall}
|
iconPosition={'start'}
|
||||||
isDisabled={loading}>
|
onClick={onUninstall}
|
||||||
|
disabled={loading}>
|
||||||
{loading ? t('plugins.uninstalling') : t('plugins.uninstall')}
|
{loading ? t('plugins.uninstalling') : t('plugins.uninstall')}
|
||||||
</Button>
|
</AntButton>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<AntButton
|
||||||
color="primary"
|
color="primary"
|
||||||
startContent={loading ? <Spinner size="sm" color="current" /> : <Download className="h-4 w-4" />}
|
variant="solid"
|
||||||
onPress={onInstall}
|
icon={loading ? <Spin size="small" /> : <Download className="h-4 w-4" />}
|
||||||
isDisabled={loading}>
|
iconPosition={'start'}
|
||||||
|
onClick={onInstall}
|
||||||
|
disabled={loading}>
|
||||||
{loading ? t('plugins.installing') : t('plugins.install')}
|
{loading ? t('plugins.installing') : t('plugins.install')}
|
||||||
</Button>
|
</AntButton>
|
||||||
)}
|
)}
|
||||||
</ModalFooter>
|
</div>
|
||||||
</ModalContent>
|
}>
|
||||||
</Modal>
|
<div>
|
||||||
|
{/* Description */}
|
||||||
|
{plugin.description && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="mb-2 font-semibold text-small">Description</h3>
|
||||||
|
<p className="text-default-600 text-small">{plugin.description}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Author */}
|
||||||
|
{plugin.author && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="mb-2 font-semibold text-small">Author</h3>
|
||||||
|
<p className="text-default-600 text-small">{plugin.author}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tools (for agents) */}
|
||||||
|
{plugin.tools && plugin.tools.length > 0 && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="mb-2 font-semibold text-small">Tools</h3>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{plugin.tools.map((tool) => (
|
||||||
|
<Tag key={tool}>{tool}</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Allowed Tools (for commands) */}
|
||||||
|
{plugin.allowed_tools && plugin.allowed_tools.length > 0 && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="mb-2 font-semibold text-small">Allowed Tools</h3>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{plugin.allowed_tools.map((tool) => (
|
||||||
|
<Tag key={tool}>{tool}</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tags */}
|
||||||
|
{plugin.tags && plugin.tags.length > 0 && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="mb-2 font-semibold text-small">Tags</h3>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{plugin.tags.map((tag) => (
|
||||||
|
<Tag key={tag}>{tag}</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Metadata */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="mb-2 font-semibold text-small">Metadata</h3>
|
||||||
|
<div className="space-y-1 text-small">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-default-500">File:</span>
|
||||||
|
<span className="font-mono text-default-600 text-tiny">{plugin.filename}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-default-500">Size:</span>
|
||||||
|
<span className="text-default-600">{(plugin.size / 1024).toFixed(2)} KB</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-default-500">Source:</span>
|
||||||
|
<span className="font-mono text-default-600 text-tiny">{plugin.sourcePath}</span>
|
||||||
|
</div>
|
||||||
|
{plugin.installedAt && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-default-500">Installed:</span>
|
||||||
|
<span className="text-default-600">{new Date(plugin.installedAt).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<h3 className="font-semibold text-small">Content</h3>
|
||||||
|
{installed && !contentLoading && !contentError && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{isEditing ? (
|
||||||
|
<>
|
||||||
|
<AntButton
|
||||||
|
danger
|
||||||
|
variant="filled"
|
||||||
|
icon={<X className="h-3 w-3" />}
|
||||||
|
iconPosition="start"
|
||||||
|
onClick={handleCancelEdit}
|
||||||
|
disabled={saving}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</AntButton>
|
||||||
|
<AntButton
|
||||||
|
color="primary"
|
||||||
|
variant="filled"
|
||||||
|
icon={saving ? <Spin size="small" /> : <Save className="h-3 w-3" />}
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving}>
|
||||||
|
{t('common.save')}
|
||||||
|
</AntButton>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<AntButton variant="filled" icon={<Edit className="h-3 w-3" />} onClick={handleEdit}>
|
||||||
|
{t('common.edit')}
|
||||||
|
</AntButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{contentLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-4">
|
||||||
|
<Spin size="small" />
|
||||||
|
</div>
|
||||||
|
) : contentError ? (
|
||||||
|
<div className="rounded-md bg-danger-50 p-3 text-danger text-small">{contentError}</div>
|
||||||
|
) : isEditing ? (
|
||||||
|
<Input.TextArea
|
||||||
|
value={editedContent}
|
||||||
|
onChange={(e) => setEditedContent(e.target.value)}
|
||||||
|
autoSize={{ minRows: 20 }}
|
||||||
|
classNames={{
|
||||||
|
textarea: 'font-mono text-tiny'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<pre className="max-h-96 overflow-auto whitespace-pre-wrap rounded-md bg-default-100 p-3 font-mono text-tiny">
|
||||||
|
{content}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AntdModal>
|
||||||
)
|
)
|
||||||
|
|
||||||
return createPortal(modalContent, document.body)
|
return createPortal(modalContent, document.body)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user