* [#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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user