summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/item-editing/calendar-item-editing.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/calendar/base/content/item-editing/calendar-item-editing.js849
1 files changed, 849 insertions, 0 deletions
diff --git a/comm/calendar/base/content/item-editing/calendar-item-editing.js b/comm/calendar/base/content/item-editing/calendar-item-editing.js
new file mode 100644
index 0000000000..a280e62f48
--- /dev/null
+++ b/comm/calendar/base/content/item-editing/calendar-item-editing.js
@@ -0,0 +1,849 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from ../calendar-management.js */
+/* import-globals-from ../calendar-views-utils.js */
+
+/* globals goUpdateCommand */
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+var { CalTransactionManager } = ChromeUtils.import("resource:///modules/CalTransactionManager.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ CalAddTransaction: "resource:///modules/CalTransactionManager.jsm",
+ CalDeleteTransaction: "resource:///modules/CalTransactionManager.jsm",
+ CalEvent: "resource:///modules/CalEvent.jsm",
+ CalModifyTransaction: "resource:///modules/CalTransactionManager.jsm",
+ CalTodo: "resource:///modules/CalTodo.jsm",
+});
+
+/* exported modifyEventWithDialog, undo, redo, setContextPartstat */
+
+/**
+ * The global calendar transaction manager.
+ *
+ * @type {CalTransactionManager}
+ */
+var gCalTransactionMgr = CalTransactionManager.getInstance();
+
+/**
+ * If a batch transaction is active, it is stored here.
+ *
+ * @type {CalBatchTransaction?}
+ */
+var gCalBatchTransaction = null;
+
+/**
+ * Sets the default values for new items, taking values from either the passed
+ * parameters or the preferences.
+ *
+ * @param {calIItemBase} aItem - The item to set up.
+ * @param {?calICalendar} aCalendar - The calendar to apply.
+ * @param {?calIDateTime} aStartDate - The start date to set.
+ * @param {?calIDateTime} aEndDate - The end date/due date to set.
+ * @param {?calIDateTime} aInitialDate - The reference date for the date pickers.
+ * @param {boolean} [aForceAllday=false] - Force the event/task to be an all-day item.
+ * @param {calIAttendee[]} aAttendees - Attendees to add, if `aItem` is an event.
+ */
+function setDefaultItemValues(
+ aItem,
+ aCalendar = null,
+ aStartDate = null,
+ aEndDate = null,
+ aInitialDate = null,
+ aForceAllday = false,
+ aAttendees = []
+) {
+ function endOfDay(aDate) {
+ let eod = aDate ? aDate.clone() : cal.dtz.now();
+ eod.hour = Services.prefs.getIntPref("calendar.view.dayendhour", 19);
+ eod.minute = 0;
+ eod.second = 0;
+ return eod;
+ }
+ function startOfDay(aDate) {
+ let sod = aDate ? aDate.clone() : cal.dtz.now();
+ sod.hour = Services.prefs.getIntPref("calendar.view.daystarthour", 8);
+ sod.minute = 0;
+ sod.second = 0;
+ return sod;
+ }
+
+ let initialDate = aInitialDate ? aInitialDate.clone() : cal.dtz.now();
+ initialDate.isDate = true;
+
+ if (aItem.isEvent()) {
+ if (aStartDate) {
+ aItem.startDate = aStartDate.clone();
+ if (aStartDate.isDate && !aForceAllday) {
+ // This is a special case where the date is specified, but the
+ // time is not. To take care, we setup up the time to our
+ // default event start time.
+ aItem.startDate = cal.dtz.getDefaultStartDate(aItem.startDate);
+ } else if (aForceAllday) {
+ // If the event should be forced to be allday, then don't set up
+ // any default hours and directly make it allday.
+ aItem.startDate.isDate = true;
+ aItem.startDate.timezone = cal.dtz.floating;
+ }
+ } else {
+ // If no start date was passed, then default to the next full hour
+ // of today, but with the date of the selected day
+ aItem.startDate = cal.dtz.getDefaultStartDate(initialDate);
+ }
+
+ if (aEndDate) {
+ aItem.endDate = aEndDate.clone();
+ if (aForceAllday) {
+ // XXX it is currently not specified, how callers that force all
+ // day should pass the end date. Right now, they should make
+ // sure that the end date is 00:00:00 of the day after.
+ aItem.endDate.isDate = true;
+ aItem.endDate.timezone = cal.dtz.floating;
+ }
+ } else {
+ aItem.endDate = aItem.startDate.clone();
+ if (aForceAllday) {
+ // All day events need to go to the beginning of the next day.
+ aItem.endDate.day++;
+ } else {
+ // If the event is not all day, then add the default event
+ // length.
+ aItem.endDate.minute += Services.prefs.getIntPref("calendar.event.defaultlength", 60);
+ }
+ }
+
+ // Free/busy status is only valid for events, must not be set for tasks.
+ aItem.setProperty("TRANSP", cal.item.getEventDefaultTransparency(aForceAllday));
+
+ for (let attendee of aAttendees) {
+ aItem.addAttendee(attendee);
+ }
+ } else if (aItem.isTodo()) {
+ let now = cal.dtz.now();
+ let initDate = initialDate ? initialDate.clone() : now;
+ initDate.isDate = false;
+ initDate.hour = now.hour;
+ initDate.minute = now.minute;
+ initDate.second = now.second;
+
+ if (aStartDate) {
+ aItem.entryDate = aStartDate.clone();
+ } else {
+ let defaultStart = Services.prefs.getStringPref("calendar.task.defaultstart", "none");
+ if (
+ Services.prefs.getIntPref("calendar.alarms.onfortodos", 0) == 1 &&
+ defaultStart == "none"
+ ) {
+ // start date is required if we want to set an alarm
+ defaultStart = "offsetcurrent";
+ }
+
+ let units = Services.prefs.getStringPref("calendar.task.defaultstartoffsetunits", "minutes");
+ if (!["days", "hours", "minutes"].includes(units)) {
+ units = "minutes";
+ }
+ let startOffset = cal.createDuration();
+ startOffset[units] = Services.prefs.getIntPref("calendar.task.defaultstartoffset", 0);
+ let start;
+
+ switch (defaultStart) {
+ case "none":
+ break;
+ case "startofday":
+ start = startOfDay(initDate);
+ break;
+ case "tomorrow":
+ start = startOfDay(initDate);
+ start.day++;
+ break;
+ case "nextweek":
+ start = startOfDay(initDate);
+ start.day += 7;
+ break;
+ case "offsetcurrent":
+ start = initDate.clone();
+ start.addDuration(startOffset);
+ break;
+ case "offsetnexthour":
+ start = initDate.clone();
+ start.second = 0;
+ start.minute = 0;
+ start.hour++;
+ start.addDuration(startOffset);
+ break;
+ }
+
+ if (start) {
+ aItem.entryDate = start;
+ }
+ }
+
+ if (aEndDate) {
+ aItem.dueDate = aEndDate.clone();
+ } else {
+ let defaultDue = Services.prefs.getStringPref("calendar.task.defaultdue", "none");
+
+ let units = Services.prefs.getStringPref("calendar.task.defaultdueoffsetunits", "minutes");
+ if (!["days", "hours", "minutes"].includes(units)) {
+ units = "minutes";
+ }
+ let dueOffset = cal.createDuration();
+ dueOffset[units] = Services.prefs.getIntPref("calendar.task.defaultdueoffset", 0);
+
+ let start = aItem.entryDate ? aItem.entryDate.clone() : initDate.clone();
+ let due;
+
+ switch (defaultDue) {
+ case "none":
+ break;
+ case "endofday":
+ due = endOfDay(start);
+ // go to tomorrow if we're past the end of today
+ if (start.compare(due) > 0) {
+ due.day++;
+ }
+ break;
+ case "tomorrow":
+ due = endOfDay(start);
+ due.day++;
+ break;
+ case "nextweek":
+ due = endOfDay(start);
+ due.day += 7;
+ break;
+ case "offsetcurrent":
+ due = start.clone();
+ due.addDuration(dueOffset);
+ break;
+ case "offsetnexthour":
+ due = start.clone();
+ due.second = 0;
+ due.minute = 0;
+ due.hour++;
+ due.addDuration(dueOffset);
+ break;
+ }
+
+ if (aItem.entryDate && due && aItem.entryDate.compare(due) > 0) {
+ // due can't be earlier than start date.
+ due = aItem.entryDate;
+ }
+
+ if (due) {
+ aItem.dueDate = due;
+ }
+ }
+ }
+
+ // Calendar
+ aItem.calendar = aCalendar || getSelectedCalendar();
+
+ // Alarms
+ cal.alarms.setDefaultValues(aItem);
+}
+
+/**
+ * Creates an event with the calendar event dialog.
+ *
+ * @param {?calICalendar} calendar - The calendar to create the event in
+ * @param {?calIDateTime} startDate - The event's start date.
+ * @param {?calIDateTime} endDate - The event's end date.
+ * @param {?string} summary - The event's title.
+ * @param {?calIEvent} event - A template event to show in the dialog
+ * @param {?boolean} forceAllDay - Make sure the event shown in the dialog is an all-day event.
+ * @param {?calIAttendee} attendees - Attendees to add to the event.
+ */
+function createEventWithDialog(
+ calendar,
+ startDate,
+ endDate,
+ summary,
+ event,
+ forceAllDay,
+ attendees
+) {
+ let onNewEvent = function (item, opcalendar, originalItem, listener, extresponse = null) {
+ if (item.id) {
+ // If the item already has an id, then this is the result of
+ // saving the item without closing, and then saving again.
+ doTransaction("modify", item, opcalendar, originalItem, listener, extresponse);
+ } else {
+ // Otherwise, this is an addition
+ doTransaction("add", item, opcalendar, null, listener, extresponse);
+ }
+ };
+
+ if (event) {
+ if (!event.isMutable) {
+ event = event.clone();
+ }
+ // If the event should be created from a template, then make sure to
+ // remove the id so that the item obtains a new id when doing the
+ // transaction
+ event.id = null;
+
+ if (forceAllDay) {
+ event.startDate.isDate = true;
+ event.endDate.isDate = true;
+ if (event.startDate.compare(event.endDate) == 0) {
+ // For a one day all day event, the end date must be 00:00:00 of
+ // the next day.
+ event.endDate.day++;
+ }
+ }
+
+ if (!event.calendar) {
+ event.calendar = calendar || getSelectedCalendar();
+ }
+ } else {
+ event = new CalEvent();
+
+ let refDate = currentView().selectedDay?.clone();
+ setDefaultItemValues(event, calendar, startDate, endDate, refDate, forceAllDay, attendees);
+ if (summary) {
+ event.title = summary;
+ }
+ }
+ openEventDialog(event, event.calendar, "new", onNewEvent);
+}
+
+/**
+ * Creates a task with the calendar event dialog.
+ *
+ * @param calendar (optional) The calendar to create the task in
+ * @param dueDate (optional) The task's due date.
+ * @param summary (optional) The task's title.
+ * @param todo (optional) A template task to show in the dialog.
+ * @param initialDate (optional) The initial date for new task datepickers
+ */
+function createTodoWithDialog(calendar, dueDate, summary, todo, initialDate) {
+ let onNewItem = function (item, opcalendar, originalItem, listener, extresponse = null) {
+ if (item.id) {
+ // If the item already has an id, then this is the result of
+ // saving the item without closing, and then saving again.
+ doTransaction("modify", item, opcalendar, originalItem, listener, extresponse);
+ } else {
+ // Otherwise, this is an addition
+ doTransaction("add", item, opcalendar, null, listener, extresponse);
+ }
+ };
+
+ if (todo) {
+ // If the todo should be created from a template, then make sure to
+ // remove the id so that the item obtains a new id when doing the
+ // transaction
+ if (todo.id) {
+ todo = todo.clone();
+ todo.id = null;
+ }
+
+ if (!todo.calendar) {
+ todo.calendar = calendar || getSelectedCalendar();
+ }
+ } else {
+ todo = new CalTodo();
+ setDefaultItemValues(todo, calendar, null, dueDate, initialDate);
+
+ if (summary) {
+ todo.title = summary;
+ }
+ }
+
+ openEventDialog(todo, calendar, "new", onNewItem, initialDate);
+}
+
+/**
+ * Opens the passed event item for viewing. This enables the modify callback in
+ * openEventDialog so invitation responses can be edited.
+ *
+ * @param {calIItemBase} item - The calendar item to view.
+ */
+function openEventDialogForViewing(item) {
+ function onDialogComplete(newItem, calendar, originalItem, listener, extresponse) {
+ doTransaction("modify", newItem, calendar, originalItem, listener, extresponse);
+ }
+ openEventDialog(item, item.calendar, "view", onDialogComplete);
+}
+
+/**
+ * Modifies the passed event in the event dialog.
+ *
+ * @param aItem The item to modify.
+ * @param aPromptOccurrence If the user should be prompted to select if the
+ * parent item or occurrence should be modified.
+ * @param initialDate (optional) The initial date for new task datepickers
+ * @param aCounterProposal (optional) An object representing the counterproposal
+ * {
+ * {JsObject} result: {
+ * type: {String} "OK"|"OUTDATED"|"NOTLATESTUPDATE"|"ERROR"|"NODIFF"
+ * descr: {String} a technical description of the problem if type is ERROR or NODIFF,
+ * otherwise an empty string
+ * },
+ * (empty if result.type = "ERROR"|"NODIFF"){Array} differences: [{
+ * property: {String} a property that is subject to the proposal
+ * proposed: {String} the proposed value
+ * original: {String} the original value
+ * }]
+ * }
+ */
+function modifyEventWithDialog(aItem, aPromptOccurrence, initialDate = null, aCounterProposal) {
+ let dlg = cal.item.findWindow(aItem);
+ if (dlg) {
+ dlg.focus();
+ return;
+ }
+
+ let onModifyItem = function (item, calendar, originalItem, listener, extresponse = null) {
+ doTransaction("modify", item, calendar, originalItem, listener, extresponse);
+ };
+
+ let item = aItem;
+ let response;
+ if (aPromptOccurrence !== false) {
+ [item, , response] = promptOccurrenceModification(aItem, true, "edit");
+ }
+
+ if (item && (response || response === undefined)) {
+ openEventDialog(item, item.calendar, "modify", onModifyItem, initialDate, aCounterProposal);
+ }
+}
+
+/**
+ * @callback onDialogComplete
+ *
+ * @param {calIItemBase} newItem
+ * @param {calICalendar} calendar
+ * @param {calIItemBase} originalItem
+ * @param {?calIOperationListener} listener
+ * @param {?object} extresponse
+ */
+
+/**
+ * Opens the event dialog with the given item (task OR event).
+ *
+ * @param {calIItemBase} calendarItem - The item to open the dialog with.
+ * @param {calICalendar} calendar - The calendar to open the dialog with.
+ * @param {string} mode - The operation the dialog should do
+ * ("new", "view", "modify").
+ * @param {onDialogComplete} callback - The callback to call when the dialog
+ * has completed.
+ * @param {?calIDateTime} initialDate - The initial date for new task
+ * datepickers.
+ * @param {?object} counterProposal - An object representing the
+ * counterproposal - see description
+ * for modifyEventWithDialog().
+ */
+function openEventDialog(
+ calendarItem,
+ calendar,
+ mode,
+ callback,
+ initialDate = null,
+ counterProposal
+) {
+ let dlg = cal.item.findWindow(calendarItem);
+ if (dlg) {
+ dlg.focus();
+ return;
+ }
+
+ // Set up some defaults
+ mode = mode || "new";
+ calendar = calendar || getSelectedCalendar();
+ let calendars = cal.manager.getCalendars();
+ calendars = calendars.filter(cal.acl.isCalendarWritable);
+
+ let isItemSupported;
+ if (calendarItem.isTodo()) {
+ isItemSupported = function (aCalendar) {
+ return aCalendar.getProperty("capabilities.tasks.supported") !== false;
+ };
+ } else if (calendarItem.isEvent()) {
+ isItemSupported = function (aCalendar) {
+ return aCalendar.getProperty("capabilities.events.supported") !== false;
+ };
+ }
+
+ // Filter out calendars that don't support the given calendar item
+ calendars = calendars.filter(isItemSupported);
+
+ // Filter out calendar/items that we cannot write to/modify
+ if (mode == "new") {
+ calendars = calendars.filter(cal.acl.userCanAddItemsToCalendar);
+ } else if (mode == "modify") {
+ calendars = calendars.filter(aCalendar => {
+ /* If the calendar is the item calendar, we check that the item
+ * can be modified. If the calendar is NOT the item calendar, we
+ * check that the user can remove items from that calendar and
+ * add items to the current one.
+ */
+ let isSameCalendar = calendarItem.calendar == aCalendar;
+ let canModify = cal.acl.userCanModifyItem(calendarItem);
+ let canMoveItems =
+ cal.acl.userCanDeleteItemsFromCalendar(calendarItem.calendar) &&
+ cal.acl.userCanAddItemsToCalendar(aCalendar);
+
+ return isSameCalendar ? canModify : canMoveItems;
+ });
+ }
+
+ if (
+ mode == "new" &&
+ (!cal.acl.isCalendarWritable(calendar) ||
+ !cal.acl.userCanAddItemsToCalendar(calendar) ||
+ !isItemSupported(calendar))
+ ) {
+ if (calendars.length < 1) {
+ // There are no writable calendars or no calendar supports the given
+ // item. Don't show the dialog.
+ return;
+ }
+ // Pick the first calendar that supports the item and is writable
+ calendar = calendars[0];
+ if (calendarItem) {
+ // XXX The dialog currently uses the items calendar as a first
+ // choice. Since we are shortly before a release to keep
+ // regression risk low, explicitly set the item's calendar here.
+ calendarItem.calendar = calendars[0];
+ }
+ }
+
+ // Setup the window arguments
+ let args = {};
+ args.calendarEvent = calendarItem;
+ args.calendar = calendar;
+ args.mode = mode;
+ args.onOk = callback;
+ args.initialStartDateValue = initialDate || cal.dtz.getDefaultStartDate();
+ args.counterProposal = counterProposal;
+ args.inTab = Services.prefs.getBoolPref("calendar.item.editInTab", false);
+ // this will be called if file->new has been selected from within the dialog
+ args.onNewEvent = function (opcalendar) {
+ createEventWithDialog(opcalendar, null, null);
+ };
+ args.onNewTodo = function (opcalendar) {
+ createTodoWithDialog(opcalendar);
+ };
+
+ // the dialog will reset this to auto when it is done loading.
+ window.setCursor("wait");
+
+ // Ask the provider if this item is an invitation. If this is the case,
+ // we'll open the summary dialog since the user is not allowed to change
+ // the details of the item.
+ let isInvitation =
+ calendar.supportsScheduling && calendar.getSchedulingSupport().isInvitation(calendarItem);
+
+ // open the dialog modeless
+ let url;
+ let isEditable = mode == "modify" && !isInvitation && cal.acl.userCanModifyItem(calendarItem);
+
+ if (cal.acl.isCalendarWritable(calendar) && (mode == "new" || isEditable)) {
+ // Currently the read-only summary dialog is never opened in a tab.
+ if (args.inTab) {
+ url = "chrome://calendar/content/calendar-item-iframe.xhtml";
+ } else {
+ url = "chrome://calendar/content/calendar-event-dialog.xhtml";
+ }
+ } else {
+ url = "chrome://calendar/content/calendar-summary-dialog.xhtml";
+ args.inTab = false;
+ args.isInvitation = isInvitation;
+ }
+
+ if (args.inTab) {
+ args.url = url;
+ let tabmail = document.getElementById("tabmail");
+ let tabtype = args.calendarEvent.isEvent() ? "calendarEvent" : "calendarTask";
+ tabmail.openTab(tabtype, args);
+ } else {
+ // open in a window
+ openDialog(url, "_blank", "chrome,titlebar,toolbar,resizable", args);
+ }
+}
+
+/**
+ * Prompts the user how the passed item should be modified. If the item is an
+ * exception or already a parent item, the item is returned without prompting.
+ * If "all occurrences" is specified, the parent item is returned. If "this
+ * occurrence only" is specified, then aItem is returned. If "this and following
+ * occurrences" is selected, aItem's parentItem is modified so that the
+ * recurrence rules end (UNTIL) just before the given occurrence. If
+ * aNeedsFuture is specified, a new item is made from the part that was stripped
+ * off the passed item.
+ *
+ * EXDATEs and RDATEs that do not fit into the items recurrence are removed. If
+ * the modified item or the future item only consist of a single occurrence,
+ * they are changed to be single items.
+ *
+ * @param aItem The item or array of items to check.
+ * @param aNeedsFuture If true, the future item is parsed.
+ * This parameter can for example be
+ * false if a deletion is being made.
+ * @param aAction Either "edit" or "delete". Sets up
+ * the labels in the occurrence prompt
+ * @returns [modifiedItem, futureItem, promptResponse]
+ * modifiedItem is a single item or array
+ * of items depending on the past aItem
+ *
+ * If "this and all following" was chosen,
+ * an array containing the item *until*
+ * the given occurrence (modifiedItem),
+ * and the item *after* the given
+ * occurrence (futureItem).
+ *
+ * If any other option was chosen,
+ * futureItem is null and the
+ * modifiedItem is either the parent item
+ * or the passed occurrence, or null if
+ * the dialog was canceled.
+ *
+ * The promptResponse parameter gives the
+ * response of the dialog as a constant.
+ */
+function promptOccurrenceModification(aItem, aNeedsFuture, aAction) {
+ const CANCEL = 0;
+ const MODIFY_OCCURRENCE = 1;
+ const MODIFY_FOLLOWING = 2;
+ const MODIFY_PARENT = 3;
+
+ let futureItems = false;
+ let pastItems = [];
+ let returnItem = null;
+ let type = CANCEL;
+ let items = Array.isArray(aItem) ? aItem : [aItem];
+
+ // Check if this actually is an instance of a recurring event
+ if (items.every(item => item == item.parentItem)) {
+ type = MODIFY_PARENT;
+ } else if (aItem && items.length) {
+ // Prompt the user. Setting modal blocks the dialog until it is closed. We
+ // use rv to pass our return value.
+ let rv = { value: CANCEL, items, action: aAction };
+ window.openDialog(
+ "chrome://calendar/content/calendar-occurrence-prompt.xhtml",
+ "PromptOccurrenceModification",
+ "centerscreen,chrome,modal,titlebar",
+ rv
+ );
+ type = rv.value;
+ }
+
+ switch (type) {
+ case MODIFY_PARENT:
+ pastItems = items.map(item => item.parentItem);
+ break;
+ case MODIFY_FOLLOWING:
+ // TODO tbd in a different bug
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ case MODIFY_OCCURRENCE:
+ pastItems = items;
+ break;
+ case CANCEL:
+ // Since we have not set past or futureItem, the return below will
+ // take care.
+ break;
+ }
+ if (aItem) {
+ returnItem = Array.isArray(aItem) ? pastItems : pastItems[0];
+ }
+ return [returnItem, futureItems, type];
+}
+
+// Undo/Redo code
+
+/**
+ * Create and commit a transaction with the given arguments to the transaction
+ * manager. Also updates the undo/redo menu.
+ *
+ * @param action The action to do.
+ * @param item The new item to add/modify/delete
+ * @param calendar The calendar to do the transaction on
+ * @param oldItem (optional) some actions require an old item
+ * @param observer (optional) the observer to call when complete.
+ * @param extResponse (optional) JS object with additional parameters for sending itip messages
+ * (see also description of checkAndSend in calItipUtils.jsm)
+ */
+async function doTransaction(action, item, calendar, oldItem, observer, extResponse = null) {
+ // This is usually a user-initiated transaction, so make sure the calendar
+ // this transaction is happening on is visible.
+ top.ensureCalendarVisible(calendar);
+
+ let manager = gCalBatchTransaction || gCalTransactionMgr;
+ let trn;
+ switch (action) {
+ case "add":
+ trn = new CalAddTransaction(item, calendar, oldItem, extResponse);
+ break;
+ case "modify":
+ trn = new CalModifyTransaction(item, calendar, oldItem, extResponse);
+ break;
+ case "delete":
+ trn = new CalDeleteTransaction(item, calendar, oldItem, extResponse);
+ break;
+ default:
+ throw new Components.Exception(
+ `Invalid action specified "${action}"`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+
+ await manager.commit(trn);
+
+ // If a batch transaction is active, do not update the menu as
+ // endBatchTransaction() will take care of that.
+ if (gCalBatchTransaction) {
+ return;
+ }
+
+ observer?.onTransactionComplete(trn.item, trn.oldItem);
+ updateUndoRedoMenu();
+}
+
+/**
+ * Undo the last operation done through the transaction manager.
+ */
+function undo() {
+ if (canUndo()) {
+ gCalTransactionMgr.undo();
+ updateUndoRedoMenu();
+ }
+}
+
+/**
+ * Redo the last undone operation in the transaction manager.
+ */
+function redo() {
+ if (canRedo()) {
+ gCalTransactionMgr.redo();
+ updateUndoRedoMenu();
+ }
+}
+
+/**
+ * Start a batch transaction on the transaction manager.
+ */
+function startBatchTransaction() {
+ gCalBatchTransaction = gCalTransactionMgr.beginBatch();
+}
+
+/**
+ * End a previously started batch transaction. NOTE: be sure to call this in a
+ * try-catch-finally-block in case you have code that could fail between
+ * startBatchTransaction and this call.
+ */
+function endBatchTransaction() {
+ gCalBatchTransaction = null;
+ updateUndoRedoMenu();
+}
+
+/**
+ * Checks if the last operation can be undone (or if there is a last operation
+ * at all).
+ */
+function canUndo() {
+ return gCalTransactionMgr.canUndo();
+}
+
+/**
+ * Checks if the last undone operation can be redone.
+ */
+function canRedo() {
+ return gCalTransactionMgr.canRedo();
+}
+
+/**
+ * Update the undo and redo commands.
+ */
+function updateUndoRedoMenu() {
+ goUpdateCommand("cmd_undo");
+ goUpdateCommand("cmd_redo");
+}
+
+/**
+ * Updates the partstat of the calendar owner for specified items triggered by a
+ * context menu operation
+ *
+ * For a documentation of the expected bahaviours for different use cases of
+ * dealing with context menu partstat actions, see also setupAttendanceMenu(...)
+ * in calendar-ui-utils.js
+ *
+ * @param {EventTarget} aTarget the target of the triggering event
+ * @param {Array} aItems an array of calEvent or calIToDo items
+ */
+function setContextPartstat(aTarget, aItems) {
+ /**
+ * Provides the participation representing the user for a provided item
+ *
+ * @param {calEvent|calTodo} aItem The calendar item to inspect
+ * @returns {?calIAttendee} An calIAttendee object or null if no
+ * participant was detected
+ */
+ function getParticipant(aItem) {
+ let party = null;
+ if (cal.itip.isInvitation(aItem)) {
+ party = cal.itip.getInvitedAttendee(aItem);
+ } else if (aItem.organizer && aItem.getAttendees().length) {
+ let calOrgId = aItem.calendar.getProperty("organizerId");
+ if (calOrgId.toLowerCase() == aItem.organizer.id.toLowerCase()) {
+ party = aItem.organizer;
+ }
+ }
+ return party;
+ }
+
+ startBatchTransaction();
+ try {
+ // TODO: make sure we overwrite the partstat of all occurrences in
+ // the selection, if the partstat of the respective master item is
+ // changed - see matrix in the doc block of setupAttendanceMenu(...)
+ // in calendar-ui-utils.js
+
+ for (let oldItem of aItems) {
+ // Skip this item if its calendar is read only.
+ if (oldItem.calendar.readOnly) {
+ continue;
+ }
+ if (aTarget.getAttribute("scope") == "all-occurrences") {
+ oldItem = oldItem.parentItem;
+ }
+ let attendee = getParticipant(oldItem);
+ if (attendee) {
+ // skip this item if the partstat for the participant hasn't
+ // changed. otherwise we would always perform update operations
+ // for recurring events on both, the master and the occurrence
+ // item
+ let partStat = aTarget.getAttribute("respvalue");
+ if (attendee.participationStatus == partStat) {
+ continue;
+ }
+
+ let newItem = oldItem.clone();
+ let newAttendee = attendee.clone();
+ newAttendee.participationStatus = partStat;
+ if (newAttendee.isOrganizer) {
+ newItem.organizer = newAttendee;
+ } else {
+ newItem.removeAttendee(attendee);
+ newItem.addAttendee(newAttendee);
+ }
+
+ let extResponse = null;
+ if (aTarget.hasAttribute("respmode")) {
+ let mode = aTarget.getAttribute("respmode");
+ let itipMode = Ci.calIItipItem[mode];
+ extResponse = { responseMode: itipMode };
+ }
+
+ doTransaction("modify", newItem, newItem.calendar, oldItem, null, extResponse);
+ }
+ }
+ } catch (e) {
+ cal.ERROR("Error setting partstat: " + e + "\r\n");
+ } finally {
+ endBatchTransaction();
+ }
+}