diff options
Diffstat (limited to 'comm/calendar/base/content/item-editing/calendar-item-editing.js')
-rw-r--r-- | comm/calendar/base/content/item-editing/calendar-item-editing.js | 849 |
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(); + } +} |