feat(web): merge plugin readme and config into single detail dialog (#2076)

* feat(web): merge plugin readme and config into single detail dialog

- Click plugin card directly opens combined dialog (left: readme, right: config)
- Remove hover overlay with separate readme/config buttons
- Dropdown menu (⋯) still available for update/delete/view source

* fix: prettier format for lucide import
This commit is contained in:
Junyan Chin
2026-03-23 22:22:31 +08:00
committed by GitHub
parent 9c659ce8fa
commit c13971d7d6
2 changed files with 44 additions and 113 deletions
@@ -38,12 +38,10 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
(props, ref) => { (props, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [pluginList, setPluginList] = useState<PluginCardVO[]>([]); const [pluginList, setPluginList] = useState<PluginCardVO[]>([]);
const [modalOpen, setModalOpen] = useState<boolean>(false); const [detailModalOpen, setDetailModalOpen] = useState<boolean>(false);
const [selectedPlugin, setSelectedPlugin] = useState<PluginCardVO | null>( const [selectedPlugin, setSelectedPlugin] = useState<PluginCardVO | null>(
null, null,
); );
const [readmeModalOpen, setReadmeModalOpen] = useState<boolean>(false);
const [readmePlugin, setReadmePlugin] = useState<PluginCardVO | null>(null);
const [showOperationModal, setShowOperationModal] = useState(false); const [showOperationModal, setShowOperationModal] = useState(false);
const [operationType, setOperationType] = useState<PluginOperationType>( const [operationType, setOperationType] = useState<PluginOperationType>(
PluginOperationType.DELETE, PluginOperationType.DELETE,
@@ -166,12 +164,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
function handlePluginClick(plugin: PluginCardVO) { function handlePluginClick(plugin: PluginCardVO) {
setSelectedPlugin(plugin); setSelectedPlugin(plugin);
setModalOpen(true); setDetailModalOpen(true);
}
function handleViewReadme(plugin: PluginCardVO) {
setReadmePlugin(plugin);
setReadmeModalOpen(true);
} }
function handlePluginDelete(plugin: PluginCardVO) { function handlePluginDelete(plugin: PluginCardVO) {
@@ -355,52 +348,46 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
</div> </div>
) : ( ) : (
<div className={`${styles.pluginListContainer}`}> <div className={`${styles.pluginListContainer}`}>
<Dialog open={modalOpen} onOpenChange={setModalOpen}> <Dialog open={detailModalOpen} onOpenChange={setDetailModalOpen}>
<DialogContent className="w-[700px] max-h-[80vh] p-0 flex flex-col"> <DialogContent className="sm:max-w-[1100px] max-w-[95vw] max-h-[85vh] p-0 flex flex-col">
<DialogHeader className="px-6 pt-6 pb-2">
<DialogTitle>{t('plugins.pluginConfig')}</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto px-6">
{selectedPlugin && (
<PluginForm
pluginAuthor={selectedPlugin.author}
pluginName={selectedPlugin.name}
onFormSubmit={(timeout?: number) => {
setModalOpen(false);
if (timeout) {
setTimeout(() => {
getPluginList();
}, timeout);
} else {
getPluginList();
}
}}
onFormCancel={() => {
setModalOpen(false);
}}
/>
)}
</div>
</DialogContent>
</Dialog>
<Dialog open={readmeModalOpen} onOpenChange={setReadmeModalOpen}>
<DialogContent className="sm:max-w-[900px] max-w-[90vw] max-h-[85vh] p-0 flex flex-col">
<DialogHeader className="px-6 pt-6 pb-2 border-b"> <DialogHeader className="px-6 pt-6 pb-2 border-b">
<DialogTitle> <DialogTitle>
{readmePlugin && {selectedPlugin &&
`${readmePlugin.author}/${readmePlugin.name} - ${t( `${selectedPlugin.author}/${selectedPlugin.name}`}
'plugins.readme',
)}`}
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="flex-1 overflow-y-auto"> <div className="flex-1 flex flex-row overflow-hidden min-h-0">
{readmePlugin && ( {/* Left side - Readme */}
<PluginReadme <div className="flex-1 overflow-y-auto border-r min-w-0">
pluginAuthor={readmePlugin.author} {selectedPlugin && (
pluginName={readmePlugin.name} <PluginReadme
/> pluginAuthor={selectedPlugin.author}
)} pluginName={selectedPlugin.name}
/>
)}
</div>
{/* Right side - Config */}
<div className="w-[380px] flex-shrink-0 overflow-y-auto px-4">
{selectedPlugin && (
<PluginForm
pluginAuthor={selectedPlugin.author}
pluginName={selectedPlugin.name}
onFormSubmit={(timeout?: number) => {
setDetailModalOpen(false);
if (timeout) {
setTimeout(() => {
getPluginList();
}, timeout);
} else {
getPluginList();
}
}}
onFormCancel={() => {
setDetailModalOpen(false);
}}
/>
)}
</div>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -413,7 +400,6 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
onCardClick={() => handlePluginClick(vo)} onCardClick={() => handlePluginClick(vo)}
onDeleteClick={() => handlePluginDelete(vo)} onDeleteClick={() => handlePluginDelete(vo)}
onUpgradeClick={() => handlePluginUpdate(vo)} onUpgradeClick={() => handlePluginUpdate(vo)}
onViewReadme={() => handleViewReadme(vo)}
/> />
</div> </div>
); );
@@ -2,15 +2,7 @@ import { PluginCardVO } from '@/app/home/plugins/components/plugin-installed/Plu
import { useState } from 'react'; import { useState } from 'react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import { BugIcon, ExternalLink, Ellipsis, Trash, ArrowUp } from 'lucide-react';
BugIcon,
ExternalLink,
Ellipsis,
Trash,
ArrowUp,
Settings,
FileText,
} from 'lucide-react';
import { getCloudServiceClientSync, systemInfo } from '@/app/infra/http'; import { getCloudServiceClientSync, systemInfo } from '@/app/infra/http';
import { httpClient } from '@/app/infra/http/HttpClient'; import { httpClient } from '@/app/infra/http/HttpClient';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -27,28 +19,20 @@ export default function PluginCardComponent({
onCardClick, onCardClick,
onDeleteClick, onDeleteClick,
onUpgradeClick, onUpgradeClick,
onViewReadme,
}: { }: {
cardVO: PluginCardVO; cardVO: PluginCardVO;
onCardClick: () => void; onCardClick: () => void;
onDeleteClick: (cardVO: PluginCardVO) => void; onDeleteClick: (cardVO: PluginCardVO) => void;
onUpgradeClick: (cardVO: PluginCardVO) => void; onUpgradeClick: (cardVO: PluginCardVO) => void;
onViewReadme: (cardVO: PluginCardVO) => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [dropdownOpen, setDropdownOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false);
const [isHovered, setIsHovered] = useState(false);
return ( return (
<> <>
<div <div
className="w-[100%] h-[10rem] bg-white rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] p-[1.2rem] cursor-pointer dark:bg-[#1f1f22] relative transition-all duration-200 hover:shadow-[0px_3px_6px_0_rgba(0,0,0,0.12)] hover:scale-[1.005]" className="w-[100%] h-[10rem] bg-white rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] p-[1.2rem] cursor-pointer dark:bg-[#1f1f22] relative transition-all duration-200 hover:shadow-[0px_3px_6px_0_rgba(0,0,0,0.12)] hover:scale-[1.005]"
onMouseEnter={() => setIsHovered(true)} onClick={() => onCardClick()}
onMouseLeave={() => {
if (!dropdownOpen) {
setIsHovered(false);
}
}}
> >
<div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]"> <div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]">
{/* Icon - fixed width */} {/* Icon - fixed width */}
@@ -145,7 +129,10 @@ export default function PluginCardComponent({
</div> </div>
{/* Menu button - fixed width and position */} {/* Menu button - fixed width and position */}
<div className="flex flex-col items-center justify-between h-full relative z-20 flex-shrink-0"> <div
className="flex flex-col items-center justify-between h-full relative z-20 flex-shrink-0"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-center"></div> <div className="flex items-center justify-center"></div>
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
@@ -153,9 +140,6 @@ export default function PluginCardComponent({
open={dropdownOpen} open={dropdownOpen}
onOpenChange={(open) => { onOpenChange={(open) => {
setDropdownOpen(open); setDropdownOpen(open);
if (!open) {
setIsHovered(false);
}
}} }}
> >
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -233,45 +217,6 @@ export default function PluginCardComponent({
</div> </div>
</div> </div>
</div> </div>
{/* Hover overlay with action buttons */}
<div
className={`absolute inset-0 bg-gray-100/55 dark:bg-black/35 rounded-[10px] flex items-center justify-center gap-3 transition-all duration-200 z-10 ${
isHovered ? 'opacity-100' : 'opacity-0 pointer-events-none'
}`}
>
<Button
onClick={(e) => {
e.stopPropagation();
onViewReadme(cardVO);
}}
className={`bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg shadow-sm flex items-center gap-2 transition-all duration-200 ${
isHovered
? 'translate-y-0 opacity-100'
: 'translate-y-1 opacity-0'
}`}
style={{ transitionDelay: isHovered ? '10ms' : '0ms' }}
>
<FileText className="w-4 h-4" />
{t('plugins.readme')}
</Button>
<Button
onClick={(e) => {
e.stopPropagation();
onCardClick();
}}
variant="outline"
className={`bg-white hover:bg-gray-100 text-gray-900 dark:bg-white dark:hover:bg-gray-100 dark:text-gray-900 px-4 py-2 rounded-lg shadow-sm flex items-center gap-2 transition-all duration-200 ${
isHovered
? 'translate-y-0 opacity-100'
: 'translate-y-1 opacity-0'
}`}
style={{ transitionDelay: isHovered ? '20ms' : '0ms' }}
>
<Settings className="w-4 h-4" />
{t('plugins.config')}
</Button>
</div>
</div> </div>
</> </>
); );