[#82] added cozy-ui for internationnalisation (#267)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2025-11-06 15:46:58 +01:00
committed by GitHub
parent c1f37e3ebd
commit 36bc027d6d
49 changed files with 12003 additions and 5002 deletions
-640
View File
@@ -1,640 +0,0 @@
import CircleIcon from "@mui/icons-material/Circle";
import CloseIcon from "@mui/icons-material/Close";
import DeleteIcon from "@mui/icons-material/Delete";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import VideocamIcon from "@mui/icons-material/Videocam";
import {
Box,
Button,
ButtonGroup,
Card,
CardActions,
CardContent,
CardHeader,
Checkbox,
Divider,
FormControl,
FormControlLabel,
IconButton,
InputLabel,
MenuItem,
Modal,
Select,
SelectChangeEvent,
TextField,
Typography,
} from "@mui/material";
import { useEffect, useState } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import AttendeeSelector from "../../components/Attendees/AttendeeSearch";
import {
handleDelete,
handleRSVP,
} from "../../components/Event/eventHandlers/eventHandlers";
import RepeatEvent from "../../components/Event/EventRepeat";
import { InfoRow } from "../../components/Event/InfoRow";
import { refreshCalendars } from "../../components/Event/utils/eventUtils";
import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils";
import { getCalendarRange } from "../../utils/dateUtils";
import {
moveEventAsync,
putEventAsync,
removeEvent,
updateEventInstanceAsync,
updateSeriesAsync,
} from "../Calendars/CalendarSlice";
import { Calendars } from "../Calendars/CalendarTypes";
import { userAttendee } from "../User/userDataTypes";
import { getEvent } from "./EventApi";
import { formatLocalDateTime } from "../../components/Event/utils/dateTimeFormatters";
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
export default function EventDisplayModal({
eventId,
calId,
open,
onClose,
typeOfAction,
}: {
eventId: string;
calId: string;
open: boolean;
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
typeOfAction?: "solo" | "all";
}) {
const dispatch = useAppDispatch();
const calendar = useAppSelector((state) => state.calendars.list[calId]);
const event = useAppSelector(
(state) => state.calendars.list[calId]?.events[eventId]
);
const user = useAppSelector((state) => state.user);
const [showAllAttendees, setShowAllAttendees] = useState(false);
const [showMore, setShowMore] = useState(false);
const calendars = Object.values(
useAppSelector((state) => state.calendars.list)
);
const userPersonnalCalendars: Calendars[] = calendars.filter(
(c) => c.id?.split("/")[0] === user.userData?.openpaasId
);
// Form state
const [title, setTitle] = useState(event?.title ?? "");
const [description, setDescription] = useState(event?.description ?? "");
const [location, setLocation] = useState(event?.location ?? "");
const [start, setStart] = useState(
formatLocalDateTime(new Date(event?.start ?? Date.now()))
);
const [end, setEnd] = useState(
formatLocalDateTime(new Date(event?.end ?? Date.now()))
);
const [allday, setAllDay] = useState(event?.allday ?? false);
const [repetition, setRepetition] = useState<RepetitionObject>(
event?.repetition ?? ({} as RepetitionObject)
);
const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? "");
const [busy, setBusy] = useState(event?.transp ?? "OPAQUE");
const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC");
const [timezone, setTimezone] = useState(event?.timezone ?? "UTC");
const [newCalId, setNewCalId] = useState(event?.calId);
const [calendarid, setCalendarid] = useState(
calId.split("/")[0] === user.userData?.openpaasId
? userPersonnalCalendars.findIndex((cal) => cal.id === calId)
: calendars.findIndex((cal) => cal.id === calId)
);
const [attendees, setAttendees] = useState(
(event?.attendee || []).filter(
(a) => a.cal_address !== event?.organizer?.cal_address
)
);
const currentUserAttendee = event?.attendee?.find(
(person) => person.cal_address === user.userData.email
);
const organizer =
event?.attendee?.find(
(a) => a.cal_address === event?.organizer?.cal_address
) ?? ({} as userAttendee);
const isOwn = organizer?.cal_address === user.userData.email;
const isOwnCal = userPersonnalCalendars.find((cal) => cal.id === calId);
const attendeeDisplayLimit = 3;
useEffect(() => {
if (!event || !calendar) {
onClose({}, "backdropClick");
}
setRepetition(event?.repetition ?? ({} as RepetitionObject));
}, [open, eventId, dispatch, onClose, event, calendar]);
useEffect(() => {
const fetchMasterEvent = async () => {
const masterEvent = await getEvent(event);
setTitle(masterEvent.title ?? "");
setDescription(masterEvent.description ?? "");
setLocation(masterEvent.location ?? "");
setStart(formatLocalDateTime(new Date(masterEvent?.start ?? Date.now())));
setEnd(formatLocalDateTime(new Date(masterEvent?.end ?? Date.now())));
setAllDay(masterEvent.allday ?? false);
setRepetition(masterEvent?.repetition ?? ({} as RepetitionObject));
setAlarm(masterEvent?.alarm?.trigger ?? "");
setBusy(masterEvent?.transp ?? "OPAQUE");
setEventClass(masterEvent?.class ?? "PUBLIC");
setTimezone(masterEvent.timezone ?? "UTC");
};
if (typeOfAction === "all") {
fetchMasterEvent();
}
}, [typeOfAction, event]);
if (!event || !calendar) return null;
const isRecurring = event.uid?.includes("/");
const handleSave = async () => {
const newEventUID = crypto.randomUUID();
const newEvent: CalendarEvent = {
calId,
title,
URL: event.URL ?? `/calendars/${calId}/${newEventUID}.ics`,
start: start,
end: end,
allday,
uid: event.uid ?? newEventUID,
description,
location,
repetition,
class: eventClass,
organizer: event.organizer,
timezone,
attendee: [organizer, ...attendees],
transp: busy,
color: userPersonnalCalendars[calendarid]?.color,
alarm: { trigger: alarm, action: "EMAIL" },
};
const [baseId, recurrenceId] = event.uid.split("/");
const calendarRange = getCalendarRange(new Date(start));
if (typeOfAction === "solo") {
dispatch(
updateEventInstanceAsync({
cal: userPersonnalCalendars[calendarid],
event: { ...newEvent, recurrenceId: recurrenceId },
})
);
} else if (typeOfAction === "all") {
dispatch(
updateSeriesAsync({
cal: userPersonnalCalendars[calendarid],
event: { ...newEvent, recurrenceId: recurrenceId },
})
);
await refreshCalendars(dispatch, calendars, calendarRange);
} else {
dispatch(
putEventAsync({ cal: userPersonnalCalendars[calendarid], newEvent })
);
}
if (newCalId !== calId) {
dispatch(
moveEventAsync({
cal: userPersonnalCalendars[calendarid],
newEvent,
newURL: `/calendars/${newCalId}/${baseId}.ics`,
})
);
dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid }));
}
onClose({}, "backdropClick");
};
const handleToggleShowMore = async () => {
setShowMore(!showMore);
};
const calList =
calId.split("/")[0] === user.userData?.openpaasId
? Object.keys(userPersonnalCalendars).map((calendar, index) => (
<MenuItem key={index} value={index}>
<Typography variant="body2">
<CircleIcon
style={{
color:
userPersonnalCalendars[index].color?.light ?? "#3788D8",
width: 12,
height: 12,
}}
/>
{userPersonnalCalendars[index].name}
</Typography>
</MenuItem>
))
: Object.keys(calendars).map((calendar, index) => (
<MenuItem key={index} value={index}>
<Typography variant="body2">
<CircleIcon
style={{
color: calendars[index].color?.light ?? "#3788D8",
width: 12,
height: 12,
}}
/>
{calendars[index].name} - {calendars[index].owner}
</Typography>
</MenuItem>
));
return (
<Modal open={open} onClose={onClose}>
<Box
style={{
position: "absolute",
top: "5vh",
left: "50%",
transform: "translate(-50%, -50%)",
width: "50vw",
maxHeight: "80vh",
}}
>
<Card style={{ padding: 16, position: "absolute" }}>
{/* Close button */}
<Box style={{ position: "absolute", top: 8, right: 8 }}>
<IconButton
size="small"
onClick={() => onClose({}, "backdropClick")}
>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
<CardHeader title={isOwn ? "Edit Event" : "Event Details"} />
<CardContent style={{ maxHeight: "75vh", overflow: "auto" }}>
{/* Title */}
<TextField
fullWidth
disabled={!isOwn}
label="Title"
value={title ?? ""}
onChange={(e) => setTitle(e.target.value)}
size="small"
margin="dense"
/>
{/* RSVP */}
{currentUserAttendee && isOwnCal && (
<Card style={{ margin: "8px 0" }}>
<ButtonGroup size="small" fullWidth>
<Button
color={
currentUserAttendee.partstat === "ACCEPTED"
? "success"
: "primary"
}
onClick={() =>
handleRSVP(
dispatch,
calendar,
user,
event,
"ACCEPTED",
undefined,
isRecurring ? typeOfAction : undefined
)
}
>
Accept
</Button>
<Button
color={
currentUserAttendee.partstat === "TENTATIVE"
? "warning"
: "primary"
}
onClick={() =>
handleRSVP(
dispatch,
calendar,
user,
event,
"TENTATIVE",
undefined,
isRecurring ? typeOfAction : undefined
)
}
>
Maybe
</Button>
<Button
color={
currentUserAttendee.partstat === "DECLINED"
? "error"
: "primary"
}
onClick={() =>
handleRSVP(
dispatch,
calendar,
user,
event,
"DECLINED",
undefined,
isRecurring ? typeOfAction : undefined
)
}
>
Decline
</Button>
<Button
color="primary"
onClick={() => console.log("proposenewtime")}
>
Propose new time
</Button>
</ButtonGroup>
</Card>
)}
{/* Calendar selector */}
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="calendar-select-label">Calendar</InputLabel>
<Select
disabled={!isOwn}
labelId="calendar-select-label"
value={calendarid.toString() ?? ""}
label="Calendar"
onChange={(e: SelectChangeEvent) => {
const newId = Number(e.target.value);
setCalendarid(newId);
setNewCalId(userPersonnalCalendars[newId].id);
}}
>
{calList}
</Select>
</FormControl>
{/* Dates */}
<TextField
fullWidth
label="Start"
disabled={!isOwn}
type={allday ? "date" : "datetime-local"}
value={allday ? start.split("T")[0] : start.slice(0, 16)}
onChange={(e) =>
setStart(formatLocalDateTime(new Date(e.target.value)))
}
size="small"
margin="dense"
InputLabelProps={{ shrink: true }}
/>
<TextField
fullWidth
disabled={!isOwn}
label="End"
type={allday ? "date" : "datetime-local"}
value={allday ? end.split("T")[0] : end.slice(0, 16)}
onChange={(e) =>
setEnd(formatLocalDateTime(new Date(e.target.value)))
}
size="small"
margin="dense"
InputLabelProps={{ shrink: true }}
/>
<FormControlLabel
control={
<Checkbox
disabled={!isOwn}
checked={allday}
onChange={() => {
const endDate = new Date(end);
const startDate = new Date(start);
setAllDay(!allday);
if (endDate.getDate() === startDate.getDate()) {
endDate.setDate(startDate.getDate() + 1);
setEnd(formatLocalDateTime(endDate));
}
}}
/>
}
label="All day"
/>
{/* Description & Location */}
<TextField
fullWidth
disabled={!isOwn}
label="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
size="small"
margin="dense"
multiline
rows={2}
/>
{isOwn && (
<AttendeeSelector
attendees={attendees}
setAttendees={setAttendees}
/>
)}
<TextField
fullWidth
label="Location"
disabled={!isOwn}
value={location}
onChange={(e) => setLocation(e.target.value)}
size="small"
margin="dense"
/>
{/* Video */}
{event.x_openpass_videoconference && (
<InfoRow
icon={<VideocamIcon style={{ fontSize: 18 }} />}
content={
<Button
variant="contained"
onClick={() =>
window.open(event.x_openpass_videoconference)
}
>
Join the video conference
</Button>
}
/>
)}
{/* Attendees */}
{event.attendee?.length > 0 && (
<Box style={{ marginBottom: 8 }}>
<Typography variant="subtitle2">Attendees:</Typography>
{organizer.cal_address &&
renderAttendeeBadge(organizer, "org", true, true)}
{(showAllAttendees
? attendees
: attendees.slice(0, attendeeDisplayLimit)
).map((a, idx) => (
<Box key={a.cal_address}>
{renderAttendeeBadge(a, idx.toString(), true)}
{isOwn && (
<IconButton
size="small"
onClick={() => {
const newAttendeesList = [...attendees];
newAttendeesList.splice(idx, 1);
setAttendees(newAttendeesList);
}}
>
<CloseIcon fontSize="small" />
</IconButton>
)}
</Box>
))}
{attendees.length > attendeeDisplayLimit && (
<Typography
variant="body2"
color="primary"
style={{ cursor: "pointer", marginTop: 4 }}
onClick={() => setShowAllAttendees(!showAllAttendees)}
>
{showAllAttendees
? "Show less"
: `Show more (${
attendees.length - attendeeDisplayLimit
} more)`}
</Typography>
)}
</Box>
)}
<Divider style={{ margin: "8px 0" }} />
{/* Extended options */}
{showMore && (
<>
{isOwn && (
<RepeatEvent
repetition={repetition}
eventStart={new Date(event.start)}
setRepetition={setRepetition}
isOwn={isOwn && typeOfAction !== "solo"}
/>
)}
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="notification">Notification</InputLabel>
<Select
labelId="notification"
label="Notification"
value={alarm}
disabled={!isOwn}
onChange={(e: SelectChangeEvent) =>
setAlarm(e.target.value)
}
>
<MenuItem value={""}>No Notification</MenuItem>
<MenuItem value={"-PT1M"}>1 minute</MenuItem>
<MenuItem value={"-PT5M"}>2 minutes</MenuItem>
<MenuItem value={"-PT10M"}>10 minutes</MenuItem>
<MenuItem value={"-PT15M"}>15 minutes</MenuItem>
<MenuItem value={"-PT30M"}>30 minutes</MenuItem>
<MenuItem value={"-PT1H"}>1 hours</MenuItem>
<MenuItem value={"-PT2H"}>2 hours</MenuItem>
<MenuItem value={"-PT5H"}>5 hours</MenuItem>
<MenuItem value={"-PT12H"}>12 hours</MenuItem>
<MenuItem value={"-PT1D"}>1 day</MenuItem>
<MenuItem value={"-PT2D"}>2 days</MenuItem>
<MenuItem value={"-PT1W"}>1 week</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="Visibility">Visibility</InputLabel>
<Select
labelId="Visibility"
label="Visibility"
value={eventClass}
disabled={!isOwn}
onChange={(e: SelectChangeEvent) =>
setEventClass(e.target.value)
}
>
<MenuItem value={"PUBLIC"}>Public</MenuItem>
<MenuItem value={"CONFIDENTIAL"}>Show time only</MenuItem>
<MenuItem value={"PRIVATE"}>Private</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="busy">is Busy</InputLabel>
<Select
labelId="busy"
value={busy}
disabled={!isOwn}
label="is busy"
onChange={(e: SelectChangeEvent) => setBusy(e.target.value)}
>
<MenuItem value={"TRANSPARENT"}>Free</MenuItem>
<MenuItem value={"OPAQUE"}>Busy </MenuItem>
</Select>
</FormControl>
{/* Error */}
{event.error && (
<InfoRow
icon={
<ErrorOutlineIcon
color="error"
style={{ fontSize: 18 }}
/>
}
text={event.error}
error
/>
)}
</>
)}
</CardContent>
<CardActions>
<ButtonGroup>
{isOwn && (
<IconButton
size="small"
onClick={() =>
handleDelete(
isRecurring,
typeOfAction,
onClose,
dispatch,
calendar,
event,
calId,
eventId
)
}
>
<DeleteIcon fontSize="small" />
</IconButton>
)}
<Button size="small" onClick={handleToggleShowMore}>
{showMore ? "Show Less" : "Show More"}
</Button>
{isOwn && (
<Button size="small" onClick={handleSave}>
Save
</Button>
)}
</ButtonGroup>
</CardActions>
</Card>
</Box>
</Modal>
);
}
+103 -63
View File
@@ -42,9 +42,9 @@ import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils";
import { getCalendarRange } from "../../utils/dateUtils";
import { deleteEventAsync } from "../Calendars/CalendarSlice";
import { dlEvent } from "./EventApi";
import EventDisplayModal from "./EventDisplay";
import { CalendarEvent } from "./EventsTypes";
import EventUpdateModal from "./EventUpdateModal";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
export default function EventPreviewModal({
eventId,
calId,
@@ -58,6 +58,7 @@ export default function EventPreviewModal({
open: boolean;
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
}) {
const { t } = useI18n();
const dispatch = useAppDispatch();
const calendars = useAppSelector((state) => state.calendars);
const timezone =
@@ -75,7 +76,6 @@ export default function EventPreviewModal({
const isRecurring = event?.uid?.includes("/");
const isOwn = calendar.ownerEmails?.includes(user.userData.email);
const [showAllAttendees, setShowAllAttendees] = useState(false);
const [openFullDisplay, setOpenFullDisplay] = useState(false);
const [openUpdateModal, setOpenUpdateModal] = useState(false);
const [openDuplicateModal, setOpenDuplicateModal] = useState(false);
const [hidePreview, setHidePreview] = useState(false);
@@ -210,7 +210,7 @@ export default function EventPreviewModal({
)
}
>
Email attendees
{t("eventPreview.emailAttendees")}
</MenuItem>
)}
<EventDuplication
@@ -254,7 +254,7 @@ export default function EventPreviewModal({
updateTempList();
}}
>
Delete event
{t("eventPreview.deleteEvent")}
</MenuItem>
)}
</Menu>
@@ -270,9 +270,7 @@ export default function EventPreviewModal({
{event.class === "PRIVATE" &&
(isOwn ? (
<Tooltip
title={
"Only you and attendees can see the details of this event."
}
title={t("eventPreview.privateEvent.tooltipOwn")}
placement="top"
>
<LockOutlineIcon />
@@ -294,23 +292,21 @@ export default function EventPreviewModal({
</Typography>
{event.transp === "TRANSPARENT" && (
<Tooltip
title={
"Others see you as available during the time range of this event."
}
title={t("eventPreview.free.tooltip")}
placement="top"
>
<Chip
icon={<CircleIcon color="success" fontSize="small" />}
label="Free"
label={t("eventPreview.free.label")}
/>
</Tooltip>
)}
</Box>
<Typography color="text.secondaryContainer" gutterBottom>
{formatDate(event.start, event.allday)}
{formatDate(event.start, t, event.allday)}
{event.end &&
formatEnd(event.start, event.end, event.allday) &&
` ${formatEnd(event.start, event.end, event.allday)} ${!event.allday && getTimezoneOffset(timezone)}`}
formatEnd(event.start, event.end, t, event.allday) &&
` ${formatEnd(event.start, event.end, t, event.allday)} ${!event.allday && getTimezoneOffset(timezone)}`}
</Typography>
</>
)
@@ -319,7 +315,9 @@ export default function EventPreviewModal({
<>
{currentUserAttendee && (
<>
<Typography sx={{ marginRight: 2 }}>Attending?</Typography>
<Typography sx={{ marginRight: 2 }}>
{t("eventPreview.attendingQuestion")}
</Typography>
<Box display="flex" gap="15px" alignItems="center">
<Button
variant={
@@ -344,7 +342,6 @@ export default function EventPreviewModal({
user,
event,
"ACCEPTED",
onClose,
type,
calendarList
@@ -363,7 +360,7 @@ export default function EventPreviewModal({
}
}}
>
Accept
{t("eventPreview.accept")}
</Button>
<Button
variant={
@@ -406,7 +403,7 @@ export default function EventPreviewModal({
}
}}
>
Maybe
{t("eventPreview.maybe")}
</Button>
<Button
variant={
@@ -449,7 +446,7 @@ export default function EventPreviewModal({
}
}}
>
Decline
{t("eventPreview.decline")}
</Button>
</Box>
</>
@@ -474,7 +471,7 @@ export default function EventPreviewModal({
window.open(event.x_openpass_videoconference)
}
>
Join the video conference
{t("eventPreview.joinVideo")}
</Button>
}
/>
@@ -497,20 +494,25 @@ export default function EventPreviewModal({
}}
>
<Box sx={{ marginRight: 2 }}>
<Typography>{attendees.length} guests</Typography>
<Typography>
{t("eventPreview.guests", {
count: attendees.length,
})}
</Typography>
<Typography
sx={{ fontSize: "13px", color: "text.secondary" }}
>
{
attendees.filter((a) => a.partstat === "ACCEPTED")
.length
}{" "}
yes,{" "}
{
attendees.filter((a) => a.partstat === "DECLINED")
.length
}{" "}
no
{t("eventPreview.yesCount", {
count: attendees.filter(
(a) => a.partstat === "ACCEPTED"
).length,
})}
,{" "}
{t("eventPreview.noCount", {
count: attendees.filter(
(a) => a.partstat === "DECLINED"
).length,
})}
</Typography>
</Box>
{!showAllAttendees && (
@@ -519,6 +521,7 @@ export default function EventPreviewModal({
renderAttendeeBadge(
organizer,
"org",
t,
showAllAttendees,
true
)}
@@ -526,6 +529,7 @@ export default function EventPreviewModal({
renderAttendeeBadge(
a,
idx.toString(),
t,
showAllAttendees
)
)}
@@ -541,7 +545,9 @@ export default function EventPreviewModal({
}}
onClick={() => setShowAllAttendees(!showAllAttendees)}
>
{showAllAttendees ? "Show less" : `Show more `}
{showAllAttendees
? t("eventPreview.showLess")
: t("eventPreview.showMore")}
</Typography>
</Box>
}
@@ -550,10 +556,10 @@ export default function EventPreviewModal({
)}
{showAllAttendees &&
organizer &&
renderAttendeeBadge(organizer, "org", showAllAttendees, true)}
renderAttendeeBadge(organizer, "org", t, showAllAttendees, true)}
{showAllAttendees &&
visibleAttendees.map((a, idx) =>
renderAttendeeBadge(a, idx.toString(), showAllAttendees)
renderAttendeeBadge(a, idx.toString(), t, showAllAttendees)
)}
{/* Location */}
{event.location && (
@@ -593,7 +599,10 @@ export default function EventPreviewModal({
<NotificationsNoneIcon />
</Box>
}
text={`${event.alarm.trigger} before by ${event.alarm.action}`}
text={t("eventPreview.alarmText", {
trigger: t(`event.form.notifications.${event.alarm.trigger}`),
action: t(`event.form.notifications.${event.alarm.action}`),
})}
style={{
fontSize: "16px",
fontFamily: "'Inter', sans-serif",
@@ -608,7 +617,7 @@ export default function EventPreviewModal({
<RepeatIcon />
</Box>
}
text={makeRecurrenceString(event)}
text={makeRecurrenceString(event, t)}
style={{
fontSize: "16px",
fontFamily: "'Inter', sans-serif",
@@ -636,7 +645,7 @@ export default function EventPreviewModal({
letterSpacing={"0.5px"}
textAlign={"center"}
>
This is a private event. Details are hidden.
{t("eventPreview.privateEvent.hiddenDetails")}
</Typography>
</Box>
)}
@@ -680,13 +689,6 @@ export default function EventPreviewModal({
afterChoiceFunc && afterChoiceFunc(type);
}}
/>
<EventDisplayModal
open={openFullDisplay}
onClose={() => setOpenFullDisplay(false)}
eventId={eventId}
calId={calId}
typeOfAction={typeOfAction}
/>
<EventUpdateModal
open={openUpdateModal}
onClose={() => {
@@ -725,43 +727,76 @@ export default function EventPreviewModal({
);
}
function makeRecurrenceString(event: CalendarEvent): string | undefined {
if (!event.repetition) {
return;
}
const recur = [`Reccurent Event · ${event.repetition.freq}`];
function makeRecurrenceString(
event: CalendarEvent,
t: Function
): string | undefined {
if (!event.repetition) return;
const recur: string[] = [
`${t("eventPreview.recurrentEvent")} · ${t(
`eventPreview.freq.${event.repetition.freq}`,
event.repetition.freq
)}`,
];
const recurType: Record<string, string> = {
daily: "days",
monthly: "months",
yearly: "years",
daily: t("eventPreview.days"),
monthly: t("eventPreview.months"),
yearly: t("eventPreview.years"),
};
if (event.repetition.byday) {
recur.push(`on ${event.repetition.byday.join(", ")}`);
recur.push(
t("eventPreview.recurrenceOnDays", {
days: event.repetition.byday
.map((s) => t(`eventPreview.onDays.${s}`))
.join(", "),
})
);
}
if (event.repetition.interval && event.repetition.interval > 1) {
recur.push(
`every ${event.repetition.interval} ${recurType[event.repetition.freq]}`
t("eventPreview.everyInterval", {
interval: event.repetition.interval,
unit: recurType[event.repetition.freq],
})
);
}
if (event.repetition.occurrences) {
recur.push(`for ${event.repetition.occurrences} occurences`);
recur.push(
t("eventPreview.forOccurrences", {
count: event.repetition.occurrences,
})
);
}
if (event.repetition.endDate) {
recur.push(`until ${event.repetition.endDate}`);
recur.push(
t("eventPreview.until", {
date: new Date(event.repetition.endDate).toLocaleDateString(
t("locale"),
{
year: "numeric",
month: "long",
day: "numeric",
}
),
})
);
}
return recur.join(", ");
}
function formatDate(date: Date | string, allday?: boolean) {
function formatDate(date: Date | string, t: Function, allday?: boolean) {
if (allday) {
return new Date(date).toLocaleDateString(undefined, {
return new Date(date).toLocaleDateString(t("locale"), {
year: "numeric",
month: "long",
weekday: "long",
day: "numeric",
});
} else {
return new Date(date).toLocaleString(undefined, {
return new Date(date).toLocaleString(t("locale"), {
year: "numeric",
month: "long",
weekday: "long",
@@ -772,7 +807,12 @@ function formatDate(date: Date | string, allday?: boolean) {
}
}
function formatEnd(start: Date | string, end: Date | string, allday?: boolean) {
function formatEnd(
start: Date | string,
end: Date | string,
t: Function,
allday?: boolean
) {
const startDate = new Date(start);
const endDate = new Date(end);
@@ -784,19 +824,19 @@ function formatEnd(start: Date | string, end: Date | string, allday?: boolean) {
if (allday) {
return sameDay
? null
: endDate.toLocaleDateString(undefined, {
: endDate.toLocaleDateString(t("locale"), {
year: "numeric",
month: "short",
day: "numeric",
});
} else {
if (sameDay) {
return endDate.toLocaleTimeString(undefined, {
return endDate.toLocaleTimeString(t("locale"), {
hour: "2-digit",
minute: "2-digit",
});
}
return endDate.toLocaleString(undefined, {
return endDate.toLocaleString(t("locale"), {
year: "numeric",
month: "short",
day: "numeric",
+36 -30
View File
@@ -30,6 +30,7 @@ import {
formatDateTimeInTimezone,
} from "../../components/Event/utils/dateTimeFormatters";
import { addDays } from "../../components/Event/utils/dateRules";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
function EventPopover({
anchorEl,
@@ -55,7 +56,7 @@ function EventPopover({
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const tempList = useAppSelector((state) => state.calendars.templist);
const calList = useAppSelector((state) => state.calendars.list);
const selectPersonnalCalendars = createSelector(
const selectPersonalCalendars = createSelector(
(state: any) => state.calendars,
(calendars: any) =>
Object.keys(calendars.list)
@@ -67,8 +68,8 @@ function EventPopover({
})
.filter((calendar) => calendar.id)
);
const userPersonnalCalendars: Calendars[] = useAppSelector(
selectPersonnalCalendars
const userPersonalCalendars: Calendars[] = useAppSelector(
selectPersonalCalendars
);
const timezoneList = useMemo(() => {
@@ -97,7 +98,7 @@ function EventPopover({
const [start, setStart] = useState(event?.start ? event.start : "");
const [end, setEnd] = useState(event?.end ? event.end : "");
const [calendarid, setCalendarid] = useState(
event?.calId ?? userPersonnalCalendars[0]?.id ?? ""
event?.calId ?? userPersonalCalendars[0]?.id ?? ""
);
const [allday, setAllDay] = useState(event?.allday ?? false);
const [repetition, setRepetition] = useState<RepetitionObject>(
@@ -126,18 +127,18 @@ function EventPopover({
// Use ref to track if we've already initialized to avoid infinite loop
const isInitializedRef = useRef(false);
const userPersonnalCalendarsRef = useRef(userPersonnalCalendars);
const userPersonalCalendarsRef = useRef(userPersonalCalendars);
// Update ref when userPersonnalCalendars changes
// Update ref when userPersonalCalendars changes
useEffect(() => {
userPersonnalCalendarsRef.current = userPersonnalCalendars;
}, [userPersonnalCalendars]);
userPersonalCalendarsRef.current = userPersonalCalendars;
}, [userPersonalCalendars]);
useEffect(() => {
if (!calendarid && userPersonnalCalendars.length > 0) {
setCalendarid(userPersonnalCalendars[0].id);
if (!calendarid && userPersonalCalendars.length > 0) {
setCalendarid(userPersonalCalendars[0].id);
}
}, [calendarid, userPersonnalCalendars]);
}, [calendarid, userPersonalCalendars]);
const resetAllStateToDefault = useCallback(() => {
setShowMore(false);
@@ -150,11 +151,11 @@ function EventPopover({
setStart("");
setEnd("");
if (
userPersonnalCalendars &&
userPersonnalCalendars.length > 0 &&
userPersonnalCalendars[0]?.id
userPersonalCalendars &&
userPersonalCalendars.length > 0 &&
userPersonalCalendars[0]?.id
) {
setCalendarid(userPersonnalCalendars[0].id);
setCalendarid(userPersonalCalendars[0].id);
}
setAllDay(false);
setRepetition({} as RepetitionObject);
@@ -164,7 +165,7 @@ function EventPopover({
setTimezone(calendarTimezone);
setHasVideoConference(false);
setMeetingLink(null);
}, [calendarTimezone, userPersonnalCalendars]);
}, [calendarTimezone, userPersonalCalendars]);
// Track if we should sync from selectedRange (only on initial selection, not on toggle)
const shouldSyncFromRangeRef = useRef(true);
@@ -396,11 +397,11 @@ function EventPopover({
}
if (
userPersonnalCalendars &&
userPersonnalCalendars.length > 0 &&
userPersonnalCalendars[0]?.id
userPersonalCalendars &&
userPersonalCalendars.length > 0 &&
userPersonalCalendars[0]?.id
) {
setCalendarid(userPersonnalCalendars[0].id);
setCalendarid(userPersonalCalendars[0].id);
}
setRepetition(event.repetition ?? ({} as RepetitionObject));
setShowRepeat(event.repetition?.freq ? true : false);
@@ -457,11 +458,11 @@ function EventPopover({
setAttendees([]);
setLocation("");
if (
userPersonnalCalendars &&
userPersonnalCalendars.length > 0 &&
userPersonnalCalendars[0]?.id
userPersonalCalendars &&
userPersonalCalendars.length > 0 &&
userPersonalCalendars[0]?.id
) {
setCalendarid(userPersonnalCalendars[0].id);
setCalendarid(userPersonalCalendars[0].id);
}
setAllDay(false);
setRepetition({} as RepetitionObject);
@@ -584,7 +585,7 @@ function EventPopover({
// Resolve target calendar safely
const targetCalendar: Calendars | undefined =
calList[calendarid] ||
userPersonnalCalendars[0] ||
userPersonalCalendars[0] ||
(Object.values(calList)[0] as Calendars | undefined);
if (!targetCalendar || !targetCalendar.id) {
console.error("No target calendar available to save event");
@@ -672,22 +673,23 @@ function EventPopover({
await updateTempCalendar(tempList, newEvent, dispatch, calendarRange);
}
};
const { t } = useI18n();
const dialogActions = (
<Box display="flex" justifyContent="space-between" width="100%" px={2}>
{!showMore && (
<Button startIcon={<AddIcon />} onClick={() => setShowMore(!showMore)}>
More options
{t("common.moreOptions")}
</Button>
)}
<Box display="flex" gap={1} ml={showMore ? "auto" : 0}>
{showMore && (
<Button variant="outlined" onClick={handleClose}>
Cancel
{t("common.cancel")}
</Button>
)}
<Button variant="contained" onClick={handleSave}>
Save
{t("actions.save")}
</Button>
</Box>
</Box>
@@ -697,7 +699,11 @@ function EventPopover({
<ResponsiveDialog
open={open}
onClose={handleClose}
title={event?.uid ? "Duplicate Event" : "Create Event"}
title={
event?.uid
? t("eventDuplication.duplicateEvent")
: t("event.createEvent")
}
isExpanded={showMore}
onExpandToggle={() => setShowMore(!showMore)}
actions={dialogActions}
@@ -739,7 +745,7 @@ function EventPopover({
showRepeat={showRepeat}
setShowRepeat={setShowRepeat}
isOpen={open}
userPersonnalCalendars={userPersonnalCalendars}
userPersonalCalendars={userPersonalCalendars}
timezoneList={timezoneList}
onStartChange={handleStartChange}
onEndChange={handleEndChange}
+12 -10
View File
@@ -28,6 +28,7 @@ import {
detectRecurringEventChanges,
} from "./eventUtils";
import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
const showErrorNotification = (message: string) => {
console.error(`[ERROR] ${message}`);
@@ -50,6 +51,7 @@ function EventUpdateModal({
eventData?: CalendarEvent | null;
typeOfAction?: "solo" | "all";
}) {
const { t } = useI18n();
const dispatch = useAppDispatch();
const tempList = useAppSelector((state) => state.calendars.templist);
const calList = useAppSelector((state) => state.calendars.list);
@@ -84,7 +86,7 @@ function EventUpdateModal({
const calendarsList = useAppSelector((state) => state.calendars.list);
const userPersonnalCalendars: Calendars[] = useMemo(() => {
const userPersonalCalendars: Calendars[] = useMemo(() => {
const allCalendars = Object.values(calendarsList) as Calendars[];
return allCalendars.filter(
(c: Calendars) => c.id?.split("/")[0] === user.userData?.openpaasId
@@ -155,7 +157,7 @@ function EventUpdateModal({
);
const [newCalId, setNewCalId] = useState(calId);
const [calendarid, setCalendarid] = useState(
calId ?? userPersonnalCalendars[0]?.id ?? ""
calId ?? userPersonalCalendars[0]?.id ?? ""
);
const [attendees, setAttendees] = useState<userAttendee[]>([]);
@@ -175,7 +177,7 @@ function EventUpdateModal({
setLocation("");
setStart("");
setEnd("");
setCalendarid(userPersonnalCalendars[0].id);
setCalendarid(userPersonalCalendars[0].id);
setAllDay(false);
setRepetition({} as RepetitionObject);
setAlarm("");
@@ -296,7 +298,7 @@ function EventUpdateModal({
}
}
}
}, [open, event, calId, userPersonnalCalendars, calendarsList]);
}, [open, event, calId, userPersonalCalendars, calendarsList]);
// Helper to close modal(s) - use onCloseAll if available to close preview modal too
const closeModal = () => {
@@ -682,7 +684,7 @@ function EventUpdateModal({
moveEventAsync({
cal: targetCalendar,
newEvent,
newURL: `/calendars/${newCalId}/${event.uid}.ics`,
newURL: `/calendars/${newCalId}/${event.uid.split("/")[0]}.ics`,
})
);
dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid }));
@@ -697,15 +699,15 @@ function EventUpdateModal({
<Box display="flex" justifyContent="space-between" width="100%" px={2}>
{!showMore && (
<Button startIcon={<AddIcon />} onClick={() => setShowMore(!showMore)}>
More options
{t("common.moreOptions")}
</Button>
)}
<Box display="flex" gap={1} ml={showMore ? "auto" : 0}>
<Button variant="outlined" onClick={handleClose}>
Cancel
{t("common.cancel")}
</Button>
<Button variant="contained" onClick={handleSave}>
Save
{t("actions.save")}
</Button>
</Box>
</Box>
@@ -717,7 +719,7 @@ function EventUpdateModal({
<ResponsiveDialog
open={open}
onClose={handleClose}
title="Update Event"
title={t("event.updateEvent")}
isExpanded={showMore}
onExpandToggle={() => setShowMore(!showMore)}
actions={dialogActions}
@@ -760,7 +762,7 @@ function EventUpdateModal({
showRepeat={typeOfAction !== "solo" && showRepeat}
setShowRepeat={setShowRepeat}
isOpen={open}
userPersonnalCalendars={userPersonnalCalendars}
userPersonalCalendars={userPersonalCalendars}
timezoneList={timezoneList}
onCalendarChange={(newCalendarId) => {
const selectedCalendar = calList[newCalendarId];
+26
View File
@@ -0,0 +1,26 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface SettingsState {
language: string;
}
const savedLang = localStorage.getItem("lang");
const defaultLang = savedLang ?? (window as any).LANG ?? "en";
const initialState: SettingsState = {
language: defaultLang,
};
export const settingsSlice = createSlice({
name: "settings",
initialState,
reducers: {
setLanguage: (state, action: PayloadAction<string>) => {
state.language = action.payload;
localStorage.setItem("lang", action.payload);
},
},
});
export const { setLanguage } = settingsSlice.actions;
export default settingsSlice.reducer;