feat(web): improve marketplace install workflows

This commit is contained in:
Hyu
2026-09-01 16:43:50 +08:00
parent 982d660236
commit a87c814dae
23 changed files with 1657 additions and 353 deletions
@@ -0,0 +1,49 @@
import { Download } from 'lucide-react';
import { Progress } from '@/components/ui/progress';
export default function MarketplaceInstallButton({
installing,
progress,
disabled,
label,
onInstall,
}: {
installing: boolean;
progress: number;
disabled: boolean;
label: string;
onInstall: () => void;
}) {
const normalizedProgress = Math.max(0, Math.min(100, Math.round(progress)));
return (
<button
type="button"
aria-label={label}
title={label}
disabled={disabled}
className="row-span-2 flex h-8 w-16 shrink-0 items-center justify-center justify-self-end rounded-md text-muted-foreground transition-colors hover:bg-background hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-45"
onPointerDown={(event) => {
event.preventDefault();
event.stopPropagation();
}}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onInstall();
}}
>
{installing ? (
<span className="flex w-14 flex-col gap-1" aria-live="polite">
<span className="text-center text-[10px] font-medium leading-none tabular-nums text-primary">
{normalizedProgress}%
</span>
<Progress value={normalizedProgress} className="h-1 bg-primary/15" />
</span>
) : (
<Download className="size-4" />
)}
</button>
);
}
@@ -113,6 +113,10 @@ import {
} from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { useSidebarData, SidebarEntityItem } from './SidebarDataContext';
import {
pluginTaskKey,
usePluginInstallTasks,
} from '@/app/home/plugins/components/plugin-install-task';
import { FeedbackPopoverContent } from './FeedbackPopover';
import {
type WorkspaceQuotaItem,
@@ -404,6 +408,7 @@ function NavItems({
const pathname = location.pathname;
const [searchParams] = useSearchParams();
const sidebarData = useSidebarData();
const refreshSidebarPlugins = sidebarData.refreshPlugins;
const quotaStatus = useWorkspaceQuotaStatus();
const { state: sidebarState, isMobile } = useSidebar();
const { t } = useTranslation();
@@ -452,19 +457,36 @@ function NavItems({
const [targetPluginItem, setTargetPluginItem] =
useState<SidebarEntityItem | null>(null);
const [deleteData, setDeleteData] = useState(false);
const {
addTask: addPluginTask,
setSelectedTaskId: setSelectedPluginTaskId,
registerOnTaskComplete,
unregisterOnTaskComplete,
} = usePluginInstallTasks();
const asyncTask = useAsyncTask({
onSuccess: () => {
const msg =
pluginOpType === PluginOperationType.DELETE
? t('plugins.deleteSuccess')
: t('plugins.updateSuccess');
toast.success(msg);
toast.success(t('plugins.deleteSuccess'));
setShowPluginOpModal(false);
sidebarData.refreshPlugins();
},
});
useEffect(() => {
const onPluginTaskComplete = (
_taskId: number,
success: boolean,
_error?: string,
operation?: 'install' | 'upgrade',
) => {
if (success && operation === 'upgrade') {
refreshSidebarPlugins();
}
};
registerOnTaskComplete(onPluginTaskComplete);
return () => unregisterOnTaskComplete(onPluginTaskComplete);
}, [refreshSidebarPlugins, registerOnTaskComplete, unregisterOnTaskComplete]);
function handlePluginDelete(item: SidebarEntityItem) {
setTargetPluginItem(item);
setPluginOpType(PluginOperationType.DELETE);
@@ -490,21 +512,37 @@ function NavItems({
? targetPluginItem.id.substring(slashIdx + 1)
: targetPluginItem.id;
const apiCall =
pluginOpType === PluginOperationType.DELETE
? httpClient.removePlugin(author, name, deleteData)
: httpClient.upgradePlugin(author, name);
if (pluginOpType === PluginOperationType.UPDATE) {
httpClient
.upgradePlugin(author, name)
.then((res) => {
addPluginTask({
taskId: res.task_id,
pluginName: `${author}/${name}`,
source: 'marketplace',
extensionType: 'plugin',
operation: 'upgrade',
});
setSelectedPluginTaskId(
pluginTaskKey(res.task_id, 'marketplace', 'upgrade'),
);
setShowPluginOpModal(false);
setTargetPluginItem(null);
asyncTask.reset();
})
.catch((error) => {
toast.error(t('plugins.updateError') + error.message);
});
return;
}
apiCall
httpClient
.removePlugin(author, name, deleteData)
.then((res) => {
asyncTask.startTask(res.task_id);
})
.catch((error) => {
const errorMessage =
pluginOpType === PluginOperationType.DELETE
? t('plugins.deleteError') + error.message
: t('plugins.updateError') + error.message;
toast.error(errorMessage);
toast.error(t('plugins.deleteError') + error.message);
});
}