Use @linagora/twake-mui 1.1.6 from registry

This commit is contained in:
lenhanphung
2026-01-28 18:49:39 +07:00
parent da0dba423e
commit 201f9c722b
19 changed files with 476 additions and 2748 deletions
@@ -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 () => {
+11 -6
View File
@@ -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();
}); });
+34 -7
View File
@@ -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");
+21 -2537
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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",
-2
View File
@@ -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>
+56 -75
View File
@@ -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}
</> </>
); );
} }
+92 -69
View File
@@ -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,
}); });
} }
+3
View File
@@ -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
View File
@@ -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"
+3
View File
@@ -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": "Ссылка на встречу скопирована",
+3
View File
@@ -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!",