[#340] show declined event in settings (#398)

* [#340] show declined event in settings


Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2025-12-09 11:11:20 +01:00
committed by GitHub
parent 2f2ee369fc
commit 3cb445ea79
9 changed files with 140 additions and 13 deletions
+10 -2
View File
@@ -77,6 +77,10 @@ export default function CalendarApp({
} }
}, [dispatch, tokens, userId]); }, [dispatch, tokens, userId]);
const view = useAppSelector((state) => state.settings.view); const view = useAppSelector((state) => state.settings.view);
const userData = useAppSelector((state) => state.user.userData);
const hideDeclinedEvents = useAppSelector(
(state) => state.settings.hideDeclinedEvents
);
const calendars = useAppSelector((state) => state.calendars.list); const calendars = useAppSelector((state) => state.calendars.list);
const tempcalendars = useAppSelector((state) => state.calendars.templist); const tempcalendars = useAppSelector((state) => state.calendars.templist);
const [selectedCalendars, setSelectedCalendars] = useState<string[]>([]); const [selectedCalendars, setSelectedCalendars] = useState<string[]>([]);
@@ -221,7 +225,9 @@ export default function CalendarApp({
let filteredEvents: CalendarEvent[] = extractEvents( let filteredEvents: CalendarEvent[] = extractEvents(
selectedCalendars, selectedCalendars,
calendars || {} calendars || {},
userData?.email,
hideDeclinedEvents
); );
const tempCalendarIds = useMemo( const tempCalendarIds = useMemo(
@@ -231,7 +237,9 @@ export default function CalendarApp({
let filteredTempEvents: CalendarEvent[] = extractEvents( let filteredTempEvents: CalendarEvent[] = extractEvents(
tempCalendarIds, tempCalendarIds,
tempcalendars || {} tempcalendars || {},
userData?.email,
hideDeclinedEvents
); );
const sortedSelectedCalendars = useMemo( const sortedSelectedCalendars = useMemo(
+20 -11
View File
@@ -138,21 +138,30 @@ export const eventToFullCalendarFormat = (
export const extractEvents = ( export const extractEvents = (
selectedCalendars: string[], selectedCalendars: string[],
calendars: Record<string, Calendars> calendars: Record<string, Calendars>,
userAddress?: string,
hideDeclinedEvents?: boolean | null
) => { ) => {
let filteredEvents: CalendarEvent[] = []; const allEvents: CalendarEvent[] = [];
selectedCalendars.forEach((id) => { selectedCalendars.forEach((id) => {
if (calendars[id] && calendars[id].events) { const calendar = calendars[id];
filteredEvents = filteredEvents if (calendar?.events) {
.concat( allEvents.push(...Object.values(calendar.events));
Object.keys(calendars[id].events).map(
(eventid) => calendars[id].events[eventid]
)
)
.filter((event) => !(event.status === "CANCELLED"));
} }
}); });
return filteredEvents;
return allEvents
.filter((event) => event.status !== "CANCELLED")
.filter(
(event) =>
!(
hideDeclinedEvents &&
event.attendee?.some(
(a) => a.cal_address === userAddress && a.partstat === "DECLINED"
)
)
);
}; };
export const updateCalsDetails = ( export const updateCalsDetails = (
+68
View File
@@ -25,6 +25,7 @@ import {
setLanguage as setSettingsLanguage, setLanguage as setSettingsLanguage,
setTimeZone as setSettingsTimeZone, setTimeZone as setSettingsTimeZone,
setIsBrowserDefaultTimeZone, setIsBrowserDefaultTimeZone,
setHideDeclinedEvents,
} from "./SettingsSlice"; } from "./SettingsSlice";
import { import {
updateUserConfigurationsAsync, updateUserConfigurationsAsync,
@@ -68,6 +69,11 @@ export default function SettingsPage() {
const alarmEmailsEnabled = useAppSelector( const alarmEmailsEnabled = useAppSelector(
(state) => state.user?.alarmEmailsEnabled ?? true (state) => state.user?.alarmEmailsEnabled ?? true
); );
const hideDeclinedEvents = useAppSelector(
(state) => state.settings?.hideDeclinedEvents
);
const [activeNavItem, setActiveNavItem] = const [activeNavItem, setActiveNavItem] =
useState<SidebarNavItem>("settings"); useState<SidebarNavItem>("settings");
const [activeSettingsSubTab, setActiveSettingsSubTab] = const [activeSettingsSubTab, setActiveSettingsSubTab] =
@@ -75,6 +81,8 @@ export default function SettingsPage() {
const [languageErrorOpen, setLanguageErrorOpen] = useState(false); const [languageErrorOpen, setLanguageErrorOpen] = useState(false);
const [timeZoneErrorOpen, setTimeZoneErrorOpen] = useState(false); const [timeZoneErrorOpen, setTimeZoneErrorOpen] = useState(false);
const [alarmEmailsErrorOpen, setAlarmEmailsErrorOpen] = useState(false); const [alarmEmailsErrorOpen, setAlarmEmailsErrorOpen] = useState(false);
const [hideDeclinedEventsErrorOpen, setHideDeclinedEventsErrorOpen] =
useState(false);
const handleBackClick = () => { const handleBackClick = () => {
dispatch(setView("calendar")); dispatch(setView("calendar"));
@@ -168,6 +176,27 @@ export default function SettingsPage() {
setTimeZoneErrorOpen(false); setTimeZoneErrorOpen(false);
}; };
const handleHideDeclinedEvents = (doHideDeclinedEvents: boolean) => {
// Optimistic update - update UI immediately
dispatch(setHideDeclinedEvents(doHideDeclinedEvents));
// Call API in background, don't wait for it
dispatch(
updateUserConfigurationsAsync({
hideDeclinedEvents: doHideDeclinedEvents,
})
)
.unwrap()
.catch((error) => {
console.error("Failed to update hide declined event:", error);
dispatch(setHideDeclinedEvents(!doHideDeclinedEvents));
setHideDeclinedEventsErrorOpen(true);
});
};
const handleHideDeclinedEventsErrorClose = () => {
setHideDeclinedEventsErrorOpen(false);
};
const handleAlarmEmailsToggle = ( const handleAlarmEmailsToggle = (
event: React.ChangeEvent<HTMLInputElement> event: React.ChangeEvent<HTMLInputElement>
) => { ) => {
@@ -315,6 +344,38 @@ export default function SettingsPage() {
</FormControl> </FormControl>
</Box> </Box>
</Box> </Box>
<Box>
<Typography variant="h6" sx={{ mb: 1 }}>
{t("settings.calAndEvent")}
</Typography>
<Box
sx={{
mb: 6,
}}
>
<FormControl size="small" sx={{ minWidth: 500 }}>
<FormControlLabel
control={
<Switch
checked={Boolean(!hideDeclinedEvents)}
onChange={() =>
handleHideDeclinedEvents(!hideDeclinedEvents)
}
aria-label={t("settings.showDeclinedEvent")}
/>
}
label={t("settings.showDeclinedEvent")}
labelPlacement="start"
sx={{
minWidth: 400,
justifyContent: "space-between",
marginLeft: 0,
mb: 2,
}}
/>
</FormControl>
</Box>
</Box>
</Box> </Box>
)} )}
{activeSettingsSubTab === "notifications" && ( {activeSettingsSubTab === "notifications" && (
@@ -381,6 +442,13 @@ export default function SettingsPage() {
"Failed to update email notifications setting" "Failed to update email notifications setting"
} }
/> />
<Snackbar
open={hideDeclinedEventsErrorOpen}
autoHideDuration={4000}
onClose={handleHideDeclinedEventsErrorClose}
message={t("settings.hideDeclinedEventsUpdateError")}
/>
</main> </main>
); );
} }
+16
View File
@@ -6,6 +6,7 @@ export interface SettingsState {
language: string; language: string;
timeZone: string | null; // Allow null to represent browser default timeZone: string | null; // Allow null to represent browser default
isBrowserDefaultTimeZone: boolean; isBrowserDefaultTimeZone: boolean;
hideDeclinedEvents: boolean | null;
view: "calendar" | "settings" | "search"; view: "calendar" | "settings" | "search";
} }
@@ -21,6 +22,7 @@ const initialState: SettingsState = {
language: defaultLang, language: defaultLang,
timeZone: defaultTimeZone, timeZone: defaultTimeZone,
isBrowserDefaultTimeZone: defaultTimeZone === null, isBrowserDefaultTimeZone: defaultTimeZone === null,
hideDeclinedEvents: null,
view: "calendar", view: "calendar",
}; };
@@ -39,6 +41,9 @@ export const settingsSlice = createSlice({
setIsBrowserDefaultTimeZone: (state, action: PayloadAction<boolean>) => { setIsBrowserDefaultTimeZone: (state, action: PayloadAction<boolean>) => {
state.isBrowserDefaultTimeZone = action.payload; state.isBrowserDefaultTimeZone = action.payload;
}, },
setHideDeclinedEvents: (state, action: PayloadAction<boolean | null>) => {
state.hideDeclinedEvents = action.payload;
},
setView: ( setView: (
state, state,
action: PayloadAction<"calendar" | "settings" | "search"> action: PayloadAction<"calendar" | "settings" | "search">
@@ -65,6 +70,16 @@ export const settingsSlice = createSlice({
state.isBrowserDefaultTimeZone = true; state.isBrowserDefaultTimeZone = true;
localStorage.setItem("timeZone", browserDefaultTimeZone); localStorage.setItem("timeZone", browserDefaultTimeZone);
} }
const esnCalendarModule = action.payload.configurations?.modules?.find(
(module: any) => module.name === "linagora.esn.calendar"
);
const hideDeclinedEventsConfig = esnCalendarModule?.configurations?.find(
(config: any) => config.name === "hideDeclinedEvents"
);
state.hideDeclinedEvents =
typeof hideDeclinedEventsConfig?.value === "boolean"
? hideDeclinedEventsConfig.value
: null;
}); });
}, },
}); });
@@ -74,5 +89,6 @@ export const {
setTimeZone, setTimeZone,
setView, setView,
setIsBrowserDefaultTimeZone, setIsBrowserDefaultTimeZone,
setHideDeclinedEvents,
} = settingsSlice.actions; } = settingsSlice.actions;
export default settingsSlice.reducer; export default settingsSlice.reducer;
+14
View File
@@ -40,6 +40,7 @@ export interface UserConfigurationUpdates {
timezone?: string | null; timezone?: string | null;
previousConfig?: Record<string, any>; previousConfig?: Record<string, any>;
alarmEmails?: boolean; alarmEmails?: boolean;
hideDeclinedEvents?: boolean;
} }
export async function updateUserConfigurations( export async function updateUserConfigurations(
@@ -47,6 +48,7 @@ export async function updateUserConfigurations(
): Promise<Response | { status: number }> { ): Promise<Response | { status: number }> {
const coreConfigs: Array<{ name: string; value: any }> = []; const coreConfigs: Array<{ name: string; value: any }> = [];
const calendarConfigs: Array<{ name: string; value: any }> = []; const calendarConfigs: Array<{ name: string; value: any }> = [];
const esnCalendarConfigs: Array<{ name: string; value: any }> = [];
if (updates.language !== undefined) { if (updates.language !== undefined) {
coreConfigs.push({ name: "language", value: updates.language }); coreConfigs.push({ name: "language", value: updates.language });
@@ -69,6 +71,12 @@ export async function updateUserConfigurations(
value: updates.alarmEmails, value: updates.alarmEmails,
}); });
} }
if (updates.hideDeclinedEvents !== undefined) {
esnCalendarConfigs.push({
name: "hideDeclinedEvents",
value: updates.hideDeclinedEvents,
});
}
const modules: Array<{ const modules: Array<{
name: string; name: string;
@@ -88,6 +96,12 @@ export async function updateUserConfigurations(
configurations: calendarConfigs, configurations: calendarConfigs,
}); });
} }
if (esnCalendarConfigs.length > 0) {
modules.push({
name: "linagora.esn.calendar",
configurations: esnCalendarConfigs,
});
}
if (modules.length === 0) { if (modules.length === 0) {
return Promise.resolve({ status: 204 }); return Promise.resolve({ status: 204 });
+3
View File
@@ -221,6 +221,9 @@
"notifications.deliveryMethod": "Delivery method", "notifications.deliveryMethod": "Delivery method",
"alarmEmailsUpdateError": "Failed to update email notifications setting", "alarmEmailsUpdateError": "Failed to update email notifications setting",
"sync.empty": "Sync settings coming soon", "sync.empty": "Sync settings coming soon",
"calAndEvent": "Calendar and events",
"showDeclinedEvent": "Show declined events",
"hideDeclinedEventsUpdateError": "Failed to update hide declined event.",
"back": "Back to calendar" "back": "Back to calendar"
}, },
"eventPreview": { "eventPreview": {
+3
View File
@@ -221,6 +221,9 @@
"notifications.deliveryMethod": "Mode de réception", "notifications.deliveryMethod": "Mode de réception",
"alarmEmailsUpdateError": "Échec de la mise à jour du mode de notification par e-mail", "alarmEmailsUpdateError": "Échec de la mise à jour du mode de notification par e-mail",
"sync.empty": "Paramètres de synchronisation à venir", "sync.empty": "Paramètres de synchronisation à venir",
"calAndEvent": "Calendrier et événements",
"showDeclinedEvent": "Afficher les événements déclinés",
"hideDeclinedEventsUpdateError": "Échec de la mise à jour du masquage des événements déclinés.",
"back": "Retour au calendrier" "back": "Retour au calendrier"
}, },
"eventPreview": { "eventPreview": {
+3
View File
@@ -221,6 +221,9 @@
"notifications.deliveryMethod": "Способ доставки", "notifications.deliveryMethod": "Способ доставки",
"alarmEmailsUpdateError": "Не удалось обновить настройки уведомлений по email", "alarmEmailsUpdateError": "Не удалось обновить настройки уведомлений по email",
"sync.empty": "Настройки синхронизации скоро появятся", "sync.empty": "Настройки синхронизации скоро появятся",
"calAndEvent": "Календарь и мероприятия",
"showDeclinedEvent": "Показать отклонённые мероприятия",
"hideDeclinedEventsUpdateError": "Не удалось обновить скрытие отклонённых событий.",
"back": "Вернуться к календарю" "back": "Вернуться к календарю"
}, },
"eventPreview": { "eventPreview": {
+3
View File
@@ -221,6 +221,9 @@
"notifications.deliveryMethod": "Phương thức gửi", "notifications.deliveryMethod": "Phương thức gửi",
"alarmEmailsUpdateError": "Cập nhật cài đặt thông báo email thất bại", "alarmEmailsUpdateError": "Cập nhật cài đặt thông báo email thất bại",
"sync.empty": "Cài đặt đồng bộ sắp có", "sync.empty": "Cài đặt đồng bộ sắp có",
"calAndEvent": "Lịch và sự kiện",
"showDeclinedEvent": "Hiển thị các sự kiện đã từ chối",
"hideDeclinedEventsUpdateError": "Không cập nhật được các sự kiện ẩn bị từ chối.",
"back": "Quay lại lịch" "back": "Quay lại lịch"
}, },
"eventPreview": { "eventPreview": {