Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -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