统一智能体对话层级并完善素材导出与文档预览
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 2m44s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 2m45s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 2m46s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 2m49s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m53s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m18s
Project CI / Frontend tests (pull_request) Failing after 4m6s
Project CI / Native shell tests (pull_request) Successful in 6m12s
Project CI / Repository checks (pull_request) Failing after 3m48s
Project CI / Backend tests (pull_request) Successful in 7m8s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m59s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 2m44s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 2m45s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 2m46s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 2m49s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m53s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m18s
Project CI / Frontend tests (pull_request) Failing after 4m6s
Project CI / Native shell tests (pull_request) Successful in 6m12s
Project CI / Repository checks (pull_request) Failing after 3m48s
Project CI / Backend tests (pull_request) Successful in 7m8s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m59s
当前Agent与策划Agent共用正文和过程表现组件,统一思考及工具调用的小字号浅色样式 素材工具栏按实际动作分组,导出紧邻删除并修复空分组重复分隔线 文档与代码使用共享Markdown预览和代码高亮,复用按需读取与缓存 移除未调用的活跃回合查询命令,保留正式快照恢复链路 补充回归用例并同步技术方案和团队约定
This commit is contained in:
@@ -10,7 +10,10 @@ import {
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
||||
import { ImageCanvasSelectedLayerToolbarView } from './ImageCanvasSelectedLayerToolbarView';
|
||||
import {
|
||||
ImageCanvasSelectedLayerToolbarView,
|
||||
type ImageCanvasSelectedToolbarAction,
|
||||
} from './ImageCanvasSelectedLayerToolbarView';
|
||||
|
||||
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
||||
return {
|
||||
@@ -67,6 +70,121 @@ function renderSelectedToolbar(
|
||||
}
|
||||
|
||||
describe('ImageCanvasSelectedLayerToolbarView', () => {
|
||||
it('常规动作与末组分别分隔,导出紧邻删除且位于删除之前', () => {
|
||||
const props = renderSelectedToolbar({
|
||||
supportedActions: new Set(['quick-edit', 'download']),
|
||||
downloadLabel: '导出',
|
||||
extraActions: <button>引用</button>,
|
||||
endActions: <button>删除素材</button>,
|
||||
});
|
||||
const toolbar = screen.getByRole('toolbar');
|
||||
expect(
|
||||
toolbar.querySelectorAll(
|
||||
'.image-canvas-editor__floating-toolbar-divider',
|
||||
),
|
||||
).toHaveLength(2);
|
||||
expect(
|
||||
within(toolbar)
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.textContent),
|
||||
).toEqual(['快速编辑', '引用', '导出', '删除素材']);
|
||||
expect(
|
||||
screen.getByRole('button', { name: '导出' }).nextElementSibling,
|
||||
).toBe(screen.getByRole('button', { name: '删除素材' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '导出' }));
|
||||
expect(props.onDownloadLayer).toHaveBeenCalledWith(props.selectedLayer);
|
||||
});
|
||||
|
||||
it('任意动作组合都不生成相邻、开头或结尾分隔线', () => {
|
||||
const actions: ImageCanvasSelectedToolbarAction[] = [
|
||||
'quick-edit',
|
||||
'crop-expand',
|
||||
'redraw',
|
||||
'download',
|
||||
];
|
||||
for (const mediaType of ['image', 'audio', 'video'] as const) {
|
||||
for (let mask = 0; mask < 1 << actions.length; mask += 1) {
|
||||
for (const withExtraActions of [false, true]) {
|
||||
renderSelectedToolbar({
|
||||
selectedLayer: createLayer({ mediaType }),
|
||||
supportedActions: new Set(
|
||||
actions.filter((_, index) => (mask & (1 << index)) !== 0),
|
||||
),
|
||||
extraActions: withExtraActions ? <button>信息</button> : null,
|
||||
});
|
||||
const toolbar = screen.getByRole('toolbar');
|
||||
const divider = '.image-canvas-editor__floating-toolbar-divider';
|
||||
expect(toolbar.querySelector(`${divider} + ${divider}`)).toBeNull();
|
||||
expect(toolbar.firstElementChild?.matches(divider) ?? false).toBe(
|
||||
false,
|
||||
);
|
||||
expect(toolbar.lastElementChild?.matches(divider) ?? false).toBe(
|
||||
false,
|
||||
);
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('空的条件动作和嵌套 Fragment 不产生空分组', () => {
|
||||
renderSelectedToolbar({
|
||||
supportedActions: new Set(['quick-edit', 'download']),
|
||||
extraActions: (
|
||||
<>
|
||||
{null}
|
||||
<>{false}</>
|
||||
</>
|
||||
),
|
||||
endActions: <>{null}</>,
|
||||
});
|
||||
const toolbar = screen.getByRole('toolbar');
|
||||
expect(
|
||||
toolbar.querySelectorAll(
|
||||
'.image-canvas-editor__floating-toolbar-divider',
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
within(toolbar)
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.getAttribute('aria-label')),
|
||||
).toEqual(['快速编辑', '下载按钮']);
|
||||
expect(toolbar.lastElementChild?.tagName).toBe('BUTTON');
|
||||
});
|
||||
|
||||
it.each(['image', 'audio', 'video', 'image-sequence', undefined] as const)(
|
||||
'%s 素材使用同一个带文字导出入口与回调',
|
||||
(mediaType) => {
|
||||
const props = renderSelectedToolbar({
|
||||
selectedLayer: createLayer({ mediaType }),
|
||||
supportedActions: new Set(['download']),
|
||||
downloadLabel: '导出',
|
||||
extraActions: <button>信息</button>,
|
||||
endActions: <button>删除素材</button>,
|
||||
});
|
||||
const toolbar = screen.getByRole('toolbar');
|
||||
expect(
|
||||
within(toolbar)
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.textContent),
|
||||
).toEqual(['信息', '导出', '删除素材']);
|
||||
expect(
|
||||
screen.getByRole('button', { name: '导出' }).nextElementSibling,
|
||||
).toBe(screen.getByRole('button', { name: '删除素材' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '导出' }));
|
||||
expect(props.onDownloadLayer).toHaveBeenCalledWith(props.selectedLayer);
|
||||
},
|
||||
);
|
||||
|
||||
it('宿主未放行下载时不展示导出入口', () => {
|
||||
renderSelectedToolbar({
|
||||
supportedActions: new Set(),
|
||||
downloadLabel: '导出',
|
||||
extraActions: <button>信息</button>,
|
||||
});
|
||||
expect(screen.queryByRole('button', { name: '导出' })).toBeNull();
|
||||
});
|
||||
|
||||
it('renders common layer actions in the Lovart toolbar order and forwards callbacks', () => {
|
||||
const props = renderSelectedToolbar();
|
||||
const toolbar = screen.getByRole('toolbar', { name: '图片工具栏' });
|
||||
|
||||
@@ -10,7 +10,13 @@ import {
|
||||
Sparkles,
|
||||
WandSparkles,
|
||||
} from 'lucide-react';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import {
|
||||
Children,
|
||||
type CSSProperties,
|
||||
Fragment,
|
||||
isValidElement,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
|
||||
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
||||
@@ -39,8 +45,12 @@ type ImageCanvasSelectedLayerToolbarViewProps = {
|
||||
* 让没有对应后端链路的画布宿主不必把「点了没反应」的按钮渲染出来。
|
||||
*/
|
||||
supportedActions?: ReadonlySet<ImageCanvasSelectedToolbarAction> | null;
|
||||
/** 宿主在工具条上追加的动作节点,渲染在下载按钮之前。 */
|
||||
/** 宿主在工具条上追加的常规动作,渲染在下载按钮之前。 */
|
||||
extraActions?: ReactNode;
|
||||
/** 工具栏末组动作,紧随下载按钮,与下载共用一个分组。 */
|
||||
endActions?: ReactNode;
|
||||
/** 提供文字时显示带文字的导出入口;缺省保持网页画布的图标按钮。 */
|
||||
downloadLabel?: string;
|
||||
selectedLayer: CanvasLayer | null;
|
||||
selectedToolbarStyle: CSSProperties | null;
|
||||
onOpenQuickEditPanel: (layer: CanvasLayer) => void;
|
||||
@@ -58,9 +68,20 @@ type ImageCanvasSelectedLayerToolbarViewProps = {
|
||||
onDownloadLayer: (layer: CanvasLayer) => void;
|
||||
};
|
||||
|
||||
/** 条件动作通常放在 Fragment 中;空 Fragment 不能产生一个空分组。 */
|
||||
function hasToolbarActions(actions: ReactNode): boolean {
|
||||
return Children.toArray(actions).some((action) =>
|
||||
isValidElement<{ children?: ReactNode }>(action) && action.type === Fragment
|
||||
? hasToolbarActions(action.props.children)
|
||||
: action !== '',
|
||||
);
|
||||
}
|
||||
|
||||
export function ImageCanvasSelectedLayerToolbarView({
|
||||
supportedActions = null,
|
||||
extraActions,
|
||||
endActions,
|
||||
downloadLabel,
|
||||
selectedLayer,
|
||||
selectedToolbarStyle,
|
||||
onOpenQuickEditPanel,
|
||||
@@ -88,12 +109,35 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
'redraw',
|
||||
canOpenRedrawPanel(selectedLayer),
|
||||
);
|
||||
const extraActionDivider = extraActions ? (
|
||||
const showDownload = isActionSupported('download', true);
|
||||
const downloadAction = showDownload ? (
|
||||
downloadLabel ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label={downloadLabel}
|
||||
title={downloadLabel}
|
||||
icon={<Download className="h-4 w-4" />}
|
||||
onClick={() => onDownloadLayer(selectedLayer)}
|
||||
>
|
||||
<span>{downloadLabel}</span>
|
||||
</CanvasChromeButton>
|
||||
) : (
|
||||
<EditorIconButton
|
||||
label="下载按钮"
|
||||
title="下载按钮"
|
||||
icon={Download}
|
||||
onClick={() => onDownloadLayer(selectedLayer)}
|
||||
/>
|
||||
)
|
||||
) : null;
|
||||
const hasExtraActions = hasToolbarActions(extraActions);
|
||||
const hasEndActions = showDownload || hasToolbarActions(endActions);
|
||||
const divider = (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="image-canvas-editor__floating-toolbar-divider"
|
||||
/>
|
||||
) : null;
|
||||
);
|
||||
|
||||
if (selectedLayer.mediaType === 'audio') {
|
||||
return (
|
||||
@@ -115,16 +159,11 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
<span>改造</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{extraActionDivider}
|
||||
{canRedraw && hasExtraActions ? divider : null}
|
||||
{extraActions}
|
||||
{isActionSupported('download', true) ? (
|
||||
<EditorIconButton
|
||||
label="下载按钮"
|
||||
title="下载按钮"
|
||||
icon={Download}
|
||||
onClick={() => onDownloadLayer(selectedLayer)}
|
||||
/>
|
||||
) : null}
|
||||
{(canRedraw || hasExtraActions) && hasEndActions ? divider : null}
|
||||
{downloadAction}
|
||||
{endActions}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -141,6 +180,25 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
'character-animation',
|
||||
selectedLayer.assetKind === 'character',
|
||||
);
|
||||
const showCropExpand =
|
||||
canRasterEdit && isActionSupported('crop-expand', true);
|
||||
const showRemoveBackground =
|
||||
canRasterEdit && isActionSupported('remove-background', true);
|
||||
const showPerfectPixel =
|
||||
canRasterEdit && isActionSupported('perfect-pixel', true);
|
||||
const showSplitIconSpritesheet =
|
||||
selectedLayer.assetKind === 'icon-spritesheet' &&
|
||||
isActionSupported('split-icon-spritesheet', true);
|
||||
const showExtractUiDesign =
|
||||
selectedLayer.assetKind === 'ui-design' &&
|
||||
isActionSupported('extract-ui-design', true);
|
||||
const hasEditingActions =
|
||||
showCropExpand ||
|
||||
showRemoveBackground ||
|
||||
showPerfectPixel ||
|
||||
showSplitIconSpritesheet ||
|
||||
showExtractUiDesign ||
|
||||
showCharacterAnimation;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -151,23 +209,18 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
{showQuickEdit ? (
|
||||
<>
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label="快速编辑"
|
||||
title="快速编辑"
|
||||
icon={<Sparkles className="h-4 w-4" />}
|
||||
onClick={() => onOpenQuickEditPanel(selectedLayer)}
|
||||
>
|
||||
<span>快速编辑</span>
|
||||
</CanvasChromeButton>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="image-canvas-editor__floating-toolbar-divider"
|
||||
/>
|
||||
</>
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label="快速编辑"
|
||||
title="快速编辑"
|
||||
icon={<Sparkles className="h-4 w-4" />}
|
||||
onClick={() => onOpenQuickEditPanel(selectedLayer)}
|
||||
>
|
||||
<span>快速编辑</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{canRasterEdit && isActionSupported('crop-expand', true) ? (
|
||||
{showQuickEdit && hasEditingActions ? divider : null}
|
||||
{showCropExpand ? (
|
||||
<EditorIconButton
|
||||
label="裁扩按钮"
|
||||
title="裁扩按钮"
|
||||
@@ -175,7 +228,7 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
onClick={() => onOpenCropExpandPanel(selectedLayer)}
|
||||
/>
|
||||
) : null}
|
||||
{canRasterEdit && isActionSupported('remove-background', true) ? (
|
||||
{showRemoveBackground ? (
|
||||
<EditorIconButton
|
||||
label="去除背景按钮"
|
||||
title="去除背景按钮"
|
||||
@@ -183,7 +236,7 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
onClick={() => onRemoveBackground(selectedLayer)}
|
||||
/>
|
||||
) : null}
|
||||
{canRasterEdit && isActionSupported('perfect-pixel', true) ? (
|
||||
{showPerfectPixel ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
// 中文注释:保存态名称不能直接用「素材类型保存中」——icon-spritesheet 图层上
|
||||
@@ -236,8 +289,7 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{selectedLayer.assetKind === 'icon-spritesheet' &&
|
||||
isActionSupported('split-icon-spritesheet', true) ? (
|
||||
{showSplitIconSpritesheet ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label={
|
||||
@@ -274,8 +326,7 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{selectedLayer.assetKind === 'ui-design' &&
|
||||
isActionSupported('extract-ui-design', true) ? (
|
||||
{showExtractUiDesign ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label="提取素材"
|
||||
@@ -299,10 +350,7 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
) : null}
|
||||
{canRedraw ? (
|
||||
<>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="image-canvas-editor__floating-toolbar-divider"
|
||||
/>
|
||||
{showQuickEdit || hasEditingActions ? divider : null}
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label="改造"
|
||||
@@ -314,16 +362,16 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
</CanvasChromeButton>
|
||||
</>
|
||||
) : null}
|
||||
{extraActionDivider}
|
||||
{(showQuickEdit || hasEditingActions || canRedraw) && hasExtraActions
|
||||
? divider
|
||||
: null}
|
||||
{extraActions}
|
||||
{isActionSupported('download', true) ? (
|
||||
<EditorIconButton
|
||||
label="下载按钮"
|
||||
title="下载按钮"
|
||||
icon={Download}
|
||||
onClick={() => onDownloadLayer(selectedLayer)}
|
||||
/>
|
||||
) : null}
|
||||
{(showQuickEdit || hasEditingActions || canRedraw || hasExtraActions) &&
|
||||
hasEndActions
|
||||
? divider
|
||||
: null}
|
||||
{downloadAction}
|
||||
{endActions}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user