feat: move plugin workspace into project settings
This commit is contained in:
@@ -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')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user