[#337] added timezone loading from backend (#386)

* [#337] added timezone loading from backend

* [#337] changed data structure to allow patching timezone while keeping other datetime params value

* [#337] fixed gap, event create modale sync with settings and added checkbox

* [#337] isBrowserDefaultTimezone is set to true when API return null timezone

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2025-12-04 15:14:43 +01:00
committed by GitHub
parent d8f5f9ae46
commit 193c1a7166
23 changed files with 1021 additions and 109 deletions
+154 -29
View File
@@ -13,19 +13,33 @@ import {
MenuItem,
Typography,
Snackbar,
FormControlLabel,
Switch,
} from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
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 { setView, setLanguage as setSettingsLanguage } from "./SettingsSlice";
import {
setView,
setLanguage as setSettingsLanguage,
setTimeZone as setSettingsTimeZone,
setIsBrowserDefaultTimeZone,
} from "./SettingsSlice";
import {
updateUserConfigurationsAsync,
setLanguage as setUserLanguage,
setTimezone as setUserTimeZone,
} from "../User/userSlice";
import { AVAILABLE_LANGUAGES } from "./constants";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import "./SettingsPage.styl";
import {
useTimeZoneList,
getTimezoneOffset,
} from "../../components/Calendar/TimezoneSelector";
import { TimezoneAutocomplete } from "../../components/Timezone/TimezoneAutocomplete";
import { browserDefaultTimeZone } from "../../utils/timezone";
type SidebarNavItem = "settings" | "sync";
type SettingsSubTab = "settings" | "notifications";
@@ -33,14 +47,30 @@ type SettingsSubTab = "settings" | "notifications";
export default function SettingsPage() {
const dispatch = useAppDispatch();
const { t } = useI18n();
const userLanguage = useAppSelector((state) => state.user?.language);
// const previousConfig = useAppSelector((state) => state.user.coreConfig);
const userLanguage = useAppSelector(
(state) => state.user?.coreConfig.language
);
const settingsLanguage = useAppSelector((state) => state.settings?.language);
const currentLanguage = userLanguage || settingsLanguage || "en";
const timezoneList = useTimeZoneList();
const userTimeZone = useAppSelector(
(state) => state.user?.coreConfig?.datetime?.timeZone
);
const settingTimeZone = useAppSelector((state) => state.settings?.timeZone);
const currentTimeZone =
userTimeZone ?? settingTimeZone ?? browserDefaultTimeZone;
const isBrowserDefault = useAppSelector(
(state) => state.settings.isBrowserDefaultTimeZone
);
const [activeNavItem, setActiveNavItem] =
useState<SidebarNavItem>("settings");
const [activeSettingsSubTab, setActiveSettingsSubTab] =
useState<SettingsSubTab>("settings");
const [languageErrorOpen, setLanguageErrorOpen] = useState(false);
const [timeZoneErrorOpen, setTimeZoneErrorOpen] = useState(false);
const handleBackClick = () => {
dispatch(setView("calendar"));
@@ -84,6 +114,53 @@ export default function SettingsPage() {
setLanguageErrorOpen(false);
};
const handleTimeZoneChange = (newTimeZone: string) => {
// 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);
// });
};
const handleTimeZoneDefaultChange = (isDefault: boolean) => {
// 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);
// });
};
const handleTimeZoneErrorClose = () => {
setTimeZoneErrorOpen(false);
};
return (
<main className="main-layout settings-layout">
<Box className="settings-sidebar">
@@ -138,33 +215,75 @@ export default function SettingsPage() {
<>
{activeSettingsSubTab === "settings" && (
<Box className="settings-tab-content">
<Typography variant="h6" sx={{ mb: 1 }}>
{t("settings.language") || "Language"}
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{ mb: 3 }}
>
{t("settings.languageDescription") ||
"This will be the language used in your Twake Calendar"}
</Typography>
<FormControl size="small" sx={{ minWidth: 500 }}>
<Select
value={currentLanguage}
onChange={handleLanguageChange}
variant="outlined"
aria-label={
t("settings.languageSelector") || "Language selector"
}
<Box sx={{ mb: 6 }}>
<Typography variant="h6" sx={{ mb: 1 }}>
{t("settings.language") || "Language"}
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{ mb: 3 }}
>
{AVAILABLE_LANGUAGES.map(({ code, label }) => (
<MenuItem key={code} value={code}>
{label}
</MenuItem>
))}
</Select>
</FormControl>
{t("settings.languageDescription") ||
"This will be the language used in your Twake Calendar"}
</Typography>
<FormControl size="small" sx={{ minWidth: 500 }}>
<Select
value={currentLanguage}
onChange={handleLanguageChange}
variant="outlined"
aria-label={
t("settings.languageSelector") || "Language selector"
}
>
{AVAILABLE_LANGUAGES.map(({ code, label }) => (
<MenuItem key={code} value={code}>
{label}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
<Box>
<Typography variant="h6" sx={{ mb: 1 }}>
{t("settings.timeZone")}
</Typography>
<Box
sx={{
mb: 6,
}}
>
<FormControl size="small" sx={{ minWidth: 500 }}>
<FormControlLabel
control={
<Switch
checked={isBrowserDefault}
onChange={() =>
handleTimeZoneDefaultChange(!isBrowserDefault)
}
aria-label={t("settings.timeZoneBrowserDefault")}
/>
}
label={t("settings.timeZoneBrowserDefault")}
labelPlacement="start"
sx={{
minWidth: 400,
justifyContent: "space-between",
marginLeft: 0,
mb: 2,
}}
/>
{!isBrowserDefault && (
<TimezoneAutocomplete
value={currentTimeZone}
zones={timezoneList.zones}
getTimezoneOffset={getTimezoneOffset}
onChange={handleTimeZoneChange}
/>
)}
</FormControl>
</Box>
</Box>
</Box>
)}
{activeSettingsSubTab === "notifications" && (
@@ -194,6 +313,12 @@ export default function SettingsPage() {
t("settings.languageUpdateError") || "Failed to update language"
}
/>
<Snackbar
open={timeZoneErrorOpen}
autoHideDuration={4000}
onClose={handleTimeZoneErrorClose}
message={t("settings.timeZoneUpdateError")}
/>
</main>
);
}
+45 -1
View File
@@ -1,15 +1,26 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { browserDefaultTimeZone } from "../../utils/timezone";
import { getOpenPaasUserDataAsync } from "../User/userSlice";
export interface SettingsState {
language: string;
timeZone: string | null; // Allow null to represent browser default
isBrowserDefaultTimeZone: boolean;
view: "calendar" | "settings" | "search";
}
const savedLang = localStorage.getItem("lang");
const defaultLang = savedLang ?? (window as any).LANG ?? "en";
const savedTimeZone = localStorage.getItem("timeZone");
// If savedTimeZone is the string "null" or doesn't exist, use null
const defaultTimeZone =
savedTimeZone === "null" || !savedTimeZone ? null : savedTimeZone;
const initialState: SettingsState = {
language: defaultLang,
timeZone: defaultTimeZone,
isBrowserDefaultTimeZone: defaultTimeZone === null,
view: "calendar",
};
@@ -21,6 +32,13 @@ export const settingsSlice = createSlice({
state.language = action.payload;
localStorage.setItem("lang", action.payload);
},
setTimeZone: (state, action: PayloadAction<string>) => {
state.timeZone = action.payload;
localStorage.setItem("timeZone", action.payload);
},
setIsBrowserDefaultTimeZone: (state, action: PayloadAction<boolean>) => {
state.isBrowserDefaultTimeZone = action.payload;
},
setView: (
state,
action: PayloadAction<"calendar" | "settings" | "search">
@@ -28,7 +46,33 @@ export const settingsSlice = createSlice({
state.view = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(getOpenPaasUserDataAsync.fulfilled, (state, action) => {
const coreModule = action.payload.configurations?.modules?.find(
(module: any) => module.name === "core"
);
const datetimeConfig = coreModule?.configurations?.find(
(config: any) => config.name === "datetime"
);
const timeZone = datetimeConfig?.value?.timeZone;
if (timeZone) {
state.timeZone = timeZone;
state.isBrowserDefaultTimeZone = false;
localStorage.setItem("timeZone", timeZone);
} else {
state.timeZone = browserDefaultTimeZone;
state.isBrowserDefaultTimeZone = true;
localStorage.setItem("timeZone", browserDefaultTimeZone);
}
});
},
});
export const { setLanguage, setView } = settingsSlice.actions;
export const {
setLanguage,
setTimeZone,
setView,
setIsBrowserDefaultTimeZone,
} = settingsSlice.actions;
export default settingsSlice.reducer;