[#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
+3 -1
View File
@@ -41,7 +41,9 @@ const isValidLanguage = (
function App() {
const error = useAppSelector((state) => state.user.error);
const userLanguage = useAppSelector((state) => state.user.language);
const userLanguage = useAppSelector(
(state) => state.user.coreConfig.language
);
const settingsLanguage = useAppSelector((state) => state.settings.language);
const savedLang = localStorage.getItem("lang");
const defaultLang = (window as any).LANG;
+6 -5
View File
@@ -10,10 +10,7 @@ import { useAppDispatch, useAppSelector } from "../../app/hooks";
import EventPopover from "../../features/Events/EventModal";
import { CalendarEvent } from "../../features/Events/EventsTypes";
import CalendarSelection from "./CalendarSelection";
import {
getCalendarDetailAsync,
setTimeZone,
} from "../../features/Calendars/CalendarSlice";
import { getCalendarDetailAsync } from "../../features/Calendars/CalendarSlice";
import ImportAlert from "../../features/Events/ImportAlert";
import {
formatDateToYYYYMMDDTHHMMSS,
@@ -46,6 +43,8 @@ import frLocale from "@fullcalendar/core/locales/fr";
import ruLocale from "@fullcalendar/core/locales/ru";
import viLocale from "@fullcalendar/core/locales/vi";
import SearchResultsPage from "../../features/Search/SearchResultsPage";
import { setTimeZone } from "../../features/Settings/SettingsSlice";
import { browserDefaultTimeZone } from "../../utils/timezone";
const localeMap: Record<string, any> = {
fr: frLocale,
@@ -108,7 +107,9 @@ export default function CalendarApp({
});
const [currentView, setCurrentView] = useState("timeGridWeek");
const timezone = useAppSelector((state) => state.calendars.timeZone);
const timezone =
useAppSelector((state) => state.settings.timeZone) ??
browserDefaultTimeZone;
const fetchedRangesRef = useRef<Record<string, string>>({});
+11 -8
View File
@@ -2,6 +2,7 @@ import { Button, Popover } from "@mui/material";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import moment from "moment";
import { MouseEvent, useMemo, useState } from "react";
import { browserDefaultTimeZone } from "../../utils/timezone";
import { TIMEZONES } from "../../utils/timezone-data";
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
@@ -18,14 +19,7 @@ export function TimezoneSelector({
}: TimezoneSelectProps) {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const timezoneList = useMemo(() => {
const zones = Object.keys(TIMEZONES.zones).sort();
const browserTz = resolveTimezone(
Intl.DateTimeFormat().resolvedOptions().timeZone
);
return { zones, browserTz, getTimezoneOffset };
}, []);
const timezoneList = useTimeZoneList();
const effectiveTimezone = value
? resolveTimezone(value)
@@ -97,6 +91,15 @@ export function TimezoneSelector({
);
}
export function useTimeZoneList() {
return useMemo(() => {
const zones = Object.keys(TIMEZONES.zones).sort();
const browserTz = resolveTimezone(browserDefaultTimeZone);
return { zones, browserTz, getTimezoneOffset };
}, []);
}
export function resolveTimezone(tzName: string): string {
if (TIMEZONES.zones[tzName]) {
return tzName;
+4 -9
View File
@@ -30,6 +30,7 @@ import { User } from "../../components/Attendees/PeopleSearch";
import { getCalendarVisibility } from "../../components/Calendar/utils/calendarUtils";
import { importFile } from "../../utils/apiUtils";
import { formatReduxError } from "../../utils/errorUtils";
import { browserDefaultTimeZone } from "../../utils/timezone";
// Define error type for rejected actions
interface RejectedError {
@@ -653,13 +654,11 @@ const CalendarSlice = createSlice({
templist: {} as Record<string, Calendars>,
pending: false,
error: null as string | null,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
} as {
list: Record<string, Calendars>;
templist: Record<string, Calendars>;
pending: boolean;
error: string | null;
timeZone: string;
},
reducers: {
createCalendar: (
@@ -720,9 +719,6 @@ const CalendarSlice = createSlice({
const { calId, event } = action.payload;
state.list[calId].events[event.uid] = event;
},
setTimeZone: (state, action: PayloadAction<string>) => {
state.timeZone = action.payload;
},
clearFetchCache: (state, action: PayloadAction<string>) => {
if (!state.list[action.payload]) return;
state.list[action.payload].lastCacheCleared = Date.now();
@@ -795,7 +791,7 @@ const CalendarSlice = createSlice({
action.payload.calId;
if (!state[type][action.payload.calId].events[id].timezone) {
state[type][action.payload.calId].events[id].timezone =
Intl.DateTimeFormat().resolvedOptions().timeZone;
browserDefaultTimeZone;
}
}
);
@@ -831,7 +827,7 @@ const CalendarSlice = createSlice({
action.payload.calId;
if (!state[type][action.payload.calId].events[id].timezone) {
state[type][action.payload.calId].events[id].timezone =
Intl.DateTimeFormat().resolvedOptions().timeZone;
browserDefaultTimeZone;
}
}
);
@@ -878,7 +874,7 @@ const CalendarSlice = createSlice({
action.payload.calId;
if (!state.list[action.payload.calId].events[id].timezone) {
state.list[action.payload.calId].events[id].timezone =
Intl.DateTimeFormat().resolvedOptions().timeZone;
browserDefaultTimeZone;
}
});
}
@@ -1172,7 +1168,6 @@ export const {
updateEventLocal,
removeTempCal,
emptyEventsCal,
setTimeZone,
clearFetchCache,
clearError,
updateCalColor,
+18 -6
View File
@@ -49,6 +49,7 @@ import { CalendarEvent } from "./EventsTypes";
import EventUpdateModal from "./EventUpdateModal";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import { userAttendee } from "../User/userDataTypes";
import { browserDefaultTimeZone } from "../../utils/timezone";
export default function EventPreviewModal({
eventId,
@@ -67,8 +68,8 @@ export default function EventPreviewModal({
const dispatch = useAppDispatch();
const calendars = useAppSelector((state) => state.calendars);
const timezone =
useAppSelector((state) => state.calendars.timeZone) ??
Intl.DateTimeFormat().resolvedOptions().timeZone;
useAppSelector((state) => state.settings.timeZone) ??
browserDefaultTimeZone;
const calendarList = Object.values(
useAppSelector((state) => state.calendars.list)
);
@@ -456,10 +457,10 @@ export default function EventPreviewModal({
)}
</Box>
<Typography color="text.secondaryContainer" gutterBottom>
{formatDate(event.start, t, event.allday)}
{formatDate(event.start, t, timezone, event.allday)}
{event.end &&
formatEnd(event.start, event.end, t, event.allday) &&
` ${formatEnd(event.start, event.end, t, event.allday)} ${!event.allday ? getTimezoneOffset(timezone, new Date(event.start)) : ""}`}
formatEnd(event.start, event.end, t, timezone, event.allday) &&
` ${formatEnd(event.start, event.end, t, timezone, event.allday)} ${!event.allday ? getTimezoneOffset(timezone, new Date(event.start)) : ""}`}
</Typography>
</>
}
@@ -952,13 +953,19 @@ function makeRecurrenceString(
return recur.join(", ");
}
function formatDate(date: Date | string, t: Function, allday?: boolean) {
function formatDate(
date: Date | string,
t: Function,
timeZone: string,
allday?: boolean
) {
if (allday) {
return new Date(date).toLocaleDateString(t("locale"), {
year: "numeric",
month: "long",
weekday: "long",
day: "numeric",
timeZone,
});
} else {
return new Date(date).toLocaleString(t("locale"), {
@@ -968,6 +975,7 @@ function formatDate(date: Date | string, t: Function, allday?: boolean) {
day: "numeric",
hour: "2-digit",
minute: "2-digit",
timeZone,
});
}
}
@@ -976,6 +984,7 @@ function formatEnd(
start: Date | string,
end: Date | string,
t: Function,
timeZone: string,
allday?: boolean
) {
const startDate = new Date(start);
@@ -993,12 +1002,14 @@ function formatEnd(
year: "numeric",
month: "short",
day: "numeric",
timeZone,
});
} else {
if (sameDay) {
return endDate.toLocaleTimeString(t("locale"), {
hour: "2-digit",
minute: "2-digit",
timeZone,
});
}
return endDate.toLocaleString(t("locale"), {
@@ -1007,6 +1018,7 @@ function formatEnd(
day: "numeric",
hour: "2-digit",
minute: "2-digit",
timeZone,
});
}
}
+4 -6
View File
@@ -41,6 +41,7 @@ import {
restoreFormDataFromTemp,
EventFormState,
} from "../../utils/eventFormTempStorage";
import { browserDefaultTimeZone } from "../../utils/timezone";
function EventPopover({
open,
@@ -84,17 +85,14 @@ function EventPopover({
const timezoneList = useMemo(() => {
const zones = Object.keys(TIMEZONES.zones).sort();
const browserTz = resolveTimezone(
Intl.DateTimeFormat().resolvedOptions().timeZone
);
const browserTz = resolveTimezone(browserDefaultTimeZone);
return { zones, browserTz, getTimezoneOffset };
}, []);
const calendarTimezone = useAppSelector((state) => state.calendars.timeZone);
const calendarTimezone = useAppSelector((state) => state.settings.timeZone);
const resolvedCalendarTimezone = useMemo(() => {
const tz =
calendarTimezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
const tz = calendarTimezone || browserDefaultTimeZone;
return resolveTimezone(tz);
}, [calendarTimezone]);
+6 -11
View File
@@ -41,6 +41,7 @@ import {
EventFormState,
EventFormContext,
} from "../../utils/eventFormTempStorage";
import { browserDefaultTimeZone } from "../../utils/timezone";
function EventUpdateModal({
eventId,
@@ -114,9 +115,7 @@ function EventUpdateModal({
const timezoneList = useMemo(() => {
const zones = Object.keys(TIMEZONES.zones).sort();
const browserTz = resolveTimezone(
Intl.DateTimeFormat().resolvedOptions().timeZone
);
const browserTz = resolveTimezone(browserDefaultTimeZone);
const getTimezoneOffset = (tzName: string): string => {
const resolvedTz = resolveTimezone(tzName);
@@ -161,7 +160,7 @@ function EventUpdateModal({
const [busy, setBusy] = useState("OPAQUE");
const [eventClass, setEventClass] = useState("PUBLIC");
const [timezone, setTimezone] = useState(
resolveTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone)
resolveTimezone(browserDefaultTimeZone)
);
const [newCalId, setNewCalId] = useState(calId);
const defaultCalendarId = useMemo(
@@ -196,9 +195,7 @@ function EventUpdateModal({
setAlarm("");
setEventClass("PUBLIC");
setBusy("OPAQUE");
setTimezone(
resolveTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone)
);
setTimezone(resolveTimezone(browserDefaultTimeZone));
setHasVideoConference(false);
setMeetingLink(null);
}, [defaultCalendarId]);
@@ -230,7 +227,7 @@ function EventUpdateModal({
// Get event's original timezone
const eventTimezone = event.timezone
? resolveTimezone(event.timezone)
: resolveTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
: resolveTimezone(browserDefaultTimeZone);
// Format dates based on all-day status
if (event.start) {
@@ -298,9 +295,7 @@ function EventUpdateModal({
const resolvedTimezone = resolveTimezone(event.timezone);
setTimezone(resolvedTimezone);
} else {
const browserTz = resolveTimezone(
Intl.DateTimeFormat().resolvedOptions().timeZone
);
const browserTz = resolveTimezone(browserDefaultTimeZone);
setTimezone(browserTz);
}
setHasVideoConference(event.x_openpass_videoconference ? true : false);
+3 -5
View File
@@ -4,6 +4,7 @@ import { CalendarEvent, RepetitionObject } from "./EventsTypes";
import { userAttendee } from "../User/userDataTypes";
import { formatDateTimeInTimezone } from "../../components/Event/utils/dateTimeFormatters";
import { addVideoConferenceToDescription } from "../../utils/videoConferenceUtils";
import { browserDefaultTimeZone } from "../../utils/timezone";
export interface TimezoneListResult {
zones: string[];
@@ -13,9 +14,7 @@ export interface TimezoneListResult {
export function createTimezoneList(): TimezoneListResult {
const zones = Object.keys(TIMEZONES.zones).sort();
const browserTz = resolveTimezone(
Intl.DateTimeFormat().resolvedOptions().timeZone
);
const browserTz = resolveTimezone(browserDefaultTimeZone);
const getTimezoneOffset = (tzName: string): string => {
const resolvedTz = resolveTimezone(tzName);
@@ -101,8 +100,7 @@ export function populateFormFromEvent(
// Get event's timezone for formatting
const eventTimezone = event.timezone
? resolveTimezone(event.timezone)
: calendarTimezone ||
resolveTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
: calendarTimezone || resolveTimezone(browserDefaultTimeZone);
// Format dates based on all-day status and timezone
if (event.start) {
+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;
+10 -3
View File
@@ -2,8 +2,8 @@ import { User } from "../../components/Attendees/PeopleSearch";
import { api } from "../../utils/apiUtils";
export async function getOpenPaasUser() {
const user = await api.get(`api/user`).json();
return user;
const user = await api.get(`api/user`);
return user.json();
}
export async function searchUsers(
@@ -38,6 +38,7 @@ export interface UserConfigurationUpdates {
language?: string;
notifications?: Record<string, unknown>;
timezone?: string;
previousConfig?: Record<string, any>;
}
export async function updateUserConfigurations(
@@ -52,7 +53,13 @@ export async function updateUserConfigurations(
coreConfigs.push({ name: "notifications", value: updates.notifications });
}
if (updates.timezone !== undefined) {
coreConfigs.push({ name: "timezone", value: updates.timezone });
coreConfigs.push({
name: "datetime",
value: {
...updates.previousConfig?.datetime,
timeZone: updates.timezone,
},
});
}
if (coreConfigs.length === 0) {
+1
View File
@@ -7,6 +7,7 @@ export interface userData {
sub: string;
openpaasId?: string;
language?: string;
timezone?: string | null;
}
export interface UserConfigurations {
+65 -7
View File
@@ -45,7 +45,12 @@ export const userSlice = createSlice({
userData: null as unknown as userData,
organiserData: null as unknown as userOrganiser,
tokens: null as unknown as Record<string, string>,
language: null as string | null,
coreConfig: {
language: null as string | null,
datetime: {
timeZone: null as string | null,
},
} as Record<string, any>,
loading: true,
error: null as unknown as string | null,
},
@@ -63,11 +68,20 @@ export const userSlice = createSlice({
state.tokens = action.payload;
},
setLanguage: (state, action) => {
state.language = action.payload;
state.coreConfig.language = action.payload;
if (state.userData) {
state.userData.language = action.payload;
}
},
setTimezone: (state, action) => {
if (!state.coreConfig.datetime) {
state.coreConfig.datetime = {};
}
state.coreConfig.datetime.timeZone = action.payload;
if (state.userData) {
state.userData.timezone = action.payload;
}
},
clearError: (state) => {
state.error = null;
},
@@ -90,18 +104,53 @@ export const userSlice = createSlice({
state.userData.email = action.payload.preferredEmail;
}
// Extract language from configurations.modules
// Extract data from configurations.modules
if (action.payload.configurations?.modules) {
const coreModule = action.payload.configurations.modules.find(
(module: any) => module.name === "core"
);
if (coreModule?.configurations) {
const newCoreConfig = Object.fromEntries(
coreModule.configurations.map(
(e: { name: string; value: any }) => [e.name, e.value]
)
);
state.coreConfig = {
...state.coreConfig,
...newCoreConfig,
};
const languageConfig = coreModule.configurations.find(
(config: any) => config.name === "language"
);
if (languageConfig?.value) {
state.language = languageConfig.value;
state.userData.language = languageConfig.value;
state.coreConfig.language = languageConfig.value;
if (state.userData)
state.userData.language = languageConfig.value;
}
const datetimeConfig = coreModule.configurations.find(
(config: any) => config.name === "datetime"
);
if (datetimeConfig?.value) {
const serverTimeZone = datetimeConfig.value.timeZone;
state.coreConfig.datetime = {
...state.coreConfig.datetime,
...datetimeConfig.value,
timeZone: serverTimeZone !== undefined ? serverTimeZone : null,
};
if (state.userData) {
state.userData.timezone =
serverTimeZone !== undefined ? serverTimeZone : null;
}
} else {
state.coreConfig.datetime = {
...state.coreConfig.datetime,
timeZone: null,
};
if (state.userData) {
state.userData.timezone = null;
}
}
}
}
@@ -118,11 +167,20 @@ export const userSlice = createSlice({
})
.addCase(updateUserConfigurationsAsync.fulfilled, (state, action) => {
if (action.payload.language !== undefined) {
state.language = action.payload.language;
state.coreConfig.language = action.payload.language;
if (state.userData) {
state.userData.language = action.payload.language;
}
}
if (action.payload.timezone !== undefined) {
if (!state.coreConfig.datetime) {
state.coreConfig.datetime = {};
}
state.coreConfig.datetime.timeZone = action.payload.timezone;
if (state.userData) {
state.userData.timezone = action.payload.timezone;
}
}
})
.addCase(updateUserConfigurationsAsync.rejected, (state, action) => {
if (action.payload?.status !== 401) {
@@ -134,7 +192,7 @@ export const userSlice = createSlice({
});
// Action creators are generated for each case reducer function
export const { setUserData, setTokens, setLanguage, clearError } =
export const { setUserData, setTokens, setLanguage, setTimezone, clearError } =
userSlice.actions;
export default userSlice.reducer;
+4
View File
@@ -212,6 +212,10 @@
"language": "Language",
"languageDescription": "This will be the language used in your Twake Calendar",
"languageSelector": "Language selector",
"languageUpdateError": "Failed to update language",
"timeZone": "Timezone",
"timeZoneBrowserDefault": "Detect time zone automatically",
"timeZoneUpdateError": "Failed to update time zone",
"notifications.empty": "Notifications settings coming soon",
"sync.empty": "Sync settings coming soon",
"back": "Back to calendar"
+4
View File
@@ -212,6 +212,10 @@
"language": "Langue",
"languageDescription": "Ce sera la langue utilisée dans votre Twake Calendar",
"languageSelector": "Sélecteur de langue",
"languageUpdateError": "Échec de la mise à jour de la langue",
"timeZone": "Fuseau horaire",
"timeZoneBrowserDefault": "Détection automatique du fuseau horaire",
"timeZoneUpdateError": "Échec de la mise à jour du fuseau horaire",
"notifications.empty": "Paramètres de notifications à venir",
"sync.empty": "Paramètres de synchronisation à venir",
"back": "Retour au calendrier"
+4
View File
@@ -212,6 +212,10 @@
"language": "Язык",
"languageDescription": "Это будет язык, используемый в вашем Twake Calendar",
"languageSelector": "Выбор языка",
"languageUpdateError": "Не удалось обновить язык",
"timeZone": "Часовой пояс",
"timeZoneBrowserDefault": "Определять часовой пояс автоматически",
"timeZoneUpdateError": "Не удалось обновить часовой пояс",
"notifications.empty": "Настройки уведомлений скоро появятся",
"sync.empty": "Настройки синхронизации скоро появятся",
"back": "Вернуться к календарю"
+4
View File
@@ -212,6 +212,10 @@
"language": "Ngôn ngữ",
"languageDescription": "Đây sẽ là ngôn ngữ được sử dụng trong Twake Calendar của bạn",
"languageSelector": "Chọn ngôn ngữ",
"languageUpdateError": "Không cập nhật được ngôn ngữ",
"timeZone": "Múi giờ",
"timeZoneBrowserDefault": "Tự động phát hiện múi giờ",
"timeZoneUpdateError": "Không cập nhật được múi giờ",
"notifications.empty": "Cài đặt thông báo sắp có",
"sync.empty": "Cài đặt đồng bộ sắp có",
"back": "Quay lại lịch"
+3
View File
@@ -6,6 +6,9 @@ import ICAL from "ical.js";
// TIMEZONES data must be imported or defined separately.
import { TIMEZONES } from "./timezone-data";
export const browserDefaultTimeZone =
Intl.DateTimeFormat().resolvedOptions().timeZone;
// Core timezone registration functionality
export function registerTimezones() {
for (const [key, data] of Object.entries(TIMEZONES.zones)) {