@@ -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 {
|
import {
|
||||||
getOpenPaasUser,
|
getOpenPaasUser,
|
||||||
updateUserConfigurations,
|
updateUserConfigurations,
|
||||||
|
getResourceDetails,
|
||||||
} from "@/features/User/userAPI";
|
} from "@/features/User/userAPI";
|
||||||
import { api } from "@/utils/apiUtils";
|
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", () => {
|
describe("updateUserConfigurations", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
|||||||
@@ -136,7 +136,16 @@ export default function CalendarSelection({
|
|||||||
(id) => extractEventBaseUuid(id) !== userId && calendars[id]?.delegated
|
(id) => extractEventBaseUuid(id) !== userId && calendars[id]?.delegated
|
||||||
);
|
);
|
||||||
const sharedCalendars = Object.keys(calendars || {}).filter(
|
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) => {
|
const handleCalendarToggle = (name: string) => {
|
||||||
@@ -149,6 +158,7 @@ export default function CalendarSelection({
|
|||||||
const [anchorElCal, setAnchorElCal] = useState<HTMLElement | null>(null);
|
const [anchorElCal, setAnchorElCal] = useState<HTMLElement | null>(null);
|
||||||
const [anchorElCalOthers, setAnchorElCalOthers] =
|
const [anchorElCalOthers, setAnchorElCalOthers] =
|
||||||
useState<HTMLElement | null>(null);
|
useState<HTMLElement | null>(null);
|
||||||
|
// const [anchorElCalResources, setAnchorElCalResources] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -194,6 +204,18 @@ export default function CalendarSelection({
|
|||||||
}}
|
}}
|
||||||
defaultExpanded
|
defaultExpanded
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CalendarAccordion
|
||||||
|
title={t("calendar.resources")}
|
||||||
|
calendars={resourceCalendars}
|
||||||
|
selectedCalendars={selectedCalendars}
|
||||||
|
showAddButton={false}
|
||||||
|
handleToggle={handleCalendarToggle}
|
||||||
|
setOpen={(_) => {
|
||||||
|
// TO DO: Implement open resource selection
|
||||||
|
}}
|
||||||
|
defaultExpanded
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CalendarPopover
|
<CalendarPopover
|
||||||
open={Boolean(anchorElCal)}
|
open={Boolean(anchorElCal)}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { RootState } from "@/app/store";
|
import { RootState } from "@/app/store";
|
||||||
import { OpenPaasUserData } from "@/features/User/type/OpenPaasUserData";
|
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 { defaultColors } from "@/utils/defaultColors";
|
||||||
import { formatReduxError, toRejectedError } from "@/utils/errorUtils";
|
import { formatReduxError, toRejectedError } from "@/utils/errorUtils";
|
||||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||||
@@ -39,11 +43,42 @@ export const getCalendarsListAsync = createAsyncThunk<
|
|||||||
const ownerDataMap = new Map<string, OpenPaasUserData>();
|
const ownerDataMap = new Map<string, OpenPaasUserData>();
|
||||||
const OWNER_BATCH_SIZE = 20;
|
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) => {
|
const fetchOwnerData = async (ownerId: string) => {
|
||||||
try {
|
try {
|
||||||
const data = await getUserDetails(ownerId);
|
const data = await getUserDetails(ownerId);
|
||||||
ownerDataMap.set(ownerId, data);
|
ownerDataMap.set(ownerId, data);
|
||||||
} catch (error) {
|
} 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);
|
console.error(`Failed to fetch user details for ${ownerId}:`, error);
|
||||||
ownerDataMap.set(ownerId, {
|
ownerDataMap.set(ownerId, {
|
||||||
firstname: "",
|
firstname: "",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface OpenPaasUserData {
|
|||||||
modules?: ModuleConfiguration[];
|
modules?: ModuleConfiguration[];
|
||||||
};
|
};
|
||||||
emails: string[];
|
emails: string[];
|
||||||
|
resource?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToUserData(
|
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,
|
ModuleConfiguration,
|
||||||
SearchResponseItem,
|
SearchResponseItem,
|
||||||
} from "./userDataTypes";
|
} from "./userDataTypes";
|
||||||
|
import { ResourceData } from "./type/ResourceData";
|
||||||
|
|
||||||
export async function getOpenPaasUser() {
|
export async function getOpenPaasUser() {
|
||||||
const user = await api.get(`api/user`);
|
const user = await api.get(`api/user`);
|
||||||
@@ -42,6 +43,13 @@ export async function getUserDetails(id: string): Promise<OpenPaasUserData> {
|
|||||||
return user as 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 {
|
export interface UserConfigurationUpdates {
|
||||||
language?: string;
|
language?: string;
|
||||||
notifications?: Record<string, unknown>;
|
notifications?: Record<string, unknown>;
|
||||||
|
|||||||
+2
-1
@@ -25,7 +25,8 @@
|
|||||||
"title": "Remove %{name}?",
|
"title": "Remove %{name}?",
|
||||||
"personalWarning": "You will lose all events in this calendar.",
|
"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."
|
"sharedWarning": "You will lose access to its events. You will still be able to add it back later."
|
||||||
}
|
},
|
||||||
|
"resources": "Resources"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"modify": "Modify",
|
"modify": "Modify",
|
||||||
|
|||||||
+2
-1
@@ -25,7 +25,8 @@
|
|||||||
"title": "Supprimer %{name} ?",
|
"title": "Supprimer %{name} ?",
|
||||||
"personalWarning": "Vous allez perdre tous les événements de ce calendrier.",
|
"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."
|
"sharedWarning": "Vous allez perdre l’accès à ses événements. Vous pourrez toujours le rajouter plus tard."
|
||||||
}
|
},
|
||||||
|
"resources": "Ressources"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"modify": "Modifier",
|
"modify": "Modifier",
|
||||||
|
|||||||
+2
-1
@@ -25,7 +25,8 @@
|
|||||||
"title": "Удалить %{name}?",
|
"title": "Удалить %{name}?",
|
||||||
"personalWarning": "Вы потеряете все события в этом календаре.",
|
"personalWarning": "Вы потеряете все события в этом календаре.",
|
||||||
"sharedWarning": "Вы потеряете доступ. Позже можно добавить снова."
|
"sharedWarning": "Вы потеряете доступ. Позже можно добавить снова."
|
||||||
}
|
},
|
||||||
|
"resources": "Ресурсы"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"modify": "Изменить",
|
"modify": "Изменить",
|
||||||
|
|||||||
+2
-1
@@ -25,7 +25,8 @@
|
|||||||
"title": "Xóa %{name}?",
|
"title": "Xóa %{name}?",
|
||||||
"personalWarning": "Bạn sẽ mất tất cả sự kiện trong lịch này.",
|
"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."
|
"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": {
|
"actions": {
|
||||||
"modify": "Chỉnh sửa",
|
"modify": "Chỉnh sửa",
|
||||||
|
|||||||
Reference in New Issue
Block a user