UI/update recurring event section (#544)

* Update global recurring event style

* #538 update date selector consistance

* update test cases
This commit is contained in:
lenhanphung
2026-02-13 15:34:33 +07:00
committed by GitHub
parent 494a73dae2
commit 53f763d6eb
7 changed files with 232 additions and 88 deletions
+6 -2
View File
@@ -539,12 +539,16 @@ export default function EventFormFields({
const newShowRepeat = !showRepeat;
setShowRepeat(newShowRepeat);
if (newShowRepeat) {
const days = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
const eventStartDate = new Date(start);
const jsDay = eventStartDate.getDay();
const icsDay = days[(jsDay + 6) % 7];
setRepetition({
freq: "daily",
freq: "weekly",
interval: 1,
occurrences: 0,
endDate: "",
byday: null,
byday: [icsDay],
} as RepetitionObject);
} else {
setRepetition({
+168 -55
View File
@@ -1,10 +1,8 @@
import { RepetitionObject } from "@/features/Events/EventsTypes";
import {
Box,
Checkbox,
FormControl,
FormControlLabel,
FormGroup,
MenuItem,
Radio,
RadioGroup,
@@ -14,8 +12,18 @@ import {
TextField,
Typography,
} from "@linagora/twake-mui";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import dayjs from "dayjs";
import "dayjs/locale/en";
import "dayjs/locale/fr";
import "dayjs/locale/ru";
import "dayjs/locale/vi";
import { useEffect, useState } from "react";
import { useI18n } from "twake-i18n";
import { ReadOnlyDateField } from "./components/ReadOnlyPickerField";
import { LONG_DATE_FORMAT } from "./utils/dateTimeFormatters";
export default function RepeatEvent({
repetition,
@@ -31,6 +39,13 @@ export default function RepeatEvent({
const { t } = useI18n();
const days = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
const day = new Date(eventStart);
const dateCalendarLayoutSx = {
"& .MuiDateCalendar-root.MuiDateCalendar-root": {
width: "260px",
maxWidth: "260px",
padding: "0 15px",
},
};
// derive endOption based on repetition
const getEndOption = () => {
if (repetition.occurrences && repetition.occurrences > 0) return "after";
@@ -79,8 +94,8 @@ export default function RepeatEvent({
<Box>
<Stack>
{/* Interval */}
<Box display="flex" alignItems="center" gap={2} mb={2}>
<Typography variant="h6">{t("event.repeat.repeatEvery")}</Typography>
<Box display="flex" alignItems="center" gap={2} mb={1}>
<Typography variant="h6">{t("event.repeat.every")}</Typography>
<TextField
type="number"
value={repetition.interval ?? 1}
@@ -93,7 +108,13 @@ export default function RepeatEvent({
}
size="small"
style={{ width: 80 }}
inputProps={{ min: 1 }}
inputProps={{
min: 1,
style: {
textAlign: "center",
paddingRight: 5,
},
}}
/>
<FormControl size="small" style={{ minWidth: 120 }}>
<Select
@@ -137,25 +158,52 @@ export default function RepeatEvent({
{/* Weekly selection */}
{repetition.freq === "weekly" && (
<Box>
<Typography variant="body2" gutterBottom>
{t("event.repeat.repeatOn")}
</Typography>
<FormGroup row>
{days.map((day) => (
<FormControlLabel
key={day}
disabled={!isOwn}
control={
<Checkbox
checked={repetition.byday?.includes(day) ?? false}
onChange={() => handleDayChange(day)}
/>
}
label={getDayLabel(day)}
/>
))}
</FormGroup>
<Box mb={2}>
<Box display="flex" gap={1}>
{days.map((dayCode) => {
const isSelected = repetition.byday?.includes(dayCode) ?? false;
const fullLabel = getDayLabel(dayCode);
const label = fullLabel.charAt(0);
return (
<Box
key={dayCode}
role="button"
aria-label={fullLabel}
onClick={() => {
if (!isOwn) return;
handleDayChange(dayCode);
}}
sx={{
width: 40,
height: 40,
borderRadius: "4px",
border: "1px solid",
borderColor: isSelected ? "primary.main" : "#AEAEC0",
color: isSelected ? "#fff" : "#8C9CAF",
fontSize: 16,
fontWeight: 400,
lineHeight: "24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
textAlign: "center",
bgcolor: isSelected ? "primary.main" : "transparent",
cursor: isOwn ? "pointer" : "default",
"&:hover": isOwn
? {
borderColor: "primary.main",
bgcolor: "primary.main",
color: "#fff",
}
: undefined,
}}
>
{label}
</Box>
);
})}
</Box>
</Box>
)}
@@ -168,6 +216,8 @@ export default function RepeatEvent({
value={endOption}
onChange={(e) => {
const value = e.target.value;
if (value === endOption) return;
setEndOption(value);
if (value === "never") {
@@ -191,7 +241,8 @@ export default function RepeatEvent({
setRepetition({
...repetition,
occurrences: null,
endDate: new Date().toISOString().slice(0, 16),
endDate:
repetition.endDate ?? new Date().toISOString().slice(0, 10),
});
}
}}
@@ -209,15 +260,102 @@ export default function RepeatEvent({
<FormControlLabel
disabled={!isOwn}
value="after"
value="on"
sx={{ mt: 1 }}
control={<Radio />}
label={
<Box display="flex" alignItems="center" gap={1}>
<Typography variant="h6">
{t("event.repeat.end.on")}
</Typography>
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale={t("locale") ?? "en"}
>
<Box
sx={{
width: 220,
"& .MuiInputBase-root": { fontSize: 14 },
"& .MuiInputBase-input": { fontSize: 14 },
}}
>
<DatePicker
sx={{ width: "100%" }}
format={LONG_DATE_FORMAT}
value={
repetition.endDate ? dayjs(repetition.endDate) : null
}
onChange={(value) => {
if (!value || !value.isValid()) return;
const newDateStr = value.format("YYYY-MM-DD");
setRepetition({
...repetition,
occurrences: null,
endDate: newDateStr,
});
if (endOption !== "on") {
setEndOption("on");
}
}}
onOpen={() => {
if (!isOwn || endOption === "on") return;
setEndOption("on");
if (!repetition.endDate) {
setRepetition({
...repetition,
occurrences: null,
endDate: new Date().toISOString().slice(0, 10),
});
} else {
setRepetition({
...repetition,
occurrences: null,
endDate: repetition.endDate,
});
}
}}
slots={{ field: ReadOnlyDateField }}
slotProps={{
field: {},
layout: { sx: dateCalendarLayoutSx },
}}
disabled={!isOwn}
/>
</Box>
</LocalizationProvider>
</Box>
}
/>
<FormControlLabel
disabled={!isOwn}
value="after"
control={<Radio />}
sx={{ mt: 1 }}
label={
<Box
display="flex"
alignItems="center"
gap={1}
onClick={() => {
if (!isOwn || endOption === "after") return;
setEndOption("after");
setRepetition({
...repetition,
endDate: null,
occurrences:
repetition.occurrences && repetition.occurrences > 0
? repetition.occurrences
: 1,
});
}}
>
<Typography variant="h6">
{t("event.repeat.end.after")}
</Typography>
<TextField
type="number"
inputProps={{ min: 1, "data-testid": "occurrences-input" }}
size="small"
value={repetition.occurrences ?? 1}
onChange={(e) => {
@@ -227,10 +365,12 @@ export default function RepeatEvent({
endDate: null,
occurrences: value > 0 ? value : 1,
});
if (endOption !== "after") {
setEndOption("after");
}
}}
style={{ width: 100 }}
inputProps={{ min: 1, "data-testid": "occurrences-input" }}
disabled={!isOwn || endOption !== "after"}
disabled={!isOwn}
/>
<Typography variant="h6">
{t("event.repeat.end.occurrences")}
@@ -238,33 +378,6 @@ export default function RepeatEvent({
</Box>
}
/>
<FormControlLabel
disabled={!isOwn}
value="on"
control={<Radio />}
label={
<Box display="flex" alignItems="center" gap={1}>
<Typography variant="h6">
{t("event.repeat.end.on")}
</Typography>
<TextField
type="date"
inputProps={{ "data-testid": "end-date" }}
size="small"
value={repetition.endDate ?? ""}
onChange={(e) =>
setRepetition({
...repetition,
occurrences: null,
endDate: e.target.value,
})
}
disabled={!isOwn || endOption !== "on"}
/>
</Box>
}
/>
</RadioGroup>
</Box>
</Stack>