* [#352] update exceptions on all update without reseting them * [#352] added tests
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { updateSeries } from "@/features/Events/api/updateSeries";
|
||||||
import {
|
import {
|
||||||
deleteEvent,
|
deleteEvent,
|
||||||
importEventFromFile,
|
importEventFromFile,
|
||||||
@@ -271,4 +272,511 @@ describe("eventApi", () => {
|
|||||||
expect(body.participants).toBeUndefined();
|
expect(body.participants).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("updateSeries", () => {
|
||||||
|
const recurringEventICS = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//Test//EN
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
DTSTART:20240201T100000Z
|
||||||
|
DTEND:20240201T110000Z
|
||||||
|
RRULE:FREQ=DAILY
|
||||||
|
SUMMARY:Old title
|
||||||
|
SEQUENCE:1
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
RECURRENCE-ID:20240202T100000Z
|
||||||
|
DTSTART:20240202T100000Z
|
||||||
|
DTEND:20240202T110000Z
|
||||||
|
SUMMARY:Old title
|
||||||
|
SEQUENCE:1
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
`;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
(api.get as jest.Mock).mockResolvedValue({
|
||||||
|
text: jest.fn().mockResolvedValue(recurringEventICS),
|
||||||
|
});
|
||||||
|
|
||||||
|
(api as jest.Mock).mockResolvedValue({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("propagates summary changes to overrides", async () => {
|
||||||
|
await updateSeries(
|
||||||
|
{ ...mockEvent, title: "New title" } as any,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
||||||
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
||||||
|
|
||||||
|
const override = vevents.find(([, props]: any) =>
|
||||||
|
props.some(([k]: any) => k === "recurrence-id")
|
||||||
|
);
|
||||||
|
|
||||||
|
const summary = override[1].find(([k]: any) => k === "summary");
|
||||||
|
expect(summary[3]).toBe("New title");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not remove recurrence-id from overrides", async () => {
|
||||||
|
await updateSeries(mockEvent as any, undefined, false);
|
||||||
|
|
||||||
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
||||||
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
||||||
|
|
||||||
|
const override = vevents.find(([, props]: any) =>
|
||||||
|
props.some(([k]: any) => k === "recurrence-id")
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(override).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("increments SEQUENCE on overrides when metadata changes", async () => {
|
||||||
|
await updateSeries(
|
||||||
|
{ ...mockEvent, title: "Changed title" } as any,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
||||||
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
||||||
|
|
||||||
|
const override = vevents.find(([, props]: any) =>
|
||||||
|
props.some(([k]: any) => k === "recurrence-id")
|
||||||
|
);
|
||||||
|
|
||||||
|
const sequence = override[1].find(([k]: any) => k === "sequence");
|
||||||
|
expect(sequence[3]).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("propagates description, location, class, and transp changes", async () => {
|
||||||
|
const icsWithMultipleFields = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
DTSTART:20240201T100000Z
|
||||||
|
DTEND:20240201T110000Z
|
||||||
|
RRULE:FREQ=DAILY
|
||||||
|
SUMMARY:Title
|
||||||
|
DESCRIPTION:Old description
|
||||||
|
LOCATION:Old location
|
||||||
|
CLASS:PUBLIC
|
||||||
|
TRANSP:OPAQUE
|
||||||
|
SEQUENCE:1
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
RECURRENCE-ID:20240202T100000Z
|
||||||
|
DTSTART:20240202T100000Z
|
||||||
|
DTEND:20240202T110000Z
|
||||||
|
SUMMARY:Title
|
||||||
|
DESCRIPTION:Old description
|
||||||
|
LOCATION:Old location
|
||||||
|
CLASS:PUBLIC
|
||||||
|
TRANSP:OPAQUE
|
||||||
|
SEQUENCE:1
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
`;
|
||||||
|
|
||||||
|
(api.get as jest.Mock).mockResolvedValueOnce({
|
||||||
|
text: jest.fn().mockResolvedValue(icsWithMultipleFields),
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateSeries(
|
||||||
|
{
|
||||||
|
...mockEvent,
|
||||||
|
description: "New description",
|
||||||
|
location: "New location",
|
||||||
|
class: "PRIVATE",
|
||||||
|
transp: "TRANSPARENT",
|
||||||
|
} as any,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
||||||
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
||||||
|
const override = vevents.find(([, props]: any) =>
|
||||||
|
props.some(([k]: any) => k === "recurrence-id")
|
||||||
|
);
|
||||||
|
|
||||||
|
const description = override[1].find(([k]: any) => k === "description");
|
||||||
|
const location = override[1].find(([k]: any) => k === "location");
|
||||||
|
const classField = override[1].find(([k]: any) => k === "class");
|
||||||
|
const transp = override[1].find(([k]: any) => k === "transp");
|
||||||
|
|
||||||
|
expect(description[3]).toBe("New description");
|
||||||
|
expect(location[3]).toBe("New location");
|
||||||
|
expect(classField[3]).toBe("PRIVATE");
|
||||||
|
expect(transp[3]).toBe("TRANSPARENT");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("propagates organizer changes", async () => {
|
||||||
|
const icsWithOrganizer = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
DTSTART:20240201T100000Z
|
||||||
|
RRULE:FREQ=DAILY
|
||||||
|
SUMMARY:Title
|
||||||
|
ORGANIZER;CN=Alice:mailto:alice@example.com
|
||||||
|
SEQUENCE:1
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
RECURRENCE-ID:20240202T100000Z
|
||||||
|
DTSTART:20240202T100000Z
|
||||||
|
SUMMARY:Title
|
||||||
|
ORGANIZER;CN=Alice:mailto:alice@example.com
|
||||||
|
SEQUENCE:1
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
`;
|
||||||
|
|
||||||
|
(api.get as jest.Mock).mockResolvedValueOnce({
|
||||||
|
text: jest.fn().mockResolvedValue(icsWithOrganizer),
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateSeries(
|
||||||
|
{
|
||||||
|
...mockEvent,
|
||||||
|
organizer: { cn: "Bob", cal_address: "bob@example.com" },
|
||||||
|
} as any,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
||||||
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
||||||
|
const override = vevents.find(([, props]: any) =>
|
||||||
|
props.some(([k]: any) => k === "recurrence-id")
|
||||||
|
);
|
||||||
|
|
||||||
|
const organizer = override[1].find(([k]: any) => k === "organizer");
|
||||||
|
expect(organizer[3]).toBe("mailto:bob@example.com");
|
||||||
|
expect(organizer[1].cn).toBe("Bob");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("propagates attendee changes", async () => {
|
||||||
|
const icsWithAttendees = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
DTSTART:20240201T100000Z
|
||||||
|
RRULE:FREQ=DAILY
|
||||||
|
SUMMARY:Title
|
||||||
|
ATTENDEE;CN=Alice:mailto:alice@example.com
|
||||||
|
SEQUENCE:1
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
RECURRENCE-ID:20240202T100000Z
|
||||||
|
DTSTART:20240202T100000Z
|
||||||
|
SUMMARY:Title
|
||||||
|
ATTENDEE;CN=Alice:mailto:alice@example.com
|
||||||
|
SEQUENCE:1
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
`;
|
||||||
|
|
||||||
|
(api.get as jest.Mock).mockResolvedValueOnce({
|
||||||
|
text: jest.fn().mockResolvedValue(icsWithAttendees),
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateSeries(
|
||||||
|
{
|
||||||
|
...mockEvent,
|
||||||
|
attendee: [
|
||||||
|
{
|
||||||
|
cn: "Bob",
|
||||||
|
cal_address: "bob@example.com",
|
||||||
|
partstat: "NEEDS-ACTION",
|
||||||
|
cutype: "INDIVIDUAL",
|
||||||
|
role: "REQ-PARTICIPANT",
|
||||||
|
rsvp: "TRUE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cn: "Charlie",
|
||||||
|
cal_address: "charlie@example.com",
|
||||||
|
partstat: "NEEDS-ACTION",
|
||||||
|
cutype: "INDIVIDUAL",
|
||||||
|
role: "REQ-PARTICIPANT",
|
||||||
|
rsvp: "TRUE",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as any,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
||||||
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
||||||
|
const override = vevents.find(([, props]: any) =>
|
||||||
|
props.some(([k]: any) => k === "recurrence-id")
|
||||||
|
);
|
||||||
|
|
||||||
|
const attendees = override[1].filter(([k]: any) => k === "attendee");
|
||||||
|
expect(attendees).toHaveLength(2);
|
||||||
|
expect(attendees[0][3]).toBe("mailto:bob@example.com");
|
||||||
|
expect(attendees[1][3]).toBe("mailto:charlie@example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("propagates x-openpaas-videoconference changes", async () => {
|
||||||
|
const icsWithVideo = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
DTSTART:20240201T100000Z
|
||||||
|
RRULE:FREQ=DAILY
|
||||||
|
SUMMARY:Title
|
||||||
|
X-OPENPAAS-VIDEOCONFERENCE:https://meet.old.com
|
||||||
|
SEQUENCE:1
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
RECURRENCE-ID:20240202T100000Z
|
||||||
|
DTSTART:20240202T100000Z
|
||||||
|
SUMMARY:Title
|
||||||
|
X-OPENPAAS-VIDEOCONFERENCE:https://meet.old.com
|
||||||
|
SEQUENCE:1
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
`;
|
||||||
|
|
||||||
|
(api.get as jest.Mock).mockResolvedValueOnce({
|
||||||
|
text: jest.fn().mockResolvedValue(icsWithVideo),
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateSeries(
|
||||||
|
{
|
||||||
|
...mockEvent,
|
||||||
|
x_openpass_videoconference: "https://meet.new.com",
|
||||||
|
} as any,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
||||||
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
||||||
|
const override = vevents.find(([, props]: any) =>
|
||||||
|
props.some(([k]: any) => k === "recurrence-id")
|
||||||
|
);
|
||||||
|
|
||||||
|
const videoconf = override[1].find(
|
||||||
|
([k]: any) => k === "x-openpaas-videoconference"
|
||||||
|
);
|
||||||
|
expect(videoconf[3]).toBe("https://meet.new.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works with multiple override instances", async () => {
|
||||||
|
const icsMultipleOverrides = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
DTSTART:20240201T100000Z
|
||||||
|
DTEND:20240201T110000Z
|
||||||
|
RRULE:FREQ=DAILY
|
||||||
|
SUMMARY:Old
|
||||||
|
SEQUENCE:1
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
RECURRENCE-ID:20240202T100000Z
|
||||||
|
DTSTART:20240202T100000Z
|
||||||
|
DTEND:20240202T110000Z
|
||||||
|
SUMMARY:Old
|
||||||
|
SEQUENCE:1
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
RECURRENCE-ID:20240203T100000Z
|
||||||
|
DTSTART:20240203T100000Z
|
||||||
|
DTEND:20240203T110000Z
|
||||||
|
SUMMARY:Old
|
||||||
|
SEQUENCE:1
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
`;
|
||||||
|
|
||||||
|
(api.get as jest.Mock).mockResolvedValueOnce({
|
||||||
|
text: jest.fn().mockResolvedValue(icsMultipleOverrides),
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateSeries(
|
||||||
|
{ ...mockEvent, title: "New title" } as any,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
||||||
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
||||||
|
const overrides = vevents.filter(([, props]: any) =>
|
||||||
|
props.some(([k]: any) => k === "recurrence-id")
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(overrides).toHaveLength(2);
|
||||||
|
overrides.forEach((override) => {
|
||||||
|
const summary = override[1].find(([k]: any) => k === "summary");
|
||||||
|
const sequence = override[1].find(([k]: any) => k === "sequence");
|
||||||
|
expect(summary[3]).toBe("New title");
|
||||||
|
expect(sequence[3]).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds sequence field when missing and metadata changes", async () => {
|
||||||
|
const icsNoSequence = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
DTSTART:20240201T100000Z
|
||||||
|
DTEND:20240201T110000Z
|
||||||
|
RRULE:FREQ=DAILY
|
||||||
|
SUMMARY:Old
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
RECURRENCE-ID:20240202T100000Z
|
||||||
|
DTSTART:20240202T100000Z
|
||||||
|
DTEND:20240202T110000Z
|
||||||
|
SUMMARY:Old
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
`;
|
||||||
|
|
||||||
|
(api.get as jest.Mock).mockResolvedValueOnce({
|
||||||
|
text: jest.fn().mockResolvedValue(icsNoSequence),
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateSeries(
|
||||||
|
{ ...mockEvent, title: "New title" } as any,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
||||||
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
||||||
|
const override = vevents.find(([, props]: any) =>
|
||||||
|
props.some(([k]: any) => k === "recurrence-id")
|
||||||
|
);
|
||||||
|
|
||||||
|
const sequence = override[1].find(([k]: any) => k === "sequence");
|
||||||
|
expect(sequence[3]).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves override-specific fields when propagating metadata", async () => {
|
||||||
|
const icsCustomFields = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
DTSTART:20240201T100000Z
|
||||||
|
DTEND:20240201T110000Z
|
||||||
|
RRULE:FREQ=DAILY
|
||||||
|
SUMMARY:Old
|
||||||
|
SEQUENCE:1
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
RECURRENCE-ID:20240202T100000Z
|
||||||
|
DTSTART:20240202T140000Z
|
||||||
|
DTEND:20240202T150000Z
|
||||||
|
SUMMARY:Old
|
||||||
|
DESCRIPTION:custom-value
|
||||||
|
SEQUENCE:1
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
`;
|
||||||
|
|
||||||
|
(api.get as jest.Mock).mockResolvedValueOnce({
|
||||||
|
text: jest.fn().mockResolvedValue(icsCustomFields),
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateSeries(
|
||||||
|
{ ...mockEvent, title: "New title" } as any,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
||||||
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
||||||
|
const override = vevents.find(([, props]: any) =>
|
||||||
|
props.some(([k]: any) => k === "recurrence-id")
|
||||||
|
);
|
||||||
|
|
||||||
|
const summary = override[1].find(([k]: any) => k === "summary");
|
||||||
|
const dtstart = override[1].find(([k]: any) => k === "dtstart");
|
||||||
|
const dtend = override[1].find(([k]: any) => k === "dtend");
|
||||||
|
const description = override[1].find(([k]: any) => k === "description");
|
||||||
|
const recurrenceId = override[1].find(
|
||||||
|
([k]: any) => k === "recurrence-id"
|
||||||
|
);
|
||||||
|
// Summary should be updated
|
||||||
|
expect(summary[3]).toBe("New title");
|
||||||
|
// Override-specific time should be preserved
|
||||||
|
expect(dtstart[3]).toBe("2024-02-02T14:00:00Z");
|
||||||
|
expect(dtend[3]).toBe("2024-02-02T15:00:00Z");
|
||||||
|
// Custom field should be preserved
|
||||||
|
expect(description[3]).toBe("custom-value");
|
||||||
|
// Recurrence-id should be preserved
|
||||||
|
expect(recurrenceId[3]).toBe("2024-02-02T10:00:00Z");
|
||||||
|
});
|
||||||
|
|
||||||
|
const recurringWithAlarmICS = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
DTSTART:20240201T100000Z
|
||||||
|
RRULE:FREQ=DAILY
|
||||||
|
SUMMARY:Old
|
||||||
|
BEGIN:VALARM
|
||||||
|
ACTION:DISPLAY
|
||||||
|
TRIGGER:-PT15M
|
||||||
|
END:VALARM
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:event1
|
||||||
|
RECURRENCE-ID:20240202T100000Z
|
||||||
|
DTSTART:20240202T100000Z
|
||||||
|
SUMMARY:Old
|
||||||
|
BEGIN:VALARM
|
||||||
|
ACTION:DISPLAY
|
||||||
|
TRIGGER:-PT15M
|
||||||
|
END:VALARM
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
`;
|
||||||
|
|
||||||
|
it("replaces VALARM on overrides when alarm changes", async () => {
|
||||||
|
(api.get as jest.Mock).mockResolvedValueOnce({
|
||||||
|
text: jest.fn().mockResolvedValue(recurringWithAlarmICS),
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateSeries(
|
||||||
|
{ ...mockEvent, alarm: { action: "EMAIL", trigger: "-PT30M" } } as any,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
||||||
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
||||||
|
|
||||||
|
const override = vevents.find(([, props]: any) =>
|
||||||
|
props.some(([k]: any) => k === "recurrence-id")
|
||||||
|
);
|
||||||
|
|
||||||
|
const alarms = override[2].filter(([n]: any) => n === "valarm");
|
||||||
|
|
||||||
|
expect(alarms).toHaveLength(1);
|
||||||
|
const action = alarms[0][1].find(([k]: any) => k === "action");
|
||||||
|
const trigger = alarms[0][1].find(([k]: any) => k === "trigger");
|
||||||
|
expect(action[3]).toBe("EMAIL");
|
||||||
|
expect(trigger[3]).toBe("-PT30M");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { renderWithProviders } from "../../utils/Renderwithproviders";
|
|||||||
|
|
||||||
jest.mock("@/features/Events/EventApi");
|
jest.mock("@/features/Events/EventApi");
|
||||||
jest.mock("@/features/Calendars/CalendarApi");
|
jest.mock("@/features/Calendars/CalendarApi");
|
||||||
|
jest.mock("@/features/Events/api/updateSeries");
|
||||||
|
|
||||||
describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
|
describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
|
||||||
const mockOnClose = jest.fn();
|
const mockOnClose = jest.fn();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { updateSeries } from "@/features/Events/EventApi";
|
import { updateSeries } from "@/features/Events/api/updateSeries";
|
||||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||||
import { toRejectedError } from "@/utils/errorUtils";
|
import { toRejectedError } from "@/utils/errorUtils";
|
||||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||||
|
|||||||
@@ -22,17 +22,12 @@ export type VObjectValue =
|
|||||||
// VObject property tuple
|
// VObject property tuple
|
||||||
export type VObjectProperty = [
|
export type VObjectProperty = [
|
||||||
string,
|
string,
|
||||||
Record<string, unknown>,
|
Record<string, VObjectValue | VObjectValue[]>,
|
||||||
string | Array<unknown>,
|
string,
|
||||||
VObjectValue,
|
VObjectValue,
|
||||||
];
|
];
|
||||||
|
|
||||||
export type VCalComponent = [
|
export type VCalComponent = [string, VObjectProperty[], VCalComponent[]?];
|
||||||
string,
|
|
||||||
VObjectProperty[],
|
|
||||||
VCalComponent[],
|
|
||||||
...unknown[],
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface Organizer {
|
export interface Organizer {
|
||||||
cn?: string;
|
cn?: string;
|
||||||
@@ -44,12 +39,7 @@ export interface CalendarItem {
|
|||||||
_links: CalDavLink;
|
_links: CalDavLink;
|
||||||
etag: string;
|
etag: string;
|
||||||
status: number;
|
status: number;
|
||||||
data: [
|
data: VCalComponent;
|
||||||
"vcalendar",
|
|
||||||
Array<
|
|
||||||
VObjectProperty | [string, VObjectProperty[], unknown[]] // vevent array
|
|
||||||
>,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main calendar data
|
// Main calendar data
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { api } from "@/utils/apiUtils";
|
import { api } from "@/utils/apiUtils";
|
||||||
import { resolveTimezoneId, convertEventDateTimeToISO } from "@/utils/timezone";
|
import { convertEventDateTimeToISO, resolveTimezoneId } from "@/utils/timezone";
|
||||||
import { TIMEZONES } from "@/utils/timezone-data";
|
import { TIMEZONES } from "@/utils/timezone-data";
|
||||||
import ICAL from "ical.js";
|
import ICAL from "ical.js";
|
||||||
import { CalDavItem } from "../Calendars/api/types";
|
import { CalDavItem } from "../Calendars/api/types";
|
||||||
|
import {
|
||||||
|
VCalComponent,
|
||||||
|
VObjectProperty,
|
||||||
|
VObjectValue,
|
||||||
|
} from "../Calendars/types/CalendarData";
|
||||||
import { SearchEventsResponse } from "../Search/types/SearchEventsResponse";
|
import { SearchEventsResponse } from "../Search/types/SearchEventsResponse";
|
||||||
import { CalendarEvent } from "./EventsTypes";
|
import { CalendarEvent } from "./EventsTypes";
|
||||||
import {
|
import {
|
||||||
@@ -12,23 +17,6 @@ import {
|
|||||||
parseCalendarEvent,
|
parseCalendarEvent,
|
||||||
} from "./eventUtils";
|
} from "./eventUtils";
|
||||||
|
|
||||||
type JCalValue = string | number | boolean | null;
|
|
||||||
|
|
||||||
type JCalParams = Record<string, JCalValue | JCalValue[]>;
|
|
||||||
|
|
||||||
type JCalProperty = [
|
|
||||||
name: string,
|
|
||||||
params: JCalParams,
|
|
||||||
type: string,
|
|
||||||
value: JCalValue,
|
|
||||||
];
|
|
||||||
|
|
||||||
type JCalComponent = [
|
|
||||||
name: string,
|
|
||||||
properties: JCalProperty[],
|
|
||||||
components?: JCalComponent[],
|
|
||||||
];
|
|
||||||
|
|
||||||
export async function reportEvent(
|
export async function reportEvent(
|
||||||
event: CalendarEvent,
|
event: CalendarEvent,
|
||||||
match: { start: string; end: string }
|
match: { start: string; end: string }
|
||||||
@@ -61,7 +49,7 @@ export async function getEvent(event: CalendarEvent, isMaster?: boolean) {
|
|||||||
let targetVevent;
|
let targetVevent;
|
||||||
if (isMaster) {
|
if (isMaster) {
|
||||||
targetVevent = vevents.find(
|
targetVevent = vevents.find(
|
||||||
([, props]: JCalComponent) =>
|
([, props]: VCalComponent) =>
|
||||||
!props.find(([k]) => k.toLowerCase() === "recurrence-id")
|
!props.find(([k]) => k.toLowerCase() === "recurrence-id")
|
||||||
);
|
);
|
||||||
if (!targetVevent) {
|
if (!targetVevent) {
|
||||||
@@ -222,7 +210,7 @@ export const deleteEventInstance = async (event: CalendarEvent) => {
|
|||||||
|
|
||||||
// Find the master VEVENT
|
// Find the master VEVENT
|
||||||
const masterIndex = vevents.findIndex(
|
const masterIndex = vevents.findIndex(
|
||||||
([, props]: JCalComponent) =>
|
([, props]: VCalComponent) =>
|
||||||
!props.find(([k]) => k.toLowerCase() === "recurrence-id")
|
!props.find(([k]) => k.toLowerCase() === "recurrence-id")
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -235,9 +223,9 @@ export const deleteEventInstance = async (event: CalendarEvent) => {
|
|||||||
const masterProps = vevents[masterIndex][1];
|
const masterProps = vevents[masterIndex][1];
|
||||||
|
|
||||||
// Check if this date is already in EXDATE (avoid duplicates)
|
// Check if this date is already in EXDATE (avoid duplicates)
|
||||||
const normalizeRecurrenceId = (id: JCalValue) =>
|
const normalizeRecurrenceId = (id: VObjectValue) =>
|
||||||
String(id ?? "").replace(/Z$/, "");
|
String(id ?? "").replace(/Z$/, "");
|
||||||
const isDuplicate = masterProps.some((prop: JCalProperty) => {
|
const isDuplicate = masterProps.some((prop: VObjectProperty) => {
|
||||||
if (prop[0].toLowerCase() === "exdate" && prop[3]) {
|
if (prop[0].toLowerCase() === "exdate" && prop[3]) {
|
||||||
return (
|
return (
|
||||||
normalizeRecurrenceId(prop[3]) === normalizeRecurrenceId(exdateValue)
|
normalizeRecurrenceId(prop[3]) === normalizeRecurrenceId(exdateValue)
|
||||||
@@ -256,7 +244,7 @@ export const deleteEventInstance = async (event: CalendarEvent) => {
|
|||||||
vevents[masterIndex][1] = masterProps;
|
vevents[masterIndex][1] = masterProps;
|
||||||
|
|
||||||
// Remove the override instance if it exists (in case it was an override being deleted)
|
// Remove the override instance if it exists (in case it was an override being deleted)
|
||||||
const filteredVevents = vevents.filter(([, props]: JCalComponent) => {
|
const filteredVevents = vevents.filter(([, props]: VCalComponent) => {
|
||||||
const recurrenceIdProp = props.find(
|
const recurrenceIdProp = props.find(
|
||||||
([k]) => k.toLowerCase() === "recurrence-id"
|
([k]) => k.toLowerCase() === "recurrence-id"
|
||||||
);
|
);
|
||||||
@@ -294,12 +282,12 @@ export const updateSeriesPartstat = async (
|
|||||||
const vevents = await getAllRecurrentEvent(event);
|
const vevents = await getAllRecurrentEvent(event);
|
||||||
|
|
||||||
// Update PARTSTAT in ALL VEVENTs (master + exceptions)
|
// Update PARTSTAT in ALL VEVENTs (master + exceptions)
|
||||||
const updatedVevents = vevents.map((vevent: JCalComponent) => {
|
const updatedVevents = vevents.map((vevent: VCalComponent) => {
|
||||||
const properties = vevent[1];
|
const properties = vevent[1];
|
||||||
const updatedProperties = properties.map((prop: JCalProperty) => {
|
const updatedProperties = properties.map((prop: VObjectProperty) => {
|
||||||
// Find ATTENDEE properties
|
// Find ATTENDEE properties
|
||||||
if (prop[0] === "attendee") {
|
if (prop[0] === "attendee") {
|
||||||
const calAddress = prop[3];
|
const calAddress = prop[3] as string;
|
||||||
// Check if this is the target attendee
|
// Check if this is the target attendee
|
||||||
if (calAddress.toLowerCase().includes(attendeeEmail.toLowerCase())) {
|
if (calAddress.toLowerCase().includes(attendeeEmail.toLowerCase())) {
|
||||||
// Update PARTSTAT parameter
|
// Update PARTSTAT parameter
|
||||||
@@ -330,57 +318,7 @@ export const updateSeriesPartstat = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateSeries = async (
|
export async function getAllRecurrentEvent(event: CalendarEvent) {
|
||||||
event: CalendarEvent,
|
|
||||||
calOwnerEmail?: string,
|
|
||||||
removeOverrides: boolean = true
|
|
||||||
) => {
|
|
||||||
const vevents = await getAllRecurrentEvent(event);
|
|
||||||
const masterIndex = vevents.findIndex(
|
|
||||||
([, props]: JCalComponent) =>
|
|
||||||
!props.find(([k]) => k.toLowerCase() === "recurrence-id")
|
|
||||||
);
|
|
||||||
if (masterIndex === -1) {
|
|
||||||
throw new Error("No master VEVENT found for this series");
|
|
||||||
}
|
|
||||||
const rrule = vevents[masterIndex][1].find(([k]: string[]) => k === "rrule");
|
|
||||||
|
|
||||||
const tzid = event.timezone;
|
|
||||||
|
|
||||||
const updatedMaster = makeVevent(event, tzid, calOwnerEmail, true);
|
|
||||||
const newRrule = updatedMaster[1].find(([k]: string[]) => k === "rrule");
|
|
||||||
if (!newRrule) {
|
|
||||||
updatedMaster[1].push(rrule);
|
|
||||||
}
|
|
||||||
|
|
||||||
const timezoneData = TIMEZONES.zones[event.timezone];
|
|
||||||
const vtimezone = makeTimezone(timezoneData, event);
|
|
||||||
|
|
||||||
let finalVevents;
|
|
||||||
if (removeOverrides) {
|
|
||||||
// When date/time/timezone/repeat rules changed, remove all override instances
|
|
||||||
finalVevents = [updatedMaster];
|
|
||||||
} else {
|
|
||||||
// When only properties changed, keep override instances
|
|
||||||
vevents[masterIndex] = updatedMaster;
|
|
||||||
finalVevents = vevents;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newJCal = [
|
|
||||||
"vcalendar",
|
|
||||||
[],
|
|
||||||
[...finalVevents, vtimezone.component.jCal],
|
|
||||||
];
|
|
||||||
return api(`dav${event.URL}`, {
|
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify(newJCal),
|
|
||||||
headers: {
|
|
||||||
"content-type": "text/calendar; charset=utf-8",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getAllRecurrentEvent(event: CalendarEvent) {
|
|
||||||
const response = await api.get(`dav${event.URL}`);
|
const response = await api.get(`dav${event.URL}`);
|
||||||
const eventData = await response.text();
|
const eventData = await response.text();
|
||||||
const jcal = ICAL.parse(eventData);
|
const jcal = ICAL.parse(eventData);
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
import { api } from "@/utils/apiUtils";
|
||||||
|
import { TIMEZONES } from "@/utils/timezone-data";
|
||||||
|
import {
|
||||||
|
VCalComponent,
|
||||||
|
VObjectProperty,
|
||||||
|
} from "../../Calendars/types/CalendarData";
|
||||||
|
import { getAllRecurrentEvent } from "../EventApi";
|
||||||
|
import { CalendarEvent } from "../EventsTypes";
|
||||||
|
import { makeTimezone, makeVevent } from "../eventUtils";
|
||||||
|
|
||||||
|
const METADATA_FIELDS = [
|
||||||
|
"summary",
|
||||||
|
"description",
|
||||||
|
"location",
|
||||||
|
"class",
|
||||||
|
"transp",
|
||||||
|
"attendee",
|
||||||
|
"organizer",
|
||||||
|
"x-openpaas-videoconference",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// Helper function to get field values from props
|
||||||
|
const getFieldValues = (props: VObjectProperty[], fieldName: string) => {
|
||||||
|
return props.filter(([k]) => k.toLowerCase() === fieldName.toLowerCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to find a single field value from props
|
||||||
|
const findFieldValue = (props: VObjectProperty[], fieldName: string) => {
|
||||||
|
return props.find(([k]) => k.toLowerCase() === fieldName.toLowerCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to serialize for comparison
|
||||||
|
const serialize = (values: VObjectProperty[] | VCalComponent[]) => {
|
||||||
|
return JSON.stringify(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to filter components by name
|
||||||
|
const filterComponentsByName = (components: VCalComponent[], name: string) => {
|
||||||
|
return components.filter(
|
||||||
|
([componentName]) => componentName.toLowerCase() === name.toLowerCase()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Detect which metadata fields changed between old and new master
|
||||||
|
const detectChangedMetadataFields = (
|
||||||
|
oldMasterProps: VObjectProperty[],
|
||||||
|
newMasterProps: VObjectProperty[]
|
||||||
|
) => {
|
||||||
|
const changedFields = new Map<string, VObjectProperty[]>();
|
||||||
|
|
||||||
|
METADATA_FIELDS.forEach((fieldName) => {
|
||||||
|
const oldValues = getFieldValues(oldMasterProps, fieldName);
|
||||||
|
const newValues = getFieldValues(newMasterProps, fieldName);
|
||||||
|
|
||||||
|
if (serialize(oldValues) !== serialize(newValues)) {
|
||||||
|
changedFields.set(fieldName.toLowerCase(), newValues);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return changedFields;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if VALARM component changed between old and new master
|
||||||
|
const detectValarmChanges = (
|
||||||
|
oldMaster: VCalComponent,
|
||||||
|
updatedMaster: VCalComponent
|
||||||
|
) => {
|
||||||
|
const oldMasterComponents = oldMaster[2] || [];
|
||||||
|
const newMasterComponents = updatedMaster[2] || [];
|
||||||
|
|
||||||
|
const oldValarm = filterComponentsByName(oldMasterComponents, "valarm");
|
||||||
|
const newValarm = filterComponentsByName(newMasterComponents, "valarm");
|
||||||
|
|
||||||
|
const valarmChanged = serialize(oldValarm) !== serialize(newValarm);
|
||||||
|
|
||||||
|
return { valarmChanged, newValarm };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply changed metadata fields to a vevent's properties
|
||||||
|
const applyMetadataChanges = (
|
||||||
|
props: VObjectProperty[],
|
||||||
|
changedFields: Map<string, VObjectProperty[]>
|
||||||
|
) => {
|
||||||
|
let newProps = [...props];
|
||||||
|
|
||||||
|
changedFields.forEach((newValues, fieldNameLower) => {
|
||||||
|
// Remove old values of this changed field from exception
|
||||||
|
const filteredProps = newProps.filter(
|
||||||
|
([k]) => k.toLowerCase() !== fieldNameLower
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add new values from updated master
|
||||||
|
newProps = [...filteredProps, ...newValues];
|
||||||
|
});
|
||||||
|
|
||||||
|
return newProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Increment the sequence number in properties
|
||||||
|
const incrementSequenceNumber = (props: VObjectProperty[]) => {
|
||||||
|
const newProps = [...props];
|
||||||
|
const sequenceIndex = newProps.findIndex(
|
||||||
|
([k]) => k.toLowerCase() === "sequence"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sequenceIndex !== -1) {
|
||||||
|
const currentSequence = parseInt(
|
||||||
|
(newProps[sequenceIndex][3] as string) || "0",
|
||||||
|
10
|
||||||
|
);
|
||||||
|
newProps[sequenceIndex] = [
|
||||||
|
newProps[sequenceIndex][0],
|
||||||
|
newProps[sequenceIndex][1],
|
||||||
|
newProps[sequenceIndex][2],
|
||||||
|
currentSequence + 1,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
newProps.push(["sequence", {}, "integer", 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update VALARM components if they changed
|
||||||
|
const updateValarmComponents = (
|
||||||
|
components: VCalComponent[],
|
||||||
|
newValarm: VCalComponent[]
|
||||||
|
) => {
|
||||||
|
return components
|
||||||
|
.filter(([name]) => name.toLowerCase() !== "valarm")
|
||||||
|
.concat(newValarm);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update a single vevent with metadata changes
|
||||||
|
const updateVeventWithMetadataChanges = (
|
||||||
|
vevent: VCalComponent,
|
||||||
|
index: number,
|
||||||
|
masterIndex: number,
|
||||||
|
updatedMaster: VCalComponent,
|
||||||
|
changedFields: Map<string, VObjectProperty[]>,
|
||||||
|
valarmChanged: boolean,
|
||||||
|
newValarm: VCalComponent[]
|
||||||
|
): VCalComponent => {
|
||||||
|
if (index === masterIndex) {
|
||||||
|
return updatedMaster;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [veventType, props, components = []] = vevent;
|
||||||
|
|
||||||
|
// Apply metadata changes
|
||||||
|
let newProps = applyMetadataChanges(props, changedFields);
|
||||||
|
|
||||||
|
// Increment sequence number if any changes were made
|
||||||
|
if (changedFields.size > 0 || valarmChanged) {
|
||||||
|
newProps = incrementSequenceNumber(newProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle VALARM component updates
|
||||||
|
let updatedComponents = components;
|
||||||
|
if (valarmChanged) {
|
||||||
|
updatedComponents = updateValarmComponents(components, newValarm);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [veventType, newProps, updatedComponents];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update all vevents with metadata changes while preserving overrides
|
||||||
|
const updateVeventsPreservingOverrides = (
|
||||||
|
vevents: VCalComponent[],
|
||||||
|
oldMaster: VCalComponent,
|
||||||
|
updatedMaster: VCalComponent,
|
||||||
|
masterIndex: number
|
||||||
|
) => {
|
||||||
|
const oldMasterProps = oldMaster[1];
|
||||||
|
const newMasterProps = updatedMaster[1];
|
||||||
|
|
||||||
|
// Detect which fields changed in the master
|
||||||
|
const changedFields = detectChangedMetadataFields(
|
||||||
|
oldMasterProps,
|
||||||
|
newMasterProps
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if VALARM component changed
|
||||||
|
const { valarmChanged, newValarm } = detectValarmChanges(
|
||||||
|
oldMaster,
|
||||||
|
updatedMaster
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update all vevents
|
||||||
|
return vevents.map((vevent, index) =>
|
||||||
|
updateVeventWithMetadataChanges(
|
||||||
|
vevent,
|
||||||
|
index,
|
||||||
|
masterIndex,
|
||||||
|
updatedMaster,
|
||||||
|
changedFields,
|
||||||
|
valarmChanged,
|
||||||
|
newValarm
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSeries = async (
|
||||||
|
event: CalendarEvent,
|
||||||
|
calOwnerEmail?: string,
|
||||||
|
removeOverrides: boolean = true
|
||||||
|
) => {
|
||||||
|
const vevents = (await getAllRecurrentEvent(event)) as VCalComponent[];
|
||||||
|
const masterIndex = vevents.findIndex(
|
||||||
|
([, props]) => !findFieldValue(props, "recurrence-id")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (masterIndex === -1) {
|
||||||
|
throw new Error("No master VEVENT found for this series");
|
||||||
|
}
|
||||||
|
|
||||||
|
const rrule = findFieldValue(vevents[masterIndex][1], "rrule");
|
||||||
|
const tzid = event.timezone;
|
||||||
|
const oldMaster = vevents[masterIndex];
|
||||||
|
|
||||||
|
const updatedMaster = makeVevent(
|
||||||
|
event,
|
||||||
|
tzid,
|
||||||
|
calOwnerEmail,
|
||||||
|
true
|
||||||
|
) as VCalComponent;
|
||||||
|
const newRrule = findFieldValue(updatedMaster[1], "rrule");
|
||||||
|
if (!newRrule && rrule) {
|
||||||
|
updatedMaster[1].push(rrule!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timezoneData = TIMEZONES.zones[event.timezone];
|
||||||
|
const vtimezone = makeTimezone(timezoneData, event);
|
||||||
|
|
||||||
|
let finalVevents: VCalComponent[];
|
||||||
|
|
||||||
|
if (removeOverrides) {
|
||||||
|
// When date/time/timezone/repeat rules changed, remove all override instances
|
||||||
|
finalVevents = [updatedMaster];
|
||||||
|
} else {
|
||||||
|
// When only properties changed, keep override instances and update their metadata
|
||||||
|
finalVevents = updateVeventsPreservingOverrides(
|
||||||
|
vevents,
|
||||||
|
oldMaster,
|
||||||
|
updatedMaster,
|
||||||
|
masterIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newJCal = [
|
||||||
|
"vcalendar",
|
||||||
|
[],
|
||||||
|
[...finalVevents, vtimezone.component.jCal],
|
||||||
|
];
|
||||||
|
|
||||||
|
return api(`dav${event.URL}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(newJCal),
|
||||||
|
headers: {
|
||||||
|
"content-type": "text/calendar; charset=utf-8",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user