Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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!);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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
@@ -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" },
|
||||||
|
|||||||
Generated
+9607
-2405
File diff suppressed because it is too large
Load Diff
+18
-7
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -21,5 +21,6 @@ export default defineConfig({
|
|||||||
distPath: {
|
distPath: {
|
||||||
root: "dist",
|
root: "dist",
|
||||||
},
|
},
|
||||||
|
minify: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+25
-12
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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")}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 l’accè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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user