From bcc5d2588acbdf90df6e7deb1c47912dd8f50ffc Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sat, 11 Oct 2025 17:46:12 +0200 Subject: [PATCH 1/3] Streamline theme management and improve AppearanceSettings component --- app/src/App.tsx | 15 ++++++++-- .../workspace/AppearanceSettings.test.tsx | 10 +++---- .../settings/workspace/AppearanceSettings.tsx | 11 ++------ .../workspace/WorkspaceSettings.test.tsx | 28 ++++++++++++------- .../settings/workspace/WorkspaceSettings.tsx | 24 ++++++++++------ app/src/contexts/ThemeContext.tsx | 16 +++++++---- app/src/contexts/WorkspaceDataContext.tsx | 3 +- 7 files changed, 65 insertions(+), 42 deletions(-) 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 => { From 769385d8c784a32ca42351e6cff9c7b0794b37d4 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sat, 11 Oct 2025 19:04:08 +0200 Subject: [PATCH 2/3] Fix workspace selection logic and update settings initialization conditions --- app/src/components/navigation/WorkspaceSwitcher.tsx | 4 ++-- app/src/components/settings/workspace/WorkspaceSettings.tsx | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/src/components/navigation/WorkspaceSwitcher.tsx b/app/src/components/navigation/WorkspaceSwitcher.tsx index 21f0f4d..9042ca5 100644 --- a/app/src/components/navigation/WorkspaceSwitcher.tsx +++ b/app/src/components/navigation/WorkspaceSwitcher.tsx @@ -110,10 +110,10 @@ const WorkspaceSwitcher: React.FC = () => { ) : ( workspaces.map((workspace) => { - const isSelected = workspace.name === currentWorkspace?.name; + const isSelected = workspace.id === currentWorkspace?.id; return ( diff --git a/app/src/components/settings/workspace/WorkspaceSettings.tsx b/app/src/components/settings/workspace/WorkspaceSettings.tsx index e02390e..73598ff 100644 --- a/app/src/components/settings/workspace/WorkspaceSettings.tsx +++ b/app/src/components/settings/workspace/WorkspaceSettings.tsx @@ -75,11 +75,9 @@ const WorkspaceSettings: React.FC = () => { const { currentWorkspace, updateSettings, updateColorScheme, colorScheme } = useWorkspace(); const { settingsModalVisible, setSettingsModalVisible } = useModalContext(); const [state, dispatch] = useReducer(settingsReducer, initialState); - const isInitialMount = useRef(true); useEffect(() => { - if (isInitialMount.current && currentWorkspace) { - isInitialMount.current = false; + if (currentWorkspace && settingsModalVisible) { const settings: Partial = { name: currentWorkspace.name, theme: currentWorkspace.theme, @@ -96,7 +94,7 @@ const WorkspaceSettings: React.FC = () => { }; dispatch({ type: SettingsActionType.INIT_SETTINGS, payload: settings }); } - }, [currentWorkspace]); + }, [currentWorkspace, settingsModalVisible]); const handleInputChange = useCallback( (key: K, value: Workspace[K]): void => { From 4ec019f2b7eb82c93493f8ea69ce0a006a91e67e Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sat, 11 Oct 2025 20:23:10 +0200 Subject: [PATCH 3/3] Fix typescript type check issues --- .../settings/workspace/AppearanceSettings.test.tsx | 2 -- .../components/settings/workspace/WorkspaceSettings.tsx | 7 ++++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/components/settings/workspace/AppearanceSettings.test.tsx b/app/src/components/settings/workspace/AppearanceSettings.test.tsx index abece3a..a383932 100644 --- a/app/src/components/settings/workspace/AppearanceSettings.test.tsx +++ b/app/src/components/settings/workspace/AppearanceSettings.test.tsx @@ -20,8 +20,6 @@ const render = (ui: React.ReactElement) => { }; describe('AppearanceSettings', () => { - const mockOnThemeChange = vi.fn(); - beforeEach(async () => { vi.clearAllMocks(); const { useTheme } = await import('../../../contexts/ThemeContext'); diff --git a/app/src/components/settings/workspace/WorkspaceSettings.tsx b/app/src/components/settings/workspace/WorkspaceSettings.tsx index 73598ff..eeb2189 100644 --- a/app/src/components/settings/workspace/WorkspaceSettings.tsx +++ b/app/src/components/settings/workspace/WorkspaceSettings.tsx @@ -1,4 +1,4 @@ -import React, { useReducer, useEffect, useCallback, useRef } from 'react'; +import React, { useReducer, useEffect, useCallback } from 'react'; import { Modal, Badge, @@ -18,7 +18,7 @@ import { useModalContext } from '../../../contexts/ModalContext'; import DangerZoneSettings from './DangerZoneSettings'; import AccordionControl from '../AccordionControl'; import { - Theme, + type Theme, type Workspace, type SettingsAction, SettingsActionType, @@ -72,7 +72,8 @@ function settingsReducer( } const WorkspaceSettings: React.FC = () => { - const { currentWorkspace, updateSettings, updateColorScheme, colorScheme } = useWorkspace(); + const { currentWorkspace, updateSettings, updateColorScheme, colorScheme } = + useWorkspace(); const { settingsModalVisible, setSettingsModalVisible } = useModalContext(); const [state, dispatch] = useReducer(settingsReducer, initialState);