Skip to content

Commit 9ed7a53

Browse files
committed
🤖 refactor: split workspaces into WorkspaceContext with tests
Following the pattern from PR #600 (projects split), this moves workspace state management from useWorkspaceManagement hook into a dedicated WorkspaceContext. - Created WorkspaceContext with workspace metadata, operations (create/remove/rename), and selection state - Comprehensive test coverage (14 tests) matching ProjectContext test patterns - Updated AppLoader to use WorkspaceProvider wrapper - Maintains same API surface for existing components via AppContext pass-through - Tests verify metadata loading, CRUD operations, event subscriptions, and error handling This eliminates prop drilling and centralizes workspace state management, making it easier to test and maintain workspace operations independently.
1 parent 0cf1057 commit 9ed7a53

File tree

3 files changed

+1096
-41
lines changed

3 files changed

+1096
-41
lines changed

src/components/AppLoader.tsx

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { useState, useEffect } from "react";
22
import App from "../App";
33
import { LoadingScreen } from "./LoadingScreen";
4-
import { useWorkspaceManagement } from "../hooks/useWorkspaceManagement";
54
import { useWorkspaceStoreRaw } from "../stores/WorkspaceStore";
65
import { useGitStatusStoreRaw } from "../stores/GitStatusStore";
76
import { usePersistedState } from "../hooks/usePersistedState";
87
import type { WorkspaceSelection } from "./ProjectSidebar";
98
import { AppProvider } from "../contexts/AppContext";
109
import { ProjectProvider, useProjectContext } from "../contexts/ProjectContext";
10+
import { WorkspaceProvider, useWorkspaceContext } from "../contexts/WorkspaceContext";
1111

1212
/**
1313
* AppLoader handles all initialization before rendering the main App:
@@ -22,12 +22,12 @@ import { ProjectProvider, useProjectContext } from "../contexts/ProjectContext";
2222
export function AppLoader() {
2323
return (
2424
<ProjectProvider>
25-
<AppLoaderInner />
25+
<AppLoaderMiddle />
2626
</ProjectProvider>
2727
);
2828
}
2929

30-
function AppLoaderInner() {
30+
function AppLoaderMiddle() {
3131
// Workspace selection - restored from localStorage immediately
3232
const [selectedWorkspace, setSelectedWorkspace] = usePersistedState<WorkspaceSelection | null>(
3333
"selectedWorkspace",
@@ -36,13 +36,26 @@ function AppLoaderInner() {
3636

3737
const { refreshProjects } = useProjectContext();
3838

39-
// Load workspace metadata
40-
// Pass empty callbacks for now - App will provide the actual handlers
41-
const workspaceManagement = useWorkspaceManagement({
42-
selectedWorkspace,
43-
onProjectsRefresh: refreshProjects,
44-
onSelectedWorkspaceUpdate: setSelectedWorkspace,
45-
});
39+
// Wrap with WorkspaceProvider
40+
return (
41+
<WorkspaceProvider
42+
selectedWorkspace={selectedWorkspace}
43+
onSelectedWorkspaceUpdate={setSelectedWorkspace}
44+
onProjectsUpdate={refreshProjects}
45+
>
46+
<AppLoaderInner
47+
selectedWorkspace={selectedWorkspace}
48+
setSelectedWorkspace={setSelectedWorkspace}
49+
/>
50+
</WorkspaceProvider>
51+
);
52+
}
53+
54+
function AppLoaderInner(props: {
55+
selectedWorkspace: WorkspaceSelection | null;
56+
setSelectedWorkspace: (workspace: WorkspaceSelection | null) => void;
57+
}) {
58+
const workspaceContext = useWorkspaceContext();
4659

4760
// Get store instances
4861
const workspaceStore = useWorkspaceStoreRaw();
@@ -53,16 +66,16 @@ function AppLoaderInner() {
5366

5467
// Sync stores when metadata finishes loading
5568
useEffect(() => {
56-
if (!workspaceManagement.loading) {
57-
workspaceStore.syncWorkspaces(workspaceManagement.workspaceMetadata);
58-
gitStatusStore.syncWorkspaces(workspaceManagement.workspaceMetadata);
69+
if (!workspaceContext.loading) {
70+
workspaceStore.syncWorkspaces(workspaceContext.workspaceMetadata);
71+
gitStatusStore.syncWorkspaces(workspaceContext.workspaceMetadata);
5972
setStoresSynced(true);
6073
} else {
6174
setStoresSynced(false);
6275
}
6376
}, [
64-
workspaceManagement.loading,
65-
workspaceManagement.workspaceMetadata,
77+
workspaceContext.loading,
78+
workspaceContext.workspaceMetadata,
6679
workspaceStore,
6780
gitStatusStore,
6881
]);
@@ -82,11 +95,11 @@ function AppLoaderInner() {
8295
const workspaceId = decodeURIComponent(hash.substring("#workspace=".length));
8396

8497
// Find workspace in metadata
85-
const metadata = workspaceManagement.workspaceMetadata.get(workspaceId);
98+
const metadata = workspaceContext.workspaceMetadata.get(workspaceId);
8699

87100
if (metadata) {
88101
// Restore from hash (overrides localStorage)
89-
setSelectedWorkspace({
102+
props.setSelectedWorkspace({
90103
workspaceId: metadata.id,
91104
projectPath: metadata.projectPath,
92105
projectName: metadata.projectName,
@@ -96,12 +109,7 @@ function AppLoaderInner() {
96109
}
97110

98111
setHasRestoredFromHash(true);
99-
}, [
100-
storesSynced,
101-
workspaceManagement.workspaceMetadata,
102-
hasRestoredFromHash,
103-
setSelectedWorkspace,
104-
]);
112+
}, [storesSynced, workspaceContext.workspaceMetadata, hasRestoredFromHash, props]);
105113

106114
// Check for launch project from server (for --add-project flag)
107115
// This only applies in server mode
@@ -110,7 +118,7 @@ function AppLoaderInner() {
110118
if (!storesSynced || !hasRestoredFromHash) return;
111119

112120
// Skip if we already have a selected workspace (from localStorage or URL hash)
113-
if (selectedWorkspace) return;
121+
if (props.selectedWorkspace) return;
114122

115123
// Only check once
116124
const checkLaunchProject = async () => {
@@ -121,14 +129,14 @@ function AppLoaderInner() {
121129
if (!launchProjectPath) return;
122130

123131
// Find first workspace in this project
124-
const projectWorkspaces = Array.from(workspaceManagement.workspaceMetadata.values()).filter(
132+
const projectWorkspaces = Array.from(workspaceContext.workspaceMetadata.values()).filter(
125133
(meta) => meta.projectPath === launchProjectPath
126134
);
127135

128136
if (projectWorkspaces.length > 0) {
129137
// Select the first workspace in the project
130138
const metadata = projectWorkspaces[0];
131-
setSelectedWorkspace({
139+
props.setSelectedWorkspace({
132140
workspaceId: metadata.id,
133141
projectPath: metadata.projectPath,
134142
projectName: metadata.projectName,
@@ -140,29 +148,23 @@ function AppLoaderInner() {
140148
};
141149

142150
void checkLaunchProject();
143-
}, [
144-
storesSynced,
145-
hasRestoredFromHash,
146-
selectedWorkspace,
147-
workspaceManagement.workspaceMetadata,
148-
setSelectedWorkspace,
149-
]);
151+
}, [storesSynced, hasRestoredFromHash, workspaceContext.workspaceMetadata, props]);
150152

151153
// Show loading screen until stores are synced
152-
if (workspaceManagement.loading || !storesSynced) {
154+
if (workspaceContext.loading || !storesSynced) {
153155
return <LoadingScreen />;
154156
}
155157

156158
// Render App with all initialized data via context
157159
return (
158160
<AppProvider
159-
workspaceMetadata={workspaceManagement.workspaceMetadata}
160-
setWorkspaceMetadata={workspaceManagement.setWorkspaceMetadata}
161-
createWorkspace={workspaceManagement.createWorkspace}
162-
removeWorkspace={workspaceManagement.removeWorkspace}
163-
renameWorkspace={workspaceManagement.renameWorkspace}
164-
selectedWorkspace={selectedWorkspace}
165-
setSelectedWorkspace={setSelectedWorkspace}
161+
workspaceMetadata={workspaceContext.workspaceMetadata}
162+
setWorkspaceMetadata={workspaceContext.setWorkspaceMetadata}
163+
createWorkspace={workspaceContext.createWorkspace}
164+
removeWorkspace={workspaceContext.removeWorkspace}
165+
renameWorkspace={workspaceContext.renameWorkspace}
166+
selectedWorkspace={props.selectedWorkspace}
167+
setSelectedWorkspace={props.setSelectedWorkspace}
166168
>
167169
<App />
168170
</AppProvider>

0 commit comments

Comments
 (0)