重构: replace canvas projectId prop drilling with zustand

This commit is contained in:
2026-07-07 12:21:32 +08:00
parent 4622b35a32
commit 981312520d
11 changed files with 40 additions and 32 deletions
+2 -1
View File
@@ -150,7 +150,8 @@
"react-native-safe-area-context": "^5.8.0",
"react-native-webview": "^13.16.1",
"three": "^0.184.0",
"vite": "^6.2.0"
"vite": "^6.2.0",
"zustand": "^5.0.14"
},
"devDependencies": {
"@colbymchenry/codegraph": "^0.8.0",
@@ -8,9 +8,10 @@ import {
waitFor,
within,
} from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { EditorAgentConversationPanelView } from './EditorAgentConversationPanelView';
import { useImageCanvasContextStore } from './useImageCanvasContextStore.ts';
import type { EditorAgentConversationClient } from './useEditorAgentConversation';
function createClient(): EditorAgentConversationClient {
@@ -85,12 +86,15 @@ function createClient(): EditorAgentConversationClient {
}
describe('EditorAgentConversationPanelView', () => {
beforeEach(() => {
useImageCanvasContextStore.getState().setProjectId('project-1');
});
it('manages conversations, attachments and message sending inside the panel', async () => {
const client = createClient();
render(
<EditorAgentConversationPanelView
projectId="project-1"
open
onToggleOpen={vi.fn()}
client={client}
@@ -196,7 +200,6 @@ describe('EditorAgentConversationPanelView', () => {
render(
<EditorAgentConversationPanelView
projectId="project-1"
open
onToggleOpen={vi.fn()}
client={client}
@@ -292,7 +295,6 @@ describe('EditorAgentConversationPanelView', () => {
const { rerender } = render(
<EditorAgentConversationPanelView
projectId="project-1"
open
onToggleOpen={vi.fn()}
client={client}
@@ -316,7 +318,6 @@ describe('EditorAgentConversationPanelView', () => {
rerender(
<EditorAgentConversationPanelView
projectId="project-1"
open={false}
onToggleOpen={vi.fn()}
client={client}
@@ -326,7 +327,6 @@ describe('EditorAgentConversationPanelView', () => {
rerender(
<EditorAgentConversationPanelView
projectId="project-1"
open
onToggleOpen={vi.fn()}
client={client}
@@ -349,7 +349,6 @@ describe('EditorAgentConversationPanelView', () => {
render(
<div onWheel={parentWheel}>
<EditorAgentConversationPanelView
projectId="project-1"
open
onToggleOpen={vi.fn()}
client={client}
@@ -387,7 +386,6 @@ describe('EditorAgentConversationPanelView', () => {
const client = createClient();
render(
<EditorAgentConversationPanelView
projectId="project-1"
open
onToggleOpen={vi.fn()}
client={client}
@@ -45,7 +45,6 @@ type EditorAgentAttachmentOption = {
};
type EditorAgentConversationPanelViewProps = {
projectId?: string | null;
open: boolean;
onToggleOpen: () => void;
layers?: CanvasLayer[];
@@ -404,7 +403,6 @@ function AttachmentPickerModal({
}
export function EditorAgentConversationPanelView({
projectId,
open,
onToggleOpen,
layers = [],
@@ -418,6 +416,9 @@ export function EditorAgentConversationPanelView({
setHasConversationMounted(true);
}
}, [open]);
const projectId = useImageCanvasContextStore(
(state) => state.projectId,
);
const effectiveProjectId = hasConversationMounted ? projectId : null;
const {
conversations,
@@ -93,7 +93,6 @@ function createSidebarProps(): ImageCanvasSidebarViewProps {
function createTopbarProps(): ImageCanvasTopbarViewProps {
return {
projectId: 'project-1',
projectTitle: '默认项目',
projectRenameValue: '默认项目',
isRenamingProject: false,
@@ -119,7 +118,6 @@ function createTopbarProps(): ImageCanvasTopbarViewProps {
function createStageProps(): ImageCanvasStageViewProps {
return {
projectId: 'project-1',
canvasViewportRef: createRef<HTMLDivElement>(),
specToolWrapRef: createRef<HTMLSpanElement>(),
musicToolWrapRef: createRef<HTMLSpanElement>(),
@@ -81,6 +81,7 @@ import { useImageCanvasEditorChrome } from './useImageCanvasEditorChrome';
import { useImageCanvasGenerationSurface } from './useImageCanvasGenerationSurface';
import { useImageCanvasKeyboardShortcuts } from './useImageCanvasKeyboardShortcuts';
import { useImageCanvasLayerCommands } from './useImageCanvasLayerCommands';
import { useImageCanvasContextStore } from './useImageCanvasContextStore.ts';
import { useImageCanvasProjectPersistence } from './useImageCanvasProjectPersistence';
import { useImageCanvasStageController } from './useImageCanvasStageController';
import { useImageCanvasStageInteractions } from './useImageCanvasStageInteractions';
@@ -1015,6 +1016,12 @@ export function ImageCanvasEditorView({
openEditorLoginModal,
onProjectAccessLost,
});
const setEditorProjectContextId = useImageCanvasContextStore(
(state) => state.setProjectId,
);
useEffect(() => {
setEditorProjectContextId(projectId);
}, [projectId, setEditorProjectContextId]);
const applyGeneratedProjectSnapshot = useCallback(
(project: EditorProjectSnapshot) => {
applyProjectSnapshot(project);
@@ -1890,7 +1897,6 @@ export function ImageCanvasEditorView({
getCanvasPointFromClient,
};
const topbarProps = {
projectId,
projectTitle,
projectRenameValue,
isRenamingProject,
@@ -1919,7 +1925,6 @@ export function ImageCanvasEditorView({
},
};
const stageProps = {
projectId,
canvasViewportRef,
specToolWrapRef,
musicToolWrapRef,
@@ -41,7 +41,6 @@ import { ImageCanvasUiAssetExtractionOverlayView } from './ImageCanvasUiAssetExt
import { ImageCanvasWorldView } from './ImageCanvasWorldView';
export type ImageCanvasStageViewProps = {
projectId?: string | null;
canvasViewportRef: RefObject<HTMLDivElement | null>;
specToolWrapRef: RefObject<HTMLSpanElement | null>;
musicToolWrapRef: RefObject<HTMLSpanElement | null>;
@@ -192,7 +191,6 @@ export type ImageCanvasStageViewProps = {
};
export function ImageCanvasStageView({
projectId,
canvasViewportRef,
specToolWrapRef,
musicToolWrapRef,
@@ -463,7 +461,6 @@ export function ImageCanvasStageView({
/>
<ImageCanvasTaskSidebarView
projectId={projectId}
refreshKey={taskListRefreshKey}
open={isTaskSidebarOpen}
onToggleOpen={onToggleTaskSidebar}
@@ -472,7 +469,6 @@ export function ImageCanvasStageView({
{isAgentConversationEnabled ? (
<EditorAgentConversationPanelView
projectId={projectId}
open={isAgentConversationOpen}
onToggleOpen={onToggleAgentConversation}
layers={layers}
@@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { ExternalGenerationTaskRecord } from '../../../packages/shared/src/contracts/externalGeneration';
import { listExternalGenerationTasks } from '../../services/external-generation';
import { ImageCanvasTaskSidebarView } from './ImageCanvasTaskSidebarView';
import { useImageCanvasContextStore } from './useImageCanvasContextStore.ts';
vi.mock('../../services/external-generation', () => ({
listExternalGenerationTasks: vi.fn().mockResolvedValue({
@@ -22,6 +23,7 @@ vi.mock('../../services/external-generation', () => ({
const listExternalGenerationTasksMock = vi.mocked(listExternalGenerationTasks);
beforeEach(() => {
useImageCanvasContextStore.getState().setProjectId('project-1');
listExternalGenerationTasksMock.mockClear();
listExternalGenerationTasksMock.mockResolvedValue({
overview: {
@@ -115,7 +117,6 @@ describe('ImageCanvasTaskSidebarView', () => {
render(
<div onWheel={parentWheel}>
<ImageCanvasTaskSidebarView
projectId="project-1"
open
onToggleOpen={vi.fn()}
onFocusExternalTask={focusExternalTask}
@@ -230,7 +231,6 @@ describe('ImageCanvasTaskSidebarView', () => {
render(
<ImageCanvasTaskSidebarView
projectId="project-1"
open
onToggleOpen={vi.fn()}
onFocusExternalTask={vi.fn()}
@@ -287,7 +287,6 @@ describe('ImageCanvasTaskSidebarView', () => {
render(
<ImageCanvasTaskSidebarView
projectId="project-1"
open
onToggleOpen={vi.fn()}
onFocusExternalTask={vi.fn()}
@@ -325,7 +324,6 @@ describe('ImageCanvasTaskSidebarView', () => {
const { rerender } = render(
<ImageCanvasTaskSidebarView
projectId="project-1"
refreshKey={0}
open
onToggleOpen={vi.fn()}
@@ -347,7 +345,6 @@ describe('ImageCanvasTaskSidebarView', () => {
];
rerender(
<ImageCanvasTaskSidebarView
projectId="project-1"
refreshKey={1}
open
onToggleOpen={vi.fn()}
@@ -388,7 +385,6 @@ describe('ImageCanvasTaskSidebarView', () => {
render(
<ImageCanvasTaskSidebarView
projectId="project-1"
open
onToggleOpen={vi.fn()}
onFocusExternalTask={vi.fn()}
@@ -19,6 +19,7 @@ import type { ExternalGenerationTaskRecord } from '../../../packages/shared/src/
import { listExternalGenerationTasks } from '../../services/external-generation';
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
import type { CanvasTaskStatus } from './ImageCanvasEditorTypes';
import { useImageCanvasContextStore } from './useImageCanvasContextStore.ts';
const COMPLETED_TASK_LIST_LIMIT = 20;
const COMPLETED_TASK_LIST_MAX_LIMIT = 100;
@@ -42,7 +43,6 @@ type TaskSidebarItem = {
};
type ImageCanvasTaskSidebarViewProps = {
projectId?: string | null;
refreshKey?: number;
open: boolean;
onToggleOpen: () => void;
@@ -252,12 +252,12 @@ function trimStoredExternalTasks(
}
export function ImageCanvasTaskSidebarView({
projectId,
refreshKey = 0,
open,
onToggleOpen,
onFocusExternalTask,
}: ImageCanvasTaskSidebarViewProps) {
const projectId = useImageCanvasContextStore((state) => state.projectId);
const normalizedProjectId = projectId?.trim() ?? '';
const [activeTab, setActiveTab] = useState<TaskSidebarTab>('active');
const [now, setNow] = useState(() => Date.now());
@@ -5,6 +5,7 @@ import { describe, expect, it, vi } from 'vitest';
import type { CanvasLayer } from './ImageCanvasEditorTypes';
import { ImageCanvasTopbarView } from './ImageCanvasTopbarView';
import { useImageCanvasContextStore } from './useImageCanvasContextStore.ts';
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
const id = overrides.id ?? 'layer-a';
@@ -30,8 +31,8 @@ function renderTopbar(
Parameters<typeof ImageCanvasTopbarView>[0]
> = {},
) {
useImageCanvasContextStore.getState().setProjectId('project-a');
const props: Parameters<typeof ImageCanvasTopbarView>[0] = {
projectId: 'project-a',
projectTitle: '默认项目',
projectRenameValue: '默认项目',
isRenamingProject: false,
@@ -144,7 +145,6 @@ describe('ImageCanvasTopbarView', () => {
const exportCanvasAssets = vi.fn();
const { rerender } = render(
<ImageCanvasTopbarView
projectId="project-a"
projectTitle="默认项目"
projectRenameValue="默认项目"
isRenamingProject={false}
@@ -175,7 +175,6 @@ describe('ImageCanvasTopbarView', () => {
rerender(
<ImageCanvasTopbarView
projectId="project-a"
projectTitle="默认项目"
projectRenameValue="默认项目"
isRenamingProject={false}
@@ -12,9 +12,9 @@ import { PlatformTextField } from '../common/PlatformTextField';
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
import type { CanvasLayer } from './ImageCanvasEditorTypes';
import type { AssetExportStatus } from './useImageCanvasAssetExportWorkflow';
import { useImageCanvasContextStore } from './useImageCanvasContextStore.ts';
export type ImageCanvasTopbarViewProps = {
projectId: string | null;
projectTitle: string;
projectRenameValue: string;
isRenamingProject: boolean;
@@ -57,7 +57,6 @@ function buildCanvasUserCode(
}
export function ImageCanvasTopbarView({
projectId,
projectTitle,
projectRenameValue,
isRenamingProject,
@@ -79,6 +78,7 @@ export function ImageCanvasTopbarView({
onOpenWallet,
onOpenAccount,
}: ImageCanvasTopbarViewProps) {
const projectId = useImageCanvasContextStore((state) => state.projectId);
const hasExportableLayer = layers.some(
(layer) => layer.src.trim().length > 0,
);
@@ -0,0 +1,14 @@
import { create } from 'zustand';
type ImageCanvasContextState = {
projectId: string | null;
setProjectId: (projectId?: string | null) => void;
};
export const useImageCanvasContextStore =
create<ImageCanvasContextState>((set) => ({
projectId: null,
setProjectId: (projectId) => {
set({ projectId: projectId?.trim() || null });
},
}));