fad1dfe4c5
* [#352] update exceptions on all update without reseting them * [#352] added tests
264 lines
7.0 KiB
TypeScript
264 lines
7.0 KiB
TypeScript
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",
|
|
},
|
|
});
|
|
};
|