merge: integrate project settings plugin workspace
# Conflicts: # src/components/layout/MainLayout.tsx # src/pages/ProjectConfiguration/index.tsx
This commit is contained in:
@@ -86,10 +86,13 @@ test.describe('Unified plugin workspace', () => {
|
||||
))).toBe(true);
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await page.keyboard.press('Escape');
|
||||
await page.getByTestId('sidebar-nav-plugins').click();
|
||||
await expect(page.getByTestId('sidebar-nav-plugins')).toHaveCount(0);
|
||||
await page.getByTestId('resource-card-plugins').click();
|
||||
|
||||
await expect(page.getByTestId('project-configuration-empty-state')).toBeVisible();
|
||||
await expect(page.getByTestId('project-plugins-sheet')).toBeVisible();
|
||||
await expect(page.getByTestId('plugins-page')).toBeVisible();
|
||||
await expect(page).toHaveURL(/\/plugins\?scope=all/);
|
||||
await expect(page).toHaveURL(/\/project-config\/plugins\?scope=all/);
|
||||
await expect(page.getByTestId('plugins-page').getByText('尚未获取')).toBeVisible();
|
||||
await page.getByRole('button', { name: '查看灵感笔记详情' }).click();
|
||||
await expect(page.getByRole('dialog', { name: '灵感笔记' })).toContainText('详细介绍');
|
||||
@@ -148,8 +151,11 @@ test.describe('Unified plugin workspace', () => {
|
||||
});
|
||||
});
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await page.getByTestId('sidebar-nav-plugins').click();
|
||||
await expect(page.getByTestId('sidebar-nav-plugins')).toHaveCount(0);
|
||||
await page.getByTestId('resource-card-plugins').click();
|
||||
|
||||
await expect(page.getByTestId('project-configuration-empty-state')).toBeVisible();
|
||||
await expect(page.getByTestId('project-plugins-sheet')).toBeVisible();
|
||||
await expect(page.getByTestId('plugins-page')).toBeVisible();
|
||||
await expect(page.getByRole('alert')).toContainText('官方目录刷新失败');
|
||||
await expect(page.getByRole('heading', { name: 'Local Search' })).toBeVisible();
|
||||
@@ -274,7 +280,10 @@ test.describe('Unified plugin workspace', () => {
|
||||
))).toBe(true);
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await page.keyboard.press('Escape');
|
||||
await page.getByTestId('sidebar-nav-plugins').click();
|
||||
await expect(page.getByTestId('sidebar-nav-plugins')).toHaveCount(0);
|
||||
await page.getByTestId('resource-card-plugins').click();
|
||||
await expect(page.getByTestId('project-configuration-empty-state')).toBeVisible();
|
||||
await expect(page.getByTestId('project-plugins-sheet')).toBeVisible();
|
||||
await expect(page.getByTestId('plugins-page')).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-nav-plugin-marketplace')).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-nav-my-plugins')).toHaveCount(0);
|
||||
|
||||
@@ -89,9 +89,13 @@ test.describe('Unified project plugin workspace', () => {
|
||||
});
|
||||
});
|
||||
|
||||
await page.getByTestId('sidebar-nav-plugins').click();
|
||||
await expect(page.getByTestId('sidebar-nav-plugins')).toHaveCount(0);
|
||||
await page.getByTestId('resource-card-plugins').click();
|
||||
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
|
||||
await expect(page.getByTestId('project-plugins-sheet')).toBeVisible();
|
||||
await expect(page.getByTestId('plugins-page')).toBeVisible();
|
||||
await expect(page).toHaveURL(/\/plugins\?scope=project/);
|
||||
await expect(page).toHaveURL(/\/project-config\/plugins\?scope=project/);
|
||||
await expect(page.getByText('项目配置')).toBeVisible();
|
||||
await page.getByRole('button', { name: '查看开发数据服务详情' }).click();
|
||||
await expect(page.getByText('按平台包含,不按单次插件调用扣点。')).toBeVisible();
|
||||
const localProjectId = await app.evaluate(() => (
|
||||
|
||||
@@ -81,7 +81,7 @@ function renderLogin(initialEntries: React.ComponentProps<typeof MemoryRouter>['
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/module-select" element={<div>Module Selection</div>} />
|
||||
<Route path="/plugins" element={<LocationProbe />} />
|
||||
<Route path="/project-config/plugins" element={<LocationProbe />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
@@ -91,7 +91,7 @@ function renderPluginLoginJourney(canonicalPath: string) {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={[canonicalPath]}>
|
||||
<Routes>
|
||||
<Route path="/plugins" element={<PluginSignInAction />} />
|
||||
<Route path="/project-config/plugins" element={<PluginSignInAction />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/module-select" element={<div>Module Selection</div>} />
|
||||
</Routes>
|
||||
@@ -245,7 +245,7 @@ describe('Login page', () => {
|
||||
});
|
||||
|
||||
it('returns a signed-out plugin action to the exact canonical workspace query after login', async () => {
|
||||
const canonicalPath = '/plugins?scope=all&source=official&state=available&q=notes&plugin=official%3Amakelore.notes';
|
||||
const canonicalPath = '/project-config/plugins?scope=all&source=official&state=available&q=notes&plugin=official%3Amakelore.notes';
|
||||
loginWithPassword.mockImplementation(async () => {
|
||||
useAuthStore.setState({ accessToken: 'password-access-token' });
|
||||
});
|
||||
|
||||
@@ -86,8 +86,8 @@ describe('MainLayout module isolation', () => {
|
||||
expect(screen.getByTestId('titlebar-stub')).toHaveAttribute('data-overlay', 'false');
|
||||
});
|
||||
|
||||
it('keeps the canonical plugin workspace outside the local project initialization gate', () => {
|
||||
renderLayout('/plugins');
|
||||
it('keeps the project-settings plugin workspace outside the local project initialization gate', () => {
|
||||
renderLayout('/project-config/plugins');
|
||||
|
||||
expect(screen.getByTestId('route-content')).toBeVisible();
|
||||
expect(screen.queryByTestId('project-initialization-gate')).not.toBeInTheDocument();
|
||||
|
||||
@@ -36,7 +36,7 @@ const MARKETPLACE_ARTIFACT_TEXT = [
|
||||
'plugin-marketplace\\/install\\/', 'plugin-marketplace\\/update\\/',
|
||||
'effectiveSkillIds', 'pluginReleaseIds',
|
||||
'/api/coding/plugin-marketplace/catalog', '/api/coding/plugin-marketplace/library',
|
||||
'免费获取', '获取、项目启用与伙伴分配彼此独立', '/plugins',
|
||||
'免费获取', '获取、项目启用与伙伴分配彼此独立', '/project-config/plugins',
|
||||
].join('\n');
|
||||
|
||||
async function createAsarFixture(source: string, archive: string) {
|
||||
@@ -389,7 +389,7 @@ describe('final Pi product artifact verification', () => {
|
||||
'makelore-device-package.v1 /api/coding/device-packages device-parent-workers',
|
||||
'/api/coding/plugin-marketplace plugin-marketplace\\/install\\/ plugin-marketplace\\/update\\/',
|
||||
'effectiveSkillIds pluginReleaseIds',
|
||||
'/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 免费获取 获取、项目启用与伙伴分配彼此独立 /plugins',
|
||||
'/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 免费获取 获取、项目启用与伙伴分配彼此独立 /project-config/plugins',
|
||||
].join('\n'));
|
||||
const appAsar = path.join(root, 'app.asar');
|
||||
await createAsarFixture(source, appAsar);
|
||||
@@ -407,7 +407,7 @@ describe('final Pi product artifact verification', () => {
|
||||
'makelore.game-resource /api/plugins/v1/hosted/game-resource/generations',
|
||||
'/api/coding/plugin-marketplace plugin-marketplace\\/install\\/ plugin-marketplace\\/update\\/',
|
||||
'effectiveSkillIds pluginReleaseIds',
|
||||
'/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 免费获取 我的插件 /plugins',
|
||||
'/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 免费获取 我的插件 /project-config/plugins',
|
||||
].join('\n'));
|
||||
const staleAsar = path.join(root, 'stale.asar');
|
||||
await createAsarFixture(staleSource, staleAsar);
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { LegacyPluginRedirect } from '@/pages/Plugins/legacy-plugin-redirect';
|
||||
import {
|
||||
LegacyPluginRedirect,
|
||||
PluginWorkspaceRedirect,
|
||||
PROJECT_SETTINGS_PLUGINS_PATH,
|
||||
} from '@/pages/Plugins/legacy-plugin-redirect';
|
||||
import { getGuardedAiModuleForPath, isProgrammingProviderRoute } from '@/lib/ai-modules';
|
||||
|
||||
function LocationProbe() {
|
||||
@@ -17,15 +21,15 @@ function LocationProbe() {
|
||||
|
||||
describe('plugin navigation cutover', () => {
|
||||
it.each([
|
||||
['/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`],
|
||||
] as const)('replace-redirects %s to %s', async (legacy, canonical) => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/previous', legacy]} initialIndex={1}>
|
||||
<Routes>
|
||||
<Route path={legacy} element={<LegacyPluginRedirect from={legacy} />} />
|
||||
<Route path="/plugins" element={<LocationProbe />} />
|
||||
<Route path={PROJECT_SETTINGS_PLUGINS_PATH} element={<LocationProbe />} />
|
||||
<Route path="/previous" element={<output data-testid="previous">previous</output>} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
@@ -36,8 +40,26 @@ describe('plugin navigation cutover', () => {
|
||||
expect(await screen.findByTestId('previous')).toBeVisible();
|
||||
});
|
||||
|
||||
it('classifies the canonical page as Code without making it provider-gated', () => {
|
||||
expect(getGuardedAiModuleForPath('/plugins')).toBe('programming');
|
||||
expect(isProgrammingProviderRoute('/plugins')).toBe(false);
|
||||
it('moves the former canonical URL into project settings without losing its query', async () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/previous', '/plugins?scope=all&state=mine']} initialIndex={1}>
|
||||
<Routes>
|
||||
<Route path="/plugins" element={<PluginWorkspaceRedirect />} />
|
||||
<Route path={PROJECT_SETTINGS_PLUGINS_PATH} element={<LocationProbe />} />
|
||||
<Route path="/previous" element={<output data-testid="previous">previous</output>} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId('location')).toHaveTextContent(
|
||||
`${PROJECT_SETTINGS_PLUGINS_PATH}?scope=all&state=mine`,
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '返回' }));
|
||||
expect(await screen.findByTestId('previous')).toBeVisible();
|
||||
});
|
||||
|
||||
it('classifies the project-settings plugin page as Code without making it provider-gated', () => {
|
||||
expect(getGuardedAiModuleForPath(PROJECT_SETTINGS_PLUGINS_PATH)).toBe('programming');
|
||||
expect(isProgrammingProviderRoute(PROJECT_SETTINGS_PLUGINS_PATH)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ function dependencies() {
|
||||
project: {
|
||||
setEnabled: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
loginReturnPath: '/plugins?scope=all&source=official&state=available',
|
||||
loginReturnPath: '/project-config/plugins?scope=all&source=official&state=available',
|
||||
navigate: vi.fn(),
|
||||
openSettings: vi.fn(),
|
||||
} satisfies PluginWorkspaceDispatchDependencies;
|
||||
@@ -65,7 +65,7 @@ describe('plugin workspace controller', () => {
|
||||
expect(deps.device.setEnabled.mock.calls).toEqual([['j', true], ['k', false]]);
|
||||
expect(deps.device.uninstall).toHaveBeenCalledExactlyOnceWith('l');
|
||||
expect(deps.navigate.mock.calls).toEqual([
|
||||
['/login', { state: { from: '/plugins?scope=all&source=official&state=available' } }],
|
||||
['/login', { state: { from: '/project-config/plugins?scope=all&source=official&state=available' } }],
|
||||
['/project-config'],
|
||||
]);
|
||||
expect(deps.openSettings).toHaveBeenCalledExactlyOnceWith();
|
||||
|
||||
@@ -208,10 +208,10 @@ function PluginsRouteHarness() {
|
||||
<>
|
||||
<Plugins />
|
||||
<div data-testid="plugins-location">{location.pathname}{location.search}</div>
|
||||
<button type="button" onClick={() => navigate('/plugins')}>打开裸插件地址</button>
|
||||
<button type="button" onClick={() => navigate('/project-config/plugins')}>打开裸插件地址</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/plugins?scope=illegal&source=remote&state=broken')}
|
||||
onClick={() => navigate('/project-config/plugins?scope=illegal&source=remote&state=broken')}
|
||||
>打开非法插件地址</button>
|
||||
</>
|
||||
);
|
||||
@@ -249,16 +249,24 @@ function preparePluginsRouteState() {
|
||||
}
|
||||
|
||||
describe('PluginsView', () => {
|
||||
it('renders as project-configuration drawer content without a duplicate standalone header', () => {
|
||||
render(<MemoryRouter><PluginsView {...props()} embedded /></MemoryRouter>);
|
||||
|
||||
expect(screen.getByTestId('plugins-page')).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '刷新全部来源' })).toBeVisible();
|
||||
expect(screen.queryByRole('heading', { name: '插件', level: 1 })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps the production URL, scope control, and fallback notice aligned across in-place changes', async () => {
|
||||
preparePluginsRouteState();
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/plugins?scope=project']}>
|
||||
<MemoryRouter initialEntries={['/project-config/plugins?scope=project']}>
|
||||
<PluginsRouteHarness />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/plugins?scope=all&source=all&state=all',
|
||||
'/project-config/plugins?scope=all&source=all&state=all',
|
||||
));
|
||||
expect(screen.getByLabelText('范围')).toHaveValue('all');
|
||||
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
|
||||
@@ -273,19 +281,19 @@ describe('PluginsView', () => {
|
||||
act(() => codingWorkspaceStore.setState({ activeProjectId: activeProject.id, activeProject }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开裸插件地址' }));
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/plugins?scope=project&source=all&state=all',
|
||||
'/project-config/plugins?scope=project&source=all&state=all',
|
||||
));
|
||||
expect(screen.getByLabelText('范围')).toHaveValue('project');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开非法插件地址' }));
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/plugins?scope=project&source=all&state=all',
|
||||
'/project-config/plugins?scope=project&source=all&state=all',
|
||||
));
|
||||
expect(screen.getByLabelText('范围')).toHaveValue('project');
|
||||
|
||||
act(() => codingWorkspaceStore.setState({ activeProjectId: null, activeProject: null }));
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/plugins?scope=all&source=all&state=all',
|
||||
'/project-config/plugins?scope=all&source=all&state=all',
|
||||
));
|
||||
expect(screen.getByLabelText('范围')).toHaveValue('all');
|
||||
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
|
||||
@@ -339,7 +347,7 @@ describe('PluginsView', () => {
|
||||
vi.spyOn(devicePackageStore.getState(), 'load').mockResolvedValue(undefined);
|
||||
vi.spyOn(codingPluginsStore.getState(), 'load').mockResolvedValue(undefined);
|
||||
|
||||
render(<MemoryRouter initialEntries={['/plugins?scope=project&source=all&state=all']}><Plugins /></MemoryRouter>);
|
||||
render(<MemoryRouter initialEntries={['/project-config/plugins?scope=project&source=all&state=all']}><Plugins /></MemoryRouter>);
|
||||
|
||||
for (const name of ['开发数据服务', 'Pi Web Search', 'makelore.retained']) {
|
||||
const heading = await screen.findByRole('heading', { name });
|
||||
@@ -388,7 +396,7 @@ describe('PluginsView', () => {
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={[
|
||||
'/plugins?scope=all&source=all&state=all&plugin=official%3Amakelore.notes',
|
||||
'/project-config/plugins?scope=all&source=all&state=all&plugin=official%3Amakelore.notes',
|
||||
]}>
|
||||
<Plugins />
|
||||
</MemoryRouter>,
|
||||
|
||||
Reference in New Issue
Block a user