AGC 生成任务侧栏:失去焦点自动收起、入口挪到排列方式之前
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 2m4s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 1m53s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 2m2s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 1m32s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 1m58s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m56s
Project CI / Repository checks (pull_request) Successful in 3m45s
Project CI / Frontend tests (pull_request) Successful in 4m12s
Project CI / Native shell tests (pull_request) Failing after 4m48s
Project CI / Backend tests (pull_request) Successful in 7m7s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m17s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 2m4s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 1m53s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 2m2s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 1m32s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 1m58s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m56s
Project CI / Repository checks (pull_request) Successful in 3m45s
Project CI / Frontend tests (pull_request) Successful in 4m12s
Project CI / Native shell tests (pull_request) Failing after 4m48s
Project CI / Backend tests (pull_request) Successful in 7m7s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m17s
- 侧栏展开时挂 document 级 pointerdown 捕获监听:点在侧栏内部与工具条那枚开合按钮以外的地方(画布、其他工具条按钮、对话框)即自动收起侧栏 - 排除开合按钮本身:它自己负责 toggle,若在「点外部」里也命中会先收起再被 toggle 打开,表现为按钮失灵 - 工具条「生成任务」入口加 data-resource-generation-task-toggle 标记,供上面的判定排除 - 「生成任务」入口从资源排列方式之后挪到之前:动作在前、排列方式(依赖 / 类型)收在行尾;入口抽成可复用元素,资源页签与运行页签各渲染一次,同一时刻只渲染一处 - 新增 tests/resourceCanvasGenerationTasksSidebarDismiss.test.tsx:点外部收起、点内部与开合按钮不收起且仍能 toggle、入口排在依赖/类型之前 验证:tsc --noEmit 通过;vitest(新用例 3 条 + 生成任务侧栏 8 条 + 侧栏样式 9 条 + 画布手动布局 9 条)通过;变异验证:关掉自动收起 effect 与把入口挪回排列方式之后,对应断言分别变红
This commit is contained in:
@@ -5767,6 +5767,39 @@ export default function ProjectDevelopmentView({
|
||||
visibleResourceIds,
|
||||
]);
|
||||
|
||||
/**
|
||||
* 「生成任务」侧栏失去焦点即自动收起:点画布、点别的工具栏按钮、点对话框都算失去焦点。
|
||||
*
|
||||
* 两处必须排除,否则会把用户刚做的动作吃掉:
|
||||
* - 侧栏内部:点任务卡、点「定位到素材」都不能收起侧栏;
|
||||
* - 工具条上那枚开合按钮:它自己负责 toggle,若在这里也被判成「点外部」就会先收起再被 toggle
|
||||
* 打开,表现为按钮失灵。点它时这里的 pointerdown 直接放行,收尾交给它的 click。
|
||||
*
|
||||
* 用 pointerdown 而不是 click:画布空白处的左键 pointerdown 会 `preventDefault()`,掐掉指针的
|
||||
* 兼容鼠标事件,document 上的 click 收不到那一次点击。
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!resourceAssetGenerationTasksPanelOpen) {
|
||||
return undefined;
|
||||
}
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
target.closest('.game-resource-generation-tasks-sidebar') ||
|
||||
target.closest('[data-resource-generation-task-toggle]')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setResourceAssetGenerationTasksPanelOpen(false);
|
||||
};
|
||||
document.addEventListener('pointerdown', handlePointerDown, true);
|
||||
return () =>
|
||||
document.removeEventListener('pointerdown', handlePointerDown, true);
|
||||
}, [resourceAssetGenerationTasksPanelOpen]);
|
||||
|
||||
/**
|
||||
* 切换当前版本只改「哪个版本是当前版本」这条记录层状态,并让运行模块重新加载当前预览。
|
||||
*
|
||||
@@ -7414,6 +7447,31 @@ export default function ProjectDevelopmentView({
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* 「生成任务」开合入口。**两个页签下都常驻**:侧栏本体是无条件渲染的非模态浮层,运行态一样
|
||||
* 可见,入口若只在资源页签,用户切到运行后关掉侧栏就再也打不开了。资源页签里它排在
|
||||
* 「依赖 / 类型」排列方式之前(动作在前、排列方式收在行尾),所以这里做成一个可复用的元素,
|
||||
* 由两处分支各自渲染一次——同一时刻只渲染一处,可访问名唯一。
|
||||
*/
|
||||
const generationTasksEntry = (
|
||||
<button
|
||||
type="button"
|
||||
className="game-workbench-resource-panel-button"
|
||||
aria-label="生成任务"
|
||||
aria-expanded={resourceAssetGenerationTasksPanelOpen}
|
||||
data-resource-generation-task-toggle=""
|
||||
data-resource-generation-task-count={resourceAssetGenerationInFlightCount}
|
||||
onClick={() =>
|
||||
setResourceAssetGenerationTasksPanelOpen((current) => !current)
|
||||
}
|
||||
>
|
||||
<ListChecks size={15} aria-hidden="true" />
|
||||
{resourceAssetGenerationInFlightCount > 0
|
||||
? `生成任务 · ${resourceAssetGenerationInFlightCount}`
|
||||
: '生成任务'}
|
||||
</button>
|
||||
);
|
||||
|
||||
if (planningStartMode) {
|
||||
return (
|
||||
<section
|
||||
@@ -7612,6 +7670,12 @@ export default function ProjectDevelopmentView({
|
||||
>
|
||||
{resourceLayoutSaving ? '保存中' : resourceLayoutNotice}
|
||||
</span>
|
||||
{/*
|
||||
「生成任务」入口排在「依赖 / 类型」排列方式之前:一个是开合任务侧栏的动作,
|
||||
一个是画布排列方式——动作在前、排列方式收在行尾。运行页签下由下面同一枚
|
||||
元素补渲染(见 `generationTasksEntry`)。
|
||||
*/}
|
||||
{generationTasksEntry}
|
||||
<div
|
||||
className="game-workbench-tabs game-resource-sort-tabs"
|
||||
role="group"
|
||||
@@ -7640,28 +7704,10 @@ export default function ProjectDevelopmentView({
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
{/*
|
||||
「生成任务」入口:**两个页签下都常驻**(不受 `mode === 'resources'` 限制)。
|
||||
侧栏本体是无条件渲染的非模态浮层,运行态一样可见;入口若只留在资源页签,
|
||||
用户切到运行后关掉侧栏就再也打不开了。有在途任务时带上数量,一眼看出还有几条在跑。
|
||||
*/}
|
||||
<button
|
||||
type="button"
|
||||
className="game-workbench-resource-panel-button"
|
||||
aria-label="生成任务"
|
||||
aria-expanded={resourceAssetGenerationTasksPanelOpen}
|
||||
data-resource-generation-task-count={
|
||||
resourceAssetGenerationInFlightCount
|
||||
}
|
||||
onClick={() =>
|
||||
setResourceAssetGenerationTasksPanelOpen((current) => !current)
|
||||
}
|
||||
>
|
||||
<ListChecks size={15} aria-hidden="true" />
|
||||
{resourceAssetGenerationInFlightCount > 0
|
||||
? `生成任务 · ${resourceAssetGenerationInFlightCount}`
|
||||
: '生成任务'}
|
||||
</button>
|
||||
{/* 运行页签(或 UI 编辑器)下资源类按钮整组收起,但「生成任务」入口必须留着。 */}
|
||||
{mode === 'resources' && !uiEditorRoute
|
||||
? null
|
||||
: generationTasksEntry}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import ProjectDevelopmentView from '../src/view/project-development';
|
||||
import {
|
||||
createGameCreationAppManifest,
|
||||
fireEvent,
|
||||
React,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from './appSurface/harness';
|
||||
|
||||
/**
|
||||
* 「生成任务」侧栏的两条交互口径:
|
||||
*
|
||||
* 1. **失去焦点即收起**:点画布、点别的工具栏按钮、点侧栏外部都算失去焦点;点侧栏内部
|
||||
* (含任务卡与「定位到素材」)与点那枚开合按钮不算——后者自己负责 toggle,否则会先被
|
||||
* 收起再被 toggle 打开,表现为按钮失灵。
|
||||
* 2. **入口顺序**:「生成任务」排在「依赖 / 类型」排列方式之前(动作在前、排列方式收行尾)。
|
||||
*/
|
||||
|
||||
function resourceGraphFor(resources: unknown) {
|
||||
const resourceIds = Array.isArray(resources)
|
||||
? resources.map(
|
||||
(resource) => (resource as { resourceId?: string }).resourceId ?? '',
|
||||
)
|
||||
: [];
|
||||
return {
|
||||
resourceIds,
|
||||
referenceEdges: [],
|
||||
taskFlows: [],
|
||||
categories: [],
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
|
||||
function installInvoke() {
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'read_local_project_resource_graph') {
|
||||
return resourceGraphFor(args?.resources);
|
||||
}
|
||||
if (command === 'read_local_project_resource_canvas_layout') {
|
||||
return {
|
||||
schemaVersion: 'game-creator-resource-layout.v1',
|
||||
projectId: args?.expectedProjectId,
|
||||
mode: args?.mode,
|
||||
revision: 0,
|
||||
positions: [],
|
||||
updatedAt: 0,
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
}
|
||||
|
||||
async function renderWorkbench(projectId: string) {
|
||||
installInvoke();
|
||||
const manifest = createGameCreationAppManifest(
|
||||
projectId,
|
||||
`${projectId} 项目`,
|
||||
);
|
||||
render(
|
||||
React.createElement(ProjectDevelopmentView, {
|
||||
projectName: manifest.name,
|
||||
projectPath: `/tmp/${projectId}`,
|
||||
manifest,
|
||||
attachments: [],
|
||||
recentRunStatus: null,
|
||||
recentRunStopReason: null,
|
||||
supervisor: React.createElement('div', null, '项目总控'),
|
||||
onHomeOpen: vi.fn(),
|
||||
onProjectsOpen: vi.fn(),
|
||||
}),
|
||||
);
|
||||
const entry = await screen.findByRole('button', { name: '生成任务' });
|
||||
fireEvent.click(entry);
|
||||
return screen.findByRole('region', { name: '生成任务' });
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('「生成任务」侧栏的开合与入口位置', () => {
|
||||
it('点侧栏外部(画布 / 其他区域)自动收起', async () => {
|
||||
const sidebar = await renderWorkbench('workbench-tasks-dismiss-outside');
|
||||
expect(sidebar).not.toBeNull();
|
||||
|
||||
fireEvent.pointerDown(document.body);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('region', { name: '生成任务' })).toBeNull(),
|
||||
);
|
||||
});
|
||||
|
||||
it('点侧栏内部与开合按钮都不收起:内部保持打开,按钮仍能 toggle 收起来', async () => {
|
||||
const sidebar = await renderWorkbench('workbench-tasks-dismiss-inside');
|
||||
// 侧栏内部点击(任务列表区域)不该被当成点外部。
|
||||
fireEvent.pointerDown(sidebar);
|
||||
expect(screen.queryByRole('region', { name: '生成任务' })).not.toBeNull();
|
||||
|
||||
// 开合按钮自己负责 toggle:点一次必须真的收起(不能先被「点外部」收起再被 toggle 打开)。
|
||||
const entry = screen.getByRole('button', { name: '生成任务' });
|
||||
fireEvent.pointerDown(entry);
|
||||
fireEvent.click(entry);
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('region', { name: '生成任务' })).toBeNull(),
|
||||
);
|
||||
|
||||
// 再点一次要能回来。
|
||||
fireEvent.click(entry);
|
||||
expect(
|
||||
await screen.findByRole('region', { name: '生成任务' }),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('「生成任务」入口排在「依赖 / 类型」排列方式之前', async () => {
|
||||
await renderWorkbench('workbench-tasks-entry-order');
|
||||
const actionsRow = document.querySelector('.game-workbench-view-actions');
|
||||
expect(actionsRow).not.toBeNull();
|
||||
const buttons = Array.from(actionsRow!.querySelectorAll('button'));
|
||||
const tasksIndex = buttons.findIndex(
|
||||
(button) => button.getAttribute('aria-label') === '生成任务',
|
||||
);
|
||||
const sortIndex = buttons.findIndex(
|
||||
(button) => button.getAttribute('aria-label') === '按依赖',
|
||||
);
|
||||
expect(tasksIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(sortIndex).toBeGreaterThanOrEqual(0);
|
||||
// 依赖 / 类型收在行尾,生成任务在它前面。
|
||||
expect(tasksIndex).toBeLessThan(sortIndex);
|
||||
expect(sortIndex).toBe(buttons.length - 2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user