[#533] updated localisation of field (#536)

* [#533] updated localisation of field + removed empty display for repetition

* [#533 & #545] fixed bug date in repetition field & set limit to repetition end to not be before start date
This commit is contained in:
Camille Moussu
2026-02-16 11:11:59 +01:00
committed by GitHub
parent 204c3a72bc
commit f8e159a618
2 changed files with 40 additions and 61 deletions
+16 -12
View File
@@ -1,8 +1,9 @@
import { RootState } from "@/app/store";
import RepeatEvent from "@/components/Event/EventRepeat"; import RepeatEvent from "@/components/Event/EventRepeat";
import * as eventThunks from "@/features/Calendars/services"; import * as eventThunks from "@/features/Calendars/services";
import EventPopover from "@/features/Events/EventModal"; import EventPopover from "@/features/Events/EventModal";
import { RepetitionObject } from "@/features/Events/EventsTypes"; import { RepetitionObject } from "@/features/Events/EventsTypes";
import { DateSelectArg } from "@fullcalendar/core"; import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
import { act, fireEvent, screen, waitFor } from "@testing-library/react"; import { act, fireEvent, screen, waitFor } from "@testing-library/react";
import { renderWithProviders } from "../utils/Renderwithproviders"; import { renderWithProviders } from "../utils/Renderwithproviders";
@@ -15,7 +16,9 @@ const baseRepetition: RepetitionObject = {
const mockOnClose = jest.fn(); const mockOnClose = jest.fn();
const mockSetSelectedRange = jest.fn(); const mockSetSelectedRange = jest.fn();
const mockCalendarRef = { current: { select: jest.fn() } } as any; const mockCalendarRef = {
current: { select: jest.fn() } as unknown as CalendarApi,
};
const preloadedState = { const preloadedState = {
user: { user: {
@@ -56,7 +59,10 @@ const defaultSelectedRange = {
resource: undefined, resource: undefined,
} as unknown as DateSelectArg; } as unknown as DateSelectArg;
function setupRepeatEvent(props?: Partial<RepetitionObject>, state?: any) { function setupRepeatEvent(
props?: Partial<RepetitionObject>,
state?: RootState
) {
const setRepetition = jest.fn(); const setRepetition = jest.fn();
renderWithProviders( renderWithProviders(
<RepeatEvent <RepeatEvent
@@ -70,9 +76,7 @@ function setupRepeatEvent(props?: Partial<RepetitionObject>, state?: any) {
return { setRepetition }; return { setRepetition };
} }
async function setupEventPopover( async function setupEventPopover() {
overrides?: Partial<{ start: string; end: string }>
) {
jest jest
.spyOn(crypto, "randomUUID") .spyOn(crypto, "randomUUID")
.mockReturnValue("fixed-uuid-with-correct-format"); .mockReturnValue("fixed-uuid-with-correct-format");
@@ -140,10 +144,10 @@ describe("RepeatEvent Component", () => {
}); });
it("renders with no repetition by default", () => { it("renders with no repetition by default", () => {
const { setRepetition } = setupRepeatEvent(); setupRepeatEvent();
// Check that interval input shows default value // Check that interval input shows default value
const intervalInput = screen.getByDisplayValue("1"); const intervalInput = screen.getByTestId("repeat-interval");
expect(intervalInput).toBeInTheDocument(); expect(intervalInput).toBeInTheDocument();
// Check that frequency dropdown shows default value // Check that frequency dropdown shows default value
@@ -170,14 +174,14 @@ describe("RepeatEvent Component", () => {
it("renders interval input when frequency is selected", () => { it("renders interval input when frequency is selected", () => {
setupRepeatEvent({ freq: "daily" }); setupRepeatEvent({ freq: "daily" });
const intervalInput = screen.getByDisplayValue("1"); const intervalInput = screen.getByTestId("repeat-interval");
expect(intervalInput).toBeInTheDocument(); expect(intervalInput).toBeInTheDocument();
}); });
it("updates interval value", () => { it("updates interval value", () => {
const { setRepetition } = setupRepeatEvent(); const { setRepetition } = setupRepeatEvent();
const intervalInput = screen.getByDisplayValue("1"); const intervalInput = screen.getByTestId("repeat-interval");
fireEvent.change(intervalInput, { target: { value: "3" } }); fireEvent.change(intervalInput, { target: { value: "3" } });
expect(setRepetition).toHaveBeenCalledWith( expect(setRepetition).toHaveBeenCalledWith(
@@ -233,7 +237,7 @@ describe("Repeat Event Integration Tests", () => {
fireEvent.click(dailyOption); fireEvent.click(dailyOption);
// Set interval to 2 // Set interval to 2
const intervalInput = screen.getByDisplayValue("1"); const intervalInput = screen.getByTestId("repeat-interval");
fireEvent.change(intervalInput, { target: { value: "2" } }); fireEvent.change(intervalInput, { target: { value: "2" } });
await expectRRule({ freq: "daily", interval: 2 }); await expectRRule({ freq: "daily", interval: 2 });
@@ -326,7 +330,7 @@ describe("Repeat Event Integration Tests", () => {
fireEvent.click(weeklyOption); fireEvent.click(weeklyOption);
// Set interval to 3 // Set interval to 3
const intervalInput = screen.getByDisplayValue("1"); const intervalInput = screen.getByTestId("repeat-interval");
fireEvent.change(intervalInput, { target: { value: "3" } }); fireEvent.change(intervalInput, { target: { value: "3" } });
await expectRRule({ freq: "weekly", interval: 3 }); await expectRRule({ freq: "weekly", interval: 3 });
+24 -49
View File
@@ -20,7 +20,6 @@ import "dayjs/locale/en";
import "dayjs/locale/fr"; import "dayjs/locale/fr";
import "dayjs/locale/ru"; import "dayjs/locale/ru";
import "dayjs/locale/vi"; import "dayjs/locale/vi";
import { useEffect, useState } from "react";
import { useI18n } from "twake-i18n"; import { useI18n } from "twake-i18n";
import { ReadOnlyDateField } from "./components/ReadOnlyPickerField"; import { ReadOnlyDateField } from "./components/ReadOnlyPickerField";
import { LONG_DATE_FORMAT } from "./utils/dateTimeFormatters"; import { LONG_DATE_FORMAT } from "./utils/dateTimeFormatters";
@@ -46,31 +45,24 @@ export default function RepeatEvent({
padding: "0 15px", padding: "0 15px",
}, },
}; };
// derive endOption based on repetition
// Fully derived — occurrences takes priority over endDate
const getEndOption = () => { const getEndOption = () => {
if (repetition.occurrences && repetition.occurrences > 0) return "after"; if (repetition.occurrences) return "after";
if (repetition.endDate) return "on"; if (repetition.endDate) return "on";
return "never"; return "never";
}; };
const [endOption, setEndOption] = useState(getEndOption()); const endOption = getEndOption();
// keep endOption in sync if repetition changes from parent const defaultEndDate = dayjs(eventStart).add(1, "day").format("YYYY-MM-DD");
useEffect(() => {
const newEndOption = getEndOption();
if (endOption !== newEndOption) {
setEndOption(newEndOption);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [repetition.occurrences, repetition.endDate]);
const handleDayChange = (day: string) => { const handleDayChange = (dayCode: string) => {
const currentDays = repetition.byday || []; const currentDays = repetition.byday || [];
const updatedDays = currentDays.includes(day) const updatedDays = currentDays.includes(dayCode)
? currentDays.filter((d) => d !== day) ? currentDays.filter((d) => d !== dayCode)
: [...currentDays, day]; : [...currentDays, dayCode];
// Only set byday if there are selected days, otherwise set to null
setRepetition({ setRepetition({
...repetition, ...repetition,
byday: updatedDays.length > 0 ? updatedDays : null, byday: updatedDays.length > 0 ? updatedDays : null,
@@ -110,6 +102,7 @@ export default function RepeatEvent({
style={{ width: 80 }} style={{ width: 80 }}
inputProps={{ inputProps={{
min: 1, min: 1,
"data-testid": "repeat-interval",
style: { style: {
textAlign: "center", textAlign: "center",
paddingRight: 5, paddingRight: 5,
@@ -122,16 +115,14 @@ export default function RepeatEvent({
disabled={!isOwn} disabled={!isOwn}
onChange={(e: SelectChangeEvent) => { onChange={(e: SelectChangeEvent) => {
if (e.target.value === "weekly") { if (e.target.value === "weekly") {
// Adjust day index for MO-SU (0-6) to match JS getDay() (0-6, SU is 0) const jsDay = day.getDay();
const jsDay = day.getDay(); // 0 for Sunday, 1 for Monday, ..., 6 for Saturday const icsDay = days[(jsDay + 6) % 7];
const icsDay = days[(jsDay + 6) % 7]; // MO is 0, TU is 1, ..., SU is 6
setRepetition({ setRepetition({
...repetition, ...repetition,
freq: e.target.value, freq: e.target.value,
byday: [icsDay], // Use byday instead of selectedDays byday: [icsDay],
}); });
} else { } else {
// For non-weekly frequencies, clear byday
setRepetition({ setRepetition({
...repetition, ...repetition,
freq: e.target.value, freq: e.target.value,
@@ -218,8 +209,6 @@ export default function RepeatEvent({
const value = e.target.value; const value = e.target.value;
if (value === endOption) return; if (value === endOption) return;
setEndOption(value);
if (value === "never") { if (value === "never") {
setRepetition({ setRepetition({
...repetition, ...repetition,
@@ -241,8 +230,7 @@ export default function RepeatEvent({
setRepetition({ setRepetition({
...repetition, ...repetition,
occurrences: null, occurrences: null,
endDate: endDate: repetition.endDate || defaultEndDate,
repetition.endDate ?? new Date().toISOString().slice(0, 10),
}); });
} }
}} }}
@@ -282,8 +270,11 @@ export default function RepeatEvent({
<DatePicker <DatePicker
sx={{ width: "100%" }} sx={{ width: "100%" }}
format={LONG_DATE_FORMAT} format={LONG_DATE_FORMAT}
minDate={dayjs(eventStart)}
value={ value={
repetition.endDate ? dayjs(repetition.endDate) : null repetition.endDate
? dayjs(repetition.endDate)
: dayjs(defaultEndDate)
} }
onChange={(value) => { onChange={(value) => {
if (!value || !value.isValid()) return; if (!value || !value.isValid()) return;
@@ -293,26 +284,14 @@ export default function RepeatEvent({
occurrences: null, occurrences: null,
endDate: newDateStr, endDate: newDateStr,
}); });
if (endOption !== "on") {
setEndOption("on");
}
}} }}
onOpen={() => { onOpen={() => {
if (!isOwn || endOption === "on") return; if (!isOwn || endOption === "on") return;
setEndOption("on"); setRepetition({
if (!repetition.endDate) { ...repetition,
setRepetition({ occurrences: null,
...repetition, endDate: repetition.endDate || defaultEndDate,
occurrences: null, });
endDate: new Date().toISOString().slice(0, 10),
});
} else {
setRepetition({
...repetition,
occurrences: null,
endDate: repetition.endDate,
});
}
}} }}
slots={{ field: ReadOnlyDateField }} slots={{ field: ReadOnlyDateField }}
slotProps={{ slotProps={{
@@ -339,7 +318,6 @@ export default function RepeatEvent({
gap={1} gap={1}
onClick={() => { onClick={() => {
if (!isOwn || endOption === "after") return; if (!isOwn || endOption === "after") return;
setEndOption("after");
setRepetition({ setRepetition({
...repetition, ...repetition,
endDate: null, endDate: null,
@@ -357,7 +335,7 @@ export default function RepeatEvent({
type="number" type="number"
inputProps={{ min: 1, "data-testid": "occurrences-input" }} inputProps={{ min: 1, "data-testid": "occurrences-input" }}
size="small" size="small"
value={repetition.occurrences ?? 1} value={repetition.occurrences || 1}
onChange={(e) => { onChange={(e) => {
const value = Number(e.target.value); const value = Number(e.target.value);
setRepetition({ setRepetition({
@@ -365,9 +343,6 @@ export default function RepeatEvent({
endDate: null, endDate: null,
occurrences: value > 0 ? value : 1, occurrences: value > 0 ? value : 1,
}); });
if (endOption !== "after") {
setEndOption("after");
}
}} }}
style={{ width: 100 }} style={{ width: 100 }}
disabled={!isOwn} disabled={!isOwn}