Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { addCalendarResourceAsync } from "@/features/Calendars/api/addCalendarResourceAsync";
|
import { addCalendarResourceAsync } from "@/features/Calendars/api/addCalendarResourceAsync";
|
||||||
import { addSharedCalendar } from "@/features/Calendars/CalendarApi";
|
import { addSharedCalendar } from "@/features/Calendars/CalendarApi";
|
||||||
import { getResourceDetails } from "@/features/User/userAPI";
|
import { getResourceDetails, getUserDetails } from "@/features/User/userAPI";
|
||||||
import { toRejectedError } from "@/utils/errorUtils";
|
import { toRejectedError } from "@/utils/errorUtils";
|
||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ jest.mock("@/utils/errorUtils");
|
|||||||
|
|
||||||
const mockedAddSharedCalendar = addSharedCalendar as jest.Mock;
|
const mockedAddSharedCalendar = addSharedCalendar as jest.Mock;
|
||||||
const mockedGetResourceDetails = getResourceDetails as jest.Mock;
|
const mockedGetResourceDetails = getResourceDetails as jest.Mock;
|
||||||
|
const mockedGetUserDetails = getUserDetails as jest.Mock;
|
||||||
const mockedToRejectedError = toRejectedError as jest.Mock;
|
const mockedToRejectedError = toRejectedError as jest.Mock;
|
||||||
|
|
||||||
describe("addCalendarResourceAsync thunk", () => {
|
describe("addCalendarResourceAsync thunk", () => {
|
||||||
@@ -55,6 +56,11 @@ describe("addCalendarResourceAsync thunk", () => {
|
|||||||
|
|
||||||
it("should add shared calendar, fetch resource details, map userData", async () => {
|
it("should add shared calendar, fetch resource details, map userData", async () => {
|
||||||
mockedGetResourceDetails.mockResolvedValueOnce(mockResolvedResourceData);
|
mockedGetResourceDetails.mockResolvedValueOnce(mockResolvedResourceData);
|
||||||
|
mockedGetUserDetails.mockResolvedValueOnce({
|
||||||
|
firstname: "Creator",
|
||||||
|
lastname: "User",
|
||||||
|
emails: ["creator@example.com"],
|
||||||
|
});
|
||||||
mockedAddSharedCalendar.mockResolvedValueOnce({});
|
mockedAddSharedCalendar.mockResolvedValueOnce({});
|
||||||
|
|
||||||
const result = await addCalendarResourceAsync(
|
const result = await addCalendarResourceAsync(
|
||||||
@@ -67,6 +73,7 @@ describe("addCalendarResourceAsync thunk", () => {
|
|||||||
mockPayload.cal
|
mockPayload.cal
|
||||||
);
|
);
|
||||||
expect(mockedGetResourceDetails).toHaveBeenCalledWith("res-456");
|
expect(mockedGetResourceDetails).toHaveBeenCalledWith("res-456");
|
||||||
|
expect(mockedGetUserDetails).toHaveBeenCalledWith("user-789");
|
||||||
|
|
||||||
expect(result.type).toBe("calendars/addCalendarResource/fulfilled");
|
expect(result.type).toBe("calendars/addCalendarResource/fulfilled");
|
||||||
expect(result.payload).toEqual({
|
expect(result.payload).toEqual({
|
||||||
@@ -75,7 +82,12 @@ describe("addCalendarResourceAsync thunk", () => {
|
|||||||
desc: "A meeting room",
|
desc: "A meeting room",
|
||||||
link: "/calendars/user-123/cal-123.json",
|
link: "/calendars/user-123/cal-123.json",
|
||||||
name: "Resource Room A",
|
name: "Resource Room A",
|
||||||
owner: mockResolvedResourceData,
|
owner: {
|
||||||
|
firstname: "Creator",
|
||||||
|
lastname: "User",
|
||||||
|
emails: ["creator@example.com"],
|
||||||
|
resource: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
getUserDetails,
|
getUserDetails,
|
||||||
} from "@/features/User/userAPI";
|
} from "@/features/User/userAPI";
|
||||||
import { getCalendars } from "@/features/Calendars/CalendarApi";
|
import { getCalendars } from "@/features/Calendars/CalendarApi";
|
||||||
import { formatReduxError, toRejectedError } from "@/utils/errorUtils";
|
import { formatReduxError } from "@/utils/errorUtils";
|
||||||
import { normalizeCalendar } from "@/features/Calendars/utils/normalizeCalendar";
|
import { normalizeCalendar } from "@/features/Calendars/utils/normalizeCalendar";
|
||||||
|
|
||||||
jest.mock("@/features/User/userAPI");
|
jest.mock("@/features/User/userAPI");
|
||||||
@@ -176,16 +176,20 @@ describe("getCalendarsListAsync", () => {
|
|||||||
ownerId: "resource-123",
|
ownerId: "resource-123",
|
||||||
});
|
});
|
||||||
|
|
||||||
// getUserDetails fails with 404 for the resource ID
|
// getUserDetails fails with 404 for the resource ID, succeeds for the creator
|
||||||
mockedGetUserDetails.mockRejectedValueOnce({ response: { status: 404 } });
|
mockedGetUserDetails.mockImplementation((id: string) => {
|
||||||
|
if (id === "resource-123")
|
||||||
|
return Promise.reject({ response: { status: 404 } });
|
||||||
|
if (id === "creator-456")
|
||||||
|
return Promise.resolve({
|
||||||
|
firstname: "Creator",
|
||||||
|
lastname: "User",
|
||||||
|
emails: [],
|
||||||
|
});
|
||||||
|
return Promise.resolve({ firstname: "", lastname: "", emails: [] });
|
||||||
|
});
|
||||||
// Then getResourceDetails is called and succeeds
|
// Then getResourceDetails is called and succeeds
|
||||||
mockedGetResourceDetails.mockResolvedValueOnce({ creator: "creator-456" });
|
mockedGetResourceDetails.mockResolvedValueOnce({ creator: "creator-456" });
|
||||||
// Then getUserDetails is called for the creator and succeeds
|
|
||||||
mockedGetUserDetails.mockResolvedValueOnce({
|
|
||||||
firstname: "Creator",
|
|
||||||
lastname: "User",
|
|
||||||
emails: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const thunk = getCalendarsListAsync();
|
const thunk = getCalendarsListAsync();
|
||||||
const result = await thunk(dispatch, getState, undefined);
|
const result = await thunk(dispatch, getState, undefined);
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ describe("Event Preview Display", () => {
|
|||||||
role: "REQ-PARTICIPANT",
|
role: "REQ-PARTICIPANT",
|
||||||
cutype: "INDIVIDUAL",
|
cutype: "INDIVIDUAL",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
cn: "Projector Room",
|
||||||
|
cal_address: "room1@test.com",
|
||||||
|
partstat: "ACCEPTED",
|
||||||
|
rsvp: "TRUE",
|
||||||
|
role: "REQ-PARTICIPANT",
|
||||||
|
cutype: "RESOURCE",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
event2: {
|
event2: {
|
||||||
@@ -163,6 +171,8 @@ describe("Event Preview Display", () => {
|
|||||||
expect(screen.getByText(/– 10:00/)).toBeInTheDocument();
|
expect(screen.getByText(/– 10:00/)).toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.getByText("Calendar")).toBeInTheDocument();
|
expect(screen.getByText("Calendar")).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText("Projector Room")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it("calls onClose when Cancel clicked", () => {
|
it("calls onClose when Cancel clicked", () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
|
|||||||
@@ -81,6 +81,22 @@ describe("EventPopover", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "room1@example.com",
|
||||||
|
objectType: "resource",
|
||||||
|
emailAddresses: [
|
||||||
|
{
|
||||||
|
value: "room1@example.com",
|
||||||
|
type: "default",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
names: [
|
||||||
|
{
|
||||||
|
displayName: "Room 1",
|
||||||
|
type: "default",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
(api.post as jest.Mock).mockReturnValue({
|
(api.post as jest.Mock).mockReturnValue({
|
||||||
json: jest.fn().mockResolvedValue(mockUsers),
|
json: jest.fn().mockResolvedValue(mockUsers),
|
||||||
@@ -211,71 +227,137 @@ describe("EventPopover", () => {
|
|||||||
});
|
});
|
||||||
it("adds a attendee", async () => {
|
it("adds a attendee", async () => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
jest
|
try {
|
||||||
.spyOn(calendarsApi, "getCalendars")
|
jest
|
||||||
.mockReturnValue({ json: jest.fn() });
|
.spyOn(calendarsApi, "getCalendars")
|
||||||
renderPopover();
|
.mockReturnValue({ json: jest.fn() });
|
||||||
fireEvent.change(screen.getByLabelText("event.form.title"), {
|
renderPopover();
|
||||||
target: { value: "newEvent" },
|
fireEvent.change(screen.getByLabelText("event.form.title"), {
|
||||||
});
|
target: { value: "newEvent" },
|
||||||
const select = screen.getByLabelText("peopleSearch.label");
|
});
|
||||||
|
const select = screen.getByLabelText("peopleSearch.label");
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
select.focus();
|
select.focus();
|
||||||
fireEvent.mouseDown(select);
|
fireEvent.mouseDown(select);
|
||||||
userEvent.type(select, "john");
|
userEvent.type(select, "john");
|
||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
jest.advanceTimersByTime(400);
|
jest.advanceTimersByTime(400);
|
||||||
});
|
});
|
||||||
await waitFor(() => expect(api.post).toHaveBeenCalledTimes(1));
|
await waitFor(() => expect(api.post).toHaveBeenCalledTimes(1));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("John Doe")).toBeInTheDocument();
|
expect(screen.getByText("John Doe")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
userEvent.click(screen.getByText("John Doe"));
|
userEvent.click(screen.getByText("John Doe"));
|
||||||
});
|
|
||||||
|
|
||||||
const spy = jest
|
|
||||||
.spyOn(eventThunks, "putEventAsync")
|
|
||||||
.mockImplementation((payload) => {
|
|
||||||
const promise = Promise.resolve(payload);
|
|
||||||
(promise as any).unwrap = () => promise;
|
|
||||||
return () => promise as any;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
const spy = jest
|
||||||
|
.spyOn(eventThunks, "putEventAsync")
|
||||||
|
.mockImplementation((payload) => {
|
||||||
|
const promise = Promise.resolve(payload);
|
||||||
|
(promise as any).unwrap = () => promise;
|
||||||
|
return () => promise as any;
|
||||||
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||||
expect(spy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
const receivedPayload = spy.mock.calls[0][0];
|
await waitFor(() => {
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
expect(receivedPayload.cal).toEqual(
|
const receivedPayload = spy.mock.calls[0][0];
|
||||||
preloadedState.calendars.list["667037022b752d0026472254/cal1"]
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(receivedPayload.newEvent.attendee).toHaveLength(2);
|
expect(receivedPayload.cal).toEqual(
|
||||||
expect(receivedPayload.newEvent.attendee).toStrictEqual([
|
preloadedState.calendars.list["667037022b752d0026472254/cal1"]
|
||||||
{
|
);
|
||||||
cn: "test",
|
|
||||||
cal_address: "test@test.com",
|
expect(receivedPayload.newEvent.attendee).toHaveLength(2);
|
||||||
partstat: "ACCEPTED",
|
expect(receivedPayload.newEvent.attendee).toStrictEqual([
|
||||||
rsvp: "FALSE",
|
{
|
||||||
role: "CHAIR",
|
cn: "test",
|
||||||
cutype: "INDIVIDUAL",
|
cal_address: "test@test.com",
|
||||||
},
|
partstat: "ACCEPTED",
|
||||||
{
|
rsvp: "FALSE",
|
||||||
cn: "John Doe",
|
role: "CHAIR",
|
||||||
cal_address: "john@example.com",
|
cutype: "INDIVIDUAL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cn: "John Doe",
|
||||||
|
cal_address: "john@example.com",
|
||||||
|
partstat: "NEEDS-ACTION",
|
||||||
|
rsvp: "FALSE",
|
||||||
|
role: "REQ-PARTICIPANT",
|
||||||
|
cutype: "INDIVIDUAL",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
jest.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds a resource", async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
try {
|
||||||
|
renderPopover();
|
||||||
|
fireEvent.change(screen.getByLabelText("event.form.title"), {
|
||||||
|
target: { value: "newEventWithResource" },
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: "common.moreOptions" })
|
||||||
|
);
|
||||||
|
|
||||||
|
const resourceCombobox = screen.getByPlaceholderText(
|
||||||
|
"resourceSearch.placeholder"
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
resourceCombobox.focus();
|
||||||
|
fireEvent.mouseDown(resourceCombobox);
|
||||||
|
});
|
||||||
|
await userEvent.type(resourceCombobox, "room");
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
jest.advanceTimersByTime(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Room 1")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText("Room 1"));
|
||||||
|
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(eventThunks, "putEventAsync")
|
||||||
|
.mockImplementation((payload) => {
|
||||||
|
const promise = Promise.resolve(payload);
|
||||||
|
(promise as any).unwrap = () => promise;
|
||||||
|
return () => promise as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const receivedPayload = spy.mock.calls[0][0];
|
||||||
|
|
||||||
|
expect(receivedPayload.newEvent.attendee).toHaveLength(2); // Organizer + 1 resource
|
||||||
|
expect(receivedPayload.newEvent.attendee[1]).toStrictEqual({
|
||||||
|
cn: "Room 1",
|
||||||
|
cal_address: "room1@example.com",
|
||||||
partstat: "NEEDS-ACTION",
|
partstat: "NEEDS-ACTION",
|
||||||
rsvp: "FALSE",
|
rsvp: "TRUE",
|
||||||
role: "REQ-PARTICIPANT",
|
role: "REQ-PARTICIPANT",
|
||||||
cutype: "INDIVIDUAL",
|
cutype: "RESOURCE",
|
||||||
},
|
});
|
||||||
]);
|
} finally {
|
||||||
|
jest.useRealTimers();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("dispatches putEventAsync and calls onClose when Save is clicked", async () => {
|
it("dispatches putEventAsync and calls onClose when Save is clicked", async () => {
|
||||||
|
|||||||
@@ -157,6 +157,89 @@ describe("EventUpdateModal Timezone Handling", () => {
|
|||||||
// Verify the timezone is still preserved (should be Asia/Bangkok)
|
// Verify the timezone is still preserved (should be Asia/Bangkok)
|
||||||
expect(titleInput).toHaveValue("Updated Event");
|
expect(titleInput).toHaveValue("Updated Event");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves resources when editing an event", async () => {
|
||||||
|
const eventDateUTC = new Date("2025-01-15T07:00:00.000Z");
|
||||||
|
|
||||||
|
const eventData = {
|
||||||
|
uid: "test-event-resource",
|
||||||
|
title: "Resource Event",
|
||||||
|
calId: "667037022b752d0026472254/cal1",
|
||||||
|
start: eventDateUTC.toISOString(),
|
||||||
|
end: new Date(eventDateUTC.getTime() + 3600000).toISOString(),
|
||||||
|
timezone: "Asia/Bangkok",
|
||||||
|
allday: false,
|
||||||
|
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||||
|
attendee: [
|
||||||
|
{ cn: "test", cal_address: "test@test.com" },
|
||||||
|
{
|
||||||
|
cn: "Conference Room",
|
||||||
|
cal_address: "room@test.com",
|
||||||
|
partstat: "ACCEPTED",
|
||||||
|
rsvp: "TRUE",
|
||||||
|
role: "REQ-PARTICIPANT",
|
||||||
|
cutype: "RESOURCE",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateWithEvent = {
|
||||||
|
...preloadedState,
|
||||||
|
calendars: {
|
||||||
|
...preloadedState.calendars,
|
||||||
|
list: {
|
||||||
|
"667037022b752d0026472254/cal1": {
|
||||||
|
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||||
|
events: {
|
||||||
|
"test-event-resource": eventData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockPutEvent = jest.spyOn(EventApi, "putEvent").mockResolvedValue({
|
||||||
|
status: 201,
|
||||||
|
url: `/calendars/667037022b752d0026472254/cal1/test-event-resource.ics`,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<EventUpdateModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
|
eventId={"test-event-resource"}
|
||||||
|
eventData={eventData}
|
||||||
|
/>,
|
||||||
|
stateWithEvent
|
||||||
|
);
|
||||||
|
|
||||||
|
// Edit the title
|
||||||
|
const titleInput = screen.getByDisplayValue("Resource Event");
|
||||||
|
fireEvent.change(titleInput, {
|
||||||
|
target: { value: "Updated Resource Event" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click Save
|
||||||
|
const saveButton = screen.getByRole("button", { name: "actions.save" });
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(saveButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockPutEvent).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const putEventCall = mockPutEvent.mock.calls[0][0];
|
||||||
|
expect(putEventCall.title).toBe("Updated Resource Event");
|
||||||
|
|
||||||
|
// Check that the resource is still in the attendee list!
|
||||||
|
const attendees = putEventCall.attendee;
|
||||||
|
const resource = attendees.find((a: any) => a.cutype === "RESOURCE");
|
||||||
|
expect(resource).toBeDefined();
|
||||||
|
expect(resource!.cn).toBe("Conference Room");
|
||||||
|
expect(resource!.cal_address).toBe("room@test.com");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
||||||
|
|||||||
@@ -291,7 +291,10 @@ export default function CalendarResources({
|
|||||||
|
|
||||||
const successfulCals = results
|
const successfulCals = results
|
||||||
.filter((result) => result.status === "fulfilled")
|
.filter((result) => result.status === "fulfilled")
|
||||||
.map((result) => (result as PromiseFulfilledResult<unknown>).value)
|
.map(
|
||||||
|
(result) =>
|
||||||
|
(result as PromiseFulfilledResult<CalendarWithOwner[]>).value
|
||||||
|
)
|
||||||
.flat()
|
.flat()
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import RepeatEvent from "./EventRepeat";
|
|||||||
import { useAllDayToggle } from "./hooks/useAllDayToggle";
|
import { useAllDayToggle } from "./hooks/useAllDayToggle";
|
||||||
import { combineDateTime, splitDateTime } from "./utils/dateTimeHelpers";
|
import { combineDateTime, splitDateTime } from "./utils/dateTimeHelpers";
|
||||||
import { validateEventForm } from "./utils/formValidation";
|
import { validateEventForm } from "./utils/formValidation";
|
||||||
|
import { Resource, ResourceSearch } from "../Attendees/ResourceSearch";
|
||||||
|
|
||||||
interface EventFormFieldsProps {
|
interface EventFormFieldsProps {
|
||||||
// Form state
|
// Form state
|
||||||
@@ -84,6 +85,8 @@ interface EventFormFieldsProps {
|
|||||||
setHasVideoConference: (hasVideoConference: boolean) => void;
|
setHasVideoConference: (hasVideoConference: boolean) => void;
|
||||||
meetingLink: string | null;
|
meetingLink: string | null;
|
||||||
setMeetingLink: (meetingLink: string | null) => void;
|
setMeetingLink: (meetingLink: string | null) => void;
|
||||||
|
selectedResources: Resource[];
|
||||||
|
setSelectedResources: (resources: Resource[]) => void;
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
showMore: boolean;
|
showMore: boolean;
|
||||||
@@ -166,6 +169,8 @@ export default function EventFormFields({
|
|||||||
onValidationChange,
|
onValidationChange,
|
||||||
showValidationErrors = false,
|
showValidationErrors = false,
|
||||||
onHasEndDateChangedChange,
|
onHasEndDateChangedChange,
|
||||||
|
selectedResources,
|
||||||
|
setSelectedResources,
|
||||||
}: EventFormFieldsProps) {
|
}: EventFormFieldsProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -478,6 +483,10 @@ export default function EventFormFields({
|
|||||||
onCalendarChange?.(newCalendarId);
|
onCalendarChange?.(newCalendarId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleResourceChange = (resources: Resource[]) => {
|
||||||
|
setSelectedResources(resources);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FieldWithLabel
|
<FieldWithLabel
|
||||||
@@ -858,6 +867,26 @@ export default function EventFormFields({
|
|||||||
|
|
||||||
{showMore && (
|
{showMore && (
|
||||||
<>
|
<>
|
||||||
|
<FieldWithLabel
|
||||||
|
label={t("event.form.resource")}
|
||||||
|
isExpanded={showMore}
|
||||||
|
>
|
||||||
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
|
<ResourceSearch
|
||||||
|
objectTypes={["resource"]}
|
||||||
|
selectedResources={selectedResources}
|
||||||
|
inputSlot={(params) => <TextField {...params} size="small" />}
|
||||||
|
onChange={async (
|
||||||
|
_event: React.SyntheticEvent,
|
||||||
|
value: Resource[]
|
||||||
|
) => {
|
||||||
|
handleResourceChange(value);
|
||||||
|
}}
|
||||||
|
hideLabel={true}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FieldWithLabel>
|
||||||
|
|
||||||
<FieldWithLabel
|
<FieldWithLabel
|
||||||
label={t("event.form.notification")}
|
label={t("event.form.notification")}
|
||||||
isExpanded={showMore}
|
isExpanded={showMore}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export function InfoRow({
|
|||||||
alignItems,
|
alignItems,
|
||||||
gap: 1,
|
gap: 1,
|
||||||
marginBottom: 1,
|
marginBottom: 1,
|
||||||
|
flexWrap: "wrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { OpenPaasUserData } from "@/features/User/type/OpenPaasUserData";
|
import { OpenPaasUserData } from "@/features/User/type/OpenPaasUserData";
|
||||||
import { getResourceDetails } from "@/features/User/userAPI";
|
import { getResourceDetails, getUserDetails } from "@/features/User/userAPI";
|
||||||
import { toRejectedError } from "@/utils/errorUtils";
|
import { toRejectedError } from "@/utils/errorUtils";
|
||||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||||
import { addSharedCalendar } from "../CalendarApi";
|
import { addSharedCalendar } from "../CalendarApi";
|
||||||
@@ -27,14 +27,24 @@ export const addCalendarResourceAsync = createAsyncThunk<
|
|||||||
>(
|
>(
|
||||||
"calendars/addCalendarResource",
|
"calendars/addCalendarResource",
|
||||||
async ({ userId, calId, cal }, { rejectWithValue }) => {
|
async ({ userId, calId, cal }, { rejectWithValue }) => {
|
||||||
|
const resourceId = cal.cal._links.self?.href
|
||||||
|
?.replace("/calendars/", "")
|
||||||
|
?.replace(".json", "")
|
||||||
|
?.split("/")[0];
|
||||||
|
|
||||||
|
let owner: OpenPaasUserData = {
|
||||||
|
firstname: "",
|
||||||
|
lastname: cal.cal["dav:name"] ?? "",
|
||||||
|
emails: [],
|
||||||
|
resource: true,
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
await addSharedCalendar(userId, calId, cal);
|
await addSharedCalendar(userId, calId, cal);
|
||||||
const ownerData = await getResourceDetails(
|
const resource = await getResourceDetails(resourceId!);
|
||||||
cal.cal._links.self.href
|
owner = {
|
||||||
.replace("/calendars/", "")
|
...(await getUserDetails(resource.creator)),
|
||||||
.replace(".json", "")
|
resource: true,
|
||||||
.split("/")[0]
|
};
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
calId: cal.cal._links.self?.href
|
calId: cal.cal._links.self?.href
|
||||||
@@ -44,7 +54,7 @@ export const addCalendarResourceAsync = createAsyncThunk<
|
|||||||
link: `/calendars/${userId}/${calId}.json`,
|
link: `/calendars/${userId}/${calId}.json`,
|
||||||
desc: cal.cal["caldav:description"] ?? "",
|
desc: cal.cal["caldav:description"] ?? "",
|
||||||
name: cal.cal["dav:name"] ?? "",
|
name: cal.cal["dav:name"] ?? "",
|
||||||
owner: ownerData,
|
owner,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return rejectWithValue(toRejectedError(err));
|
return rejectWithValue(toRejectedError(err));
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import { userAttendee } from "../User/models/attendee";
|
|||||||
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||||
import { useEventOrganizer } from "./useEventOrganizer";
|
import { useEventOrganizer } from "./useEventOrganizer";
|
||||||
import { buildDelegatedEventURL } from "./utils/buildDelegatedEventURL";
|
import { buildDelegatedEventURL } from "./utils/buildDelegatedEventURL";
|
||||||
|
import { Resource } from "@/components/Attendees/ResourceSearch";
|
||||||
|
|
||||||
function EventPopover({
|
function EventPopover({
|
||||||
open,
|
open,
|
||||||
@@ -86,6 +87,16 @@ function EventPopover({
|
|||||||
return resolveTimezone(tz);
|
return resolveTimezone(tz);
|
||||||
}, [calendarTimezone]);
|
}, [calendarTimezone]);
|
||||||
|
|
||||||
|
const resources: Resource[] = useMemo(() => {
|
||||||
|
const resourcesInEvent =
|
||||||
|
event?.attendee?.filter((attendee) => attendee.cutype === "RESOURCE") ??
|
||||||
|
[];
|
||||||
|
return resourcesInEvent.map((resource) => ({
|
||||||
|
email: resource.cal_address,
|
||||||
|
displayName: resource.cn,
|
||||||
|
}));
|
||||||
|
}, [event?.attendee]);
|
||||||
|
|
||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
const [showDescription, setShowDescription] = useState(
|
const [showDescription, setShowDescription] = useState(
|
||||||
event?.description ? true : false
|
event?.description ? true : false
|
||||||
@@ -127,6 +138,7 @@ function EventPopover({
|
|||||||
const [repetition, setRepetition] = useState<RepetitionObject>(
|
const [repetition, setRepetition] = useState<RepetitionObject>(
|
||||||
event?.repetition ?? ({} as RepetitionObject)
|
event?.repetition ?? ({} as RepetitionObject)
|
||||||
);
|
);
|
||||||
|
const [selectedResources, setSelectedResources] = useState(resources ?? []);
|
||||||
|
|
||||||
// Derive the effective organizer based on the selected calendar.
|
// Derive the effective organizer based on the selected calendar.
|
||||||
// When a delegated calendar is selected, the organizer must be the
|
// When a delegated calendar is selected, the organizer must be the
|
||||||
@@ -139,11 +151,16 @@ function EventPopover({
|
|||||||
|
|
||||||
const [attendees, setAttendees] = useState<userAttendee[]>(
|
const [attendees, setAttendees] = useState<userAttendee[]>(
|
||||||
event?.attendee
|
event?.attendee
|
||||||
? event.attendee.filter((a) => a.cal_address !== organizer?.cal_address)
|
? event.attendee.filter(
|
||||||
|
(a) =>
|
||||||
|
a.cal_address !== organizer?.cal_address && a.cutype !== "RESOURCE"
|
||||||
|
)
|
||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? "");
|
const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? "");
|
||||||
const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC");
|
const [eventClass, setEventClass] = useState<string>(
|
||||||
|
event?.class ?? "PUBLIC"
|
||||||
|
);
|
||||||
const [busy, setBusy] = useState(event?.transp ?? "OPAQUE");
|
const [busy, setBusy] = useState(event?.transp ?? "OPAQUE");
|
||||||
const [timezone, setTimezone] = useState(
|
const [timezone, setTimezone] = useState(
|
||||||
event?.timezone ? resolveTimezone(event.timezone) : resolvedCalendarTimezone
|
event?.timezone ? resolveTimezone(event.timezone) : resolvedCalendarTimezone
|
||||||
@@ -200,6 +217,7 @@ function EventPopover({
|
|||||||
setHasVideoConference(false);
|
setHasVideoConference(false);
|
||||||
setMeetingLink(null);
|
setMeetingLink(null);
|
||||||
setHasEndDateChanged(false);
|
setHasEndDateChanged(false);
|
||||||
|
setSelectedResources([]);
|
||||||
}, [resolvedCalendarTimezone, defaultCalendarId]);
|
}, [resolvedCalendarTimezone, defaultCalendarId]);
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -466,7 +484,9 @@ function EventPopover({
|
|||||||
setAttendees(
|
setAttendees(
|
||||||
event.attendee
|
event.attendee
|
||||||
? event.attendee.filter(
|
? event.attendee.filter(
|
||||||
(a) => a.cal_address !== organizer?.cal_address
|
(a) =>
|
||||||
|
a.cal_address !== organizer?.cal_address &&
|
||||||
|
a.cutype !== "RESOURCE"
|
||||||
)
|
)
|
||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
@@ -491,17 +511,23 @@ function EventPopover({
|
|||||||
setDescription(event.description);
|
setDescription(event.description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setSelectedResources(resources ?? []);
|
||||||
} else if (event && event.attendee && event.attendee.length > 0) {
|
} else if (event && event.attendee && event.attendee.length > 0) {
|
||||||
// Handle tempEvent case (no uid but has attendees from temp calendar search)
|
// Handle tempEvent case (no uid but has attendees from temp calendar search)
|
||||||
setAttendees(
|
setAttendees(
|
||||||
event.attendee.filter((a) => a.cal_address !== organizer?.cal_address)
|
event.attendee.filter(
|
||||||
|
(a) =>
|
||||||
|
a.cal_address !== organizer?.cal_address && a.cutype !== "RESOURCE"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
setSelectedResources(resources ?? []);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
event,
|
event,
|
||||||
organizer?.cal_address,
|
organizer?.cal_address,
|
||||||
resolvedCalendarTimezone,
|
resolvedCalendarTimezone,
|
||||||
defaultCalendarId,
|
defaultCalendarId,
|
||||||
|
resources,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Reset state when creating new event (event is empty object or undefined)
|
// Reset state when creating new event (event is empty object or undefined)
|
||||||
@@ -535,6 +561,7 @@ function EventPopover({
|
|||||||
setHasVideoConference(false);
|
setHasVideoConference(false);
|
||||||
setMeetingLink(null);
|
setMeetingLink(null);
|
||||||
setHasEndDateChanged(false);
|
setHasEndDateChanged(false);
|
||||||
|
setSelectedResources([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isCreatingNew) {
|
if (!isCreatingNew) {
|
||||||
@@ -640,6 +667,7 @@ function EventPopover({
|
|||||||
resetAllStateToDefault();
|
resetAllStateToDefault();
|
||||||
setStart("");
|
setStart("");
|
||||||
setEnd("");
|
setEnd("");
|
||||||
|
setSelectedResources([]);
|
||||||
shouldSyncFromRangeRef.current = true; // Reset for next time
|
shouldSyncFromRangeRef.current = true; // Reset for next time
|
||||||
isCalendarIdUserSelectedRef.current = false; // Reset so next open gets fresh default
|
isCalendarIdUserSelectedRef.current = false; // Reset so next open gets fresh default
|
||||||
};
|
};
|
||||||
@@ -666,6 +694,7 @@ function EventPopover({
|
|||||||
showDescription,
|
showDescription,
|
||||||
showRepeat,
|
showRepeat,
|
||||||
hasEndDateChanged,
|
hasEndDateChanged,
|
||||||
|
resources: selectedResources,
|
||||||
};
|
};
|
||||||
return buildEventFormTempData(formState);
|
return buildEventFormTempData(formState);
|
||||||
}, [
|
}, [
|
||||||
@@ -688,6 +717,7 @@ function EventPopover({
|
|||||||
showDescription,
|
showDescription,
|
||||||
showRepeat,
|
showRepeat,
|
||||||
hasEndDateChanged,
|
hasEndDateChanged,
|
||||||
|
selectedResources,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Check for temp data when modal opens
|
// Check for temp data when modal opens
|
||||||
@@ -722,6 +752,7 @@ function EventPopover({
|
|||||||
setShowDescription,
|
setShowDescription,
|
||||||
setShowRepeat,
|
setShowRepeat,
|
||||||
setHasEndDateChanged,
|
setHasEndDateChanged,
|
||||||
|
setSelectedResources,
|
||||||
});
|
});
|
||||||
// Clear the error flag but keep data until successful save
|
// Clear the error flag but keep data until successful save
|
||||||
const updatedTempData = { ...tempData, fromError: false };
|
const updatedTempData = { ...tempData, fromError: false };
|
||||||
@@ -766,7 +797,7 @@ function EventPopover({
|
|||||||
uid: newEventUID,
|
uid: newEventUID,
|
||||||
description,
|
description,
|
||||||
location,
|
location,
|
||||||
class: eventClass,
|
class: eventClass as "PUBLIC" | "PRIVATE" | "CONFIDENTIAL",
|
||||||
repetition,
|
repetition,
|
||||||
organizer,
|
organizer,
|
||||||
timezone,
|
timezone,
|
||||||
@@ -787,6 +818,20 @@ function EventPopover({
|
|||||||
x_openpass_videoconference: meetingLink || undefined,
|
x_openpass_videoconference: meetingLink || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Map data of resources to attendee before creating event
|
||||||
|
if (selectedResources?.length) {
|
||||||
|
selectedResources.forEach((resource: Resource) => {
|
||||||
|
newEvent.attendee.push({
|
||||||
|
cn: resource?.displayName ?? "",
|
||||||
|
cal_address: resource?.email ?? "",
|
||||||
|
partstat: "NEEDS-ACTION",
|
||||||
|
rsvp: "TRUE",
|
||||||
|
role: "REQ-PARTICIPANT",
|
||||||
|
cutype: "RESOURCE",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (allday) {
|
if (allday) {
|
||||||
const startDateOnly = (start || "").split("T")[0];
|
const startDateOnly = (start || "").split("T")[0];
|
||||||
const endDateOnlyUI = (end || start || "").split("T")[0];
|
const endDateOnlyUI = (end || start || "").split("T")[0];
|
||||||
@@ -957,6 +1002,8 @@ function EventPopover({
|
|||||||
onValidationChange={setIsFormValid}
|
onValidationChange={setIsFormValid}
|
||||||
showValidationErrors={showValidationErrors}
|
showValidationErrors={showValidationErrors}
|
||||||
onHasEndDateChangedChange={setHasEndDateChanged}
|
onHasEndDateChangedChange={setHasEndDateChanged}
|
||||||
|
setSelectedResources={setSelectedResources}
|
||||||
|
selectedResources={selectedResources}
|
||||||
/>
|
/>
|
||||||
</ResponsiveDialog>
|
</ResponsiveDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ export function EventPreviewActionMenu({
|
|||||||
const mailSpaUrl = window.MAIL_SPA_URL ?? null;
|
const mailSpaUrl = window.MAIL_SPA_URL ?? null;
|
||||||
|
|
||||||
const attendees = event.attendee ?? [];
|
const attendees = event.attendee ?? [];
|
||||||
const otherAttendees = attendees.filter((a) => a.cal_address !== userEmail);
|
const otherAttendees = attendees.filter(
|
||||||
|
(a) => a.cal_address !== userEmail && a.cutype !== "RESOURCE"
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu open={Boolean(anchorEl)} onClose={onClose} anchorEl={anchorEl}>
|
<Menu open={Boolean(anchorEl)} onClose={onClose} anchorEl={anchorEl}>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { InfoRow } from "@/components/Event/InfoRow";
|
import { InfoRow } from "@/components/Event/InfoRow";
|
||||||
import { Box, Button, Typography } from "@linagora/twake-mui";
|
import { Box, Button, Typography } from "@linagora/twake-mui";
|
||||||
|
import LayersOutlinedIcon from "@mui/icons-material/LayersOutlined";
|
||||||
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
||||||
import LocationOnOutlinedIcon from "@mui/icons-material/LocationOnOutlined";
|
import LocationOnOutlinedIcon from "@mui/icons-material/LocationOnOutlined";
|
||||||
import NotificationsNoneIcon from "@mui/icons-material/NotificationsNone";
|
import NotificationsNoneIcon from "@mui/icons-material/NotificationsNone";
|
||||||
@@ -11,6 +12,7 @@ import { useI18n } from "twake-i18n";
|
|||||||
import { CalendarEvent } from "../EventsTypes";
|
import { CalendarEvent } from "../EventsTypes";
|
||||||
import { EventPreviewAttendees } from "./EventPreviewAttendees";
|
import { EventPreviewAttendees } from "./EventPreviewAttendees";
|
||||||
import { makeRecurrenceString } from "./utils/makeRecurrenceString";
|
import { makeRecurrenceString } from "./utils/makeRecurrenceString";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
interface EventPreviewDetailsProps {
|
interface EventPreviewDetailsProps {
|
||||||
event: CalendarEvent;
|
event: CalendarEvent;
|
||||||
@@ -28,13 +30,25 @@ export function EventPreviewDetails({
|
|||||||
const infoIconColor = alpha(theme.palette.grey[900], 0.9);
|
const infoIconColor = alpha(theme.palette.grey[900], 0.9);
|
||||||
const infoIconSx = { minWidth: "25px", marginRight: 2, color: infoIconColor };
|
const infoIconSx = { minWidth: "25px", marginRight: 2, color: infoIconColor };
|
||||||
|
|
||||||
|
const resources = useMemo(
|
||||||
|
() => event?.attendee?.filter((attendee) => attendee.cutype === "RESOURCE"),
|
||||||
|
[event?.attendee]
|
||||||
|
);
|
||||||
|
const eventAttendees = useMemo(
|
||||||
|
() =>
|
||||||
|
event?.attendee?.filter((attendee) => attendee.cutype !== "RESOURCE") ??
|
||||||
|
[],
|
||||||
|
[event?.attendee]
|
||||||
|
);
|
||||||
|
|
||||||
const attendees =
|
const attendees =
|
||||||
event.attendee?.filter(
|
eventAttendees?.filter(
|
||||||
(a) => a.cal_address !== event.organizer?.cal_address
|
(a) => a.cal_address !== event.organizer?.cal_address
|
||||||
) || [];
|
) || [];
|
||||||
const organizer = event.attendee?.find(
|
const organizer = eventAttendees?.find(
|
||||||
(a) => a.cal_address === event.organizer?.cal_address
|
(a) => a.cal_address === event.organizer?.cal_address
|
||||||
);
|
);
|
||||||
|
|
||||||
const showDetails = isNotPrivate || isOwn;
|
const showDetails = isNotPrivate || isOwn;
|
||||||
|
|
||||||
if (!showDetails) {
|
if (!showDetails) {
|
||||||
@@ -91,7 +105,7 @@ export function EventPreviewDetails({
|
|||||||
<EventPreviewAttendees
|
<EventPreviewAttendees
|
||||||
attendees={attendees}
|
attendees={attendees}
|
||||||
organizer={organizer}
|
organizer={organizer}
|
||||||
allAttendees={event.attendee ?? []}
|
allAttendees={eventAttendees ?? []}
|
||||||
start={event.start}
|
start={event.start}
|
||||||
end={event.end}
|
end={event.end}
|
||||||
timezone={event.timezone}
|
timezone={event.timezone}
|
||||||
@@ -112,6 +126,56 @@ export function EventPreviewDetails({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Resource */}
|
||||||
|
{resources && (
|
||||||
|
<InfoRow
|
||||||
|
alignItems="flex-start"
|
||||||
|
icon={
|
||||||
|
<Box sx={infoIconSx}>
|
||||||
|
<LayersOutlinedIcon />
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
content={resources.map((resource, index) => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginRight: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="textPrimary"
|
||||||
|
sx={{
|
||||||
|
wordBreak: "break-word",
|
||||||
|
whiteSpace: "pre-line",
|
||||||
|
maxHeight: "33vh",
|
||||||
|
overflowY: "auto",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{resource.cn}
|
||||||
|
{index < resources.length - 1 ? "," : ""}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
wordBreak: "break-word",
|
||||||
|
whiteSpace: "pre-line",
|
||||||
|
overflowY: "auto",
|
||||||
|
width: "100%",
|
||||||
|
fontSize: "13px",
|
||||||
|
color: "#717D96",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t(`eventPreview.${resource.partstat}`)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
style={{
|
||||||
|
fontSize: "16px",
|
||||||
|
fontFamily: "'Inter', sans-serif",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{event.description && (
|
{event.description && (
|
||||||
<InfoRow
|
<InfoRow
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import { deleteEvent, getEvent, putEvent } from "./EventApi";
|
|||||||
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||||
import { moveEventBetweenCalendars } from "./updateEventHelpers/moveEventBetweenCalendars";
|
import { moveEventBetweenCalendars } from "./updateEventHelpers/moveEventBetweenCalendars";
|
||||||
import { detectRecurringEventChanges } from "./utils/detectRecurringEventChanges";
|
import { detectRecurringEventChanges } from "./utils/detectRecurringEventChanges";
|
||||||
|
import { Resource } from "@/components/Attendees/ResourceSearch";
|
||||||
|
|
||||||
function EventUpdateModal({
|
function EventUpdateModal({
|
||||||
eventId,
|
eventId,
|
||||||
@@ -91,6 +92,16 @@ function EventUpdateModal({
|
|||||||
return { zones, browserTz, getTimezoneOffset };
|
return { zones, browserTz, getTimezoneOffset };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const resources: Resource[] = useMemo(() => {
|
||||||
|
const resourcesInEvent =
|
||||||
|
event?.attendee?.filter((attendee) => attendee.cutype === "RESOURCE") ??
|
||||||
|
[];
|
||||||
|
return resourcesInEvent.map((resource) => ({
|
||||||
|
email: resource.cal_address,
|
||||||
|
displayName: resource.cn,
|
||||||
|
}));
|
||||||
|
}, [event?.attendee]);
|
||||||
|
|
||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
const [showDescription, setShowDescription] = useState(
|
const [showDescription, setShowDescription] = useState(
|
||||||
event?.description ? true : false
|
event?.description ? true : false
|
||||||
@@ -129,6 +140,7 @@ function EventUpdateModal({
|
|||||||
const [isFormValid, setIsFormValid] = useState(false);
|
const [isFormValid, setIsFormValid] = useState(false);
|
||||||
const [showValidationErrors, setShowValidationErrors] = useState(false);
|
const [showValidationErrors, setShowValidationErrors] = useState(false);
|
||||||
const [hasEndDateChanged, setHasEndDateChanged] = useState(false);
|
const [hasEndDateChanged, setHasEndDateChanged] = useState(false);
|
||||||
|
const [selectedResources, setSelectedResources] = useState(resources ?? []);
|
||||||
|
|
||||||
const resetAllStateToDefault = useCallback(() => {
|
const resetAllStateToDefault = useCallback(() => {
|
||||||
setShowMore(false);
|
setShowMore(false);
|
||||||
@@ -151,6 +163,7 @@ function EventUpdateModal({
|
|||||||
setTimezone(resolveTimezone(browserDefaultTimeZone));
|
setTimezone(resolveTimezone(browserDefaultTimeZone));
|
||||||
setHasVideoConference(false);
|
setHasVideoConference(false);
|
||||||
setMeetingLink(null);
|
setMeetingLink(null);
|
||||||
|
setSelectedResources([]);
|
||||||
}, [defaultCalendarId]);
|
}, [defaultCalendarId]);
|
||||||
|
|
||||||
// Prevent repeated initialization loops
|
// Prevent repeated initialization loops
|
||||||
@@ -297,7 +310,8 @@ function EventUpdateModal({
|
|||||||
eventToDisplay.attendee
|
eventToDisplay.attendee
|
||||||
? eventToDisplay.attendee.filter(
|
? eventToDisplay.attendee.filter(
|
||||||
(a: userAttendee) =>
|
(a: userAttendee) =>
|
||||||
a.cal_address !== eventToDisplay.organizer?.cal_address
|
a.cal_address !== eventToDisplay.organizer?.cal_address &&
|
||||||
|
a.cutype !== "RESOURCE"
|
||||||
)
|
)
|
||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
@@ -335,6 +349,15 @@ function EventUpdateModal({
|
|||||||
setDescription(eventToDisplay.description);
|
setDescription(eventToDisplay.description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSelectedResources(
|
||||||
|
(eventToDisplay.attendee ?? [])
|
||||||
|
.filter((a: userAttendee) => a.cutype === "RESOURCE")
|
||||||
|
.map((a) => ({
|
||||||
|
email: a.cal_address,
|
||||||
|
displayName: a.cn,
|
||||||
|
}))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [
|
||||||
@@ -345,6 +368,7 @@ function EventUpdateModal({
|
|||||||
calList,
|
calList,
|
||||||
masterEvent,
|
masterEvent,
|
||||||
isLoadingMasterEvent,
|
isLoadingMasterEvent,
|
||||||
|
resources,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 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
|
||||||
@@ -387,6 +411,7 @@ function EventUpdateModal({
|
|||||||
showDescription,
|
showDescription,
|
||||||
showRepeat,
|
showRepeat,
|
||||||
hasEndDateChanged,
|
hasEndDateChanged,
|
||||||
|
resources: selectedResources,
|
||||||
};
|
};
|
||||||
const context: EventFormContext = {
|
const context: EventFormContext = {
|
||||||
eventId,
|
eventId,
|
||||||
@@ -417,6 +442,7 @@ function EventUpdateModal({
|
|||||||
eventId,
|
eventId,
|
||||||
calId,
|
calId,
|
||||||
typeOfAction,
|
typeOfAction,
|
||||||
|
selectedResources,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Check for temp data when modal opens
|
// Check for temp data when modal opens
|
||||||
@@ -454,6 +480,7 @@ function EventUpdateModal({
|
|||||||
setShowDescription,
|
setShowDescription,
|
||||||
setShowRepeat,
|
setShowRepeat,
|
||||||
setHasEndDateChanged,
|
setHasEndDateChanged,
|
||||||
|
setSelectedResources,
|
||||||
});
|
});
|
||||||
// Clear the error flag but keep data until successful save
|
// Clear the error flag but keep data until successful save
|
||||||
const updatedTempData = { ...tempData, fromError: false };
|
const updatedTempData = { ...tempData, fromError: false };
|
||||||
@@ -612,6 +639,23 @@ function EventUpdateModal({
|
|||||||
x_openpass_videoconference: meetingLink || undefined,
|
x_openpass_videoconference: meetingLink || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Map data of resources to attendee before creating event
|
||||||
|
if (selectedResources?.length) {
|
||||||
|
if (!newEvent.attendee) {
|
||||||
|
newEvent.attendee = [];
|
||||||
|
}
|
||||||
|
selectedResources.forEach((resource: Resource) => {
|
||||||
|
newEvent.attendee.push({
|
||||||
|
cn: resource?.displayName ?? "",
|
||||||
|
cal_address: resource?.email ?? "",
|
||||||
|
partstat: "NEEDS-ACTION",
|
||||||
|
rsvp: "TRUE",
|
||||||
|
role: "REQ-PARTICIPANT",
|
||||||
|
cutype: "RESOURCE",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Special case: When converting recurring event to non-recurring
|
// Special case: When converting recurring event to non-recurring
|
||||||
if (
|
if (
|
||||||
recurrenceId &&
|
recurrenceId &&
|
||||||
@@ -1088,6 +1132,8 @@ function EventUpdateModal({
|
|||||||
onValidationChange={setIsFormValid}
|
onValidationChange={setIsFormValid}
|
||||||
showValidationErrors={showValidationErrors}
|
showValidationErrors={showValidationErrors}
|
||||||
onHasEndDateChangedChange={setHasEndDateChanged}
|
onHasEndDateChangedChange={setHasEndDateChanged}
|
||||||
|
selectedResources={selectedResources}
|
||||||
|
setSelectedResources={setSelectedResources}
|
||||||
/>
|
/>
|
||||||
</ResponsiveDialog>
|
</ResponsiveDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Resource } from "@/components/Attendees/ResourceSearch";
|
||||||
import { Calendar } from "../Calendars/CalendarTypes";
|
import { Calendar } from "../Calendars/CalendarTypes";
|
||||||
import { VObjectProperty } from "../Calendars/types/CalendarData";
|
import { VObjectProperty } from "../Calendars/types/CalendarData";
|
||||||
import { userAttendee } from "../User/models/attendee";
|
import { userAttendee } from "../User/models/attendee";
|
||||||
@@ -29,6 +30,7 @@ export interface CalendarEvent {
|
|||||||
alarm?: AlarmObject;
|
alarm?: AlarmObject;
|
||||||
exdates?: string[];
|
exdates?: string[];
|
||||||
passthroughProps?: VObjectProperty[];
|
passthroughProps?: VObjectProperty[];
|
||||||
|
selectedResources?: Resource[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RepetitionObject {
|
export interface RepetitionObject {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export type AttendeeRole = "CHAIR" | "REQ-PARTICIPANT" | "OPT-PARTICIPANT";
|
export type AttendeeRole = "CHAIR" | "REQ-PARTICIPANT" | "OPT-PARTICIPANT";
|
||||||
export type CuType = "INDIVIDUAL" | "GROUP";
|
export type CuType = "INDIVIDUAL" | "GROUP" | "RESOURCE";
|
||||||
export type PartStat = "ACCEPTED" | "DECLINED" | "TENTATIVE" | "NEEDS-ACTION";
|
export type PartStat = "ACCEPTED" | "DECLINED" | "TENTATIVE" | "NEEDS-ACTION";
|
||||||
|
|
||||||
export interface userAttendee {
|
export interface userAttendee {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export async function getUserDetails(id: string): Promise<OpenPaasUserData> {
|
|||||||
|
|
||||||
export async function getResourceDetails(id: string): Promise<ResourceData> {
|
export async function getResourceDetails(id: string): Promise<ResourceData> {
|
||||||
const resource = await api
|
const resource = await api
|
||||||
.get(`linagora.esn.resource/api/resources/${id}`)
|
.get(`linagora.esn.resource/api/resources/${encodeURIComponent(id)}`)
|
||||||
.json();
|
.json();
|
||||||
return resource as ResourceData;
|
return resource as ResourceData;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -187,7 +187,9 @@
|
|||||||
"busy": "Busy",
|
"busy": "Busy",
|
||||||
"visibleTo": "Visible to",
|
"visibleTo": "Visible to",
|
||||||
"visibleAll": "All",
|
"visibleAll": "All",
|
||||||
"visibleParticipants": "Participants"
|
"visibleParticipants": "Participants",
|
||||||
|
"resource": "Resource",
|
||||||
|
"select_resource_placeholder": "Add resource"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"titleRequired": "Title is required",
|
"titleRequired": "Title is required",
|
||||||
@@ -281,6 +283,7 @@
|
|||||||
"ACCEPTED": "Accept",
|
"ACCEPTED": "Accept",
|
||||||
"TENTATIVE": "Maybe",
|
"TENTATIVE": "Maybe",
|
||||||
"DECLINED": "Decline",
|
"DECLINED": "Decline",
|
||||||
|
"NEEDS-ACTION": "Pending",
|
||||||
"showMore": "Show more",
|
"showMore": "Show more",
|
||||||
"showLess": "Show less",
|
"showLess": "Show less",
|
||||||
"joinVideo": "Join the video conference",
|
"joinVideo": "Join the video conference",
|
||||||
|
|||||||
+4
-1
@@ -188,7 +188,9 @@
|
|||||||
"busy": "Occupé",
|
"busy": "Occupé",
|
||||||
"visibleTo": "Visible par",
|
"visibleTo": "Visible par",
|
||||||
"visibleAll": "Tous",
|
"visibleAll": "Tous",
|
||||||
"visibleParticipants": "Participants"
|
"visibleParticipants": "Participants",
|
||||||
|
"resource": "Ressource",
|
||||||
|
"select_resource_placeholder": "Ajouter une ressource"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"titleRequired": "Le titre est obligatoire",
|
"titleRequired": "Le titre est obligatoire",
|
||||||
@@ -282,6 +284,7 @@
|
|||||||
"ACCEPTED": "Accepter",
|
"ACCEPTED": "Accepter",
|
||||||
"TENTATIVE": "Peut-être",
|
"TENTATIVE": "Peut-être",
|
||||||
"DECLINED": "Décliner",
|
"DECLINED": "Décliner",
|
||||||
|
"NEEDS-ACTION": "En attente",
|
||||||
"showMore": "Afficher plus",
|
"showMore": "Afficher plus",
|
||||||
"showLess": "Afficher moins",
|
"showLess": "Afficher moins",
|
||||||
"joinVideo": "Rejoindre la visioconférence",
|
"joinVideo": "Rejoindre la visioconférence",
|
||||||
|
|||||||
+4
-1
@@ -188,7 +188,9 @@
|
|||||||
"busy": "Занят",
|
"busy": "Занят",
|
||||||
"visibleTo": "Видно для",
|
"visibleTo": "Видно для",
|
||||||
"visibleAll": "Всех",
|
"visibleAll": "Всех",
|
||||||
"visibleParticipants": "Участников"
|
"visibleParticipants": "Участников",
|
||||||
|
"resource": "Ресурс",
|
||||||
|
"select_resource_placeholder": "Добавить ресурс"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"titleRequired": "Укажите название",
|
"titleRequired": "Укажите название",
|
||||||
@@ -282,6 +284,7 @@
|
|||||||
"ACCEPTED": "Да",
|
"ACCEPTED": "Да",
|
||||||
"TENTATIVE": "Возможно",
|
"TENTATIVE": "Возможно",
|
||||||
"DECLINED": "Нет",
|
"DECLINED": "Нет",
|
||||||
|
"NEEDS-ACTION": "В ожидании",
|
||||||
"showMore": "Показать больше",
|
"showMore": "Показать больше",
|
||||||
"showLess": "Показать меньше",
|
"showLess": "Показать меньше",
|
||||||
"joinVideo": "Присоединиться к видеоконференции",
|
"joinVideo": "Присоединиться к видеоконференции",
|
||||||
|
|||||||
+4
-1
@@ -186,7 +186,9 @@
|
|||||||
"busy": "Bận",
|
"busy": "Bận",
|
||||||
"visibleTo": "Hiển thị với",
|
"visibleTo": "Hiển thị với",
|
||||||
"visibleAll": "Tất cả",
|
"visibleAll": "Tất cả",
|
||||||
"visibleParticipants": "Người tham gia"
|
"visibleParticipants": "Người tham gia",
|
||||||
|
"resource": "Tài nguyên",
|
||||||
|
"select_resource_placeholder": "Thêm tài nguyên"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"titleRequired": "Tiêu đề là bắt buộc",
|
"titleRequired": "Tiêu đề là bắt buộc",
|
||||||
@@ -280,6 +282,7 @@
|
|||||||
"ACCEPTED": "Chấp nhận",
|
"ACCEPTED": "Chấp nhận",
|
||||||
"TENTATIVE": "Có thể",
|
"TENTATIVE": "Có thể",
|
||||||
"DECLINED": "Từ chối",
|
"DECLINED": "Từ chối",
|
||||||
|
"NEEDS-ACTION": "Đang chờ",
|
||||||
"showMore": "Xem thêm",
|
"showMore": "Xem thêm",
|
||||||
"showLess": "Thu gọn",
|
"showLess": "Thu gọn",
|
||||||
"joinVideo": "Tham gia cuộc họp video",
|
"joinVideo": "Tham gia cuộc họp video",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Resource } from "@/components/Attendees/ResourceSearch";
|
||||||
import { RepetitionObject } from "@/features/Events/EventsTypes";
|
import { RepetitionObject } from "@/features/Events/EventsTypes";
|
||||||
import { userAttendee } from "@/features/User/models/attendee";
|
import { userAttendee } from "@/features/User/models/attendee";
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ export interface EventFormTempData {
|
|||||||
typeOfAction?: "solo" | "all";
|
typeOfAction?: "solo" | "all";
|
||||||
// Flag to indicate this is from an error
|
// Flag to indicate this is from an error
|
||||||
fromError?: boolean;
|
fromError?: boolean;
|
||||||
|
resources?: Resource[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY_PREFIX = "eventFormTempData_";
|
const STORAGE_KEY_PREFIX = "eventFormTempData_";
|
||||||
@@ -94,6 +96,7 @@ export interface EventFormState {
|
|||||||
showDescription?: boolean;
|
showDescription?: boolean;
|
||||||
showRepeat?: boolean;
|
showRepeat?: boolean;
|
||||||
hasEndDateChanged?: boolean;
|
hasEndDateChanged?: boolean;
|
||||||
|
resources?: Resource[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EventFormContext {
|
export interface EventFormContext {
|
||||||
@@ -133,6 +136,7 @@ export interface EventFormSetters {
|
|||||||
setShowDescription?: (value: boolean) => void;
|
setShowDescription?: (value: boolean) => void;
|
||||||
setShowRepeat?: (value: boolean) => void;
|
setShowRepeat?: (value: boolean) => void;
|
||||||
setHasEndDateChanged?: (value: boolean) => void;
|
setHasEndDateChanged?: (value: boolean) => void;
|
||||||
|
setSelectedResources?: (value: Resource[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function restoreFormDataFromTemp(
|
export function restoreFormDataFromTemp(
|
||||||
@@ -169,4 +173,5 @@ export function restoreFormDataFromTemp(
|
|||||||
) {
|
) {
|
||||||
setters.setHasEndDateChanged(tempData.hasEndDateChanged);
|
setters.setHasEndDateChanged(tempData.hasEndDateChanged);
|
||||||
}
|
}
|
||||||
|
setters.setSelectedResources?.(tempData.resources ?? []);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user