feat: move plugin workspace into project settings

This commit is contained in:
inman
2026-09-07 10:06:37 +08:00
parent f8eee430f4
commit be1764e016
19 changed files with 363 additions and 76 deletions

View File

@@ -1,11 +1,12 @@
import { useEffect, useMemo, useRef, type FormEvent } from 'react';
import { AlertCircle, Puzzle, RefreshCw, Search } from 'lucide-react';
import { AlertCircle, ArrowLeft, Puzzle, RefreshCw, Search } from 'lucide-react';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select } from '@/components/ui/select';
import { cn } from '@/lib/utils';
import type { DataServiceInstanceState } from '../../../shared/data-service';
import { useAuthStore } from '@/stores/auth';
import { codingPluginsStore, useCodingPluginsStore } from '@/stores/coding-plugins';
@@ -23,6 +24,7 @@ import type {
} from './plugin-workspace-model';
import { buildPluginWorkspaceProjection, isBundledOfficialPlugin } from './plugin-workspace-model';
import { resolvePluginWorkspaceSearch, serializePluginWorkspaceSearch } from './plugin-workspace-query';
import { PROJECT_SETTINGS_PLUGINS_PATH } from './legacy-plugin-redirect';
const DELIVERY_TEXT = {
system_included: '随 MakeLore 提供',
@@ -151,6 +153,8 @@ export interface PluginsViewProps {
onResetDataService(): void | Promise<void>;
onRemoveDataServiceCollection(collection: string): void | Promise<void>;
onRemoveDataServiceProject(): void | Promise<void>;
embedded?: boolean;
onBackToProjectSettings?(): void;
}
export function PluginsView(props: PluginsViewProps) {
@@ -176,21 +180,46 @@ export function PluginsView(props: PluginsViewProps) {
};
return (
<main data-testid="plugins-page" className="mx-auto w-full max-w-7xl text-foreground">
<header className="flex flex-wrap items-start justify-between gap-4">
<div>
<p className="text-sm font-medium text-brand">MakeLore Code</p>
<h1 className="mt-1 text-balance text-3xl font-semibold tracking-[-0.03em]">插件</h1>
<p className="mt-2 max-w-3xl text-pretty text-sm leading-6 text-muted-foreground">
MakeLore 运营发布官方插件;本机 Skill 和 Pi extension 仅通过对话安装。获取、项目启用与伙伴分配彼此独立,本页集中展示账号、设备、当前项目与伙伴状态。
</p>
<main
data-testid="plugins-page"
className={cn('w-full text-foreground', !props.embedded && 'mx-auto max-w-7xl')}
>
{props.embedded ? (
<div className="flex justify-end">
<Button type="button" variant="outline" className="min-h-10" onClick={() => void props.onRefresh()}>
<RefreshCw className="mr-2 h-4 w-4" />刷新全部来源
</Button>
</div>
) : <header className="flex flex-wrap items-start justify-between gap-4">
<div className="flex min-w-0 items-start gap-3">
{props.onBackToProjectSettings ? (
<Button
type="button"
variant="ghost"
size="icon"
data-testid="project-plugins-back-button"
aria-label="返回项目配置"
title="返回项目配置"
onClick={props.onBackToProjectSettings}
className="mt-0.5 h-8 w-8 shrink-0 text-muted-foreground hover:bg-surface-subtle hover:text-foreground"
>
<ArrowLeft className="h-4 w-4" />
</Button>
) : null}
<div className="min-w-0">
<p className="text-sm font-medium text-brand">项目配置</p>
<h1 className="mt-1 text-balance text-3xl font-semibold tracking-[-0.03em]">插件</h1>
<p className="mt-2 max-w-3xl text-pretty text-sm leading-6 text-muted-foreground">
MakeLore 运营发布官方插件;本机 Skill 和 Pi extension 仅通过对话安装。获取、项目启用与伙伴分配彼此独立,这里集中展示账号、设备、当前项目与伙伴状态。
</p>
</div>
</div>
<Button type="button" variant="outline" className="min-h-10" onClick={() => void props.onRefresh()}>
<RefreshCw className="mr-2 h-4 w-4" />刷新全部来源
</Button>
</header>
</header>}
<section aria-label="插件筛选" className="mt-6 rounded-2xl border border-border/70 bg-surface-subtle p-4">
<section aria-label="插件筛选" className={cn(props.embedded ? 'mt-4' : 'mt-6', 'rounded-2xl border border-border/70 bg-surface-subtle p-4')}>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-[0.8fr_0.8fr_1fr_2fr_auto]">
<label className="text-sm font-medium">
范围
@@ -351,7 +380,7 @@ function scopeFallbackSearchFromLocationState(state: unknown): string | null {
return typeof value === 'string' ? value : null;
}
export function Plugins() {
export function Plugins({ embedded = false }: { embedded?: boolean } = {}) {
const navigate = useNavigate();
const location = useLocation();
const [searchParams, setSearchParams] = useSearchParams();
@@ -613,7 +642,7 @@ export function Plugins() {
marketplace: pluginMarketplaceStore.getState(),
device: devicePackageStore.getState(),
project: codingPluginsStore.getState(),
loginReturnPath: `/plugins?${serializePluginWorkspaceSearch(filters).toString()}`,
loginReturnPath: `${PROJECT_SETTINGS_PLUGINS_PATH}?${serializePluginWorkspaceSearch(filters).toString()}`,
navigate: (path, options) => navigate(path, options),
openSettings: () => document.getElementById('plugin-settings')?.scrollIntoView({ behavior: 'smooth', block: 'start' }),
}));
@@ -641,6 +670,8 @@ export function Plugins() {
onResetDataService={() => safe(codingPluginsStore.getState().reset())}
onRemoveDataServiceCollection={(collection) => safe(codingPluginsStore.getState().removeCollection(collection))}
onRemoveDataServiceProject={() => safe(codingPluginsStore.getState().removeProject())}
embedded={embedded}
onBackToProjectSettings={embedded ? undefined : () => navigate('/project-config')}
/>
);
}

View File

@@ -1,13 +1,26 @@
import { Navigate } from 'react-router-dom';
import { Navigate, useLocation } from 'react-router-dom';
export const PROJECT_SETTINGS_PLUGINS_PATH = '/project-config/plugins';
export type LegacyPluginPath = '/plugin-marketplace' | '/my-plugins' | '/project-plugins';
const LEGACY_PLUGIN_REDIRECTS: Record<LegacyPluginPath, string> = {
'/plugin-marketplace': '/plugins?scope=all&source=official',
'/my-plugins': '/plugins?scope=all&state=mine',
'/project-plugins': '/plugins?scope=project',
'/plugin-marketplace': `${PROJECT_SETTINGS_PLUGINS_PATH}?scope=all&source=official`,
'/my-plugins': `${PROJECT_SETTINGS_PLUGINS_PATH}?scope=all&state=mine`,
'/project-plugins': `${PROJECT_SETTINGS_PLUGINS_PATH}?scope=project`,
};
export function LegacyPluginRedirect({ from }: { from: LegacyPluginPath }) {
return <Navigate to={LEGACY_PLUGIN_REDIRECTS[from]} replace />;
}
export function PluginWorkspaceRedirect() {
const location = useLocation();
return (
<Navigate
to={{ pathname: PROJECT_SETTINGS_PLUGINS_PATH, search: location.search }}
state={location.state}
replace
/>
);
}