[#82] added cozy-ui for internationnalisation (#267)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2025-11-06 15:46:58 +01:00
committed by GitHub
parent c1f37e3ebd
commit 36bc027d6d
49 changed files with 12003 additions and 5002 deletions
+33 -31
View File
@@ -1,10 +1,10 @@
import { fireEvent, screen, waitFor } from "@testing-library/react"; import { act, fireEvent, screen, waitFor } from "@testing-library/react";
import CalendarApp from "../../src/components/Calendar/Calendar"; import CalendarApp from "../../src/components/Calendar/Calendar";
import * as eventThunks from "../../src/features/Calendars/CalendarSlice"; import * as eventThunks from "../../src/features/Calendars/CalendarSlice";
import { renderWithProviders } from "../utils/Renderwithproviders"; import { renderWithProviders } from "../utils/Renderwithproviders";
import { searchUsers } from "../../src/features/User/userAPI"; import { searchUsers } from "../../src/features/User/userAPI";
import * as calendarThunks from "../../src/features/Calendars/CalendarSlice"; import * as calendarThunks from "../../src/features/Calendars/CalendarSlice";
import { act, useRef } from "react"; import { useRef } from "react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import CalendarLayout from "../../src/components/Calendar/CalendarLayout"; import CalendarLayout from "../../src/components/Calendar/CalendarLayout";
@@ -41,7 +41,7 @@ describe("CalendarSelection", () => {
calendars: { calendars: {
list: { list: {
"user1/cal1": { "user1/cal1": {
name: "Calendar personnal", name: "Calendar personal",
id: "user1/cal1", id: "user1/cal1",
color: { light: "#FF0000", dark: "#000" }, color: { light: "#FF0000", dark: "#000" },
ownerEmails: ["alice@example.com"], ownerEmails: ["alice@example.com"],
@@ -146,11 +146,11 @@ describe("CalendarSelection", () => {
preloadedState preloadedState
); );
}); });
expect(screen.getByText("Personnal Calendars")).toBeInTheDocument(); expect(screen.getByText("calendar.personal")).toBeInTheDocument();
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument(); expect(screen.getByText("calendar.delegated")).toBeInTheDocument();
expect(screen.getByText("Other Calendars")).toBeInTheDocument(); expect(screen.getByText("calendar.other")).toBeInTheDocument();
expect(screen.getByLabelText("Calendar personnal")).toBeInTheDocument(); expect(screen.getByLabelText("Calendar personal")).toBeInTheDocument();
expect(screen.getByLabelText("Calendar delegated")).toBeInTheDocument(); expect(screen.getByLabelText("Calendar delegated")).toBeInTheDocument();
expect(screen.getByLabelText("Calendar shared")).toBeInTheDocument(); expect(screen.getByLabelText("Calendar shared")).toBeInTheDocument();
}); });
@@ -162,16 +162,16 @@ describe("CalendarSelection", () => {
preloadedState preloadedState
); );
}); });
expect(screen.getByText("Personnal Calendars")).toBeInTheDocument(); expect(screen.getByText("calendar.personal")).toBeInTheDocument();
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument(); expect(screen.getByText("calendar.delegated")).toBeInTheDocument();
expect(screen.getByText("Other Calendars")).toBeInTheDocument(); expect(screen.getByText("calendar.other")).toBeInTheDocument();
expect(screen.getByLabelText("Calendar personnal")).toBeInTheDocument(); expect(screen.getByLabelText("Calendar personal")).toBeInTheDocument();
expect(screen.getByLabelText("Calendar delegated")).toBeInTheDocument(); expect(screen.getByLabelText("Calendar delegated")).toBeInTheDocument();
expect(screen.getByLabelText("Calendar shared")).toBeInTheDocument(); expect(screen.getByLabelText("Calendar shared")).toBeInTheDocument();
const sharedAccordionSummary = screen const sharedAccordionSummary = screen
.getByText("Other Calendars") .getByText("calendar.other")
.closest(".MuiAccordionSummary-root"); .closest(".MuiAccordionSummary-root");
const addButton = screen.getAllByTestId("AddIcon")[2]; const addButton = screen.getAllByTestId("AddIcon")[2];
@@ -185,12 +185,12 @@ describe("CalendarSelection", () => {
}); });
expect(sharedAccordionSummary).toHaveAttribute("aria-expanded", "true"); expect(sharedAccordionSummary).toHaveAttribute("aria-expanded", "true");
}); });
it("BUGFIX: remove dots in mini calendar when unselecting personnal calendar", async () => { it("BUGFIX: remove dots in mini calendar when unselecting personal calendar", async () => {
await act(async () => await act(async () =>
renderWithProviders(<CalendarLayout />, preloadedState) renderWithProviders(<CalendarLayout />, preloadedState)
); );
const checkbox = screen.getByLabelText("Calendar personnal"); const checkbox = screen.getByLabelText("Calendar personal");
// checkbox checked : events shown // checkbox checked : events shown
expect( expect(
screen.getByTestId( screen.getByTestId(
@@ -223,9 +223,9 @@ describe("CalendarSelection", () => {
renderWithProviders(<CalendarLayout />, preloadedState) renderWithProviders(<CalendarLayout />, preloadedState)
); );
// hide personnal event first // hide personal event first
await act(async () => { await act(async () => {
fireEvent.click(screen.getByLabelText("Calendar personnal")); fireEvent.click(screen.getByLabelText("Calendar personal"));
}); });
const checkbox = screen.getByLabelText("Calendar delegated"); const checkbox = screen.getByLabelText("Calendar delegated");
@@ -250,9 +250,9 @@ describe("CalendarSelection", () => {
renderWithProviders(<CalendarLayout />, preloadedState) renderWithProviders(<CalendarLayout />, preloadedState)
); );
// hide personnal event first // hide personal event first
await act(async () => { await act(async () => {
fireEvent.click(screen.getByLabelText("Calendar personnal")); fireEvent.click(screen.getByLabelText("Calendar personal"));
}); });
const checkbox = screen.getByLabelText("Calendar shared"); const checkbox = screen.getByLabelText("Calendar shared");
@@ -286,7 +286,7 @@ describe("CalendarSelection", () => {
); );
}); });
expect(screen.getByLabelText("Calendar personnal")).toBeChecked(); expect(screen.getByLabelText("Calendar personal")).toBeChecked();
expect(screen.getByLabelText("Calendar delegated")).toBeChecked(); expect(screen.getByLabelText("Calendar delegated")).toBeChecked();
expect(screen.getByLabelText("Calendar shared")).toBeChecked(); expect(screen.getByLabelText("Calendar shared")).toBeChecked();
}); });
@@ -299,12 +299,12 @@ describe("CalendarSelection", () => {
); );
}); });
expect(screen.getByLabelText("Calendar personnal")).toBeChecked(); expect(screen.getByLabelText("Calendar personal")).toBeChecked();
expect(screen.getByLabelText("Calendar delegated")).not.toBeChecked(); expect(screen.getByLabelText("Calendar delegated")).not.toBeChecked();
expect(screen.getByLabelText("Calendar shared")).not.toBeChecked(); expect(screen.getByLabelText("Calendar shared")).not.toBeChecked();
await act(async () => { await act(async () => {
fireEvent.click(screen.getByLabelText("Calendar personnal")); fireEvent.click(screen.getByLabelText("Calendar personal"));
fireEvent.click(screen.getByLabelText("Calendar shared")); fireEvent.click(screen.getByLabelText("Calendar shared"));
}); });
@@ -333,7 +333,7 @@ describe("calendar Availability search", () => {
calendars: { calendars: {
list: { list: {
"user1/cal1": { "user1/cal1": {
name: "Calendar personnal", name: "Calendar personal",
id: "user1/cal1", id: "user1/cal1",
color: { light: "#FF0000", dark: "#000" }, color: { light: "#FF0000", dark: "#000" },
ownerEmails: ["alice@example.com"], ownerEmails: ["alice@example.com"],
@@ -364,7 +364,7 @@ describe("calendar Availability search", () => {
renderWithProviders(<CalendarTestWrapper />, preloadedState) renderWithProviders(<CalendarTestWrapper />, preloadedState)
); );
const input = screen.getByPlaceholderText(/start typing a name or email/i); const input = screen.getByPlaceholderText("peopleSearch.placeholder");
act(() => { act(() => {
userEvent.type(input, "New"); userEvent.type(input, "New");
}); });
@@ -394,7 +394,7 @@ describe("calendar Availability search", () => {
renderWithProviders(<CalendarTestWrapper />, preloadedState) renderWithProviders(<CalendarTestWrapper />, preloadedState)
); );
const input = screen.getByPlaceholderText(/start typing a name or email/i); const input = screen.getByPlaceholderText("peopleSearch.placeholder");
await act(async () => userEvent.type(input, "Alice")); await act(async () => userEvent.type(input, "Alice"));
const option = await screen.findByText("Alice"); const option = await screen.findByText("Alice");
@@ -423,7 +423,7 @@ describe("calendar Availability search", () => {
renderWithProviders(<CalendarTestWrapper />, preloadedState) renderWithProviders(<CalendarTestWrapper />, preloadedState)
); );
const input = screen.getByPlaceholderText(/start typing a name or email/i); const input = screen.getByPlaceholderText("peopleSearch.placeholder");
await act(async () => userEvent.type(input, "New")); await act(async () => userEvent.type(input, "New"));
const option = await screen.findByText("New User"); const option = await screen.findByText("New User");
@@ -433,10 +433,12 @@ describe("calendar Availability search", () => {
}); });
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
await act(async () => { await act(async () => {
fireEvent.click(screen.getByRole("button", { name: /create event/i })); fireEvent.click(
screen.getByRole("button", { name: "event.createEvent" })
);
}); });
await waitFor(() => { await waitFor(() => {
expect(screen.getAllByText(/Create Event/i)).toHaveLength(2); expect(screen.getAllByText("event.createEvent")).toHaveLength(2);
expect(screen.getAllByText(/New User/i)).toHaveLength(2); expect(screen.getAllByText(/New User/i)).toHaveLength(2);
}); });
}); });
@@ -460,7 +462,7 @@ describe("calendar Availability search", () => {
renderWithProviders(<CalendarTestWrapper />, preloadedState) renderWithProviders(<CalendarTestWrapper />, preloadedState)
); );
const input = screen.getByPlaceholderText(/start typing a name or email/i); const input = screen.getByPlaceholderText("peopleSearch.placeholder");
await act(async () => userEvent.type(input, "New")); await act(async () => userEvent.type(input, "New"));
const option = await screen.findByText("New User"); const option = await screen.findByText("New User");
@@ -473,12 +475,12 @@ describe("calendar Availability search", () => {
}); });
await waitFor(() => { await waitFor(() => {
expect(screen.getAllByText(/Create Event/i)).toHaveLength(2); expect(screen.getAllByText("event.createEvent")).toHaveLength(2);
expect(screen.getAllByText(/New User/i)).toHaveLength(2); expect(screen.getAllByText(/New User/i)).toHaveLength(2);
}); });
}); });
it("BUGFIX: can untoggle all personnal calendars", async () => { it("BUGFIX: can untoggle all calendar.personal", async () => {
await act(async () => await act(async () =>
renderWithProviders(<CalendarTestWrapper />, { renderWithProviders(<CalendarTestWrapper />, {
user: preloadedState.user, user: preloadedState.user,
@@ -489,7 +491,7 @@ describe("calendar Availability search", () => {
}) })
); );
const checkbox = screen.getByLabelText("Calendar personnal"); const checkbox = screen.getByLabelText("Calendar personal");
expect(checkbox).toBeChecked(); expect(checkbox).toBeChecked();
fireEvent.click(checkbox); // toggle off fireEvent.click(checkbox); // toggle off
+3 -4
View File
@@ -1,6 +1,5 @@
import { fireEvent, screen, waitFor } from "@testing-library/react"; import { fireEvent, screen, waitFor, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { act } from "react";
import CalendarSearch from "../../src/components/Calendar/CalendarSearch"; import CalendarSearch from "../../src/components/Calendar/CalendarSearch";
import * as CalendarApi from "../../src/features/Calendars/CalendarApi"; import * as CalendarApi from "../../src/features/Calendars/CalendarApi";
import * as CalendarSlice from "../../src/features/Calendars/CalendarSlice"; import * as CalendarSlice from "../../src/features/Calendars/CalendarSlice";
@@ -188,7 +187,7 @@ describe("CalendarSearch", () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getByText("No more Calendar for Test User") screen.getByText("calendar.noMoreCalendarsFor(name=Test User)")
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@@ -223,7 +222,7 @@ describe("CalendarSearch", () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getByText("No publicly available calendars for Test User") screen.getByText("calendar.noPublicCalendarsFor(name=Test User)")
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
}); });
+22 -14
View File
@@ -44,7 +44,7 @@ describe("CalendarSelection", () => {
jest.clearAllMocks(); jest.clearAllMocks();
cleanup(); cleanup();
}); });
it("renders personal, delegated and other calendars", () => { it("renders personal, delegated and calendar.other", () => {
renderWithProviders( renderWithProviders(
<CalendarSelection <CalendarSelection
selectedCalendars={["user1/cal1"]} selectedCalendars={["user1/cal1"]}
@@ -56,9 +56,9 @@ describe("CalendarSelection", () => {
} }
); );
expect(screen.getByText("Personnal Calendars")).toBeInTheDocument(); expect(screen.getByText("calendar.personal")).toBeInTheDocument();
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument(); expect(screen.getByText("calendar.delegated")).toBeInTheDocument();
expect(screen.getByText("Other Calendars")).toBeInTheDocument(); expect(screen.getByText("calendar.other")).toBeInTheDocument();
expect(screen.getByLabelText("Calendar personal")).toBeChecked(); expect(screen.getByLabelText("Calendar personal")).toBeChecked();
expect(screen.getByLabelText("Calendar delegated")).not.toBeChecked(); expect(screen.getByLabelText("Calendar delegated")).not.toBeChecked();
@@ -125,11 +125,13 @@ describe("CalendarSelection", () => {
fireEvent.click(addButtons[1]); fireEvent.click(addButtons[1]);
await waitFor(() => await waitFor(() =>
expect(screen.getByText(/Add new calendar/i)).toBeInTheDocument() expect(
screen.getByText("calendarPopover.tabs.addNew")
).toBeInTheDocument()
); );
}); });
it("Navigates to deletion dialog and deletes personnal cal", async () => { it("Navigates to deletion dialog and deletes personal cal", async () => {
const spy = jest const spy = jest
.spyOn(calendarThunks, "removeCalendarAsync") .spyOn(calendarThunks, "removeCalendarAsync")
.mockImplementation((payload) => { .mockImplementation((payload) => {
@@ -152,7 +154,9 @@ describe("CalendarSelection", () => {
userEvent.click(screen.getByText(/delete/i)); userEvent.click(screen.getByText(/delete/i));
await waitFor(() => await waitFor(() =>
expect(screen.getByText("Remove Calendar personal?")).toBeInTheDocument() expect(
screen.getByText("calendar.delete.title(name=Calendar personal)")
).toBeInTheDocument()
); );
fireEvent.click(screen.getByRole("button", { name: /delete/i })); fireEvent.click(screen.getByRole("button", { name: /delete/i }));
@@ -182,7 +186,9 @@ describe("CalendarSelection", () => {
userEvent.click(screen.getByText(/remove/i)); userEvent.click(screen.getByText(/remove/i));
await waitFor(() => await waitFor(() =>
expect(screen.getByText("Remove Calendar delegated?")).toBeInTheDocument() expect(
screen.getByText("calendar.delete.title(name=Calendar delegated)")
).toBeInTheDocument()
); );
fireEvent.click(screen.getByRole("button", { name: /remove/i })); fireEvent.click(screen.getByRole("button", { name: /remove/i }));
@@ -204,10 +210,12 @@ describe("CalendarSelection", () => {
const addButtons = screen.getAllByTestId("AddIcon"); const addButtons = screen.getAllByTestId("AddIcon");
fireEvent.click(addButtons[1]); // seccond Add button (other) fireEvent.click(addButtons[1]); // seccond Add button (other)
expect(screen.getByText("Browse other calendars")).toBeInTheDocument(); expect(
screen.getByText("calendar.browseOtherCalendars")
).toBeInTheDocument();
}); });
it("when only personnal calendars are in the state, only personnal calendars and the title for other to be added are shown", () => { it("when only calendar.personal are in the state, only calendar.personal and the title for other to be added are shown", () => {
renderWithProviders( renderWithProviders(
<CalendarSelection <CalendarSelection
selectedCalendars={[]} selectedCalendars={[]}
@@ -224,9 +232,9 @@ describe("CalendarSelection", () => {
} }
); );
expect(screen.getByText("Personnal Calendars")).toBeInTheDocument(); expect(screen.getByText("calendar.personal")).toBeInTheDocument();
expect(screen.queryByText("Delegated Calendars")).not.toBeInTheDocument(); expect(screen.queryByText("calendar.delegated")).not.toBeInTheDocument();
expect(screen.queryByText("Other Calendars")).toBeInTheDocument(); expect(screen.queryByText("calendar.other")).toBeInTheDocument();
}); });
it("renders nothing when no calendars are present", () => { it("renders nothing when no calendars are present", () => {
@@ -256,7 +264,7 @@ describe("CalendarSelection", () => {
} }
); );
const delegatedAccordionSummary = screen const delegatedAccordionSummary = screen
.getByText("Delegated Calendars") .getByText("calendar.delegated")
.closest(".MuiAccordionSummary-root"); .closest(".MuiAccordionSummary-root");
fireEvent.click(delegatedAccordionSummary!); fireEvent.click(delegatedAccordionSummary!);
+12 -7
View File
@@ -1,6 +1,5 @@
import { fireEvent, screen, waitFor } from "@testing-library/react"; import { fireEvent, screen, waitFor } from "@testing-library/react";
import EventDuplication from "../../src/components/Event/EventDuplicate"; import EventDuplication from "../../src/components/Event/EventDuplicate";
import EventDisplayModal from "../../src/features/Events/EventDisplay";
import EventPopover from "../../src/features/Events/EventModal"; import EventPopover from "../../src/features/Events/EventModal";
import { renderWithProviders } from "../utils/Renderwithproviders"; import { renderWithProviders } from "../utils/Renderwithproviders";
import EventPreviewModal from "../../src/features/Events/EventDisplayPreview"; import EventPreviewModal from "../../src/features/Events/EventDisplayPreview";
@@ -31,8 +30,8 @@ const preloadedState = {
URL: "calendars/667037022b752d0026472254/cal1/event1.ics", URL: "calendars/667037022b752d0026472254/cal1/event1.ics",
title: "Test Event", title: "Test Event",
calId: "667037022b752d0026472254/cal1", calId: "667037022b752d0026472254/cal1",
start: day, start: day.toISOString(),
end: day, end: day.toISOString(),
timezone: "UTC", timezone: "UTC",
organizer: { cn: "test", cal_address: "test@test.com" }, organizer: { cn: "test", cal_address: "test@test.com" },
attendee: [ attendee: [
@@ -77,7 +76,9 @@ describe("EventDuplication", () => {
preloadedState preloadedState
); );
fireEvent.click(screen.getByRole("menuitem", { name: /Duplicate event/i })); fireEvent.click(
screen.getByRole("menuitem", { name: "eventDuplication.duplicateEvent" })
);
expect(onOpenDuplicate).toHaveBeenCalled(); expect(onOpenDuplicate).toHaveBeenCalled();
}); });
@@ -126,7 +127,7 @@ describe("EventPopover", () => {
fireEvent.change(screen.getByLabelText(/Title/i), { fireEvent.change(screen.getByLabelText(/Title/i), {
target: { value: "Duplicated Event" }, target: { value: "Duplicated Event" },
}); });
fireEvent.click(screen.getByRole("button", { name: /Save/i })); fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
waitFor(() => expect(onClose).toHaveBeenCalled()); waitFor(() => expect(onClose).toHaveBeenCalled());
}); });
@@ -145,8 +146,12 @@ describe("EventDisplayModal", () => {
); );
fireEvent.click(screen.getByTestId("MoreVertIcon")); fireEvent.click(screen.getByTestId("MoreVertIcon"));
fireEvent.click(screen.getByRole("menuitem", { name: /Duplicate event/i })); fireEvent.click(
expect(screen.getAllByText(/Duplicate Event/i)[1]).toBeInTheDocument(); screen.getByRole("menuitem", { name: "eventDuplication.duplicateEvent" })
);
expect(
screen.getAllByText("eventDuplication.duplicateEvent")[1]
).toBeInTheDocument();
expect( expect(
screen.getByDisplayValue( screen.getByDisplayValue(
preloadedState.calendars.list["667037022b752d0026472254/cal1"].events preloadedState.calendars.list["667037022b752d0026472254/cal1"].events
+2 -2
View File
@@ -34,7 +34,7 @@ describe("Calendar App Component Display Tests", () => {
/>, />,
preloadedState preloadedState
); );
const logoElement = screen.getByAltText("Calendar"); const logoElement = screen.getByAltText("menubar.logoAlt");
expect(logoElement).toBeInTheDocument(); expect(logoElement).toBeInTheDocument();
}); });
it("renders the main title", () => { it("renders the main title", () => {
@@ -52,7 +52,7 @@ describe("Calendar App Component Display Tests", () => {
/>, />,
preloadedState preloadedState
); );
expect(screen.getByAltText("Calendar")).toBeInTheDocument(); expect(screen.getByAltText("menubar.logoAlt")).toBeInTheDocument();
}); });
it("shows avatar with user initials", () => { it("shows avatar with user initials", () => {
+39 -21
View File
@@ -99,20 +99,22 @@ async function setupEventPopover(
); );
// Fill in title // Fill in title
const titleInput = screen.getByLabelText("Title"); const titleInput = screen.getByLabelText("event.form.title");
fireEvent.change(titleInput, { target: { value: "Meeting" } }); fireEvent.change(titleInput, { target: { value: "Meeting" } });
// Click More options to expand the dialog // Click More options to expand the dialog
const showMoreButton = screen.getByRole("button", { name: /More options/i }); const showMoreButton = screen.getByRole("button", {
name: "common.moreOptions",
});
fireEvent.click(showMoreButton); fireEvent.click(showMoreButton);
// Check Repeat checkbox to show repeat options // Check Repeat checkbox to show repeat options
const repeatCheckbox = screen.getByLabelText("Repeat"); const repeatCheckbox = screen.getByLabelText("event.form.repeat");
fireEvent.click(repeatCheckbox); fireEvent.click(repeatCheckbox);
// Wait for RepeatEvent component to be rendered // Wait for RepeatEvent component to be rendered
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Day(s)")).toBeInTheDocument(); expect(screen.getByText("event.repeat.frequency.days")).toBeInTheDocument();
}); });
} }
@@ -154,7 +156,7 @@ describe("RepeatEvent Component", () => {
fireEvent.mouseDown(frequencySelect); fireEvent.mouseDown(frequencySelect);
// Select Week(s) // Select Week(s)
const weeklyOption = screen.getByText("Week(s)"); const weeklyOption = screen.getByText("event.repeat.frequency.weeks");
fireEvent.click(weeklyOption); fireEvent.click(weeklyOption);
expect(setRepetition).toHaveBeenCalledWith( expect(setRepetition).toHaveBeenCalledWith(
@@ -183,7 +185,7 @@ describe("RepeatEvent Component", () => {
it("toggles day selection for weekly frequency", () => { it("toggles day selection for weekly frequency", () => {
const { setRepetition } = setupRepeatEvent({ freq: "weekly" }); const { setRepetition } = setupRepeatEvent({ freq: "weekly" });
const mondayCheckbox = screen.getByLabelText("MO"); const mondayCheckbox = screen.getByLabelText("event.repeat.days.monday");
fireEvent.click(mondayCheckbox); fireEvent.click(mondayCheckbox);
expect(setRepetition).toHaveBeenCalledWith( expect(setRepetition).toHaveBeenCalledWith(
@@ -203,9 +205,11 @@ describe("Repeat Event Integration Tests", () => {
// When Repeat checkbox is checked, repetition is set to empty object // When Repeat checkbox is checked, repetition is set to empty object
// We need to set the frequency manually // We need to set the frequency manually
const frequencySelect = screen.getByText("Day(s)"); const frequencySelect = screen.getByText("event.repeat.frequency.days");
fireEvent.mouseDown(frequencySelect); fireEvent.mouseDown(frequencySelect);
const dailyOption = screen.getByRole("option", { name: "Day(s)" }); const dailyOption = screen.getByRole("option", {
name: "event.repeat.frequency.days",
});
fireEvent.click(dailyOption); fireEvent.click(dailyOption);
await expectRRule({ freq: "daily", interval: 1 }); await expectRRule({ freq: "daily", interval: 1 });
@@ -259,13 +263,17 @@ describe("Repeat Event Integration Tests", () => {
await setupEventPopover(); await setupEventPopover();
// Select Week(s) frequency // Select Week(s) frequency
const frequencySelect = screen.getByText("Day(s)"); const frequencySelect = screen.getByText("event.repeat.frequency.days");
fireEvent.mouseDown(frequencySelect); fireEvent.mouseDown(frequencySelect);
const weeklyOption = screen.getByRole("option", { name: "Week(s)" }); const weeklyOption = screen.getByRole("option", {
name: "event.repeat.frequency.weeks",
});
fireEvent.click(weeklyOption); fireEvent.click(weeklyOption);
// Select Thursday // Select Thursday
const thursdayCheckbox = screen.getByLabelText("TH"); const thursdayCheckbox = screen.getByLabelText(
"event.repeat.days.thursday"
);
fireEvent.click(thursdayCheckbox); fireEvent.click(thursdayCheckbox);
await expectRRule({ await expectRRule({
@@ -280,9 +288,11 @@ describe("Repeat Event Integration Tests", () => {
await setupEventPopover(); await setupEventPopover();
// Select Week(s) frequency // Select Week(s) frequency
const frequencySelect = screen.getByText("Day(s)"); const frequencySelect = screen.getByText("event.repeat.frequency.days");
fireEvent.mouseDown(frequencySelect); fireEvent.mouseDown(frequencySelect);
const weeklyOption = screen.getByRole("option", { name: "Week(s)" }); const weeklyOption = screen.getByRole("option", {
name: "event.repeat.frequency.weeks",
});
fireEvent.click(weeklyOption); fireEvent.click(weeklyOption);
// Set interval to 3 // Set interval to 3
@@ -297,9 +307,11 @@ describe("Repeat Event Integration Tests", () => {
await setupEventPopover(); await setupEventPopover();
// Select Month(s) frequency // Select Month(s) frequency
const frequencySelect = screen.getByText("Day(s)"); const frequencySelect = screen.getByText("event.repeat.frequency.days");
fireEvent.mouseDown(frequencySelect); fireEvent.mouseDown(frequencySelect);
const monthlyOption = screen.getByRole("option", { name: "Month(s)" }); const monthlyOption = screen.getByRole("option", {
name: "event.repeat.frequency.months",
});
fireEvent.click(monthlyOption); fireEvent.click(monthlyOption);
await expectRRule({ freq: "monthly", interval: 1 }); await expectRRule({ freq: "monthly", interval: 1 });
@@ -310,9 +322,11 @@ describe("Repeat Event Integration Tests", () => {
await setupEventPopover(); await setupEventPopover();
// Select Month(s) frequency // Select Month(s) frequency
const frequencySelect = screen.getByText("Day(s)"); const frequencySelect = screen.getByText("event.repeat.frequency.days");
fireEvent.mouseDown(frequencySelect); fireEvent.mouseDown(frequencySelect);
const monthlyOption = screen.getByRole("option", { name: "Month(s)" }); const monthlyOption = screen.getByRole("option", {
name: "event.repeat.frequency.months",
});
fireEvent.click(monthlyOption); fireEvent.click(monthlyOption);
// Select "After" end option // Select "After" end option
@@ -331,9 +345,11 @@ describe("Repeat Event Integration Tests", () => {
await setupEventPopover(); await setupEventPopover();
// Select Year(s) frequency // Select Year(s) frequency
const frequencySelect = screen.getByText("Day(s)"); const frequencySelect = screen.getByText("event.repeat.frequency.days");
fireEvent.mouseDown(frequencySelect); fireEvent.mouseDown(frequencySelect);
const yearlyOption = screen.getByRole("option", { name: "Year(s)" }); const yearlyOption = screen.getByRole("option", {
name: "event.repeat.frequency.years",
});
fireEvent.click(yearlyOption); fireEvent.click(yearlyOption);
await expectRRule({ freq: "yearly", interval: 1 }); await expectRRule({ freq: "yearly", interval: 1 });
@@ -344,9 +360,11 @@ describe("Repeat Event Integration Tests", () => {
await setupEventPopover(); await setupEventPopover();
// Select Year(s) frequency // Select Year(s) frequency
const frequencySelect = screen.getByText("Day(s)"); const frequencySelect = screen.getByText("event.repeat.frequency.days");
fireEvent.mouseDown(frequencySelect); fireEvent.mouseDown(frequencySelect);
const yearlyOption = screen.getByRole("option", { name: "Year(s)" }); const yearlyOption = screen.getByRole("option", {
name: "event.repeat.frequency.years",
});
fireEvent.click(yearlyOption); fireEvent.click(yearlyOption);
// First choose "After" with 5 occurrences // First choose "After" with 5 occurrences
@@ -33,7 +33,7 @@ describe("CalendarPopover", () => {
renderPopover(); renderPopover();
expect(screen.getByLabelText(/Name/i)).toBeInTheDocument(); expect(screen.getByLabelText(/Name/i)).toBeInTheDocument();
expect(screen.getByText(/add description/i)).toBeInTheDocument(); expect(screen.getByText("calendar.addDescription")).toBeInTheDocument();
}); });
it("updates name and description fields", () => { it("updates name and description fields", () => {
@@ -43,7 +43,7 @@ describe("CalendarPopover", () => {
fireEvent.change(nameInput, { target: { value: "My Calendar" } }); fireEvent.change(nameInput, { target: { value: "My Calendar" } });
expect(nameInput).toHaveValue("My Calendar"); expect(nameInput).toHaveValue("My Calendar");
fireEvent.click(screen.getByText(/add description/i)); fireEvent.click(screen.getByText("calendar.addDescription"));
const descInput = screen.getByLabelText(/Description/i); const descInput = screen.getByLabelText(/Description/i);
fireEvent.change(descInput, { target: { value: "Test description" } }); fireEvent.change(descInput, { target: { value: "Test description" } });
expect(descInput).toHaveValue("Test description"); expect(descInput).toHaveValue("Test description");
@@ -60,7 +60,7 @@ describe("CalendarPopover", () => {
fireEvent.change(screen.getByLabelText(/Name/i), { fireEvent.change(screen.getByLabelText(/Name/i), {
target: { value: "Test Calendar" }, target: { value: "Test Calendar" },
}); });
fireEvent.click(screen.getByText(/add description/i)); fireEvent.click(screen.getByText("calendar.addDescription"));
fireEvent.change(screen.getByLabelText(/Description/i), { fireEvent.change(screen.getByLabelText(/Description/i), {
target: { value: "Test Description" }, target: { value: "Test Description" },
}); });
@@ -168,7 +168,7 @@ describe("CalendarPopover (editing mode)", () => {
}); });
// Save // Save
fireEvent.click(screen.getByRole("button", { name: /Save/i })); fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
await waitFor(() => await waitFor(() =>
expect(spy).toHaveBeenCalledWith( expect(spy).toHaveBeenCalledWith(
@@ -291,7 +291,7 @@ describe("CalendarPopover - Tabs Scenarios", () => {
expect(publicButton).toHaveAttribute("aria-pressed", "false"); expect(publicButton).toHaveAttribute("aria-pressed", "false");
// Save // Save
fireEvent.click(screen.getByRole("button", { name: /Save/i })); fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
await waitFor(() => await waitFor(() =>
expect(patchSpy).toHaveBeenCalledWith( expect(patchSpy).toHaveBeenCalledWith(
@@ -322,7 +322,7 @@ describe("CalendarPopover - Tabs Scenarios", () => {
fireEvent.click(screen.getByRole("tab", { name: /Access/i })); fireEvent.click(screen.getByRole("tab", { name: /Access/i }));
// Expect text field with caldav link // Expect text field with caldav link
const input = screen.getByLabelText(/CalDAV access/i); const input = screen.getByLabelText("calendar.caldav_access");
expect(input).toHaveValue("https://cal.example.org/calendars/user1/cal1"); expect(input).toHaveValue("https://cal.example.org/calendars/user1/cal1");
// Click copy button (find button containing ContentCopyIcon) // Click copy button (find button containing ContentCopyIcon)
@@ -338,7 +338,7 @@ describe("CalendarPopover - Tabs Scenarios", () => {
// Snackbar should appear // Snackbar should appear
await waitFor(() => await waitFor(() =>
expect(screen.getByText(/Link copied!/i)).toBeInTheDocument() expect(screen.getByText("common.link_copied")).toBeInTheDocument()
); );
}); });
@@ -371,11 +371,11 @@ describe("CalendarPopover - Tabs Scenarios", () => {
fireEvent.change(screen.getByLabelText(/Name/i), { fireEvent.change(screen.getByLabelText(/Name/i), {
target: { value: "Imported Calendar" }, target: { value: "Imported Calendar" },
}); });
const fileInput = screen.getByLabelText(/select file/i); const fileInput = screen.getByLabelText("common.select_file");
fireEvent.change(fileInput, { target: { files: [file] } }); fireEvent.change(fileInput, { target: { files: [file] } });
// Click Import // Click Import
fireEvent.click(screen.getByRole("button", { name: "Import" })); fireEvent.click(screen.getByRole("button", { name: "actions.import" }));
await waitFor(() => expect(createSpy).toHaveBeenCalled()); await waitFor(() => expect(createSpy).toHaveBeenCalled());
await waitFor(() => expect(importSpy).toHaveBeenCalled()); await waitFor(() => expect(importSpy).toHaveBeenCalled());
@@ -402,10 +402,10 @@ describe("CalendarPopover - Tabs Scenarios", () => {
); );
fireEvent.click(screen.getByRole("tab", { name: /Import/i })); fireEvent.click(screen.getByRole("tab", { name: /Import/i }));
const fileInput = screen.getByLabelText(/select file/i); const fileInput = screen.getByLabelText("common.select_file");
fireEvent.change(fileInput, { target: { files: [file] } }); fireEvent.change(fileInput, { target: { files: [file] } });
fireEvent.click(screen.getByRole("button", { name: "Import" })); fireEvent.click(screen.getByRole("button", { name: "actions.import" }));
await waitFor(() => await waitFor(() =>
expect(importSpy).toHaveBeenCalledWith( expect(importSpy).toHaveBeenCalledWith(
@@ -427,7 +427,9 @@ describe("CalendarPopover - Tabs Scenarios", () => {
fireEvent.click(screen.getByRole("tab", { name: /Import/i })); fireEvent.click(screen.getByRole("tab", { name: /Import/i }));
const importButton = screen.getByRole("button", { name: /Import/i }); const importButton = screen.getByRole("button", {
name: "actions.import",
});
expect(importButton).toBeDisabled(); expect(importButton).toBeDisabled();
}); });
}); });
File diff suppressed because it is too large Load Diff
+34 -44
View File
@@ -110,7 +110,7 @@ describe("EventPopover", () => {
renderPopover(); renderPopover();
// Check dialog title // Check dialog title
expect(screen.getByText("Create Event")).toBeInTheDocument(); expect(screen.getByText("event.createEvent")).toBeInTheDocument();
// Check inputs exist by their roles // Check inputs exist by their roles
const titleInput = screen.getByRole("textbox", { name: /title/i }); const titleInput = screen.getByRole("textbox", { name: /title/i });
@@ -118,7 +118,7 @@ describe("EventPopover", () => {
// Description input is only visible after clicking "Add description" button // Description input is only visible after clicking "Add description" button
const addDescriptionButton = screen.getByRole("button", { const addDescriptionButton = screen.getByRole("button", {
name: /Add description/i, name: "event.form.addDescription",
}); });
expect(addDescriptionButton).toBeInTheDocument(); expect(addDescriptionButton).toBeInTheDocument();
@@ -127,20 +127,20 @@ describe("EventPopover", () => {
// Check button // Check button
expect( expect(
screen.getByRole("button", { name: /More options/i }) screen.getByRole("button", { name: "common.moreOptions" })
).toBeInTheDocument(); ).toBeInTheDocument();
// Extended mode // Extended mode
fireEvent.click(screen.getByRole("button", { name: /More options/i })); fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
// Back button appears // Back button appears
expect(screen.getByLabelText("show less")).toBeInTheDocument(); expect(screen.getByLabelText("show less")).toBeInTheDocument();
// Extended labels appear // Extended labels appear
expect(screen.getAllByText("Repeat")).toHaveLength(1); expect(screen.getAllByText("event.form.repeat")).toHaveLength(1);
expect(screen.getAllByText("Notification")).toHaveLength(1); expect(screen.getAllByText("event.form.notification")).toHaveLength(1);
expect(screen.getAllByText("Visible to")).toHaveLength(1); expect(screen.getAllByText("event.form.visibleTo")).toHaveLength(1);
expect(screen.getAllByText("Show me as")).toHaveLength(1); expect(screen.getAllByText("event.form.showMeAs")).toHaveLength(1);
}); });
it("fills start from selectedRange", () => { it("fills start from selectedRange", () => {
@@ -160,31 +160,35 @@ describe("EventPopover", () => {
it("updates inputs on change", () => { it("updates inputs on change", () => {
renderPopover(); renderPopover();
fireEvent.change(screen.getByLabelText("Title"), { fireEvent.change(screen.getByLabelText("event.form.title"), {
target: { value: "My Event" }, target: { value: "My Event" },
}); });
expect(screen.getByLabelText("Title")).toHaveValue("My Event"); expect(screen.getByLabelText("event.form.title")).toHaveValue("My Event");
// Click "Add description" button first // Click "Add description" button first
fireEvent.click(screen.getByRole("button", { name: /Add description/i })); fireEvent.click(
screen.getByRole("button", { name: "event.form.addDescription" })
);
fireEvent.change(screen.getByLabelText("Description"), { fireEvent.change(screen.getByLabelText("event.form.description"), {
target: { value: "Event Description" }, target: { value: "Event Description" },
}); });
expect(screen.getByLabelText("Description")).toHaveValue( expect(screen.getByLabelText("event.form.description")).toHaveValue(
"Event Description" "Event Description"
); );
fireEvent.change(screen.getByLabelText("Location"), { fireEvent.change(screen.getByLabelText("event.form.location"), {
target: { value: "Conference Room" }, target: { value: "Conference Room" },
}); });
expect(screen.getByLabelText("Location")).toHaveValue("Conference Room"); expect(screen.getByLabelText("event.form.location")).toHaveValue(
"Conference Room"
);
}); });
it("changes selected calendar", async () => { it("changes selected calendar", async () => {
renderPopover(); renderPopover();
const select = screen.getByLabelText("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");
@@ -197,10 +201,10 @@ describe("EventPopover", () => {
it("adds a attendee", async () => { it("adds a attendee", async () => {
jest.useFakeTimers(); jest.useFakeTimers();
renderPopover(); renderPopover();
fireEvent.change(screen.getByLabelText("Title"), { fireEvent.change(screen.getByLabelText("event.form.title"), {
target: { value: "newEvent" }, target: { value: "newEvent" },
}); });
const select = screen.getByLabelText("Start typing a name or email"); const select = screen.getByLabelText("peopleSearch.label");
act(() => { act(() => {
select.focus(); select.focus();
@@ -225,7 +229,7 @@ describe("EventPopover", () => {
return () => Promise.resolve(payload) as any; return () => Promise.resolve(payload) as any;
}); });
fireEvent.click(screen.getByRole("button", { name: /Save/i })); fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
await waitFor(() => { await waitFor(() => {
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
@@ -262,8 +266,6 @@ describe("EventPopover", () => {
renderPopover(); renderPopover();
const newEvent = { const newEvent = {
title: "Meeting", title: "Meeting",
start: "2025-07-18T00:00:00.000Z",
end: "2025-07-18T00:00:00.000Z",
allday: false, allday: false,
uid: "6045c603-11ab-43c5-bc30-0641420bb3a8", uid: "6045c603-11ab-43c5-bc30-0641420bb3a8",
description: "Discuss project", description: "Discuss project",
@@ -274,17 +276,17 @@ describe("EventPopover", () => {
}; };
// Fill inputs // Fill inputs
fireEvent.change(screen.getByLabelText("Title"), { fireEvent.change(screen.getByLabelText("event.form.title"), {
target: { value: newEvent.title }, target: { value: newEvent.title },
}); });
fireEvent.click(screen.getByLabelText("All day"));
// Click "Add description" button first // Click "Add description" button first
fireEvent.click(screen.getByRole("button", { name: /Add description/i })); fireEvent.click(
fireEvent.change(screen.getByLabelText("Description"), { screen.getByRole("button", { name: "event.form.addDescription" })
);
fireEvent.change(screen.getByLabelText("event.form.description"), {
target: { value: newEvent.description }, target: { value: newEvent.description },
}); });
fireEvent.change(screen.getByLabelText("Location"), { fireEvent.change(screen.getByLabelText("event.form.location"), {
target: { value: newEvent.location }, target: { value: newEvent.location },
}); });
const spy = jest const spy = jest
@@ -293,7 +295,7 @@ describe("EventPopover", () => {
return () => Promise.resolve(payload) as any; return () => Promise.resolve(payload) as any;
}); });
fireEvent.click(screen.getByRole("button", { name: /Save/i })); fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
await waitFor(() => { await waitFor(() => {
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
@@ -307,20 +309,6 @@ describe("EventPopover", () => {
expect(receivedPayload.newEvent.title).toBe(newEvent.title); expect(receivedPayload.newEvent.title).toBe(newEvent.title);
expect(receivedPayload.newEvent.description).toBe(newEvent.description); expect(receivedPayload.newEvent.description).toBe(newEvent.description);
expect(
formatDateToYYYYMMDDTHHMMSS(
new Date(receivedPayload.newEvent.start)
).split("T")[0]
).toBe(formatDateToYYYYMMDDTHHMMSS(new Date(newEvent.start)).split("T")[0]);
const expectedAllDayEnd = new Date(newEvent.end);
expectedAllDayEnd.setUTCDate(expectedAllDayEnd.getUTCDate() + 1);
expect(
formatDateToYYYYMMDDTHHMMSS(
new Date(receivedPayload.newEvent.end || new Date())
).split("T")[0]
).toBe(formatDateToYYYYMMDDTHHMMSS(expectedAllDayEnd).split("T")[0]);
expect(receivedPayload.newEvent.location).toBe(newEvent.location); expect(receivedPayload.newEvent.location).toBe(newEvent.location);
expect(receivedPayload.newEvent.organizer).toEqual(newEvent.organizer); expect(receivedPayload.newEvent.organizer).toEqual(newEvent.organizer);
expect(receivedPayload.newEvent.color).toEqual( expect(receivedPayload.newEvent.color).toEqual(
@@ -335,7 +323,7 @@ describe("EventPopover", () => {
renderPopover(); renderPopover();
// Cancel button only appears in expanded mode // Cancel button only appears in expanded mode
fireEvent.click(screen.getByRole("button", { name: /More options/i })); fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
fireEvent.click(screen.getByRole("button", { name: /Cancel/i })); fireEvent.click(screen.getByRole("button", { name: /Cancel/i }));
@@ -345,7 +333,9 @@ describe("EventPopover", () => {
it("BUGFIX: Prefill Calendar field", async () => { it("BUGFIX: Prefill Calendar field", async () => {
renderPopover(); renderPopover();
await waitFor(() => await waitFor(() =>
expect(screen.getByLabelText("Calendar")).toHaveTextContent("Calendar 1") expect(screen.getByLabelText("event.form.calendar")).toHaveTextContent(
"Calendar 1"
)
); );
}); });
}); });
File diff suppressed because it is too large Load Diff
@@ -279,7 +279,7 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
}); });
// Uncheck repeat checkbox // Uncheck repeat checkbox
const repeatCheckbox = screen.getByLabelText("Repeat"); const repeatCheckbox = screen.getByLabelText("event.form.repeat");
expect(repeatCheckbox).toBeChecked(); expect(repeatCheckbox).toBeChecked();
await act(async () => { await act(async () => {
@@ -289,7 +289,7 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
expect(repeatCheckbox).not.toBeChecked(); expect(repeatCheckbox).not.toBeChecked();
// Click Save button // Click Save button
const saveButton = screen.getByRole("button", { name: /Save/i }); const saveButton = screen.getByRole("button", { name: "actions.save" });
await act(async () => { await act(async () => {
fireEvent.click(saveButton); fireEvent.click(saveButton);
@@ -397,13 +397,13 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
}); });
// Uncheck repeat checkbox and save // Uncheck repeat checkbox and save
const repeatCheckbox = screen.getByLabelText("Repeat"); const repeatCheckbox = screen.getByLabelText("event.form.repeat");
await act(async () => { await act(async () => {
fireEvent.click(repeatCheckbox); fireEvent.click(repeatCheckbox);
}); });
const saveButton = screen.getByRole("button", { name: /Save/i }); const saveButton = screen.getByRole("button", { name: "actions.save" });
await act(async () => { await act(async () => {
fireEvent.click(saveButton); fireEvent.click(saveButton);
@@ -512,13 +512,13 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
}); });
// Uncheck repeat and save // Uncheck repeat and save
const repeatCheckbox = screen.getByLabelText("Repeat"); const repeatCheckbox = screen.getByLabelText("event.form.repeat");
await act(async () => { await act(async () => {
fireEvent.click(repeatCheckbox); fireEvent.click(repeatCheckbox);
}); });
const saveButton = screen.getByRole("button", { name: /Save/i }); const saveButton = screen.getByRole("button", { name: "actions.save" });
await act(async () => { await act(async () => {
fireEvent.click(saveButton); fireEvent.click(saveButton);
+13 -15
View File
@@ -138,10 +138,10 @@ describe("Update tempcalendars called with correct params", () => {
await waitFor(() => { await waitFor(() => {
fireEvent.click(screen.getByTestId("MoreVertIcon")); fireEvent.click(screen.getByTestId("MoreVertIcon"));
expect(screen.getByText("Delete event")).toBeInTheDocument(); expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument();
}); });
const deleteMenuItem = screen.getByText("Delete event"); const deleteMenuItem = screen.getByText("eventPreview.deleteEvent");
fireEvent.click(deleteMenuItem); fireEvent.click(deleteMenuItem);
await waitFor(() => await waitFor(() =>
@@ -191,10 +191,10 @@ describe("Update tempcalendars called with correct params", () => {
await waitFor(() => { await waitFor(() => {
fireEvent.click(screen.getByTestId("MoreVertIcon")); fireEvent.click(screen.getByTestId("MoreVertIcon"));
expect(screen.getByText("Delete event")).toBeInTheDocument(); expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument();
}); });
const deleteMenuItem = screen.getByText("Delete event"); const deleteMenuItem = screen.getByText("eventPreview.deleteEvent");
fireEvent.click(deleteMenuItem); fireEvent.click(deleteMenuItem);
await waitFor(() => { await waitFor(() => {
@@ -233,9 +233,7 @@ describe("Update tempcalendars called with correct params", () => {
const titleInput = screen.getByLabelText(/title/i); const titleInput = screen.getByLabelText(/title/i);
fireEvent.change(titleInput, { target: { value: "New Event" } }); fireEvent.change(titleInput, { target: { value: "New Event" } });
const attendeeInput = screen.getByLabelText( const attendeeInput = screen.getByLabelText(/peopleSearch.label/i);
/Start typing a name or email/i
);
fireEvent.change(attendeeInput, { target: { value: "attendee@test.com" } }); fireEvent.change(attendeeInput, { target: { value: "attendee@test.com" } });
fireEvent.keyDown(attendeeInput, { key: "Enter", code: "Enter" }); fireEvent.keyDown(attendeeInput, { key: "Enter", code: "Enter" });
@@ -559,10 +557,10 @@ describe("Update tempcalendars called with correct params", () => {
await waitFor(() => { await waitFor(() => {
fireEvent.click(screen.getByTestId("MoreVertIcon")); fireEvent.click(screen.getByTestId("MoreVertIcon"));
expect(screen.getByText("Delete event")).toBeInTheDocument(); expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument();
}); });
const deleteMenuItem = screen.getByText("Delete event"); const deleteMenuItem = screen.getByText("eventPreview.deleteEvent");
fireEvent.click(deleteMenuItem); fireEvent.click(deleteMenuItem);
await waitFor(() => { await waitFor(() => {
@@ -597,10 +595,10 @@ describe("Update tempcalendars called with correct params", () => {
await waitFor(() => { await waitFor(() => {
fireEvent.click(screen.getByTestId("MoreVertIcon")); fireEvent.click(screen.getByTestId("MoreVertIcon"));
expect(screen.getByText("Delete event")).toBeInTheDocument(); expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument();
}); });
const deleteMenuItem = screen.getByText("Delete event"); const deleteMenuItem = screen.getByText("eventPreview.deleteEvent");
fireEvent.click(deleteMenuItem); fireEvent.click(deleteMenuItem);
await waitFor(() => { await waitFor(() => {
@@ -673,10 +671,10 @@ describe("Update tempcalendars called with correct params", () => {
await waitFor(() => { await waitFor(() => {
fireEvent.click(screen.getByTestId("MoreVertIcon")); fireEvent.click(screen.getByTestId("MoreVertIcon"));
expect(screen.getByText("Delete event")).toBeInTheDocument(); expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument();
}); });
const deleteMenuItem = screen.getByText("Delete event"); const deleteMenuItem = screen.getByText("eventPreview.deleteEvent");
fireEvent.click(deleteMenuItem); fireEvent.click(deleteMenuItem);
await waitFor(() => { await waitFor(() => {
@@ -767,10 +765,10 @@ describe("Update tempcalendars called with correct params", () => {
await waitFor(() => { await waitFor(() => {
fireEvent.click(screen.getByTestId("MoreVertIcon")); fireEvent.click(screen.getByTestId("MoreVertIcon"));
expect(screen.getByText("Delete event")).toBeInTheDocument(); expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument();
}); });
const deleteMenuItem = screen.getByText("Delete event"); const deleteMenuItem = screen.getByText("eventPreview.deleteEvent");
fireEvent.click(deleteMenuItem); fireEvent.click(deleteMenuItem);
await waitFor(() => { await waitFor(() => {
+19 -6
View File
@@ -1,13 +1,11 @@
import type { RenderOptions } from "@testing-library/react"; import type { RenderOptions } from "@testing-library/react";
import { render } from "@testing-library/react"; import { render } from "@testing-library/react";
import { I18nContext } from "cozy-ui/transpiled/react/providers/I18n";
import React, { PropsWithChildren } from "react"; import React, { PropsWithChildren } from "react";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { MemoryRouter } from "react-router-dom"; import { MemoryRouter } from "react-router-dom";
import type { AppStore, RootState } from "../../src/app/store"; import type { AppStore, RootState } from "../../src/app/store";
import { setupStore } from "../../src/app/store"; import { setupStore } from "../../src/app/store";
import { userData, userOrganiser } from "../../src/features/User/userDataTypes";
interface ExtendedRenderOptions extends Omit<RenderOptions, "queries"> { interface ExtendedRenderOptions extends Omit<RenderOptions, "queries"> {
preloadedState?: Partial<RootState>; preloadedState?: Partial<RootState>;
store?: AppStore; store?: AppStore;
@@ -23,9 +21,24 @@ export function renderWithProviders(
const Wrapper = ({ children }: PropsWithChildren) => { const Wrapper = ({ children }: PropsWithChildren) => {
return ( return (
<MemoryRouter initialEntries={["/"]}> <I18nContext.Provider
<Provider store={store}>{children}</Provider> value={{
</MemoryRouter> t: (key: string, vars?: Record<string, string>) => {
if (vars) {
const params = Object.entries(vars)
.map(([k, v]) => `${k}=${v}`)
.join(",");
return `${key}(${params})`;
}
return key;
},
f: (date: Date, formatStr: string) => date.toString(),
}}
>
<MemoryRouter initialEntries={["/"]}>
<Provider store={store}>{children}</Provider>
</MemoryRouter>
</I18nContext.Provider>
); );
}; };
+1 -1
View File
@@ -33,7 +33,7 @@ const config: Config = {
"<rootDir>/fileTransformer.ts", "<rootDir>/fileTransformer.ts",
}, },
transformIgnorePatterns: [ transformIgnorePatterns: [
"/node_modules/(?!(preact|@fullcalendar|react-calendar|get-user-locale|memoize|mimic-function|@wojtekmaj|ky)/)", "/node_modules/(?!(preact|@fullcalendar|react-calendar|get-user-locale|memoize|mimic-function|@wojtekmaj|ky|cozy-ui)/)",
], ],
moduleNameMapper: { "^preact(/(.*)|$)": "preact$1" }, moduleNameMapper: { "^preact(/(.*)|$)": "preact$1" },
+9607 -2405
View File
File diff suppressed because it is too large Load Diff
+18 -7
View File
@@ -16,23 +16,20 @@
"@mui/material": "^7.1.2", "@mui/material": "^7.1.2",
"@mui/x-date-pickers": "^8.14.0", "@mui/x-date-pickers": "^8.14.0",
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.8.2",
"@types/node": "^16.18.126", "cozy-ui": "^133.0.0-beta.1",
"@types/react": "^19.1.8", "dayjs": "^1.11.19",
"@types/react-dom": "^19.1.6",
"i18next": "^23.7.9", "i18next": "^23.7.9",
"ical.js": "^2.2.0", "ical.js": "^2.2.0",
"ky": "^1.8.1", "ky": "^1.8.1",
"moment": "^2.30.1", "moment": "^2.30.1",
"moment-timezone": "^0.5.48", "moment-timezone": "^0.5.48",
"openid-client": "^6.5.3", "openid-client": "^6.5.3",
"react": "^19.1.0", "react": "^18.2.0",
"react-colorful": "^5.6.1", "react-colorful": "^5.6.1",
"react-dom": "^19.1.0", "react-dom": "^18.2.0",
"react-i18next": "^13.5.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^6.23.1", "react-router-dom": "^6.23.1",
"redux-first-history": "^5.2.0", "redux-first-history": "^5.2.0",
"ts-node": "^10.9.2",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
@@ -71,6 +68,9 @@
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^20.19.24",
"@types/react": "^18.3.26",
"@types/react-dom": "^18.3.7",
"@typescript-eslint/eslint-plugin": "^8.44.1", "@typescript-eslint/eslint-plugin": "^8.44.1",
"@typescript-eslint/parser": "^8.44.1", "@typescript-eslint/parser": "^8.44.1",
"babel-jest": "^30.0.4", "babel-jest": "^30.0.4",
@@ -90,5 +90,16 @@
"prettier": "^3.6.2", "prettier": "^3.6.2",
"ts-jest": "^29.4.0", "ts-jest": "^29.4.0",
"typescript": "^4.9.5" "typescript": "^4.9.5"
},
"overrides": {
"@material-ui/core": "npm:@mui/material@latest",
"@material-ui/lab": "npm:@mui/lab@latest",
"@material-ui/styles": "npm:@mui/styles@latest",
"@material-ui/system": "npm:@mui/system@latest",
"@material-ui/pickers": "npm:@mui/x-date-pickers@latest",
"react-select": "^5.8.0",
"react-swipeable-views": "^0.14.0",
"react-markdown": "^9.0.0",
"@babel/runtime": "^7.26.10"
} }
} }
+1
View File
@@ -9,3 +9,4 @@ var CALENDAR_BASE_URL = "https://calendar.example.com";
var MAIL_SPA_URL = "https://mail.example.com"; var MAIL_SPA_URL = "https://mail.example.com";
var VIDEO_CONFERENCE_BASE_URL = "https://meet.linagora.com"; var VIDEO_CONFERENCE_BASE_URL = "https://meet.linagora.com";
var DEBUG = false; var DEBUG = false;
var LANG = "en";
+1
View File
@@ -21,5 +21,6 @@ export default defineConfig({
distPath: { distPath: {
root: "dist", root: "dist",
}, },
minify: false,
}, },
}); });
+25 -12
View File
@@ -12,29 +12,42 @@ import { CustomThemeProvider } from "./theme/ThemeProvider";
import { useAppDispatch, useAppSelector } from "./app/hooks"; import { useAppDispatch, useAppSelector } from "./app/hooks";
import { push } from "redux-first-history"; import { push } from "redux-first-history";
import { ErrorSnackbar } from "./components/Error/ErrorSnackbar"; import { ErrorSnackbar } from "./components/Error/ErrorSnackbar";
import I18n from "cozy-ui/transpiled/react/providers/I18n";
import en from "./locales/en.json";
import fr from "./locales/fr.json";
const locale = { en, fr };
function App() { function App() {
const error = useAppSelector((state) => state.user.error); const error = useAppSelector((state) => state.user.error);
const lang = useAppSelector((state) => state.settings.language);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
useEffect(() => { useEffect(() => {
if (error) { if (error) {
dispatch(push("/error")); dispatch(push("/error"));
} }
}); });
return ( return (
<CustomThemeProvider> <CustomThemeProvider>
<Suspense fallback={<Loading />}> <I18n
<Router history={history}> dictRequire={(lang: keyof typeof locale) => locale[lang]}
<Routes> lang={lang}
<Route path="/" element={<HandleLogin />} /> >
<Route path="/calendar" element={<CalendarLayout />} /> <Suspense fallback={<Loading />}>
<Route path="/callback" element={<CallbackResume />} /> <Router history={history}>
<Route path="/error" element={<Error />} /> <Routes>
</Routes> <Route path="/" element={<HandleLogin />} />
</Router> <Route path="/calendar" element={<CalendarLayout />} />
<ErrorSnackbar error={error} type="user" /> <Route path="/callback" element={<CallbackResume />} />
<ErrorSnackbar error={error} type="user" /> <Route path="/error" element={<Error />} />
</Suspense> </Routes>
</Router>
<ErrorSnackbar error={error} type="user" />
<ErrorSnackbar error={error} type="user" />
</Suspense>
</I18n>
</CustomThemeProvider> </CustomThemeProvider>
); );
} }
+2
View File
@@ -1,5 +1,6 @@
import { combineReducers, configureStore } from "@reduxjs/toolkit"; import { combineReducers, configureStore } from "@reduxjs/toolkit";
import userReducer from "../features/User/userSlice"; import userReducer from "../features/User/userSlice";
import settingsReducer from "../features/Settings/SettingsSlice";
import eventsCalendar from "../features/Calendars/CalendarSlice"; import eventsCalendar from "../features/Calendars/CalendarSlice";
import { createReduxHistoryContext } from "redux-first-history"; import { createReduxHistoryContext } from "redux-first-history";
import { createBrowserHistory } from "history"; import { createBrowserHistory } from "history";
@@ -11,6 +12,7 @@ const rootReducer = combineReducers({
router: routerReducer, router: routerReducer,
user: userReducer, user: userReducer,
calendars: eventsCalendar, calendars: eventsCalendar,
settings: settingsReducer,
}); });
export const setupStore = (preloadedState?: Partial<RootState>) => { export const setupStore = (preloadedState?: Partial<RootState>) => {
+6 -4
View File
@@ -11,6 +11,7 @@ import PeopleOutlineOutlinedIcon from "@mui/icons-material/PeopleOutlineOutlined
import Chip from "@mui/material/Chip"; import Chip from "@mui/material/Chip";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import { getAccessiblePair } from "../Calendar/utils/calendarColorsUtils"; import { getAccessiblePair } from "../Calendar/utils/calendarColorsUtils";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
export interface User { export interface User {
email: string; email: string;
@@ -35,6 +36,7 @@ export function PeopleSearch({
freeSolo?: boolean; freeSolo?: boolean;
onToggleEventPreview?: Function; onToggleEventPreview?: Function;
}) { }) {
const { t } = useI18n();
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [options, setOptions] = useState<User[]>([]); const [options, setOptions] = useState<User[]>([]);
@@ -79,7 +81,7 @@ export function PeopleSearch({
onChange={(event, value) => { onChange={(event, value) => {
const last = value[value.length - 1]; const last = value[value.length - 1];
if (typeof last === "string" && !isValidEmail(last)) { if (typeof last === "string" && !isValidEmail(last)) {
setError(`"${last}" is not a valid email address`); setError(t("peopleSearch.invalidEmail", { email: last }));
return; return;
} }
setError(null); setError(null);
@@ -93,8 +95,8 @@ export function PeopleSearch({
{...params} {...params}
error={!!error} error={!!error}
helperText={error} helperText={error}
placeholder="Start typing a name or email" placeholder={t("peopleSearch.placeholder")}
label="Start typing a name or email" label={t("peopleSearch.label")}
autoComplete="off" autoComplete="off"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter" && onToggleEventPreview) { if (e.key === "Enter" && onToggleEventPreview) {
@@ -134,7 +136,7 @@ export function PeopleSearch({
if (selectedUsers.find((u) => u.email === option.email)) return null; if (selectedUsers.find((u) => u.email === option.email)) return null;
const { key, ...otherProps } = props as any; const { key, ...otherProps } = props as any;
return ( return (
<ListItem key={key} {...otherProps} disableGutters> <ListItem key={key + option?.email} {...otherProps} disableGutters>
<ListItemAvatar> <ListItemAvatar>
<Avatar src={option.avatarUrl} alt={option.displayName} /> <Avatar src={option.avatarUrl} alt={option.displayName} />
</ListItemAvatar> </ListItemAvatar>
+9 -2
View File
@@ -3,10 +3,13 @@ import { Box, IconButton, TextField } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
import { Calendars } from "../../features/Calendars/CalendarTypes"; import { Calendars } from "../../features/Calendars/CalendarTypes";
import { SnackbarAlert } from "../Loading/SnackBarAlert"; import { SnackbarAlert } from "../Loading/SnackBarAlert";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
export function AccessTab({ calendar }: { calendar: Calendars }) { export function AccessTab({ calendar }: { calendar: Calendars }) {
const { t } = useI18n();
const calDAVLink = `${(window as any).CALENDAR_BASE_URL}${calendar.link.replace(".json", "")}`; const calDAVLink = `${(window as any).CALENDAR_BASE_URL}${calendar.link.replace(".json", "")}`;
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleCopy = () => { const handleCopy = () => {
navigator.clipboard.writeText(calDAVLink); navigator.clipboard.writeText(calDAVLink);
setOpen(true); setOpen(true);
@@ -18,7 +21,7 @@ export function AccessTab({ calendar }: { calendar: Calendars }) {
<TextField <TextField
disabled disabled
fullWidth fullWidth
label="CalDAV access" label={t("calendar.caldav_access")}
value={calDAVLink} value={calDAVLink}
slotProps={{ slotProps={{
input: { input: {
@@ -31,7 +34,11 @@ export function AccessTab({ calendar }: { calendar: Calendars }) {
}} }}
/> />
</Box> </Box>
<SnackbarAlert setOpen={setOpen} open={open} message="Link copied!" /> <SnackbarAlert
setOpen={setOpen}
open={open}
message={t("common.link_copied")}
/>
</> </>
); );
} }
+10 -4
View File
@@ -42,6 +42,7 @@ import { MiniCalendar } from "./MiniCalendar";
import { User } from "../Attendees/PeopleSearch"; import { User } from "../Attendees/PeopleSearch";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import { updateDarkColor } from "./utils/calendarColorsUtils"; import { updateDarkColor } from "./utils/calendarColorsUtils";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
interface CalendarAppProps { interface CalendarAppProps {
calendarRef: React.RefObject<CalendarApi | null>; calendarRef: React.RefObject<CalendarApi | null>;
@@ -286,6 +287,8 @@ export default function CalendarApp({
(window as any).__calendarRef = calendarRef; (window as any).__calendarRef = calendarRef;
} }
const { t, lang } = useI18n();
return ( return (
<main className="main-layout"> <main className="main-layout">
<Box <Box
@@ -312,7 +315,7 @@ export default function CalendarApp({
eventHandlers.handleDateSelect(null as unknown as DateSelectArg) eventHandlers.handleDateSelect(null as unknown as DateSelectArg)
} }
> >
<AddIcon /> <p>Create Event</p> <AddIcon /> <p>{t("event.createEvent")}</p>
</Button> </Button>
</Box> </Box>
@@ -356,6 +359,7 @@ export default function CalendarApp({
initialView="timeGridWeek" initialView="timeGridWeek"
firstDay={1} firstDay={1}
editable={true} editable={true}
locale={lang}
selectable={true} selectable={true}
timeZone={timezone} timeZone={timezone}
height={"100%"} height={"100%"}
@@ -382,7 +386,9 @@ export default function CalendarApp({
weekNumberContent={(arg) => { weekNumberContent={(arg) => {
return ( return (
<div className="weekSelector"> <div className="weekSelector">
<div>{arg.text}</div> <div>
{t("menubar.views.week")} {arg.num}
</div>
<TimezoneSelector <TimezoneSelector
value={timezone} value={timezone}
onChange={(newTimezone: string) => onChange={(newTimezone: string) =>
@@ -393,7 +399,7 @@ export default function CalendarApp({
); );
}} }}
dayCellContent={(arg) => { dayCellContent={(arg) => {
const month = arg.date.toLocaleDateString("en-US", { const month = arg.date.toLocaleDateString(t("locale"), {
month: "short", month: "short",
}); });
if (arg.view.type === "dayGridMonth") { if (arg.view.type === "dayGridMonth") {
@@ -453,7 +459,7 @@ export default function CalendarApp({
dayHeaderContent={(arg) => { dayHeaderContent={(arg) => {
const date = arg.date.getDate(); const date = arg.date.getDate();
const weekDay = arg.date const weekDay = arg.date
.toLocaleDateString("en-US", { weekday: "short" }) .toLocaleDateString(t("locale"), { weekday: "short" })
.toUpperCase(); .toUpperCase();
return ( return (
<div className="fc-daygrid-day-top"> <div className="fc-daygrid-day-top">
@@ -10,6 +10,7 @@ import {
} from "@mui/material"; } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
import { HexColorPicker } from "react-colorful"; import { HexColorPicker } from "react-colorful";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import { defaultColors, getAccessiblePair } from "./utils/calendarColorsUtils"; import { defaultColors, getAccessiblePair } from "./utils/calendarColorsUtils";
export function ColorPicker({ export function ColorPicker({
@@ -54,6 +55,7 @@ export function ColorPicker({
</Box> </Box>
); );
} }
function ColorBox({ function ColorBox({
color, color,
onChange, onChange,
@@ -107,11 +109,11 @@ function ColorPickerBox({
onChange: (color: Record<string, string>) => void; onChange: (color: Record<string, string>) => void;
selectedColor: Record<string, string>; selectedColor: Record<string, string>;
}) { }) {
const { t } = useI18n();
const [oldColor] = useState( const [oldColor] = useState(
selectedColor ?? { light: "#ffffff", dark: "#808080" } selectedColor ?? { light: "#ffffff", dark: "#808080" }
); );
const [color, setColor] = useState(oldColor); const [color, setColor] = useState(oldColor);
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null); const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const theme = useTheme(); const theme = useTheme();
@@ -139,12 +141,13 @@ function ColorPickerBox({
setColor(newColor); setColor(newColor);
onChange(newColor); onChange(newColor);
}; };
return ( return (
<> <>
<Box <Box
key={"colorPicker"} key={"colorPicker"}
role="button" role="button"
aria-label={`select custom color`} aria-label={t("colorPicker.selectCustom")}
onClick={handleClick} onClick={handleClick}
style={{ style={{
width: "46px", width: "46px",
@@ -198,10 +201,10 @@ function ColorPickerBox({
}} }}
> >
<Typography variant="subtitle1" fontWeight="600"> <Typography variant="subtitle1" fontWeight="600">
Choose custom colour {t("colorPicker.title")}
</Typography> </Typography>
<Typography variant="body2" sx={{ mb: 2, color: "text.secondary" }}> <Typography variant="body2" sx={{ mb: 2, color: "text.secondary" }}>
Choose a background colour for this calendar {t("colorPicker.subtitle")}
</Typography> </Typography>
<Box sx={{ mb: 2 }}> <Box sx={{ mb: 2 }}>
@@ -214,7 +217,7 @@ function ColorPickerBox({
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}> <Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
<Typography variant="body2" sx={{ mr: 1 }}> <Typography variant="body2" sx={{ mr: 1 }}>
Hex {t("colorPicker.hex")}
</Typography> </Typography>
<TextField <TextField
value={color.light?.toUpperCase()} value={color.light?.toUpperCase()}
@@ -226,13 +229,13 @@ function ColorPickerBox({
</Box> </Box>
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}> <Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}>
<Button onClick={handleClose}>Cancel</Button> <Button onClick={handleClose}>{t("common.cancel")}</Button>
<Button <Button
variant="contained" variant="contained"
onClick={handleSave} onClick={handleSave}
sx={{ textTransform: "none" }} sx={{ textTransform: "none" }}
> >
Save {t("actions.save")}
</Button> </Button>
</Box> </Box>
</Popover> </Popover>
+2 -2
View File
@@ -4,9 +4,9 @@ import { Calendars } from "../../features/Calendars/CalendarTypes";
import { CalendarName } from "./CalendarName"; import { CalendarName } from "./CalendarName";
export function CalendarItemList( export function CalendarItemList(
userPersonnalCalendars: Calendars[] userPersonalCalendars: Calendars[]
): React.ReactNode { ): React.ReactNode {
return Object.values(userPersonnalCalendars).map((calendar) => ( return Object.values(userPersonalCalendars).map((calendar) => (
<MenuItem key={calendar.id} value={calendar.id}> <MenuItem key={calendar.id} value={calendar.id}>
<CalendarName calendar={calendar} /> <CalendarName calendar={calendar} />
</MenuItem> </MenuItem>
+19 -5
View File
@@ -13,6 +13,7 @@ import { AccessTab } from "./AccessTab";
import { ImportTab } from "./ImportTab"; import { ImportTab } from "./ImportTab";
import { SettingsTab } from "./SettingsTab"; import { SettingsTab } from "./SettingsTab";
import { defaultColors } from "./utils/calendarColorsUtils"; import { defaultColors } from "./utils/calendarColorsUtils";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
function CalendarPopover({ function CalendarPopover({
open, open,
@@ -26,6 +27,7 @@ function CalendarPopover({
) => void; ) => void;
calendar?: Calendars; calendar?: Calendars;
}) { }) {
const { t } = useI18n();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const userId = const userId =
useAppSelector((state) => state.user.userData?.openpaasId) ?? ""; useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
@@ -181,10 +183,18 @@ function CalendarPopover({
<Tabs value={tab} onChange={(e, v) => setTab(v)}> <Tabs value={tab} onChange={(e, v) => setTab(v)}>
<Tab <Tab
value="settings" value="settings"
label={calendar ? "Settings" : "Add new calendar"} label={
calendar
? t("calendarPopover.tabs.settings")
: t("calendarPopover.tabs.addNew")
}
/> />
{calendar && <Tab value="access" label="Access" />} {calendar && (
{isOwn && <Tab value="import" label="Import" />} <Tab value="access" label={t("calendarPopover.tabs.access")} />
)}
{isOwn && (
<Tab value="import" label={t("calendarPopover.tabs.import")} />
)}
</Tabs> </Tabs>
} }
actions={ actions={
@@ -193,14 +203,18 @@ function CalendarPopover({
variant="outlined" variant="outlined"
onClick={(e) => handleClose({}, "backdropClick")} onClick={(e) => handleClose({}, "backdropClick")}
> >
Cancel {t("common.cancel")}
</Button> </Button>
<Button <Button
disabled={tab === "import" ? !importedContent : !name.trim()} disabled={tab === "import" ? !importedContent : !name.trim()}
variant="contained" variant="contained"
onClick={tab === "import" ? handleImport : handleSave} onClick={tab === "import" ? handleImport : handleSave}
> >
{tab === "import" ? "Import" : calendar ? "Save" : "Create"} {tab === "import"
? t("actions.import")
: calendar
? t("actions.save")
: t("actions.create")}
</Button> </Button>
</DialogActions> </DialogActions>
} }
+27 -17
View File
@@ -2,23 +2,19 @@ import CloseIcon from "@mui/icons-material/Close";
import Avatar from "@mui/material/Avatar"; import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import CardActions from "@mui/material/CardActions";
import CardContent from "@mui/material/CardContent";
import CardHeader from "@mui/material/CardHeader";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import Modal from "@mui/material/Modal"; import { useTheme } from "@mui/material/styles";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import { useState } from "react"; import { useState } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { getCalendars } from "../../features/Calendars/CalendarApi"; import { getCalendars } from "../../features/Calendars/CalendarApi";
import { addSharedCalendarAsync } from "../../features/Calendars/CalendarSlice"; import { addSharedCalendarAsync } from "../../features/Calendars/CalendarSlice";
import { ColorPicker } from "./CalendarColorPicker";
import { Calendars } from "../../features/Calendars/CalendarTypes"; import { Calendars } from "../../features/Calendars/CalendarTypes";
import { User, PeopleSearch } from "../Attendees/PeopleSearch"; import { PeopleSearch, User } from "../Attendees/PeopleSearch";
import { defaultColors, getAccessiblePair } from "./utils/calendarColorsUtils";
import { useTheme } from "@mui/material/styles";
import { ResponsiveDialog } from "../Dialog"; import { ResponsiveDialog } from "../Dialog";
import { ColorPicker } from "./CalendarColorPicker";
import { defaultColors, getAccessiblePair } from "./utils/calendarColorsUtils";
interface CalendarWithOwner { interface CalendarWithOwner {
cal: Record<string, any>; cal: Record<string, any>;
@@ -104,6 +100,7 @@ function SelectedCalendarsList({
color: Record<string, string> color: Record<string, string>
) => void; ) => void;
}) { }) {
const { t } = useI18n();
if (selectedCal.length === 0) return null; if (selectedCal.length === 0) return null;
const groupedByOwner = selectedCal.reduce< const groupedByOwner = selectedCal.reduce<
@@ -144,7 +141,7 @@ function SelectedCalendarsList({
return ( return (
<Box mt={2}> <Box mt={2}>
<Typography variant="subtitle1" gutterBottom> <Typography variant="subtitle1" gutterBottom>
Name {t("common.name")}
</Typography> </Typography>
{Object.values(groupedByOwner).map( {Object.values(groupedByOwner).map(
@@ -160,14 +157,26 @@ function SelectedCalendarsList({
onColorChange={(color) => onColorChange(cal, color)} onColorChange={(color) => onColorChange(cal, color)}
/> />
) : ( ) : (
<Typography color="textSecondary"> <Typography
No publicly available calendars for {owner.displayName} key={t("calendar.noPublicCalendarsFor", {
name: owner.displayName,
})}
color="textSecondary"
>
{t("calendar.noPublicCalendarsFor", {
name: owner.displayName,
})}
</Typography> </Typography>
) )
) )
) : alreadyExisting ? ( ) : alreadyExisting ? (
<Typography color="textSecondary"> <Typography
No more Calendar for {owner.displayName} key={t("calendar.noMoreCalendarsFor", {
name: owner.displayName,
})}
color="textSecondary"
>
{t("calendar.noMoreCalendarsFor", { name: owner.displayName })}
</Typography> </Typography>
) : null} ) : null}
</Box> </Box>
@@ -230,6 +239,7 @@ export default function CalendarSearch({
setSelectedUsers([]); setSelectedUsers([]);
} }
}; };
const { t } = useI18n();
return ( return (
<ResponsiveDialog <ResponsiveDialog
@@ -240,17 +250,17 @@ export default function CalendarSearch({
setSelectedCalendars([]); setSelectedCalendars([]);
setSelectedUsers([]); setSelectedUsers([]);
}} }}
title="Browse other calendars" title={t("calendar.browseOtherCalendars")}
actions={ actions={
<> <>
<Button <Button
variant="outlined" variant="outlined"
onClick={() => onClose({}, "backdropClick")} onClick={() => onClose({}, "backdropClick")}
> >
Cancel {t("common.cancel")}
</Button> </Button>
<Button variant="contained" onClick={handleSave}> <Button variant="contained" onClick={handleSave}>
Add {t("actions.add")}
</Button> </Button>
</> </>
} }
+17 -18
View File
@@ -16,6 +16,7 @@ import { Divider, ListItem, Menu, MenuItem } from "@mui/material";
import { removeCalendarAsync } from "../../features/Calendars/CalendarSlice"; import { removeCalendarAsync } from "../../features/Calendars/CalendarSlice";
import { DeleteCalendarDialog } from "./DeleteCalendarDialog"; import { DeleteCalendarDialog } from "./DeleteCalendarDialog";
import { trimLongTextWithoutSpace } from "../../utils/textUtils"; import { trimLongTextWithoutSpace } from "../../utils/textUtils";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
function CalendarAccordion({ function CalendarAccordion({
title, title,
@@ -80,7 +81,7 @@ function CalendarAccordion({
key={id} key={id}
calendars={allCalendars} calendars={allCalendars}
id={id} id={id}
isPersonnal={defaultExpanded} isPersonal={defaultExpanded}
selectedCalendars={selectedCalendars} selectedCalendars={selectedCalendars}
handleCalendarToggle={handleToggle} handleCalendarToggle={handleToggle}
setOpen={() => setOpen(id)} setOpen={() => setOpen(id)}
@@ -98,11 +99,12 @@ export default function CalendarSelection({
selectedCalendars: string[]; selectedCalendars: string[];
setSelectedCalendars: Function; setSelectedCalendars: Function;
}) { }) {
const { t } = useI18n();
const userId = const userId =
useAppSelector((state) => state.user.userData?.openpaasId) ?? ""; useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const calendars = useAppSelector((state) => state.calendars.list); const calendars = useAppSelector((state) => state.calendars.list);
const personnalCalendars = Object.keys(calendars).filter( const personalCalendars = Object.keys(calendars).filter(
(id) => id.split("/")[0] === userId (id) => id.split("/")[0] === userId
); );
const delegatedCalendars = Object.keys(calendars).filter( const delegatedCalendars = Object.keys(calendars).filter(
@@ -127,8 +129,8 @@ export default function CalendarSelection({
<> <>
<div> <div>
<CalendarAccordion <CalendarAccordion
title="Personnal Calendars" title={t("calendar.personal")}
calendars={personnalCalendars} calendars={personalCalendars}
selectedCalendars={selectedCalendars} selectedCalendars={selectedCalendars}
handleToggle={handleCalendarToggle} handleToggle={handleCalendarToggle}
showAddButton showAddButton
@@ -141,7 +143,7 @@ export default function CalendarSelection({
/> />
<CalendarAccordion <CalendarAccordion
title="Delegated Calendars" title={t("calendar.delegated")}
calendars={delegatedCalendars} calendars={delegatedCalendars}
selectedCalendars={selectedCalendars} selectedCalendars={selectedCalendars}
handleToggle={handleCalendarToggle} handleToggle={handleCalendarToggle}
@@ -155,7 +157,7 @@ export default function CalendarSelection({
/> />
<CalendarAccordion <CalendarAccordion
title="Other Calendars" title={t("calendar.other")}
calendars={sharedCalendars} calendars={sharedCalendars}
selectedCalendars={selectedCalendars} selectedCalendars={selectedCalendars}
showAddButton showAddButton
@@ -197,18 +199,19 @@ export default function CalendarSelection({
function CalendarSelector({ function CalendarSelector({
calendars, calendars,
id, id,
isPersonnal, isPersonal,
selectedCalendars, selectedCalendars,
handleCalendarToggle, handleCalendarToggle,
setOpen, setOpen,
}: { }: {
calendars: Record<string, Calendars>; calendars: Record<string, Calendars>;
id: string; id: string;
isPersonnal: boolean; isPersonal: boolean;
selectedCalendars: string[]; selectedCalendars: string[];
handleCalendarToggle: (name: string) => void; handleCalendarToggle: (name: string) => void;
setOpen: Function; setOpen: Function;
}) { }) {
const { t } = useI18n();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const calLink = const calLink =
useAppSelector((state) => state.calendars.list[id].link) ?? ""; useAppSelector((state) => state.calendars.list[id].link) ?? "";
@@ -222,7 +225,7 @@ function CalendarSelector({
setAnchorEl(null); setAnchorEl(null);
}; };
const [userId, calId] = id.split("/"); const [userId, calId] = id.split("/");
const isDefault = isPersonnal && userId === calId; const isDefault = isPersonal && userId === calId;
const [deletePopupOpen, setDeletePopupOpen] = useState(false); const [deletePopupOpen, setDeletePopupOpen] = useState(false);
const handleDeleteConfirm = () => { const handleDeleteConfirm = () => {
@@ -244,14 +247,10 @@ function CalendarSelector({
justifyContent: "space-between", justifyContent: "space-between",
transition: "background-color 0.2s ease", transition: "background-color 0.2s ease",
padding: "8px 24px 8px 16px", padding: "8px 24px 8px 16px",
"& .MoreBtn": { "& .MoreBtn": { opacity: 0 },
opacity: 0,
},
"&:hover": { "&:hover": {
backgroundColor: "#F3F3F6", backgroundColor: "#F3F3F6",
"& .MoreBtn": { "& .MoreBtn": { opacity: 1 },
opacity: 1,
},
}, },
}} }}
> >
@@ -293,12 +292,12 @@ function CalendarSelector({
handleClose(); handleClose();
}} }}
> >
Modify {t("actions.modify")}
</MenuItem> </MenuItem>
{!isDefault && <Divider />} {!isDefault && <Divider />}
{!isDefault && ( {!isDefault && (
<MenuItem onClick={() => setDeletePopupOpen(!deletePopupOpen)}> <MenuItem onClick={() => setDeletePopupOpen(!deletePopupOpen)}>
{isPersonnal ? "Delete" : "Remove"} {isPersonal ? t("actions.delete") : t("actions.remove")}
</MenuItem> </MenuItem>
)} )}
</Menu> </Menu>
@@ -308,7 +307,7 @@ function CalendarSelector({
setDeletePopupOpen={setDeletePopupOpen} setDeletePopupOpen={setDeletePopupOpen}
calendars={calendars} calendars={calendars}
id={id} id={id}
isPersonnal={isPersonnal} isPersonal={isPersonal}
handleDeleteConfirm={handleDeleteConfirm} handleDeleteConfirm={handleDeleteConfirm}
/> />
</> </>
@@ -5,37 +5,45 @@ import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText"; import DialogContentText from "@mui/material/DialogContentText";
import DialogActions from "@mui/material/DialogActions"; import DialogActions from "@mui/material/DialogActions";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
export function DeleteCalendarDialog({ export function DeleteCalendarDialog({
deletePopupOpen, deletePopupOpen,
setDeletePopupOpen, setDeletePopupOpen,
calendars, calendars,
id, id,
isPersonnal, isPersonal,
handleDeleteConfirm, handleDeleteConfirm,
}: { }: {
deletePopupOpen: boolean; deletePopupOpen: boolean;
setDeletePopupOpen: (e: boolean) => void; setDeletePopupOpen: (e: boolean) => void;
calendars: Record<string, Calendars>; calendars: Record<string, Calendars>;
id: string; id: string;
isPersonnal: boolean; isPersonal: boolean;
handleDeleteConfirm: () => void; handleDeleteConfirm: () => void;
}) { }) {
const { t } = useI18n();
return ( return (
<Dialog open={deletePopupOpen} onClose={() => setDeletePopupOpen(false)}> <Dialog open={deletePopupOpen} onClose={() => setDeletePopupOpen(false)}>
<DialogTitle>Remove {calendars[id].name}?</DialogTitle> <DialogTitle>
{t("calendar.delete.title", { name: calendars[id].name })}
</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>
Are you sure you want to remove this calendar?{" "} {isPersonal
{isPersonnal ? t("calendar.delete.personalWarning")
? "You will loose all events in this calendar." : t("calendar.delete.sharedWarning")}
: "You will loose access to its events. You will still be able to add it back later."}
</DialogContentText> </DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setDeletePopupOpen(false)}>Cancel</Button> <Button onClick={() => setDeletePopupOpen(false)}>
{t("common.cancel")}
</Button>
<Button onClick={handleDeleteConfirm} variant="contained"> <Button onClick={handleDeleteConfirm} variant="contained">
{isPersonnal ? "Delete" : "Remove"} {isPersonal ? t("actions.delete") : t("actions.remove")}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
+12 -10
View File
@@ -14,6 +14,7 @@ import { useEffect, useState } from "react";
import { useAppSelector } from "../../app/hooks"; import { useAppSelector } from "../../app/hooks";
import { CalendarItemList } from "./CalendarItemList"; import { CalendarItemList } from "./CalendarItemList";
import { SettingsTab } from "./SettingsTab"; import { SettingsTab } from "./SettingsTab";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
export function ImportTab({ export function ImportTab({
userId, userId,
@@ -37,11 +38,12 @@ export function ImportTab({
setVisibility: Function; setVisibility: Function;
}; };
}) { }) {
const { t } = useI18n();
const [importMode, setImportMode] = useState<"file" | "url">("file"); const [importMode, setImportMode] = useState<"file" | "url">("file");
const [importFile, setImportFile] = useState<File | null>(null); const [importFile, setImportFile] = useState<File | null>(null);
const [importUrl, setImportUrl] = useState(""); const [importUrl, setImportUrl] = useState("");
const calendars = useAppSelector((state) => state.calendars.list); const calendars = useAppSelector((state) => state.calendars.list);
const personnalCalendars = Object.values(calendars).filter( const personalCalendars = Object.values(calendars).filter(
(cal) => cal.id.split("/")[0] === userId (cal) => cal.id.split("/")[0] === userId
); );
@@ -59,14 +61,14 @@ export function ImportTab({
size="small" size="small"
sx={{ mb: 2 }} sx={{ mb: 2 }}
> >
<ToggleButton value="file">File</ToggleButton> <ToggleButton value="file">{t("common.import_file")}</ToggleButton>
{/* <ToggleButton value="url">URL</ToggleButton> */} {/* <ToggleButton value="url">{t("common.import_url")}</ToggleButton> */}
</ToggleButtonGroup> </ToggleButtonGroup>
{importMode === "file" && ( {importMode === "file" && (
<> <>
<Button variant="outlined" component="label" sx={{ mb: 1 }}> <Button variant="outlined" component="label" sx={{ mb: 1 }}>
Select file {t("common.select_file")}
<input <input
type="file" type="file"
hidden hidden
@@ -85,7 +87,7 @@ export function ImportTab({
display="block" display="block"
mb={2} mb={2}
> >
Import events from an ICS file to one of your calendars. {t("calendar.import_file_description")}
</Typography> </Typography>
</> </>
)} )}
@@ -93,7 +95,7 @@ export function ImportTab({
{importMode === "url" && ( {importMode === "url" && (
<TextField <TextField
fullWidth fullWidth
label="ICS feed URL" label={t("calendar.ics_feed_url")}
value={importUrl} value={importUrl}
onChange={(e) => setImportUrl(e.target.value)} onChange={(e) => setImportUrl(e.target.value)}
size="small" size="small"
@@ -102,15 +104,15 @@ export function ImportTab({
)} )}
<FormControl fullWidth size="small" sx={{ mt: 2 }}> <FormControl fullWidth size="small" sx={{ mt: 2 }}>
<InputLabel id="import-to-label">Import to</InputLabel> <InputLabel id="import-to-label">{t("calendar.import_to")}</InputLabel>
<Select <Select
labelId="import-to-label" labelId="import-to-label"
label="Import to" label={t("calendar.import_to")}
value={importTarget} value={importTarget}
onChange={(e) => setImportTarget(e.target.value)} onChange={(e) => setImportTarget(e.target.value)}
> >
<MenuItem value="new">New calendar</MenuItem> <MenuItem value="new">{t("calendar.new_calendar")}</MenuItem>
{CalendarItemList(personnalCalendars)} {CalendarItemList(personalCalendars)}
</Select> </Select>
</FormControl> </FormControl>
+6 -1
View File
@@ -12,6 +12,7 @@ import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { refreshCalendars } from "../Event/utils/eventUtils"; import { refreshCalendars } from "../Event/utils/eventUtils";
import { getCalendarDetailAsync } from "../../features/Calendars/CalendarSlice"; import { getCalendarDetailAsync } from "../../features/Calendars/CalendarSlice";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
export function MiniCalendar({ export function MiniCalendar({
calendarRef, calendarRef,
@@ -29,10 +30,14 @@ export function MiniCalendar({
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const calendars = useAppSelector((state) => state.calendars); const calendars = useAppSelector((state) => state.calendars);
const [visibleDate, setVisibleDate] = useState(selectedDate); const [visibleDate, setVisibleDate] = useState(selectedDate);
const { t } = useI18n();
useEffect(() => setVisibleDate(selectedDate), [selectedDate]); useEffect(() => setVisibleDate(selectedDate), [selectedDate]);
return ( return (
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="en-gb"> <LocalizationProvider
dateAdapter={AdapterMoment}
adapterLocale={t("locale") ?? "en-gb"}
>
<DateCalendar <DateCalendar
value={moment(visibleDate)} value={moment(visibleDate)}
onChange={(dateMoment) => { onChange={(dateMoment) => {
+14 -8
View File
@@ -9,7 +9,8 @@ import {
ToggleButtonGroup, ToggleButtonGroup,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { useState } from "react"; import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import { useState, useEffect } from "react";
import { useAppSelector } from "../../app/hooks"; import { useAppSelector } from "../../app/hooks";
import { Calendars } from "../../features/Calendars/CalendarTypes"; import { Calendars } from "../../features/Calendars/CalendarTypes";
import { ColorPicker } from "./CalendarColorPicker"; import { ColorPicker } from "./CalendarColorPicker";
@@ -35,16 +36,21 @@ export function SettingsTab({
setVisibility: Function; setVisibility: Function;
calendar?: Calendars; calendar?: Calendars;
}) { }) {
const { t } = useI18n();
const [toggleDesc, setToggleDesc] = useState(Boolean(description)); const [toggleDesc, setToggleDesc] = useState(Boolean(description));
const userId = const userId =
useAppSelector((state) => state.user.userData?.openpaasId) ?? ""; useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const isOwn = calendar ? calendar.id.split("/")[0] === userId : true; const isOwn = calendar ? calendar.id.split("/")[0] === userId : true;
useEffect(() => {
if (description) setToggleDesc(true);
}, [description]);
return ( return (
<Box mt={2}> <Box mt={2}>
<TextField <TextField
fullWidth fullWidth
label="Name" label={t("common.name")}
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
size="small" size="small"
@@ -58,14 +64,14 @@ export function SettingsTab({
onClick={() => setToggleDesc(!toggleDesc)} onClick={() => setToggleDesc(!toggleDesc)}
startIcon={<FormatListBulletedIcon />} startIcon={<FormatListBulletedIcon />}
> >
Add description {t("calendar.addDescription")}
</Button> </Button>
)} )}
{toggleDesc && ( {toggleDesc && (
<TextField <TextField
fullWidth fullWidth
label="Description" label={t("common.description")}
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
size="small" size="small"
@@ -77,7 +83,7 @@ export function SettingsTab({
<Box mt={2}> <Box mt={2}>
<Typography variant="body2" gutterBottom> <Typography variant="body2" gutterBottom>
Color {t("calendar.color")}
</Typography> </Typography>
<ColorPicker <ColorPicker
onChange={(color) => setColor(color)} onChange={(color) => setColor(color)}
@@ -88,7 +94,7 @@ export function SettingsTab({
{isOwn && ( {isOwn && (
<Box mt={2}> <Box mt={2}>
<Typography variant="body2" gutterBottom> <Typography variant="body2" gutterBottom>
New events created will be visible to: {t("calendar.newEventsVisibility")}
</Typography> </Typography>
<ToggleButtonGroup <ToggleButtonGroup
value={visibility} value={visibility}
@@ -98,12 +104,12 @@ export function SettingsTab({
> >
<ToggleButton value="public"> <ToggleButton value="public">
<PublicIcon fontSize="small" /> <PublicIcon fontSize="small" />
All {t("common.all")}
</ToggleButton> </ToggleButton>
<ToggleButton value="private"> <ToggleButton value="private">
<LockIcon fontSize="small" /> <LockIcon fontSize="small" />
You {t("common.you")}
</ToggleButton> </ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
</Box> </Box>
+3 -2
View File
@@ -1,4 +1,5 @@
import { Button, Popover } from "@mui/material"; import { Button, Popover } from "@mui/material";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import { MouseEvent, useMemo, useState } from "react"; import { MouseEvent, useMemo, useState } from "react";
import { TIMEZONES } from "../../utils/timezone-data"; import { TIMEZONES } from "../../utils/timezone-data";
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete"; import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
@@ -34,7 +35,7 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) {
}; };
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const { t } = useI18n();
return ( return (
<> <>
<Button <Button
@@ -49,7 +50,7 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) {
lineHeight: 1.2, lineHeight: 1.2,
}} }}
> >
{selectedOffset || "Select Timezone"} {selectedOffset || t("common.select_timezone")}
</Button> </Button>
<Popover <Popover
+6 -6
View File
@@ -4,8 +4,10 @@ import { Box, Button, Fade, Paper, Stack, Typography } from "@mui/material";
import { useEffect } from "react"; import { useEffect } from "react";
import { push } from "redux-first-history"; import { push } from "redux-first-history";
import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
export function Error() { export function Error() {
const { t } = useI18n();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const userError = useAppSelector((state) => state.user.error); const userError = useAppSelector((state) => state.user.error);
const calendarError = useAppSelector((state) => state.calendars.error); const calendarError = useAppSelector((state) => state.calendars.error);
@@ -16,7 +18,7 @@ export function Error() {
} }
}, [calendarError, dispatch]); }, [calendarError, dispatch]);
const errorMessage = userError || calendarError || "Unknown error"; const errorMessage = userError || calendarError || t("error.unknown");
return ( return (
<Fade in timeout={500}> <Fade in timeout={500}>
@@ -56,7 +58,7 @@ export function Error() {
</Box> </Box>
<Typography variant="h5" fontWeight={600}> <Typography variant="h5" fontWeight={600}>
Something went wrong {t("error.title")}
</Typography> </Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}> <Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}>
@@ -67,9 +69,7 @@ export function Error() {
variant="contained" variant="contained"
color="error" color="error"
startIcon={<ReplayIcon />} startIcon={<ReplayIcon />}
onClick={() => { onClick={() => window.location.reload()}
window.location.reload();
}}
sx={{ sx={{
textTransform: "none", textTransform: "none",
fontWeight: 600, fontWeight: 600,
@@ -79,7 +79,7 @@ export function Error() {
boxShadow: "none", boxShadow: "none",
}} }}
> >
Try Again {t("error.retry")}
</Button> </Button>
</Stack> </Stack>
</Paper> </Paper>
+9 -4
View File
@@ -4,6 +4,7 @@ import Button from "@mui/material/Button";
import { useAppDispatch } from "../../app/hooks"; import { useAppDispatch } from "../../app/hooks";
import { clearError as calendarClearError } from "../../features/Calendars/CalendarSlice"; import { clearError as calendarClearError } from "../../features/Calendars/CalendarSlice";
import { clearError as userClearError } from "../../features/User/userSlice"; import { clearError as userClearError } from "../../features/User/userSlice";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
export function ErrorSnackbar({ export function ErrorSnackbar({
error, error,
@@ -12,7 +13,9 @@ export function ErrorSnackbar({
error: string | null; error: string | null;
type: "user" | "calendar"; type: "user" | "calendar";
}) { }) {
const { t } = useI18n();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleCloseSnackbar = () => { const handleCloseSnackbar = () => {
dispatch(type === "calendar" ? calendarClearError() : userClearError()); dispatch(type === "calendar" ? calendarClearError() : userClearError());
}; };
@@ -29,11 +32,11 @@ export function ErrorSnackbar({
sx={{ width: "100%" }} sx={{ width: "100%" }}
action={ action={
<Button color="inherit" size="small" onClick={handleCloseSnackbar}> <Button color="inherit" size="small" onClick={handleCloseSnackbar}>
OK {t("common.ok")}
</Button> </Button>
} }
> >
{error} {error || t("error.unknown")}
</Alert> </Alert>
</Snackbar> </Snackbar>
); );
@@ -46,11 +49,13 @@ export function EventErrorSnackbar({
messages: string[]; messages: string[];
onClose: () => void; onClose: () => void;
}) { }) {
const { t } = useI18n();
const open = messages.length > 0; const open = messages.length > 0;
const summary = const summary =
messages.length === 1 messages.length === 1
? messages[0] ? messages[0]
: `${messages.length} events with errors`; : t("error.multipleEvents", { count: messages.length });
return ( return (
<Snackbar <Snackbar
@@ -65,7 +70,7 @@ export function EventErrorSnackbar({
sx={{ width: "100%" }} sx={{ width: "100%" }}
action={ action={
<Button color="inherit" size="small" onClick={onClose}> <Button color="inherit" size="small" onClick={onClose}>
OK {t("common.ok")}
</Button> </Button>
} }
> >
+8 -6
View File
@@ -11,6 +11,7 @@ import {
} from "@mui/material"; } from "@mui/material";
import { CalendarEvent } from "../../features/Events/EventsTypes"; import { CalendarEvent } from "../../features/Events/EventsTypes";
import { useState } from "react"; import { useState } from "react";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
export function EditModeDialog({ export function EditModeDialog({
type, type,
@@ -23,6 +24,7 @@ export function EditModeDialog({
event: CalendarEvent; event: CalendarEvent;
eventAction: (type: "solo" | "all" | undefined) => void; eventAction: (type: "solo" | "all" | undefined) => void;
}) { }) {
const { t } = useI18n();
const [typeOfAction, setTypeOfAction] = useState<"solo" | "all" | undefined>( const [typeOfAction, setTypeOfAction] = useState<"solo" | "all" | undefined>(
"solo" "solo"
); );
@@ -37,8 +39,8 @@ export function EditModeDialog({
return ( return (
<Dialog open={Boolean(type)} onClose={handleClose}> <Dialog open={Boolean(type)} onClose={handleClose}>
<DialogTitle> <DialogTitle>
{type === "edit" && "Update the reccurent event"} {type === "edit" && t("editModeDialog.updateRecurrentEvent")}
{type === "attendance" && "Update the participation status"} {type === "attendance" && t("editModeDialog.updateParticipationStatus")}
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<RadioGroup <RadioGroup
@@ -50,19 +52,19 @@ export function EditModeDialog({
<FormControlLabel <FormControlLabel
value="solo" value="solo"
control={<Radio />} control={<Radio />}
label="This event" label={t("editModeDialog.thisEvent")}
/> />
<FormControlLabel <FormControlLabel
value="all" value="all"
control={<Radio />} control={<Radio />}
label="All the events" label={t("editModeDialog.allEvents")}
/> />
</RadioGroup> </RadioGroup>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<ButtonGroup> <ButtonGroup>
<Button onClick={handleClose}>Cancel</Button> <Button onClick={handleClose}>{t("common.cancel")}</Button>
<Button onClick={handleEvent}>Ok</Button> <Button onClick={handleEvent}>{t("common.ok")}</Button>
</ButtonGroup> </ButtonGroup>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
+3 -1
View File
@@ -1,5 +1,6 @@
import { MenuItem } from "@mui/material"; import { MenuItem } from "@mui/material";
import { CalendarEvent } from "../../features/Events/EventsTypes"; import { CalendarEvent } from "../../features/Events/EventsTypes";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
export default function EventDuplication({ export default function EventDuplication({
onClose, onClose,
@@ -10,13 +11,14 @@ export default function EventDuplication({
event: CalendarEvent; event: CalendarEvent;
onOpenDuplicate?: () => void; onOpenDuplicate?: () => void;
}) { }) {
const { t } = useI18n();
return ( return (
<MenuItem <MenuItem
onClick={() => { onClick={() => {
onOpenDuplicate?.(); onOpenDuplicate?.();
}} }}
> >
Duplicate event {t("eventDuplication.duplicateEvent")}
</MenuItem> </MenuItem>
); );
} }
+97 -48
View File
@@ -33,6 +33,7 @@ import {
} from "../../utils/videoConferenceUtils"; } from "../../utils/videoConferenceUtils";
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete"; import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
import { CalendarItemList } from "../Calendar/CalendarItemList"; import { CalendarItemList } from "../Calendar/CalendarItemList";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import { FieldWithLabel } from "./components/FieldWithLabel"; import { FieldWithLabel } from "./components/FieldWithLabel";
import { DateTimeFields } from "./components/DateTimeFields"; import { DateTimeFields } from "./components/DateTimeFields";
import { useAllDayToggle } from "./hooks/useAllDayToggle"; import { useAllDayToggle } from "./hooks/useAllDayToggle";
@@ -84,7 +85,7 @@ interface EventFormFieldsProps {
isOpen?: boolean; isOpen?: boolean;
// Data // Data
userPersonnalCalendars: Calendars[]; userPersonalCalendars: Calendars[];
timezoneList: { timezoneList: {
zones: string[]; zones: string[];
browserTz: string; browserTz: string;
@@ -145,7 +146,7 @@ export default function EventFormFields({
showRepeat, showRepeat,
setShowRepeat, setShowRepeat,
isOpen = false, isOpen = false,
userPersonnalCalendars, userPersonalCalendars,
timezoneList, timezoneList,
onStartChange, onStartChange,
onEndChange, onEndChange,
@@ -155,6 +156,8 @@ export default function EventFormFields({
showValidationErrors = false, showValidationErrors = false,
onHasEndDateChangedChange, onHasEndDateChangedChange,
}: EventFormFieldsProps) { }: EventFormFieldsProps) {
const { t } = useI18n();
// Internal state for 4 separate fields // Internal state for 4 separate fields
const [startDate, setStartDate] = React.useState(""); const [startDate, setStartDate] = React.useState("");
const [startTime, setStartTime] = React.useState(""); const [startTime, setStartTime] = React.useState("");
@@ -406,15 +409,15 @@ export default function EventFormFields({
<FieldWithLabel <FieldWithLabel
label={ label={
<> <>
Title <span style={{ color: "red" }}>*</span> {t("event.form.title")} <span style={{ color: "red" }}>*</span>
</> </>
} }
isExpanded={showMore} isExpanded={showMore}
> >
<TextField <TextField
fullWidth fullWidth
label={!showMore ? "Title" : ""} label={!showMore ? t("event.form.title") : ""}
placeholder="Add title" placeholder={t("event.form.titlePlaceholder")}
value={title} value={title}
onChange={(e) => { onChange={(e) => {
setTitle(e.target.value); setTitle(e.target.value);
@@ -439,18 +442,21 @@ export default function EventFormFields({
color: "text.secondary", color: "text.secondary",
}} }}
> >
Add description {t("event.form.addDescription")}
</Button> </Button>
</Box> </Box>
</FieldWithLabel> </FieldWithLabel>
)} )}
{showDescription && ( {showDescription && (
<FieldWithLabel label="Description" isExpanded={showMore}> <FieldWithLabel
label={t("event.form.description")}
isExpanded={showMore}
>
<TextField <TextField
fullWidth fullWidth
label={!showMore ? "Description" : ""} label={!showMore ? t("event.form.description") : ""}
placeholder="Add description" placeholder={t("event.form.descriptionPlaceholder")}
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
size="small" size="small"
@@ -471,7 +477,7 @@ export default function EventFormFields({
</FieldWithLabel> </FieldWithLabel>
)} )}
<FieldWithLabel label="Date & Time" isExpanded={showMore}> <FieldWithLabel label={t("event.form.dateTime")} isExpanded={showMore}>
<DateTimeFields <DateTimeFields
startDate={startDate} startDate={startDate}
startTime={startTime} startTime={startTime}
@@ -496,7 +502,7 @@ export default function EventFormFields({
control={ control={
<Checkbox checked={allday} onChange={handleAllDayToggle} /> <Checkbox checked={allday} onChange={handleAllDayToggle} />
} }
label="All day" label={t("event.form.allDay")}
/> />
<FormControlLabel <FormControlLabel
control={ control={
@@ -528,7 +534,7 @@ export default function EventFormFields({
}} }}
/> />
} }
label="Repeat" label={t("event.form.repeat")}
/> />
<TimezoneAutocomplete <TimezoneAutocomplete
value={timezone} value={timezone}
@@ -538,7 +544,7 @@ export default function EventFormFields({
showIcon={true} showIcon={true}
width={240} width={240}
size="small" size="small"
placeholder="Select timezone" placeholder={t("event.form.timezonePlaceholder")}
/> />
</Box> </Box>
</FieldWithLabel> </FieldWithLabel>
@@ -554,11 +560,17 @@ export default function EventFormFields({
</FieldWithLabel> </FieldWithLabel>
)} )}
<FieldWithLabel label="Participants" isExpanded={showMore}> <FieldWithLabel
label={t("event.form.participants")}
isExpanded={showMore}
>
<AttendeeSelector attendees={attendees} setAttendees={setAttendees} /> <AttendeeSelector attendees={attendees} setAttendees={setAttendees} />
</FieldWithLabel> </FieldWithLabel>
<FieldWithLabel label="Video meeting" isExpanded={showMore}> <FieldWithLabel
label={t("event.form.videoMeeting")}
isExpanded={showMore}
>
<Box display="flex" gap={1} alignItems="center"> <Box display="flex" gap={1} alignItems="center">
<Button <Button
startIcon={<VideocamIcon />} startIcon={<VideocamIcon />}
@@ -570,7 +582,7 @@ export default function EventFormFields({
display: hasVideoConference ? "none" : "flex", display: hasVideoConference ? "none" : "flex",
}} }}
> >
Add Visio conference {t("event.form.addVisioConference")}
</Button> </Button>
{hasVideoConference && meetingLink && ( {hasVideoConference && meetingLink && (
@@ -585,14 +597,14 @@ export default function EventFormFields({
mr: 1, mr: 1,
}} }}
> >
Join Visio conference {t("event.form.joinVisioConference")}
</Button> </Button>
<IconButton <IconButton
onClick={handleCopyMeetingLink} onClick={handleCopyMeetingLink}
size="small" size="small"
sx={{ color: "primary.main" }} sx={{ color: "primary.main" }}
aria-label="Copy meeting link" aria-label={t("event.form.copyMeetingLink")}
title="Copy meeting link" title={t("event.form.copyMeetingLink")}
> >
<CopyIcon /> <CopyIcon />
</IconButton> </IconButton>
@@ -600,8 +612,8 @@ export default function EventFormFields({
onClick={handleDeleteVideoConference} onClick={handleDeleteVideoConference}
size="small" size="small"
sx={{ color: "error.main" }} sx={{ color: "error.main" }}
aria-label="Remove video conference" aria-label={t("event.form.removeVideoConference")}
title="Remove video conference" title={t("event.form.removeVideoConference")}
> >
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
@@ -610,11 +622,11 @@ export default function EventFormFields({
</Box> </Box>
</FieldWithLabel> </FieldWithLabel>
<FieldWithLabel label="Location" isExpanded={showMore}> <FieldWithLabel label={t("event.form.location")} isExpanded={showMore}>
<TextField <TextField
fullWidth fullWidth
label={!showMore ? "Location" : ""} label={!showMore ? t("event.form.location") : ""}
placeholder="Add location" placeholder={t("event.form.locationPlaceholder")}
value={location} value={location}
onChange={(e) => setLocation(e.target.value)} onChange={(e) => setLocation(e.target.value)}
size="small" size="small"
@@ -622,65 +634,102 @@ export default function EventFormFields({
/> />
</FieldWithLabel> </FieldWithLabel>
<FieldWithLabel label="Calendar" isExpanded={showMore}> <FieldWithLabel label={t("event.form.calendar")} isExpanded={showMore}>
<FormControl fullWidth margin="dense" size="small"> <FormControl fullWidth margin="dense" size="small">
{!showMore && ( {!showMore && (
<InputLabel id="calendar-select-label">Calendar</InputLabel> <InputLabel id="calendar-select-label">
{t("event.form.calendar")}
</InputLabel>
)} )}
<Select <Select
labelId="calendar-select-label" labelId="calendar-select-label"
value={calendarid ?? ""} value={calendarid ?? ""}
label={!showMore ? "Calendar" : ""} label={!showMore ? t("event.form.calendar") : ""}
displayEmpty displayEmpty
onChange={(e: SelectChangeEvent) => onChange={(e: SelectChangeEvent) =>
handleCalendarChange(e.target.value) handleCalendarChange(e.target.value)
} }
> >
{CalendarItemList(userPersonnalCalendars)} {CalendarItemList(userPersonalCalendars)}
</Select> </Select>
</FormControl> </FormControl>
</FieldWithLabel> </FieldWithLabel>
{showMore && ( {showMore && (
<> <>
<FieldWithLabel label="Notification" isExpanded={showMore}> <FieldWithLabel
label={t("event.form.notification")}
isExpanded={showMore}
>
<FormControl fullWidth margin="dense" size="small"> <FormControl fullWidth margin="dense" size="small">
<Select <Select
labelId="notification" labelId="notification"
value={alarm} value={alarm}
onChange={(e: SelectChangeEvent) => setAlarm(e.target.value)} onChange={(e: SelectChangeEvent) => setAlarm(e.target.value)}
> >
<MenuItem value={""}>No Notification</MenuItem> <MenuItem value="">{t("event.form.notifications.")}</MenuItem>
<MenuItem value={"-PT1M"}>1 minute</MenuItem> <MenuItem value="-PT1M">
<MenuItem value={"-PT5M"}>2 minutes</MenuItem> {t("event.form.notifications.-PT1M")}
<MenuItem value={"-PT10M"}>10 minutes</MenuItem> </MenuItem>
<MenuItem value={"-PT15M"}>15 minutes</MenuItem> <MenuItem value="-PT5M">
<MenuItem value={"-PT30M"}>30 minutes</MenuItem> {t("event.form.notifications.-PT5M")}
<MenuItem value={"-PT1H"}>1 hours</MenuItem> </MenuItem>
<MenuItem value={"-PT2H"}>2 hours</MenuItem> <MenuItem value="-PT10M">
<MenuItem value={"-PT5H"}>5 hours</MenuItem> {t("event.form.notifications.-PT10M")}
<MenuItem value={"-PT12H"}>12 hours</MenuItem> </MenuItem>
<MenuItem value={"-PT1D"}>1 day</MenuItem> <MenuItem value="-PT15M">
<MenuItem value={"-PT2D"}>2 days</MenuItem> {t("event.form.notifications.-PT15M")}
<MenuItem value={"-PT1W"}>1 week</MenuItem> </MenuItem>
<MenuItem value="-PT30M">
{t("event.form.notifications.-PT30M")}
</MenuItem>
<MenuItem value="-PT1H">
{t("event.form.notifications.-PT1H")}
</MenuItem>
<MenuItem value="-PT2H">
{t("event.form.notifications.-PT2H")}
</MenuItem>
<MenuItem value="-PT5H">
{t("event.form.notifications.-PT5H")}
</MenuItem>
<MenuItem value="-PT12H">
{t("event.form.notifications.-PT12H")}
</MenuItem>
<MenuItem value="-PT1D">
{t("event.form.notifications.-PT1D")}
</MenuItem>
<MenuItem value="-PT2D">
{t("event.form.notifications.-PT2D")}
</MenuItem>
<MenuItem value="-PT1W">
{t("event.form.notifications.-PT1W")}
</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</FieldWithLabel> </FieldWithLabel>
<FieldWithLabel label="Show me as" isExpanded={showMore}> <FieldWithLabel
label={t("event.form.showMeAs")}
isExpanded={showMore}
>
<FormControl fullWidth margin="dense" size="small"> <FormControl fullWidth margin="dense" size="small">
<Select <Select
labelId="busy" labelId="busy"
value={busy} value={busy}
onChange={(e: SelectChangeEvent) => setBusy(e.target.value)} onChange={(e: SelectChangeEvent) => setBusy(e.target.value)}
> >
<MenuItem value={"TRANSPARENT"}>Free</MenuItem> <MenuItem value={"TRANSPARENT"}>
<MenuItem value={"OPAQUE"}>Busy </MenuItem> {t("event.form.free")}
</MenuItem>
<MenuItem value={"OPAQUE"}>{t("event.form.busy")}</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</FieldWithLabel> </FieldWithLabel>
<FieldWithLabel label="Visible to" isExpanded={showMore}> <FieldWithLabel
label={t("event.form.visibleTo")}
isExpanded={showMore}
>
<ToggleButtonGroup <ToggleButtonGroup
value={eventClass} value={eventClass}
exclusive exclusive
@@ -693,11 +742,11 @@ export default function EventFormFields({
> >
<ToggleButton value="PUBLIC" sx={{ width: "140px" }}> <ToggleButton value="PUBLIC" sx={{ width: "140px" }}>
<PublicIcon sx={{ mr: 1, fontSize: "16px" }} /> <PublicIcon sx={{ mr: 1, fontSize: "16px" }} />
All {t("event.form.visibleAll")}
</ToggleButton> </ToggleButton>
<ToggleButton value="PRIVATE" sx={{ width: "140px" }}> <ToggleButton value="PRIVATE" sx={{ width: "140px" }}>
<LockIcon sx={{ mr: 1, fontSize: "16px" }} /> <LockIcon sx={{ mr: 1, fontSize: "16px" }} />
Participants {t("event.form.visibleParticipants")}
</ToggleButton> </ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
</FieldWithLabel> </FieldWithLabel>
+35 -12
View File
@@ -15,6 +15,7 @@ import {
} from "@mui/material"; } from "@mui/material";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { RepetitionObject } from "../../features/Events/EventsTypes"; import { RepetitionObject } from "../../features/Events/EventsTypes";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
export default function RepeatEvent({ export default function RepeatEvent({
repetition, repetition,
@@ -27,6 +28,7 @@ export default function RepeatEvent({
setRepetition: Function; setRepetition: Function;
isOwn?: boolean; isOwn?: boolean;
}) { }) {
const { t } = useI18n();
const days = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]; const days = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
const day = new Date(eventStart); const day = new Date(eventStart);
// derive endOption based on repetition // derive endOption based on repetition
@@ -60,12 +62,25 @@ export default function RepeatEvent({
}); });
}; };
const getDayLabel = (day: string) => {
const dayMap: { [key: string]: string } = {
MO: t("event.repeat.days.monday"),
TU: t("event.repeat.days.tuesday"),
WE: t("event.repeat.days.wednesday"),
TH: t("event.repeat.days.thursday"),
FR: t("event.repeat.days.friday"),
SA: t("event.repeat.days.saturday"),
SU: t("event.repeat.days.sunday"),
};
return dayMap[day] || day;
};
return ( return (
<Box> <Box>
<Stack> <Stack>
{/* Interval */} {/* Interval */}
<Box display="flex" alignItems="center" gap={2} mb={2}> <Box display="flex" alignItems="center" gap={2} mb={2}>
<Typography>Repeat every</Typography> <Typography>{t("event.repeat.repeatEvery")}</Typography>
<TextField <TextField
type="number" type="number"
value={repetition.interval ?? 1} value={repetition.interval ?? 1}
@@ -104,10 +119,18 @@ export default function RepeatEvent({
} }
}} }}
> >
<MenuItem value={"daily"}>Day(s)</MenuItem> <MenuItem value={"daily"}>
<MenuItem value={"weekly"}>Week(s)</MenuItem> {t("event.repeat.frequency.days")}
<MenuItem value={"monthly"}>Month(s)</MenuItem> </MenuItem>
<MenuItem value={"yearly"}>Year(s)</MenuItem> <MenuItem value={"weekly"}>
{t("event.repeat.frequency.weeks")}
</MenuItem>
<MenuItem value={"monthly"}>
{t("event.repeat.frequency.months")}
</MenuItem>
<MenuItem value={"yearly"}>
{t("event.repeat.frequency.years")}
</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</Box> </Box>
@@ -116,7 +139,7 @@ export default function RepeatEvent({
{repetition.freq === "weekly" && ( {repetition.freq === "weekly" && (
<Box> <Box>
<Typography variant="body2" gutterBottom> <Typography variant="body2" gutterBottom>
Repeat on: {t("event.repeat.repeatOn")}
</Typography> </Typography>
<FormGroup row> <FormGroup row>
{days.map((day) => ( {days.map((day) => (
@@ -129,7 +152,7 @@ export default function RepeatEvent({
onChange={() => handleDayChange(day)} onChange={() => handleDayChange(day)}
/> />
} }
label={day} label={getDayLabel(day)}
/> />
))} ))}
</FormGroup> </FormGroup>
@@ -139,7 +162,7 @@ export default function RepeatEvent({
{/* End options */} {/* End options */}
<Box> <Box>
<Typography variant="body2" gutterBottom> <Typography variant="body2" gutterBottom>
End: {t("event.repeat.end.label")}
</Typography> </Typography>
<RadioGroup <RadioGroup
value={endOption} value={endOption}
@@ -170,7 +193,7 @@ export default function RepeatEvent({
disabled={!isOwn} disabled={!isOwn}
value="never" value="never"
control={<Radio />} control={<Radio />}
label="Never" label={t("event.repeat.end.never")}
/> />
<FormControlLabel <FormControlLabel
@@ -179,7 +202,7 @@ export default function RepeatEvent({
control={<Radio />} control={<Radio />}
label={ label={
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
After {t("event.repeat.end.after")}
<TextField <TextField
type="number" type="number"
size="small" size="small"
@@ -195,7 +218,7 @@ export default function RepeatEvent({
inputProps={{ min: 1, "data-testid": "occurrences-input" }} inputProps={{ min: 1, "data-testid": "occurrences-input" }}
disabled={!isOwn || endOption !== "after"} disabled={!isOwn || endOption !== "after"}
/> />
occurrences {t("event.repeat.end.occurrences")}
</Box> </Box>
} }
/> />
@@ -206,7 +229,7 @@ export default function RepeatEvent({
control={<Radio />} control={<Radio />}
label={ label={
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
On {t("event.repeat.end.on")}
<TextField <TextField
type="date" type="date"
inputProps={{ "data-testid": "end-date" }} inputProps={{ "data-testid": "end-date" }}
@@ -6,8 +6,10 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs, { Dayjs } from "dayjs"; import dayjs, { Dayjs } from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat"; import customParseFormat from "dayjs/plugin/customParseFormat";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import { LONG_DATE_FORMAT } from "../utils/dateTimeFormatters"; import { LONG_DATE_FORMAT } from "../utils/dateTimeFormatters";
import "dayjs/locale/fr";
import "dayjs/locale/en";
dayjs.extend(customParseFormat); dayjs.extend(customParseFormat);
/** /**
@@ -54,15 +56,21 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
onEndDateChange, onEndDateChange,
onEndTimeChange, onEndTimeChange,
}) => { }) => {
const { t, lang } = useI18n();
const isExpanded = showMore; const isExpanded = showMore;
const shouldShowEndDateNormal = allday || !!showEndDate; const shouldShowEndDateNormal = allday || !!showEndDate;
const shouldShowFullFieldsInNormal = !allday && hasEndDateChanged; const shouldShowFullFieldsInNormal = !allday && hasEndDateChanged;
const showSingleDateField = const showSingleDateField =
!isExpanded && !shouldShowEndDateNormal && !shouldShowFullFieldsInNormal; !isExpanded && !shouldShowEndDateNormal && !shouldShowFullFieldsInNormal;
const startDateLabel = showSingleDateField ? "Date" : "Start Date"; const startDateLabel = showSingleDateField
? t("dateTimeFields.date")
: t("dateTimeFields.startDate");
return ( return (
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="en"> <LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale={lang ?? "en"}
>
<Box <Box
display="flex" display="flex"
flexDirection="column" flexDirection="column"
@@ -73,7 +81,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
<Box display="flex" gap={1} flexDirection="row" alignItems="center"> <Box display="flex" gap={1} flexDirection="row" alignItems="center">
<Box sx={{ maxWidth: "300px", width: "48%" }}> <Box sx={{ maxWidth: "300px", width: "48%" }}>
<DatePicker <DatePicker
label="Start Date" label={t("dateTimeFields.startDate")}
format={LONG_DATE_FORMAT} format={LONG_DATE_FORMAT}
value={startDate ? dayjs(startDate) : null} value={startDate ? dayjs(startDate) : null}
onChange={(newValue) => { onChange={(newValue) => {
@@ -99,7 +107,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
{!allday && ( {!allday && (
<Box sx={{ width: "110px" }}> <Box sx={{ width: "110px" }}>
<TimePicker <TimePicker
label="Start Time" label={t("dateTimeFields.startTime")}
ampm={false} ampm={false}
value={startTime ? dayjs(startTime, "HH:mm") : null} value={startTime ? dayjs(startTime, "HH:mm") : null}
onChange={(newValue) => { onChange={(newValue) => {
@@ -127,7 +135,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
<Box display="flex" gap={1} flexDirection="row" alignItems="center"> <Box display="flex" gap={1} flexDirection="row" alignItems="center">
<Box sx={{ maxWidth: "300px", width: "48%" }}> <Box sx={{ maxWidth: "300px", width: "48%" }}>
<DatePicker <DatePicker
label="End Date" label={t("dateTimeFields.endDate")}
format={LONG_DATE_FORMAT} format={LONG_DATE_FORMAT}
value={endDate ? dayjs(endDate) : null} value={endDate ? dayjs(endDate) : null}
onChange={(newValue) => { onChange={(newValue) => {
@@ -154,7 +162,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
{!allday && ( {!allday && (
<Box sx={{ width: "110px" }}> <Box sx={{ width: "110px" }}>
<TimePicker <TimePicker
label="End Time" label={t("dateTimeFields.endTime")}
ampm={false} ampm={false}
value={endTime ? dayjs(endTime, "HH:mm") : null} value={endTime ? dayjs(endTime, "HH:mm") : null}
onChange={(newValue) => { onChange={(newValue) => {
@@ -185,7 +193,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
<Box display="flex" gap={1} flexDirection="row" alignItems="center"> <Box display="flex" gap={1} flexDirection="row" alignItems="center">
<Box sx={{ maxWidth: "300px", width: "48%" }}> <Box sx={{ maxWidth: "300px", width: "48%" }}>
<DatePicker <DatePicker
label="Start Date" label={t("dateTimeFields.startDate")}
format={LONG_DATE_FORMAT} format={LONG_DATE_FORMAT}
value={startDate ? dayjs(startDate) : null} value={startDate ? dayjs(startDate) : null}
onChange={(newValue) => { onChange={(newValue) => {
@@ -210,7 +218,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
</Box> </Box>
<Box sx={{ maxWidth: "300px", width: "48%" }}> <Box sx={{ maxWidth: "300px", width: "48%" }}>
<DatePicker <DatePicker
label="End Date" label={t("dateTimeFields.endDate")}
format={LONG_DATE_FORMAT} format={LONG_DATE_FORMAT}
value={endDate ? dayjs(endDate) : null} value={endDate ? dayjs(endDate) : null}
onChange={(newValue) => { onChange={(newValue) => {
@@ -264,7 +272,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
</Box> </Box>
<Box sx={{ maxWidth: "110px" }}> <Box sx={{ maxWidth: "110px" }}>
<TimePicker <TimePicker
label="Start Time" label={t("dateTimeFields.startTime")}
ampm={false} ampm={false}
value={startTime ? dayjs(startTime, "HH:mm") : null} value={startTime ? dayjs(startTime, "HH:mm") : null}
onChange={(newValue) => { onChange={(newValue) => {
@@ -290,7 +298,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
</Box> </Box>
<Box sx={{ maxWidth: "110px" }}> <Box sx={{ maxWidth: "110px" }}>
<TimePicker <TimePicker
label="End Time" label={t("dateTimeFields.endTime")}
ampm={false} ampm={false}
value={endTime ? dayjs(endTime, "HH:mm") : null} value={endTime ? dayjs(endTime, "HH:mm") : null}
onChange={(newValue) => { onChange={(newValue) => {
+3 -2
View File
@@ -17,6 +17,7 @@ import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils";
export function renderAttendeeBadge( export function renderAttendeeBadge(
a: userAttendee, a: userAttendee,
key: string, key: string,
t: Function,
isFull?: boolean, isFull?: boolean,
isOrganizer?: boolean isOrganizer?: boolean
) { ) {
@@ -28,7 +29,7 @@ export function renderAttendeeBadge(
) : null; ) : null;
if (!isFull) { if (!isFull) {
return <Avatar {...stringAvatar(a.cn || a.cal_address)} />; return <Avatar key={key} {...stringAvatar(a.cn || a.cal_address)} />;
} else { } else {
return ( return (
<Box <Box
@@ -77,7 +78,7 @@ export function renderAttendeeBadge(
</Typography> </Typography>
{isOrganizer && ( {isOrganizer && (
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
Organizer {t("event.organizer")}
</Typography> </Typography>
)} )}
</Box> </Box>
+95 -27
View File
@@ -20,6 +20,8 @@ import {
} from "@mui/material"; } from "@mui/material";
import { push } from "redux-first-history"; import { push } from "redux-first-history";
import { CalendarApi } from "@fullcalendar/core"; import { CalendarApi } from "@fullcalendar/core";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import { setLanguage } from "../../features/Settings/SettingsSlice";
export type AppIconProps = { export type AppIconProps = {
name: string; name: string;
@@ -44,9 +46,11 @@ export function Menubar({
currentView, currentView,
onViewChange, onViewChange,
}: MenubarProps) { }: MenubarProps) {
const { t, f, lang } = useI18n();
const user = useAppSelector((state) => state.user.userData); const user = useAppSelector((state) => state.user.userData);
const applist: AppIconProps[] = (window as any).appList ?? []; const applist: AppIconProps[] = (window as any).appList ?? [];
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [langAnchorEl, setLangAnchorEl] = useState<null | HTMLElement>(null);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
if (!user) { if (!user) {
@@ -99,6 +103,18 @@ export function Menubar({
}; };
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const langOpen = Boolean(langAnchorEl);
const handleLangClick = (event: React.MouseEvent<HTMLElement>) => {
setLangAnchorEl(event.currentTarget);
};
const handleLangClose = () => setLangAnchorEl(null);
const availableLangs = [
{ code: "en", label: "English" },
{ code: "fr", label: "Français" },
];
return ( return (
<> <>
<header className="menubar"> <header className="menubar">
@@ -112,7 +128,9 @@ export function Menubar({
<Button onClick={() => handleNavigation("prev")}> <Button onClick={() => handleNavigation("prev")}>
<ChevronLeftIcon /> <ChevronLeftIcon />
</Button> </Button>
<Button onClick={() => handleNavigation("today")}>Today</Button> <Button onClick={() => handleNavigation("today")}>
{t("menubar.today")}
</Button>
<Button onClick={() => handleNavigation("next")}> <Button onClick={() => handleNavigation("next")}>
<ChevronRightIcon /> <ChevronRightIcon />
</Button> </Button>
@@ -122,17 +140,18 @@ export function Menubar({
<div className="menu-items"> <div className="menu-items">
<div className="current-date-time"> <div className="current-date-time">
<Typography variant="h6" component="div"> <Typography variant="h6" component="div">
{currentDate.toLocaleDateString("en-US", { {f(currentDate, "MMMM yyyy")}
month: "long",
year: "numeric",
})}
</Typography> </Typography>
</div> </div>
</div> </div>
</div> </div>
<div className="right-menu"> <div className="right-menu">
<div className="menu-items"> <div className="menu-items">
<IconButton onClick={onRefresh}> <IconButton
onClick={onRefresh}
aria-label={t("menubar.refresh")}
title={t("menubar.refresh")}
>
<RefreshIcon /> <RefreshIcon />
</IconButton> </IconButton>
</div> </div>
@@ -145,37 +164,53 @@ export function Menubar({
value={currentView} value={currentView}
onChange={(e) => handleViewChange(e.target.value)} onChange={(e) => handleViewChange(e.target.value)}
variant="outlined" variant="outlined"
aria-label={t("menubar.viewSelector")}
> >
<MenuItem value="dayGridMonth">Month</MenuItem> <MenuItem value="dayGridMonth">
<MenuItem value="timeGridWeek">Week</MenuItem> {t("menubar.views.month")}
<MenuItem value="timeGridDay">Day</MenuItem> </MenuItem>
<MenuItem value="timeGridWeek">
{t("menubar.views.week")}
</MenuItem>
<MenuItem value="timeGridDay">
{t("menubar.views.day")}
</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</div> </div>
<div className="menu-items"> <div className="menu-items">
{applist.length > 0 && ( {applist.length > 0 && (
<IconButton onClick={handleOpen} style={{ marginRight: 8 }}> <IconButton
onClick={handleOpen}
style={{ marginRight: 8 }}
aria-label={t("menubar.apps")}
title={t("menubar.apps")}
>
<AppsIcon /> <AppsIcon />
</IconButton> </IconButton>
)} )}
</div> </div>
<div className="menu-items"> <div className="menu-items">
<Avatar <IconButton onClick={handleLangClick}>
style={{ <Avatar
backgroundColor: stringToColor( style={{
user && user.family_name backgroundColor: stringToColor(
? user.family_name user && user.family_name
: user && user.email ? user.family_name
? user.email : user && user.email
: "" ? user.email
), : ""
}} ),
sizes="large" }}
> sizes="large"
{user?.name && user?.family_name aria-label={t("menubar.userProfile")}
? `${user.name[0]}${user.family_name[0]}` >
: (user?.email?.[0] ?? "")} {user?.name && user?.family_name
</Avatar> ? `${user.name[0]}${user.family_name[0]}`
: (user?.email?.[0] ?? "")}
</Avatar>
</IconButton>
</div> </div>
</div> </div>
</header> </header>
@@ -198,13 +233,46 @@ export function Menubar({
))} ))}
</div> </div>
</Popover> </Popover>
<Popover
open={langOpen}
anchorEl={langAnchorEl}
onClose={handleLangClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
>
<FormControl size="small" style={{ minWidth: 100, marginRight: 8 }}>
<Select
value={lang}
onChange={(e) => {
dispatch(setLanguage(e.target.value));
}}
variant="outlined"
aria-label={t("menubar.languageSelector")}
>
{availableLangs.map(({ code, label }) => (
<MenuItem key={code} value={code}>
{label}
</MenuItem>
))}
</Select>
</FormControl>
</Popover>
</> </>
); );
} }
export function MainTitle() { export function MainTitle() {
const { t } = useI18n();
return ( return (
<div className="menubar-item tc-home"> <div className="menubar-item tc-home">
<img className="logo" src={logo} alt="Calendar" /> <img className="logo" src={logo} alt={t("menubar.logoAlt")} />
</div> </div>
); );
} }
-640
View File
@@ -1,640 +0,0 @@
import CircleIcon from "@mui/icons-material/Circle";
import CloseIcon from "@mui/icons-material/Close";
import DeleteIcon from "@mui/icons-material/Delete";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import VideocamIcon from "@mui/icons-material/Videocam";
import {
Box,
Button,
ButtonGroup,
Card,
CardActions,
CardContent,
CardHeader,
Checkbox,
Divider,
FormControl,
FormControlLabel,
IconButton,
InputLabel,
MenuItem,
Modal,
Select,
SelectChangeEvent,
TextField,
Typography,
} from "@mui/material";
import { useEffect, useState } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import AttendeeSelector from "../../components/Attendees/AttendeeSearch";
import {
handleDelete,
handleRSVP,
} from "../../components/Event/eventHandlers/eventHandlers";
import RepeatEvent from "../../components/Event/EventRepeat";
import { InfoRow } from "../../components/Event/InfoRow";
import { refreshCalendars } from "../../components/Event/utils/eventUtils";
import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils";
import { getCalendarRange } from "../../utils/dateUtils";
import {
moveEventAsync,
putEventAsync,
removeEvent,
updateEventInstanceAsync,
updateSeriesAsync,
} from "../Calendars/CalendarSlice";
import { Calendars } from "../Calendars/CalendarTypes";
import { userAttendee } from "../User/userDataTypes";
import { getEvent } from "./EventApi";
import { formatLocalDateTime } from "../../components/Event/utils/dateTimeFormatters";
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
export default function EventDisplayModal({
eventId,
calId,
open,
onClose,
typeOfAction,
}: {
eventId: string;
calId: string;
open: boolean;
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
typeOfAction?: "solo" | "all";
}) {
const dispatch = useAppDispatch();
const calendar = useAppSelector((state) => state.calendars.list[calId]);
const event = useAppSelector(
(state) => state.calendars.list[calId]?.events[eventId]
);
const user = useAppSelector((state) => state.user);
const [showAllAttendees, setShowAllAttendees] = useState(false);
const [showMore, setShowMore] = useState(false);
const calendars = Object.values(
useAppSelector((state) => state.calendars.list)
);
const userPersonnalCalendars: Calendars[] = calendars.filter(
(c) => c.id?.split("/")[0] === user.userData?.openpaasId
);
// Form state
const [title, setTitle] = useState(event?.title ?? "");
const [description, setDescription] = useState(event?.description ?? "");
const [location, setLocation] = useState(event?.location ?? "");
const [start, setStart] = useState(
formatLocalDateTime(new Date(event?.start ?? Date.now()))
);
const [end, setEnd] = useState(
formatLocalDateTime(new Date(event?.end ?? Date.now()))
);
const [allday, setAllDay] = useState(event?.allday ?? false);
const [repetition, setRepetition] = useState<RepetitionObject>(
event?.repetition ?? ({} as RepetitionObject)
);
const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? "");
const [busy, setBusy] = useState(event?.transp ?? "OPAQUE");
const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC");
const [timezone, setTimezone] = useState(event?.timezone ?? "UTC");
const [newCalId, setNewCalId] = useState(event?.calId);
const [calendarid, setCalendarid] = useState(
calId.split("/")[0] === user.userData?.openpaasId
? userPersonnalCalendars.findIndex((cal) => cal.id === calId)
: calendars.findIndex((cal) => cal.id === calId)
);
const [attendees, setAttendees] = useState(
(event?.attendee || []).filter(
(a) => a.cal_address !== event?.organizer?.cal_address
)
);
const currentUserAttendee = event?.attendee?.find(
(person) => person.cal_address === user.userData.email
);
const organizer =
event?.attendee?.find(
(a) => a.cal_address === event?.organizer?.cal_address
) ?? ({} as userAttendee);
const isOwn = organizer?.cal_address === user.userData.email;
const isOwnCal = userPersonnalCalendars.find((cal) => cal.id === calId);
const attendeeDisplayLimit = 3;
useEffect(() => {
if (!event || !calendar) {
onClose({}, "backdropClick");
}
setRepetition(event?.repetition ?? ({} as RepetitionObject));
}, [open, eventId, dispatch, onClose, event, calendar]);
useEffect(() => {
const fetchMasterEvent = async () => {
const masterEvent = await getEvent(event);
setTitle(masterEvent.title ?? "");
setDescription(masterEvent.description ?? "");
setLocation(masterEvent.location ?? "");
setStart(formatLocalDateTime(new Date(masterEvent?.start ?? Date.now())));
setEnd(formatLocalDateTime(new Date(masterEvent?.end ?? Date.now())));
setAllDay(masterEvent.allday ?? false);
setRepetition(masterEvent?.repetition ?? ({} as RepetitionObject));
setAlarm(masterEvent?.alarm?.trigger ?? "");
setBusy(masterEvent?.transp ?? "OPAQUE");
setEventClass(masterEvent?.class ?? "PUBLIC");
setTimezone(masterEvent.timezone ?? "UTC");
};
if (typeOfAction === "all") {
fetchMasterEvent();
}
}, [typeOfAction, event]);
if (!event || !calendar) return null;
const isRecurring = event.uid?.includes("/");
const handleSave = async () => {
const newEventUID = crypto.randomUUID();
const newEvent: CalendarEvent = {
calId,
title,
URL: event.URL ?? `/calendars/${calId}/${newEventUID}.ics`,
start: start,
end: end,
allday,
uid: event.uid ?? newEventUID,
description,
location,
repetition,
class: eventClass,
organizer: event.organizer,
timezone,
attendee: [organizer, ...attendees],
transp: busy,
color: userPersonnalCalendars[calendarid]?.color,
alarm: { trigger: alarm, action: "EMAIL" },
};
const [baseId, recurrenceId] = event.uid.split("/");
const calendarRange = getCalendarRange(new Date(start));
if (typeOfAction === "solo") {
dispatch(
updateEventInstanceAsync({
cal: userPersonnalCalendars[calendarid],
event: { ...newEvent, recurrenceId: recurrenceId },
})
);
} else if (typeOfAction === "all") {
dispatch(
updateSeriesAsync({
cal: userPersonnalCalendars[calendarid],
event: { ...newEvent, recurrenceId: recurrenceId },
})
);
await refreshCalendars(dispatch, calendars, calendarRange);
} else {
dispatch(
putEventAsync({ cal: userPersonnalCalendars[calendarid], newEvent })
);
}
if (newCalId !== calId) {
dispatch(
moveEventAsync({
cal: userPersonnalCalendars[calendarid],
newEvent,
newURL: `/calendars/${newCalId}/${baseId}.ics`,
})
);
dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid }));
}
onClose({}, "backdropClick");
};
const handleToggleShowMore = async () => {
setShowMore(!showMore);
};
const calList =
calId.split("/")[0] === user.userData?.openpaasId
? Object.keys(userPersonnalCalendars).map((calendar, index) => (
<MenuItem key={index} value={index}>
<Typography variant="body2">
<CircleIcon
style={{
color:
userPersonnalCalendars[index].color?.light ?? "#3788D8",
width: 12,
height: 12,
}}
/>
{userPersonnalCalendars[index].name}
</Typography>
</MenuItem>
))
: Object.keys(calendars).map((calendar, index) => (
<MenuItem key={index} value={index}>
<Typography variant="body2">
<CircleIcon
style={{
color: calendars[index].color?.light ?? "#3788D8",
width: 12,
height: 12,
}}
/>
{calendars[index].name} - {calendars[index].owner}
</Typography>
</MenuItem>
));
return (
<Modal open={open} onClose={onClose}>
<Box
style={{
position: "absolute",
top: "5vh",
left: "50%",
transform: "translate(-50%, -50%)",
width: "50vw",
maxHeight: "80vh",
}}
>
<Card style={{ padding: 16, position: "absolute" }}>
{/* Close button */}
<Box style={{ position: "absolute", top: 8, right: 8 }}>
<IconButton
size="small"
onClick={() => onClose({}, "backdropClick")}
>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
<CardHeader title={isOwn ? "Edit Event" : "Event Details"} />
<CardContent style={{ maxHeight: "75vh", overflow: "auto" }}>
{/* Title */}
<TextField
fullWidth
disabled={!isOwn}
label="Title"
value={title ?? ""}
onChange={(e) => setTitle(e.target.value)}
size="small"
margin="dense"
/>
{/* RSVP */}
{currentUserAttendee && isOwnCal && (
<Card style={{ margin: "8px 0" }}>
<ButtonGroup size="small" fullWidth>
<Button
color={
currentUserAttendee.partstat === "ACCEPTED"
? "success"
: "primary"
}
onClick={() =>
handleRSVP(
dispatch,
calendar,
user,
event,
"ACCEPTED",
undefined,
isRecurring ? typeOfAction : undefined
)
}
>
Accept
</Button>
<Button
color={
currentUserAttendee.partstat === "TENTATIVE"
? "warning"
: "primary"
}
onClick={() =>
handleRSVP(
dispatch,
calendar,
user,
event,
"TENTATIVE",
undefined,
isRecurring ? typeOfAction : undefined
)
}
>
Maybe
</Button>
<Button
color={
currentUserAttendee.partstat === "DECLINED"
? "error"
: "primary"
}
onClick={() =>
handleRSVP(
dispatch,
calendar,
user,
event,
"DECLINED",
undefined,
isRecurring ? typeOfAction : undefined
)
}
>
Decline
</Button>
<Button
color="primary"
onClick={() => console.log("proposenewtime")}
>
Propose new time
</Button>
</ButtonGroup>
</Card>
)}
{/* Calendar selector */}
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="calendar-select-label">Calendar</InputLabel>
<Select
disabled={!isOwn}
labelId="calendar-select-label"
value={calendarid.toString() ?? ""}
label="Calendar"
onChange={(e: SelectChangeEvent) => {
const newId = Number(e.target.value);
setCalendarid(newId);
setNewCalId(userPersonnalCalendars[newId].id);
}}
>
{calList}
</Select>
</FormControl>
{/* Dates */}
<TextField
fullWidth
label="Start"
disabled={!isOwn}
type={allday ? "date" : "datetime-local"}
value={allday ? start.split("T")[0] : start.slice(0, 16)}
onChange={(e) =>
setStart(formatLocalDateTime(new Date(e.target.value)))
}
size="small"
margin="dense"
InputLabelProps={{ shrink: true }}
/>
<TextField
fullWidth
disabled={!isOwn}
label="End"
type={allday ? "date" : "datetime-local"}
value={allday ? end.split("T")[0] : end.slice(0, 16)}
onChange={(e) =>
setEnd(formatLocalDateTime(new Date(e.target.value)))
}
size="small"
margin="dense"
InputLabelProps={{ shrink: true }}
/>
<FormControlLabel
control={
<Checkbox
disabled={!isOwn}
checked={allday}
onChange={() => {
const endDate = new Date(end);
const startDate = new Date(start);
setAllDay(!allday);
if (endDate.getDate() === startDate.getDate()) {
endDate.setDate(startDate.getDate() + 1);
setEnd(formatLocalDateTime(endDate));
}
}}
/>
}
label="All day"
/>
{/* Description & Location */}
<TextField
fullWidth
disabled={!isOwn}
label="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
size="small"
margin="dense"
multiline
rows={2}
/>
{isOwn && (
<AttendeeSelector
attendees={attendees}
setAttendees={setAttendees}
/>
)}
<TextField
fullWidth
label="Location"
disabled={!isOwn}
value={location}
onChange={(e) => setLocation(e.target.value)}
size="small"
margin="dense"
/>
{/* Video */}
{event.x_openpass_videoconference && (
<InfoRow
icon={<VideocamIcon style={{ fontSize: 18 }} />}
content={
<Button
variant="contained"
onClick={() =>
window.open(event.x_openpass_videoconference)
}
>
Join the video conference
</Button>
}
/>
)}
{/* Attendees */}
{event.attendee?.length > 0 && (
<Box style={{ marginBottom: 8 }}>
<Typography variant="subtitle2">Attendees:</Typography>
{organizer.cal_address &&
renderAttendeeBadge(organizer, "org", true, true)}
{(showAllAttendees
? attendees
: attendees.slice(0, attendeeDisplayLimit)
).map((a, idx) => (
<Box key={a.cal_address}>
{renderAttendeeBadge(a, idx.toString(), true)}
{isOwn && (
<IconButton
size="small"
onClick={() => {
const newAttendeesList = [...attendees];
newAttendeesList.splice(idx, 1);
setAttendees(newAttendeesList);
}}
>
<CloseIcon fontSize="small" />
</IconButton>
)}
</Box>
))}
{attendees.length > attendeeDisplayLimit && (
<Typography
variant="body2"
color="primary"
style={{ cursor: "pointer", marginTop: 4 }}
onClick={() => setShowAllAttendees(!showAllAttendees)}
>
{showAllAttendees
? "Show less"
: `Show more (${
attendees.length - attendeeDisplayLimit
} more)`}
</Typography>
)}
</Box>
)}
<Divider style={{ margin: "8px 0" }} />
{/* Extended options */}
{showMore && (
<>
{isOwn && (
<RepeatEvent
repetition={repetition}
eventStart={new Date(event.start)}
setRepetition={setRepetition}
isOwn={isOwn && typeOfAction !== "solo"}
/>
)}
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="notification">Notification</InputLabel>
<Select
labelId="notification"
label="Notification"
value={alarm}
disabled={!isOwn}
onChange={(e: SelectChangeEvent) =>
setAlarm(e.target.value)
}
>
<MenuItem value={""}>No Notification</MenuItem>
<MenuItem value={"-PT1M"}>1 minute</MenuItem>
<MenuItem value={"-PT5M"}>2 minutes</MenuItem>
<MenuItem value={"-PT10M"}>10 minutes</MenuItem>
<MenuItem value={"-PT15M"}>15 minutes</MenuItem>
<MenuItem value={"-PT30M"}>30 minutes</MenuItem>
<MenuItem value={"-PT1H"}>1 hours</MenuItem>
<MenuItem value={"-PT2H"}>2 hours</MenuItem>
<MenuItem value={"-PT5H"}>5 hours</MenuItem>
<MenuItem value={"-PT12H"}>12 hours</MenuItem>
<MenuItem value={"-PT1D"}>1 day</MenuItem>
<MenuItem value={"-PT2D"}>2 days</MenuItem>
<MenuItem value={"-PT1W"}>1 week</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="Visibility">Visibility</InputLabel>
<Select
labelId="Visibility"
label="Visibility"
value={eventClass}
disabled={!isOwn}
onChange={(e: SelectChangeEvent) =>
setEventClass(e.target.value)
}
>
<MenuItem value={"PUBLIC"}>Public</MenuItem>
<MenuItem value={"CONFIDENTIAL"}>Show time only</MenuItem>
<MenuItem value={"PRIVATE"}>Private</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="busy">is Busy</InputLabel>
<Select
labelId="busy"
value={busy}
disabled={!isOwn}
label="is busy"
onChange={(e: SelectChangeEvent) => setBusy(e.target.value)}
>
<MenuItem value={"TRANSPARENT"}>Free</MenuItem>
<MenuItem value={"OPAQUE"}>Busy </MenuItem>
</Select>
</FormControl>
{/* Error */}
{event.error && (
<InfoRow
icon={
<ErrorOutlineIcon
color="error"
style={{ fontSize: 18 }}
/>
}
text={event.error}
error
/>
)}
</>
)}
</CardContent>
<CardActions>
<ButtonGroup>
{isOwn && (
<IconButton
size="small"
onClick={() =>
handleDelete(
isRecurring,
typeOfAction,
onClose,
dispatch,
calendar,
event,
calId,
eventId
)
}
>
<DeleteIcon fontSize="small" />
</IconButton>
)}
<Button size="small" onClick={handleToggleShowMore}>
{showMore ? "Show Less" : "Show More"}
</Button>
{isOwn && (
<Button size="small" onClick={handleSave}>
Save
</Button>
)}
</ButtonGroup>
</CardActions>
</Card>
</Box>
</Modal>
);
}
+103 -63
View File
@@ -42,9 +42,9 @@ import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils";
import { getCalendarRange } from "../../utils/dateUtils"; import { getCalendarRange } from "../../utils/dateUtils";
import { deleteEventAsync } from "../Calendars/CalendarSlice"; import { deleteEventAsync } from "../Calendars/CalendarSlice";
import { dlEvent } from "./EventApi"; import { dlEvent } from "./EventApi";
import EventDisplayModal from "./EventDisplay";
import { CalendarEvent } from "./EventsTypes"; import { CalendarEvent } from "./EventsTypes";
import EventUpdateModal from "./EventUpdateModal"; import EventUpdateModal from "./EventUpdateModal";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
export default function EventPreviewModal({ export default function EventPreviewModal({
eventId, eventId,
calId, calId,
@@ -58,6 +58,7 @@ export default function EventPreviewModal({
open: boolean; open: boolean;
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void; onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
}) { }) {
const { t } = useI18n();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const calendars = useAppSelector((state) => state.calendars); const calendars = useAppSelector((state) => state.calendars);
const timezone = const timezone =
@@ -75,7 +76,6 @@ export default function EventPreviewModal({
const isRecurring = event?.uid?.includes("/"); const isRecurring = event?.uid?.includes("/");
const isOwn = calendar.ownerEmails?.includes(user.userData.email); const isOwn = calendar.ownerEmails?.includes(user.userData.email);
const [showAllAttendees, setShowAllAttendees] = useState(false); const [showAllAttendees, setShowAllAttendees] = useState(false);
const [openFullDisplay, setOpenFullDisplay] = useState(false);
const [openUpdateModal, setOpenUpdateModal] = useState(false); const [openUpdateModal, setOpenUpdateModal] = useState(false);
const [openDuplicateModal, setOpenDuplicateModal] = useState(false); const [openDuplicateModal, setOpenDuplicateModal] = useState(false);
const [hidePreview, setHidePreview] = useState(false); const [hidePreview, setHidePreview] = useState(false);
@@ -210,7 +210,7 @@ export default function EventPreviewModal({
) )
} }
> >
Email attendees {t("eventPreview.emailAttendees")}
</MenuItem> </MenuItem>
)} )}
<EventDuplication <EventDuplication
@@ -254,7 +254,7 @@ export default function EventPreviewModal({
updateTempList(); updateTempList();
}} }}
> >
Delete event {t("eventPreview.deleteEvent")}
</MenuItem> </MenuItem>
)} )}
</Menu> </Menu>
@@ -270,9 +270,7 @@ export default function EventPreviewModal({
{event.class === "PRIVATE" && {event.class === "PRIVATE" &&
(isOwn ? ( (isOwn ? (
<Tooltip <Tooltip
title={ title={t("eventPreview.privateEvent.tooltipOwn")}
"Only you and attendees can see the details of this event."
}
placement="top" placement="top"
> >
<LockOutlineIcon /> <LockOutlineIcon />
@@ -294,23 +292,21 @@ export default function EventPreviewModal({
</Typography> </Typography>
{event.transp === "TRANSPARENT" && ( {event.transp === "TRANSPARENT" && (
<Tooltip <Tooltip
title={ title={t("eventPreview.free.tooltip")}
"Others see you as available during the time range of this event."
}
placement="top" placement="top"
> >
<Chip <Chip
icon={<CircleIcon color="success" fontSize="small" />} icon={<CircleIcon color="success" fontSize="small" />}
label="Free" label={t("eventPreview.free.label")}
/> />
</Tooltip> </Tooltip>
)} )}
</Box> </Box>
<Typography color="text.secondaryContainer" gutterBottom> <Typography color="text.secondaryContainer" gutterBottom>
{formatDate(event.start, event.allday)} {formatDate(event.start, t, event.allday)}
{event.end && {event.end &&
formatEnd(event.start, event.end, event.allday) && formatEnd(event.start, event.end, t, event.allday) &&
` ${formatEnd(event.start, event.end, event.allday)} ${!event.allday && getTimezoneOffset(timezone)}`} ` ${formatEnd(event.start, event.end, t, event.allday)} ${!event.allday && getTimezoneOffset(timezone)}`}
</Typography> </Typography>
</> </>
) )
@@ -319,7 +315,9 @@ export default function EventPreviewModal({
<> <>
{currentUserAttendee && ( {currentUserAttendee && (
<> <>
<Typography sx={{ marginRight: 2 }}>Attending?</Typography> <Typography sx={{ marginRight: 2 }}>
{t("eventPreview.attendingQuestion")}
</Typography>
<Box display="flex" gap="15px" alignItems="center"> <Box display="flex" gap="15px" alignItems="center">
<Button <Button
variant={ variant={
@@ -344,7 +342,6 @@ export default function EventPreviewModal({
user, user,
event, event,
"ACCEPTED", "ACCEPTED",
onClose, onClose,
type, type,
calendarList calendarList
@@ -363,7 +360,7 @@ export default function EventPreviewModal({
} }
}} }}
> >
Accept {t("eventPreview.accept")}
</Button> </Button>
<Button <Button
variant={ variant={
@@ -406,7 +403,7 @@ export default function EventPreviewModal({
} }
}} }}
> >
Maybe {t("eventPreview.maybe")}
</Button> </Button>
<Button <Button
variant={ variant={
@@ -449,7 +446,7 @@ export default function EventPreviewModal({
} }
}} }}
> >
Decline {t("eventPreview.decline")}
</Button> </Button>
</Box> </Box>
</> </>
@@ -474,7 +471,7 @@ export default function EventPreviewModal({
window.open(event.x_openpass_videoconference) window.open(event.x_openpass_videoconference)
} }
> >
Join the video conference {t("eventPreview.joinVideo")}
</Button> </Button>
} }
/> />
@@ -497,20 +494,25 @@ export default function EventPreviewModal({
}} }}
> >
<Box sx={{ marginRight: 2 }}> <Box sx={{ marginRight: 2 }}>
<Typography>{attendees.length} guests</Typography> <Typography>
{t("eventPreview.guests", {
count: attendees.length,
})}
</Typography>
<Typography <Typography
sx={{ fontSize: "13px", color: "text.secondary" }} sx={{ fontSize: "13px", color: "text.secondary" }}
> >
{ {t("eventPreview.yesCount", {
attendees.filter((a) => a.partstat === "ACCEPTED") count: attendees.filter(
.length (a) => a.partstat === "ACCEPTED"
}{" "} ).length,
yes,{" "} })}
{ ,{" "}
attendees.filter((a) => a.partstat === "DECLINED") {t("eventPreview.noCount", {
.length count: attendees.filter(
}{" "} (a) => a.partstat === "DECLINED"
no ).length,
})}
</Typography> </Typography>
</Box> </Box>
{!showAllAttendees && ( {!showAllAttendees && (
@@ -519,6 +521,7 @@ export default function EventPreviewModal({
renderAttendeeBadge( renderAttendeeBadge(
organizer, organizer,
"org", "org",
t,
showAllAttendees, showAllAttendees,
true true
)} )}
@@ -526,6 +529,7 @@ export default function EventPreviewModal({
renderAttendeeBadge( renderAttendeeBadge(
a, a,
idx.toString(), idx.toString(),
t,
showAllAttendees showAllAttendees
) )
)} )}
@@ -541,7 +545,9 @@ export default function EventPreviewModal({
}} }}
onClick={() => setShowAllAttendees(!showAllAttendees)} onClick={() => setShowAllAttendees(!showAllAttendees)}
> >
{showAllAttendees ? "Show less" : `Show more `} {showAllAttendees
? t("eventPreview.showLess")
: t("eventPreview.showMore")}
</Typography> </Typography>
</Box> </Box>
} }
@@ -550,10 +556,10 @@ export default function EventPreviewModal({
)} )}
{showAllAttendees && {showAllAttendees &&
organizer && organizer &&
renderAttendeeBadge(organizer, "org", showAllAttendees, true)} renderAttendeeBadge(organizer, "org", t, showAllAttendees, true)}
{showAllAttendees && {showAllAttendees &&
visibleAttendees.map((a, idx) => visibleAttendees.map((a, idx) =>
renderAttendeeBadge(a, idx.toString(), showAllAttendees) renderAttendeeBadge(a, idx.toString(), t, showAllAttendees)
)} )}
{/* Location */} {/* Location */}
{event.location && ( {event.location && (
@@ -593,7 +599,10 @@ export default function EventPreviewModal({
<NotificationsNoneIcon /> <NotificationsNoneIcon />
</Box> </Box>
} }
text={`${event.alarm.trigger} before by ${event.alarm.action}`} text={t("eventPreview.alarmText", {
trigger: t(`event.form.notifications.${event.alarm.trigger}`),
action: t(`event.form.notifications.${event.alarm.action}`),
})}
style={{ style={{
fontSize: "16px", fontSize: "16px",
fontFamily: "'Inter', sans-serif", fontFamily: "'Inter', sans-serif",
@@ -608,7 +617,7 @@ export default function EventPreviewModal({
<RepeatIcon /> <RepeatIcon />
</Box> </Box>
} }
text={makeRecurrenceString(event)} text={makeRecurrenceString(event, t)}
style={{ style={{
fontSize: "16px", fontSize: "16px",
fontFamily: "'Inter', sans-serif", fontFamily: "'Inter', sans-serif",
@@ -636,7 +645,7 @@ export default function EventPreviewModal({
letterSpacing={"0.5px"} letterSpacing={"0.5px"}
textAlign={"center"} textAlign={"center"}
> >
This is a private event. Details are hidden. {t("eventPreview.privateEvent.hiddenDetails")}
</Typography> </Typography>
</Box> </Box>
)} )}
@@ -680,13 +689,6 @@ export default function EventPreviewModal({
afterChoiceFunc && afterChoiceFunc(type); afterChoiceFunc && afterChoiceFunc(type);
}} }}
/> />
<EventDisplayModal
open={openFullDisplay}
onClose={() => setOpenFullDisplay(false)}
eventId={eventId}
calId={calId}
typeOfAction={typeOfAction}
/>
<EventUpdateModal <EventUpdateModal
open={openUpdateModal} open={openUpdateModal}
onClose={() => { onClose={() => {
@@ -725,43 +727,76 @@ export default function EventPreviewModal({
); );
} }
function makeRecurrenceString(event: CalendarEvent): string | undefined { function makeRecurrenceString(
if (!event.repetition) { event: CalendarEvent,
return; t: Function
} ): string | undefined {
const recur = [`Reccurent Event · ${event.repetition.freq}`]; if (!event.repetition) return;
const recur: string[] = [
`${t("eventPreview.recurrentEvent")} · ${t(
`eventPreview.freq.${event.repetition.freq}`,
event.repetition.freq
)}`,
];
const recurType: Record<string, string> = { const recurType: Record<string, string> = {
daily: "days", daily: t("eventPreview.days"),
monthly: "months", monthly: t("eventPreview.months"),
yearly: "years", yearly: t("eventPreview.years"),
}; };
if (event.repetition.byday) { if (event.repetition.byday) {
recur.push(`on ${event.repetition.byday.join(", ")}`); recur.push(
t("eventPreview.recurrenceOnDays", {
days: event.repetition.byday
.map((s) => t(`eventPreview.onDays.${s}`))
.join(", "),
})
);
} }
if (event.repetition.interval && event.repetition.interval > 1) { if (event.repetition.interval && event.repetition.interval > 1) {
recur.push( recur.push(
`every ${event.repetition.interval} ${recurType[event.repetition.freq]}` t("eventPreview.everyInterval", {
interval: event.repetition.interval,
unit: recurType[event.repetition.freq],
})
); );
} }
if (event.repetition.occurrences) { if (event.repetition.occurrences) {
recur.push(`for ${event.repetition.occurrences} occurences`); recur.push(
t("eventPreview.forOccurrences", {
count: event.repetition.occurrences,
})
);
} }
if (event.repetition.endDate) { if (event.repetition.endDate) {
recur.push(`until ${event.repetition.endDate}`); recur.push(
t("eventPreview.until", {
date: new Date(event.repetition.endDate).toLocaleDateString(
t("locale"),
{
year: "numeric",
month: "long",
day: "numeric",
}
),
})
);
} }
return recur.join(", "); return recur.join(", ");
} }
function formatDate(date: Date | string, allday?: boolean) { function formatDate(date: Date | string, t: Function, allday?: boolean) {
if (allday) { if (allday) {
return new Date(date).toLocaleDateString(undefined, { return new Date(date).toLocaleDateString(t("locale"), {
year: "numeric", year: "numeric",
month: "long", month: "long",
weekday: "long", weekday: "long",
day: "numeric", day: "numeric",
}); });
} else { } else {
return new Date(date).toLocaleString(undefined, { return new Date(date).toLocaleString(t("locale"), {
year: "numeric", year: "numeric",
month: "long", month: "long",
weekday: "long", weekday: "long",
@@ -772,7 +807,12 @@ function formatDate(date: Date | string, allday?: boolean) {
} }
} }
function formatEnd(start: Date | string, end: Date | string, allday?: boolean) { function formatEnd(
start: Date | string,
end: Date | string,
t: Function,
allday?: boolean
) {
const startDate = new Date(start); const startDate = new Date(start);
const endDate = new Date(end); const endDate = new Date(end);
@@ -784,19 +824,19 @@ function formatEnd(start: Date | string, end: Date | string, allday?: boolean) {
if (allday) { if (allday) {
return sameDay return sameDay
? null ? null
: endDate.toLocaleDateString(undefined, { : endDate.toLocaleDateString(t("locale"), {
year: "numeric", year: "numeric",
month: "short", month: "short",
day: "numeric", day: "numeric",
}); });
} else { } else {
if (sameDay) { if (sameDay) {
return endDate.toLocaleTimeString(undefined, { return endDate.toLocaleTimeString(t("locale"), {
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
}); });
} }
return endDate.toLocaleString(undefined, { return endDate.toLocaleString(t("locale"), {
year: "numeric", year: "numeric",
month: "short", month: "short",
day: "numeric", day: "numeric",
+36 -30
View File
@@ -30,6 +30,7 @@ import {
formatDateTimeInTimezone, formatDateTimeInTimezone,
} from "../../components/Event/utils/dateTimeFormatters"; } from "../../components/Event/utils/dateTimeFormatters";
import { addDays } from "../../components/Event/utils/dateRules"; import { addDays } from "../../components/Event/utils/dateRules";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
function EventPopover({ function EventPopover({
anchorEl, anchorEl,
@@ -55,7 +56,7 @@ function EventPopover({
useAppSelector((state) => state.user.userData?.openpaasId) ?? ""; useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const tempList = useAppSelector((state) => state.calendars.templist); const tempList = useAppSelector((state) => state.calendars.templist);
const calList = useAppSelector((state) => state.calendars.list); const calList = useAppSelector((state) => state.calendars.list);
const selectPersonnalCalendars = createSelector( const selectPersonalCalendars = createSelector(
(state: any) => state.calendars, (state: any) => state.calendars,
(calendars: any) => (calendars: any) =>
Object.keys(calendars.list) Object.keys(calendars.list)
@@ -67,8 +68,8 @@ function EventPopover({
}) })
.filter((calendar) => calendar.id) .filter((calendar) => calendar.id)
); );
const userPersonnalCalendars: Calendars[] = useAppSelector( const userPersonalCalendars: Calendars[] = useAppSelector(
selectPersonnalCalendars selectPersonalCalendars
); );
const timezoneList = useMemo(() => { const timezoneList = useMemo(() => {
@@ -97,7 +98,7 @@ function EventPopover({
const [start, setStart] = useState(event?.start ? event.start : ""); const [start, setStart] = useState(event?.start ? event.start : "");
const [end, setEnd] = useState(event?.end ? event.end : ""); const [end, setEnd] = useState(event?.end ? event.end : "");
const [calendarid, setCalendarid] = useState( const [calendarid, setCalendarid] = useState(
event?.calId ?? userPersonnalCalendars[0]?.id ?? "" event?.calId ?? userPersonalCalendars[0]?.id ?? ""
); );
const [allday, setAllDay] = useState(event?.allday ?? false); const [allday, setAllDay] = useState(event?.allday ?? false);
const [repetition, setRepetition] = useState<RepetitionObject>( const [repetition, setRepetition] = useState<RepetitionObject>(
@@ -126,18 +127,18 @@ function EventPopover({
// Use ref to track if we've already initialized to avoid infinite loop // Use ref to track if we've already initialized to avoid infinite loop
const isInitializedRef = useRef(false); const isInitializedRef = useRef(false);
const userPersonnalCalendarsRef = useRef(userPersonnalCalendars); const userPersonalCalendarsRef = useRef(userPersonalCalendars);
// Update ref when userPersonnalCalendars changes // Update ref when userPersonalCalendars changes
useEffect(() => { useEffect(() => {
userPersonnalCalendarsRef.current = userPersonnalCalendars; userPersonalCalendarsRef.current = userPersonalCalendars;
}, [userPersonnalCalendars]); }, [userPersonalCalendars]);
useEffect(() => { useEffect(() => {
if (!calendarid && userPersonnalCalendars.length > 0) { if (!calendarid && userPersonalCalendars.length > 0) {
setCalendarid(userPersonnalCalendars[0].id); setCalendarid(userPersonalCalendars[0].id);
} }
}, [calendarid, userPersonnalCalendars]); }, [calendarid, userPersonalCalendars]);
const resetAllStateToDefault = useCallback(() => { const resetAllStateToDefault = useCallback(() => {
setShowMore(false); setShowMore(false);
@@ -150,11 +151,11 @@ function EventPopover({
setStart(""); setStart("");
setEnd(""); setEnd("");
if ( if (
userPersonnalCalendars && userPersonalCalendars &&
userPersonnalCalendars.length > 0 && userPersonalCalendars.length > 0 &&
userPersonnalCalendars[0]?.id userPersonalCalendars[0]?.id
) { ) {
setCalendarid(userPersonnalCalendars[0].id); setCalendarid(userPersonalCalendars[0].id);
} }
setAllDay(false); setAllDay(false);
setRepetition({} as RepetitionObject); setRepetition({} as RepetitionObject);
@@ -164,7 +165,7 @@ function EventPopover({
setTimezone(calendarTimezone); setTimezone(calendarTimezone);
setHasVideoConference(false); setHasVideoConference(false);
setMeetingLink(null); setMeetingLink(null);
}, [calendarTimezone, userPersonnalCalendars]); }, [calendarTimezone, userPersonalCalendars]);
// Track if we should sync from selectedRange (only on initial selection, not on toggle) // Track if we should sync from selectedRange (only on initial selection, not on toggle)
const shouldSyncFromRangeRef = useRef(true); const shouldSyncFromRangeRef = useRef(true);
@@ -396,11 +397,11 @@ function EventPopover({
} }
if ( if (
userPersonnalCalendars && userPersonalCalendars &&
userPersonnalCalendars.length > 0 && userPersonalCalendars.length > 0 &&
userPersonnalCalendars[0]?.id userPersonalCalendars[0]?.id
) { ) {
setCalendarid(userPersonnalCalendars[0].id); setCalendarid(userPersonalCalendars[0].id);
} }
setRepetition(event.repetition ?? ({} as RepetitionObject)); setRepetition(event.repetition ?? ({} as RepetitionObject));
setShowRepeat(event.repetition?.freq ? true : false); setShowRepeat(event.repetition?.freq ? true : false);
@@ -457,11 +458,11 @@ function EventPopover({
setAttendees([]); setAttendees([]);
setLocation(""); setLocation("");
if ( if (
userPersonnalCalendars && userPersonalCalendars &&
userPersonnalCalendars.length > 0 && userPersonalCalendars.length > 0 &&
userPersonnalCalendars[0]?.id userPersonalCalendars[0]?.id
) { ) {
setCalendarid(userPersonnalCalendars[0].id); setCalendarid(userPersonalCalendars[0].id);
} }
setAllDay(false); setAllDay(false);
setRepetition({} as RepetitionObject); setRepetition({} as RepetitionObject);
@@ -584,7 +585,7 @@ function EventPopover({
// Resolve target calendar safely // Resolve target calendar safely
const targetCalendar: Calendars | undefined = const targetCalendar: Calendars | undefined =
calList[calendarid] || calList[calendarid] ||
userPersonnalCalendars[0] || userPersonalCalendars[0] ||
(Object.values(calList)[0] as Calendars | undefined); (Object.values(calList)[0] as Calendars | undefined);
if (!targetCalendar || !targetCalendar.id) { if (!targetCalendar || !targetCalendar.id) {
console.error("No target calendar available to save event"); console.error("No target calendar available to save event");
@@ -672,22 +673,23 @@ function EventPopover({
await updateTempCalendar(tempList, newEvent, dispatch, calendarRange); await updateTempCalendar(tempList, newEvent, dispatch, calendarRange);
} }
}; };
const { t } = useI18n();
const dialogActions = ( const dialogActions = (
<Box display="flex" justifyContent="space-between" width="100%" px={2}> <Box display="flex" justifyContent="space-between" width="100%" px={2}>
{!showMore && ( {!showMore && (
<Button startIcon={<AddIcon />} onClick={() => setShowMore(!showMore)}> <Button startIcon={<AddIcon />} onClick={() => setShowMore(!showMore)}>
More options {t("common.moreOptions")}
</Button> </Button>
)} )}
<Box display="flex" gap={1} ml={showMore ? "auto" : 0}> <Box display="flex" gap={1} ml={showMore ? "auto" : 0}>
{showMore && ( {showMore && (
<Button variant="outlined" onClick={handleClose}> <Button variant="outlined" onClick={handleClose}>
Cancel {t("common.cancel")}
</Button> </Button>
)} )}
<Button variant="contained" onClick={handleSave}> <Button variant="contained" onClick={handleSave}>
Save {t("actions.save")}
</Button> </Button>
</Box> </Box>
</Box> </Box>
@@ -697,7 +699,11 @@ function EventPopover({
<ResponsiveDialog <ResponsiveDialog
open={open} open={open}
onClose={handleClose} onClose={handleClose}
title={event?.uid ? "Duplicate Event" : "Create Event"} title={
event?.uid
? t("eventDuplication.duplicateEvent")
: t("event.createEvent")
}
isExpanded={showMore} isExpanded={showMore}
onExpandToggle={() => setShowMore(!showMore)} onExpandToggle={() => setShowMore(!showMore)}
actions={dialogActions} actions={dialogActions}
@@ -739,7 +745,7 @@ function EventPopover({
showRepeat={showRepeat} showRepeat={showRepeat}
setShowRepeat={setShowRepeat} setShowRepeat={setShowRepeat}
isOpen={open} isOpen={open}
userPersonnalCalendars={userPersonnalCalendars} userPersonalCalendars={userPersonalCalendars}
timezoneList={timezoneList} timezoneList={timezoneList}
onStartChange={handleStartChange} onStartChange={handleStartChange}
onEndChange={handleEndChange} onEndChange={handleEndChange}
+12 -10
View File
@@ -28,6 +28,7 @@ import {
detectRecurringEventChanges, detectRecurringEventChanges,
} from "./eventUtils"; } from "./eventUtils";
import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils"; import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
const showErrorNotification = (message: string) => { const showErrorNotification = (message: string) => {
console.error(`[ERROR] ${message}`); console.error(`[ERROR] ${message}`);
@@ -50,6 +51,7 @@ function EventUpdateModal({
eventData?: CalendarEvent | null; eventData?: CalendarEvent | null;
typeOfAction?: "solo" | "all"; typeOfAction?: "solo" | "all";
}) { }) {
const { t } = useI18n();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const tempList = useAppSelector((state) => state.calendars.templist); const tempList = useAppSelector((state) => state.calendars.templist);
const calList = useAppSelector((state) => state.calendars.list); const calList = useAppSelector((state) => state.calendars.list);
@@ -84,7 +86,7 @@ function EventUpdateModal({
const calendarsList = useAppSelector((state) => state.calendars.list); const calendarsList = useAppSelector((state) => state.calendars.list);
const userPersonnalCalendars: Calendars[] = useMemo(() => { const userPersonalCalendars: Calendars[] = useMemo(() => {
const allCalendars = Object.values(calendarsList) as Calendars[]; const allCalendars = Object.values(calendarsList) as Calendars[];
return allCalendars.filter( return allCalendars.filter(
(c: Calendars) => c.id?.split("/")[0] === user.userData?.openpaasId (c: Calendars) => c.id?.split("/")[0] === user.userData?.openpaasId
@@ -155,7 +157,7 @@ function EventUpdateModal({
); );
const [newCalId, setNewCalId] = useState(calId); const [newCalId, setNewCalId] = useState(calId);
const [calendarid, setCalendarid] = useState( const [calendarid, setCalendarid] = useState(
calId ?? userPersonnalCalendars[0]?.id ?? "" calId ?? userPersonalCalendars[0]?.id ?? ""
); );
const [attendees, setAttendees] = useState<userAttendee[]>([]); const [attendees, setAttendees] = useState<userAttendee[]>([]);
@@ -175,7 +177,7 @@ function EventUpdateModal({
setLocation(""); setLocation("");
setStart(""); setStart("");
setEnd(""); setEnd("");
setCalendarid(userPersonnalCalendars[0].id); setCalendarid(userPersonalCalendars[0].id);
setAllDay(false); setAllDay(false);
setRepetition({} as RepetitionObject); setRepetition({} as RepetitionObject);
setAlarm(""); setAlarm("");
@@ -296,7 +298,7 @@ function EventUpdateModal({
} }
} }
} }
}, [open, event, calId, userPersonnalCalendars, calendarsList]); }, [open, event, calId, userPersonalCalendars, calendarsList]);
// Helper to close modal(s) - use onCloseAll if available to close preview modal too // Helper to close modal(s) - use onCloseAll if available to close preview modal too
const closeModal = () => { const closeModal = () => {
@@ -682,7 +684,7 @@ function EventUpdateModal({
moveEventAsync({ moveEventAsync({
cal: targetCalendar, cal: targetCalendar,
newEvent, newEvent,
newURL: `/calendars/${newCalId}/${event.uid}.ics`, newURL: `/calendars/${newCalId}/${event.uid.split("/")[0]}.ics`,
}) })
); );
dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid })); dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid }));
@@ -697,15 +699,15 @@ function EventUpdateModal({
<Box display="flex" justifyContent="space-between" width="100%" px={2}> <Box display="flex" justifyContent="space-between" width="100%" px={2}>
{!showMore && ( {!showMore && (
<Button startIcon={<AddIcon />} onClick={() => setShowMore(!showMore)}> <Button startIcon={<AddIcon />} onClick={() => setShowMore(!showMore)}>
More options {t("common.moreOptions")}
</Button> </Button>
)} )}
<Box display="flex" gap={1} ml={showMore ? "auto" : 0}> <Box display="flex" gap={1} ml={showMore ? "auto" : 0}>
<Button variant="outlined" onClick={handleClose}> <Button variant="outlined" onClick={handleClose}>
Cancel {t("common.cancel")}
</Button> </Button>
<Button variant="contained" onClick={handleSave}> <Button variant="contained" onClick={handleSave}>
Save {t("actions.save")}
</Button> </Button>
</Box> </Box>
</Box> </Box>
@@ -717,7 +719,7 @@ function EventUpdateModal({
<ResponsiveDialog <ResponsiveDialog
open={open} open={open}
onClose={handleClose} onClose={handleClose}
title="Update Event" title={t("event.updateEvent")}
isExpanded={showMore} isExpanded={showMore}
onExpandToggle={() => setShowMore(!showMore)} onExpandToggle={() => setShowMore(!showMore)}
actions={dialogActions} actions={dialogActions}
@@ -760,7 +762,7 @@ function EventUpdateModal({
showRepeat={typeOfAction !== "solo" && showRepeat} showRepeat={typeOfAction !== "solo" && showRepeat}
setShowRepeat={setShowRepeat} setShowRepeat={setShowRepeat}
isOpen={open} isOpen={open}
userPersonnalCalendars={userPersonnalCalendars} userPersonalCalendars={userPersonalCalendars}
timezoneList={timezoneList} timezoneList={timezoneList}
onCalendarChange={(newCalendarId) => { onCalendarChange={(newCalendarId) => {
const selectedCalendar = calList[newCalendarId]; const selectedCalendar = calList[newCalendarId];
+26
View File
@@ -0,0 +1,26 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface SettingsState {
language: string;
}
const savedLang = localStorage.getItem("lang");
const defaultLang = savedLang ?? (window as any).LANG ?? "en";
const initialState: SettingsState = {
language: defaultLang,
};
export const settingsSlice = createSlice({
name: "settings",
initialState,
reducers: {
setLanguage: (state, action: PayloadAction<string>) => {
state.language = action.payload;
localStorage.setItem("lang", action.payload);
},
},
});
export const { setLanguage } = settingsSlice.actions;
export default settingsSlice.reducer;
+232
View File
@@ -0,0 +1,232 @@
{
"locale": "en-gb",
"calendar": {
"personal": "Personal Calendars",
"delegated": "Delegated Calendars",
"other": "Other Calendars",
"caldav_access": "CalDAV access",
"import_file_description": "Import events from an ICS file to one of your calendars.",
"ics_feed_url": "ICS feed URL",
"import_to": "Import to",
"new_calendar": "New calendar",
"addDescription": "Add description",
"color": "Color",
"newEventsVisibility": "New events created will be visible to:",
"browseOtherCalendars": "Browse other calendars",
"noPublicCalendarsFor": "No publicly available calendars for %{name}",
"noMoreCalendarsFor": "No more calendars for %{name}",
"delete": {
"title": "Remove %{name}?",
"personalWarning": "You will lose all events in this calendar.",
"sharedWarning": "You will lose access to its events. You will still be able to add it back later."
}
},
"actions": {
"modify": "Modify",
"delete": "Delete",
"remove": "Remove",
"save": "Save",
"create": "Create",
"import": "Import",
"add": "Add"
},
"common": {
"cancel": "Cancel",
"ok": "Ok",
"link_copied": "Link copied!",
"import_file": "File",
"select_file": "Select file",
"import_url": "URL",
"name": "Name",
"description": "Description",
"all": "All",
"you": "You",
"select_timezone": "Select Timezone",
"moreOptions": "More options"
},
"calendarPopover": {
"tabs": {
"settings": "Settings",
"addNew": "Add new calendar",
"access": "Access",
"import": "Import"
}
},
"colorPicker": {
"selectCustom": "Select custom color",
"title": "Choose custom color",
"subtitle": "Choose a background color for this calendar",
"hex": "Hex"
},
"event": {
"createEvent": "Create Event",
"updateEvent": "Update Event",
"organizer": "Organizer",
"repeat": {
"repeatEvery": "Repeat every",
"frequency": {
"days": "Day(s)",
"weeks": "Week(s)",
"months": "Month(s)",
"years": "Year(s)"
},
"repeatOn": "Repeat on:",
"days": {
"monday": "MO",
"tuesday": "TU",
"wednesday": "WE",
"thursday": "TH",
"friday": "FR",
"saturday": "SA",
"sunday": "SU"
},
"end": {
"label": "End:",
"never": "Never",
"after": "After",
"on": "On",
"occurrences": "occurrences"
}
},
"form": {
"title": "Title",
"titlePlaceholder": "Add title",
"addDescription": "Add description",
"description": "Description",
"descriptionPlaceholder": "Add description",
"dateTime": "Date & Time",
"start": "Start",
"end": "End",
"allDay": "All day",
"repeat": "Repeat",
"timezonePlaceholder": "Select timezone",
"participants": "Participants",
"videoMeeting": "Video meeting",
"addVisioConference": "Add Visio conference",
"joinVisioConference": "Join Visio conference",
"copyMeetingLink": "Copy meeting link",
"removeVideoConference": "Remove video conference",
"location": "Location",
"locationPlaceholder": "Add location",
"calendar": "Calendar",
"notification": "Notification",
"notifications": {
"": "No notification",
"-PT1M": "1 minute before",
"-PT5M": "5 minutes before",
"-PT10M": "10 minutes before",
"-PT15M": "15 minutes before",
"-PT30M": "30 minutes before",
"-PT1H": "1 hour before",
"-PT2H": "2 hours before",
"-PT5H": "5 hours before",
"-PT12H": "12 hours before",
"-PT1D": "1 day before",
"-PT2D": "2 days before",
"-PT1W": "1 week before",
"EMAIL": "email"
},
"showMeAs": "Show me as",
"free": "Free",
"busy": "Busy",
"visibleTo": "Visible to",
"visibleAll": "All",
"visibleParticipants": "Participants"
},
"validation": {
"titleRequired": "Title is required",
"startRequired": "Start date/time is required",
"invalidStart": "Invalid start date/time",
"endRequired": "End date/time is required",
"invalidEnd": "Invalid end date/time",
"endAfterStart": "End time must be after start time"
}
},
"peopleSearch": {
"label": "Start typing a name or email",
"placeholder": "Start typing a name or email",
"invalidEmail": "%{email} is not a valid email address"
},
"error": {
"title": "Something went wrong",
"unknown": "An unknown error occurred",
"retry": "Try Again",
"multipleEvents": "%{count} events with errors"
},
"editModeDialog": {
"updateRecurrentEvent": "Update the recurrent event",
"updateParticipationStatus": "Update the participation status",
"thisEvent": "This event",
"allEvents": "All the events"
},
"eventDuplication": {
"duplicateEvent": "Duplicate event"
},
"menubar": {
"today": "Today",
"refresh": "Refresh",
"viewSelector": "Select view",
"languageSelector": "Select language",
"views": {
"month": "Month",
"week": "Week",
"day": "Day"
},
"apps": "Apps",
"userProfile": "User profile",
"logoAlt": "Calendar"
},
"eventPreview": {
"emailAttendees": "Email attendees",
"deleteEvent": "Delete event",
"privateEvent": {
"tooltipOwn": "Only you and attendees can see the details of this event.",
"hiddenDetails": "This is a private event. Details are hidden."
},
"free": {
"label": "Free",
"tooltip": "Others see you as available during the time range of this event."
},
"attendingQuestion": "Attending?",
"accept": "Accept",
"maybe": "Maybe",
"decline": "Decline",
"showMore": "Show more",
"showLess": "Show less",
"joinVideo": "Join the video conference",
"guests": "%{count} guests",
"yesCount": "%{count} yes",
"noCount": "%{count} no",
"recurrentEvent": "Recurrent Event",
"freq": {
"daily": "daily",
"weekly": "weekly",
"monthly": "monthly",
"yearly": "yearly"
},
"onDays": {
"MO": "monday",
"TU": "tuesday",
"WE": "wednesday",
"TH": "thursday",
"FR": "friday",
"SA": "saturday",
"SU": "sunday"
},
"recurrenceOnDays": "on %{days}",
"everyInterval": "every %{interval} %{unit}",
"forOccurrences": "for %{count} occurrences",
"until": "until %{date}",
"days": "days",
"months": "months",
"years": "years",
"alarmText": "%{trigger} by %{action}"
},
"dateTimeFields": {
"date": "Date",
"startDate": "Start Date",
"startTime": "Start Time",
"endDate": "End Date",
"endTime": "End Time"
}
}
+232
View File
@@ -0,0 +1,232 @@
{
"locale": "fr-fr",
"calendar": {
"personal": "Calendriers personnels",
"delegated": "Calendriers délégués",
"other": "Autres calendriers",
"caldav_access": "Accès CalDAV",
"import_file_description": "Importer des événements depuis un fichier ICS vers l'un de vos calendriers.",
"ics_feed_url": "URL du flux ICS",
"import_to": "Importer vers",
"new_calendar": "Nouveau calendrier",
"addDescription": "Ajouter une description",
"color": "Couleur",
"newEventsVisibility": "Les nouveaux événements créés seront visibles par :",
"browseOtherCalendars": "Parcourir d'autres calendriers",
"noPublicCalendarsFor": "Aucun calendrier public disponible pour %{name}",
"noMoreCalendarsFor": "Plus de calendriers disponibles pour %{name}",
"delete": {
"title": "Supprimer %{name} ?",
"personalWarning": "Vous allez perdre tous les événements de ce calendrier.",
"sharedWarning": "Vous allez perdre laccès à ses événements. Vous pourrez toujours le rajouter plus tard."
}
},
"actions": {
"modify": "Modifier",
"delete": "Supprimer",
"remove": "Retirer",
"save": "Enregistrer",
"create": "Créer",
"import": "Importer",
"add": "Ajouter"
},
"common": {
"cancel": "Annuler",
"ok": "OK",
"link_copied": "Lien copié !",
"import_file": "Fichier",
"select_file": "Sélectionner un fichier",
"import_url": "URL",
"name": "Nom",
"description": "Description",
"all": "Tous",
"you": "Vous",
"select_timezone": "Sélectionner le fuseau horaire",
"moreOptions": "Plus d'options"
},
"calendarPopover": {
"tabs": {
"settings": "Paramètres",
"addNew": "Ajouter un nouveau calendrier",
"access": "Accès",
"import": "Importer"
}
},
"colorPicker": {
"selectCustom": "Sélectionner une couleur personnalisée",
"title": "Choisir une couleur personnalisée",
"subtitle": "Choisissez une couleur d'arrière-plan pour ce calendrier",
"hex": "Hex"
},
"event": {
"createEvent": "Créer un événement",
"updateEvent": "Mettre à jour un événement",
"organizer": "Organisateur",
"repeat": {
"repeatEvery": "Répéter tous les",
"frequency": {
"days": "jour(s)",
"weeks": "semaine(s)",
"months": "mois",
"years": "année(s)"
},
"repeatOn": "Répéter le :",
"days": {
"monday": "LU",
"tuesday": "MA",
"wednesday": "ME",
"thursday": "JE",
"friday": "VE",
"saturday": "SA",
"sunday": "DI"
},
"end": {
"label": "Fin :",
"never": "Jamais",
"after": "Après",
"on": "Le",
"occurrences": "occurrences"
}
},
"form": {
"title": "Titre",
"titlePlaceholder": "Ajouter un titre",
"addDescription": "Ajouter une description",
"description": "Description",
"descriptionPlaceholder": "Ajouter une description",
"dateTime": "Date et heure",
"start": "Début",
"end": "Fin",
"allDay": "Toute la journée",
"repeat": "Répéter",
"timezonePlaceholder": "Sélectionner un fuseau horaire",
"participants": "Participants",
"videoMeeting": "Visioconférence",
"addVisioConference": "Ajouter une visioconférence",
"joinVisioConference": "Rejoindre la visioconférence",
"copyMeetingLink": "Copier le lien de la réunion",
"removeVideoConference": "Supprimer la visioconférence",
"location": "Lieu",
"locationPlaceholder": "Ajouter un lieu",
"calendar": "Calendrier",
"notification": "Notification",
"notifications": {
"": "Aucune notification",
"-PT1M": "1 minute avant",
"-PT5M": "5 minutes avant",
"-PT10M": "10 minutes avant",
"-PT15M": "15 minutes avant",
"-PT30M": "30 minutes avant",
"-PT1H": "1 heure avant",
"-PT2H": "2 heures avant",
"-PT5H": "5 heures avant",
"-PT12H": "12 heures avant",
"-PT1D": "1 jour avant",
"-PT2D": "2 jours avant",
"-PT1W": "1 semaine avant",
"EMAIL": "e-mail"
},
"showMeAs": "M'afficher comme",
"free": "Libre",
"busy": "Occupé",
"visibleTo": "Visible par",
"visibleAll": "Tous",
"visibleParticipants": "Participants"
},
"validation": {
"titleRequired": "Le titre est obligatoire",
"startRequired": "La date/heure de début est obligatoire",
"invalidStart": "Date/heure de début invalide",
"endRequired": "La date/heure de fin est obligatoire",
"invalidEnd": "Date/heure de fin invalide",
"endAfterStart": "L'heure de fin doit être après l'heure de début"
}
},
"peopleSearch": {
"label": "Commencez à saisir un nom ou un e-mail",
"placeholder": "Commencez à saisir un nom ou un e-mail",
"invalidEmail": "%{email} n'est pas une adresse e-mail valide"
},
"error": {
"title": "Une erreur s'est produite",
"unknown": "Une erreur inconnue s'est produite",
"retry": "Réessayer",
"multipleEvents": "%{count} événements comportent des erreurs"
},
"editModeDialog": {
"updateRecurrentEvent": "Mettre à jour l'événement récurrent",
"updateParticipationStatus": "Mettre à jour le statut de participation",
"thisEvent": "Cet événement",
"allEvents": "Tous les événements"
},
"eventDuplication": {
"duplicateEvent": "Dupliquer l'événement"
},
"menubar": {
"today": "Aujourd'hui",
"refresh": "Actualiser",
"viewSelector": "Sélectionner la vue",
"languageSelector": "Sélectionner la langue",
"views": {
"month": "Mois",
"week": "Semaine",
"day": "Jour"
},
"apps": "Applications",
"userProfile": "Profil utilisateur",
"logoAlt": "Calendrier"
},
"eventPreview": {
"emailAttendees": "Envoyer un e-mail aux participants",
"deleteEvent": "Supprimer l'événement",
"privateEvent": {
"tooltipOwn": "Seuls vous et les participants pouvez voir les détails de cet événement.",
"hiddenDetails": "Ceci est un événement privé. Les détails sont masqués."
},
"free": {
"label": "Libre",
"tooltip": "Les autres vous voient comme disponible pendant la plage horaire de cet événement."
},
"attendingQuestion": "Vous participez ?",
"accept": "Accepter",
"maybe": "Peut-être",
"decline": "Décliner",
"showMore": "Afficher plus",
"showLess": "Afficher moins",
"joinVideo": "Rejoindre la visioconférence",
"guests": "%{count} invités",
"yesCount": "%{count} oui",
"noCount": "%{count} non",
"recurrentEvent": "Événement récurrent",
"freq": {
"daily": "quotidien",
"weekly": "hebdomadaire",
"monthly": "mensuel",
"yearly": "annuel"
},
"onDays": {
"MO": "lundi",
"TU": "mardi",
"WE": "mercredi",
"TH": "jeudi",
"FR": "vendredi",
"SA": "samedi",
"SU": "dimanche"
},
"recurrenceOnDays": "le %{days}",
"everyInterval": "tous les %{interval} %{unit}",
"forOccurrences": "pour %{count} occurrences",
"until": "jusqu'au %{date}",
"days": "jours",
"months": "mois",
"years": "ans",
"alarmText": "%{trigger} par %{action}"
},
"dateTimeFields": {
"date": "Date",
"startDate": "Daye du début",
"startTime": "Heure du début",
"endDate": "Date de la fin",
"endTime": "Heure de la fin"
}
}