* feat: #333 load language from backend configuration * feat(settings): sync language with user configs api * Update __test__/features/Settings/SettingsPage.test.tsx Co-authored-by: Lê Nhân Phụng <lenhanphung@Phung-Mac-M4.local> Co-authored-by: Benoit TELLIER <btellier@linagora.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -2,8 +2,16 @@ import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import SettingsPage from "../../../src/features/Settings/SettingsPage";
|
import SettingsPage from "../../../src/features/Settings/SettingsPage";
|
||||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||||
|
import { api } from "../../../src/utils/apiUtils";
|
||||||
|
|
||||||
|
jest.mock("../../../src/utils/apiUtils");
|
||||||
|
|
||||||
describe("SettingsPage", () => {
|
describe("SettingsPage", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
const preloadedState = {
|
const preloadedState = {
|
||||||
user: {
|
user: {
|
||||||
userData: {
|
userData: {
|
||||||
@@ -14,6 +22,11 @@ describe("SettingsPage", () => {
|
|||||||
sid: "mockSid",
|
sid: "mockSid",
|
||||||
openpaasId: "667037022b752d0026472254",
|
openpaasId: "667037022b752d0026472254",
|
||||||
},
|
},
|
||||||
|
organiserData: null,
|
||||||
|
tokens: null,
|
||||||
|
language: "en",
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
language: "en",
|
language: "en",
|
||||||
@@ -82,49 +95,95 @@ describe("SettingsPage", () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("displays all available language options", async () => {
|
it("displays all available language options and uses language from user state", async () => {
|
||||||
|
const stateWithUserLanguage = {
|
||||||
|
user: {
|
||||||
|
userData: {
|
||||||
|
sub: "test",
|
||||||
|
email: "test@test.com",
|
||||||
|
family_name: "Doe",
|
||||||
|
name: "John",
|
||||||
|
sid: "mockSid",
|
||||||
|
openpaasId: "667037022b752d0026472254",
|
||||||
|
},
|
||||||
|
organiserData: null,
|
||||||
|
tokens: null,
|
||||||
|
language: "fr",
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
language: "en",
|
||||||
|
view: "settings",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
renderWithProviders(<SettingsPage />, stateWithUserLanguage);
|
||||||
|
|
||||||
|
const languageSelect = screen.getByLabelText("settings.languageSelector");
|
||||||
|
|
||||||
|
// Verify Select exists
|
||||||
|
expect(languageSelect).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Verify that the underlying native input reflects the user language ("fr")
|
||||||
|
const nativeInput = languageSelect.querySelector(
|
||||||
|
'input[aria-hidden="true"]'
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
expect(nativeInput).not.toBeNull();
|
||||||
|
expect(nativeInput?.value).toBe("fr");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates language immediately (optimistic update) and calls API in background", async () => {
|
||||||
|
(api.patch as jest.Mock).mockResolvedValue({ status: 204 });
|
||||||
|
|
||||||
|
const { store } = renderWithProviders(<SettingsPage />, preloadedState);
|
||||||
|
|
||||||
|
const languageSelect = screen.getByLabelText("settings.languageSelector");
|
||||||
|
|
||||||
|
// MUI Select uses a native input element - find and change it
|
||||||
|
const nativeInput = languageSelect.querySelector(
|
||||||
|
'input[aria-hidden="true"]'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
expect(nativeInput).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Simulate change event on the native input
|
||||||
|
Object.defineProperty(nativeInput, "value", {
|
||||||
|
writable: true,
|
||||||
|
value: "fr",
|
||||||
|
});
|
||||||
|
fireEvent.change(nativeInput, { target: { value: "fr" } });
|
||||||
|
|
||||||
|
// Language should be updated immediately (optimistic update)
|
||||||
|
await waitFor(() => {
|
||||||
|
const state = store.getState();
|
||||||
|
expect(state.user?.language).toBe("fr");
|
||||||
|
expect(state.settings.language).toBe("fr");
|
||||||
|
});
|
||||||
|
|
||||||
|
// API should be called in background
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(api.patch).toHaveBeenCalledWith(
|
||||||
|
"api/configurations?scope=user",
|
||||||
|
expect.objectContaining({
|
||||||
|
json: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "core",
|
||||||
|
configurations: expect.arrayContaining([
|
||||||
|
expect.objectContaining({ name: "language", value: "fr" }),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saves language change to localStorage immediately (optimistic update)", async () => {
|
||||||
|
(api.patch as jest.Mock).mockResolvedValue({ status: 204 });
|
||||||
|
|
||||||
renderWithProviders(<SettingsPage />, preloadedState);
|
renderWithProviders(<SettingsPage />, preloadedState);
|
||||||
|
|
||||||
const languageSelect = screen.getByLabelText("settings.languageSelector");
|
const languageSelect = screen.getByLabelText("settings.languageSelector");
|
||||||
|
|
||||||
// Click on the select to open dropdown
|
|
||||||
fireEvent.mouseDown(languageSelect);
|
|
||||||
|
|
||||||
// Wait for menu to appear - MUI Select uses Menu internally
|
|
||||||
// Note: In test environment, Select may not open menu, so we verify Select exists and has correct value
|
|
||||||
expect(languageSelect).toBeInTheDocument();
|
|
||||||
expect(languageSelect).toHaveTextContent("English");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("dispatches setLanguage action when language is changed", async () => {
|
|
||||||
const { store } = renderWithProviders(<SettingsPage />, preloadedState);
|
|
||||||
|
|
||||||
const languageSelect = screen.getByLabelText("settings.languageSelector");
|
|
||||||
|
|
||||||
// MUI Select uses a native input element - find and change it
|
|
||||||
const nativeInput = languageSelect.querySelector(
|
|
||||||
'input[aria-hidden="true"]'
|
|
||||||
) as HTMLInputElement;
|
|
||||||
expect(nativeInput).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Simulate change event on the native input
|
|
||||||
Object.defineProperty(nativeInput, "value", {
|
|
||||||
writable: true,
|
|
||||||
value: "fr",
|
|
||||||
});
|
|
||||||
fireEvent.change(nativeInput, { target: { value: "fr" } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const state = store.getState();
|
|
||||||
expect(state.settings.language).toBe("fr");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("saves language change to localStorage", async () => {
|
|
||||||
const { store } = renderWithProviders(<SettingsPage />, preloadedState);
|
|
||||||
|
|
||||||
const languageSelect = screen.getByLabelText("settings.languageSelector");
|
|
||||||
|
|
||||||
// MUI Select uses a native input element - find and change it
|
// MUI Select uses a native input element - find and change it
|
||||||
const nativeInput = languageSelect.querySelector(
|
const nativeInput = languageSelect.querySelector(
|
||||||
'input[aria-hidden="true"]'
|
'input[aria-hidden="true"]'
|
||||||
@@ -138,11 +197,43 @@ describe("SettingsPage", () => {
|
|||||||
});
|
});
|
||||||
fireEvent.change(nativeInput, { target: { value: "fr" } });
|
fireEvent.change(nativeInput, { target: { value: "fr" } });
|
||||||
|
|
||||||
|
// localStorage should be updated immediately (optimistic update)
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(localStorage.getItem("lang")).toBe("fr");
|
expect(localStorage.getItem("lang")).toBe("fr");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rolls back language change if API call fails", async () => {
|
||||||
|
(api.patch as jest.Mock).mockRejectedValue(new Error("API Error"));
|
||||||
|
|
||||||
|
const { store } = renderWithProviders(<SettingsPage />, preloadedState);
|
||||||
|
|
||||||
|
const languageSelect = screen.getByLabelText("settings.languageSelector");
|
||||||
|
|
||||||
|
// MUI Select uses a native input element - find and change it
|
||||||
|
const nativeInput = languageSelect.querySelector(
|
||||||
|
'input[aria-hidden="true"]'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
expect(nativeInput).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Simulate change event on the native input
|
||||||
|
Object.defineProperty(nativeInput, "value", {
|
||||||
|
writable: true,
|
||||||
|
value: "fr",
|
||||||
|
});
|
||||||
|
fireEvent.change(nativeInput, { target: { value: "fr" } });
|
||||||
|
|
||||||
|
// Wait for rollback - language should be rolled back to "en" after error
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
const state = store.getState();
|
||||||
|
expect(state.user?.language).toBe("en");
|
||||||
|
expect(state.settings.language).toBe("en");
|
||||||
|
},
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("shows empty state in Notifications tab", () => {
|
it("shows empty state in Notifications tab", () => {
|
||||||
renderWithProviders(<SettingsPage />, preloadedState);
|
renderWithProviders(<SettingsPage />, preloadedState);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { clientConfig } from "../../../src/features/User/oidcAuth";
|
import { clientConfig } from "../../../src/features/User/oidcAuth";
|
||||||
import { getOpenPaasUser } from "../../../src/features/User/userAPI";
|
import {
|
||||||
|
getOpenPaasUser,
|
||||||
|
updateUserConfigurations,
|
||||||
|
} from "../../../src/features/User/userAPI";
|
||||||
import { api } from "../../../src/utils/apiUtils";
|
import { api } from "../../../src/utils/apiUtils";
|
||||||
|
|
||||||
jest.mock("../../../src/utils/apiUtils");
|
jest.mock("../../../src/utils/apiUtils");
|
||||||
|
|
||||||
clientConfig.url = "https://example.com";
|
clientConfig.url = "https://example.com";
|
||||||
|
|
||||||
describe("getOpenPaasUserId", () => {
|
describe("getOpenPaasUser", () => {
|
||||||
it("should fetch and return user data", async () => {
|
it("should fetch and return user data", async () => {
|
||||||
const mockUser = { id: "123", name: "OpenPaas User" };
|
const mockUser = { id: "123", name: "OpenPaas User" };
|
||||||
|
|
||||||
@@ -20,3 +23,54 @@ describe("getOpenPaasUserId", () => {
|
|||||||
expect(result).toEqual(mockUser);
|
expect(result).toEqual(mockUser);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("updateUserConfigurations", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should PATCH configurations with language update", async () => {
|
||||||
|
const mockResponse = { status: 204 };
|
||||||
|
(api.patch as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
await updateUserConfigurations({ language: "vi" });
|
||||||
|
|
||||||
|
expect(api.patch).toHaveBeenCalledWith("api/configurations?scope=user", {
|
||||||
|
json: [
|
||||||
|
{
|
||||||
|
name: "core",
|
||||||
|
configurations: [{ name: "language", value: "vi" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should PATCH configurations with multiple updates", async () => {
|
||||||
|
const mockResponse = { status: 204 };
|
||||||
|
(api.patch as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
await updateUserConfigurations({
|
||||||
|
language: "fr",
|
||||||
|
timezone: "Europe/Paris",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(api.patch).toHaveBeenCalledWith("api/configurations?scope=user", {
|
||||||
|
json: [
|
||||||
|
{
|
||||||
|
name: "core",
|
||||||
|
configurations: [
|
||||||
|
{ name: "language", value: "fr" },
|
||||||
|
{ name: "timezone", value: "Europe/Paris" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty updates without calling API", async () => {
|
||||||
|
const result = await updateUserConfigurations({});
|
||||||
|
|
||||||
|
expect(api.patch).not.toHaveBeenCalled();
|
||||||
|
expect(result).toEqual({ status: 204 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+20
-2
@@ -13,6 +13,7 @@ import { useAppDispatch, useAppSelector } from "./app/hooks";
|
|||||||
import { push } from "redux-first-history";
|
import { push } from "redux-first-history";
|
||||||
import { ErrorSnackbar } from "./components/Error/ErrorSnackbar";
|
import { ErrorSnackbar } from "./components/Error/ErrorSnackbar";
|
||||||
import I18n from "cozy-ui/transpiled/react/providers/I18n";
|
import I18n from "cozy-ui/transpiled/react/providers/I18n";
|
||||||
|
import { AVAILABLE_LANGUAGES } from "./features/Settings/constants";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
enGB,
|
enGB,
|
||||||
@@ -29,16 +30,33 @@ import vi from "./locales/vi.json";
|
|||||||
const locale = { en, fr, ru, vi };
|
const locale = { en, fr, ru, vi };
|
||||||
const dateLocales = { en: enGB, fr: frLocale, ru: ruLocale, vi: viLocale };
|
const dateLocales = { en: enGB, fr: frLocale, ru: ruLocale, vi: viLocale };
|
||||||
|
|
||||||
|
const SUPPORTED_LANGUAGES = AVAILABLE_LANGUAGES.map((lang) => lang.code);
|
||||||
|
type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
|
||||||
|
|
||||||
|
const isValidLanguage = (
|
||||||
|
lang: string | null | undefined
|
||||||
|
): lang is SupportedLanguage => {
|
||||||
|
return !!lang && SUPPORTED_LANGUAGES.includes(lang as SupportedLanguage);
|
||||||
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const error = useAppSelector((state) => state.user.error);
|
const error = useAppSelector((state) => state.user.error);
|
||||||
const lang = useAppSelector((state) => state.settings.language);
|
const userLanguage = useAppSelector((state) => state.user.language);
|
||||||
|
const settingsLanguage = useAppSelector((state) => state.settings.language);
|
||||||
|
const savedLang = localStorage.getItem("lang");
|
||||||
|
const defaultLang = (window as any).LANG;
|
||||||
|
|
||||||
|
const lang =
|
||||||
|
[userLanguage, settingsLanguage, savedLang, defaultLang].find(
|
||||||
|
(l): l is string => isValidLanguage(l)
|
||||||
|
) || "en";
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) {
|
if (error) {
|
||||||
dispatch(push("/error"));
|
dispatch(push("/error"));
|
||||||
}
|
}
|
||||||
});
|
}, [error, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomThemeProvider>
|
<CustomThemeProvider>
|
||||||
|
|||||||
@@ -12,12 +12,17 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Typography,
|
Typography,
|
||||||
|
Snackbar,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
import SettingsIcon from "@mui/icons-material/Settings";
|
import SettingsIcon from "@mui/icons-material/Settings";
|
||||||
import SyncIcon from "@mui/icons-material/Sync";
|
import SyncIcon from "@mui/icons-material/Sync";
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import { setView, setLanguage } from "./SettingsSlice";
|
import { setView, setLanguage as setSettingsLanguage } from "./SettingsSlice";
|
||||||
|
import {
|
||||||
|
updateUserConfigurationsAsync,
|
||||||
|
setLanguage as setUserLanguage,
|
||||||
|
} from "../User/userSlice";
|
||||||
import { AVAILABLE_LANGUAGES } from "./constants";
|
import { AVAILABLE_LANGUAGES } from "./constants";
|
||||||
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
|
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
|
||||||
import "./SettingsPage.styl";
|
import "./SettingsPage.styl";
|
||||||
@@ -27,11 +32,15 @@ type SettingsSubTab = "settings" | "notifications";
|
|||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t, lang } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const userLanguage = useAppSelector((state) => state.user?.language);
|
||||||
|
const settingsLanguage = useAppSelector((state) => state.settings?.language);
|
||||||
|
const currentLanguage = userLanguage || settingsLanguage || "en";
|
||||||
const [activeNavItem, setActiveNavItem] =
|
const [activeNavItem, setActiveNavItem] =
|
||||||
useState<SidebarNavItem>("settings");
|
useState<SidebarNavItem>("settings");
|
||||||
const [activeSettingsSubTab, setActiveSettingsSubTab] =
|
const [activeSettingsSubTab, setActiveSettingsSubTab] =
|
||||||
useState<SettingsSubTab>("settings");
|
useState<SettingsSubTab>("settings");
|
||||||
|
const [languageErrorOpen, setLanguageErrorOpen] = useState(false);
|
||||||
|
|
||||||
const handleBackClick = () => {
|
const handleBackClick = () => {
|
||||||
dispatch(setView("calendar"));
|
dispatch(setView("calendar"));
|
||||||
@@ -52,7 +61,27 @@ export default function SettingsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleLanguageChange = (event: any) => {
|
const handleLanguageChange = (event: any) => {
|
||||||
dispatch(setLanguage(event.target.value));
|
const newLanguage = event.target.value;
|
||||||
|
const previousLanguage = currentLanguage;
|
||||||
|
|
||||||
|
// Optimistic update - update UI immediately
|
||||||
|
dispatch(setUserLanguage(newLanguage));
|
||||||
|
dispatch(setSettingsLanguage(newLanguage));
|
||||||
|
|
||||||
|
// Call API in background, don't wait for it
|
||||||
|
dispatch(updateUserConfigurationsAsync({ language: newLanguage }))
|
||||||
|
.unwrap()
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Failed to update language:", error);
|
||||||
|
// Rollback on error
|
||||||
|
dispatch(setUserLanguage(previousLanguage));
|
||||||
|
dispatch(setSettingsLanguage(previousLanguage));
|
||||||
|
setLanguageErrorOpen(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLanguageErrorClose = () => {
|
||||||
|
setLanguageErrorOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -122,7 +151,7 @@ export default function SettingsPage() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<FormControl size="small" sx={{ minWidth: 500 }}>
|
<FormControl size="small" sx={{ minWidth: 500 }}>
|
||||||
<Select
|
<Select
|
||||||
value={lang}
|
value={currentLanguage}
|
||||||
onChange={handleLanguageChange}
|
onChange={handleLanguageChange}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
aria-label={
|
aria-label={
|
||||||
@@ -157,6 +186,14 @@ export default function SettingsPage() {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Snackbar
|
||||||
|
open={languageErrorOpen}
|
||||||
|
autoHideDuration={4000}
|
||||||
|
onClose={handleLanguageErrorClose}
|
||||||
|
message={
|
||||||
|
t("settings.languageUpdateError") || "Failed to update language"
|
||||||
|
}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,3 +33,38 @@ export async function getUserDetails(id: string) {
|
|||||||
const user = await api.get(`api/users/${id}`).json();
|
const user = await api.get(`api/users/${id}`).json();
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserConfigurationUpdates {
|
||||||
|
language?: string;
|
||||||
|
notifications?: Record<string, unknown>;
|
||||||
|
timezone?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUserConfigurations(
|
||||||
|
updates: UserConfigurationUpdates
|
||||||
|
): Promise<Response | { status: number }> {
|
||||||
|
const coreConfigs: Array<{ name: string; value: any }> = [];
|
||||||
|
|
||||||
|
if (updates.language !== undefined) {
|
||||||
|
coreConfigs.push({ name: "language", value: updates.language });
|
||||||
|
}
|
||||||
|
if (updates.notifications !== undefined) {
|
||||||
|
coreConfigs.push({ name: "notifications", value: updates.notifications });
|
||||||
|
}
|
||||||
|
if (updates.timezone !== undefined) {
|
||||||
|
coreConfigs.push({ name: "timezone", value: updates.timezone });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coreConfigs.length === 0) {
|
||||||
|
return Promise.resolve({ status: 204 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return await api.patch(`api/configurations?scope=user`, {
|
||||||
|
json: [
|
||||||
|
{
|
||||||
|
name: "core",
|
||||||
|
configurations: coreConfigs,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,8 +6,28 @@ export interface userData {
|
|||||||
sid: string;
|
sid: string;
|
||||||
sub: string;
|
sub: string;
|
||||||
openpaasId?: string;
|
openpaasId?: string;
|
||||||
|
language?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserConfigurations {
|
||||||
|
modules?: Array<{
|
||||||
|
name: string;
|
||||||
|
configurations?: Array<{
|
||||||
|
name: string;
|
||||||
|
value: any;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationSettings {
|
||||||
|
email?: boolean;
|
||||||
|
push?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotificationSettingsExtended = NotificationSettings & {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
export interface userOrganiser {
|
export interface userOrganiser {
|
||||||
cn: string;
|
cn: string;
|
||||||
cal_address: string;
|
cal_address: string;
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||||
import { userData, userOrganiser } from "./userDataTypes";
|
import { userData, userOrganiser } from "./userDataTypes";
|
||||||
import { getOpenPaasUser } from "./userAPI";
|
import {
|
||||||
|
getOpenPaasUser,
|
||||||
|
updateUserConfigurations,
|
||||||
|
UserConfigurationUpdates,
|
||||||
|
} from "./userAPI";
|
||||||
import { formatReduxError } from "../../utils/errorUtils";
|
import { formatReduxError } from "../../utils/errorUtils";
|
||||||
|
|
||||||
export const getOpenPaasUserDataAsync = createAsyncThunk<
|
export const getOpenPaasUserDataAsync = createAsyncThunk<
|
||||||
Record<string, string>,
|
Record<string, any>,
|
||||||
void,
|
void,
|
||||||
{ rejectValue: { message: string; status?: number } }
|
{ rejectValue: { message: string; status?: number } }
|
||||||
>("user/getOpenPaasUserData", async (_, { rejectWithValue }) => {
|
>("user/getOpenPaasUserData", async (_, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const user = (await getOpenPaasUser()) as Record<string, string>;
|
const user = (await getOpenPaasUser()) as Record<string, any>;
|
||||||
return user;
|
return user;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue({
|
return rejectWithValue({
|
||||||
@@ -19,12 +23,29 @@ export const getOpenPaasUserDataAsync = createAsyncThunk<
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const updateUserConfigurationsAsync = createAsyncThunk<
|
||||||
|
UserConfigurationUpdates,
|
||||||
|
UserConfigurationUpdates,
|
||||||
|
{ rejectValue: { message: string; status?: number } }
|
||||||
|
>("user/updateConfigurations", async (updates, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
await updateUserConfigurations(updates);
|
||||||
|
return updates;
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const userSlice = createSlice({
|
export const userSlice = createSlice({
|
||||||
name: "user",
|
name: "user",
|
||||||
initialState: {
|
initialState: {
|
||||||
userData: null as unknown as userData,
|
userData: null as unknown as userData,
|
||||||
organiserData: null as unknown as userOrganiser,
|
organiserData: null as unknown as userOrganiser,
|
||||||
tokens: null as unknown as Record<string, string>,
|
tokens: null as unknown as Record<string, string>,
|
||||||
|
language: null as string | null,
|
||||||
loading: true,
|
loading: true,
|
||||||
error: null as unknown as string | null,
|
error: null as unknown as string | null,
|
||||||
},
|
},
|
||||||
@@ -41,6 +62,12 @@ export const userSlice = createSlice({
|
|||||||
setTokens: (state, action) => {
|
setTokens: (state, action) => {
|
||||||
state.tokens = action.payload;
|
state.tokens = action.payload;
|
||||||
},
|
},
|
||||||
|
setLanguage: (state, action) => {
|
||||||
|
state.language = action.payload;
|
||||||
|
if (state.userData) {
|
||||||
|
state.userData.language = action.payload;
|
||||||
|
}
|
||||||
|
},
|
||||||
clearError: (state) => {
|
clearError: (state) => {
|
||||||
state.error = null;
|
state.error = null;
|
||||||
},
|
},
|
||||||
@@ -62,6 +89,22 @@ export const userSlice = createSlice({
|
|||||||
state.organiserData.cal_address = action.payload.preferredEmail;
|
state.organiserData.cal_address = action.payload.preferredEmail;
|
||||||
state.userData.email = action.payload.preferredEmail;
|
state.userData.email = action.payload.preferredEmail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract language from configurations.modules
|
||||||
|
if (action.payload.configurations?.modules) {
|
||||||
|
const coreModule = action.payload.configurations.modules.find(
|
||||||
|
(module: any) => module.name === "core"
|
||||||
|
);
|
||||||
|
if (coreModule?.configurations) {
|
||||||
|
const languageConfig = coreModule.configurations.find(
|
||||||
|
(config: any) => config.name === "language"
|
||||||
|
);
|
||||||
|
if (languageConfig?.value) {
|
||||||
|
state.language = languageConfig.value;
|
||||||
|
state.userData.language = languageConfig.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.addCase(getOpenPaasUserDataAsync.pending, (state) => {
|
.addCase(getOpenPaasUserDataAsync.pending, (state) => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
@@ -72,11 +115,26 @@ export const userSlice = createSlice({
|
|||||||
state.error =
|
state.error =
|
||||||
action.payload?.message || "Failed to fetch user information";
|
action.payload?.message || "Failed to fetch user information";
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.addCase(updateUserConfigurationsAsync.fulfilled, (state, action) => {
|
||||||
|
if (action.payload.language !== undefined) {
|
||||||
|
state.language = action.payload.language;
|
||||||
|
if (state.userData) {
|
||||||
|
state.userData.language = action.payload.language;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(updateUserConfigurationsAsync.rejected, (state, action) => {
|
||||||
|
if (action.payload?.status !== 401) {
|
||||||
|
state.error =
|
||||||
|
action.payload?.message || "Failed to update user configurations";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Action creators are generated for each case reducer function
|
// Action creators are generated for each case reducer function
|
||||||
export const { setUserData, setTokens, clearError } = userSlice.actions;
|
export const { setUserData, setTokens, setLanguage, clearError } =
|
||||||
|
userSlice.actions;
|
||||||
|
|
||||||
export default userSlice.reducer;
|
export default userSlice.reducer;
|
||||||
|
|||||||
Reference in New Issue
Block a user