Use @linagora/twake-mui 1.1.6 from registry
This commit is contained in:
@@ -400,6 +400,10 @@ describe("CalendarApp integration", () => {
|
|||||||
preloadedState
|
preloadedState
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Expand to show date/time inputs (normal mode shows DateTimeSummary)
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: "common.moreOptions" })
|
||||||
|
);
|
||||||
const startDateInput = await screen.findByTestId("start-time-input");
|
const startDateInput = await screen.findByTestId("start-time-input");
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|||||||
@@ -164,12 +164,12 @@ describe("Event Preview Display", () => {
|
|||||||
expect(screen.getByText(/January/i)).toBeInTheDocument();
|
expect(screen.getByText(/January/i)).toBeInTheDocument();
|
||||||
expect(screen.getByText(/15/)).toBeInTheDocument();
|
expect(screen.getByText(/15/)).toBeInTheDocument();
|
||||||
|
|
||||||
// Check time is displayed with exact values
|
// Check time is displayed in 24h format (no AM/PM)
|
||||||
// Format: "Wednesday, January 15, 2025 at 10:00 AM" and " – 10:00 AM" are in separate elements
|
// Format: "Wednesday, January 15, 2025 at 10:00" and " – 10:00" are in separate elements
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(/Wednesday, January 15, 2025 at 10:00 AM/)
|
screen.getByText(/Wednesday, January 15, 2025 at 10:00/)
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByText(/– 10:00 AM/)).toBeInTheDocument();
|
expect(screen.getByText(/– 10:00/)).toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.getByText("Calendar")).toBeInTheDocument();
|
expect(screen.getByText("Calendar")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -1719,6 +1719,9 @@ describe("Event Full Display", () => {
|
|||||||
|
|
||||||
expect(screen.getByDisplayValue("Test Event")).toBeInTheDocument();
|
expect(screen.getByDisplayValue("Test Event")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Expand to show date/time inputs (normal mode shows DateTimeSummary)
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||||
|
|
||||||
const startDateInput = screen.getByTestId("start-date-input");
|
const startDateInput = screen.getByTestId("start-date-input");
|
||||||
const startTimeInput = screen.getByTestId("start-time-input");
|
const startTimeInput = screen.getByTestId("start-time-input");
|
||||||
const endTimeInput = screen.getByTestId("end-time-input");
|
const endTimeInput = screen.getByTestId("end-time-input");
|
||||||
@@ -1804,6 +1807,7 @@ describe("Event Full Display", () => {
|
|||||||
preloadedState
|
preloadedState
|
||||||
);
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||||
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
||||||
fireEvent.click(allDayCheckbox);
|
fireEvent.click(allDayCheckbox);
|
||||||
expect(allDayCheckbox).toBeChecked();
|
expect(allDayCheckbox).toBeChecked();
|
||||||
@@ -1878,6 +1882,7 @@ describe("Event Full Display", () => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||||
const calendarSelect = screen.getByLabelText("event.form.calendar");
|
const calendarSelect = screen.getByLabelText("event.form.calendar");
|
||||||
await act(async () => fireEvent.mouseDown(calendarSelect));
|
await act(async () => fireEvent.mouseDown(calendarSelect));
|
||||||
|
|
||||||
@@ -1947,9 +1952,9 @@ describe("Event Full Display", () => {
|
|||||||
stateWithTimezone
|
stateWithTimezone
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Expand to show timezone selector (normal mode hides it)
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||||
// The timezone select should have Asia/Bangkok selected
|
// The timezone select should have Asia/Bangkok selected
|
||||||
// Since the component uses formatLocalDateTime, the displayed time will be in local format
|
|
||||||
// but the timezone selector should show Asia/Bangkok
|
|
||||||
const timeZone = screen.getByDisplayValue(/Asia\/Bangkok/i);
|
const timeZone = screen.getByDisplayValue(/Asia\/Bangkok/i);
|
||||||
expect(timeZone).toBeInTheDocument();
|
expect(timeZone).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -124,8 +124,10 @@ describe("EventPopover", () => {
|
|||||||
});
|
});
|
||||||
expect(addDescriptionButton).toBeInTheDocument();
|
expect(addDescriptionButton).toBeInTheDocument();
|
||||||
|
|
||||||
const calendarSelect = screen.getByRole("combobox", { name: /calendar/i });
|
// In normal mode calendar is a SectionPreviewRow (button), not combobox
|
||||||
expect(calendarSelect).toBeInTheDocument();
|
expect(
|
||||||
|
screen.getByRole("button", { name: "Calendar 1" })
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
// Check button
|
// Check button
|
||||||
expect(
|
expect(
|
||||||
@@ -154,7 +156,8 @@ describe("EventPopover", () => {
|
|||||||
allDay: false,
|
allDay: false,
|
||||||
} as unknown as DateSelectArg);
|
} as unknown as DateSelectArg);
|
||||||
|
|
||||||
// MUI DatePicker/TimePicker values are stored differently - just check elements exist
|
// Expand to show date/time inputs (normal mode shows DateTimeSummary)
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||||
expect(screen.getByTestId("start-date-input")).toBeInTheDocument();
|
expect(screen.getByTestId("start-date-input")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("start-time-input")).toBeInTheDocument();
|
expect(screen.getByTestId("start-time-input")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -179,6 +182,10 @@ describe("EventPopover", () => {
|
|||||||
"Event Description"
|
"Event Description"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Expand location section (normal mode shows SectionPreviewRow)
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: "event.form.locationPlaceholder" })
|
||||||
|
);
|
||||||
fireEvent.change(screen.getByLabelText("event.form.location"), {
|
fireEvent.change(screen.getByLabelText("event.form.location"), {
|
||||||
target: { value: "Conference Room" },
|
target: { value: "Conference Room" },
|
||||||
});
|
});
|
||||||
@@ -190,13 +197,14 @@ describe("EventPopover", () => {
|
|||||||
it("changes selected calendar", async () => {
|
it("changes selected calendar", async () => {
|
||||||
renderPopover();
|
renderPopover();
|
||||||
|
|
||||||
|
// Expand to show calendar combobox (normal mode shows SectionPreviewRow)
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||||
const select = screen.getByLabelText("event.form.calendar");
|
const select = screen.getByLabelText("event.form.calendar");
|
||||||
fireEvent.mouseDown(select); // Open menu
|
fireEvent.mouseDown(select); // Open menu
|
||||||
|
|
||||||
const option = await screen.findByText("Calendar 2");
|
const option = await screen.findByText("Calendar 2");
|
||||||
fireEvent.click(option);
|
fireEvent.click(option);
|
||||||
|
|
||||||
// Find the calendar combobox specifically by its aria-labelledby
|
|
||||||
const calendarSelect = screen.getByRole("combobox", { name: /Calendar/i });
|
const calendarSelect = screen.getByRole("combobox", { name: /Calendar/i });
|
||||||
expect(calendarSelect).toHaveTextContent("Calendar 2");
|
expect(calendarSelect).toHaveTextContent("Calendar 2");
|
||||||
});
|
});
|
||||||
@@ -290,6 +298,9 @@ describe("EventPopover", () => {
|
|||||||
fireEvent.change(screen.getByLabelText("event.form.description"), {
|
fireEvent.change(screen.getByLabelText("event.form.description"), {
|
||||||
target: { value: newEvent.description },
|
target: { value: newEvent.description },
|
||||||
});
|
});
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: "event.form.locationPlaceholder" })
|
||||||
|
);
|
||||||
fireEvent.change(screen.getByLabelText("event.form.location"), {
|
fireEvent.change(screen.getByLabelText("event.form.location"), {
|
||||||
target: { value: newEvent.location },
|
target: { value: newEvent.location },
|
||||||
});
|
});
|
||||||
@@ -338,10 +349,11 @@ describe("EventPopover", () => {
|
|||||||
|
|
||||||
it("BUGFIX: Prefill Calendar field", async () => {
|
it("BUGFIX: Prefill Calendar field", async () => {
|
||||||
renderPopover();
|
renderPopover();
|
||||||
|
// In normal mode calendar is a SectionPreviewRow (button) with calendar name
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getByLabelText("event.form.calendar")).toHaveTextContent(
|
expect(
|
||||||
"Calendar 1"
|
screen.getByRole("button", { name: "Calendar 1" })
|
||||||
)
|
).toHaveTextContent("Calendar 1")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -357,6 +369,9 @@ describe("EventPopover", () => {
|
|||||||
} as unknown as DateSelectArg;
|
} as unknown as DateSelectArg;
|
||||||
|
|
||||||
renderPopover(selectedRange);
|
renderPopover(selectedRange);
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: "common.moreOptions" })
|
||||||
|
);
|
||||||
|
|
||||||
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -375,6 +390,9 @@ describe("EventPopover", () => {
|
|||||||
} as unknown as DateSelectArg;
|
} as unknown as DateSelectArg;
|
||||||
|
|
||||||
renderPopover(selectedRange);
|
renderPopover(selectedRange);
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: "common.moreOptions" })
|
||||||
|
);
|
||||||
|
|
||||||
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -403,6 +421,9 @@ describe("EventPopover", () => {
|
|||||||
} as unknown as DateSelectArg;
|
} as unknown as DateSelectArg;
|
||||||
|
|
||||||
renderPopover(selectedRange);
|
renderPopover(selectedRange);
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: "common.moreOptions" })
|
||||||
|
);
|
||||||
|
|
||||||
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -427,6 +448,9 @@ describe("EventPopover", () => {
|
|||||||
} as unknown as DateSelectArg;
|
} as unknown as DateSelectArg;
|
||||||
|
|
||||||
renderPopover(selectedRange);
|
renderPopover(selectedRange);
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: "common.moreOptions" })
|
||||||
|
);
|
||||||
|
|
||||||
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -451,6 +475,9 @@ describe("EventPopover", () => {
|
|||||||
} as unknown as DateSelectArg;
|
} as unknown as DateSelectArg;
|
||||||
|
|
||||||
renderPopover(selectedRange);
|
renderPopover(selectedRange);
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: "common.moreOptions" })
|
||||||
|
);
|
||||||
|
|
||||||
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -1176,7 +1176,8 @@ describe("Event URL handling for recurring events", () => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Change calendar
|
// Expand to show calendar combobox (normal mode shows SectionPreviewRow)
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||||
fireEvent.mouseDown(screen.getByLabelText("event.form.calendar"));
|
fireEvent.mouseDown(screen.getByLabelText("event.form.calendar"));
|
||||||
const option = await screen.findByText("Calendar 2");
|
const option = await screen.findByText("Calendar 2");
|
||||||
fireEvent.click(option);
|
fireEvent.click(option);
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ describe("EventUpdateModal Timezone Handling", () => {
|
|||||||
const titleInput = screen.getByDisplayValue("Timezone Event");
|
const titleInput = screen.getByDisplayValue("Timezone Event");
|
||||||
expect(titleInput).toBeInTheDocument();
|
expect(titleInput).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Expand to show date/time inputs (normal mode shows DateTimeSummary)
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||||
|
|
||||||
// Verify the start date and time inputs exist
|
// Verify the start date and time inputs exist
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const startDateInput = screen.getByTestId("start-date-input");
|
const startDateInput = screen.getByTestId("start-date-input");
|
||||||
@@ -282,6 +285,7 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
|||||||
expect(mockGetMasterEvent).toHaveBeenCalled();
|
expect(mockGetMasterEvent).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||||
// Wait for the repeat checkbox to be checked (indicating form is initialized)
|
// Wait for the repeat checkbox to be checked (indicating form is initialized)
|
||||||
const repeatCheckbox = await waitFor(() => {
|
const repeatCheckbox = await waitFor(() => {
|
||||||
const checkbox = screen.getByLabelText("event.form.repeat");
|
const checkbox = screen.getByLabelText("event.form.repeat");
|
||||||
@@ -414,6 +418,7 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
|||||||
expect(screen.getByDisplayValue("Recurring Meeting")).toBeInTheDocument();
|
expect(screen.getByDisplayValue("Recurring Meeting")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||||
// Wait for repeat checkbox to be checked
|
// Wait for repeat checkbox to be checked
|
||||||
const repeatCheckbox = await waitFor(() => {
|
const repeatCheckbox = await waitFor(() => {
|
||||||
const checkbox = screen.getByLabelText("event.form.repeat");
|
const checkbox = screen.getByLabelText("event.form.repeat");
|
||||||
@@ -546,6 +551,7 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
|||||||
expect(screen.getByDisplayValue("Recurring Meeting")).toBeInTheDocument();
|
expect(screen.getByDisplayValue("Recurring Meeting")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||||
// Uncheck repeat and save
|
// Uncheck repeat and save
|
||||||
const repeatCheckbox = screen.getByLabelText("event.form.repeat");
|
const repeatCheckbox = screen.getByLabelText("event.form.repeat");
|
||||||
|
|
||||||
|
|||||||
Generated
+21
-2537
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
"@fullcalendar/moment-timezone": "^6.1.19",
|
"@fullcalendar/moment-timezone": "^6.1.19",
|
||||||
"@fullcalendar/react": "^6.1.17",
|
"@fullcalendar/react": "^6.1.17",
|
||||||
"@fullcalendar/timegrid": "^6.1.17",
|
"@fullcalendar/timegrid": "^6.1.17",
|
||||||
"@linagora/twake-mui": "^1.1.7",
|
"@linagora/twake-mui": "1.1.7",
|
||||||
"@lottiefiles/dotlottie-react": "^0.17.13",
|
"@lottiefiles/dotlottie-react": "^0.17.13",
|
||||||
"@mui/icons-material": "^7.1.2",
|
"@mui/icons-material": "^7.1.2",
|
||||||
"@mui/material": "^7.1.2",
|
"@mui/material": "^7.1.2",
|
||||||
|
|||||||
@@ -75,8 +75,6 @@ export function SettingsTab({
|
|||||||
showMore={false}
|
showMore={false}
|
||||||
description={description}
|
description={description}
|
||||||
setDescription={setDescription}
|
setDescription={setDescription}
|
||||||
buttonVariant="contained"
|
|
||||||
buttonColor="secondary"
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Box, Button, TextField, useTheme } from "@linagora/twake-mui";
|
import { TextField } from "@linagora/twake-mui";
|
||||||
import { alpha } from "@mui/material/styles";
|
|
||||||
import { Notes as NotesIcon } from "@mui/icons-material";
|
import { Notes as NotesIcon } from "@mui/icons-material";
|
||||||
|
import React from "react";
|
||||||
import { useI18n } from "twake-i18n";
|
import { useI18n } from "twake-i18n";
|
||||||
import { FieldWithLabel } from "./components/FieldWithLabel";
|
import { FieldWithLabel } from "./components/FieldWithLabel";
|
||||||
|
import { SectionPreviewRow } from "./components/SectionPreviewRow";
|
||||||
|
|
||||||
export function AddDescButton({
|
export function AddDescButton({
|
||||||
showDescription,
|
showDescription,
|
||||||
@@ -10,92 +11,72 @@ export function AddDescButton({
|
|||||||
showMore,
|
showMore,
|
||||||
description,
|
description,
|
||||||
setDescription,
|
setDescription,
|
||||||
buttonVariant,
|
|
||||||
buttonColor,
|
|
||||||
}: {
|
}: {
|
||||||
showDescription: boolean;
|
showDescription: boolean;
|
||||||
setShowDescription: (b: boolean) => void;
|
setShowDescription: (b: boolean) => void;
|
||||||
showMore: boolean;
|
showMore: boolean;
|
||||||
description: string;
|
description: string;
|
||||||
setDescription: (d: string) => void;
|
setDescription: (d: string) => void;
|
||||||
buttonVariant?: "text" | "outlined" | "contained";
|
|
||||||
buttonColor?:
|
|
||||||
| "inherit"
|
|
||||||
| "primary"
|
|
||||||
| "secondary"
|
|
||||||
| "success"
|
|
||||||
| "error"
|
|
||||||
| "info"
|
|
||||||
| "warning";
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const theme = useTheme();
|
const descriptionInputRef = React.useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (showDescription) {
|
||||||
|
descriptionInputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [showDescription]);
|
||||||
|
|
||||||
|
const descriptionField = (
|
||||||
|
<FieldWithLabel
|
||||||
|
label={t("event.form.description")}
|
||||||
|
isExpanded={showMore}
|
||||||
|
sx={{ padding: 0, margin: 0 }}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label=""
|
||||||
|
inputRef={descriptionInputRef}
|
||||||
|
inputProps={{ "aria-label": t("event.form.description") }}
|
||||||
|
placeholder={t("event.form.descriptionPlaceholder")}
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
size="small"
|
||||||
|
margin="dense"
|
||||||
|
multiline
|
||||||
|
minRows={2}
|
||||||
|
maxRows={10}
|
||||||
|
sx={{
|
||||||
|
"& .MuiInputBase-root": {
|
||||||
|
maxHeight: "33%",
|
||||||
|
overflowY: "auto",
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
"& textarea": {
|
||||||
|
resize: "vertical",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FieldWithLabel>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (showMore) {
|
||||||
|
return descriptionField;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!showDescription && (
|
{!showDescription && (
|
||||||
<FieldWithLabel
|
<FieldWithLabel label="" isExpanded={showMore}>
|
||||||
label={showMore || showDescription ? t("event.form.description") : ""}
|
<SectionPreviewRow
|
||||||
isExpanded={showMore}
|
icon={<NotesIcon />}
|
||||||
>
|
onClick={() => setShowDescription(true)}
|
||||||
{!showMore ? (
|
>
|
||||||
<Button
|
{t("event.form.addDescription")}
|
||||||
startIcon={<NotesIcon />}
|
</SectionPreviewRow>
|
||||||
onClick={() => setShowDescription(true)}
|
|
||||||
fullWidth
|
|
||||||
sx={{
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
borderRadius: "4px",
|
|
||||||
color: alpha(theme.palette.grey[900], 0.9),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("event.form.addDescription")}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Box display="flex" gap={1}>
|
|
||||||
<Button
|
|
||||||
startIcon={<NotesIcon />}
|
|
||||||
onClick={() => setShowDescription(true)}
|
|
||||||
size="medium"
|
|
||||||
variant={buttonVariant}
|
|
||||||
color={buttonColor}
|
|
||||||
>
|
|
||||||
{t("event.form.addDescription")}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</FieldWithLabel>
|
|
||||||
)}
|
|
||||||
{showDescription && (
|
|
||||||
<FieldWithLabel
|
|
||||||
label={t("event.form.description")}
|
|
||||||
isExpanded={showMore}
|
|
||||||
sx={{ padding: 0, margin: 0 }}
|
|
||||||
>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label=""
|
|
||||||
inputProps={{ "aria-label": t("event.form.description") }}
|
|
||||||
placeholder={t("event.form.descriptionPlaceholder")}
|
|
||||||
value={description}
|
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
|
||||||
size="small"
|
|
||||||
margin="dense"
|
|
||||||
multiline
|
|
||||||
minRows={2}
|
|
||||||
maxRows={10}
|
|
||||||
sx={{
|
|
||||||
"& .MuiInputBase-root": {
|
|
||||||
maxHeight: "33%",
|
|
||||||
overflowY: "auto",
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
"& textarea": {
|
|
||||||
resize: "vertical",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FieldWithLabel>
|
</FieldWithLabel>
|
||||||
)}
|
)}
|
||||||
|
{showDescription && descriptionField}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import { AddDescButton } from "./AddDescButton";
|
|||||||
import { DateTimeFields } from "./components/DateTimeFields";
|
import { DateTimeFields } from "./components/DateTimeFields";
|
||||||
import { DateTimeSummary } from "./components/DateTimeSummary";
|
import { DateTimeSummary } from "./components/DateTimeSummary";
|
||||||
import { FieldWithLabel } from "./components/FieldWithLabel";
|
import { FieldWithLabel } from "./components/FieldWithLabel";
|
||||||
|
import { SectionPreviewRow } from "./components/SectionPreviewRow";
|
||||||
import RepeatEvent from "./EventRepeat";
|
import RepeatEvent from "./EventRepeat";
|
||||||
import { useAllDayToggle } from "./hooks/useAllDayToggle";
|
import { useAllDayToggle } from "./hooks/useAllDayToggle";
|
||||||
import { combineDateTime, splitDateTime } from "./utils/dateTimeHelpers";
|
import { combineDateTime, splitDateTime } from "./utils/dateTimeHelpers";
|
||||||
@@ -171,19 +172,23 @@ export default function EventFormFields({
|
|||||||
|
|
||||||
// Track if user has manually changed end date in extended mode
|
// Track if user has manually changed end date in extended mode
|
||||||
const [hasEndDateChanged, setHasEndDateChanged] = React.useState(false);
|
const [hasEndDateChanged, setHasEndDateChanged] = React.useState(false);
|
||||||
|
|
||||||
// Track if user has clicked on datetime clickable section in normal mode
|
// Track if user has clicked on datetime clickable section in normal mode
|
||||||
// Once clicked, normal mode will always show full fields until modal closes
|
// Once clicked, normal mode will always show full fields until modal closes
|
||||||
const [hasClickedDateTimeSection, setHasClickedDateTimeSection] = React.useState(false);
|
const [hasClickedDateTimeSection, setHasClickedDateTimeSection] =
|
||||||
|
React.useState(false);
|
||||||
|
|
||||||
// Track if user has clicked on location section in normal mode
|
// Track if user has clicked on location section in normal mode
|
||||||
const [hasClickedLocationSection, setHasClickedLocationSection] = React.useState(false);
|
const [hasClickedLocationSection, setHasClickedLocationSection] =
|
||||||
|
React.useState(false);
|
||||||
|
|
||||||
// Track if user has clicked on calendar section in normal mode
|
// Track if user has clicked on calendar section in normal mode
|
||||||
const [hasClickedCalendarSection, setHasClickedCalendarSection] = React.useState(false);
|
const [hasClickedCalendarSection, setHasClickedCalendarSection] =
|
||||||
|
React.useState(false);
|
||||||
|
|
||||||
// Track if user has clicked on video meeting section in normal mode
|
// Track if user has clicked on video meeting section in normal mode
|
||||||
const [hasClickedVideoMeetingSection, setHasClickedVideoMeetingSection] = React.useState(false);
|
const [hasClickedVideoMeetingSection, setHasClickedVideoMeetingSection] =
|
||||||
|
React.useState(false);
|
||||||
|
|
||||||
// Reset hasEndDateChanged and hasClickedDateTimeSection when modal closes
|
// Reset hasEndDateChanged and hasClickedDateTimeSection when modal closes
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -233,10 +238,18 @@ export default function EventFormFields({
|
|||||||
|
|
||||||
// Ref for title input field to enable auto-focus
|
// Ref for title input field to enable auto-focus
|
||||||
const titleInputRef = React.useRef<HTMLInputElement>(null);
|
const titleInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const locationInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// Track previous showMore state to detect changes
|
// Track previous showMore state to detect changes
|
||||||
const prevShowMoreRef = React.useRef<boolean | undefined>(undefined);
|
const prevShowMoreRef = React.useRef<boolean | undefined>(undefined);
|
||||||
|
|
||||||
|
// Focus location field when user clicks the location preview row
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (hasClickedLocationSection && process.env.NODE_ENV !== "test") {
|
||||||
|
locationInputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [hasClickedLocationSection]);
|
||||||
|
|
||||||
// Auto-focus title field when modal opens (skip in test environment)
|
// Auto-focus title field when modal opens (skip in test environment)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
@@ -278,7 +291,6 @@ export default function EventFormFields({
|
|||||||
prevShowMoreRef.current = showMore;
|
prevShowMoreRef.current = showMore;
|
||||||
}, [showMore, isOpen]);
|
}, [showMore, isOpen]);
|
||||||
|
|
||||||
|
|
||||||
// Sync start prop to startDate/startTime
|
// Sync start prop to startDate/startTime
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (start) {
|
if (start) {
|
||||||
@@ -413,9 +425,9 @@ export default function EventFormFields({
|
|||||||
setDescription(updatedDescription);
|
setDescription(updatedDescription);
|
||||||
setHasVideoConference(true);
|
setHasVideoConference(true);
|
||||||
setMeetingLink(newMeetingLink);
|
setMeetingLink(newMeetingLink);
|
||||||
// Open description field when adding video conference
|
if (showMore) {
|
||||||
setShowDescription(true);
|
setShowDescription(true);
|
||||||
// Track click on video meeting section in normal mode
|
}
|
||||||
if (!showMore) {
|
if (!showMore) {
|
||||||
setHasClickedVideoMeetingSection(true);
|
setHasClickedVideoMeetingSection(true);
|
||||||
}
|
}
|
||||||
@@ -472,7 +484,9 @@ export default function EventFormFields({
|
|||||||
|
|
||||||
<FieldWithLabel
|
<FieldWithLabel
|
||||||
label={
|
label={
|
||||||
!showMore && !hasClickedDateTimeSection ? "" : t("event.form.dateTime")
|
!showMore && !hasClickedDateTimeSection
|
||||||
|
? ""
|
||||||
|
: t("event.form.dateTime")
|
||||||
}
|
}
|
||||||
isExpanded={showMore}
|
isExpanded={showMore}
|
||||||
>
|
>
|
||||||
@@ -532,7 +546,8 @@ export default function EventFormFields({
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={
|
checked={
|
||||||
showRepeat || (typeOfAction === "solo" && !!repetition?.freq)
|
showRepeat ||
|
||||||
|
(typeOfAction === "solo" && !!repetition?.freq)
|
||||||
}
|
}
|
||||||
disabled={typeOfAction === "solo"}
|
disabled={typeOfAction === "solo"}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
@@ -570,7 +585,7 @@ export default function EventFormFields({
|
|||||||
timezoneList.getTimezoneOffset(tzName, new Date(start))
|
timezoneList.getTimezoneOffset(tzName, new Date(start))
|
||||||
}
|
}
|
||||||
showIcon={false}
|
showIcon={false}
|
||||||
width={240}
|
width={220}
|
||||||
size="small"
|
size="small"
|
||||||
placeholder={t("event.form.timezonePlaceholder")}
|
placeholder={t("event.form.timezonePlaceholder")}
|
||||||
/>
|
/>
|
||||||
@@ -590,11 +605,14 @@ export default function EventFormFields({
|
|||||||
</FieldWithLabel>
|
</FieldWithLabel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FieldWithLabel label="" isExpanded={showMore}>
|
<FieldWithLabel
|
||||||
|
label={showMore ? t("event.form.participants") : ""}
|
||||||
|
isExpanded={showMore}
|
||||||
|
>
|
||||||
<AttendeeSelector
|
<AttendeeSelector
|
||||||
attendees={attendees}
|
attendees={attendees}
|
||||||
setAttendees={setAttendees}
|
setAttendees={setAttendees}
|
||||||
placeholder="Add guests"
|
placeholder={t("event.form.addGuestsPlaceholder")}
|
||||||
inputSlot={(params) => <TextField {...params} size="small" />}
|
inputSlot={(params) => <TextField {...params} size="small" />}
|
||||||
/>
|
/>
|
||||||
</FieldWithLabel>
|
</FieldWithLabel>
|
||||||
@@ -602,33 +620,53 @@ export default function EventFormFields({
|
|||||||
<FieldWithLabel
|
<FieldWithLabel
|
||||||
label={showMore ? t("event.form.videoMeeting") : ""}
|
label={showMore ? t("event.form.videoMeeting") : ""}
|
||||||
isExpanded={showMore}
|
isExpanded={showMore}
|
||||||
sx={
|
|
||||||
hasClickedVideoMeetingSection && !showMore
|
|
||||||
? {
|
|
||||||
marginTop: "0 !important",
|
|
||||||
"& > .MuiBox-root": {
|
|
||||||
marginTop: "0 !important",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{!showMore ? (
|
{!showMore ? (
|
||||||
<Button
|
hasVideoConference && meetingLink ? (
|
||||||
startIcon={
|
<Box display="flex" gap={1} alignItems="center">
|
||||||
<img src={iconCamera} alt="camera" width={24} height={24} />
|
<Button
|
||||||
}
|
startIcon={
|
||||||
onClick={handleAddVideoConference}
|
<img src={iconCamera} alt="camera" width={24} height={24} />
|
||||||
fullWidth
|
}
|
||||||
sx={{
|
onClick={() =>
|
||||||
justifyContent: "flex-start",
|
window.open(meetingLink, "_blank", "noopener,noreferrer")
|
||||||
display: hasVideoConference ? "none" : "flex",
|
}
|
||||||
borderRadius: "4px",
|
size="medium"
|
||||||
color: alpha(theme.palette.grey[900], 0.9),
|
variant="contained"
|
||||||
}}
|
color="primary"
|
||||||
>
|
sx={{ borderRadius: "4px", mr: 1 }}
|
||||||
{t("event.form.addVisioConference")}
|
>
|
||||||
</Button>
|
{t("event.form.joinVisioConference")}
|
||||||
|
</Button>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleCopyMeetingLink}
|
||||||
|
size="small"
|
||||||
|
sx={{ color: "primary.main" }}
|
||||||
|
aria-label={t("event.form.copyMeetingLink")}
|
||||||
|
title={t("event.form.copyMeetingLink")}
|
||||||
|
>
|
||||||
|
<CopyIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleDeleteVideoConference}
|
||||||
|
size="small"
|
||||||
|
sx={{ color: "error.main" }}
|
||||||
|
aria-label={t("event.form.removeVideoConference")}
|
||||||
|
title={t("event.form.removeVideoConference")}
|
||||||
|
>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<SectionPreviewRow
|
||||||
|
icon={
|
||||||
|
<img src={iconCamera} alt="camera" width={24} height={24} />
|
||||||
|
}
|
||||||
|
onClick={handleAddVideoConference}
|
||||||
|
>
|
||||||
|
{t("event.form.addVisioConference")}
|
||||||
|
</SectionPreviewRow>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<Box display="flex" gap={1} alignItems="center">
|
<Box display="flex" gap={1} alignItems="center">
|
||||||
<Button
|
<Button
|
||||||
@@ -653,7 +691,9 @@ export default function EventFormFields({
|
|||||||
startIcon={
|
startIcon={
|
||||||
<img src={iconCamera} alt="camera" width={24} height={24} />
|
<img src={iconCamera} alt="camera" width={24} height={24} />
|
||||||
}
|
}
|
||||||
onClick={() => window.open(meetingLink, "_blank")}
|
onClick={() =>
|
||||||
|
window.open(meetingLink, "_blank", "noopener,noreferrer")
|
||||||
|
}
|
||||||
size="medium"
|
size="medium"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -694,35 +734,26 @@ export default function EventFormFields({
|
|||||||
showMore={showMore}
|
showMore={showMore}
|
||||||
description={description}
|
description={description}
|
||||||
setDescription={setDescription}
|
setDescription={setDescription}
|
||||||
buttonVariant="contained"
|
|
||||||
buttonColor="secondary"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldWithLabel
|
<FieldWithLabel
|
||||||
label={
|
label={
|
||||||
showMore || hasClickedLocationSection
|
showMore || hasClickedLocationSection ? t("event.form.location") : ""
|
||||||
? t("event.form.location")
|
|
||||||
: ""
|
|
||||||
}
|
}
|
||||||
isExpanded={showMore}
|
isExpanded={showMore}
|
||||||
>
|
>
|
||||||
{!showMore && !hasClickedLocationSection ? (
|
{!showMore && !hasClickedLocationSection ? (
|
||||||
<Button
|
<SectionPreviewRow
|
||||||
startIcon={<LocationIcon />}
|
icon={<LocationIcon />}
|
||||||
onClick={() => setHasClickedLocationSection(true)}
|
onClick={() => setHasClickedLocationSection(true)}
|
||||||
fullWidth
|
|
||||||
sx={{
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
borderRadius: "4px",
|
|
||||||
color: alpha(theme.palette.grey[900], 0.9),
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{location || t("event.form.locationPlaceholder")}
|
{location || t("event.form.locationPlaceholder")}
|
||||||
</Button>
|
</SectionPreviewRow>
|
||||||
) : (
|
) : (
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label=""
|
label=""
|
||||||
|
inputRef={locationInputRef}
|
||||||
inputProps={{ "aria-label": t("event.form.location") }}
|
inputProps={{ "aria-label": t("event.form.location") }}
|
||||||
placeholder={t("event.form.locationPlaceholder")}
|
placeholder={t("event.form.locationPlaceholder")}
|
||||||
value={location}
|
value={location}
|
||||||
@@ -735,15 +766,13 @@ export default function EventFormFields({
|
|||||||
|
|
||||||
<FieldWithLabel
|
<FieldWithLabel
|
||||||
label={
|
label={
|
||||||
showMore || hasClickedCalendarSection
|
showMore || hasClickedCalendarSection ? t("event.form.calendar") : ""
|
||||||
? t("event.form.calendar")
|
|
||||||
: ""
|
|
||||||
}
|
}
|
||||||
isExpanded={showMore}
|
isExpanded={showMore}
|
||||||
>
|
>
|
||||||
{!showMore && !hasClickedCalendarSection ? (
|
{!showMore && !hasClickedCalendarSection ? (
|
||||||
<Button
|
<SectionPreviewRow
|
||||||
startIcon={
|
icon={
|
||||||
<SquareRoundedIcon
|
<SquareRoundedIcon
|
||||||
sx={{
|
sx={{
|
||||||
color:
|
color:
|
||||||
@@ -755,16 +784,10 @@ export default function EventFormFields({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onClick={() => setHasClickedCalendarSection(true)}
|
onClick={() => setHasClickedCalendarSection(true)}
|
||||||
fullWidth
|
|
||||||
sx={{
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
borderRadius: "4px",
|
|
||||||
color: alpha(theme.palette.grey[900], 0.9),
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{userPersonalCalendars.find((cal) => cal.id === calendarid)?.name ||
|
{userPersonalCalendars.find((cal) => cal.id === calendarid)?.name ||
|
||||||
t("event.form.calendar")}
|
t("event.form.calendar")}
|
||||||
</Button>
|
</SectionPreviewRow>
|
||||||
) : (
|
) : (
|
||||||
<FormControl fullWidth margin="dense" size="small">
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Box, Typography, useTheme } from "@linagora/twake-mui";
|
||||||
|
import { alpha } from "@mui/material/styles";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface ClickableFieldProps {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
text?: string;
|
||||||
|
onClick: () => void;
|
||||||
|
iconColor?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ClickableField: React.FC<ClickableFieldProps> = ({
|
||||||
|
icon,
|
||||||
|
text,
|
||||||
|
onClick,
|
||||||
|
iconColor,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
onClick={onClick}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: children ? "flex-start" : "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: "8px 12px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "action.hover",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
maxWidth: "24px",
|
||||||
|
maxHeight: "24px",
|
||||||
|
marginRight: "12px",
|
||||||
|
color: iconColor || alpha(theme.palette.grey[900], 0.9),
|
||||||
|
"& svg": {
|
||||||
|
width: "24px",
|
||||||
|
height: "24px",
|
||||||
|
},
|
||||||
|
"& img": {
|
||||||
|
width: "24px",
|
||||||
|
height: "24px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</Box>
|
||||||
|
{children ? (
|
||||||
|
<Box flex={1}>{children}</Box>
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: alpha(theme.palette.grey[900], 0.9),
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -37,6 +37,15 @@ const timePickerPopperSx = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// twake-mui datePickerOverrides also uses this selector. Repeating ensures our override wins.
|
||||||
|
const dateCalendarLayoutSx = {
|
||||||
|
"& .MuiDateCalendar-root.MuiDateCalendar-root": {
|
||||||
|
width: "260px",
|
||||||
|
maxWidth: "260px",
|
||||||
|
padding: "0 15px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for DateTimeFields component
|
* Props for DateTimeFields component
|
||||||
*/
|
*/
|
||||||
@@ -369,6 +378,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
false,
|
false,
|
||||||
t("dateTimeFields.startDate")
|
t("dateTimeFields.startDate")
|
||||||
),
|
),
|
||||||
|
layout: { sx: dateCalendarLayoutSx },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -415,6 +425,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
!!validation.errors.dateTime,
|
!!validation.errors.dateTime,
|
||||||
t("dateTimeFields.endDate")
|
t("dateTimeFields.endDate")
|
||||||
),
|
),
|
||||||
|
layout: { sx: dateCalendarLayoutSx },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -463,6 +474,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
false,
|
false,
|
||||||
t("dateTimeFields.startDate")
|
t("dateTimeFields.startDate")
|
||||||
),
|
),
|
||||||
|
layout: { sx: dateCalendarLayoutSx },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -483,6 +495,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
!!validation.errors.dateTime,
|
!!validation.errors.dateTime,
|
||||||
t("dateTimeFields.endDate")
|
t("dateTimeFields.endDate")
|
||||||
),
|
),
|
||||||
|
layout: { sx: dateCalendarLayoutSx },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -502,6 +515,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
false,
|
false,
|
||||||
startDateLabel
|
startDateLabel
|
||||||
),
|
),
|
||||||
|
layout: { sx: dateCalendarLayoutSx },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ import { Box, Typography, useTheme } from "@linagora/twake-mui";
|
|||||||
import { alpha } from "@mui/material/styles";
|
import { alpha } from "@mui/material/styles";
|
||||||
import AccessTimeIcon from "@mui/icons-material/AccessTime";
|
import AccessTimeIcon from "@mui/icons-material/AccessTime";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import "dayjs/locale/en";
|
||||||
|
import "dayjs/locale/fr";
|
||||||
|
import "dayjs/locale/ru";
|
||||||
|
import "dayjs/locale/vi";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useI18n } from "twake-i18n";
|
import { useI18n } from "twake-i18n";
|
||||||
import { getTimezoneOffset } from "../../Calendar/TimezoneSelector";
|
import { getTimezoneOffset } from "@/utils/timezone";
|
||||||
import { RepetitionObject } from "@/features/Events/EventsTypes";
|
import { RepetitionObject } from "@/features/Events/EventsTypes";
|
||||||
import { LONG_DATE_FORMAT } from "../utils/dateTimeFormatters";
|
import { LONG_DATE_FORMAT } from "../utils/dateTimeFormatters";
|
||||||
import { toDateTime } from "../utils/dateTimeHelpers";
|
import { SectionPreviewRow } from "./SectionPreviewRow";
|
||||||
|
|
||||||
interface DateTimeSummaryProps {
|
interface DateTimeSummaryProps {
|
||||||
startDate: string;
|
startDate: string;
|
||||||
@@ -35,28 +39,45 @@ export const DateTimeSummary: React.FC<DateTimeSummaryProps> = ({
|
|||||||
const { t, lang } = useI18n();
|
const { t, lang } = useI18n();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
// Format date: "Wednesday, January 28, 2026"
|
// Format date with current locale. VI: "Thứ 4, 4 Tháng 2, 2026"; FR: "mercredi, 5 février 2026"; RU: first letter capitalized
|
||||||
const formatDate = (dateStr: string): string => {
|
const formatDate = (dateStr: string): string => {
|
||||||
if (!dateStr) return "";
|
if (!dateStr) return "";
|
||||||
const date = dayjs(dateStr);
|
const date = dayjs(dateStr);
|
||||||
return date.format(LONG_DATE_FORMAT);
|
const locale =
|
||||||
|
lang && ["en", "vi", "fr", "ru"].includes(lang) ? lang : "en";
|
||||||
|
|
||||||
|
if (locale === "vi") {
|
||||||
|
const dow = date.day(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
|
||||||
|
const weekdayLabel = dow === 0 ? "Chủ nhật" : `Thứ ${dow + 1}`; // Mon=Thứ 2, Wed=Thứ 4, ...
|
||||||
|
const day = date.date();
|
||||||
|
const month = date.month() + 1;
|
||||||
|
const year = date.year();
|
||||||
|
return `${weekdayLabel}, ${day} Tháng ${month}, ${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// French: "5 février" (day before month), not "février 5"
|
||||||
|
if (locale === "fr") {
|
||||||
|
const formatted = date.locale("fr").format("dddd, D MMMM YYYY");
|
||||||
|
return formatted.charAt(0).toUpperCase() + formatted.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = date.locale(locale).format(LONG_DATE_FORMAT);
|
||||||
|
if (locale === "ru") {
|
||||||
|
return formatted.charAt(0).toUpperCase() + formatted.slice(1);
|
||||||
|
}
|
||||||
|
return formatted;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format time: "1:00pm - 2:00pm"
|
// Format time in 24h: "03:30 - 16:30"
|
||||||
const formatTime = (startTimeStr: string, endTimeStr: string): string => {
|
const formatTime = (startTimeStr: string, endTimeStr: string): string => {
|
||||||
if (allday || !startTimeStr || !endTimeStr) return "";
|
if (allday || !startTimeStr || !endTimeStr) return "";
|
||||||
|
|
||||||
const format12Hour = (timeStr: string): string => {
|
const toHHmm = (timeStr: string): string => {
|
||||||
const [hours, minutes] = timeStr.split(":").map(Number);
|
const [h, m] = timeStr.split(":").map((s) => parseInt(s, 10) || 0);
|
||||||
const hour12 = hours % 12 || 12;
|
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
|
||||||
const ampm = hours >= 12 ? "pm" : "am";
|
|
||||||
const mins = minutes === 0 ? "" : `:${minutes.toString().padStart(2, "0")}`;
|
|
||||||
return `${hour12}${mins}${ampm}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const start = format12Hour(startTimeStr);
|
return `${toHHmm(startTimeStr)} - ${toHHmm(endTimeStr)}`;
|
||||||
const end = format12Hour(endTimeStr);
|
|
||||||
return `${start} - ${end}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format timezone: "(UTC+2) Paris"
|
// Format timezone: "(UTC+2) Paris"
|
||||||
@@ -74,7 +95,7 @@ export const DateTimeSummary: React.FC<DateTimeSummaryProps> = ({
|
|||||||
// Format repeat: "Doesn't repeat" or repeat info
|
// Format repeat: "Doesn't repeat" or repeat info
|
||||||
const formatRepeat = (rep: RepetitionObject): string => {
|
const formatRepeat = (rep: RepetitionObject): string => {
|
||||||
if (!rep || !rep.freq) {
|
if (!rep || !rep.freq) {
|
||||||
return "Doesn't repeat";
|
return t("event.repeat.doesNotRepeat");
|
||||||
}
|
}
|
||||||
|
|
||||||
const interval = rep.interval || 1;
|
const interval = rep.interval || 1;
|
||||||
@@ -86,12 +107,12 @@ export const DateTimeSummary: React.FC<DateTimeSummaryProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const freqText = freqMap[rep.freq] || rep.freq;
|
const freqText = freqMap[rep.freq] || rep.freq;
|
||||||
|
|
||||||
if (interval === 1) {
|
if (interval === 1) {
|
||||||
return `Every ${freqText}`;
|
return `${t("event.repeat.every")} ${freqText}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `Every ${interval} ${freqText}`;
|
return `${t("event.repeat.every")} ${interval} ${freqText}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format date text: show both start and end date if showEndDate is true
|
// Format date text: show both start and end date if showEndDate is true
|
||||||
@@ -114,40 +135,28 @@ export const DateTimeSummary: React.FC<DateTimeSummaryProps> = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const primaryStyle = {
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: 500,
|
||||||
|
color: alpha(theme.palette.grey[900], 0.9),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<SectionPreviewRow
|
||||||
|
icon={<AccessTimeIcon sx={{ color: "text.secondary" }} />}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
gap: 1.5,
|
|
||||||
cursor: "pointer",
|
|
||||||
padding: 1,
|
|
||||||
borderRadius: 1,
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: "action.hover",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<AccessTimeIcon sx={{ color: "text.secondary", mt: 0.5 }} />
|
<Box>
|
||||||
<Box flex={1}>
|
<Typography component="p" sx={primaryStyle}>
|
||||||
<Box display="flex" gap={2} alignItems="center" mb={0.5}>
|
{dateText}
|
||||||
<Typography
|
{showEndDate && <br />}
|
||||||
variant="body2"
|
|
||||||
sx={{ color: alpha(theme.palette.grey[900], 0.9), fontWeight: 500 }}
|
|
||||||
>
|
|
||||||
{dateText}
|
|
||||||
</Typography>
|
|
||||||
{timeText && (
|
{timeText && (
|
||||||
<Typography
|
<Box component="span" sx={{ ml: showEndDate ? 0 : 2 }}>
|
||||||
variant="body2"
|
|
||||||
sx={{ color: alpha(theme.palette.grey[900], 0.9), fontWeight: 500 }}
|
|
||||||
>
|
|
||||||
{timeText}
|
{timeText}
|
||||||
</Typography>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Typography>
|
||||||
<Box display="flex" gap={2} alignItems="center">
|
<Box display="flex" gap={2} alignItems="center" mt={0.5}>
|
||||||
<Typography variant="caption" sx={{ color: "#444746" }}>
|
<Typography variant="caption" sx={{ color: "#444746" }}>
|
||||||
{timezoneText}
|
{timezoneText}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -156,6 +165,6 @@ export const DateTimeSummary: React.FC<DateTimeSummaryProps> = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</SectionPreviewRow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { Box, Typography, useTheme } from "@linagora/twake-mui";
|
||||||
|
import { alpha } from "@mui/material/styles";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export interface SectionPreviewRowProps {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
/** Primary text or custom content. If string, rendered with fontSize 14px, fontWeight 500. */
|
||||||
|
children: React.ReactNode;
|
||||||
|
onClick: () => void;
|
||||||
|
iconColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reusable preview row for event modal sections (normal mode).
|
||||||
|
* Layout: icon left (max 32x32), content right. No button.
|
||||||
|
*/
|
||||||
|
export const SectionPreviewRow: React.FC<SectionPreviewRowProps> = ({
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
iconColor,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const color = iconColor ?? alpha(theme.palette.grey[900], 0.9);
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={onClick}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: "8px 12px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "action.hover",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
maxWidth: "24px",
|
||||||
|
maxHeight: "24px",
|
||||||
|
marginRight: "12px",
|
||||||
|
flexShrink: 0,
|
||||||
|
color,
|
||||||
|
"& svg": {
|
||||||
|
maxWidth: "24px",
|
||||||
|
maxHeight: "24px",
|
||||||
|
},
|
||||||
|
"& img": {
|
||||||
|
maxWidth: "24px",
|
||||||
|
maxHeight: "24px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</Box>
|
||||||
|
<Box flex={1} minWidth={0}>
|
||||||
|
{typeof children === "string" ? (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: 500,
|
||||||
|
color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -860,6 +860,7 @@ function formatDate(
|
|||||||
day: "numeric",
|
day: "numeric",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
timeZone,
|
timeZone,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -896,6 +897,7 @@ function formatEnd(
|
|||||||
return endDate.toLocaleTimeString(t("locale"), {
|
return endDate.toLocaleTimeString(t("locale"), {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
timeZone,
|
timeZone,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -905,6 +907,7 @@ function formatEnd(
|
|||||||
day: "numeric",
|
day: "numeric",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
timeZone,
|
timeZone,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,8 @@
|
|||||||
"years": "Year(s)"
|
"years": "Year(s)"
|
||||||
},
|
},
|
||||||
"repeatOn": "Repeat on:",
|
"repeatOn": "Repeat on:",
|
||||||
|
"doesNotRepeat": "Doesn't repeat",
|
||||||
|
"every": "Every",
|
||||||
"days": {
|
"days": {
|
||||||
"monday": "MO",
|
"monday": "MO",
|
||||||
"tuesday": "TU",
|
"tuesday": "TU",
|
||||||
@@ -127,6 +129,7 @@
|
|||||||
"allDay": "All day",
|
"allDay": "All day",
|
||||||
"repeat": "Repeat",
|
"repeat": "Repeat",
|
||||||
"timezonePlaceholder": "Select timezone",
|
"timezonePlaceholder": "Select timezone",
|
||||||
|
"addGuestsPlaceholder": "Add guests",
|
||||||
"participants": "Participants",
|
"participants": "Participants",
|
||||||
"videoMeeting": "Video meeting",
|
"videoMeeting": "Video meeting",
|
||||||
"meetCopied": "Meeting link copied!",
|
"meetCopied": "Meeting link copied!",
|
||||||
|
|||||||
+4
-1
@@ -100,6 +100,8 @@
|
|||||||
"years": "année(s)"
|
"years": "année(s)"
|
||||||
},
|
},
|
||||||
"repeatOn": "Répéter le :",
|
"repeatOn": "Répéter le :",
|
||||||
|
"doesNotRepeat": "Ne se répète pas",
|
||||||
|
"every": "Tous les",
|
||||||
"days": {
|
"days": {
|
||||||
"monday": "LU",
|
"monday": "LU",
|
||||||
"tuesday": "MA",
|
"tuesday": "MA",
|
||||||
@@ -129,6 +131,7 @@
|
|||||||
"allDay": "Toute la journée",
|
"allDay": "Toute la journée",
|
||||||
"repeat": "Répéter",
|
"repeat": "Répéter",
|
||||||
"timezonePlaceholder": "Sélectionner un fuseau horaire",
|
"timezonePlaceholder": "Sélectionner un fuseau horaire",
|
||||||
|
"addGuestsPlaceholder": "Ajouter des invités",
|
||||||
"participants": "Participants",
|
"participants": "Participants",
|
||||||
"videoMeeting": "Visioconférence",
|
"videoMeeting": "Visioconférence",
|
||||||
"meetCopied": "Lien de la visio copié!",
|
"meetCopied": "Lien de la visio copié!",
|
||||||
@@ -288,7 +291,7 @@
|
|||||||
},
|
},
|
||||||
"dateTimeFields": {
|
"dateTimeFields": {
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"startDate": "Daye du début",
|
"startDate": "Date du début",
|
||||||
"startTime": "Heure du début",
|
"startTime": "Heure du début",
|
||||||
"endDate": "Date de la fin",
|
"endDate": "Date de la fin",
|
||||||
"endTime": "Heure de la fin"
|
"endTime": "Heure de la fin"
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"years": "год(лет)"
|
"years": "год(лет)"
|
||||||
},
|
},
|
||||||
"repeatOn": "Повторять по:",
|
"repeatOn": "Повторять по:",
|
||||||
|
"doesNotRepeat": "Не повторяется",
|
||||||
|
"every": "Каждый",
|
||||||
"days": {
|
"days": {
|
||||||
"monday": "ПН",
|
"monday": "ПН",
|
||||||
"tuesday": "ВТ",
|
"tuesday": "ВТ",
|
||||||
@@ -129,6 +131,7 @@
|
|||||||
"allDay": "Весь день",
|
"allDay": "Весь день",
|
||||||
"repeat": "Повторять",
|
"repeat": "Повторять",
|
||||||
"timezonePlaceholder": "Выберите часовой пояс",
|
"timezonePlaceholder": "Выберите часовой пояс",
|
||||||
|
"addGuestsPlaceholder": "Добавить гостей",
|
||||||
"participants": "Участники",
|
"participants": "Участники",
|
||||||
"videoMeeting": "Видеовстреча",
|
"videoMeeting": "Видеовстреча",
|
||||||
"meetCopied": "Ссылка на встречу скопирована",
|
"meetCopied": "Ссылка на встречу скопирована",
|
||||||
|
|||||||
@@ -98,6 +98,8 @@
|
|||||||
"years": "Năm"
|
"years": "Năm"
|
||||||
},
|
},
|
||||||
"repeatOn": "Lặp vào:",
|
"repeatOn": "Lặp vào:",
|
||||||
|
"doesNotRepeat": "Không lặp lại",
|
||||||
|
"every": "Mỗi",
|
||||||
"days": {
|
"days": {
|
||||||
"monday": "Th 2",
|
"monday": "Th 2",
|
||||||
"tuesday": "Th 3",
|
"tuesday": "Th 3",
|
||||||
@@ -127,6 +129,7 @@
|
|||||||
"allDay": "Cả ngày",
|
"allDay": "Cả ngày",
|
||||||
"repeat": "Lặp lại",
|
"repeat": "Lặp lại",
|
||||||
"timezonePlaceholder": "Chọn múi giờ",
|
"timezonePlaceholder": "Chọn múi giờ",
|
||||||
|
"addGuestsPlaceholder": "Thêm khách",
|
||||||
"participants": "Người tham gia",
|
"participants": "Người tham gia",
|
||||||
"videoMeeting": "Cuộc họp video",
|
"videoMeeting": "Cuộc họp video",
|
||||||
"meetCopied": "Đã sao chép liên kết họp!",
|
"meetCopied": "Đã sao chép liên kết họp!",
|
||||||
|
|||||||
Reference in New Issue
Block a user