Skip to content

Commit be95e1f

Browse files
committed
feat: Implement AppContext for global state management and navigation handling
1 parent 4a094d0 commit be95e1f

6 files changed

Lines changed: 179 additions & 79 deletions

File tree

client/src/App.tsx

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
WeatherSunnyRegular,
2121
} from "@fluentui/react-icons";
2222

23-
import { ChatProvider, WorkspaceProvider } from "./context";
23+
import { AppProvider, ChatProvider, WorkspaceProvider, useApp } from "./context";
2424
import {
2525
ChatPanel,
2626
CollapsibleSidebar,
@@ -41,14 +41,9 @@ const AppContent: React.FC<AppContentProps> = ({
4141
onToggleTheme,
4242
}) => {
4343
const styles = useStyles();
44+
const { state, closeFile } = useApp();
4445

4546
const [isHealthy, setIsHealthy] = useState<boolean | null>(null);
46-
const [showJobPanel, setShowJobPanel] = useState(false);
47-
const [selectedWorkspaceForJobs, setSelectedWorkspaceForJobs] = useState<string | null>(null);
48-
const [selectedFile, setSelectedFile] = useState<{
49-
workspaceId: string;
50-
fileName: string;
51-
} | null>(null);
5247

5348
// Check API health on mount
5449
useEffect(() => {
@@ -90,20 +85,28 @@ const AppContent: React.FC<AppContentProps> = ({
9085
);
9186
};
9287

93-
const handleWorkspaceSelect = (workspaceId: string, fileName: string) => {
94-
setSelectedFile({ workspaceId, fileName });
95-
setShowJobPanel(false);
96-
setSelectedWorkspaceForJobs(null);
97-
};
98-
99-
const handleJobsClick = (workspaceId: string) => {
100-
setSelectedWorkspaceForJobs(workspaceId);
101-
setShowJobPanel(true);
102-
setSelectedFile(null);
103-
};
104-
105-
const handleCloseFile = () => {
106-
setSelectedFile(null);
88+
const renderContent = () => {
89+
switch (state.currentView) {
90+
case "file":
91+
return state.selectedFile ? (
92+
<WorkspaceFileViewer
93+
workspaceId={state.selectedFile.workspaceId}
94+
fileName={state.selectedFile.fileName}
95+
onClose={closeFile}
96+
/>
97+
) : (
98+
<ChatPanel />
99+
);
100+
case "jobs":
101+
return (
102+
<JobExecutionPanel
103+
workspaceId={state.selectedWorkspaceForJobs || undefined}
104+
/>
105+
);
106+
case "chat":
107+
default:
108+
return <ChatPanel />;
109+
}
107110
};
108111

109112
return (
@@ -135,25 +138,10 @@ const AppContent: React.FC<AppContentProps> = ({
135138
{/* Main Content */}
136139
<div className={styles.mainContainer}>
137140
{/* Collapsible Sidebar */}
138-
<CollapsibleSidebar
139-
onWorkspaceSelect={handleWorkspaceSelect}
140-
onJobsClick={handleJobsClick}
141-
/>
141+
<CollapsibleSidebar />
142142

143143
{/* Content Area */}
144-
<main className={styles.content}>
145-
{selectedFile ? (
146-
<WorkspaceFileViewer
147-
workspaceId={selectedFile.workspaceId}
148-
fileName={selectedFile.fileName}
149-
onClose={handleCloseFile}
150-
/>
151-
) : showJobPanel ? (
152-
<JobExecutionPanel workspaceId={selectedWorkspaceForJobs || undefined} />
153-
) : (
154-
<ChatPanel />
155-
)}
156-
</main>
144+
<main className={styles.content}>{renderContent()}</main>
157145
</div>
158146
</div>
159147
);
@@ -177,11 +165,13 @@ const App: React.FC = () => {
177165

178166
return (
179167
<FluentProvider theme={isDarkMode ? webDarkTheme : webLightTheme}>
180-
<WorkspaceProvider>
181-
<ChatProvider>
182-
<AppContent isDarkMode={isDarkMode} onToggleTheme={toggleTheme} />
183-
</ChatProvider>
184-
</WorkspaceProvider>
168+
<AppProvider>
169+
<WorkspaceProvider>
170+
<ChatProvider>
171+
<AppContent isDarkMode={isDarkMode} onToggleTheme={toggleTheme} />
172+
</ChatProvider>
173+
</WorkspaceProvider>
174+
</AppProvider>
185175
</FluentProvider>
186176
);
187177
};

client/src/components/Sidebar/CollapsibleSidebar.tsx

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,24 @@ import {
1919
import { ConversationSection } from "../Sidebar/ConversationSection";
2020
import { WorkspaceSection } from "../Sidebar/WorkspaceSection";
2121
import { JobSection } from "../Sidebar/JobSection";
22-
import { useChat } from "../../context";
22+
import { useChat, useApp } from "../../context";
2323
import { useCollapsibleSidebarStyles as useStyles } from "../../styles";
2424

25-
interface CollapsibleSidebarProps {
26-
onWorkspaceSelect?: (workspaceId: string, fileName: string) => void;
27-
onJobsClick?: (workspaceId: string) => void;
28-
}
29-
30-
export const CollapsibleSidebar: React.FC<CollapsibleSidebarProps> = ({
31-
onWorkspaceSelect,
32-
onJobsClick,
33-
}) => {
25+
export const CollapsibleSidebar: React.FC = () => {
3426
const styles = useStyles();
3527
const { startNewChat } = useChat();
28+
const { navigateToChat } = useApp();
3629
const [isCollapsed, setIsCollapsed] = useState(false);
3730

3831
const toggleSidebar = () => {
3932
setIsCollapsed(!isCollapsed);
4033
};
4134

35+
const handleNewChat = () => {
36+
startNewChat();
37+
navigateToChat();
38+
};
39+
4240
return (
4341
<div
4442
className={mergeClasses(
@@ -64,7 +62,7 @@ export const CollapsibleSidebar: React.FC<CollapsibleSidebarProps> = ({
6462
<Button
6563
icon={<AddRegular />}
6664
appearance="transparent"
67-
onClick={startNewChat}
65+
onClick={handleNewChat}
6866
className={styles.navButton}
6967
/>
7068
</Tooltip>
@@ -78,10 +76,7 @@ export const CollapsibleSidebar: React.FC<CollapsibleSidebarProps> = ({
7876
<ConversationSection />
7977

8078
{/* Workspaces Section */}
81-
<WorkspaceSection
82-
onWorkspaceSelect={onWorkspaceSelect}
83-
onJobsClick={onJobsClick}
84-
/>
79+
<WorkspaceSection />
8580

8681
{/* Job Execution Section */}
8782
<JobSection />

client/src/components/Sidebar/ConversationSection.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import {
3737
SearchRegular,
3838
DismissRegular,
3939
} from "@fluentui/react-icons";
40-
import { useChat } from "../../context";
40+
import { useChat, useApp } from "../../context";
4141
import { APP_STRINGS } from "../../constants";
4242
import { ConversationListItem } from "../../types";
4343
import { useConversationSectionStyles as useStyles } from "../../styles";
@@ -86,6 +86,7 @@ export const ConversationSection: React.FC = () => {
8686
deleteConversation,
8787
renameConversation,
8888
} = useChat();
89+
const { navigateToChat } = useApp();
8990

9091
const [isExpanded, setIsExpanded] = useState(true);
9192
const [searchExpanded, setSearchExpanded] = useState(false);
@@ -125,10 +126,12 @@ export const ConversationSection: React.FC = () => {
125126

126127
const handleConversationClick = (conversation: ConversationListItem) => {
127128
loadConversation(conversation.id);
129+
navigateToChat();
128130
};
129131

130132
const handleNewChat = () => {
131133
startNewChat();
134+
navigateToChat();
132135
};
133136

134137
const openRenameDialog = (conversation: ConversationListItem) => {

client/src/components/Sidebar/WorkspaceSection.tsx

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,15 @@ import {
4545
ChatRegular,
4646
PlayRegular,
4747
} from "@fluentui/react-icons";
48-
import { useWorkspace } from "../../context";
48+
import { useWorkspace, useApp } from "../../context";
4949
import { workspacesApi } from "../../api";
5050
import { Workspace } from "../../types";
5151
import { useWorkspaceSectionStyles as useStyles } from "../../styles";
5252

53-
interface WorkspaceSectionProps {
54-
onWorkspaceSelect?: (workspaceId: string, fileName: string) => void;
55-
onJobsClick?: (workspaceId: string) => void;
56-
}
57-
58-
export const WorkspaceSection: React.FC<WorkspaceSectionProps> = ({
59-
onWorkspaceSelect,
60-
onJobsClick,
61-
}) => {
53+
export const WorkspaceSection: React.FC = () => {
6254
const styles = useStyles();
6355
const { state, loadWorkspaces, selectWorkspace } = useWorkspace();
56+
const { navigateToFile, navigateToJobs } = useApp();
6457

6558
const [isExpanded, setIsExpanded] = useState(false);
6659
const [expandedWorkspace, setExpandedWorkspace] = useState<string | null>(null);
@@ -116,9 +109,7 @@ export const WorkspaceSection: React.FC<WorkspaceSectionProps> = ({
116109
};
117110

118111
const handleFileClick = (workspaceId: string, fileName: string) => {
119-
if (onWorkspaceSelect) {
120-
onWorkspaceSelect(workspaceId, fileName);
121-
}
112+
navigateToFile(workspaceId, fileName);
122113
};
123114

124115
const openCreateDialog = () => {
@@ -148,8 +139,8 @@ export const WorkspaceSection: React.FC<WorkspaceSectionProps> = ({
148139
setNewWorkspaceName("");
149140

150141
// Automatically open sap-parameters.yaml after creation
151-
if (useBoilerplate && onWorkspaceSelect) {
152-
onWorkspaceSelect(workspaceName, "sap-parameters.yaml");
142+
if (useBoilerplate) {
143+
navigateToFile(workspaceName, "sap-parameters.yaml");
153144
}
154145
} catch (error: any) {
155146
const errorMsg = error?.response?.data?.detail || error?.message || "Failed to create workspace";
@@ -330,11 +321,7 @@ export const WorkspaceSection: React.FC<WorkspaceSectionProps> = ({
330321
</div>
331322
<div
332323
className={styles.fileItem}
333-
onClick={() => {
334-
if (onJobsClick) {
335-
onJobsClick(workspace.workspace_id);
336-
}
337-
}}
324+
onClick={() => navigateToJobs(workspace.workspace_id)}
338325
>
339326
<PlayRegular className={styles.fileIcon} />
340327
<Text>Jobs</Text>

client/src/context/AppContext.tsx

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
/**
5+
* App Context
6+
* Global state management for application-level navigation and view state.
7+
*/
8+
9+
import React, {
10+
createContext,
11+
useContext,
12+
useReducer,
13+
useCallback,
14+
ReactNode,
15+
} from "react";
16+
17+
type ViewType = "chat" | "jobs" | "file";
18+
19+
interface SelectedFile {
20+
workspaceId: string;
21+
fileName: string;
22+
}
23+
24+
interface AppState {
25+
currentView: ViewType;
26+
selectedFile: SelectedFile | null;
27+
selectedWorkspaceForJobs: string | null;
28+
}
29+
30+
type AppAction =
31+
| { type: "NAVIGATE_TO_CHAT" }
32+
| { type: "NAVIGATE_TO_JOBS"; payload: string }
33+
| { type: "NAVIGATE_TO_FILE"; payload: SelectedFile }
34+
| { type: "CLOSE_FILE" };
35+
36+
const initialState: AppState = {
37+
currentView: "chat",
38+
selectedFile: null,
39+
selectedWorkspaceForJobs: null,
40+
};
41+
42+
const appReducer = (state: AppState, action: AppAction): AppState => {
43+
switch (action.type) {
44+
case "NAVIGATE_TO_CHAT":
45+
return {
46+
...state,
47+
currentView: "chat",
48+
selectedFile: null,
49+
selectedWorkspaceForJobs: null,
50+
};
51+
case "NAVIGATE_TO_JOBS":
52+
return {
53+
...state,
54+
currentView: "jobs",
55+
selectedFile: null,
56+
selectedWorkspaceForJobs: action.payload,
57+
};
58+
case "NAVIGATE_TO_FILE":
59+
return {
60+
...state,
61+
currentView: "file",
62+
selectedFile: action.payload,
63+
selectedWorkspaceForJobs: null,
64+
};
65+
case "CLOSE_FILE":
66+
return {
67+
...state,
68+
currentView: "chat",
69+
selectedFile: null,
70+
};
71+
default:
72+
return state;
73+
}
74+
};
75+
76+
interface AppContextType {
77+
state: AppState;
78+
navigateToChat: () => void;
79+
navigateToJobs: (workspaceId: string) => void;
80+
navigateToFile: (workspaceId: string, fileName: string) => void;
81+
closeFile: () => void;
82+
}
83+
84+
const AppContext = createContext<AppContextType | undefined>(undefined);
85+
86+
export const AppProvider: React.FC<{ children: ReactNode }> = ({
87+
children,
88+
}) => {
89+
const [state, dispatch] = useReducer(appReducer, initialState);
90+
91+
const navigateToChat = useCallback(() => {
92+
dispatch({ type: "NAVIGATE_TO_CHAT" });
93+
}, []);
94+
95+
const navigateToJobs = useCallback((workspaceId: string) => {
96+
dispatch({ type: "NAVIGATE_TO_JOBS", payload: workspaceId });
97+
}, []);
98+
99+
const navigateToFile = useCallback((workspaceId: string, fileName: string) => {
100+
dispatch({ type: "NAVIGATE_TO_FILE", payload: { workspaceId, fileName } });
101+
}, []);
102+
103+
const closeFile = useCallback(() => {
104+
dispatch({ type: "CLOSE_FILE" });
105+
}, []);
106+
107+
const value: AppContextType = {
108+
state,
109+
navigateToChat,
110+
navigateToJobs,
111+
navigateToFile,
112+
closeFile,
113+
};
114+
115+
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
116+
};
117+
118+
export const useApp = (): AppContextType => {
119+
const context = useContext(AppContext);
120+
if (!context) {
121+
throw new Error("useApp must be used within an AppProvider");
122+
}
123+
return context;
124+
};

client/src/context/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
* Re-exports all context providers and hooks.
77
*/
88

9+
export { AppProvider, useApp } from "./AppContext";
910
export { ChatProvider, useChat } from "./ChatContext";
1011
export { WorkspaceProvider, useWorkspace } from "./WorkspaceContext";

0 commit comments

Comments
 (0)