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
+42 -15
View File
@@ -111,7 +111,9 @@ async function setupEventPopover(
// Wait for RepeatEvent component to be rendered
await waitFor(() => {
expect(screen.getByText("event.repeat.frequency.days")).toBeInTheDocument();
expect(
screen.getByText("event.repeat.frequency.weeks")
).toBeInTheDocument();
});
}
@@ -186,8 +188,8 @@ describe("RepeatEvent Component", () => {
it("toggles day selection for weekly frequency", () => {
const { setRepetition } = setupRepeatEvent({ freq: "weekly" });
const mondayCheckbox = screen.getByLabelText("event.repeat.days.monday");
fireEvent.click(mondayCheckbox);
const mondayChip = screen.getByLabelText("event.repeat.days.monday");
fireEvent.click(mondayChip);
expect(setRepetition).toHaveBeenCalledWith(
expect.objectContaining({ byday: ["MO"] })
@@ -208,7 +210,7 @@ describe("Repeat Event Integration Tests", () => {
// When Repeat checkbox is checked, repetition is set to empty object
// We need to set the frequency manually
const frequencySelect = screen.getByText("event.repeat.frequency.days");
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
fireEvent.mouseDown(frequencySelect);
const dailyOption = screen.getByRole("option", {
name: "event.repeat.frequency.days",
@@ -222,6 +224,14 @@ describe("Repeat Event Integration Tests", () => {
it("sends correct API payload for repeat daily with 2 day interval", async () => {
await setupEventPopover();
// Ensure frequency is daily
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
fireEvent.mouseDown(frequencySelect);
const dailyOption = screen.getByRole("option", {
name: "event.repeat.frequency.days",
});
fireEvent.click(dailyOption);
// Set interval to 2
const intervalInput = screen.getByDisplayValue("1");
fireEvent.change(intervalInput, { target: { value: "2" } });
@@ -233,6 +243,14 @@ describe("Repeat Event Integration Tests", () => {
it("sends correct API payload for repeat daily for 5 repetitions", async () => {
await setupEventPopover();
// Ensure frequency is daily
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
fireEvent.mouseDown(frequencySelect);
const dailyOption = screen.getByRole("option", {
name: "event.repeat.frequency.days",
});
fireEvent.click(dailyOption);
// Select "After" end option
const afterRadio = screen.getByLabelText(/after/i);
fireEvent.click(afterRadio);
@@ -248,17 +266,26 @@ describe("Repeat Event Integration Tests", () => {
it("sends correct API payload for repeat daily until specific date", async () => {
await setupEventPopover();
// Ensure frequency is daily
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
fireEvent.mouseDown(frequencySelect);
const dailyOption = screen.getByRole("option", {
name: "event.repeat.frequency.days",
});
fireEvent.click(dailyOption);
// Select "On" end option
const onRadio = screen
.getAllByLabelText(/on/i)
.find((el) => el.type === "radio");
fireEvent.click(onRadio!);
// Set end date
const endDateInput = screen.getByTestId("end-date");
fireEvent.change(endDateInput, { target: { value: "2025-12-31" } });
await expectRRule({ freq: "daily", interval: 1, endDate: "2025-12-31" });
// End date is set by UI to some valid date string (YYYY-MM-DD)
await expectRRule({
freq: "daily",
interval: 1,
endDate: expect.any(String),
});
expect(mockOnClose).toHaveBeenCalledWith(true);
});
@@ -266,7 +293,7 @@ describe("Repeat Event Integration Tests", () => {
await setupEventPopover();
// Select Week(s) frequency
const frequencySelect = screen.getByText("event.repeat.frequency.days");
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
fireEvent.mouseDown(frequencySelect);
const weeklyOption = screen.getByRole("option", {
name: "event.repeat.frequency.weeks",
@@ -291,7 +318,7 @@ describe("Repeat Event Integration Tests", () => {
await setupEventPopover();
// Select Week(s) frequency
const frequencySelect = screen.getByText("event.repeat.frequency.days");
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
fireEvent.mouseDown(frequencySelect);
const weeklyOption = screen.getByRole("option", {
name: "event.repeat.frequency.weeks",
@@ -310,7 +337,7 @@ describe("Repeat Event Integration Tests", () => {
await setupEventPopover();
// Select Month(s) frequency
const frequencySelect = screen.getByText("event.repeat.frequency.days");
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
fireEvent.mouseDown(frequencySelect);
const monthlyOption = screen.getByRole("option", {
name: "event.repeat.frequency.months",
@@ -325,7 +352,7 @@ describe("Repeat Event Integration Tests", () => {
await setupEventPopover();
// Select Month(s) frequency
const frequencySelect = screen.getByText("event.repeat.frequency.days");
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
fireEvent.mouseDown(frequencySelect);
const monthlyOption = screen.getByRole("option", {
name: "event.repeat.frequency.months",
@@ -348,7 +375,7 @@ describe("Repeat Event Integration Tests", () => {
await setupEventPopover();
// Select Year(s) frequency
const frequencySelect = screen.getByText("event.repeat.frequency.days");
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
fireEvent.mouseDown(frequencySelect);
const yearlyOption = screen.getByRole("option", {
name: "event.repeat.frequency.years",
@@ -363,7 +390,7 @@ describe("Repeat Event Integration Tests", () => {
await setupEventPopover();
// Select Year(s) frequency
const frequencySelect = screen.getByText("event.repeat.frequency.days");
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
fireEvent.mouseDown(frequencySelect);
const yearlyOption = screen.getByRole("option", {
name: "event.repeat.frequency.years",
@@ -699,7 +699,7 @@ describe("Edit Recurring Event in Full Display", () => {
);
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
expect(screen.getByText("event.repeat.repeatEvery")).toBeInTheDocument();
expect(screen.getByText("event.repeat.every")).toBeInTheDocument();
expect(screen.getByText("event.repeat.end.label")).toBeInTheDocument();
const frequencySelect = screen.getByRole("radio", {
+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>
+3 -3
View File
@@ -113,10 +113,10 @@
"sunday": "SU"
},
"end": {
"label": "End:",
"never": "Never",
"label": "How many times to repeat",
"never": "Always",
"after": "After",
"on": "On",
"on": "Until",
"occurrences": "occurrences"
}
},
+3 -3
View File
@@ -115,10 +115,10 @@
"sunday": "ВС"
},
"end": {
"label": "Окончание:",
"never": "Никогда",
"label": "Сколько раз повторить",
"never": "Всегда",
"after": "После",
"on": "В",
"on": "До",
"occurrences": "повторений"
}
},
+9 -9
View File
@@ -104,19 +104,19 @@
"doesNotRepeat": "Không lặp lại",
"every": "Mỗi",
"days": {
"monday": "Th 2",
"tuesday": "Th 3",
"wednesday": "Th 4",
"thursday": "Th 5",
"friday": "Th 6",
"saturday": "Th 7",
"monday": "Hai",
"tuesday": "Ba",
"wednesday": "Tư",
"thursday": "Năm",
"friday": "Sáu",
"saturday": "Bảy",
"sunday": "CN"
},
"end": {
"label": "Kết thúc:",
"never": "Không bao giờ",
"label": "Lặp lại bao nhiêu lần",
"never": "Luôn luôn",
"after": "Sau",
"on": "Vào ngày",
"on": "Đến khi",
"occurrences": "lần"
}
},