refactor(plugin-tasks): consolidate plugin task components and improve state management

- Removed redundant components and consolidated logic for handling plugin tasks.
- Updated the rendering logic to directly manage plugin states within the main component.
- Enhanced the modal handling for clearing tasks and displaying statuses.
- Improved the overall structure and readability of the code by eliminating unnecessary files and simplifying the component hierarchy.
This commit is contained in:
CodingOnStar 2025-12-29 15:30:13 +08:00
parent 4733a70913
commit b58a482475
6 changed files with 637 additions and 2404 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,34 @@
import {
RiCheckboxCircleFill,
RiErrorWarningFill,
RiInstallLine,
RiLoaderLine,
} from '@remixicon/react'
import {
useCallback,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import {
ErrorPluginsSection,
RunningPluginsSection,
SuccessPluginsSection,
} from './plugin-task-list'
import PluginTaskTrigger from './plugin-task-trigger'
import { usePluginTaskPanel } from './use-plugin-task-panel'
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
import Tooltip from '@/app/components/base/tooltip'
import DownloadingIcon from '@/app/components/header/plugins-nav/downloading-icon'
import CardIcon from '@/app/components/plugins/card/base/card-icon'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { useGetLanguage } from '@/context/i18n'
import { cn } from '@/utils/classnames'
import { usePluginTaskStatus } from './hooks'
const PluginTasks = () => {
const {
open,
setOpen,
tip,
taskStatus,
handleClearAll,
handleClearErrors,
handleClearSingle,
} = usePluginTaskPanel()
const { t } = useTranslation()
const language = useGetLanguage()
const [open, setOpen] = useState(false)
const {
errorPlugins,
successPlugins,
@ -35,15 +42,68 @@ const PluginTasks = () => {
isInstallingWithError,
isSuccess,
isFailed,
} = taskStatus
handleClearErrorPlugin,
} = usePluginTaskStatus()
const { getIconUrl } = useGetIcon()
const handleClearAllWithModal = useCallback(async () => {
// Clear all completed plugins (success and error) but keep running ones
const completedPlugins = [...successPlugins, ...errorPlugins]
// Clear all completed plugins individually
for (const plugin of completedPlugins)
await handleClearErrorPlugin(plugin.taskId, plugin.plugin_unique_identifier)
// Only close modal if no plugins are still installing
if (runningPluginsLength === 0)
setOpen(false)
}, [successPlugins, errorPlugins, handleClearErrorPlugin, runningPluginsLength])
const handleClearErrorsWithModal = useCallback(async () => {
// Clear only error plugins, not all plugins
for (const plugin of errorPlugins)
await handleClearErrorPlugin(plugin.taskId, plugin.plugin_unique_identifier)
// Only close modal if no plugins are still installing
if (runningPluginsLength === 0)
setOpen(false)
}, [errorPlugins, handleClearErrorPlugin, runningPluginsLength])
const handleClearSingleWithModal = useCallback(async (taskId: string, pluginId: string) => {
await handleClearErrorPlugin(taskId, pluginId)
// Only close modal if no plugins are still installing
if (runningPluginsLength === 0)
setOpen(false)
}, [handleClearErrorPlugin, runningPluginsLength])
const tip = useMemo(() => {
if (isInstallingWithError)
return t('plugin.task.installingWithError', { installingLength: runningPluginsLength, successLength: successPluginsLength, errorLength: errorPluginsLength })
if (isInstallingWithSuccess)
return t('plugin.task.installingWithSuccess', { installingLength: runningPluginsLength, successLength: successPluginsLength })
if (isInstalling)
return t('plugin.task.installing')
if (isFailed)
return t('plugin.task.installedError', { errorLength: errorPluginsLength })
if (isSuccess)
return t('plugin.task.installSuccess', { successLength: successPluginsLength })
return t('plugin.task.installed')
}, [
errorPluginsLength,
isFailed,
isInstalling,
isInstallingWithError,
isInstallingWithSuccess,
isSuccess,
runningPluginsLength,
successPluginsLength,
t,
])
// Show icon if there are any plugin tasks (completed, running, or failed)
// Only hide when there are absolutely no plugin tasks
if (totalPluginsLength === 0)
return null
const canOpenPanel = isFailed || isInstalling || isInstallingWithSuccess || isInstallingWithError || isSuccess
return (
<div className="flex items-center">
<PortalToFollowElem
@ -57,33 +117,204 @@ const PluginTasks = () => {
>
<PortalToFollowElemTrigger
onClick={() => {
if (canOpenPanel)
setOpen(!open)
if (isFailed || isInstalling || isInstallingWithSuccess || isInstallingWithError || isSuccess)
setOpen(v => !v)
}}
>
<PluginTaskTrigger
tip={tip}
isInstalling={isInstalling}
isInstallingWithSuccess={isInstallingWithSuccess}
isInstallingWithError={isInstallingWithError}
isSuccess={isSuccess}
isFailed={isFailed}
successPluginsLength={successPluginsLength}
runningPluginsLength={runningPluginsLength}
errorPluginsLength={errorPluginsLength}
totalPluginsLength={totalPluginsLength}
<Tooltip
popupContent={tip}
asChild
offset={8}
>
<div
className={cn(
'relative flex h-8 w-8 items-center justify-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs hover:bg-components-button-secondary-bg-hover',
(isInstallingWithError || isFailed) && 'cursor-pointer border-components-button-destructive-secondary-border-hover bg-state-destructive-hover hover:bg-state-destructive-hover-alt',
(isInstalling || isInstallingWithSuccess || isSuccess) && 'cursor-pointer hover:bg-components-button-secondary-bg-hover',
)}
id="plugin-task-trigger"
>
{
(isInstalling || isInstallingWithError) && (
<DownloadingIcon />
)
}
{
!(isInstalling || isInstallingWithError) && (
<RiInstallLine
className={cn(
'h-4 w-4 text-components-button-secondary-text',
(isInstallingWithError || isFailed) && 'text-components-button-destructive-secondary-text',
)}
/>
)
}
<div className="absolute -right-1 -top-1">
{
(isInstalling || isInstallingWithSuccess) && (
<ProgressCircle
percentage={successPluginsLength / totalPluginsLength * 100}
circleFillColor="fill-components-progress-brand-bg"
/>
)
}
{
isInstallingWithError && (
<ProgressCircle
percentage={runningPluginsLength / totalPluginsLength * 100}
circleFillColor="fill-components-progress-brand-bg"
sectorFillColor="fill-components-progress-error-border"
circleStrokeColor="stroke-components-progress-error-border"
/>
)
}
{
(isSuccess || (successPluginsLength > 0 && runningPluginsLength === 0 && errorPluginsLength === 0)) && (
<RiCheckboxCircleFill className="h-3.5 w-3.5 text-text-success" />
)
}
{
isFailed && (
<RiErrorWarningFill className="h-3.5 w-3.5 text-text-destructive" />
)
}
</div>
</div>
</Tooltip>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[11]">
<div className="w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg">
<RunningPluginsSection plugins={runningPlugins} />
<SuccessPluginsSection plugins={successPlugins} onClearAll={handleClearAll} />
<ErrorPluginsSection
plugins={errorPlugins}
onClearAll={handleClearErrors}
onClearSingle={handleClearSingle}
{/* Running Plugins */}
{runningPlugins.length > 0 && (
<>
<div className="system-sm-semibold-uppercase sticky top-0 flex h-7 items-center justify-between px-2 pt-1 text-text-secondary">
{t('plugin.task.installing')}
{' '}
(
{runningPlugins.length}
)
</div>
<div className="max-h-[200px] overflow-y-auto">
{runningPlugins.map(runningPlugin => (
<div
key={runningPlugin.plugin_unique_identifier}
className="flex items-center rounded-lg p-2 hover:bg-state-base-hover"
>
<div className="relative mr-2 flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge">
<RiLoaderLine className="absolute -bottom-0.5 -right-0.5 z-10 h-3 w-3 animate-spin text-text-accent" />
<CardIcon
size="tiny"
src={getIconUrl(runningPlugin.icon)}
/>
</div>
<div className="grow">
<div className="system-md-regular truncate text-text-secondary">
{runningPlugin.labels[language]}
</div>
<div className="system-xs-regular text-text-tertiary">
{t('plugin.task.installing')}
</div>
</div>
</div>
))}
</div>
</>
)}
{/* Success Plugins */}
{successPlugins.length > 0 && (
<>
<div className="system-sm-semibold-uppercase sticky top-0 flex h-7 items-center justify-between px-2 pt-1 text-text-secondary">
{t('plugin.task.installed')}
{' '}
(
{successPlugins.length}
)
<Button
className="shrink-0"
size="small"
variant="ghost"
onClick={() => handleClearAllWithModal()}
>
{t('plugin.task.clearAll')}
</Button>
</div>
<div className="max-h-[200px] overflow-y-auto">
{successPlugins.map(successPlugin => (
<div
key={successPlugin.plugin_unique_identifier}
className="flex items-center rounded-lg p-2 hover:bg-state-base-hover"
>
<div className="relative mr-2 flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge">
<RiCheckboxCircleFill className="absolute -bottom-0.5 -right-0.5 z-10 h-3 w-3 text-text-success" />
<CardIcon
size="tiny"
src={getIconUrl(successPlugin.icon)}
/>
</div>
<div className="grow">
<div className="system-md-regular truncate text-text-secondary">
{successPlugin.labels[language]}
</div>
<div className="system-xs-regular text-text-success">
{successPlugin.message || t('plugin.task.installed')}
</div>
</div>
</div>
))}
</div>
</>
)}
{/* Error Plugins */}
{errorPlugins.length > 0 && (
<>
<div className="system-sm-semibold-uppercase sticky top-0 flex h-7 items-center justify-between px-2 pt-1 text-text-secondary">
{t('plugin.task.installError', { errorLength: errorPlugins.length })}
<Button
className="shrink-0"
size="small"
variant="ghost"
onClick={() => handleClearErrorsWithModal()}
>
{t('plugin.task.clearAll')}
</Button>
</div>
<div className="max-h-[200px] overflow-y-auto">
{errorPlugins.map(errorPlugin => (
<div
key={errorPlugin.plugin_unique_identifier}
className="flex items-center rounded-lg p-2 hover:bg-state-base-hover"
>
<div className="relative mr-2 flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge">
<RiErrorWarningFill className="absolute -bottom-0.5 -right-0.5 z-10 h-3 w-3 text-text-destructive" />
<CardIcon
size="tiny"
src={getIconUrl(errorPlugin.icon)}
/>
</div>
<div className="grow">
<div className="system-md-regular truncate text-text-secondary">
{errorPlugin.labels[language]}
</div>
<div className="system-xs-regular break-all text-text-destructive">
{errorPlugin.message}
</div>
</div>
<Button
className="shrink-0"
size="small"
variant="ghost"
onClick={() => handleClearSingleWithModal(errorPlugin.taskId, errorPlugin.plugin_unique_identifier)}
>
{t('common.operation.clear')}
</Button>
</div>
))}
</div>
</>
)}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>

View File

@ -1,202 +0,0 @@
import type { ReactNode } from 'react'
import type { PluginStatus } from '@/app/components/plugins/types'
import {
RiCheckboxCircleFill,
RiErrorWarningFill,
RiLoaderLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import CardIcon from '@/app/components/plugins/card/base/card-icon'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { useGetLanguage } from '@/context/i18n'
// Plugin item base component
type PluginTaskItemProps = {
plugin: PluginStatus
statusIcon: ReactNode
statusText: string
statusClassName?: string
action?: ReactNode
}
const PluginTaskItem = ({
plugin,
statusIcon,
statusText,
statusClassName = 'text-text-tertiary',
action,
}: PluginTaskItemProps) => {
const language = useGetLanguage()
const { getIconUrl } = useGetIcon()
return (
<div className="flex items-center rounded-lg p-2 hover:bg-state-base-hover">
<div className="relative mr-2 flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge">
{statusIcon}
<CardIcon
size="tiny"
src={getIconUrl(plugin.icon)}
/>
</div>
<div className="grow">
<div className="system-md-regular truncate text-text-secondary">
{plugin.labels[language]}
</div>
<div className={`system-xs-regular ${statusClassName}`}>
{statusText}
</div>
</div>
{action}
</div>
)
}
// Section header component
type SectionHeaderProps = {
title: string
count: number
action?: ReactNode
}
const SectionHeader = ({ title, count, action }: SectionHeaderProps) => (
<div className="system-sm-semibold-uppercase sticky top-0 flex h-7 items-center justify-between px-2 pt-1 text-text-secondary">
{title}
{' '}
(
{count}
)
{action}
</div>
)
// Running plugins section
type RunningPluginsSectionProps = {
plugins: PluginStatus[]
}
export const RunningPluginsSection = ({ plugins }: RunningPluginsSectionProps) => {
const { t } = useTranslation()
if (plugins.length === 0)
return null
return (
<>
<SectionHeader title={t('plugin.task.installing')} count={plugins.length} />
<div className="max-h-[200px] overflow-y-auto">
{plugins.map(plugin => (
<PluginTaskItem
key={plugin.plugin_unique_identifier}
plugin={plugin}
statusIcon={
<RiLoaderLine className="absolute -bottom-0.5 -right-0.5 z-10 h-3 w-3 animate-spin text-text-accent" />
}
statusText={t('plugin.task.installing')}
/>
))}
</div>
</>
)
}
// Success plugins section
type SuccessPluginsSectionProps = {
plugins: PluginStatus[]
onClearAll: () => void
}
export const SuccessPluginsSection = ({ plugins, onClearAll }: SuccessPluginsSectionProps) => {
const { t } = useTranslation()
if (plugins.length === 0)
return null
return (
<>
<SectionHeader
title={t('plugin.task.installed')}
count={plugins.length}
action={(
<Button
className="shrink-0"
size="small"
variant="ghost"
onClick={onClearAll}
>
{t('plugin.task.clearAll')}
</Button>
)}
/>
<div className="max-h-[200px] overflow-y-auto">
{plugins.map(plugin => (
<PluginTaskItem
key={plugin.plugin_unique_identifier}
plugin={plugin}
statusIcon={
<RiCheckboxCircleFill className="absolute -bottom-0.5 -right-0.5 z-10 h-3 w-3 text-text-success" />
}
statusText={plugin.message || t('plugin.task.installed')}
statusClassName="text-text-success"
/>
))}
</div>
</>
)
}
// Error plugins section
type ErrorPluginsSectionProps = {
plugins: PluginStatus[]
onClearAll: () => void
onClearSingle: (taskId: string, pluginId: string) => void
}
export const ErrorPluginsSection = ({ plugins, onClearAll, onClearSingle }: ErrorPluginsSectionProps) => {
const { t } = useTranslation()
if (plugins.length === 0)
return null
return (
<>
<SectionHeader
title={t('plugin.task.installError', { errorLength: plugins.length })}
count={plugins.length}
action={(
<Button
className="shrink-0"
size="small"
variant="ghost"
onClick={onClearAll}
>
{t('plugin.task.clearAll')}
</Button>
)}
/>
<div className="max-h-[200px] overflow-y-auto">
{plugins.map(plugin => (
<PluginTaskItem
key={plugin.plugin_unique_identifier}
plugin={plugin}
statusIcon={
<RiErrorWarningFill className="absolute -bottom-0.5 -right-0.5 z-10 h-3 w-3 text-text-destructive" />
}
statusText={plugin.message}
statusClassName="break-all text-text-destructive"
action={(
<Button
className="shrink-0"
size="small"
variant="ghost"
onClick={() => onClearSingle(plugin.taskId, plugin.plugin_unique_identifier)}
>
{t('common.operation.clear')}
</Button>
)}
/>
))}
</div>
</>
)
}

View File

@ -1,94 +0,0 @@
import {
RiCheckboxCircleFill,
RiErrorWarningFill,
RiInstallLine,
} from '@remixicon/react'
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
import Tooltip from '@/app/components/base/tooltip'
import DownloadingIcon from '@/app/components/header/plugins-nav/downloading-icon'
import { cn } from '@/utils/classnames'
type PluginTaskTriggerProps = {
tip: string
isInstalling: boolean
isInstallingWithSuccess: boolean
isInstallingWithError: boolean
isSuccess: boolean
isFailed: boolean
successPluginsLength: number
runningPluginsLength: number
errorPluginsLength: number
totalPluginsLength: number
}
const PluginTaskTrigger = ({
tip,
isInstalling,
isInstallingWithSuccess,
isInstallingWithError,
isSuccess,
isFailed,
successPluginsLength,
runningPluginsLength,
errorPluginsLength,
totalPluginsLength,
}: PluginTaskTriggerProps) => {
const showDownloadingIcon = isInstalling || isInstallingWithError
const hasError = isInstallingWithError || isFailed
const showSuccessIndicator = isSuccess || (successPluginsLength > 0 && runningPluginsLength === 0 && errorPluginsLength === 0)
return (
<Tooltip
popupContent={tip}
asChild
offset={8}
>
<div
className={cn(
'relative flex h-8 w-8 items-center justify-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs hover:bg-components-button-secondary-bg-hover',
hasError && 'cursor-pointer border-components-button-destructive-secondary-border-hover bg-state-destructive-hover hover:bg-state-destructive-hover-alt',
(isInstalling || isInstallingWithSuccess || isSuccess) && 'cursor-pointer hover:bg-components-button-secondary-bg-hover',
)}
id="plugin-task-trigger"
>
{/* Main Icon */}
{showDownloadingIcon
? <DownloadingIcon />
: (
<RiInstallLine
className={cn(
'h-4 w-4 text-components-button-secondary-text',
hasError && 'text-components-button-destructive-secondary-text',
)}
/>
)}
{/* Status Indicator */}
<div className="absolute -right-1 -top-1">
{(isInstalling || isInstallingWithSuccess) && (
<ProgressCircle
percentage={successPluginsLength / totalPluginsLength * 100}
circleFillColor="fill-components-progress-brand-bg"
/>
)}
{isInstallingWithError && (
<ProgressCircle
percentage={runningPluginsLength / totalPluginsLength * 100}
circleFillColor="fill-components-progress-brand-bg"
sectorFillColor="fill-components-progress-error-border"
circleStrokeColor="stroke-components-progress-error-border"
/>
)}
{showSuccessIndicator && (
<RiCheckboxCircleFill className="h-3.5 w-3.5 text-text-success" />
)}
{isFailed && (
<RiErrorWarningFill className="h-3.5 w-3.5 text-text-destructive" />
)}
</div>
</div>
</Tooltip>
)
}
export default PluginTaskTrigger

View File

@ -1,94 +0,0 @@
import type { PluginStatus } from '@/app/components/plugins/types'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { usePluginTaskStatus } from './hooks'
type PluginTaskPanelState = {
open: boolean
setOpen: (open: boolean) => void
tip: string
taskStatus: ReturnType<typeof usePluginTaskStatus>
handleClearAll: () => Promise<void>
handleClearErrors: () => Promise<void>
handleClearSingle: (taskId: string, pluginId: string) => Promise<void>
}
export const usePluginTaskPanel = (): PluginTaskPanelState => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const taskStatus = usePluginTaskStatus()
const {
errorPlugins,
successPlugins,
runningPluginsLength,
successPluginsLength,
errorPluginsLength,
isInstalling,
isInstallingWithSuccess,
isInstallingWithError,
isSuccess,
isFailed,
handleClearErrorPlugin,
} = taskStatus
const closeIfNoRunning = useCallback(() => {
if (runningPluginsLength === 0)
setOpen(false)
}, [runningPluginsLength])
const clearPlugins = useCallback(async (plugins: PluginStatus[]) => {
for (const plugin of plugins)
await handleClearErrorPlugin(plugin.taskId, plugin.plugin_unique_identifier)
}, [handleClearErrorPlugin])
const handleClearAll = useCallback(async () => {
// Clear all completed plugins (success and error) but keep running ones
await clearPlugins([...successPlugins, ...errorPlugins])
closeIfNoRunning()
}, [successPlugins, errorPlugins, clearPlugins, closeIfNoRunning])
const handleClearErrors = useCallback(async () => {
await clearPlugins(errorPlugins)
closeIfNoRunning()
}, [errorPlugins, clearPlugins, closeIfNoRunning])
const handleClearSingle = useCallback(async (taskId: string, pluginId: string) => {
await handleClearErrorPlugin(taskId, pluginId)
closeIfNoRunning()
}, [handleClearErrorPlugin, closeIfNoRunning])
const tip = useMemo(() => {
if (isInstallingWithError)
return t('plugin.task.installingWithError', { installingLength: runningPluginsLength, successLength: successPluginsLength, errorLength: errorPluginsLength })
if (isInstallingWithSuccess)
return t('plugin.task.installingWithSuccess', { installingLength: runningPluginsLength, successLength: successPluginsLength })
if (isInstalling)
return t('plugin.task.installing')
if (isFailed)
return t('plugin.task.installedError', { errorLength: errorPluginsLength })
if (isSuccess)
return t('plugin.task.installSuccess', { successLength: successPluginsLength })
return t('plugin.task.installed')
}, [
errorPluginsLength,
isFailed,
isInstalling,
isInstallingWithError,
isInstallingWithSuccess,
isSuccess,
runningPluginsLength,
successPluginsLength,
t,
])
return {
open,
setOpen,
tip,
taskStatus,
handleClearAll,
handleClearErrors,
handleClearSingle,
}
}