Skip to content

Commit 412b896

Browse files
committed
🤖 refactor: WorkspaceProvider calls useProjectContext internally
Eliminated onProjectsUpdate prop by having WorkspaceProvider call useProjectContext() directly. This removes the need for AppLoaderMiddle and simplifies the component tree. Benefits: - WorkspaceProvider is self-contained - No callback prop drilling - Cleaner AppLoader structure - ~30 net lines removed
1 parent 115f7bb commit 412b896

File tree

3 files changed

+41
-103
lines changed

3 files changed

+41
-103
lines changed

src/components/AppLoader.tsx

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,34 +14,20 @@ import { WorkspaceProvider, useWorkspaceContext } from "../contexts/WorkspaceCon
1414
* 3. Only render App when everything is ready
1515
*
1616
* WorkspaceContext handles workspace selection restoration (localStorage, URL hash, launch project).
17+
* WorkspaceProvider must be nested inside ProjectProvider so it can call useProjectContext().
1718
* This ensures App.tsx can assume stores are always synced and removes
1819
* the need for conditional guards in effects.
1920
*/
2021
export function AppLoader() {
2122
return (
2223
<ProjectProvider>
23-
<AppLoaderMiddle />
24+
<WorkspaceProvider>
25+
<AppLoaderInner />
26+
</WorkspaceProvider>
2427
</ProjectProvider>
2528
);
2629
}
2730

28-
/**
29-
* Middle component that has access to ProjectContext and wraps WorkspaceProvider
30-
*/
31-
function AppLoaderMiddle() {
32-
const { refreshProjects } = useProjectContext();
33-
34-
return (
35-
<WorkspaceProvider
36-
onProjectsUpdate={() => {
37-
void refreshProjects();
38-
}}
39-
>
40-
<AppLoaderInner />
41-
</WorkspaceProvider>
42-
);
43-
}
44-
4531
/**
4632
* Inner component that has access to both ProjectContext and WorkspaceContext.
4733
* Syncs stores and shows loading screen until ready.

src/contexts/WorkspaceContext.test.tsx

Lines changed: 24 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,8 @@ describe("WorkspaceContext", () => {
6666
list: () => Promise.resolve([]),
6767
});
6868

69-
const onProjectsUpdate = mock(() => {});
7069

71-
const ctx = await setup({
72-
onProjectsUpdate,
73-
});
70+
const ctx = await setup();
7471

7572
await waitFor(() => expect(ctx().workspaceMetadata.size).toBe(2));
7673
expect(workspaceApi.list).toHaveBeenCalled();
@@ -88,11 +85,8 @@ describe("WorkspaceContext", () => {
8885
list: () => Promise.resolve([]),
8986
});
9087

91-
const onProjectsUpdate = mock(() => {});
9288

93-
const ctx = await setup({
94-
onProjectsUpdate,
95-
});
89+
const ctx = await setup();
9690

9791
// Should have empty workspaces after failed load
9892
await waitFor(() => {
@@ -137,11 +131,8 @@ describe("WorkspaceContext", () => {
137131
list: () => Promise.resolve([]),
138132
});
139133

140-
const onProjectsUpdate = mock(() => {});
141134

142-
const ctx = await setup({
143-
onProjectsUpdate,
144-
});
135+
const ctx = await setup();
145136

146137
await waitFor(() => expect(ctx().workspaceMetadata.size).toBe(1));
147138

@@ -176,11 +167,8 @@ describe("WorkspaceContext", () => {
176167
list: () => Promise.resolve([]),
177168
});
178169

179-
const onProjectsUpdate = mock(() => {});
180170

181-
const ctx = await setup({
182-
onProjectsUpdate,
183-
});
171+
const ctx = await setup();
184172

185173
await waitFor(() => expect(ctx().loading).toBe(false));
186174

@@ -210,11 +198,8 @@ describe("WorkspaceContext", () => {
210198
list: () => Promise.resolve([]),
211199
});
212200

213-
const onProjectsUpdate = mock(() => {});
214201

215-
const ctx = await setup({
216-
onProjectsUpdate,
217-
});
202+
const ctx = await setup();
218203

219204
await waitFor(() => expect(ctx().loading).toBe(false));
220205

@@ -244,11 +229,8 @@ describe("WorkspaceContext", () => {
244229
list: () => Promise.resolve([]),
245230
});
246231

247-
const onProjectsUpdate = mock(() => {});
248232

249-
const ctx = await setup({
250-
onProjectsUpdate,
251-
});
233+
const ctx = await setup();
252234

253235
await waitFor(() => expect(ctx().loading).toBe(false));
254236

@@ -294,11 +276,8 @@ describe("WorkspaceContext", () => {
294276
list: () => Promise.resolve([]),
295277
});
296278

297-
const onProjectsUpdate = mock(() => {});
298279

299-
const ctx = await setup({
300-
onProjectsUpdate,
301-
});
280+
const ctx = await setup();
302281

303282
await waitFor(() => expect(ctx().loading).toBe(false));
304283

@@ -350,11 +329,8 @@ describe("WorkspaceContext", () => {
350329
list: () => Promise.resolve([]),
351330
});
352331

353-
const onProjectsUpdate = mock(() => {});
354332

355-
const ctx = await setup({
356-
onProjectsUpdate,
357-
});
333+
const ctx = await setup();
358334

359335
await waitFor(() => expect(ctx().loading).toBe(false));
360336

@@ -406,11 +382,8 @@ describe("WorkspaceContext", () => {
406382
list: () => Promise.resolve([]),
407383
});
408384

409-
const onProjectsUpdate = mock(() => {});
410385

411-
const ctx = await setup({
412-
onProjectsUpdate,
413-
});
386+
const ctx = await setup();
414387

415388
await waitFor(() => expect(ctx().loading).toBe(false));
416389

@@ -448,11 +421,8 @@ describe("WorkspaceContext", () => {
448421
list: () => Promise.resolve([]),
449422
});
450423

451-
const onProjectsUpdate = mock(() => {});
452424

453-
const ctx = await setup({
454-
onProjectsUpdate,
455-
});
425+
const ctx = await setup();
456426

457427
await waitFor(() => expect(ctx().loading).toBe(false));
458428

@@ -470,11 +440,8 @@ describe("WorkspaceContext", () => {
470440
list: () => Promise.resolve([]),
471441
});
472442

473-
const onProjectsUpdate = mock(() => {});
474443

475-
const ctx = await setup({
476-
onProjectsUpdate,
477-
});
444+
const ctx = await setup();
478445

479446
await waitFor(() => expect(ctx().loading).toBe(false));
480447

@@ -510,11 +477,8 @@ describe("WorkspaceContext", () => {
510477
list: () => Promise.resolve([]),
511478
});
512479

513-
const onProjectsUpdate = mock(() => {});
514480

515-
const ctx = await setup({
516-
onProjectsUpdate,
517-
});
481+
const ctx = await setup();
518482

519483
await waitFor(() => expect(ctx().loading).toBe(false));
520484

@@ -566,11 +530,8 @@ describe("WorkspaceContext", () => {
566530
list: () => Promise.resolve([]),
567531
});
568532

569-
const onProjectsUpdate = mock(() => {});
570533

571-
const ctx = await setup({
572-
onProjectsUpdate,
573-
});
534+
const ctx = await setup();
574535

575536
await waitFor(() => expect(ctx().workspaceMetadata.has("ws-1")).toBe(true));
576537

@@ -598,11 +559,8 @@ describe("WorkspaceContext", () => {
598559
list: () => Promise.resolve([]),
599560
});
600561

601-
const onProjectsUpdate = mock(() => {});
602562

603-
const ctx = await setup({
604-
onProjectsUpdate,
605-
});
563+
const ctx = await setup();
606564

607565
await waitFor(() => expect(ctx().workspaceMetadata.size).toBe(1));
608566

@@ -611,16 +569,22 @@ describe("WorkspaceContext", () => {
611569
});
612570
});
613571

614-
async function setup(props: { onProjectsUpdate: (projects: Map<string, ProjectConfig>) => void }) {
572+
async function setup() {
615573
const contextRef = { current: null as WorkspaceContext | null };
616574
function ContextCapture() {
617575
contextRef.current = useWorkspaceContext();
618576
return null;
619577
}
578+
579+
// WorkspaceProvider needs ProjectProvider to call useProjectContext
580+
const { ProjectProvider } = await import("@/contexts/ProjectContext");
581+
620582
render(
621-
<WorkspaceProvider onProjectsUpdate={props.onProjectsUpdate}>
622-
<ContextCapture />
623-
</WorkspaceProvider>
583+
<ProjectProvider>
584+
<WorkspaceProvider>
585+
<ContextCapture />
586+
</WorkspaceProvider>
587+
</ProjectProvider>
624588
);
625589
await waitFor(() => expect(contextRef.current).toBeTruthy());
626590
return () => contextRef.current!;

src/contexts/WorkspaceContext.tsx

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {
1010
import type { FrontendWorkspaceMetadata } from "@/types/workspace";
1111
import type { WorkspaceSelection } from "@/components/ProjectSidebar";
1212
import type { RuntimeConfig } from "@/types/runtime";
13-
import type { ProjectConfig } from "@/config";
1413
import { deleteWorkspaceStorage } from "@/constants/storage";
1514
import { usePersistedState } from "@/hooks/usePersistedState";
15+
import { useProjectContext } from "@/contexts/ProjectContext";
1616

1717
/**
1818
* Ensure workspace metadata has createdAt timestamp.
@@ -75,10 +75,12 @@ const WorkspaceContext = createContext<WorkspaceContext | undefined>(undefined);
7575

7676
interface WorkspaceProviderProps {
7777
children: ReactNode;
78-
onProjectsUpdate: (projects: Map<string, ProjectConfig>) => void;
7978
}
8079

8180
export function WorkspaceProvider(props: WorkspaceProviderProps) {
81+
// Get project refresh function from ProjectContext
82+
const { refreshProjects } = useProjectContext();
83+
8284
const [workspaceMetadata, setWorkspaceMetadata] = useState<
8385
Map<string, FrontendWorkspaceMetadata>
8486
>(new Map());
@@ -109,17 +111,14 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {
109111

110112
// Load metadata once on mount
111113
useEffect(() => {
112-
const { onProjectsUpdate } = props;
113114
void (async () => {
114115
await loadWorkspaceMetadata();
115116
// After loading metadata (which may trigger migration), reload projects
116117
// to ensure frontend has the updated config with workspace IDs
117-
const projectsList = await window.api.projects.list();
118-
const loadedProjects = new Map<string, ProjectConfig>(projectsList);
119-
onProjectsUpdate(loadedProjects);
118+
await refreshProjects();
120119
setLoading(false);
121120
})();
122-
}, [loadWorkspaceMetadata, props]);
121+
}, [loadWorkspaceMetadata, refreshProjects]);
123122

124123
// Restore workspace from URL hash (overrides localStorage)
125124
// Runs once after metadata is loaded
@@ -188,7 +187,6 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {
188187

189188
// Subscribe to metadata updates (for create/rename/delete operations)
190189
useEffect(() => {
191-
const { onProjectsUpdate } = props;
192190
const unsubscribe = window.api.workspace.onMetadata(
193191
(event: { workspaceId: string; metadata: FrontendWorkspaceMetadata | null }) => {
194192
setWorkspaceMetadata((prev) => {
@@ -206,11 +204,7 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {
206204
// If this is a new workspace (e.g., from fork), reload projects
207205
// to ensure the sidebar shows the updated workspace list
208206
if (isNewWorkspace) {
209-
void (async () => {
210-
const projectsList = await window.api.projects.list();
211-
const loadedProjects = new Map<string, ProjectConfig>(projectsList);
212-
onProjectsUpdate(loadedProjects);
213-
})();
207+
void refreshProjects();
214208
}
215209

216210
return updated;
@@ -221,7 +215,7 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {
221215
return () => {
222216
unsubscribe();
223217
};
224-
}, [props]);
218+
}, [refreshProjects]);
225219

226220
const createWorkspace = useCallback(
227221
async (
@@ -242,9 +236,7 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {
242236
);
243237
if (result.success) {
244238
// Backend has already updated the config - reload projects to get updated state
245-
const projectsList = await window.api.projects.list();
246-
const loadedProjects = new Map<string, ProjectConfig>(projectsList);
247-
props.onProjectsUpdate(loadedProjects);
239+
await refreshProjects();
248240

249241
// Update metadata immediately to avoid race condition with validation effect
250242
ensureCreatedAt(result.metadata);
@@ -280,9 +272,7 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {
280272
deleteWorkspaceStorage(workspaceId);
281273

282274
// Backend has already updated the config - reload projects to get updated state
283-
const projectsList = await window.api.projects.list();
284-
const loadedProjects = new Map<string, ProjectConfig>(projectsList);
285-
props.onProjectsUpdate(loadedProjects);
275+
await refreshProjects();
286276

287277
// Reload workspace metadata
288278
await loadWorkspaceMetadata();
@@ -302,7 +292,7 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {
302292
return { success: false, error: errorMessage };
303293
}
304294
},
305-
[loadWorkspaceMetadata, props, selectedWorkspace, setSelectedWorkspace]
295+
[loadWorkspaceMetadata, refreshProjects, selectedWorkspace, setSelectedWorkspace]
306296
);
307297

308298
const renameWorkspace = useCallback(
@@ -311,9 +301,7 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {
311301
const result = await window.api.workspace.rename(workspaceId, newName);
312302
if (result.success) {
313303
// Backend has already updated the config - reload projects to get updated state
314-
const projectsList = await window.api.projects.list();
315-
const loadedProjects = new Map<string, ProjectConfig>(projectsList);
316-
props.onProjectsUpdate(loadedProjects);
304+
await refreshProjects();
317305

318306
// Reload workspace metadata
319307
await loadWorkspaceMetadata();
@@ -345,7 +333,7 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {
345333
return { success: false, error: errorMessage };
346334
}
347335
},
348-
[loadWorkspaceMetadata, props, selectedWorkspace, setSelectedWorkspace]
336+
[loadWorkspaceMetadata, refreshProjects, selectedWorkspace, setSelectedWorkspace]
349337
);
350338

351339
const refreshWorkspaceMetadata = useCallback(async () => {

0 commit comments

Comments
 (0)