[#421] added eslint check to CI (#534)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2026-02-10 10:41:37 +01:00
committed by GitHub
parent 8a6ec8fc39
commit d6e464afad
116 changed files with 1232 additions and 801 deletions
+5 -4
View File
@@ -1,10 +1,11 @@
import { api } from "@/utils/apiUtils";
import { CalendarInput, CalendarList } from "./types/CalendarData";
export async function getCalendars(
userId: string,
scope: string = "personal=true&sharedDelegationStatus=accepted&sharedPublicSubscription=true&",
signal?: AbortSignal
) {
): Promise<CalendarList> {
const calendars = await api
.get(`dav/calendars/${userId}.json?${scope}`, {
headers: {
@@ -13,7 +14,7 @@ export async function getCalendars(
signal,
})
.json();
return calendars;
return calendars as CalendarList;
}
export async function getCalendar(
@@ -59,7 +60,7 @@ export async function postCalendar(
export async function addSharedCalendar(
userId: string,
calId: string,
cal: Record<string, any>
cal: CalendarInput
) {
const response = await api.post(`dav/calendars/${userId}.json`, {
headers: {
@@ -70,7 +71,7 @@ export async function addSharedCalendar(
...cal.cal,
"dav:name":
cal.cal["dav:name"] === "#default"
? cal.owner.displayName + "'s calendar"
? (cal.owner?.displayName ?? "Unknown") + "'s calendar"
: cal.cal["dav:name"],
"calendarserver:source": {
acl: cal.cal.acl,
@@ -1,7 +1,8 @@
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { getUserDetails } from "@/features/User/userAPI";
import { toRejectedError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { addSharedCalendar } from "../CalendarApi";
import { CalendarInput } from "../types/CalendarData";
import { RejectedError } from "../types/RejectedError";
export const addSharedCalendarAsync = createAsyncThunk<
@@ -14,14 +15,14 @@ export const addSharedCalendarAsync = createAsyncThunk<
owner: string;
ownerEmails: string[];
},
{ userId: string; calId: string; cal: Record<string, any> },
{ userId: string; calId: string; cal: CalendarInput },
{ rejectValue: RejectedError }
>(
"calendars/addSharedCalendar",
async ({ userId, calId, cal }, { rejectWithValue }) => {
try {
await addSharedCalendar(userId, calId, cal);
const ownerData: any = await getUserDetails(
const ownerData = await getUserDetails(
cal.cal._links.self.href
.replace("/calendars/", "")
.replace(".json", "")
@@ -29,28 +30,25 @@ export const addSharedCalendarAsync = createAsyncThunk<
);
return {
calId: cal.cal._links.self.href
.replace("/calendars/", "")
calId: cal.cal._links.self?.href
?.replace("/calendars/", "")
.replace(".json", ""),
color: cal.color,
link: `/calendars/${userId}/${calId}.json`,
desc: cal.cal["caldav:description"],
desc: cal.cal["caldav:description"] ?? "",
name:
ownerData.id !== userId && cal.cal["dav:name"] === "#default"
? `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
ownerData.lastname
ownerData.lastname ?? ""
}` + "'s calendar"
: cal.cal["dav:name"],
: (cal.cal["dav:name"] ?? ""),
owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
ownerData.lastname
ownerData.lastname ?? ""
}`,
ownerEmails: ownerData.emails,
};
} catch (err: any) {
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
});
} catch (err) {
return rejectWithValue(toRejectedError(err));
}
}
);
@@ -1,6 +1,5 @@
import { getUserDetails } from "@/features/User/userAPI";
import { userData } from "@/features/User/userDataTypes";
import { formatReduxError } from "@/utils/errorUtils";
import { toRejectedError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { postCalendar } from "../CalendarApi";
import { RejectedError } from "../types/RejectedError";
@@ -45,11 +44,8 @@ export const createCalendarAsync = createAsyncThunk<
owner,
ownerEmails: userData.email ? [userData.email] : [],
};
} catch (err: any) {
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
});
} catch (err) {
return rejectWithValue(toRejectedError(err));
}
}
);
@@ -1,5 +1,5 @@
import { deleteEvent } from "@/features/Events/EventApi";
import { formatReduxError } from "@/utils/errorUtils";
import { toRejectedError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { RejectedError } from "../types/RejectedError";
@@ -13,11 +13,8 @@ export const deleteEventAsync = createAsyncThunk<
try {
await deleteEvent(eventURL);
return { calId, eventId };
} catch (err: any) {
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
});
} catch (err) {
return rejectWithValue(toRejectedError(err));
}
}
);
@@ -11,12 +11,13 @@ export const deleteEventInstanceAsync = createAsyncThunk<
{ rejectValue: RejectedError }
>("calendars/delEventInstance", async ({ cal, event }, { rejectWithValue }) => {
try {
await deleteEventInstance(event, cal.ownerEmails?.[0]);
await deleteEventInstance(event);
return { calId: cal.id, eventId: event.uid };
} catch (err: any) {
} catch (err) {
const error = err as { response?: { status?: number } };
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
status: error.response?.status,
});
}
});
@@ -1,9 +1,14 @@
import { CalendarEvent } from "@/features/Events/EventsTypes";
import { formatReduxError } from "@/utils/errorUtils";
import {
CalendarData,
CalendarItem,
} from "@/features/Calendars/types/CalendarData";
import { toRejectedError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { getCalendar } from "../CalendarApi";
import { RejectedError } from "../types/RejectedError";
import { extractCalendarEvents } from "../utils/extractCalendarEvents";
import { defaultColors } from "@/components/Calendar/utils/calendarColorsUtils";
export const getCalendarDetailAsync = createAsyncThunk<
{
@@ -23,24 +28,27 @@ export const getCalendarDetailAsync = createAsyncThunk<
"calendars/getCalendarDetails",
async ({ calId, match, calType, signal }, { rejectWithValue }) => {
try {
const calendar = (await getCalendar(calId, match, signal)) as any;
const calendar = (await getCalendar(
calId,
match,
signal
)) as CalendarData;
const color = calendar["apple:color"];
const color = calendar["apple:color"]
? { light: calendar["apple:color"], dark: calendar["apple:color"] }
: defaultColors[0];
const syncToken = calendar._embedded?.["sync-token"];
const items = calendar._embedded?.["dav:item"];
const events: CalendarEvent[] = Array.isArray(items)
? items.flatMap((item: any) =>
? items.flatMap((item: CalendarItem) =>
extractCalendarEvents(item, { calId, color })
)
: [];
return { calId, events, calType, syncToken };
} catch (err: any) {
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
});
} catch (err) {
return rejectWithValue(toRejectedError(err));
}
}
);
@@ -1,32 +1,33 @@
import { RootState } from "@/app/store";
import { OpenPaasUserData } from "@/features/User/type/OpenPaasUserData";
import { getOpenPaasUser, getUserDetails } from "@/features/User/userAPI";
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { getCalendars } from "../CalendarApi";
import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes";
import { CalendarData } from "../types/CalendarData";
import { RejectedError } from "../types/RejectedError";
import { normalizeCalendar } from "../utils/normalizeCalendar";
export const getCalendarsListAsync = createAsyncThunk<
{ importedCalendars: Record<string, Calendar>; errors: string },
void,
{ rejectValue: RejectedError; state: any }
{ rejectValue: RejectedError; state: RootState }
>("calendars/getCalendars", async (_, { rejectWithValue, getState }) => {
const state = getState() as any;
const state = getState();
const existingCalendars = state.calendars.list || {};
const existingUser = { id: state.user?.userData?.openpaasId || undefined };
try {
const fetchedCalendars: Record<string, Calendar> = {};
const user = existingUser.id
? existingUser
: ((await getOpenPaasUser()) as Record<string, string>);
const calendars = (await getCalendars(user.id)) as Record<string, any>;
const rawCalendars = calendars._embedded["dav:calendar"] as Record<
string,
any
>[];
: ((await getOpenPaasUser()) as OpenPaasUserData);
const calendars = await getCalendars(user.id);
const rawCalendars = calendars._embedded["dav:calendar"];
const errors: string[] = [];
const normalizedCalendars = rawCalendars.map((cal) =>
const normalizedCalendars = rawCalendars.map((cal: CalendarData) =>
normalizeCalendar(cal)
);
@@ -34,14 +35,14 @@ export const getCalendarsListAsync = createAsyncThunk<
new Set(normalizedCalendars.map(({ ownerId }) => ownerId).filter(Boolean))
);
const ownerDataMap = new Map<string, any>();
const ownerDataMap = new Map<string, OpenPaasUserData>();
const OWNER_BATCH_SIZE = 20;
const fetchOwnerData = async (ownerId: string) => {
try {
const data = await getUserDetails(ownerId);
ownerDataMap.set(ownerId, data);
} catch (error: any) {
} catch (error) {
console.error(`Failed to fetch user details for ${ownerId}:`, error);
ownerDataMap.set(ownerId, {
firstname: "",
@@ -72,7 +73,7 @@ export const getCalendarsListAsync = createAsyncThunk<
const color = {
light: cal["apple:color"] ?? "#006BD8",
dark: cal["X-TWAKE-Dark-theme-color"] ?? "#FFF",
dark: "#FFF",
};
fetchedCalendars[id] = {
id,
@@ -120,10 +121,11 @@ export const getCalendarsListAsync = createAsyncThunk<
importedCalendars,
errors: errors.join("\n"),
};
} catch (err: any) {
} catch (err) {
const error = err as { response?: { status?: number } };
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
status: error.response?.status,
});
}
});
@@ -15,10 +15,11 @@ export const getEventAsync = createAsyncThunk<
calId: event.calId,
event: response,
};
} catch (err: any) {
} catch (err) {
const error = err as { response?: { status?: number } };
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
status: error.response?.status,
});
}
});
@@ -4,8 +4,8 @@ import { getUserDetails } from "@/features/User/userAPI";
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { getCalendars } from "../CalendarApi";
import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes";
import { RejectedError } from "../types/RejectedError";
export const getTempCalendarsListAsync = createAsyncThunk<
Record<string, Calendar>,
@@ -20,10 +20,10 @@ export const getTempCalendarsListAsync = createAsyncThunk<
`TRANSLATION:calendar.userDoesNotHaveValidId|name=${encodeURIComponent(username)}`
);
}
const calendars = (await getCalendars(
const calendars = await getCalendars(
tempUser.openpaasId,
"sharedPublic=true&"
)) as Record<string, any>;
);
const rawCalendars = calendars._embedded?.["dav:calendar"];
if (!rawCalendars || rawCalendars.length === 0) {
@@ -36,17 +36,20 @@ export const getTempCalendarsListAsync = createAsyncThunk<
}
for (const cal of rawCalendars) {
const name = cal["dav:name"];
const description = cal["caldav:description"];
const name = cal["dav:name"] ?? "";
const description = cal["caldav:description"] ?? "";
const delegated = cal["calendarserver:delegatedsource"] ? true : false;
const source = cal["calendarserver:source"]
? cal["calendarserver:source"]._links.self.href
: cal._links.self.href;
const link = cal._links.self.href;
? cal["calendarserver:source"]._links.self?.href
: cal._links.self?.href;
if (!source) {
throw new Error("No source for calendar");
}
const link = cal._links.self?.href ?? "";
const id = source.replace("/calendars/", "").replace(".json", "");
const visibility = getCalendarVisibility(cal["acl"]);
const ownerData: any = await getUserDetails(id.split("/")[0]);
const visibility = getCalendarVisibility(cal["acl"] ?? []);
const ownerData = await getUserDetails(id.split("/")[0]);
importedCalendars[id] = {
id,
@@ -66,10 +69,11 @@ export const getTempCalendarsListAsync = createAsyncThunk<
}
return importedCalendars;
} catch (err: any) {
} catch (err) {
const error = err as { response?: { status?: number } };
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
status: error.response?.status,
});
}
});
@@ -22,10 +22,11 @@ export const importEventFromFileAsync = createAsyncThunk<
});
}
await importEventFromFile(id, calLink);
} catch (err: any) {
} catch (err) {
const error = err as { response?: { status?: number } };
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
status: error.response?.status,
});
}
});
@@ -1,15 +1,9 @@
import { moveEvent } from "@/features/Events/EventApi";
import { CalendarEvent } from "@/features/Events/EventsTypes";
import { parseCalendarEvent } from "@/features/Events/eventUtils";
import {
computeWeekRange,
formatDateToYYYYMMDDTHHMMSS,
} from "@/utils/dateUtils";
import { formatReduxError } from "@/utils/errorUtils";
import { toRejectedError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { getCalendar } from "../CalendarApi";
import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes";
import { RejectedError } from "../types/RejectedError";
export const moveEventAsync = createAsyncThunk<
{ calId: string },
@@ -24,11 +18,8 @@ export const moveEventAsync = createAsyncThunk<
return {
calId: cal.id,
};
} catch (err: any) {
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
});
} catch (err) {
return rejectWithValue(toRejectedError(err));
}
}
);
@@ -1,4 +1,4 @@
import { formatReduxError } from "@/utils/errorUtils";
import { toRejectedError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { updateAclCalendar } from "../CalendarApi";
import { RejectedError } from "../types/RejectedError";
@@ -25,11 +25,8 @@ export const patchACLCalendarAsync = createAsyncThunk<
calLink,
request,
};
} catch (err: any) {
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
});
} catch (err) {
return rejectWithValue(toRejectedError(err));
}
}
);
@@ -1,4 +1,4 @@
import { formatReduxError } from "@/utils/errorUtils";
import { toRejectedError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { proppatchCalendar } from "../CalendarApi";
import { RejectedError } from "../types/RejectedError";
@@ -25,11 +25,8 @@ export const patchCalendarAsync = createAsyncThunk<
calLink,
patch,
};
} catch (err: any) {
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
});
} catch (err) {
return rejectWithValue(toRejectedError(err));
}
}
);
@@ -1,9 +1,9 @@
import { putEvent } from "@/features/Events/EventApi";
import { CalendarEvent } from "@/features/Events/EventsTypes";
import { formatReduxError } from "@/utils/errorUtils";
import { toRejectedError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes";
import { RejectedError } from "../types/RejectedError";
export const putEventAsync = createAsyncThunk<
{ calId: string; calType?: "temp" },
@@ -22,11 +22,8 @@ export const putEventAsync = createAsyncThunk<
calId: cal.id,
calType,
};
} catch (err: any) {
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
});
} catch (err) {
return rejectWithValue(toRejectedError(err));
}
}
);
@@ -1,10 +1,10 @@
import { CalendarEvent } from "@/features/Events/EventsTypes";
import { formatReduxError } from "@/utils/errorUtils";
import { toRejectedError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import pMap from "p-map";
import { fetchSyncTokenChanges } from "../api/fetchSyncTokenChanges";
import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes";
import { RejectedError } from "../types/RejectedError";
import { expandEventFunction } from "../utils/expandEventFunction";
import { processSyncUpdates } from "../utils/processSyncTokenUpdates";
@@ -70,11 +70,8 @@ export const refreshCalendarWithSyncToken = createAsyncThunk<
syncToken: newSyncToken,
syncStatus: newSyncToken ? "SUCCESS" : "NO_NEW_SYNC_TOKEN",
};
} catch (err: any) {
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
});
} catch (err) {
return rejectWithValue(toRejectedError(err));
}
}
);
@@ -1,4 +1,4 @@
import { formatReduxError } from "@/utils/errorUtils";
import { toRejectedError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { removeCalendar } from "../CalendarApi";
import { RejectedError } from "../types/RejectedError";
@@ -20,11 +20,8 @@ export const removeCalendarAsync = createAsyncThunk<
return {
calId,
};
} catch (err: any) {
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
});
} catch (err) {
return rejectWithValue(toRejectedError(err));
}
}
);
@@ -1,6 +1,6 @@
import { putEventWithOverrides } from "@/features/Events/EventApi";
import { CalendarEvent } from "@/features/Events/EventsTypes";
import { formatReduxError } from "@/utils/errorUtils";
import { toRejectedError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes";
@@ -15,11 +15,8 @@ export const updateEventInstanceAsync = createAsyncThunk<
try {
await putEventWithOverrides(event, cal.ownerEmails?.[0]);
return { calId: cal.id, event };
} catch (err: any) {
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
});
} catch (err) {
return rejectWithValue(toRejectedError(err));
}
}
);
@@ -1,9 +1,9 @@
import { updateSeries } from "@/features/Events/EventApi";
import { CalendarEvent } from "@/features/Events/EventsTypes";
import { formatReduxError } from "@/utils/errorUtils";
import { toRejectedError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes";
import { RejectedError } from "../types/RejectedError";
export const updateSeriesAsync = createAsyncThunk<
void,
@@ -14,11 +14,8 @@ export const updateSeriesAsync = createAsyncThunk<
async ({ cal, event, removeOverrides = true }, { rejectWithValue }) => {
try {
await updateSeries(event, cal.ownerEmails?.[0] ?? "", removeOverrides);
} catch (err: any) {
return rejectWithValue({
message: formatReduxError(err),
status: err.response?.status,
});
} catch (err) {
return rejectWithValue(toRejectedError(err));
}
}
);
@@ -0,0 +1,7 @@
// Type for async thunk results
export interface AsyncThunkResult {
type: string;
error?: { message?: string };
payload?: { message?: string };
unwrap?: () => Promise<unknown>;
}
@@ -0,0 +1,90 @@
import { User } from "@/components/Attendees/PeopleSearch";
import { CalDavLink } from "../api/types";
// Access control entry
export interface AclEntry {
privilege: string;
principal: string;
protected: boolean;
}
// VObject property value can be various types
export type VObjectValue =
| string
| number
| boolean
| Date
| Record<string, unknown>
| null
| RepetitionRule
| undefined;
// VObject property tuple
export type VObjectProperty = [
string,
Record<string, unknown>,
string | Array<unknown>,
VObjectValue,
];
export type VCalComponent = [
string,
VObjectProperty[],
VCalComponent[],
...unknown[],
];
export interface Organizer {
cn?: string;
email: string;
}
// The `dav:item` object in _embedded
export interface CalendarItem {
_links: CalDavLink;
etag: string;
status: number;
data: [
"vcalendar",
Array<
VObjectProperty | [string, VObjectProperty[], unknown[]] // vevent array
>,
];
}
// Main calendar data
export interface CalendarData {
_links: CalDavLink;
"caldav:description"?: string;
"dav:name"?: string;
"apple:color"?: string;
id?: string;
acl?: AclEntry[];
invite?: unknown;
_embedded: {
"sync-token": string;
"dav:item": CalendarItem[];
};
"calendarserver:source"?: { _links: CalDavLink };
"calendarserver:delegatedsource"?: string;
}
export interface CalendarList {
_embedded: { "dav:calendar": CalendarData[] };
}
// Calendar input for forms or UI
export interface CalendarInput {
cal: CalendarData;
color: Record<string, string>;
owner?: User;
}
// Vevent repetition rule
export interface RepetitionRule {
freq: string;
interval?: number;
count?: number;
until?: string;
byday?: string | string[];
}
@@ -20,7 +20,7 @@ export function expandEventFunction(
});
return events;
} catch (err) {
console.error("Failed to fetch event", eventUrl);
console.error("Failed to fetch event", eventUrl, err);
return undefined;
}
};
@@ -2,6 +2,7 @@ import { defaultColors } from "@/components/Calendar/utils/calendarColorsUtils";
import { CalendarEvent } from "@/features/Events/EventsTypes";
import { parseCalendarEvent } from "@/features/Events/eventUtils";
import { CalDavItem } from "../api/types";
import { VCalComponent } from "../types/CalendarData";
export function extractCalendarEvents(
item: CalDavItem,
@@ -50,7 +51,7 @@ export function extractCalendarEvents(
.filter(Boolean) as CalendarEvent[];
}
function extractValarm(vevent: any[]) {
function extractValarm(vevent: VCalComponent[]) {
const subComponents = vevent[2];
if (!Array.isArray(subComponents)) {
return undefined;
@@ -1,19 +1,23 @@
import { getCalendarVisibility } from "@/components/Calendar/utils/calendarUtils";
import { CalendarData } from "../types/CalendarData";
export function normalizeCalendar(rawCalendar: Record<string, any>) {
export function normalizeCalendar(rawCalendar: CalendarData) {
const description = rawCalendar["caldav:description"];
let delegated = false;
let source = rawCalendar["calendarserver:source"]
? rawCalendar["calendarserver:source"]._links.self.href
: rawCalendar._links.self.href;
const link = rawCalendar._links.self.href;
? rawCalendar["calendarserver:source"]._links.self?.href
: rawCalendar._links.self?.href;
const link = rawCalendar._links.self?.href;
if (rawCalendar["calendarserver:delegatedsource"]) {
source = rawCalendar["calendarserver:delegatedsource"];
delegated = true;
}
if (!source) {
throw new Error("No source for calendar");
}
const id = source.replace("/calendars/", "").replace(".json", "");
const ownerId = id.split("/")[0];
const visibility = getCalendarVisibility(rawCalendar["acl"]);
const visibility = getCalendarVisibility(rawCalendar["acl"] ?? []);
return {
cal: rawCalendar,
description,
@@ -30,6 +30,6 @@ export function processSyncUpdates(
}
function extractFileNameFromHref(href: string): string {
const fileNameMatch = href.match(/\/([^\/]+)\.ics$/); // CalDAV href are like /calendars/userID/CalendarID/EventId.ics
const fileNameMatch = href.match(/\/([^/]+)\.ics$/); // CalDAV href are like /calendars/userID/CalendarID/EventId.ics
return fileNameMatch ? fileNameMatch[1] : href;
}