@@ -0,0 +1,199 @@
|
||||
import { getCalendarsListAsync } from "@/features/Calendars/services/getCalendarsListAsync";
|
||||
import {
|
||||
getOpenPaasUser,
|
||||
getResourceDetails,
|
||||
getUserDetails,
|
||||
} from "@/features/User/userAPI";
|
||||
import { getCalendars } from "@/features/Calendars/CalendarApi";
|
||||
import { formatReduxError, toRejectedError } from "@/utils/errorUtils";
|
||||
import { normalizeCalendar } from "@/features/Calendars/utils/normalizeCalendar";
|
||||
|
||||
jest.mock("@/features/User/userAPI");
|
||||
jest.mock("@/features/Calendars/CalendarApi");
|
||||
jest.mock("@/utils/errorUtils");
|
||||
jest.mock("@/features/Calendars/utils/normalizeCalendar");
|
||||
|
||||
const mockedGetOpenPaasUser = getOpenPaasUser as jest.Mock;
|
||||
const mockedGetUserDetails = getUserDetails as jest.Mock;
|
||||
const mockedgetResourceDetails = getResourceDetails as jest.Mock;
|
||||
const mockedGetCalendars = getCalendars as jest.Mock;
|
||||
const mockedFormatReduxError = formatReduxError as jest.Mock;
|
||||
const mockedToRejectedError = toRejectedError as jest.Mock;
|
||||
const mockedNormalizeCalendar = normalizeCalendar as jest.Mock;
|
||||
|
||||
describe("getCalendarsListAsync", () => {
|
||||
let dispatch: jest.Mock;
|
||||
let getState: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
dispatch = jest.fn();
|
||||
getState = jest.fn();
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockedFormatReduxError.mockImplementation((err) => {
|
||||
if (err?.message) return err.message;
|
||||
if (typeof err === "string") return err;
|
||||
return JSON.stringify(err);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle successful execution and merge with existing calendars", async () => {
|
||||
getState.mockReturnValue({
|
||||
calendars: {
|
||||
list: {
|
||||
"cal-existing": {
|
||||
id: "cal-existing",
|
||||
color: { light: "red", dark: "#FFF" },
|
||||
events: { "event-1": {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
userData: { openpaasId: "user-123" },
|
||||
},
|
||||
});
|
||||
|
||||
const mockCalendarsResponse = {
|
||||
_embedded: {
|
||||
"dav:calendar": [{ id: "cal-existing" }, { id: "cal-new" }],
|
||||
},
|
||||
};
|
||||
mockedGetCalendars.mockResolvedValue(mockCalendarsResponse);
|
||||
|
||||
mockedNormalizeCalendar
|
||||
.mockReturnValueOnce({
|
||||
cal: { "dav:name": "Existing Cal", "apple:color": "blue" },
|
||||
id: "cal-existing",
|
||||
ownerId: "user-123",
|
||||
description: "old cal",
|
||||
delegated: false,
|
||||
link: "/link/1",
|
||||
visibility: 1,
|
||||
access: 3,
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
cal: { "dav:name": "New Cal" },
|
||||
id: "cal-new",
|
||||
ownerId: "user-456",
|
||||
description: "new cal",
|
||||
delegated: true,
|
||||
link: "/link/2",
|
||||
visibility: 2,
|
||||
access: 2,
|
||||
invite: [{ href: "", principal: "", access: 3, inviteStatus: 1 }],
|
||||
});
|
||||
|
||||
mockedGetUserDetails
|
||||
.mockResolvedValueOnce({
|
||||
firstname: "John",
|
||||
lastname: "Doe",
|
||||
emails: ["john@example.com"],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
firstname: "Jane",
|
||||
lastname: "Smith",
|
||||
emails: ["jane@example.com"],
|
||||
});
|
||||
|
||||
const thunk = getCalendarsListAsync();
|
||||
const result = await thunk(dispatch, getState, undefined);
|
||||
|
||||
expect(result.type).toBe("calendars/getCalendars/fulfilled");
|
||||
const payload = result.payload as {
|
||||
importedCalendars: any;
|
||||
errors: string;
|
||||
};
|
||||
|
||||
expect(payload.errors).toBe("");
|
||||
expect(Object.keys(payload.importedCalendars)).toHaveLength(2);
|
||||
|
||||
expect(payload.importedCalendars["cal-existing"]).toMatchObject({
|
||||
id: "cal-existing",
|
||||
color: { light: "blue", dark: "#FFF" },
|
||||
events: { "event-1": {} },
|
||||
});
|
||||
|
||||
expect(payload.importedCalendars["cal-new"]).toMatchObject({
|
||||
id: "cal-new",
|
||||
name: "New Cal",
|
||||
owner: { firstname: "Jane", lastname: "Smith" },
|
||||
});
|
||||
|
||||
expect(mockedGetOpenPaasUser).not.toHaveBeenCalled(); // User ID existed in state
|
||||
expect(mockedGetCalendars).toHaveBeenCalledWith("user-123");
|
||||
});
|
||||
|
||||
it("should fetch user using getOpenPaasUser if openpaasId is not in state", async () => {
|
||||
getState.mockReturnValue({ calendars: {}, user: {} });
|
||||
mockedGetOpenPaasUser.mockResolvedValue({ id: "fetched-user-123" });
|
||||
mockedGetCalendars.mockResolvedValue({ _embedded: { "dav:calendar": [] } });
|
||||
|
||||
const thunk = getCalendarsListAsync();
|
||||
await thunk(dispatch, getState, undefined);
|
||||
|
||||
expect(mockedGetOpenPaasUser).toHaveBeenCalled();
|
||||
expect(mockedGetCalendars).toHaveBeenCalledWith("fetched-user-123");
|
||||
});
|
||||
|
||||
it("should handle error when API call fails", async () => {
|
||||
getState.mockReturnValue({ calendars: {}, user: {} });
|
||||
mockedGetOpenPaasUser.mockResolvedValue({ id: "fetched-user-123" });
|
||||
|
||||
mockedGetCalendars.mockRejectedValue({
|
||||
response: { status: 500 },
|
||||
message: "Server Error",
|
||||
});
|
||||
mockedToRejectedError.mockReturnValueOnce({
|
||||
status: 500,
|
||||
message: "Server Error",
|
||||
});
|
||||
|
||||
const thunk = getCalendarsListAsync();
|
||||
const result = await thunk(dispatch, getState, undefined);
|
||||
|
||||
expect(result.type).toBe("calendars/getCalendars/rejected");
|
||||
expect(result.payload).toEqual({
|
||||
status: 500,
|
||||
message: "Server Error",
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch resource detail as fallback when user not found", async () => {
|
||||
getState.mockReturnValue({
|
||||
calendars: {},
|
||||
user: { userData: { openpaasId: "user-123" } },
|
||||
});
|
||||
mockedGetCalendars.mockResolvedValue({
|
||||
_embedded: { "dav:calendar": [{ id: "cal-1" }] },
|
||||
});
|
||||
mockedNormalizeCalendar.mockReturnValue({
|
||||
cal: { "dav:name": "Resource Cal" },
|
||||
id: "cal-1",
|
||||
ownerId: "resource-123",
|
||||
});
|
||||
|
||||
// getUserDetails fails with 404 for the resource ID
|
||||
mockedGetUserDetails.mockRejectedValueOnce({ response: { status: 404 } });
|
||||
// Then getResourceDetails is called and succeeds
|
||||
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 result = await thunk(dispatch, getState, undefined);
|
||||
|
||||
const payload = result.payload as any;
|
||||
expect(mockedgetResourceDetails).toHaveBeenCalledWith("resource-123");
|
||||
expect(mockedGetUserDetails).toHaveBeenCalledWith("creator-456");
|
||||
expect(payload.importedCalendars["cal-1"].owner).toEqual({
|
||||
firstname: "Creator",
|
||||
lastname: "User",
|
||||
emails: [],
|
||||
resource: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { clientConfig } from "@/features/User/oidcAuth";
|
||||
import {
|
||||
getOpenPaasUser,
|
||||
updateUserConfigurations,
|
||||
getResourceDetails,
|
||||
} from "@/features/User/userAPI";
|
||||
import { api } from "@/utils/apiUtils";
|
||||
|
||||
@@ -24,6 +25,24 @@ describe("getOpenPaasUser", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getResourceDetails", () => {
|
||||
it("should fetch and return resource details", async () => {
|
||||
const mockResource = { _id: "res-123", name: "Meeting Room A" };
|
||||
const resourceId = "res-123";
|
||||
|
||||
(api.get as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue(mockResource),
|
||||
});
|
||||
|
||||
const result = await getResourceDetails(resourceId);
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith(
|
||||
`linagora.esn.resource/api/resources/${resourceId}`
|
||||
);
|
||||
expect(result).toEqual(mockResource);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateUserConfigurations", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
@@ -136,7 +136,16 @@ export default function CalendarSelection({
|
||||
(id) => extractEventBaseUuid(id) !== userId && calendars[id]?.delegated
|
||||
);
|
||||
const sharedCalendars = Object.keys(calendars || {}).filter(
|
||||
(id) => extractEventBaseUuid(id) !== userId && !calendars?.[id]?.delegated
|
||||
(id) =>
|
||||
extractEventBaseUuid(id) !== userId &&
|
||||
!calendars?.[id]?.delegated &&
|
||||
!calendars?.[id]?.owner?.resource
|
||||
);
|
||||
const resourceCalendars = Object.keys(calendars || {}).filter(
|
||||
(id) =>
|
||||
extractEventBaseUuid(id) !== userId &&
|
||||
!calendars?.[id]?.delegated &&
|
||||
calendars?.[id]?.owner?.resource
|
||||
);
|
||||
|
||||
const handleCalendarToggle = (name: string) => {
|
||||
@@ -149,6 +158,7 @@ export default function CalendarSelection({
|
||||
const [anchorElCal, setAnchorElCal] = useState<HTMLElement | null>(null);
|
||||
const [anchorElCalOthers, setAnchorElCalOthers] =
|
||||
useState<HTMLElement | null>(null);
|
||||
// const [anchorElCalResources, setAnchorElCalResources] = useState<HTMLElement | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -194,6 +204,18 @@ export default function CalendarSelection({
|
||||
}}
|
||||
defaultExpanded
|
||||
/>
|
||||
|
||||
<CalendarAccordion
|
||||
title={t("calendar.resources")}
|
||||
calendars={resourceCalendars}
|
||||
selectedCalendars={selectedCalendars}
|
||||
showAddButton={false}
|
||||
handleToggle={handleCalendarToggle}
|
||||
setOpen={(_) => {
|
||||
// TO DO: Implement open resource selection
|
||||
}}
|
||||
defaultExpanded
|
||||
/>
|
||||
</div>
|
||||
<CalendarPopover
|
||||
open={Boolean(anchorElCal)}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { RootState } from "@/app/store";
|
||||
import { OpenPaasUserData } from "@/features/User/type/OpenPaasUserData";
|
||||
import { getOpenPaasUser, getUserDetails } from "@/features/User/userAPI";
|
||||
import {
|
||||
getOpenPaasUser,
|
||||
getResourceDetails,
|
||||
getUserDetails,
|
||||
} from "@/features/User/userAPI";
|
||||
import { defaultColors } from "@/utils/defaultColors";
|
||||
import { formatReduxError, toRejectedError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
@@ -39,11 +43,42 @@ export const getCalendarsListAsync = createAsyncThunk<
|
||||
const ownerDataMap = new Map<string, OpenPaasUserData>();
|
||||
const OWNER_BATCH_SIZE = 20;
|
||||
|
||||
const fetchResourceData = async (ownerId: string) => {
|
||||
try {
|
||||
const data = await getResourceDetails(ownerId);
|
||||
const ownerData = await getUserDetails(data.creator);
|
||||
ownerDataMap.set(ownerId, {
|
||||
...ownerData,
|
||||
resource: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to fetch resource details for ${ownerId}:`,
|
||||
error
|
||||
);
|
||||
ownerDataMap.set(ownerId, {
|
||||
firstname: "",
|
||||
lastname: "Unknown User",
|
||||
emails: [],
|
||||
resource: true,
|
||||
});
|
||||
errors.push(formatReduxError(error));
|
||||
}
|
||||
};
|
||||
|
||||
const fetchOwnerData = async (ownerId: string) => {
|
||||
try {
|
||||
const data = await getUserDetails(ownerId);
|
||||
ownerDataMap.set(ownerId, data);
|
||||
} catch (error) {
|
||||
const status = (error as { response?: { status?: number } }).response
|
||||
?.status;
|
||||
|
||||
if (status === 404) {
|
||||
await fetchResourceData(ownerId);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`Failed to fetch user details for ${ownerId}:`, error);
|
||||
ownerDataMap.set(ownerId, {
|
||||
firstname: "",
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface OpenPaasUserData {
|
||||
modules?: ModuleConfiguration[];
|
||||
};
|
||||
emails: string[];
|
||||
resource?: boolean;
|
||||
}
|
||||
|
||||
export function ToUserData(
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface ResourceData {
|
||||
_id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
icon: string;
|
||||
deleted: boolean;
|
||||
creator: string;
|
||||
administrators: {
|
||||
_id: string;
|
||||
id: string;
|
||||
objectType: string;
|
||||
}[];
|
||||
timestamps: {
|
||||
creation: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
resource?: boolean;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ModuleConfiguration,
|
||||
SearchResponseItem,
|
||||
} from "./userDataTypes";
|
||||
import { ResourceData } from "./type/ResourceData";
|
||||
|
||||
export async function getOpenPaasUser() {
|
||||
const user = await api.get(`api/user`);
|
||||
@@ -42,6 +43,13 @@ export async function getUserDetails(id: string): Promise<OpenPaasUserData> {
|
||||
return user as OpenPaasUserData;
|
||||
}
|
||||
|
||||
export async function getResourceDetails(id: string): Promise<ResourceData> {
|
||||
const resource = await api
|
||||
.get(`linagora.esn.resource/api/resources/${id}`)
|
||||
.json();
|
||||
return resource as ResourceData;
|
||||
}
|
||||
|
||||
export interface UserConfigurationUpdates {
|
||||
language?: string;
|
||||
notifications?: Record<string, unknown>;
|
||||
|
||||
+2
-1
@@ -25,7 +25,8 @@
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"resources": "Resources"
|
||||
},
|
||||
"actions": {
|
||||
"modify": "Modify",
|
||||
|
||||
+2
-1
@@ -25,7 +25,8 @@
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"resources": "Ressources"
|
||||
},
|
||||
"actions": {
|
||||
"modify": "Modifier",
|
||||
|
||||
+2
-1
@@ -25,7 +25,8 @@
|
||||
"title": "Удалить %{name}?",
|
||||
"personalWarning": "Вы потеряете все события в этом календаре.",
|
||||
"sharedWarning": "Вы потеряете доступ. Позже можно добавить снова."
|
||||
}
|
||||
},
|
||||
"resources": "Ресурсы"
|
||||
},
|
||||
"actions": {
|
||||
"modify": "Изменить",
|
||||
|
||||
+2
-1
@@ -25,7 +25,8 @@
|
||||
"title": "Xóa %{name}?",
|
||||
"personalWarning": "Bạn sẽ mất tất cả sự kiện trong lịch này.",
|
||||
"sharedWarning": "Bạn sẽ mất quyền truy cập các sự kiện của lịch. Bạn vẫn có thể thêm lại sau."
|
||||
}
|
||||
},
|
||||
"resources": "Tài nguyên"
|
||||
},
|
||||
"actions": {
|
||||
"modify": "Chỉnh sửa",
|
||||
|
||||
Reference in New Issue
Block a user