From f34556d33da936835eb58d4cff52f55f6e3fa5a7 Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 17 Jun 2026 10:56:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E5=9B=BE=E7=89=87=E7=94=BB?= =?UTF-8?q?=E5=B8=83=E5=9B=BE=E7=89=87=E4=BF=A1=E6=81=AF=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增图片信息弹窗组件,承接 metadata 详情渲染和 UnifiedModal 接入 修复未登录进入编辑器时项目和素材接口抢跑 401 修复重置画布视图点击事件误传导致适合视图报错 补充图片信息弹窗、鉴权门禁和重置按钮回归测试 更新前端拆分文档和 TRACKING 浏览器回归记录 --- TRACKING.md | 1 + ...构】图片画布编辑器前端拆分计划-2026-06-17.md | 11 ++- .../ImageCanvasEditorView.test.tsx | 46 ++++++++- .../image-editor/ImageCanvasEditorView.tsx | 97 ++++-------------- .../ImageCanvasMetadataModalView.test.tsx | 99 +++++++++++++++++++ .../ImageCanvasMetadataModalView.tsx | 83 ++++++++++++++++ .../image-editor/ImageCanvasStageView.tsx | 2 +- .../useImageCanvasAssetLibrary.test.tsx | 12 +++ .../useImageCanvasAssetLibrary.ts | 7 +- .../useImageCanvasProjectPersistence.test.tsx | 15 ++- .../useImageCanvasProjectPersistence.ts | 13 ++- 11 files changed, 303 insertions(+), 83 deletions(-) create mode 100644 src/components/image-editor/ImageCanvasMetadataModalView.test.tsx create mode 100644 src/components/image-editor/ImageCanvasMetadataModalView.tsx diff --git a/TRACKING.md b/TRACKING.md index daac9ef9..cda2a93e 100644 --- a/TRACKING.md +++ b/TRACKING.md @@ -135,3 +135,4 @@ - 2026-06-17 前端拆分第十七阶段:新增 `useImageCanvasViewportControls`,把视口状态、画布尺寸、小地图投影、适合视图、中心缩放、滚轮语义、坐标换算和小地图移动从主视图抽出;主视图继续保留图层拖拽、框选、生成占位拖拽、上传 drop 和历史触发时机。验证命令:`npm run test -- src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`,登录后素材 / 画布 / 小地图和底部工具栏可见;普通滚轮不改变缩放,Ctrl 滚轮从 `100%` 到 `110%`;背景设置点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,登录后控制台无前端 error。 - 2026-06-17 前端拆分第十八阶段:新增 `useImageCanvasStageInteractions`,把画布舞台 pointer 状态机、选择 / 框选、多选拖拽、生成占位拖拽、抓手 / Space 临时抓手 / 中键平移、小地图 click / drag 分流和吸附线状态从主视图抽出;主视图继续保留上传 drop、右键菜单、生成提交、项目持久化和工具栏动作分流。新增 hook 单测覆盖多选拖拽、框选、临时抓手、生成占位和小地图分流;主视图从 1802 行降至 1452 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新和未登录上传均弹出 `账号入口`,登录后素材 / 画布 / 小地图和底部工具栏保持可见;`画布背景设置` 点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;普通滚轮不改变缩放,Ctrl 滚轮从 `146%` 到 `161%`;抓手 / 文字 / 选择工具可连续切换;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,关闭对话框后占位图保留,登录后控制台无前端 error。 - 2026-06-17 前端拆分第十九阶段:新增 `useImageCanvasCanvasDropWorkflow`,把画布区域 drag over / drag leave / drop 分流从主视图抽出,覆盖素材库图片拖入画布、本地文件拖入画布、无关拖拽不拦截、默认文件夹选择和画布遮罩清理;主视图继续注入素材建层、文件上传、drop 点换算和素材移动高亮清理。新增 hook 单测覆盖拖拽入画布细节,主视图从 1452 行降至 1405 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 登录态刷新后素材 / 画布 / 小地图和底部工具栏可见,真实鼠标拖拽素材库图片到画布时出现 `添加到画布` 遮罩,松手后画布图层数量从 4 增至 5;`画布背景设置` 点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;抓手工具可切回选择工具,登录后控制台无前端 error。 +- 2026-06-17 前端拆分第二十阶段:新增 `ImageCanvasMetadataModalView`,把图片信息弹窗从主视图抽出,承载图片类型、生成输入、参考图、模型、分辨率、Provider、Task 和 Object 信息渲染;主视图只保留 `metadataLayer` 状态和关闭回调。同步修复未登录进入编辑器时项目 / 素材接口抢跑 401、`重置画布视图` 点击事件误传给适合视图函数的问题。新增组件单测覆盖生成图 metadata、上传图 fallback 和关闭回调,新增 hook / 主视图测试覆盖未登录不请求受保护素材 / 工程数据和重置按钮回归;主视图从 1405 行降至 1337 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后直接弹出 `账号入口`,且未登录状态下没有发起 `/api/editor/*` 请求;登录临时开发账号后 `重置画布视图` 无控制台错误,`画布背景设置` 保持 Lovart 式白色浮层,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)`,上传素材可加入画布,右上角图片信息按钮可打开不透明白底元数据弹窗,关闭后 `AI画布工具栏` 仍可见。 diff --git a/docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md b/docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md index 6a101a9d..36a61206 100644 --- a/docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md +++ b/docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md @@ -167,6 +167,15 @@ - 主视图继续提供已有能力注入:账号级素材列表、默认素材文件夹、屏幕点转画布 drop 点、素材建层、文件上传、素材移动高亮清理;drop hook 不直接创建资源、不访问 API,也不读取项目持久化状态。 - 该 hook 用独立单测覆盖素材库图片拖入画布、文件拖入画布、无关拖拽不拦截和 drag leave 清理遮罩;主视图集成测试继续覆盖真实 DOM 中的“素材库拖到画布”和“文件拖到画布”路径。 +## 第二十阶段模块 + +- `ImageCanvasMetadataModalView.tsx` + - 承载图片信息弹窗渲染:图片类型、生成输入字段、参考图、模型、分辨率、Provider、Task 和 Object 信息。 + - 主视图只保留 `metadataLayer` 状态和打开 / 关闭动作;图片信息的视觉结构、fallback 文案和 `UnifiedModal` 接入不再散在主视图底部。 + - 该组件用独立单测覆盖生成图 metadata、上传图 fallback、参考图渲染和关闭回调;主视图集成测试继续覆盖从画布图层点击右上角信息按钮打开弹窗。 + - 本阶段同步把项目 / 素材初始加载挂到 `AuthGate` 的受保护数据可访问状态之后;未登录进入编辑器只拉起 `账号入口`,不再抢跑 `/api/editor/*` 造成素材或工程读取 401 噪声。 + - `重置画布视图` 按钮必须显式调用 `onFitLayers()`,不能把 React click event 作为目标图层数组透传给适合视图逻辑。 + ## 后续阶段 - 后续可继续选择更高内聚的交互 workflow 或持久化边界,不再把生成链路继续拆成浅层 wrapper。 @@ -174,7 +183,7 @@ ## 验证计划 -- `npm run test -- src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx` +- `npm run test -- src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx` - `npm run typecheck` - `npm run check:encoding` - `git diff --check` diff --git a/src/components/image-editor/ImageCanvasEditorView.test.tsx b/src/components/image-editor/ImageCanvasEditorView.test.tsx index cfac1f31..4a71822b 100644 --- a/src/components/image-editor/ImageCanvasEditorView.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.test.tsx @@ -397,6 +397,35 @@ describe('ImageCanvasEditorView', () => { }), ); + render( + + + , + ); + + await waitFor(() => { + expect(openLoginModal).toHaveBeenCalledTimes(1); + }); + }); + + it('opens the login modal immediately when entering the editor while logged out', async () => { + const openLoginModal = vi.fn(); + render( @@ -406,6 +435,10 @@ describe('ImageCanvasEditorView', () => { await waitFor(() => { expect(openLoginModal).toHaveBeenCalledTimes(1); }); + expect(typeof openLoginModal.mock.calls[0]?.[0]).toBe('function'); + expect(loadEditorAssetLibraryMock).not.toHaveBeenCalled(); + expect(loadEditorProjectMock).not.toHaveBeenCalled(); + expect(loadOrCreateRecentEditorProjectMock).not.toHaveBeenCalled(); }); it('renames the current project from the canvas topbar', async () => { @@ -976,13 +1009,14 @@ describe('ImageCanvasEditorView', () => { new File(['image'], '登录后上传.png', { type: 'image/png' }), ]); - expect(openLoginModal).toHaveBeenCalledTimes(1); + expect(openLoginModal).toHaveBeenCalled(); expect(createEditorAssetMock).not.toHaveBeenCalled(); expect( screen.queryByRole('button', { name: '上传失败登录后上传.png' }), ).toBeNull(); - const resumeUpload = openLoginModal.mock.calls[0]?.[0]; + const resumeUpload = + openLoginModal.mock.calls[openLoginModal.mock.calls.length - 1]?.[0]; expect(typeof resumeUpload).toBe('function'); rerender( { expect(screen.queryByRole('button', { name: '画布小地图' })).toBeNull(); }); + it('resets the canvas view without forwarding the click event to fit layers', () => { + render(); + + expect(() => { + fireEvent.click(screen.getByRole('button', { name: '重置画布视图' })); + }).not.toThrow(); + }); + it('uses normal wheel for vertical canvas scroll and ctrl wheel for zoom', () => { render(); diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx index a2b99b56..d3d9da0f 100644 --- a/src/components/image-editor/ImageCanvasEditorView.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.tsx @@ -11,10 +11,10 @@ import { import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformTextField } from '../common/PlatformTextField'; -import { UnifiedModal } from '../common/UnifiedModal'; import { useAuthUi } from '../auth/AuthUiContext'; import { EditorIconButton } from './ImageCanvasEditorPrimitives'; import { ImageCanvasGenerationComposerView } from './ImageCanvasGenerationComposerView'; +import { ImageCanvasMetadataModalView } from './ImageCanvasMetadataModalView'; import { getCanvasLayersByIds, resolveContextTargetLayerIds, @@ -34,7 +34,6 @@ import { ICON_COMPOSER_HORIZONTAL_CHROME_REM, ICON_COMPOSER_MIN_WIDTH_REM, ICON_DESCRIPTION_CARD_WIDTH_REM, - formatLayerImageType, getGenerationFrameAriaLabel, getGenerationFrameLabel, getLayerKindLabel, @@ -83,6 +82,7 @@ export function ImageCanvasEditorView() { const assetListRef = useRef(null); const assetPointerDragRef = useRef(null); const authUiRef = useRef(authUi); + const hasRequestedLoginRef = useRef(false); const layerCounterRef = useRef(0); const layersRef = useRef([]); const viewportRef = useRef(DEFAULT_IMAGE_CANVAS_VIEWPORT); @@ -149,6 +149,20 @@ export function ImageCanvasEditorView() { }, [], ); + + useEffect(() => { + if (!authUi || authUi.user || authUi.canAccessProtectedData) { + hasRequestedLoginRef.current = false; + return; + } + if (hasRequestedLoginRef.current) { + return; + } + hasRequestedLoginRef.current = true; + authUi.openLoginModal(() => { + window.location.reload(); + }); + }, [authUi]); const { projectTitle, setProjectTitle, @@ -260,6 +274,7 @@ export function ImageCanvasEditorView() { handleAssetMarqueePointerUp, } = useImageCanvasAssetLibrary({ assetListRef, + canAccessProtectedData: authUi ? authUi.canAccessProtectedData : true, openEditorLoginModal, onDeleteAssets: removeCanvasLayersLinkedToAssets, }); @@ -440,6 +455,7 @@ export function ImageCanvasEditorView() { setters: projectPersistenceSetters, layers, viewport, + canAccessProtectedData: authUi ? authUi.canAccessProtectedData : true, openEditorLoginModal, }); const { @@ -1323,81 +1339,10 @@ export function ImageCanvasEditorView() { - setMetadataLayer(null)} - panelClassName="image-canvas-editor__metadata-dialog" - bodyClassName="image-canvas-editor__metadata-body" - > - {metadataLayer ? ( -
-
图片类型
-
{formatLayerImageType(metadataLayer)}
-
生成输入
-
- {metadataLayer.generationInputs?.fields.length || - metadataLayer.generationInputs?.references.length ? ( - <> - {metadataLayer.generationInputs.fields.map((field) => ( -
- - {field.title} - - {field.value} -
- ))} - {metadataLayer.generationInputs.references.length ? ( -
- {metadataLayer.generationInputs.references.map( - (reference) => ( -
- - - - {reference.title} - - {reference.label} - -
- ), - )} -
- ) : null} - - ) : ( - '-' - )} -
-
Model
-
{metadataLayer.model ?? '-'}
-
Resolution
-
- {metadataLayer.originalWidth} x {metadataLayer.originalHeight} px -
-
Provider
-
{metadataLayer.provider ?? '-'}
-
Task
-
{metadataLayer.taskId ?? '-'}
-
Object
-
- {metadataLayer.objectKey ?? metadataLayer.assetObjectId ?? '-'} -
-
- ) : null} -
+ /> ); } diff --git a/src/components/image-editor/ImageCanvasMetadataModalView.test.tsx b/src/components/image-editor/ImageCanvasMetadataModalView.test.tsx new file mode 100644 index 00000000..ba2e0efa --- /dev/null +++ b/src/components/image-editor/ImageCanvasMetadataModalView.test.tsx @@ -0,0 +1,99 @@ +/* @vitest-environment jsdom */ + +import { fireEvent, render, screen, within } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import type { CanvasLayer } from './ImageCanvasEditorTypes'; +import { ImageCanvasMetadataModalView } from './ImageCanvasMetadataModalView'; + +function createLayer(overrides: Partial = {}): CanvasLayer { + return { + id: 'layer-1', + resourceId: 'resource-1', + title: '生成主图', + src: 'data:image/png;base64,layer', + x: 0, + y: 0, + width: 320, + height: 240, + originalWidth: 1024, + originalHeight: 768, + zIndex: 1, + sourceType: 'generated', + ...overrides, + }; +} + +describe('ImageCanvasMetadataModalView', () => { + it('renders generated layer metadata with generation inputs and references', () => { + render( + , + ); + + const dialog = screen.getByRole('dialog', { name: '生成主图图片信息' }); + + expect(within(dialog).getByText('生成图片')).toBeTruthy(); + expect(within(dialog).getByText('生成提示词')).toBeTruthy(); + expect(within(dialog).getByText('清爽游戏按钮')).toBeTruthy(); + expect(within(dialog).getByText('参考图')).toBeTruthy(); + expect(within(dialog).getByText('角色立绘')).toBeTruthy(); + expect(within(dialog).getByText('gpt-image-2')).toBeTruthy(); + expect(within(dialog).getByText('1024 x 768 px')).toBeTruthy(); + expect(within(dialog).getByText('VectorEngine')).toBeTruthy(); + expect(within(dialog).getByText('task-123')).toBeTruthy(); + expect(within(dialog).getByText('generated/object.png')).toBeTruthy(); + }); + + it('renders upload fallback values and closes through the modal shell', () => { + const onClose = vi.fn(); + render( + , + ); + + const dialog = screen.getByRole('dialog', { name: '上传素材图片信息' }); + + expect(within(dialog).getByText('上传图片')).toBeTruthy(); + expect(within(dialog).getAllByText('-').length).toBeGreaterThanOrEqual(3); + expect(within(dialog).getByText('asset-object-1')).toBeTruthy(); + + fireEvent.click(within(dialog).getByRole('button', { name: '关闭图片信息' })); + + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it('does not render a dialog when no layer is selected', () => { + render(); + + expect(screen.queryByRole('dialog', { name: '图片信息' })).toBeNull(); + }); +}); diff --git a/src/components/image-editor/ImageCanvasMetadataModalView.tsx b/src/components/image-editor/ImageCanvasMetadataModalView.tsx new file mode 100644 index 00000000..6d9d6c7e --- /dev/null +++ b/src/components/image-editor/ImageCanvasMetadataModalView.tsx @@ -0,0 +1,83 @@ +import { UnifiedModal } from '../common/UnifiedModal'; +import { formatLayerImageType } from './ImageCanvasGenerationModel'; +import type { CanvasLayer } from './ImageCanvasEditorTypes'; + +type ImageCanvasMetadataModalViewProps = { + layer: CanvasLayer | null; + onClose: () => void; +}; + +export function ImageCanvasMetadataModalView({ + layer, + onClose, +}: ImageCanvasMetadataModalViewProps) { + return ( + + {layer ? ( +
+
图片类型
+
{formatLayerImageType(layer)}
+
生成输入
+
+ {layer.generationInputs?.fields.length || + layer.generationInputs?.references.length ? ( + <> + {layer.generationInputs.fields.map((field) => ( +
+ + {field.title} + + {field.value} +
+ ))} + {layer.generationInputs.references.length ? ( +
+ {layer.generationInputs.references.map((reference) => ( +
+ + + + {reference.title} + + {reference.label} + +
+ ))} +
+ ) : null} + + ) : ( + '-' + )} +
+
Model
+
{layer.model ?? '-'}
+
Resolution
+
+ {layer.originalWidth} x {layer.originalHeight} px +
+
Provider
+
{layer.provider ?? '-'}
+
Task
+
{layer.taskId ?? '-'}
+
Object
+
{layer.objectKey ?? layer.assetObjectId ?? '-'}
+
+ ) : null} +
+ ); +} diff --git a/src/components/image-editor/ImageCanvasStageView.tsx b/src/components/image-editor/ImageCanvasStageView.tsx index 5a6a2c95..73c976c8 100644 --- a/src/components/image-editor/ImageCanvasStageView.tsx +++ b/src/components/image-editor/ImageCanvasStageView.tsx @@ -784,7 +784,7 @@ export function ImageCanvasStageView({ label="重置画布视图" title="重置画布视图" icon={RotateCcw} - onClick={onFitLayers} + onClick={() => onFitLayers()} />
void) | null) => void; onDeleteAssets?: (assets: EditorAsset[]) => void; }) { const assetListRef = useRef(null); const assetLibrary = useImageCanvasAssetLibrary({ assetListRef, + canAccessProtectedData, openEditorLoginModal, onDeleteAssets, }); @@ -259,6 +262,15 @@ describe('useImageCanvasAssetLibrary', () => { }); }); + it('does not request the protected asset library before login is available', () => { + render(); + + expect(loadEditorAssetLibraryMock).not.toHaveBeenCalled(); + expect(screen.getByTestId('folders').textContent).toBe( + 'project:项目素材:false', + ); + }); + it('creates a local folder and replaces it with the persisted folder id', async () => { let resolveCreateFolder: ( folder: Awaited>, diff --git a/src/components/image-editor/useImageCanvasAssetLibrary.ts b/src/components/image-editor/useImageCanvasAssetLibrary.ts index 48e44cd9..4d650bfb 100644 --- a/src/components/image-editor/useImageCanvasAssetLibrary.ts +++ b/src/components/image-editor/useImageCanvasAssetLibrary.ts @@ -40,10 +40,12 @@ function isEditorAuthError(error: unknown) { export function useImageCanvasAssetLibrary({ assetListRef, + canAccessProtectedData, openEditorLoginModal, onDeleteAssets, }: { assetListRef: RefObject; + canAccessProtectedData: boolean; openEditorLoginModal: (postLoginAction?: (() => void) | null) => void; onDeleteAssets?: (assets: EditorAsset[]) => void; }) { @@ -94,6 +96,9 @@ export function useImageCanvasAssetLibrary({ selectableAssets.every((asset) => selectedAssetIds.has(asset.id)); useEffect(() => { + if (!canAccessProtectedData) { + return undefined; + } let cancelled = false; loadEditorAssetLibrary() .then((library) => { @@ -118,7 +123,7 @@ export function useImageCanvasAssetLibrary({ return () => { cancelled = true; }; - }, [openEditorLoginModal]); + }, [canAccessProtectedData, openEditorLoginModal]); const resolveAssetFolderId = useCallback( (clientX: number, clientY: number) => { diff --git a/src/components/image-editor/useImageCanvasProjectPersistence.test.tsx b/src/components/image-editor/useImageCanvasProjectPersistence.test.tsx index 035f6e32..ca9c0b88 100644 --- a/src/components/image-editor/useImageCanvasProjectPersistence.test.tsx +++ b/src/components/image-editor/useImageCanvasProjectPersistence.test.tsx @@ -43,7 +43,11 @@ function createLayer(id: string): CanvasLayer { }; } -function ProjectPersistenceHarness() { +function ProjectPersistenceHarness({ + canAccessProtectedData = true, +}: { + canAccessProtectedData?: boolean; +}) { const [layers, setLayers] = useState([]); const [viewport, setViewport] = useState({ x: 0, @@ -79,6 +83,7 @@ function ProjectPersistenceHarness() { }, layers, viewport, + canAccessProtectedData, openEditorLoginModal: vi.fn(), }); @@ -169,4 +174,12 @@ describe('useImageCanvasProjectPersistence', () => { ); }); }); + + it('does not load protected project data before login is available', () => { + render(); + + expect(loadOrCreateRecentEditorProjectMock).not.toHaveBeenCalled(); + expect(loadEditorProjectMock).not.toHaveBeenCalled(); + expect(screen.getByTestId('project-id').textContent).toBe('-'); + }); }); diff --git a/src/components/image-editor/useImageCanvasProjectPersistence.ts b/src/components/image-editor/useImageCanvasProjectPersistence.ts index d5144a19..0592bbd8 100644 --- a/src/components/image-editor/useImageCanvasProjectPersistence.ts +++ b/src/components/image-editor/useImageCanvasProjectPersistence.ts @@ -46,12 +46,14 @@ export function useImageCanvasProjectPersistence({ setters, layers, viewport, + canAccessProtectedData, openEditorLoginModal, }: { refs: ImageCanvasProjectPersistenceRefs; setters: ImageCanvasProjectPersistenceSetters; layers: CanvasLayer[]; viewport: CanvasViewport; + canAccessProtectedData: boolean; openEditorLoginModal: (postLoginAction?: (() => void) | null) => void; }) { const projectIdRef = useRef(null); @@ -145,6 +147,10 @@ export function useImageCanvasProjectPersistence({ ); useEffect(() => { + if (!canAccessProtectedData) { + setIsProjectReady(false); + return undefined; + } let cancelled = false; const projectIdFromQuery = typeof window === 'undefined' @@ -202,7 +208,12 @@ export function useImageCanvasProjectPersistence({ return () => { cancelled = true; }; - }, [createProjectResourceForLayer, openEditorLoginModal, setters]); + }, [ + canAccessProtectedData, + createProjectResourceForLayer, + openEditorLoginModal, + setters, + ]); useEffect(() => { if (!projectId || !isProjectReady) {