diff --git a/__test__/features/Settings/SettingUserSync.test.tsx b/__test__/features/Settings/SettingUserSync.test.tsx
index d596209..5a68ef8 100644
--- a/__test__/features/Settings/SettingUserSync.test.tsx
+++ b/__test__/features/Settings/SettingUserSync.test.tsx
@@ -13,6 +13,8 @@ import { api } from "../../../src/utils/apiUtils";
import { browserDefaultTimeZone } from "../../../src/utils/timezone";
import { renderWithProviders } from "../../utils/Renderwithproviders";
+jest.mock("../../../src/utils/apiUtils");
+
describe("Timezone synchronization after getOpenPaasUserDataAsync", () => {
let apiGetSpy: jest.SpyInstance;
@@ -315,6 +317,9 @@ describe("Timezone Logic - Backend to Frontend Flow", () => {
});
describe("User Actions - Changing Timezone", () => {
+ beforeEach(() => {
+ (api.patch as jest.Mock).mockResolvedValue({ status: 204 });
+ });
test("User enables browser default => Settings gets browser TZ, User gets null", async () => {
const { store } = renderWithProviders(, {
user: {
diff --git a/__test__/features/Settings/SettingsPage.test.tsx b/__test__/features/Settings/SettingsPage.test.tsx
index b387c1a..658e734 100644
--- a/__test__/features/Settings/SettingsPage.test.tsx
+++ b/__test__/features/Settings/SettingsPage.test.tsx
@@ -24,12 +24,17 @@ describe("SettingsPage", () => {
},
organiserData: null,
tokens: null,
- coreConfig: { language: "en" },
+ coreConfig: {
+ language: "en",
+ datetime: { timeZone: "UTC" },
+ },
loading: false,
error: null,
},
settings: {
language: "en",
+ timeZone: "UTC",
+ isBrowserDefaultTimeZone: false,
view: "settings",
},
};
@@ -110,12 +115,14 @@ describe("SettingsPage", () => {
},
organiserData: null,
tokens: null,
- coreConfig: { language: "fr" },
+ coreConfig: { language: "fr", datetime: { timeZone: "UTC" } },
loading: false,
error: null,
},
settings: {
language: "en",
+ timeZone: "UTC",
+ isBrowserDefaultTimeZone: false,
view: "settings",
},
};
@@ -260,6 +267,9 @@ describe("SettingsPage", () => {
});
describe("Timezone Settings", () => {
+ beforeEach(() => {
+ (api.patch as jest.Mock).mockResolvedValue({ status: 204 });
+ });
it("displays timezone selector in Settings tab", () => {
renderWithProviders(, preloadedState);
@@ -289,6 +299,7 @@ describe("SettingsPage", () => {
settings: {
language: "en",
timeZone: "UTC",
+ isBrowserDefaultTimeZone: false,
view: "settings",
},
};
@@ -298,7 +309,7 @@ describe("SettingsPage", () => {
expect(screen.getByDisplayValue(/America\/New York/i)).toBeDefined();
});
- it("updates timezone immediately (optimistic update)", async () => {
+ it("updates timezone immediately (optimistic update) and calls API", async () => {
const { store } = renderWithProviders(, preloadedState);
const timezoneInput = screen.getAllByRole("combobox")[1];
@@ -319,6 +330,27 @@ describe("SettingsPage", () => {
const state = store.getState();
expect(state.settings.timeZone).toBe("Europe/Paris");
});
+
+ 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: "datetime",
+ value: expect.objectContaining({
+ timeZone: "Europe/Paris",
+ }),
+ }),
+ ]),
+ }),
+ ]),
+ })
+ );
+ });
});
it("handles timezone change with different timezone values", async () => {
@@ -340,7 +372,30 @@ describe("SettingsPage", () => {
expect(state.settings.timeZone).toBe("Asia/Tokyo");
});
- // Test with America/Los_Angeles
+ 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: "datetime",
+ value: expect.objectContaining({
+ timeZone: "Asia/Tokyo",
+ }),
+ }),
+ ]),
+ }),
+ ]),
+ })
+ );
+ });
+
+ jest.clearAllMocks();
+ (api.patch as jest.Mock).mockResolvedValue({ status: 204 });
+
fireEvent.change(timezoneInput, {
target: { value: "America/Los_Angeles" },
});
@@ -357,6 +412,172 @@ describe("SettingsPage", () => {
const state = store.getState();
expect(state.settings.timeZone).toBe("America/Los_Angeles");
});
+
+ 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: "datetime",
+ value: expect.objectContaining({
+ timeZone: "America/Los_Angeles",
+ }),
+ }),
+ ]),
+ }),
+ ]),
+ })
+ );
+ });
+ });
+
+ it("enables browser default timezone and calls API with null", async () => {
+ const { store } = renderWithProviders(, preloadedState);
+
+ const browserDefaultSwitch = screen.getByRole("switch", {
+ name: /settings.timeZoneBrowserDefault/i,
+ });
+
+ expect(browserDefaultSwitch).not.toBeChecked();
+
+ fireEvent.click(browserDefaultSwitch);
+
+ await waitFor(() => {
+ expect(browserDefaultSwitch).toBeChecked();
+ });
+
+ await waitFor(() => {
+ const state = store.getState();
+ expect(state.settings.isBrowserDefaultTimeZone).toBe(true);
+ });
+
+ await waitFor(() => {
+ const state = store.getState();
+ expect(state.user?.coreConfig.datetime.timeZone).toBeNull();
+ });
+
+ 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: "datetime",
+ value: expect.objectContaining({
+ timeZone: null,
+ }),
+ }),
+ ]),
+ }),
+ ]),
+ })
+ );
+ });
+ });
+
+ it("disables browser default timezone and shows timezone selector", async () => {
+ const stateWithBrowserDefault = {
+ ...preloadedState,
+ user: {
+ ...preloadedState.user,
+ coreConfig: {
+ language: "en",
+ datetime: { timeZone: null },
+ },
+ },
+ settings: {
+ ...preloadedState.settings,
+ isBrowserDefaultTimeZone: true,
+ },
+ };
+
+ renderWithProviders(, stateWithBrowserDefault);
+
+ const browserDefaultSwitch = screen.getByRole("switch", {
+ name: /settings.timeZoneBrowserDefault/i,
+ });
+
+ expect(browserDefaultSwitch).toBeChecked();
+
+ expect(screen.getAllByRole("combobox")).toHaveLength(1);
+
+ fireEvent.click(browserDefaultSwitch);
+
+ await waitFor(() => {
+ expect(browserDefaultSwitch).not.toBeChecked();
+ });
+
+ await waitFor(() => {
+ expect(screen.getAllByRole("combobox")).toHaveLength(2);
+ });
+ });
+
+ it("rolls back timezone change if API call fails", async () => {
+ (api.patch as jest.Mock).mockRejectedValue(new Error("API Error"));
+
+ const { store } = renderWithProviders(, preloadedState);
+
+ const timezoneInput = screen.getAllByRole("combobox")[1];
+
+ fireEvent.change(timezoneInput, { target: { value: "Europe/Paris" } });
+ const option = await screen.findByText(/Europe\/Paris/i);
+ fireEvent.click(option);
+
+ await waitFor(() => {
+ const state = store.getState();
+ expect(state.user?.coreConfig.datetime.timeZone).toBe("UTC");
+ });
+ await waitFor(
+ () => {
+ const state = store.getState();
+ expect(state.settings.timeZone).toBe("UTC");
+ },
+ { timeout: 3000 }
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("settings.timeZoneUpdateError")
+ ).toBeInTheDocument();
+ });
+ });
+
+ it("rolls back browser default change if API call fails", async () => {
+ (api.patch as jest.Mock).mockRejectedValue(new Error("API Error"));
+
+ const { store } = renderWithProviders(, preloadedState);
+
+ const browserDefaultSwitch = screen.getByRole("switch", {
+ name: /settings.timeZoneBrowserDefault/i,
+ });
+
+ fireEvent.click(browserDefaultSwitch);
+
+ await waitFor(
+ () => {
+ const state = store.getState();
+ expect(state.settings.isBrowserDefaultTimeZone).toBe(false);
+ },
+ { timeout: 3000 }
+ );
+
+ await waitFor(() => {
+ const state = store.getState();
+ expect(state.user?.coreConfig.datetime.timeZone).toBe("UTC");
+ });
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("settings.timeZoneUpdateError")
+ ).toBeInTheDocument();
+ });
});
it("uses UTC as default timezone when no timezone is set", () => {
@@ -379,6 +600,7 @@ describe("SettingsPage", () => {
settings: {
language: "en",
timeZone: undefined,
+ isBrowserDefaultTimeZone: false,
view: "settings",
},
};
@@ -388,5 +610,53 @@ describe("SettingsPage", () => {
const timezoneInput = screen.getByDisplayValue("(UTC) UTC");
expect(timezoneInput).toBeDefined();
});
+
+ it("preserves other datetime properties when updating timezone", async () => {
+ const stateWithDatetimeConfig = {
+ ...preloadedState,
+ user: {
+ ...preloadedState.user,
+ coreConfig: {
+ language: "en",
+ datetime: {
+ timeZone: "UTC",
+ format: "24h",
+ firstDayOfWeek: 1,
+ },
+ },
+ },
+ };
+
+ renderWithProviders(, stateWithDatetimeConfig);
+
+ const timezoneInput = screen.getAllByRole("combobox")[1];
+
+ fireEvent.change(timezoneInput, { target: { value: "Europe/Paris" } });
+ const option = await screen.findByText(/Europe\/Paris/i);
+ fireEvent.click(option);
+
+ 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: "datetime",
+ value: {
+ timeZone: "Europe/Paris",
+ format: "24h",
+ firstDayOfWeek: 1,
+ },
+ }),
+ ]),
+ }),
+ ]),
+ })
+ );
+ });
+ });
});
});
diff --git a/src/features/Settings/SettingsPage.tsx b/src/features/Settings/SettingsPage.tsx
index ddbdb7b..f197138 100644
--- a/src/features/Settings/SettingsPage.tsx
+++ b/src/features/Settings/SettingsPage.tsx
@@ -47,7 +47,7 @@ type SettingsSubTab = "settings" | "notifications";
export default function SettingsPage() {
const dispatch = useAppDispatch();
const { t } = useI18n();
- // const previousConfig = useAppSelector((state) => state.user.coreConfig);
+ const previousConfig = useAppSelector((state) => state.user.coreConfig);
const userLanguage = useAppSelector(
(state) => state.user?.coreConfig.language
);
@@ -115,46 +115,49 @@ export default function SettingsPage() {
};
const handleTimeZoneChange = (newTimeZone: string) => {
- // const previousTimeZone = currentTimeZone;
+ const previousTimeZone = currentTimeZone;
// Optimistic update - update UI immediately
dispatch(setUserTimeZone(newTimeZone));
dispatch(setSettingsTimeZone(newTimeZone));
- // // Call API in background, don't wait for it
- // dispatch(
- // updateUserConfigurationsAsync({ timezone: newTimeZone, previousConfig })
- // )
- // .unwrap()
- // .catch((error) => {
- // console.error("Failed to update TimeZone:", error);
- // // Rollback on error
- // dispatch(setUserTimeZone(previousTimeZone));
- // dispatch(setSettingsTimeZone(previousTimeZone));
- // setTimeZoneErrorOpen(true);
- // });
+ // Call API in background, don't wait for it
+ dispatch(
+ updateUserConfigurationsAsync({ timezone: newTimeZone, previousConfig })
+ )
+ .unwrap()
+ .catch((error) => {
+ console.error("Failed to update TimeZone:", error);
+ // Rollback on error
+ dispatch(setUserTimeZone(previousTimeZone));
+ dispatch(setSettingsTimeZone(previousTimeZone));
+ setTimeZoneErrorOpen(true);
+ });
};
const handleTimeZoneDefaultChange = (isDefault: boolean) => {
+ const previousTimeZone = currentTimeZone;
+
// Optimistic update - update UI immediately
dispatch(setIsBrowserDefaultTimeZone(isDefault));
if (isDefault) {
dispatch(setUserTimeZone(null));
dispatch(setSettingsTimeZone(browserDefaultTimeZone));
- }
- // // Call API in background, don't wait for it
- // dispatch(
- // updateUserConfigurationsAsync({ timezone: newTimeZone, previousConfig })
- // )
- // .unwrap()
- // .catch((error) => {
- // console.error("Failed to update TimeZone:", error);
- // // Rollback on error
- // dispatch(setUserTimeZone(previousTimeZone));
- // dispatch(setSettingsTimeZone(previousTimeZone));
- // setTimeZoneErrorOpen(true);
- // });
+ // Call API in background, don't wait for it
+ dispatch(
+ updateUserConfigurationsAsync({ timezone: null, previousConfig })
+ )
+ .unwrap()
+ .catch((error) => {
+ console.error("Failed to update TimeZone:", error);
+ // Rollback on error
+ dispatch(setUserTimeZone(previousTimeZone));
+ dispatch(setSettingsTimeZone(previousTimeZone));
+ dispatch(setIsBrowserDefaultTimeZone(!isDefault));
+ setTimeZoneErrorOpen(true);
+ });
+ }
};
const handleTimeZoneErrorClose = () => {
diff --git a/src/features/User/userAPI.ts b/src/features/User/userAPI.ts
index ff0c5d9..741e801 100644
--- a/src/features/User/userAPI.ts
+++ b/src/features/User/userAPI.ts
@@ -37,7 +37,7 @@ export async function getUserDetails(id: string) {
export interface UserConfigurationUpdates {
language?: string;
notifications?: Record;
- timezone?: string;
+ timezone?: string | null;
previousConfig?: Record;
}