修复编辑器定价持久化与资源预览
将模型定价改为 SpacetimeDB 强类型持久化并兼容旧配置种子迁移 修复快速编辑支付弹窗期间框选显示和交互冻结 补齐后台图片放大以及视频音频资源预览 收紧主站和 External 资源换签授权并记录管理员跨用户审计 固化外部生成入队价格、attempt 钱包结算和最终 lease 失败收口 加固运行时身份轮换、bootstrap secret 与生产构建发布门禁 同步生成绑定、定向测试、运维脚本和项目文档
This commit is contained in:
@@ -573,6 +573,158 @@ describe('ImageCanvasEditorView', () => {
|
||||
expect(screen.queryByPlaceholderText('输入兑换码')).toBeNull();
|
||||
});
|
||||
|
||||
it('suspends canvas interaction during account payment dialogs and restores completed quick edit selections', async () => {
|
||||
render(
|
||||
<AuthUiContext.Provider
|
||||
value={createAuthValue({
|
||||
user: {
|
||||
id: 'user-1',
|
||||
publicUserCode: 'U001',
|
||||
displayName: '测试用户',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: '138****0000',
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
},
|
||||
canAccessProtectedData: true,
|
||||
})}
|
||||
>
|
||||
<ImageCanvasEditorView />
|
||||
</AuthUiContext.Provider>,
|
||||
);
|
||||
|
||||
const sourceLayer = (
|
||||
await screen.findByAltText('画布图片:拼图素材')
|
||||
).closest('button')!;
|
||||
dispatchPointerEvent(sourceLayer, 'pointerdown', {
|
||||
button: 0,
|
||||
pointerId: 71,
|
||||
clientX: 120,
|
||||
clientY: 120,
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '快速编辑' }));
|
||||
|
||||
const quickEditDialog = screen.getByRole('dialog', {
|
||||
name: '快速编辑图片',
|
||||
});
|
||||
const selectionToolbar = screen.getByRole('toolbar', {
|
||||
name: '快速编辑框选工具',
|
||||
});
|
||||
const rectTool = within(selectionToolbar).getByRole('button', {
|
||||
name: '矩形框选',
|
||||
});
|
||||
fireEvent.click(rectTool);
|
||||
|
||||
const selectionCanvas = screen.getByRole('application', {
|
||||
name: 'UI素材框选画布',
|
||||
});
|
||||
dispatchPointerEvent(selectionCanvas, 'pointerdown', {
|
||||
pointerId: 72,
|
||||
clientX: 20,
|
||||
clientY: 20,
|
||||
});
|
||||
dispatchPointerEvent(selectionCanvas, 'pointermove', {
|
||||
pointerId: 72,
|
||||
clientX: 90,
|
||||
clientY: 90,
|
||||
});
|
||||
dispatchPointerEvent(selectionCanvas, 'pointerup', {
|
||||
pointerId: 72,
|
||||
clientX: 90,
|
||||
clientY: 90,
|
||||
});
|
||||
|
||||
expect(
|
||||
(
|
||||
within(quickEditDialog).getByLabelText(
|
||||
'快速编辑提示词',
|
||||
) as HTMLTextAreaElement
|
||||
).value,
|
||||
).toContain('对1号红色圈选框里的内容做以下修改');
|
||||
|
||||
dispatchPointerEvent(selectionCanvas, 'pointerdown', {
|
||||
pointerId: 73,
|
||||
clientX: 30,
|
||||
clientY: 30,
|
||||
});
|
||||
dispatchPointerEvent(selectionCanvas, 'pointermove', {
|
||||
pointerId: 73,
|
||||
clientX: 110,
|
||||
clientY: 110,
|
||||
});
|
||||
fireEvent.keyDown(window, {
|
||||
key: 'c',
|
||||
code: 'KeyC',
|
||||
ctrlKey: true,
|
||||
});
|
||||
|
||||
const viewport = screen.getByLabelText('画布工作区');
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', {
|
||||
name: '泥点余额 1,234泥点',
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
await screen.findByRole('dialog', { name: '账户充值' }),
|
||||
).toBeTruthy();
|
||||
|
||||
fireEvent.keyDown(window, { key: 'Delete', code: 'Delete' });
|
||||
fireEvent.contextMenu(viewport, { clientX: 320, clientY: 220 });
|
||||
const pasteEvent = new Event('paste', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
act(() => {
|
||||
window.dispatchEvent(pasteEvent);
|
||||
});
|
||||
|
||||
expect(screen.getByAltText('画布图片:拼图素材')).toBeTruthy();
|
||||
expect(screen.getAllByAltText('画布图片:拼图素材')).toHaveLength(1);
|
||||
expect(screen.queryByRole('menu', { name: '画布右键菜单' })).toBeNull();
|
||||
const pausedToolbar = screen.getByRole('toolbar', {
|
||||
name: '快速编辑框选工具',
|
||||
});
|
||||
expect(
|
||||
within(pausedToolbar)
|
||||
.getByRole('button', { name: '矩形框选' })
|
||||
.getAttribute('aria-pressed'),
|
||||
).toBe('true');
|
||||
expect(viewport.getAttribute('aria-disabled')).toBe('true');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭账户充值' }));
|
||||
|
||||
const resumedToolbar = await screen.findByRole('toolbar', {
|
||||
name: '快速编辑框选工具',
|
||||
});
|
||||
expect(viewport.getAttribute('aria-disabled')).toBeNull();
|
||||
expect(
|
||||
within(resumedToolbar)
|
||||
.getByRole('button', { name: '矩形框选' })
|
||||
.getAttribute('aria-pressed'),
|
||||
).toBe('true');
|
||||
|
||||
const resumedCanvas = screen.getByRole('application', {
|
||||
name: 'UI素材框选画布',
|
||||
});
|
||||
dispatchPointerEvent(resumedCanvas, 'pointermove', {
|
||||
pointerId: 73,
|
||||
clientX: 110,
|
||||
clientY: 110,
|
||||
});
|
||||
dispatchPointerEvent(resumedCanvas, 'pointerup', {
|
||||
pointerId: 73,
|
||||
clientX: 110,
|
||||
clientY: 110,
|
||||
});
|
||||
|
||||
const resumedPrompt = screen.getByLabelText(
|
||||
'快速编辑提示词',
|
||||
) as HTMLTextAreaElement;
|
||||
expect(resumedPrompt.value).toContain('对1号红色圈选框里的内容做以下修改');
|
||||
expect(resumedPrompt.value).not.toContain('2号红色圈选框');
|
||||
});
|
||||
|
||||
it('opens the login modal immediately when entering the editor while logged out', async () => {
|
||||
const openLoginModal = vi.fn();
|
||||
|
||||
@@ -1478,9 +1630,7 @@ describe('ImageCanvasEditorView', () => {
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
expect(screen.queryByRole('button', { name: '画布 Agent' })).toBeNull();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: '打开画布 Agent' }),
|
||||
).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '打开画布 Agent' })).toBeNull();
|
||||
expect(screen.queryByLabelText('发送给画布 Agent')).toBeNull();
|
||||
expect(listEditorAgentConversationsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -519,6 +519,12 @@ export function ImageCanvasEditorView({
|
||||
requestLogin: () => authUiRef.current?.openLoginModal(),
|
||||
currentUser: authUi?.user ?? null,
|
||||
});
|
||||
const isAccountPaymentModalOpen =
|
||||
isRewardCodeOpen ||
|
||||
isRechargeOpen ||
|
||||
Boolean(nativeWechatPayment) ||
|
||||
Boolean(rechargePaymentResult) ||
|
||||
Boolean(wechatRechargeOrderConfirmationState);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authUi || authUi.user || authUi.canAccessProtectedData) {
|
||||
@@ -1347,6 +1353,8 @@ export function ImageCanvasEditorView({
|
||||
moveQuickEditSelectionPointer,
|
||||
endUiAssetExtractionPointer,
|
||||
endQuickEditSelectionPointer,
|
||||
cancelUiAssetExtractionPointer,
|
||||
cancelQuickEditSelectionPointer,
|
||||
cancelUiAssetExtraction,
|
||||
appendUiAssetExtractionReferences,
|
||||
removeUiAssetExtractionReference,
|
||||
@@ -1581,6 +1589,30 @@ export function ImageCanvasEditorView({
|
||||
onCloseImageContextMenu: () => setImageContextMenu(null),
|
||||
});
|
||||
resetCanvasInteractionStateRef.current = clearActiveInteraction;
|
||||
useEffect(() => {
|
||||
if (!isAccountPaymentModalOpen) {
|
||||
return;
|
||||
}
|
||||
clearActiveInteraction();
|
||||
cancelUiAssetExtractionPointer();
|
||||
cancelQuickEditSelectionPointer();
|
||||
}, [
|
||||
cancelQuickEditSelectionPointer,
|
||||
cancelUiAssetExtractionPointer,
|
||||
clearActiveInteraction,
|
||||
isAccountPaymentModalOpen,
|
||||
]);
|
||||
const openAccountPaymentModal = useCallback(() => {
|
||||
clearActiveInteraction();
|
||||
cancelUiAssetExtractionPointer();
|
||||
cancelQuickEditSelectionPointer();
|
||||
openRechargeOrRewardCodeModal();
|
||||
}, [
|
||||
cancelQuickEditSelectionPointer,
|
||||
cancelUiAssetExtractionPointer,
|
||||
clearActiveInteraction,
|
||||
openRechargeOrRewardCodeModal,
|
||||
]);
|
||||
const handleCanvasPointerDownWithUiExtractionDismiss = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (uiAssetExtractionState) {
|
||||
@@ -1777,6 +1809,7 @@ export function ImageCanvasEditorView({
|
||||
);
|
||||
|
||||
useImageCanvasKeyboardShortcuts({
|
||||
isInteractionPaused: isAccountPaymentModalOpen,
|
||||
generateDialogRef,
|
||||
selectedLayerIdRef,
|
||||
selectedLayerIdsRef,
|
||||
@@ -1848,6 +1881,9 @@ export function ImageCanvasEditorView({
|
||||
if (isEditablePasteTarget(event.target)) {
|
||||
return;
|
||||
}
|
||||
if (isAccountPaymentModalOpen) {
|
||||
return;
|
||||
}
|
||||
const didPasteCanvasClipboard = pasteCanvasClipboard();
|
||||
if (didPasteCanvasClipboard) {
|
||||
event.preventDefault();
|
||||
@@ -1865,7 +1901,7 @@ export function ImageCanvasEditorView({
|
||||
return () => {
|
||||
window.removeEventListener('paste', handleClipboardPaste);
|
||||
};
|
||||
}, [addUploadedFiles, pasteCanvasClipboard]);
|
||||
}, [addUploadedFiles, isAccountPaymentModalOpen, pasteCanvasClipboard]);
|
||||
|
||||
useEffect(() => {
|
||||
const blockBrowserZoom = (event: WheelEvent) => {
|
||||
@@ -2005,7 +2041,7 @@ export function ImageCanvasEditorView({
|
||||
resetProjectRenameError,
|
||||
exportCanvasAssets,
|
||||
onOpenShortcuts: () => setIsShortcutDialogOpen(true),
|
||||
onOpenWallet: openRechargeOrRewardCodeModal,
|
||||
onOpenWallet: openAccountPaymentModal,
|
||||
onOpenAccount: () => {
|
||||
if (authUi?.user) {
|
||||
authUi.openAccountModal();
|
||||
@@ -2019,6 +2055,7 @@ export function ImageCanvasEditorView({
|
||||
specToolWrapRef,
|
||||
musicToolWrapRef,
|
||||
publicationToolWrapRef,
|
||||
isInteractionPaused: isAccountPaymentModalOpen,
|
||||
isPanning,
|
||||
effectiveTool,
|
||||
canvasBackgroundColor,
|
||||
@@ -2040,8 +2077,12 @@ export function ImageCanvasEditorView({
|
||||
generateDialog,
|
||||
cropExpandPanel: generationSurface.cropExpandPanel,
|
||||
cropExpandSourceLayer: generationSurface.cropExpandSourceLayer,
|
||||
uiAssetExtractionState,
|
||||
uiAssetExtractionSourceLayer,
|
||||
uiAssetExtractionState: isAccountPaymentModalOpen
|
||||
? null
|
||||
: uiAssetExtractionState,
|
||||
uiAssetExtractionSourceLayer: isAccountPaymentModalOpen
|
||||
? null
|
||||
: uiAssetExtractionSourceLayer,
|
||||
quickEditSelectionState,
|
||||
quickEditSelectionSourceLayer,
|
||||
generationComposerStyle,
|
||||
|
||||
@@ -45,6 +45,7 @@ export type ImageCanvasStageViewProps = {
|
||||
specToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||
musicToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||
publicationToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||
isInteractionPaused?: boolean;
|
||||
isPanning: boolean;
|
||||
effectiveTool: CanvasTool;
|
||||
canvasBackgroundColor: string;
|
||||
@@ -196,6 +197,7 @@ export function ImageCanvasStageView({
|
||||
specToolWrapRef,
|
||||
musicToolWrapRef,
|
||||
publicationToolWrapRef,
|
||||
isInteractionPaused = false,
|
||||
isPanning,
|
||||
effectiveTool,
|
||||
canvasBackgroundColor,
|
||||
@@ -316,16 +318,20 @@ export function ImageCanvasStageView({
|
||||
<div
|
||||
ref={canvasViewportRef}
|
||||
className={`image-canvas-editor__viewport ${isPanning ? 'image-canvas-editor__viewport--panning' : ''} image-canvas-editor__viewport--tool-${effectiveTool}`}
|
||||
style={{ backgroundColor: canvasBackgroundColor }}
|
||||
style={{
|
||||
backgroundColor: canvasBackgroundColor,
|
||||
pointerEvents: isInteractionPaused ? 'none' : undefined,
|
||||
}}
|
||||
aria-label="画布工作区"
|
||||
onPointerDown={onCanvasPointerDown}
|
||||
onPointerMove={onCanvasPointerMove}
|
||||
onPointerUp={onCanvasPointerUp}
|
||||
onPointerCancel={onCanvasPointerUp}
|
||||
onDragOver={onCanvasDragOver}
|
||||
onDragLeave={onCanvasDragLeave}
|
||||
onDrop={onCanvasDrop}
|
||||
onContextMenu={onCanvasContextMenu}
|
||||
aria-disabled={isInteractionPaused || undefined}
|
||||
onPointerDown={isInteractionPaused ? undefined : onCanvasPointerDown}
|
||||
onPointerMove={isInteractionPaused ? undefined : onCanvasPointerMove}
|
||||
onPointerUp={isInteractionPaused ? undefined : onCanvasPointerUp}
|
||||
onPointerCancel={isInteractionPaused ? undefined : onCanvasPointerUp}
|
||||
onDragOver={isInteractionPaused ? undefined : onCanvasDragOver}
|
||||
onDragLeave={isInteractionPaused ? undefined : onCanvasDragLeave}
|
||||
onDrop={isInteractionPaused ? undefined : onCanvasDrop}
|
||||
onContextMenu={isInteractionPaused ? undefined : onCanvasContextMenu}
|
||||
>
|
||||
{uploadDropTarget === 'canvas' ? (
|
||||
<div
|
||||
|
||||
@@ -1650,14 +1650,17 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setActiveSidebarPanel('layers');
|
||||
} catch (error) {
|
||||
if (backgroundRemovalDialogId) {
|
||||
updateCanvasGenerationDialogById(backgroundRemovalDialogId, (dialog) => ({
|
||||
...dialog,
|
||||
status: 'failed',
|
||||
errorMessage:
|
||||
error instanceof Error && error.message.trim()
|
||||
? error.message
|
||||
: '去除背景失败',
|
||||
}));
|
||||
updateCanvasGenerationDialogById(
|
||||
backgroundRemovalDialogId,
|
||||
(dialog) => ({
|
||||
...dialog,
|
||||
status: 'failed',
|
||||
errorMessage:
|
||||
error instanceof Error && error.message.trim()
|
||||
? error.message
|
||||
: '去除背景失败',
|
||||
}),
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -2120,6 +2123,17 @@ export function useImageCanvasGenerationWorkflow({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const cancelUiAssetExtractionPointer = useCallback(() => {
|
||||
setUiAssetExtractionState((currentState) =>
|
||||
currentState?.draftMark
|
||||
? {
|
||||
...currentState,
|
||||
draftMark: null,
|
||||
}
|
||||
: currentState,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const endQuickEditSelectionPointer = useCallback(() => {
|
||||
const currentSelectionState = quickEditSelectionStateRef.current;
|
||||
if (!currentSelectionState?.draftMark) {
|
||||
@@ -2179,6 +2193,17 @@ export function useImageCanvasGenerationWorkflow({
|
||||
);
|
||||
}, [setGenerateDialog, setQuickEditPanel, updateQuickEditSelectionState]);
|
||||
|
||||
const cancelQuickEditSelectionPointer = useCallback(() => {
|
||||
updateQuickEditSelectionState((currentState) =>
|
||||
currentState?.draftMark
|
||||
? {
|
||||
...currentState,
|
||||
draftMark: null,
|
||||
}
|
||||
: currentState,
|
||||
);
|
||||
}, [updateQuickEditSelectionState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!quickEditSelectionState) {
|
||||
return;
|
||||
@@ -2349,6 +2374,8 @@ export function useImageCanvasGenerationWorkflow({
|
||||
moveQuickEditSelectionPointer,
|
||||
endUiAssetExtractionPointer,
|
||||
endQuickEditSelectionPointer,
|
||||
cancelUiAssetExtractionPointer,
|
||||
cancelQuickEditSelectionPointer,
|
||||
cancelUiAssetExtraction,
|
||||
submitUiAssetExtraction,
|
||||
characterAnimationPanel: effectiveCharacterAnimationPanel,
|
||||
@@ -2453,6 +2480,8 @@ export function useImageCanvasGenerationWorkflow({
|
||||
moveQuickEditSelectionPointer,
|
||||
endUiAssetExtractionPointer,
|
||||
endQuickEditSelectionPointer,
|
||||
cancelUiAssetExtractionPointer,
|
||||
cancelQuickEditSelectionPointer,
|
||||
cancelUiAssetExtraction,
|
||||
submitUiAssetExtraction,
|
||||
setEffectiveCharacterAnimationPanel,
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { useRef, useState } from 'react';
|
||||
import {
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type {
|
||||
@@ -58,6 +63,7 @@ function KeyboardShortcutsHarness({
|
||||
initialGenerateDialog = null,
|
||||
initialQuickEditPanel = null,
|
||||
initialTool = 'select',
|
||||
isInteractionPaused = false,
|
||||
undoCanvasChange = vi.fn(),
|
||||
redoCanvasChange = vi.fn(),
|
||||
deleteLayerById = vi.fn(),
|
||||
@@ -79,12 +85,15 @@ function KeyboardShortcutsHarness({
|
||||
startProjectRename = vi.fn(),
|
||||
exportCanvasAssets = vi.fn(),
|
||||
closeEditorChromePanels = vi.fn(),
|
||||
setIsSpacePanning: setIsSpacePanningOverride,
|
||||
setShiftPressed: setShiftPressedOverride,
|
||||
}: {
|
||||
selectedLayerId?: string | null;
|
||||
selectedLayerIds?: string[];
|
||||
initialGenerateDialog?: GenerateDialogState | null;
|
||||
initialQuickEditPanel?: QuickEditPanelState | null;
|
||||
initialTool?: CanvasTool;
|
||||
isInteractionPaused?: boolean;
|
||||
undoCanvasChange?: () => void;
|
||||
redoCanvasChange?: () => void;
|
||||
deleteLayerById?: (layerId: string | null) => void;
|
||||
@@ -108,6 +117,8 @@ function KeyboardShortcutsHarness({
|
||||
startProjectRename?: () => void;
|
||||
exportCanvasAssets?: () => void;
|
||||
closeEditorChromePanels?: () => void;
|
||||
setIsSpacePanning?: Dispatch<SetStateAction<boolean>>;
|
||||
setShiftPressed?: Dispatch<SetStateAction<boolean>>;
|
||||
}) {
|
||||
const [activeTool, setActiveTool] = useState<CanvasTool>(initialTool);
|
||||
const [generateDialog, setGenerateDialogState] =
|
||||
@@ -141,8 +152,8 @@ function KeyboardShortcutsHarness({
|
||||
const [, setIsUiDesignSpecMenuOpen] = useState(true);
|
||||
const [, setIsPickingUiDesignSpecFromCanvas] =
|
||||
useState(true);
|
||||
const [isSpacePanning, setIsSpacePanning] = useState(false);
|
||||
const [shiftPressed, setShiftPressed] = useState(false);
|
||||
const [isSpacePanning, setIsSpacePanningState] = useState(false);
|
||||
const [shiftPressed, setShiftPressedState] = useState(false);
|
||||
const generateDialogRef = useRef<GenerateDialogState | null>(generateDialog);
|
||||
const selectedLayerIdRef = useRef<string | null>(selectedLayerId);
|
||||
const selectedLayerIdsRef = useRef<string[]>(selectedLayerIds);
|
||||
@@ -151,6 +162,7 @@ function KeyboardShortcutsHarness({
|
||||
selectedLayerIdsRef.current = selectedLayerIds;
|
||||
|
||||
useImageCanvasKeyboardShortcuts({
|
||||
isInteractionPaused,
|
||||
generateDialogRef,
|
||||
selectedLayerIdRef,
|
||||
selectedLayerIdsRef,
|
||||
@@ -200,8 +212,8 @@ function KeyboardShortcutsHarness({
|
||||
setIsPickingIconSpecFromCanvas,
|
||||
setIsUiDesignSpecMenuOpen,
|
||||
setIsPickingUiDesignSpecFromCanvas,
|
||||
setIsSpacePanning,
|
||||
setShiftPressed,
|
||||
setIsSpacePanning: setIsSpacePanningOverride ?? setIsSpacePanningState,
|
||||
setShiftPressed: setShiftPressedOverride ?? setShiftPressedState,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -483,6 +495,82 @@ describe('useImageCanvasKeyboardShortcuts', () => {
|
||||
expect(screen.getByTestId('space-panning').textContent).toBe('false');
|
||||
});
|
||||
|
||||
it('pauses canvas shortcuts and clears held interaction state while account dialogs are open', () => {
|
||||
const selectAllCanvasObjects = vi.fn();
|
||||
const deleteSelectedCanvasObjects = vi.fn();
|
||||
const clearCanvasSelection = vi.fn();
|
||||
const switchTool = vi.fn();
|
||||
const setIsSpacePanning = vi.fn();
|
||||
const setShiftPressed = vi.fn();
|
||||
const { rerender } = render(
|
||||
<KeyboardShortcutsHarness
|
||||
selectedLayerIds={['layer-selected']}
|
||||
selectAllCanvasObjects={selectAllCanvasObjects}
|
||||
deleteSelectedCanvasObjects={deleteSelectedCanvasObjects}
|
||||
clearCanvasSelection={clearCanvasSelection}
|
||||
switchTool={switchTool}
|
||||
setIsSpacePanning={setIsSpacePanning}
|
||||
setShiftPressed={setShiftPressed}
|
||||
/>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
fireEvent.keyDown(window, { key: 'Shift', code: 'ShiftLeft' });
|
||||
fireEvent.keyDown(window, { key: ' ', code: 'Space' });
|
||||
});
|
||||
expect(setShiftPressed).toHaveBeenLastCalledWith(true);
|
||||
expect(setIsSpacePanning).toHaveBeenLastCalledWith(true);
|
||||
|
||||
rerender(
|
||||
<KeyboardShortcutsHarness
|
||||
selectedLayerIds={['layer-selected']}
|
||||
isInteractionPaused
|
||||
selectAllCanvasObjects={selectAllCanvasObjects}
|
||||
deleteSelectedCanvasObjects={deleteSelectedCanvasObjects}
|
||||
clearCanvasSelection={clearCanvasSelection}
|
||||
switchTool={switchTool}
|
||||
setIsSpacePanning={setIsSpacePanning}
|
||||
setShiftPressed={setShiftPressed}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(setShiftPressed).toHaveBeenLastCalledWith(false);
|
||||
expect(setIsSpacePanning).toHaveBeenLastCalledWith(false);
|
||||
|
||||
const selectAllEvent = new KeyboardEvent('keydown', {
|
||||
key: 'a',
|
||||
code: 'KeyA',
|
||||
ctrlKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
const deleteEvent = new KeyboardEvent('keydown', {
|
||||
key: 'Delete',
|
||||
code: 'Delete',
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
const escapeEvent = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
code: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
window.dispatchEvent(selectAllEvent);
|
||||
window.dispatchEvent(deleteEvent);
|
||||
window.dispatchEvent(escapeEvent);
|
||||
fireEvent.keyDown(window, { key: 'v', code: 'KeyV' });
|
||||
|
||||
expect(selectAllCanvasObjects).not.toHaveBeenCalled();
|
||||
expect(deleteSelectedCanvasObjects).not.toHaveBeenCalled();
|
||||
expect(clearCanvasSelection).not.toHaveBeenCalled();
|
||||
expect(switchTool).not.toHaveBeenCalled();
|
||||
expect(selectAllEvent.defaultPrevented).toBe(false);
|
||||
expect(deleteEvent.defaultPrevented).toBe(false);
|
||||
expect(escapeEvent.defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
it('nudges selected canvas objects with arrow keys', () => {
|
||||
const nudgeSelectedCanvasObjects = vi.fn();
|
||||
render(
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
} from './ImageCanvasEditorTypes';
|
||||
|
||||
type UseImageCanvasKeyboardShortcutsOptions = {
|
||||
isInteractionPaused?: boolean;
|
||||
generateDialogRef: RefObject<GenerateDialogState | null>;
|
||||
selectedLayerIdRef: RefObject<string | null>;
|
||||
selectedLayerIdsRef?: RefObject<string[]>;
|
||||
@@ -156,6 +157,7 @@ function getArrowDelta(event: KeyboardEvent) {
|
||||
}
|
||||
|
||||
export function useImageCanvasKeyboardShortcuts({
|
||||
isInteractionPaused = false,
|
||||
generateDialogRef,
|
||||
selectedLayerIdRef,
|
||||
selectedLayerIdsRef,
|
||||
@@ -202,6 +204,14 @@ export function useImageCanvasKeyboardShortcuts({
|
||||
setIsSpacePanning,
|
||||
setShiftPressed,
|
||||
}: UseImageCanvasKeyboardShortcutsOptions) {
|
||||
useEffect(() => {
|
||||
if (!isInteractionPaused) {
|
||||
return;
|
||||
}
|
||||
setShiftPressed(false);
|
||||
setIsSpacePanning(false);
|
||||
}, [isInteractionPaused, setIsSpacePanning, setShiftPressed]);
|
||||
|
||||
useEffect(() => {
|
||||
const closeTransientEditorPanels = () => {
|
||||
closeEditorChromePanels();
|
||||
@@ -253,6 +263,9 @@ export function useImageCanvasKeyboardShortcuts({
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (isInteractionPaused) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isCtrlShortcut(event) &&
|
||||
event.code === 'KeyZ' &&
|
||||
@@ -461,6 +474,9 @@ export function useImageCanvasKeyboardShortcuts({
|
||||
setIsSpacePanning(true);
|
||||
};
|
||||
const handleKeyUp = (event: KeyboardEvent) => {
|
||||
if (isInteractionPaused) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Shift') {
|
||||
setShiftPressed(false);
|
||||
}
|
||||
@@ -501,6 +517,7 @@ export function useImageCanvasKeyboardShortcuts({
|
||||
exportCanvasAssets,
|
||||
fitLayers,
|
||||
generateDialogRef,
|
||||
isInteractionPaused,
|
||||
moveSelectedCanvasLayers,
|
||||
nudgeSelectedCanvasObjects,
|
||||
requestRemoveCanvasGenerationDialog,
|
||||
|
||||
Reference in New Issue
Block a user