Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,6 @@ go.work.sum
main
*.db
data

# Feature specifications
spec.md
3 changes: 2 additions & 1 deletion app/src/components/modals/user/DeleteUserModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import React from 'react';
import { MantineProvider } from '@mantine/core';
import DeleteUserModal from './DeleteUserModal';
import { UserRole, type User } from '@/types/models';
import { UserRole, Theme, type User } from '@/types/models';

// Mock notifications
vi.mock('@mantine/notifications', () => ({
Expand Down Expand Up @@ -36,6 +36,7 @@ describe('DeleteUserModal', () => {
email: '[email protected]',
displayName: 'Test User',
role: UserRole.Editor,
theme: Theme.Dark,
createdAt: '2024-01-01T00:00:00Z',
lastWorkspaceId: 1,
};
Expand Down
4 changes: 3 additions & 1 deletion app/src/components/modals/user/EditUserModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import React from 'react';
import { MantineProvider } from '@mantine/core';
import EditUserModal from './EditUserModal';
import { UserRole, type User } from '@/types/models';
import { UserRole, Theme, type User } from '@/types/models';

// Mock notifications
vi.mock('@mantine/notifications', () => ({
Expand Down Expand Up @@ -36,6 +36,7 @@ describe('EditUserModal', () => {
email: '[email protected]',
displayName: 'Test User',
role: UserRole.Editor,
theme: Theme.Dark,
createdAt: '2024-01-01T00:00:00Z',
lastWorkspaceId: 1,
};
Expand Down Expand Up @@ -187,6 +188,7 @@ describe('EditUserModal', () => {
email: '[email protected]',
displayName: 'New User',
role: UserRole.Admin,
theme: Theme.Dark,
};

rerender(
Expand Down
7 changes: 6 additions & 1 deletion app/src/components/navigation/UserMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import { fireEvent, waitFor } from '@testing-library/react';
import { render } from '../../test/utils';
import UserMenu from './UserMenu';
import { UserRole } from '../../types/models';
import { UserRole, Theme } from '../../types/models';

// Mock the contexts
vi.mock('../../contexts/AuthContext', () => ({
Expand Down Expand Up @@ -37,6 +37,7 @@ describe('UserMenu', () => {
email: '[email protected]',
displayName: 'Test User',
role: UserRole.Editor,
theme: Theme.Dark,
createdAt: '2024-01-01T00:00:00Z',
lastWorkspaceId: 1,
};
Expand All @@ -53,6 +54,7 @@ describe('UserMenu', () => {
login: vi.fn(),
refreshToken: vi.fn(),
refreshUser: vi.fn(),
updateProfile: vi.fn(),
});
});

Expand Down Expand Up @@ -84,6 +86,7 @@ describe('UserMenu', () => {
login: vi.fn(),
refreshToken: vi.fn(),
refreshUser: vi.fn(),
updateProfile: vi.fn(),
});

const { getByLabelText, getByText } = render(
Expand Down Expand Up @@ -145,6 +148,7 @@ describe('UserMenu', () => {
id: mockUser.id,
email: mockUser.email,
role: mockUser.role,
theme: mockUser.theme,
createdAt: mockUser.createdAt,
lastWorkspaceId: mockUser.lastWorkspaceId,
};
Expand All @@ -157,6 +161,7 @@ describe('UserMenu', () => {
login: vi.fn(),
refreshToken: vi.fn(),
refreshUser: vi.fn(),
updateProfile: vi.fn(),
});

const { getByLabelText, getByText } = render(
Expand Down
17 changes: 17 additions & 0 deletions app/src/components/settings/account/AccountSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const AccountSettings: React.FC<AccountSettingsProps> = ({
email: user.email,
currentPassword: '',
newPassword: '',
theme: user.theme,
};
dispatch({
type: SettingsActionType.INIT_SETTINGS,
Expand All @@ -107,6 +108,13 @@ const AccountSettings: React.FC<AccountSettingsProps> = ({
});
};

const handleThemeChange = (theme: string): void => {
dispatch({
type: SettingsActionType.UPDATE_LOCAL_SETTINGS,
payload: { theme } as UserProfileSettings,
});
};

const handleSubmit = async (): Promise<void> => {
const updates: UserProfileSettings = {};
const needsPasswordConfirmation =
Expand All @@ -117,6 +125,14 @@ const AccountSettings: React.FC<AccountSettingsProps> = ({
updates.displayName = state.localSettings.displayName || '';
}

// Add theme if changed
if (
state.localSettings.theme &&
state.localSettings.theme !== state.initialSettings.theme
) {
updates.theme = state.localSettings.theme;
}

// Handle password change
if (state.localSettings.newPassword) {
if (!state.localSettings.currentPassword) {
Expand Down Expand Up @@ -216,6 +232,7 @@ const AccountSettings: React.FC<AccountSettingsProps> = ({
<ProfileSettings
settings={state.localSettings}
onInputChange={handleInputChange}
onThemeChange={handleThemeChange}
/>
</Accordion.Panel>
</Accordion.Item>
Expand Down
19 changes: 19 additions & 0 deletions app/src/components/settings/account/ProfileSettings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@
import { MantineProvider } from '@mantine/core';
import ProfileSettings from './ProfileSettings';
import type { UserProfileSettings } from '@/types/models';
import { Theme, UserRole, type User } from '@/types/models';

Check failure on line 7 in app/src/components/settings/account/ProfileSettings.test.tsx

View workflow job for this annotation

GitHub Actions / TypeScript Type Check

'@/types/models' import is duplicated

// Mock user for AuthContext
const mockUser: User = {
id: 1,
email: '[email protected]',
displayName: 'Test User',
role: UserRole.Editor,
theme: Theme.Dark,
createdAt: '2024-01-01T00:00:00Z',
lastWorkspaceId: 1,
};

// Mock the auth context
vi.mock('../../../contexts/AuthContext', () => ({
useAuth: () => ({
user: mockUser,
}),
}));

// Helper wrapper component for testing
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
Expand Down
78 changes: 54 additions & 24 deletions app/src/components/settings/account/ProfileSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,66 @@
import React from 'react';
import { Box, Stack, TextInput } from '@mantine/core';
import type { UserProfileSettings } from '@/types/models';
import { Box, Stack, TextInput, Group, Text, Switch } from '@mantine/core';
import { useAuth } from '@/contexts/AuthContext';
import { Theme, type UserProfileSettings } from '@/types/models';

interface ProfileSettingsProps {
settings: UserProfileSettings;
onInputChange: (key: keyof UserProfileSettings, value: string) => void;
onThemeChange?: (theme: Theme) => void;
}

const ProfileSettings: React.FC<ProfileSettingsProps> = ({
settings,
onInputChange,
}) => (
<Box>
<Stack gap="md">
<TextInput
label="Display Name"
type="text"
value={settings.displayName || ''}
onChange={(e) => onInputChange('displayName', e.currentTarget.value)}
placeholder="Enter display name"
data-testid="display-name-input"
/>
<TextInput
label="Email"
type="email"
value={settings.email || ''}
onChange={(e) => onInputChange('email', e.currentTarget.value)}
placeholder="Enter email"
data-testid="email-input"
/>
</Stack>
</Box>
);
onThemeChange,
}) => {
const { user } = useAuth();
const currentTheme = settings.theme || user?.theme || Theme.Dark;

const handleThemeToggle = () => {
const newTheme = currentTheme === Theme.Dark ? Theme.Light : Theme.Dark;
if (onThemeChange) {
onThemeChange(newTheme);
}
};

return (
<Box>
<Stack gap="md">
<TextInput
label="Display Name"
type="text"
value={settings.displayName || ''}
onChange={(e) => onInputChange('displayName', e.currentTarget.value)}
placeholder="Enter display name"
data-testid="display-name-input"
/>
<TextInput
label="Email"
type="email"
value={settings.email || ''}
onChange={(e) => onInputChange('email', e.currentTarget.value)}
placeholder="Enter email"
data-testid="email-input"
/>
<Box mb="md">
<Group justify="space-between" align="center">
<div>
<Text size="sm">Default Dark Mode</Text>
<Text size="xs" c="dimmed">
Sets the default theme for new workspaces
</Text>
</div>
<Switch
checked={currentTheme === Theme.Dark}
onChange={handleThemeToggle}
data-testid="theme-toggle"
/>
</Group>
</Box>
</Stack>
</Box>
);
};

export default ProfileSettings;
3 changes: 2 additions & 1 deletion app/src/components/settings/admin/AdminDashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { render as rtlRender, screen, fireEvent } from '@testing-library/react';
import React from 'react';
import { MantineProvider } from '@mantine/core';
import AdminDashboard from './AdminDashboard';
import { UserRole, type User } from '@/types/models';
import { UserRole, Theme, type User } from '@/types/models';

// Mock the auth context
const mockCurrentUser: User = {
id: 1,
email: '[email protected]',
displayName: 'Admin User',
role: UserRole.Admin,
theme: Theme.Dark,
createdAt: '2024-01-01T00:00:00Z',
lastWorkspaceId: 1,
};
Expand Down
5 changes: 4 additions & 1 deletion app/src/components/settings/admin/AdminUsersTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import React from 'react';
import { MantineProvider } from '@mantine/core';
import AdminUsersTab from './AdminUsersTab';
import { UserRole, type User } from '@/types/models';
import { UserRole, Theme, type User } from '@/types/models';

// Mock the user admin hook
const mockCreate = vi.fn();
Expand Down Expand Up @@ -123,6 +123,7 @@ describe('AdminUsersTab', () => {
email: '[email protected]',
displayName: 'Admin User',
role: UserRole.Admin,
theme: Theme.Dark,
createdAt: '2024-01-01T00:00:00Z',
lastWorkspaceId: 1,
};
Expand All @@ -134,6 +135,7 @@ describe('AdminUsersTab', () => {
email: '[email protected]',
displayName: 'Editor User',
role: UserRole.Editor,
theme: Theme.Dark,
createdAt: '2024-01-15T00:00:00Z',
lastWorkspaceId: 2,
},
Expand All @@ -142,6 +144,7 @@ describe('AdminUsersTab', () => {
email: '[email protected]',
displayName: 'Viewer User',
role: UserRole.Viewer,
theme: Theme.Dark,
createdAt: '2024-02-01T00:00:00Z',
lastWorkspaceId: 3,
},
Expand Down
3 changes: 2 additions & 1 deletion app/src/contexts/AuthContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act, waitFor } from '@testing-library/react';
import React from 'react';
import { AuthProvider, useAuth } from './AuthContext';
import { UserRole, type User } from '@/types/models';
import { UserRole, Theme, type User } from '@/types/models';

// Set up mocks before imports are used
vi.mock('@/api/auth', () => {
Expand Down Expand Up @@ -42,6 +42,7 @@ const mockUser: User = {
email: '[email protected]',
displayName: 'Test User',
role: UserRole.Editor,
theme: Theme.Dark,
createdAt: '2024-01-01T00:00:00Z',
lastWorkspaceId: 1,
};
Expand Down
30 changes: 29 additions & 1 deletion app/src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
refreshToken as apiRefreshToken,
getCurrentUser,
} from '@/api/auth';
import type { User } from '@/types/models';
import { updateProfile as apiUpdateProfile } from '@/api/user';
import type { User, UserProfileSettings } from '@/types/models';

interface AuthContextType {
user: User | null;
Expand All @@ -22,6 +23,7 @@ interface AuthContextType {
logout: () => Promise<void>;
refreshToken: () => Promise<boolean>;
refreshUser: () => Promise<void>;
updateProfile: (updates: UserProfileSettings) => Promise<User>;
}

const AuthContext = createContext<AuthContextType | null>(null);
Expand Down Expand Up @@ -109,6 +111,31 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
}
}, []);

const updateProfile = useCallback(
async (updates: UserProfileSettings): Promise<User> => {
try {
const updatedUser = await apiUpdateProfile(updates);
setUser(updatedUser);
notifications.show({
title: 'Success',
message: 'Profile updated successfully',
color: 'green',
});
return updatedUser;
} catch (error) {
console.error('Failed to update profile:', error);
notifications.show({
title: 'Error',
message:
error instanceof Error ? error.message : 'Failed to update profile',
color: 'red',
});
throw error;
}
},
[]
);

const value: AuthContextType = {
user,
loading,
Expand All @@ -117,6 +144,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
logout,
refreshToken,
refreshUser,
updateProfile,
};

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
Expand Down
Loading
Loading