UI/update recurring event section (#544)
* Update global recurring event style * #538 update date selector consistance * update test cases
This commit is contained in:
@@ -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", {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
@@ -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
@@ -115,10 +115,10 @@
|
||||
"sunday": "ВС"
|
||||
},
|
||||
"end": {
|
||||
"label": "Окончание:",
|
||||
"never": "Никогда",
|
||||
"label": "Сколько раз повторить",
|
||||
"never": "Всегда",
|
||||
"after": "После",
|
||||
"on": "В",
|
||||
"on": "До",
|
||||
"occurrences": "повторений"
|
||||
}
|
||||
},
|
||||
|
||||
+9
-9
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user