diff --git a/__test__/components/EventModifications.test.tsx b/__test__/components/EventModifications.test.tsx
index d36bdff..1851c5a 100644
--- a/__test__/components/EventModifications.test.tsx
+++ b/__test__/components/EventModifications.test.tsx
@@ -400,6 +400,10 @@ describe("CalendarApp integration", () => {
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");
await act(async () => {
diff --git a/__test__/components/Menubar.test.tsx b/__test__/components/Menubar.test.tsx
index e95bf60..2e39e87 100644
--- a/__test__/components/Menubar.test.tsx
+++ b/__test__/components/Menubar.test.tsx
@@ -1,9 +1,14 @@
import { Menubar } from "@/components/Menubar/Menubar";
import * as oidcAuth from "@/features/User/oidcAuth";
+import { redirectTo } from "@/utils/navigation";
import "@testing-library/jest-dom";
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
import { renderWithProviders } from "../utils/Renderwithproviders";
+jest.mock("@/utils/navigation", () => ({
+ redirectTo: jest.fn(),
+}));
+
describe("Calendar App Component Display Tests", () => {
const preloadedState = {
user: {
@@ -320,7 +325,7 @@ describe("Calendar App Component Display Tests", () => {
expect(avatar).toBeInTheDocument();
});
- it("shows AppsIcon when applist is not empty", () => {
+ it("shows apps button when applist is not empty", () => {
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
@@ -335,10 +340,10 @@ describe("Calendar App Component Display Tests", () => {
/>,
preloadedState
);
- expect(screen.getByTestId("AppsIcon")).toBeInTheDocument();
+ expect(screen.getByLabelText("menubar.apps")).toBeInTheDocument();
});
- it("opens popover when clicking AppsIcon", () => {
+ it("opens popover when clicking apps button", () => {
(window as any).appList = [
{ name: "Twake", icon: "twake.svg", link: "/twake" },
{ name: "Calendar", icon: "calendar.svg", link: "/calendar" },
@@ -356,7 +361,7 @@ describe("Calendar App Component Display Tests", () => {
/>,
preloadedState
);
- const appsButton = screen.getByTestId("AppsIcon");
+ const appsButton = screen.getByLabelText("menubar.apps");
fireEvent.click(appsButton);
expect(screen.getByText("Twake")).toBeInTheDocument();
expect(screen.getByText("Calendar")).toBeInTheDocument();
@@ -377,7 +382,7 @@ describe("Calendar App Component Display Tests", () => {
/>,
preloadedState
);
- const appsButton = screen.getByTestId("AppsIcon");
+ const appsButton = screen.getByLabelText("menubar.apps");
fireEvent.click(appsButton);
const testLink = screen.getByRole("link", { name: /test/i });
@@ -532,7 +537,7 @@ describe("Menubar interaction with expanded Dialog", () => {
preloadedState
);
- const appsButton = screen.getByTestId("AppsIcon");
+ const appsButton = screen.getByLabelText("menubar.apps");
expect(appsButton).toBeVisible();
fireEvent.click(appsButton);
@@ -717,10 +722,17 @@ describe("Menubar interaction with expanded Dialog", () => {
});
describe("Menubar logout flow", () => {
+ const redirectToMock = redirectTo as jest.Mock;
+
beforeEach(() => {
localStorage.clear();
sessionStorage.clear();
jest.clearAllMocks();
+ redirectToMock.mockReset();
+ });
+
+ afterEach(() => {
+ redirectToMock.mockReset();
});
function renderMenubar(user = { name: "John", email: "test@example.com" }) {
@@ -756,6 +768,7 @@ describe("Menubar logout flow", () => {
fireEvent.click(screen.getByText("menubar.logout"));
});
expect(logoutSpy).toHaveBeenCalled();
+ expect(redirectToMock).toHaveBeenCalledWith("https://logout.url/");
expect(sessionStorage.length).toBe(0);
});
diff --git a/__test__/components/ResponsiveDialog.test.tsx b/__test__/components/ResponsiveDialog.test.tsx
index 14da4ff..f0e0d2e 100644
--- a/__test__/components/ResponsiveDialog.test.tsx
+++ b/__test__/components/ResponsiveDialog.test.tsx
@@ -248,7 +248,7 @@ describe("ResponsiveDialog", () => {
onClose={mockOnClose}
title="Test"
isExpanded={true}
- headerHeight="100px"
+ headerHeight="70px"
>
Custom Header Content
diff --git a/__test__/features/Events/EventDisplay.test.tsx b/__test__/features/Events/EventDisplay.test.tsx
index 7dc3a6d..8ae1e48 100644
--- a/__test__/features/Events/EventDisplay.test.tsx
+++ b/__test__/features/Events/EventDisplay.test.tsx
@@ -164,12 +164,12 @@ describe("Event Preview Display", () => {
expect(screen.getByText(/January/i)).toBeInTheDocument();
expect(screen.getByText(/15/)).toBeInTheDocument();
- // Check time is displayed with exact values
- // Format: "Wednesday, January 15, 2025 at 10:00 AM" and " – 10:00 AM" are in separate elements
+ // Check time is displayed in 24h format (no AM/PM)
+ // Format: "Wednesday, January 15, 2025 at 10:00" and " – 10:00" are in separate elements
expect(
- screen.getByText(/Wednesday, January 15, 2025 at 10:00 AM/)
+ screen.getByText(/Wednesday, January 15, 2025 at 10:00/)
).toBeInTheDocument();
- expect(screen.getByText(/– 10:00 AM/)).toBeInTheDocument();
+ expect(screen.getByText(/– 10:00/)).toBeInTheDocument();
expect(screen.getByText("Calendar")).toBeInTheDocument();
});
@@ -1719,6 +1719,9 @@ describe("Event Full Display", () => {
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 startTimeInput = screen.getByTestId("start-time-input");
const endTimeInput = screen.getByTestId("end-time-input");
@@ -1804,6 +1807,7 @@ describe("Event Full Display", () => {
preloadedState
);
+ fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
fireEvent.click(allDayCheckbox);
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");
await act(async () => fireEvent.mouseDown(calendarSelect));
@@ -1947,9 +1952,9 @@ describe("Event Full Display", () => {
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
- // 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);
expect(timeZone).toBeInTheDocument();
});
diff --git a/__test__/features/Events/EventModal.test.tsx b/__test__/features/Events/EventModal.test.tsx
index 8ae1037..51094ba 100644
--- a/__test__/features/Events/EventModal.test.tsx
+++ b/__test__/features/Events/EventModal.test.tsx
@@ -124,8 +124,10 @@ describe("EventPopover", () => {
});
expect(addDescriptionButton).toBeInTheDocument();
- const calendarSelect = screen.getByRole("combobox", { name: /calendar/i });
- expect(calendarSelect).toBeInTheDocument();
+ // In normal mode calendar is a SectionPreviewRow (button), not combobox
+ expect(
+ screen.getByRole("button", { name: "Calendar 1" })
+ ).toBeInTheDocument();
// Check button
expect(
@@ -154,7 +156,8 @@ describe("EventPopover", () => {
allDay: false,
} 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-time-input")).toBeInTheDocument();
});
@@ -179,6 +182,10 @@ describe("EventPopover", () => {
"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"), {
target: { value: "Conference Room" },
});
@@ -190,13 +197,14 @@ describe("EventPopover", () => {
it("changes selected calendar", async () => {
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");
fireEvent.mouseDown(select); // Open menu
const option = await screen.findByText("Calendar 2");
fireEvent.click(option);
- // Find the calendar combobox specifically by its aria-labelledby
const calendarSelect = screen.getByRole("combobox", { name: /Calendar/i });
expect(calendarSelect).toHaveTextContent("Calendar 2");
});
@@ -290,6 +298,9 @@ describe("EventPopover", () => {
fireEvent.change(screen.getByLabelText("event.form.description"), {
target: { value: newEvent.description },
});
+ fireEvent.click(
+ screen.getByRole("button", { name: "event.form.locationPlaceholder" })
+ );
fireEvent.change(screen.getByLabelText("event.form.location"), {
target: { value: newEvent.location },
});
@@ -338,10 +349,11 @@ describe("EventPopover", () => {
it("BUGFIX: Prefill Calendar field", async () => {
renderPopover();
+ // In normal mode calendar is a SectionPreviewRow (button) with calendar name
await waitFor(() =>
- expect(screen.getByLabelText("event.form.calendar")).toHaveTextContent(
- "Calendar 1"
- )
+ expect(
+ screen.getByRole("button", { name: "Calendar 1" })
+ ).toHaveTextContent("Calendar 1")
);
});
@@ -357,6 +369,9 @@ describe("EventPopover", () => {
} as unknown as DateSelectArg;
renderPopover(selectedRange);
+ fireEvent.click(
+ screen.getByRole("button", { name: "common.moreOptions" })
+ );
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
await waitFor(() => {
@@ -375,6 +390,9 @@ describe("EventPopover", () => {
} as unknown as DateSelectArg;
renderPopover(selectedRange);
+ fireEvent.click(
+ screen.getByRole("button", { name: "common.moreOptions" })
+ );
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
await waitFor(() => {
@@ -403,6 +421,9 @@ describe("EventPopover", () => {
} as unknown as DateSelectArg;
renderPopover(selectedRange);
+ fireEvent.click(
+ screen.getByRole("button", { name: "common.moreOptions" })
+ );
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
await waitFor(() => {
@@ -427,6 +448,9 @@ describe("EventPopover", () => {
} as unknown as DateSelectArg;
renderPopover(selectedRange);
+ fireEvent.click(
+ screen.getByRole("button", { name: "common.moreOptions" })
+ );
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
await waitFor(() => {
@@ -451,6 +475,9 @@ describe("EventPopover", () => {
} as unknown as DateSelectArg;
renderPopover(selectedRange);
+ fireEvent.click(
+ screen.getByRole("button", { name: "common.moreOptions" })
+ );
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
await waitFor(() => {
diff --git a/__test__/features/Events/EventRepetition.test.tsx b/__test__/features/Events/EventRepetition.test.tsx
index 727cefc..0e500dd 100644
--- a/__test__/features/Events/EventRepetition.test.tsx
+++ b/__test__/features/Events/EventRepetition.test.tsx
@@ -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"));
const option = await screen.findByText("Calendar 2");
fireEvent.click(option);
diff --git a/__test__/features/Events/EventUpdateModal.recurring.test.tsx b/__test__/features/Events/EventUpdateModal.recurring.test.tsx
index a33ef31..5454fe0 100644
--- a/__test__/features/Events/EventUpdateModal.recurring.test.tsx
+++ b/__test__/features/Events/EventUpdateModal.recurring.test.tsx
@@ -129,6 +129,10 @@ describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
screen.queryByDisplayValue("Modified Instance Title")
).not.toBeInTheDocument();
+ // Expand more options to show timezone (normal mode shows summary first)
+ fireEvent.click(
+ screen.getByRole("button", { name: "common.moreOptions" })
+ );
// Verify timezone dropdown shows master timezone
await waitFor(() => {
expect(screen.getByDisplayValue(/New York/i)).toBeDefined();
@@ -493,8 +497,12 @@ describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
expect(screen.getByDisplayValue("Daily Standup")).toBeInTheDocument();
});
+ // Expand more options to show date/time inputs (normal mode shows DateTimeSummary first)
+ fireEvent.click(
+ screen.getByRole("button", { name: "common.moreOptions" })
+ );
+ const input = await waitFor(() => screen.getByTestId("start-time-input"));
// Change time to 2:00 PM (14:00)
- const input = screen.getByTestId("start-time-input");
await userEvent.click(input);
await userEvent.clear(input);
await userEvent.type(input, "14:00{enter}");
diff --git a/__test__/features/Events/EventUpdateModal.test.tsx b/__test__/features/Events/EventUpdateModal.test.tsx
index 502fdfc..f7340cd 100644
--- a/__test__/features/Events/EventUpdateModal.test.tsx
+++ b/__test__/features/Events/EventUpdateModal.test.tsx
@@ -93,6 +93,9 @@ describe("EventUpdateModal Timezone Handling", () => {
const titleInput = screen.getByDisplayValue("Timezone Event");
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
await waitFor(() => {
const startDateInput = screen.getByTestId("start-date-input");
@@ -282,6 +285,7 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
expect(mockGetMasterEvent).toHaveBeenCalled();
});
+ fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
// Wait for the repeat checkbox to be checked (indicating form is initialized)
const repeatCheckbox = await waitFor(() => {
const checkbox = screen.getByLabelText("event.form.repeat");
@@ -414,6 +418,7 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
expect(screen.getByDisplayValue("Recurring Meeting")).toBeInTheDocument();
});
+ fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
// Wait for repeat checkbox to be checked
const repeatCheckbox = await waitFor(() => {
const checkbox = screen.getByLabelText("event.form.repeat");
@@ -546,6 +551,7 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
expect(screen.getByDisplayValue("Recurring Meeting")).toBeInTheDocument();
});
+ fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
// Uncheck repeat and save
const repeatCheckbox = screen.getByLabelText("event.form.repeat");
diff --git a/package-lock.json b/package-lock.json
index f06f5bb..7583aac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,7 @@
"@fullcalendar/moment-timezone": "^6.1.19",
"@fullcalendar/react": "^6.1.17",
"@fullcalendar/timegrid": "^6.1.17",
- "@linagora/twake-mui": "1.1.4",
+ "@linagora/twake-mui": "1.1.8",
"@lottiefiles/dotlottie-react": "^0.17.13",
"@mui/icons-material": "^7.1.2",
"@mui/material": "^7.1.2",
@@ -3136,9 +3136,10 @@
}
},
"node_modules/@linagora/twake-mui": {
- "version": "1.1.4",
- "resolved": "https://npm.pkg.github.com/download/@linagora/twake-mui/1.1.4/56321d9e10cd99550e751f15c34aaac6842442c1",
- "integrity": "sha512-L6e+6KgqFUxFY6avmk4UG4/bsdfzo6TxBkahUZlKxvCNEEikVe8z2Cxd0JtUUAWrPGZonTII9n2KhdFSNmNxvg==",
+ "version": "1.1.8",
+ "resolved": "https://npm.pkg.github.com/download/@linagora/twake-mui/1.1.8/e629a126e62dffd939a8d3d8de3ce024de5a2154",
+ "integrity": "sha512-H2MxtZIy5w1A8VNTNAoM4Pjuph/et4YP8DfzQ+5HCsjRJn6SA/T3nZRZOqC26SxY9kl3XJCKIEKklL+x5af7Vg==",
+ "license": "MIT",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
diff --git a/package.json b/package.json
index 2ff1e1e..416d7d8 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
"@fullcalendar/moment-timezone": "^6.1.19",
"@fullcalendar/react": "^6.1.17",
"@fullcalendar/timegrid": "^6.1.17",
- "@linagora/twake-mui": "1.1.4",
+ "@linagora/twake-mui": "1.1.8",
"@lottiefiles/dotlottie-react": "^0.17.13",
"@mui/icons-material": "^7.1.2",
"@mui/material": "^7.1.2",
diff --git a/src/components/Attendees/AttendeeSearch.tsx b/src/components/Attendees/AttendeeSearch.tsx
index 647ce7c..5324bea 100644
--- a/src/components/Attendees/AttendeeSearch.tsx
+++ b/src/components/Attendees/AttendeeSearch.tsx
@@ -12,6 +12,7 @@ export default function UserSearch({
setAttendees,
disabled,
inputSlot,
+ placeholder,
}: {
attendees: userAttendee[];
setAttendees: Function;
@@ -19,8 +20,9 @@ export default function UserSearch({
inputSlot?: (
params: ExtendedAutocompleteRenderInputParams
) => React.ReactNode;
+ placeholder?: string;
}) {
- const [selectedUsers, setSelectedUsers] = useState(
+ const [selectedUsers, setSelectedUsers] = useState(
attendees.map((attendee) => ({
email: attendee.cal_address,
displayName: attendee.cn ?? "",
@@ -44,6 +46,7 @@ export default function UserSearch({
objectTypes={["user", "contact"]}
disabled={disabled}
inputSlot={inputSlot}
+ placeholder={placeholder}
onChange={(event: any, value: User[]) => {
setAttendees(
value.map((attendee: User) =>
diff --git a/src/components/Calendar/Calendar.styl b/src/components/Calendar/Calendar.styl
index fe3e3bf..238a4fe 100644
--- a/src/components/Calendar/Calendar.styl
+++ b/src/components/Calendar/Calendar.styl
@@ -6,7 +6,7 @@
.main-layout
display flex
- height calc(100vh - 90px)
+ height calc(100vh - 70px)
.main-layout.calendar-layout
background-color #fff
@@ -117,7 +117,7 @@
margin-top 2px
width 6px
height 6px
- background-color #d93025
+ background-color #1D192B1F
border-radius 50%
margin-left auto
margin-right auto
diff --git a/src/components/Calendar/CustomCalendar.styl b/src/components/Calendar/CustomCalendar.styl
index 633fe5b..6165078 100644
--- a/src/components/Calendar/CustomCalendar.styl
+++ b/src/components/Calendar/CustomCalendar.styl
@@ -73,7 +73,7 @@ span.fc-daygrid-day-number.current-date
color #fff
span.fc-daygrid-day-number.current-date:after
- background-color #f67e35
+ background-color #FB9E3A
td.fc-timegrid-slot.fc-timegrid-slot-label.fc-scrollgrid-shrink,
td.fc-timegrid-slot.fc-timegrid-slot-label.fc-timegrid-slot-minor
@@ -244,7 +244,7 @@ tbody > tr.fc-scrollgrid-section:first-of-type .fc-scroller
overflow hidden !important
.navigation-controls
- margin-right 40px
+ margin-right 16px
.fc .fc-timegrid-slot-minor
border-top-style: none
diff --git a/src/components/Calendar/SettingsTab.tsx b/src/components/Calendar/SettingsTab.tsx
index 3d80a97..c445093 100644
--- a/src/components/Calendar/SettingsTab.tsx
+++ b/src/components/Calendar/SettingsTab.tsx
@@ -50,21 +50,26 @@ export function SettingsTab({
<>
{/* Form group 1: Name field - first group, margin top 0 */}
- setName(e.target.value)}
- size="small"
- sx={{
- "&.MuiFormControl-root": {
- marginTop: 0,
- marginBottom: 0,
- },
- }}
- />
+
+ {t("calendarPopover.settings.calendarName")}
+
+
+ setName(e.target.value)}
+ size="small"
+ sx={{
+ "&.MuiFormControl-root": {
+ marginTop: 0,
+ marginBottom: 0,
+ },
+ }}
+ />
+
{/* Form group 2: Description */}
@@ -75,8 +80,6 @@ export function SettingsTab({
showMore={false}
description={description}
setDescription={setDescription}
- buttonVariant="contained"
- buttonColor="secondary"
/>
diff --git a/src/components/Dialog/README.md b/src/components/Dialog/README.md
index b8ef040..3639d05 100644
--- a/src/components/Dialog/README.md
+++ b/src/components/Dialog/README.md
@@ -4,8 +4,8 @@ A highly reusable dialog component that supports both normal and expanded (fulls
## Features
-- ✅ **Two Modes**: Normal popup (685px) and expanded fullscreen mode
-- ✅ **Preserves Header**: Expanded mode doesn't cover app header (90px default)
+- ✅ **Two Modes**: Normal popup (570px) and expanded fullscreen mode
+- ✅ **Preserves Header**: Expanded mode doesn't cover app header (70px default)
- ✅ **Clean Expanded View**: No backdrop/shadow in expanded mode for seamless integration
- ✅ **Instant Transition**: No animation when expanding for immediate feedback
- ✅ **Back Navigation**: Expanded mode shows back arrow icon in header for easy collapse
@@ -62,9 +62,9 @@ function MyComponent() {
| `actions` | `ReactNode` | - | Action buttons |
| `isExpanded` | `boolean` | `false` | Toggle fullscreen mode |
| `onExpandToggle` | `() => void` | - | Handler for back button click when expanded |
-| `normalMaxWidth` | `string` | `"685px"` | Max width in normal mode |
+| `normalMaxWidth` | `string` | `"570px"` | Max width in normal mode |
| `expandedContentMaxWidth` | `string` | `"990px"` | Content container max-width in expanded mode |
-| `headerHeight` | `string` | `"90px"` | App header height to preserve |
+| `headerHeight` | `string` | `"70px"` | App header height to preserve |
| `normalSpacing` | `number` | `2` | Stack spacing in normal mode (MUI spacing units: 1 = 8px) |
| `expandedSpacing` | `number` | `2` | Stack spacing in expanded mode (MUI spacing units: 1 = 8px) |
| `contentSx` | `SxProps` | - | Custom styles for DialogContent |
@@ -149,7 +149,7 @@ For apps with different header heights:
open={open}
onClose={handleClose}
title="Custom Header Height"
- headerHeight="80px"
+ headerHeight="70px"
isExpanded={true}
>
@@ -160,7 +160,7 @@ For apps with different header heights:
### Normal Mode (`isExpanded={false}`)
-- Dialog: max-width = `normalMaxWidth` (default 685px)
+- Dialog: max-width = `normalMaxWidth` (default 570px)
- Content: 100% width
- Height: auto (fits content)
- Position: centered with 32px margin
@@ -215,7 +215,7 @@ import { ResponsiveDialog } from "@/components/Dialog";
1. **Use `isExpanded` for Show More/Less**: Toggle between modes for better UX
2. **Provide `onExpandToggle` handler**: Required for back button functionality in expanded mode
3. **Hide expand button in actions when expanded**: Back arrow in header provides collapse action
-4. **Keep `normalMaxWidth` reasonable**: 685px works well for forms
+4. **Keep `normalMaxWidth` reasonable**: 570px works well for forms
5. **Center content in expanded mode**: Default 990px provides good reading width
6. **Preserve header**: Default 90px - adjust via `headerHeight` prop if needed
7. **MUI Stack handles spacing**: Children wrapped in Stack with configurable spacing prop - override with `normalSpacing`/`expandedSpacing` if needed
diff --git a/src/components/Dialog/ResponsiveDialog.tsx b/src/components/Dialog/ResponsiveDialog.tsx
index 332dc02..03a3ee3 100644
--- a/src/components/Dialog/ResponsiveDialog.tsx
+++ b/src/components/Dialog/ResponsiveDialog.tsx
@@ -21,7 +21,7 @@ import React, { ReactNode } from "react";
* ResponsiveDialog - A reusable dialog component that can switch between normal and expanded modes
*
* Features:
- * - Normal mode: Dialog with customizable max-width (default 685px)
+ * - Normal mode: Dialog with customizable max-width (default 570px)
* - Expanded mode: Full height dialog (excluding app header) with centered content container
* - Fully customizable with sx props for Dialog, DialogTitle, and DialogContent
* - Preserves app header visibility in expanded mode
@@ -58,7 +58,7 @@ interface ResponsiveDialogProps extends Omit<
isExpanded?: boolean;
/** Callback when expand/collapse button is clicked (required if using isExpanded) */
onExpandToggle?: () => void;
- /** Max width in normal mode (default: "685px") */
+ /** Max width in normal mode (default: "570px") */
normalMaxWidth?: string;
/** Max width of content container in expanded mode (default: "990px") */
expandedContentMaxWidth?: string;
@@ -98,9 +98,9 @@ function ResponsiveDialog({
actions,
isExpanded = false,
onExpandToggle,
- normalMaxWidth = "685px",
+ normalMaxWidth = "570px",
expandedContentMaxWidth = "990px",
- headerHeight = "90px",
+ headerHeight = "70px",
normalSpacing = 2,
expandedSpacing = 2,
contentSx,
@@ -116,6 +116,7 @@ function ResponsiveDialog({
}: ResponsiveDialogProps) {
const baseSx: SxProps = {
"& .MuiBackdrop-root": {
+ backgroundColor: "rgba(0, 0, 0, 0.1)",
opacity: isExpanded ? "0 !important" : undefined,
transition: isExpanded ? "none !important" : undefined,
pointerEvents: isExpanded ? "none" : undefined,
@@ -202,8 +203,9 @@ function ResponsiveDialog({
onClick={onExpandToggle}
aria-label="expand"
size="small"
+ sx={{ marginRight: 1 }}
>
-
+
)}
diff --git a/src/components/Event/AddDescButton.tsx b/src/components/Event/AddDescButton.tsx
index 068f3a0..d09513d 100644
--- a/src/components/Event/AddDescButton.tsx
+++ b/src/components/Event/AddDescButton.tsx
@@ -1,7 +1,9 @@
-import { Box, Button, TextField } from "@linagora/twake-mui";
-import { Description as DescriptionIcon } from "@mui/icons-material";
+import { TextField } from "@linagora/twake-mui";
+import { Notes as NotesIcon } from "@mui/icons-material";
+import React from "react";
import { useI18n } from "twake-i18n";
import { FieldWithLabel } from "./components/FieldWithLabel";
+import { SectionPreviewRow } from "./components/SectionPreviewRow";
export function AddDescButton({
showDescription,
@@ -9,73 +11,72 @@ export function AddDescButton({
showMore,
description,
setDescription,
- buttonVariant,
- buttonColor,
}: {
showDescription: boolean;
setShowDescription: (b: boolean) => void;
showMore: boolean;
description: string;
setDescription: (d: string) => void;
- buttonVariant?: "text" | "outlined" | "contained";
- buttonColor?:
- | "inherit"
- | "primary"
- | "secondary"
- | "success"
- | "error"
- | "info"
- | "warning";
}) {
const { t } = useI18n();
+ const descriptionInputRef = React.useRef(null);
+
+ React.useEffect(() => {
+ if (showDescription) {
+ descriptionInputRef.current?.focus();
+ }
+ }, [showDescription]);
+
+ const descriptionField = (
+
+ 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",
+ },
+ }}
+ />
+
+ );
+
+ if (showMore) {
+ return descriptionField;
+ }
+
return (
<>
{!showDescription && (
-
-
- }
- onClick={() => setShowDescription(true)}
- size="medium"
- variant={buttonVariant}
- color={buttonColor}
- >
- {t("event.form.addDescription")}
-
-
-
- )}
- {showDescription && (
-
- 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",
- },
- }}
- />
+
+ }
+ onClick={() => setShowDescription(true)}
+ >
+ {t("event.form.addDescription")}
+
)}
+ {showDescription && descriptionField}
>
);
}
diff --git a/src/components/Event/EventFormFields.tsx b/src/components/Event/EventFormFields.tsx
index 19f8c5c..592bd92 100644
--- a/src/components/Event/EventFormFields.tsx
+++ b/src/components/Event/EventFormFields.tsx
@@ -5,6 +5,7 @@ import iconCamera from "@/static/images/icon-camera.svg";
import {
addVideoConferenceToDescription,
generateMeetingLink,
+ removeVideoConferenceFromDescription,
} from "@/utils/videoConferenceUtils";
import {
Box,
@@ -20,12 +21,16 @@ import {
ToggleButton,
ToggleButtonGroup,
Typography,
+ useTheme,
} from "@linagora/twake-mui";
+import { alpha } from "@mui/material/styles";
import {
Close as DeleteIcon,
ContentCopy as CopyIcon,
Public as PublicIcon,
+ LocationOn as LocationIcon,
} from "@mui/icons-material";
+import SquareRoundedIcon from "@mui/icons-material/SquareRounded";
import LockOutlineIcon from "@mui/icons-material/LockOutline";
import React from "react";
import { useI18n } from "twake-i18n";
@@ -35,7 +40,9 @@ import { SnackbarAlert } from "../Loading/SnackBarAlert";
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
import { AddDescButton } from "./AddDescButton";
import { DateTimeFields } from "./components/DateTimeFields";
+import { DateTimeSummary } from "./components/DateTimeSummary";
import { FieldWithLabel } from "./components/FieldWithLabel";
+import { SectionPreviewRow } from "./components/SectionPreviewRow";
import RepeatEvent from "./EventRepeat";
import { useAllDayToggle } from "./hooks/useAllDayToggle";
import { combineDateTime, splitDateTime } from "./utils/dateTimeHelpers";
@@ -156,6 +163,7 @@ export default function EventFormFields({
onHasEndDateChangedChange,
}: EventFormFieldsProps) {
const { t } = useI18n();
+ const theme = useTheme();
// Internal state for 4 separate fields
const [startDate, setStartDate] = React.useState("");
@@ -166,10 +174,26 @@ export default function EventFormFields({
// Track if user has manually changed end date in extended mode
const [hasEndDateChanged, setHasEndDateChanged] = React.useState(false);
- // Reset hasEndDateChanged when modal closes
+ // Track if user has clicked on datetime clickable section in normal mode
+ // Once clicked, normal mode will always show full fields until modal closes
+ const [hasClickedDateTimeSection, setHasClickedDateTimeSection] =
+ React.useState(false);
+
+ // Track if user has clicked on location section in normal mode
+ const [hasClickedLocationSection, setHasClickedLocationSection] =
+ React.useState(false);
+
+ // Track if user has clicked on calendar section in normal mode
+ const [hasClickedCalendarSection, setHasClickedCalendarSection] =
+ React.useState(false);
+
+ // Reset hasEndDateChanged and hasClickedDateTimeSection when modal closes
React.useEffect(() => {
if (!isOpen) {
setHasEndDateChanged(false);
+ setHasClickedDateTimeSection(false);
+ setHasClickedLocationSection(false);
+ setHasClickedCalendarSection(false);
}
}, [isOpen]);
@@ -210,10 +234,18 @@ export default function EventFormFields({
// Ref for title input field to enable auto-focus
const titleInputRef = React.useRef(null);
+ const locationInputRef = React.useRef(null);
// Track previous showMore state to detect changes
const prevShowMoreRef = React.useRef(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)
React.useEffect(() => {
if (isOpen) {
@@ -389,6 +421,9 @@ export default function EventFormFields({
setDescription(updatedDescription);
setHasVideoConference(true);
setMeetingLink(newMeetingLink);
+ if (showMore) {
+ setShowDescription(true);
+ }
};
const [openToast, setOpenToast] = React.useState(false);
@@ -405,11 +440,7 @@ export default function EventFormFields({
};
const handleDeleteVideoConference = () => {
- const updatedDescription = description.replace(
- /\nVisio: https?:\/\/[^\s]+/,
- ""
- );
- setDescription(updatedDescription);
+ setDescription(removeVideoConferenceFromDescription(description));
setHasVideoConference(false);
setMeetingLink(null);
};
@@ -421,7 +452,10 @@ export default function EventFormFields({
return (
<>
-
+
-
-
-
- {}}
- />
-
-
-
-
-
- }
- label={
- {t("event.form.allDay")}
- }
- />
- {
- const newShowRepeat = !showRepeat;
- setShowRepeat(newShowRepeat);
- if (newShowRepeat) {
- setRepetition({
- freq: "daily",
- interval: 1,
- occurrences: 0,
- endDate: "",
- byday: null,
- } as RepetitionObject);
- } else {
- setRepetition({
- freq: "",
- interval: 1,
- occurrences: 0,
- endDate: "",
- byday: null,
- } as RepetitionObject);
- }
- }}
- />
- }
- label={
- {t("event.form.repeat")}
- }
- />
-
- timezoneList.getTimezoneOffset(tzName, new Date(start))
- }
- showIcon={false}
- width={240}
- size="small"
- placeholder={t("event.form.timezonePlaceholder")}
- />
-
-
-
- {(showRepeat || (typeOfAction === "solo" && repetition?.freq)) && (
-
-
+ {!showMore && !hasClickedDateTimeSection ? (
+ setHasClickedDateTimeSection(true)}
/>
+ ) : (
+ {}}
+ />
+ )}
+
+
+ {!(!showMore && !hasClickedDateTimeSection) && (
+
+
+
+ }
+ label={
+ {t("event.form.allDay")}
+ }
+ />
+ {
+ const newShowRepeat = !showRepeat;
+ setShowRepeat(newShowRepeat);
+ if (newShowRepeat) {
+ setRepetition({
+ freq: "daily",
+ interval: 1,
+ occurrences: 0,
+ endDate: "",
+ byday: null,
+ } as RepetitionObject);
+ } else {
+ setRepetition({
+ freq: "",
+ interval: 1,
+ occurrences: 0,
+ endDate: "",
+ byday: null,
+ } as RepetitionObject);
+ }
+ }}
+ />
+ }
+ label={
+ {t("event.form.repeat")}
+ }
+ />
+
+ timezoneList.getTimezoneOffset(tzName, new Date(start))
+ }
+ showIcon={false}
+ width={220}
+ size="small"
+ placeholder={t("event.form.timezonePlaceholder")}
+ />
+
)}
+ {!(!showMore && !hasClickedDateTimeSection) &&
+ (showRepeat || (typeOfAction === "solo" && repetition?.freq)) && (
+
+
+
+ )}
+
}
/>
-
-
- }
- onClick={handleAddVideoConference}
- size="medium"
- variant="contained"
- color="secondary"
- sx={{
- borderRadius: "4px",
- display: hasVideoConference ? "none" : "flex",
- }}
- >
- {t("event.form.addVisioConference")}
-
-
- {hasVideoConference && meetingLink && (
- <>
+ {!showMore ? (
+ hasVideoConference && meetingLink ? (
+
}
- onClick={() => window.open(meetingLink, "_blank")}
+ onClick={() =>
+ window.open(meetingLink, "_blank", "noopener,noreferrer")
+ }
size="medium"
variant="contained"
color="primary"
- sx={{
- borderRadius: "4px",
- mr: 1,
- }}
+ sx={{ borderRadius: "4px", mr: 1 }}
>
{t("event.form.joinVisioConference")}
@@ -608,38 +645,153 @@ export default function EventFormFields({
>
- >
- )}
-
+
+ ) : (
+
+ }
+ onClick={handleAddVideoConference}
+ >
+ {t("event.form.addVisioConference")}
+
+ )
+ ) : (
+
+
+ }
+ onClick={handleAddVideoConference}
+ size="medium"
+ variant="contained"
+ color="secondary"
+ sx={{
+ borderRadius: "4px",
+ display: hasVideoConference ? "none" : "flex",
+ }}
+ >
+ {t("event.form.addVisioConference")}
+
+
+ {hasVideoConference && meetingLink && (
+ <>
+
+ }
+ onClick={() =>
+ window.open(meetingLink, "_blank", "noopener,noreferrer")
+ }
+ size="medium"
+ variant="contained"
+ color="primary"
+ sx={{
+ borderRadius: "4px",
+ mr: 1,
+ }}
+ >
+ {t("event.form.joinVisioConference")}
+
+
+
+
+
+
+
+ >
+ )}
+
+ )}
-
- setLocation(e.target.value)}
- size="small"
- margin="dense"
- />
-
+
-
-
-
-
+ {location || t("event.form.locationPlaceholder")}
+
+ ) : (
+ setLocation(e.target.value)}
+ size="small"
+ margin="dense"
+ />
+ )}
+
+
+
+ {!showMore && !hasClickedCalendarSection ? (
+ cal.id === calendarid)
+ ?.color?.light ?? "#3788D8",
+ width: 24,
+ height: 24,
+ }}
+ />
+ }
+ onClick={() => setHasClickedCalendarSection(true)}
+ >
+ {userPersonalCalendars.find((cal) => cal.id === calendarid)?.name ||
+ t("event.form.calendar")}
+
+ ) : (
+
+
+
+ )}
{showMore && (
diff --git a/src/components/Event/InfoRow.tsx b/src/components/Event/InfoRow.tsx
index de21085..436eaea 100644
--- a/src/components/Event/InfoRow.tsx
+++ b/src/components/Event/InfoRow.tsx
@@ -8,6 +8,7 @@ type InfoRowProps = {
data?: string; // optional link target
content?: React.ReactNode; // if provided, overrides text rendering
style?: React.CSSProperties;
+ alignItems?: React.CSSProperties["alignItems"];
};
function detectUrls(text: string) {
@@ -62,12 +63,13 @@ export function InfoRow({
data,
content,
style,
+ alignItems = "center",
}: InfoRowProps) {
return (
void;
+ iconColor?: string;
+ children?: React.ReactNode;
+ ariaLabel?: string;
+}
+
+export const ClickableField: React.FC = ({
+ icon,
+ text,
+ onClick,
+ iconColor,
+ children,
+ ariaLabel,
+}) => {
+ const theme = useTheme();
+
+ return (
+ {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ onClick();
+ }
+ }}
+ sx={{
+ display: "flex",
+ alignItems: children ? "flex-start" : "center",
+ cursor: "pointer",
+ padding: "8px 12px",
+ borderRadius: "4px",
+ "&:hover": {
+ backgroundColor: "action.hover",
+ },
+ "&:focus-visible": {
+ outline: `2px solid ${theme.palette.primary.main}`,
+ outlineOffset: "2px",
+ },
+ }}
+ >
+
+ {icon}
+
+ {children ? (
+ {children}
+ ) : (
+
+ {text}
+
+ )}
+
+ );
+};
diff --git a/src/components/Event/components/DateTimeFields.tsx b/src/components/Event/components/DateTimeFields.tsx
index 0e26a70..b380784 100644
--- a/src/components/Event/components/DateTimeFields.tsx
+++ b/src/components/Event/components/DateTimeFields.tsx
@@ -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
*/
@@ -369,6 +378,7 @@ export const DateTimeFields: React.FC = ({
false,
t("dateTimeFields.startDate")
),
+ layout: { sx: dateCalendarLayoutSx },
}}
/>
@@ -415,6 +425,7 @@ export const DateTimeFields: React.FC = ({
!!validation.errors.dateTime,
t("dateTimeFields.endDate")
),
+ layout: { sx: dateCalendarLayoutSx },
}}
/>
@@ -463,6 +474,7 @@ export const DateTimeFields: React.FC = ({
false,
t("dateTimeFields.startDate")
),
+ layout: { sx: dateCalendarLayoutSx },
}}
/>
@@ -483,6 +495,7 @@ export const DateTimeFields: React.FC = ({
!!validation.errors.dateTime,
t("dateTimeFields.endDate")
),
+ layout: { sx: dateCalendarLayoutSx },
}}
/>
@@ -502,6 +515,7 @@ export const DateTimeFields: React.FC = ({
false,
startDateLabel
),
+ layout: { sx: dateCalendarLayoutSx },
}}
/>
diff --git a/src/components/Event/components/DateTimeSummary.tsx b/src/components/Event/components/DateTimeSummary.tsx
new file mode 100644
index 0000000..91c1d11
--- /dev/null
+++ b/src/components/Event/components/DateTimeSummary.tsx
@@ -0,0 +1,171 @@
+import { Box, Typography, useTheme } from "@linagora/twake-mui";
+import { alpha } from "@mui/material/styles";
+import AccessTimeIcon from "@mui/icons-material/AccessTime";
+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 { useI18n } from "twake-i18n";
+import { getTimezoneOffset } from "@/utils/timezone";
+import { RepetitionObject } from "@/features/Events/EventsTypes";
+import { LONG_DATE_FORMAT } from "../utils/dateTimeFormatters";
+import { SectionPreviewRow } from "./SectionPreviewRow";
+
+interface DateTimeSummaryProps {
+ startDate: string;
+ startTime: string;
+ endDate: string;
+ endTime: string;
+ allday: boolean;
+ timezone: string;
+ repetition: RepetitionObject;
+ showEndDate: boolean;
+ onClick: () => void;
+}
+
+export const DateTimeSummary: React.FC = ({
+ startDate,
+ startTime,
+ endDate,
+ endTime,
+ allday,
+ timezone,
+ repetition,
+ showEndDate,
+ onClick,
+}) => {
+ const { t, lang } = useI18n();
+ const theme = useTheme();
+
+ // 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 => {
+ if (!dateStr) return "";
+ const date = dayjs(dateStr);
+ 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 in 24h: "03:30 - 16:30"
+ const formatTime = (startTimeStr: string, endTimeStr: string): string => {
+ if (allday || !startTimeStr || !endTimeStr) return "";
+
+ const toHHmm = (timeStr: string): string => {
+ const [h, m] = timeStr.split(":").map((s) => parseInt(s, 10) || 0);
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
+ };
+
+ return `${toHHmm(startTimeStr)} - ${toHHmm(endTimeStr)}`;
+ };
+
+ // Format timezone: "(UTC+2) Paris". Use event date for offset (DST correctness).
+ const formatTimezone = (tz: string, dateStr?: string): string => {
+ if (!tz) return "";
+ try {
+ const dateForOffset = dateStr ? dayjs(dateStr).toDate() : new Date();
+ const offset = getTimezoneOffset(tz, dateForOffset);
+ const tzName = tz.replace(/_/g, " ");
+ return `(${offset}) ${tzName}`;
+ } catch (error) {
+ return tz.replace(/_/g, " ");
+ }
+ };
+
+ // Format repeat: "Doesn't repeat" or repeat info
+ const formatRepeat = (rep: RepetitionObject): string => {
+ if (!rep || !rep.freq) {
+ return t("event.repeat.doesNotRepeat");
+ }
+
+ const interval = rep.interval || 1;
+ const freqMap: { [key: string]: string } = {
+ daily: t("event.repeat.frequency.days"),
+ weekly: t("event.repeat.frequency.weeks"),
+ monthly: t("event.repeat.frequency.months"),
+ yearly: t("event.repeat.frequency.years"),
+ };
+
+ const freqText = freqMap[rep.freq] || rep.freq;
+
+ if (interval === 1) {
+ return `${t("event.repeat.every")} ${freqText}`;
+ }
+
+ return `${t("event.repeat.every")} ${interval} ${freqText}`;
+ };
+
+ // Format date text: show both start and end date if showEndDate is true
+ const formatDateText = (): string => {
+ if (showEndDate && endDate && endDate !== startDate) {
+ const startDateText = formatDate(startDate);
+ const endDateText = formatDate(endDate);
+ return `${startDateText} - ${endDateText}`;
+ }
+ return formatDate(startDate);
+ };
+
+ const dateText = formatDateText();
+ const timeText = formatTime(startTime, endTime);
+ const timezoneText = formatTimezone(timezone, startDate);
+ const repeatText = formatRepeat(repetition);
+
+ // Don't render if no date
+ if (!startDate) {
+ return null;
+ }
+
+ const primaryStyle = {
+ fontSize: "14px",
+ fontWeight: 500,
+ color: alpha(theme.palette.grey[900], 0.9),
+ };
+
+ return (
+ }
+ onClick={onClick}
+ >
+
+
+ {dateText}
+ {showEndDate &&
}
+ {timeText && (
+
+ {timeText}
+
+ )}
+
+
+
+ {timezoneText}
+
+
+ {repeatText}
+
+
+
+
+ );
+};
diff --git a/src/components/Event/components/SectionPreviewRow.tsx b/src/components/Event/components/SectionPreviewRow.tsx
new file mode 100644
index 0000000..4732b63
--- /dev/null
+++ b/src/components/Event/components/SectionPreviewRow.tsx
@@ -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 = ({
+ 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 (
+
+
+ {icon}
+
+
+ {typeof children === "string" ? (
+
+ {children}
+
+ ) : (
+ children
+ )}
+
+
+ );
+};
diff --git a/src/components/Event/utils/eventUtils.tsx b/src/components/Event/utils/eventUtils.tsx
index a3253d8..a5a6207 100644
--- a/src/components/Event/utils/eventUtils.tsx
+++ b/src/components/Event/utils/eventUtils.tsx
@@ -65,16 +65,7 @@ export function renderAttendeeBadge(
-
- {a.cn || a.cal_address}
-
+ {a.cn || a.cal_address}
{isOrganizer && (
{t("event.organizer")}
diff --git a/src/components/Menubar/EventSearchBar.tsx b/src/components/Menubar/EventSearchBar.tsx
index f099562..228fb4c 100644
--- a/src/components/Menubar/EventSearchBar.tsx
+++ b/src/components/Menubar/EventSearchBar.tsx
@@ -214,7 +214,7 @@ export default function SearchBar() {
}}
>
{!extended && (
- setExtended(true)}>
+ setExtended(true)}>
)}
diff --git a/src/components/Menubar/Menubar.styl b/src/components/Menubar/Menubar.styl
index ade328e..402d18b 100644
--- a/src/components/Menubar/Menubar.styl
+++ b/src/components/Menubar/Menubar.styl
@@ -38,20 +38,22 @@
padding 0.5rem
width 100%
z-index 1300
- height 80px
+ height 60px
background-color #fff
.menubar-item
display flex
align-items center
- width calc(330px - 0.5rem)
+ margin-right 65px
.tc-home
cursor pointer
.logo
- padding 0.5rem 1rem
+ padding 0
font-size 1.5rem
+ max-width 230px
+ margin-left 1rem
.nav-month
padding-right 2px
@@ -91,6 +93,14 @@
justify-content flex-end
align-items center
+.current-date-time
+ font-family Roboto
+ font-size 22px
+ font-style normal
+ font-weight 400
+ line-height 36px
+ color #243B55
+
body.fullscreen-view .navigation-controls,
body.fullscreen-view .current-date-time,
body.fullscreen-view .refresh-button,
diff --git a/src/components/Menubar/Menubar.tsx b/src/components/Menubar/Menubar.tsx
index f858024..0c1d2fd 100644
--- a/src/components/Menubar/Menubar.tsx
+++ b/src/components/Menubar/Menubar.tsx
@@ -4,6 +4,7 @@ import { Logout } from "@/features/User/oidcAuth";
import logo from "@/static/header-logo.svg";
import { getInitials, stringToGradient } from "@/utils/avatarUtils";
import { getUserDisplayName } from "@/utils/userUtils";
+import { redirectTo } from "@/utils/navigation";
import { CalendarApi } from "@fullcalendar/core";
import {
Avatar,
@@ -19,7 +20,8 @@ import {
Select,
Typography,
} from "@linagora/twake-mui";
-import AppsIcon from "@mui/icons-material/Apps";
+import WidgetsOutlinedIcon from "@mui/icons-material/WidgetsOutlined";
+import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import LogoutIcon from "@mui/icons-material/Logout";
@@ -139,7 +141,7 @@ export function Menubar({
const handleLogoutClick = async () => {
const logoutUrl = await Logout();
sessionStorage.removeItem("tokenSet");
- window.location.assign(logoutUrl.href);
+ redirectTo(logoutUrl.href);
handleUserMenuClose();
};
@@ -195,9 +197,7 @@ export function Menubar({
-
- {dateLabel}
-
+
{dateLabel}
@@ -211,6 +211,7 @@ export function Menubar({
onClick={onRefresh}
aria-label={t("menubar.refresh")}
title={t("menubar.refresh")}
+ sx={{ mr: 1 }}
>
@@ -226,7 +227,11 @@ export function Menubar({
onChange={(e) => handleViewChange(e.target.value)}
variant="outlined"
aria-label={t("menubar.viewSelector")}
- sx={{ height: "43px" }}
+ sx={{
+ borderRadius: "12px",
+ marginLeft: 1,
+ "& fieldset": { borderRadius: "12px" },
+ }}
>