diff --git a/app/src/App.tsx b/app/src/App.tsx index c71c03c..b397599 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,5 +1,9 @@ import React from 'react'; -import { MantineProvider, ColorSchemeScript } from '@mantine/core'; +import { + MantineProvider, + ColorSchemeScript, + localStorageColorSchemeManager, +} from '@mantine/core'; import { Notifications } from '@mantine/notifications'; import { ModalsProvider } from '@mantine/modals'; import Layout from './components/layout/Layout'; @@ -39,11 +43,18 @@ const AuthenticatedContent: React.FC = () => { type AppProps = object; +const colorSchemeManager = localStorageColorSchemeManager({ + key: 'mantine-color-scheme', +}); + const App: React.FC = () => { return ( <> - + diff --git a/app/src/components/settings/workspace/AppearanceSettings.test.tsx b/app/src/components/settings/workspace/AppearanceSettings.test.tsx index adf30af..abece3a 100644 --- a/app/src/components/settings/workspace/AppearanceSettings.test.tsx +++ b/app/src/components/settings/workspace/AppearanceSettings.test.tsx @@ -32,7 +32,7 @@ describe('AppearanceSettings', () => { }); it('renders dark mode toggle with correct state', () => { - render(); + render(); expect(screen.getByText('Dark Mode')).toBeInTheDocument(); const toggle = screen.getByRole('switch'); @@ -46,20 +46,19 @@ describe('AppearanceSettings', () => { updateColorScheme: mockUpdateColorScheme, }); - render(); + render(); const toggle = screen.getByRole('switch'); expect(toggle).toBeChecked(); }); it('toggles theme from light to dark', () => { - render(); + render(); const toggle = screen.getByRole('switch'); fireEvent.click(toggle); expect(mockUpdateColorScheme).toHaveBeenCalledWith(Theme.Dark); - expect(mockOnThemeChange).toHaveBeenCalledWith(Theme.Dark); }); it('toggles theme from dark to light', async () => { @@ -69,12 +68,11 @@ describe('AppearanceSettings', () => { updateColorScheme: mockUpdateColorScheme, }); - render(); + render(); const toggle = screen.getByRole('switch'); fireEvent.click(toggle); expect(mockUpdateColorScheme).toHaveBeenCalledWith(Theme.Light); - expect(mockOnThemeChange).toHaveBeenCalledWith(Theme.Light); }); }); diff --git a/app/src/components/settings/workspace/AppearanceSettings.tsx b/app/src/components/settings/workspace/AppearanceSettings.tsx index 537d38b..8802148 100644 --- a/app/src/components/settings/workspace/AppearanceSettings.tsx +++ b/app/src/components/settings/workspace/AppearanceSettings.tsx @@ -1,21 +1,14 @@ import React from 'react'; import { Text, Switch, Group, Box } from '@mantine/core'; -import { useTheme } from '../../../contexts/ThemeContext'; import { Theme } from '@/types/models'; +import { useTheme } from '../../../contexts/ThemeContext'; -interface AppearanceSettingsProps { - onThemeChange: (newTheme: Theme) => void; -} - -const AppearanceSettings: React.FC = ({ - onThemeChange, -}) => { +const AppearanceSettings: React.FC = () => { const { colorScheme, updateColorScheme } = useTheme(); const handleThemeChange = (): void => { const newTheme = colorScheme === 'dark' ? Theme.Light : Theme.Dark; updateColorScheme(newTheme); - onThemeChange(newTheme); }; return ( diff --git a/app/src/components/settings/workspace/WorkspaceSettings.test.tsx b/app/src/components/settings/workspace/WorkspaceSettings.test.tsx index 71b177a..b7c11d2 100644 --- a/app/src/components/settings/workspace/WorkspaceSettings.test.tsx +++ b/app/src/components/settings/workspace/WorkspaceSettings.test.tsx @@ -11,6 +11,7 @@ import WorkspaceSettings from './WorkspaceSettings'; import { Theme } from '@/types/models'; const mockUpdateSettings = vi.fn(); +const mockUpdateColorScheme = vi.fn(); vi.mock('../../../hooks/useWorkspace', () => ({ useWorkspace: vi.fn(), })); @@ -48,11 +49,9 @@ vi.mock('./GeneralSettings', () => ({ })); vi.mock('./AppearanceSettings', () => ({ - default: ({ onThemeChange }: { onThemeChange: (theme: string) => void }) => ( + default: () => (
- + Appearance Settings
), })); @@ -105,7 +104,7 @@ describe('WorkspaceSettings', () => { updateSettings: mockUpdateSettings, loading: false, colorScheme: 'light', - updateColorScheme: vi.fn(), + updateColorScheme: mockUpdateColorScheme, switchWorkspace: vi.fn(), deleteCurrentWorkspace: vi.fn(), }); @@ -152,13 +151,10 @@ describe('WorkspaceSettings', () => { }); }); - it('handles theme changes', () => { + it('renders appearance settings', () => { render(); - const themeToggle = screen.getByTestId('theme-toggle'); - fireEvent.click(themeToggle); - - expect(screen.getByText('Unsaved Changes')).toBeInTheDocument(); + expect(screen.getByTestId('appearance-settings')).toBeInTheDocument(); }); it('closes modal when cancel is clicked', () => { @@ -192,4 +188,16 @@ describe('WorkspaceSettings', () => { expect(mockUpdateSettings).not.toHaveBeenCalled(); }); + + it('reverts theme when canceling', () => { + render(); + + // Click cancel + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); + fireEvent.click(cancelButton); + + // Theme should be reverted to saved state (Light) + expect(mockUpdateColorScheme).toHaveBeenCalledWith(Theme.Light); + expect(mockSetSettingsModalVisible).toHaveBeenCalledWith(false); + }); }); diff --git a/app/src/components/settings/workspace/WorkspaceSettings.tsx b/app/src/components/settings/workspace/WorkspaceSettings.tsx index 109f999..e02390e 100644 --- a/app/src/components/settings/workspace/WorkspaceSettings.tsx +++ b/app/src/components/settings/workspace/WorkspaceSettings.tsx @@ -18,7 +18,7 @@ import { useModalContext } from '../../../contexts/ModalContext'; import DangerZoneSettings from './DangerZoneSettings'; import AccordionControl from '../AccordionControl'; import { - type Theme, + Theme, type Workspace, type SettingsAction, SettingsActionType, @@ -72,7 +72,7 @@ function settingsReducer( } const WorkspaceSettings: React.FC = () => { - const { currentWorkspace, updateSettings } = useWorkspace(); + const { currentWorkspace, updateSettings, updateColorScheme, colorScheme } = useWorkspace(); const { settingsModalVisible, setSettingsModalVisible } = useModalContext(); const [state, dispatch] = useReducer(settingsReducer, initialState); const isInitialMount = useRef(true); @@ -118,7 +118,13 @@ const WorkspaceSettings: React.FC = () => { return; } - await updateSettings(state.localSettings); + // Save with current Mantine theme + const settingsToSave = { + ...state.localSettings, + theme: colorScheme as Theme, + }; + + await updateSettings(settingsToSave); dispatch({ type: SettingsActionType.MARK_SAVED }); notifications.show({ message: 'Settings saved successfully', @@ -137,8 +143,12 @@ const WorkspaceSettings: React.FC = () => { }; const handleClose = useCallback(() => { + // Revert theme to saved state + if (state.initialSettings.theme) { + updateColorScheme(state.initialSettings.theme); + } setSettingsModalVisible(false); - }, [setSettingsModalVisible]); + }, [setSettingsModalVisible, state.initialSettings.theme, updateColorScheme]); return ( { Appearance - - handleInputChange('theme', newTheme as Theme) - } - /> + diff --git a/app/src/contexts/ThemeContext.tsx b/app/src/contexts/ThemeContext.tsx index 530e726..3f518ed 100644 --- a/app/src/contexts/ThemeContext.tsx +++ b/app/src/contexts/ThemeContext.tsx @@ -2,6 +2,7 @@ import React, { createContext, useContext, useCallback, + useMemo, type ReactNode, } from 'react'; import { useMantineColorScheme, type MantineColorScheme } from '@mantine/core'; @@ -32,11 +33,16 @@ export const ThemeProvider: React.FC = ({ children }) => { ); // Ensure colorScheme is never undefined by falling back to light theme - const value: ThemeContextType = { - colorScheme: - colorScheme === 'light' || colorScheme === 'dark' ? colorScheme : 'light', - updateColorScheme, - }; + const normalizedColorScheme = + colorScheme === 'light' || colorScheme === 'dark' ? colorScheme : 'light'; + + const value: ThemeContextType = useMemo( + () => ({ + colorScheme: normalizedColorScheme, + updateColorScheme, + }), + [normalizedColorScheme, updateColorScheme] + ); return ( {children} diff --git a/app/src/contexts/WorkspaceDataContext.tsx b/app/src/contexts/WorkspaceDataContext.tsx index aa02bbd..508c8f5 100644 --- a/app/src/contexts/WorkspaceDataContext.tsx +++ b/app/src/contexts/WorkspaceDataContext.tsx @@ -74,7 +74,8 @@ export const WorkspaceDataProvider: React.FC = ({ }); } }, - [updateColorScheme] + // eslint-disable-next-line react-hooks/exhaustive-deps + [] ); const loadFirstAvailableWorkspace = useCallback(async (): Promise => {