Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
import {
|
||||
screen,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
act,
|
||||
render,
|
||||
} from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import {
|
||||
DateTimeFieldsProps,
|
||||
DateTimeFields,
|
||||
} from "../../src/components/Event/components/DateTimeFields";
|
||||
|
||||
jest.mock("cozy-ui/transpiled/react/providers/I18n", () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key,
|
||||
lang: "en",
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("DateTimeFields", () => {
|
||||
const mockHandlers = {
|
||||
onStartDateChange: jest.fn(),
|
||||
onStartTimeChange: jest.fn(),
|
||||
onEndDateChange: jest.fn(),
|
||||
onEndTimeChange: jest.fn(),
|
||||
onToggleEndDate: jest.fn(),
|
||||
};
|
||||
|
||||
const defaultProps: DateTimeFieldsProps = {
|
||||
startDate: "2025-07-18",
|
||||
startTime: "09:00",
|
||||
endDate: "2025-07-18",
|
||||
endTime: "10:00",
|
||||
allday: false,
|
||||
showMore: true,
|
||||
hasEndDateChanged: false,
|
||||
showEndDate: false,
|
||||
validation: {
|
||||
errors: {
|
||||
dateTime: "",
|
||||
},
|
||||
},
|
||||
...mockHandlers,
|
||||
};
|
||||
|
||||
const renderField = async (props: Partial<DateTimeFieldsProps> = {}) => {
|
||||
await act(async () =>
|
||||
render(<DateTimeFields {...defaultProps} {...props} />)
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("moves END forward when START moves after END (normal mode)", async () => {
|
||||
await renderField({
|
||||
startDate: "2025-01-01",
|
||||
startTime: "10:00",
|
||||
endDate: "2025-01-01",
|
||||
endTime: "11:00",
|
||||
showMore: true,
|
||||
});
|
||||
|
||||
const startTimeInput = screen.getByTestId("start-time-input");
|
||||
|
||||
fireEvent.change(startTimeInput, { target: { value: "12:00" } });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onStartTimeChange).toHaveBeenCalledWith("12:00")
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onEndTimeChange).toHaveBeenCalledWith("13:00")
|
||||
);
|
||||
});
|
||||
|
||||
it("moves END forward by full duration when START date jumps after END date", async () => {
|
||||
await renderField({
|
||||
startDate: "2025-01-01",
|
||||
startTime: "09:00",
|
||||
endDate: "2025-01-01",
|
||||
endTime: "10:00",
|
||||
showMore: true,
|
||||
});
|
||||
|
||||
const calendarButton = screen.getAllByRole("button");
|
||||
await userEvent.click(calendarButton[0]);
|
||||
const dayButton = screen.getByRole("gridcell", { name: "3" });
|
||||
await userEvent.click(dayButton);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onStartDateChange).toHaveBeenCalledWith("2025-01-03")
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onEndDateChange).toHaveBeenCalledWith("2025-01-03")
|
||||
);
|
||||
});
|
||||
|
||||
it("moves START backward when END moves before START (normal mode)", async () => {
|
||||
await renderField({
|
||||
startDate: "2025-01-01",
|
||||
startTime: "10:00",
|
||||
endDate: "2025-01-01",
|
||||
endTime: "11:00",
|
||||
showMore: true,
|
||||
});
|
||||
|
||||
const endTimeInput = screen.getByTestId("end-time-input");
|
||||
|
||||
fireEvent.change(endTimeInput, { target: { value: "08:00" } });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onEndTimeChange).toHaveBeenCalledWith("08:00")
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onStartTimeChange).toHaveBeenCalledWith("07:00")
|
||||
);
|
||||
});
|
||||
|
||||
it("moves START backward properly when END date jumps before START date", async () => {
|
||||
await renderField({
|
||||
startDate: "2025-01-05",
|
||||
startTime: "09:00",
|
||||
endDate: "2025-01-05",
|
||||
endTime: "10:00",
|
||||
showMore: true,
|
||||
});
|
||||
|
||||
const calendarButton = screen.getAllByRole("button");
|
||||
await userEvent.click(calendarButton[2]);
|
||||
const dayButton = screen.getByRole("gridcell", { name: "3" });
|
||||
await userEvent.click(dayButton);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onEndDateChange).toHaveBeenCalledWith("2025-01-03")
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onStartDateChange).toHaveBeenCalledWith("2025-01-03")
|
||||
);
|
||||
});
|
||||
|
||||
it("pushes END forward in whole days for allday events", async () => {
|
||||
await renderField({
|
||||
allday: true,
|
||||
startDate: "2025-02-01",
|
||||
endDate: "2025-02-03",
|
||||
showEndDate: true,
|
||||
});
|
||||
|
||||
const calendarButton = screen.getAllByRole("button");
|
||||
await userEvent.click(calendarButton[0]);
|
||||
const dayButton = screen.getByRole("gridcell", { name: "10" });
|
||||
await userEvent.click(dayButton);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onStartDateChange).toHaveBeenCalledWith("2025-02-10")
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onEndDateChange).toHaveBeenCalledWith("2025-02-12")
|
||||
);
|
||||
});
|
||||
|
||||
it("moves START backward in whole days for allday when END moves earlier", async () => {
|
||||
await renderField({
|
||||
allday: true,
|
||||
startDate: "2025-05-10",
|
||||
endDate: "2025-05-15",
|
||||
showEndDate: true,
|
||||
});
|
||||
|
||||
const calendarButton = screen.getAllByRole("button");
|
||||
await userEvent.click(calendarButton[1]);
|
||||
const dayButton = screen.getByRole("gridcell", { name: "1" });
|
||||
await userEvent.click(dayButton);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onEndDateChange).toHaveBeenCalledWith("2025-05-01")
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onStartDateChange).toHaveBeenCalledWith("2025-04-26")
|
||||
);
|
||||
});
|
||||
|
||||
it("does not call handlers if invalid (null) date value", async () => {
|
||||
await renderField({
|
||||
showMore: true,
|
||||
});
|
||||
|
||||
const startDateInput = screen.getByTestId("start-date-input");
|
||||
|
||||
fireEvent.change(startDateInput, { target: { value: "" } });
|
||||
|
||||
expect(mockHandlers.onStartDateChange).not.toHaveBeenCalled();
|
||||
expect(mockHandlers.onEndDateChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shift to preserve original duration (normal case)", async () => {
|
||||
await renderField({
|
||||
startDate: "2025-01-01",
|
||||
startTime: "09:00",
|
||||
endDate: "2025-01-01",
|
||||
endTime: "10:00",
|
||||
showMore: true,
|
||||
});
|
||||
|
||||
const startTimeInput = screen.getByTestId("start-time-input");
|
||||
|
||||
fireEvent.change(startTimeInput, { target: { value: "09:30" } });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onStartTimeChange).toHaveBeenCalledWith("09:30")
|
||||
);
|
||||
expect(mockHandlers.onEndTimeChange).toHaveBeenCalledWith("10:30");
|
||||
});
|
||||
|
||||
it("preserves 1-hour duration across midnight when changing start time from 22:30 to 23:45", async () => {
|
||||
await renderField({
|
||||
startDate: "2025-01-15",
|
||||
startTime: "22:30",
|
||||
endDate: "2025-01-15",
|
||||
endTime: "23:30",
|
||||
showMore: true,
|
||||
});
|
||||
|
||||
const startTimeInput = screen.getByTestId("start-time-input");
|
||||
|
||||
// Change start time from 22:30 to 23:45
|
||||
fireEvent.change(startTimeInput, { target: { value: "23:45" } });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onStartTimeChange).toHaveBeenCalledWith("23:45")
|
||||
);
|
||||
|
||||
// End date should move to the next day
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onEndDateChange).toHaveBeenCalledWith(
|
||||
"2025-01-16",
|
||||
"00:45"
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -265,9 +265,9 @@ export default function EventFormFields({
|
||||
|
||||
// Change handlers for 4 separate fields
|
||||
const handleStartDateChange = React.useCallback(
|
||||
(newDate: string) => {
|
||||
(newDate: string, newTime?: string) => {
|
||||
setStartDate(newDate);
|
||||
const newStart = combineDateTime(newDate, startTime);
|
||||
const newStart = combineDateTime(newDate, newTime ? newTime : startTime);
|
||||
|
||||
if (onStartChange) {
|
||||
onStartChange(newStart);
|
||||
@@ -279,9 +279,9 @@ export default function EventFormFields({
|
||||
);
|
||||
|
||||
const handleStartTimeChange = React.useCallback(
|
||||
(newTime: string) => {
|
||||
(newTime: string, newDate?: string) => {
|
||||
setStartTime(newTime);
|
||||
const newStart = combineDateTime(startDate, newTime);
|
||||
const newStart = combineDateTime(newDate ? newDate : startDate, newTime);
|
||||
if (onStartChange) {
|
||||
onStartChange(newStart);
|
||||
} else {
|
||||
@@ -292,13 +292,13 @@ export default function EventFormFields({
|
||||
);
|
||||
|
||||
const handleEndDateChange = React.useCallback(
|
||||
(newDate: string) => {
|
||||
(newDate: string, newTime?: string) => {
|
||||
setEndDate(newDate);
|
||||
// Track if user changed end date in extended mode
|
||||
if (showMore) {
|
||||
setHasEndDateChanged(true);
|
||||
}
|
||||
const newEnd = combineDateTime(newDate, endTime);
|
||||
const newEnd = combineDateTime(newDate, newTime ? newTime : endTime);
|
||||
if (onEndChange) {
|
||||
onEndChange(newEnd);
|
||||
} else {
|
||||
@@ -309,9 +309,10 @@ export default function EventFormFields({
|
||||
);
|
||||
|
||||
const handleEndTimeChange = React.useCallback(
|
||||
(newTime: string) => {
|
||||
(newTime: string, newDate?: string) => {
|
||||
setEndTime(newTime);
|
||||
const newEnd = combineDateTime(endDate, newTime);
|
||||
|
||||
const newEnd = combineDateTime(newDate ? newDate : endDate, newTime);
|
||||
if (onEndChange) {
|
||||
onEndChange(newEnd);
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||
import { TimePicker } from "@mui/x-date-pickers/TimePicker";
|
||||
@@ -13,6 +13,9 @@ import "dayjs/locale/en";
|
||||
import "dayjs/locale/ru";
|
||||
import "dayjs/locale/vi";
|
||||
|
||||
import { PickerValue } from "@mui/x-date-pickers/internals";
|
||||
import { dtDate, dtTime, toDateTime } from "../utils/dateTimeHelpers";
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
/**
|
||||
@@ -35,7 +38,7 @@ export interface DateTimeFieldsProps {
|
||||
};
|
||||
onStartDateChange: (date: string) => void;
|
||||
onStartTimeChange: (time: string) => void;
|
||||
onEndDateChange: (date: string) => void;
|
||||
onEndDateChange: (date: string, time?: string) => void;
|
||||
onEndTimeChange: (time: string) => void;
|
||||
}
|
||||
|
||||
@@ -60,15 +63,199 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
onEndTimeChange,
|
||||
}) => {
|
||||
const { t, lang } = useI18n();
|
||||
|
||||
const initialDurationRef = React.useRef<number | null>(null);
|
||||
const isUserActionRef = React.useRef(false);
|
||||
|
||||
const getCurrentDuration = React.useCallback((): number => {
|
||||
const start = toDateTime(startDate, startTime);
|
||||
const end = toDateTime(endDate, endTime);
|
||||
|
||||
if (allday) {
|
||||
return Math.max(end.startOf("day").diff(start.startOf("day"), "day"), 0);
|
||||
} else {
|
||||
return Math.max(end.diff(start, "minute"), 0);
|
||||
}
|
||||
}, [startDate, startTime, endDate, endTime, allday]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isUserActionRef.current && startDate && endDate) {
|
||||
initialDurationRef.current = getCurrentDuration();
|
||||
}
|
||||
queueMicrotask(() => {
|
||||
isUserActionRef.current = false;
|
||||
});
|
||||
}, [startDate, endDate, startTime, endTime, getCurrentDuration]);
|
||||
|
||||
const spansMultipleDays = React.useMemo(() => {
|
||||
return startDate !== endDate;
|
||||
}, [startDate, endDate]);
|
||||
|
||||
const isExpanded = showMore;
|
||||
const shouldShowEndDateNormal = allday || !!showEndDate;
|
||||
const shouldShowFullFieldsInNormal = !allday && hasEndDateChanged;
|
||||
const shouldShowEndDateNormal = allday || showEndDate;
|
||||
const shouldShowFullFieldsInNormal =
|
||||
(!allday && hasEndDateChanged) || spansMultipleDays;
|
||||
const showSingleDateField =
|
||||
!isExpanded && !shouldShowEndDateNormal && !shouldShowFullFieldsInNormal;
|
||||
|
||||
const startDateLabel = showSingleDateField
|
||||
? t("dateTimeFields.date")
|
||||
: t("dateTimeFields.startDate");
|
||||
|
||||
const handleStartDateChange = (value: PickerValue) => {
|
||||
if (!value || !value.isValid()) return;
|
||||
|
||||
isUserActionRef.current = true;
|
||||
const newDateStr = dtDate(value as Dayjs);
|
||||
const duration = initialDurationRef.current ?? getCurrentDuration();
|
||||
|
||||
onStartDateChange(newDateStr);
|
||||
|
||||
// Preserve duration by adjusting end
|
||||
const newStart = toDateTime(newDateStr, startTime);
|
||||
let newEnd: Dayjs;
|
||||
|
||||
if (allday) {
|
||||
newEnd = newStart.add(duration, "day");
|
||||
} else {
|
||||
newEnd = newStart.add(duration, "minute");
|
||||
}
|
||||
|
||||
const newEndDate = dtDate(newEnd);
|
||||
const newEndTime = dtTime(newEnd);
|
||||
|
||||
if (newEndDate !== endDate) {
|
||||
onEndDateChange(newEndDate);
|
||||
}
|
||||
if (!allday && newEndTime !== endTime) {
|
||||
onEndTimeChange(newEndTime);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartTimeChange = (value: PickerValue) => {
|
||||
if (!value || !value.isValid()) return;
|
||||
|
||||
isUserActionRef.current = true;
|
||||
const newTimeStr = dtTime(value as Dayjs);
|
||||
const duration = initialDurationRef.current ?? getCurrentDuration();
|
||||
|
||||
onStartTimeChange(newTimeStr);
|
||||
|
||||
const newStart = toDateTime(startDate, newTimeStr);
|
||||
const newEnd = newStart.add(duration, "minute");
|
||||
|
||||
const newEndDate = dtDate(newEnd);
|
||||
const newEndTime = dtTime(newEnd);
|
||||
|
||||
if (newEndDate !== endDate && newEndTime !== endTime) {
|
||||
onEndDateChange(newEndDate, newEndTime);
|
||||
} else {
|
||||
if (newEndDate !== endDate) {
|
||||
onEndDateChange(newEndDate);
|
||||
}
|
||||
if (!allday && newEndTime !== endTime) {
|
||||
onEndTimeChange(newEndTime);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleEndDateChange = (value: PickerValue) => {
|
||||
if (!value || !value.isValid()) return;
|
||||
|
||||
isUserActionRef.current = true;
|
||||
const newDateStr = dtDate(value as Dayjs);
|
||||
const newEnd = toDateTime(newDateStr, endTime);
|
||||
const currentStart = toDateTime(startDate, startTime);
|
||||
|
||||
if (newEnd.isBefore(currentStart)) {
|
||||
const duration = initialDurationRef.current ?? getCurrentDuration();
|
||||
let newStart: Dayjs;
|
||||
|
||||
if (allday) {
|
||||
newStart = newEnd.subtract(duration, "day");
|
||||
} else {
|
||||
newStart = newEnd.subtract(duration, "minute");
|
||||
}
|
||||
|
||||
const newStartDate = dtDate(newStart);
|
||||
const newStartTime = dtTime(newStart);
|
||||
|
||||
if (newStartDate !== startDate) {
|
||||
onStartDateChange(newStartDate);
|
||||
}
|
||||
if (!allday && newStartTime !== startTime) {
|
||||
onStartTimeChange(newStartTime);
|
||||
}
|
||||
} else {
|
||||
initialDurationRef.current = newEnd.diff(
|
||||
currentStart,
|
||||
allday ? "day" : "minute"
|
||||
);
|
||||
}
|
||||
|
||||
onEndDateChange(newDateStr);
|
||||
};
|
||||
|
||||
const handleEndTimeChange = (value: PickerValue) => {
|
||||
if (!value || !value.isValid()) return;
|
||||
|
||||
isUserActionRef.current = true;
|
||||
const newTimeStr = dtTime(value as Dayjs);
|
||||
const newEnd = toDateTime(endDate, newTimeStr);
|
||||
const currentStart = toDateTime(startDate, startTime);
|
||||
|
||||
// If end is before start, adjust start to maintain duration
|
||||
if (newEnd.isBefore(currentStart)) {
|
||||
const duration = initialDurationRef.current ?? getCurrentDuration();
|
||||
const newStart = newEnd.subtract(duration, "minute");
|
||||
|
||||
const newStartDate = dtDate(newStart);
|
||||
const newStartTime = dtTime(newStart);
|
||||
|
||||
if (newStartDate !== startDate) {
|
||||
onStartDateChange(newStartDate);
|
||||
}
|
||||
if (newStartTime !== startTime) {
|
||||
onStartTimeChange(newStartTime);
|
||||
}
|
||||
} else {
|
||||
// Update duration when user changes end
|
||||
initialDurationRef.current = newEnd.diff(currentStart, "minute");
|
||||
}
|
||||
|
||||
onEndTimeChange(newTimeStr);
|
||||
};
|
||||
|
||||
// Memoize parsed date/time values
|
||||
const startDateValue = useMemo(
|
||||
() => (startDate ? dayjs(startDate) : null),
|
||||
[startDate]
|
||||
);
|
||||
const startTimeValue = useMemo(
|
||||
() => (startTime ? dayjs(startTime, "HH:mm") : null),
|
||||
[startTime]
|
||||
);
|
||||
const endDateValue = useMemo(
|
||||
() => (endDate ? dayjs(endDate) : null),
|
||||
[endDate]
|
||||
);
|
||||
const endTimeValue = useMemo(
|
||||
() => (endTime ? dayjs(endTime, "HH:mm") : null),
|
||||
[endTime]
|
||||
);
|
||||
|
||||
const getSlotProps = (testId: string, hasError = false) => ({
|
||||
textField: {
|
||||
size: "small" as const,
|
||||
margin: "dense" as const,
|
||||
fullWidth: true,
|
||||
InputLabelProps: { shrink: true },
|
||||
error: hasError,
|
||||
sx: { width: "100%" },
|
||||
inputProps: { "data-testid": testId },
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<LocalizationProvider
|
||||
dateAdapter={AdapterDayjs}
|
||||
@@ -91,25 +278,9 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
<DatePicker
|
||||
label={t("dateTimeFields.startDate")}
|
||||
format={LONG_DATE_FORMAT}
|
||||
value={startDate ? dayjs(startDate) : null}
|
||||
onChange={(newValue) => {
|
||||
const value = newValue as Dayjs | null;
|
||||
if (!value || !value.isValid()) {
|
||||
return;
|
||||
}
|
||||
const formatted = value.format("YYYY-MM-DD");
|
||||
onStartDateChange(formatted);
|
||||
}}
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: "small",
|
||||
margin: "dense" as const,
|
||||
fullWidth: true,
|
||||
InputLabelProps: { shrink: true },
|
||||
sx: { width: "100%" },
|
||||
inputProps: { "data-testid": "start-date-input" },
|
||||
},
|
||||
}}
|
||||
value={startDateValue}
|
||||
onChange={handleStartDateChange}
|
||||
slotProps={getSlotProps("start-date-input")}
|
||||
/>
|
||||
</Box>
|
||||
{!allday && (
|
||||
@@ -117,25 +288,9 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
<TimePicker
|
||||
label={t("dateTimeFields.startTime")}
|
||||
ampm={false}
|
||||
value={startTime ? dayjs(startTime, "HH:mm") : null}
|
||||
onChange={(newValue) => {
|
||||
const value = newValue as Dayjs | null;
|
||||
if (!value || !value.isValid()) {
|
||||
return;
|
||||
}
|
||||
const formatted = value?.format("HH:mm") || "";
|
||||
onStartTimeChange(formatted);
|
||||
}}
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: "small",
|
||||
margin: "dense" as const,
|
||||
fullWidth: true,
|
||||
InputLabelProps: { shrink: true },
|
||||
sx: { width: "100%" },
|
||||
inputProps: { "data-testid": "start-time-input" },
|
||||
},
|
||||
}}
|
||||
value={startTimeValue}
|
||||
onChange={handleStartTimeChange}
|
||||
slotProps={getSlotProps("start-time-input")}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
@@ -145,26 +300,12 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
<DatePicker
|
||||
label={t("dateTimeFields.endDate")}
|
||||
format={LONG_DATE_FORMAT}
|
||||
value={endDate ? dayjs(endDate) : null}
|
||||
onChange={(newValue) => {
|
||||
const value = newValue as Dayjs | null;
|
||||
if (!value || !value.isValid()) {
|
||||
return;
|
||||
}
|
||||
const formatted = value.format("YYYY-MM-DD");
|
||||
onEndDateChange(formatted);
|
||||
}}
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: "small",
|
||||
margin: "dense" as const,
|
||||
fullWidth: true,
|
||||
InputLabelProps: { shrink: true },
|
||||
error: !!validation.errors.dateTime,
|
||||
sx: { width: "100%" },
|
||||
inputProps: { "data-testid": "end-date-input" },
|
||||
},
|
||||
}}
|
||||
value={endDateValue}
|
||||
onChange={handleEndDateChange}
|
||||
slotProps={getSlotProps(
|
||||
"end-date-input",
|
||||
!!validation.errors.dateTime
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
{!allday && (
|
||||
@@ -172,26 +313,12 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
<TimePicker
|
||||
label={t("dateTimeFields.endTime")}
|
||||
ampm={false}
|
||||
value={endTime ? dayjs(endTime, "HH:mm") : null}
|
||||
onChange={(newValue) => {
|
||||
const value = newValue as Dayjs | null;
|
||||
if (!value || !value.isValid()) {
|
||||
return;
|
||||
}
|
||||
const formatted = value?.format("HH:mm") || "";
|
||||
onEndTimeChange(formatted);
|
||||
}}
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: "small",
|
||||
margin: "dense" as const,
|
||||
fullWidth: true,
|
||||
InputLabelProps: { shrink: true },
|
||||
error: !!validation.errors.dateTime,
|
||||
sx: { width: "100%" },
|
||||
inputProps: { "data-testid": "end-time-input" },
|
||||
},
|
||||
}}
|
||||
value={endTimeValue}
|
||||
onChange={handleEndTimeChange}
|
||||
slotProps={getSlotProps(
|
||||
"end-time-input",
|
||||
!!validation.errors.dateTime
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
@@ -203,51 +330,21 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
<DatePicker
|
||||
label={t("dateTimeFields.startDate")}
|
||||
format={LONG_DATE_FORMAT}
|
||||
value={startDate ? dayjs(startDate) : null}
|
||||
onChange={(newValue) => {
|
||||
const value = newValue as Dayjs | null;
|
||||
if (!value || !value.isValid()) {
|
||||
return;
|
||||
}
|
||||
const formatted = value.format("YYYY-MM-DD");
|
||||
onStartDateChange(formatted);
|
||||
}}
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: "small",
|
||||
margin: "dense" as const,
|
||||
fullWidth: true,
|
||||
InputLabelProps: { shrink: true },
|
||||
sx: { width: "100%" },
|
||||
inputProps: { "data-testid": "start-date-input" },
|
||||
},
|
||||
}}
|
||||
value={startDateValue}
|
||||
onChange={handleStartDateChange}
|
||||
slotProps={getSlotProps("start-date-input")}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||
<DatePicker
|
||||
label={t("dateTimeFields.endDate")}
|
||||
format={LONG_DATE_FORMAT}
|
||||
value={endDate ? dayjs(endDate) : null}
|
||||
onChange={(newValue) => {
|
||||
const value = newValue as Dayjs | null;
|
||||
if (!value || !value.isValid()) {
|
||||
return;
|
||||
}
|
||||
const formatted = value.format("YYYY-MM-DD");
|
||||
onEndDateChange(formatted);
|
||||
}}
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: "small",
|
||||
margin: "dense" as const,
|
||||
fullWidth: true,
|
||||
InputLabelProps: { shrink: true },
|
||||
error: !!validation.errors.dateTime,
|
||||
sx: { width: "100%" },
|
||||
inputProps: { "data-testid": "end-date-input" },
|
||||
},
|
||||
}}
|
||||
value={endDateValue}
|
||||
onChange={handleEndDateChange}
|
||||
slotProps={getSlotProps(
|
||||
"end-date-input",
|
||||
!!validation.errors.dateTime
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -257,66 +354,27 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
<DatePicker
|
||||
label={startDateLabel}
|
||||
format={LONG_DATE_FORMAT}
|
||||
value={startDate ? dayjs(startDate) : null}
|
||||
onChange={(newValue) => {
|
||||
const value = newValue as Dayjs | null;
|
||||
if (!value || !value.isValid()) {
|
||||
return;
|
||||
}
|
||||
const formatted = value.format("YYYY-MM-DD");
|
||||
onStartDateChange(formatted);
|
||||
}}
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: "small",
|
||||
margin: "dense" as const,
|
||||
fullWidth: true,
|
||||
InputLabelProps: { shrink: true },
|
||||
sx: { width: "100%" },
|
||||
inputProps: { "data-testid": "start-date-input" },
|
||||
},
|
||||
}}
|
||||
value={startDateValue}
|
||||
onChange={handleStartDateChange}
|
||||
slotProps={getSlotProps("start-date-input")}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ maxWidth: "110px" }}>
|
||||
<TimePicker
|
||||
label={t("dateTimeFields.startTime")}
|
||||
ampm={false}
|
||||
value={startTime ? dayjs(startTime, "HH:mm") : null}
|
||||
onChange={(newValue) => {
|
||||
const value = newValue as Dayjs | null;
|
||||
if (!value || !value.isValid()) {
|
||||
return;
|
||||
}
|
||||
const formatted = value?.format("HH:mm") || "";
|
||||
onStartTimeChange(formatted);
|
||||
}}
|
||||
value={startTimeValue}
|
||||
onChange={handleStartTimeChange}
|
||||
disabled={allday}
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: "small",
|
||||
margin: "dense" as const,
|
||||
fullWidth: true,
|
||||
InputLabelProps: { shrink: true },
|
||||
sx: { width: "100%" },
|
||||
inputProps: { "data-testid": "start-time-input" },
|
||||
},
|
||||
}}
|
||||
slotProps={getSlotProps("start-time-input")}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ maxWidth: "110px" }}>
|
||||
<TimePicker
|
||||
label={t("dateTimeFields.endTime")}
|
||||
ampm={false}
|
||||
value={endTime ? dayjs(endTime, "HH:mm") : null}
|
||||
onChange={(newValue) => {
|
||||
const value = newValue as Dayjs | null;
|
||||
if (!value || !value.isValid()) {
|
||||
return;
|
||||
}
|
||||
const formatted = value?.format("HH:mm") || "";
|
||||
onEndTimeChange(formatted);
|
||||
}}
|
||||
value={endTimeValue}
|
||||
onChange={handleEndTimeChange}
|
||||
disabled={allday}
|
||||
slotProps={{
|
||||
textField: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import moment from "moment-timezone";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
|
||||
/**
|
||||
* Helper functions for date/time string manipulation
|
||||
@@ -68,3 +69,17 @@ export function convertFormDateTimeToISO(
|
||||
}
|
||||
return momentDate.toDate().toISOString();
|
||||
}
|
||||
|
||||
/** Convert date + time strings → Dayjs */
|
||||
export const toDateTime = (date: string, time: string): Dayjs => {
|
||||
const d = dayjs(date, "YYYY-MM-DD", true);
|
||||
if (!time) return d.startOf("day");
|
||||
const [h, m] = time.split(":").map(Number);
|
||||
return d.hour(h).minute(m).second(0).millisecond(0);
|
||||
};
|
||||
|
||||
/** Extract date “YYYY-MM-DD” */
|
||||
export const dtDate = (d: Dayjs) => d.format("YYYY-MM-DD");
|
||||
|
||||
/** Extract time “HH:mm” */
|
||||
export const dtTime = (d: Dayjs) => d.format("HH:mm");
|
||||
|
||||
Reference in New Issue
Block a user