diff options
Diffstat (limited to '')
-rw-r--r-- | comm/calendar/base/src/CalTransactionManager.jsm | 372 |
1 files changed, 372 insertions, 0 deletions
diff --git a/comm/calendar/base/src/CalTransactionManager.jsm b/comm/calendar/base/src/CalTransactionManager.jsm new file mode 100644 index 0000000000..67f01733ec --- /dev/null +++ b/comm/calendar/base/src/CalTransactionManager.jsm @@ -0,0 +1,372 @@ +/* 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/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = [ + "CalTransactionManager", + "CalTransaction", + "CalBatchTransaction", + "CalAddTransaction", + "CalModifyTransaction", + "CalDeleteTransaction", +]; + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +const OP_ADD = Ci.calIOperationListener.ADD; +const OP_MODIFY = Ci.calIOperationListener.MODIFY; +const OP_DELETE = Ci.calIOperationListener.DELETE; + +let transactionManager = null; + +/** + * CalTransactionManager is used to track user initiated operations on calendar + * items. These transactions can be undone or repeated when appropriate. + * + * This implementation is used instead of nsITransactionManager because better + * support for async transactions and access to batch transactions is needed + * which nsITransactionManager does not provide. + */ +class CalTransactionManager { + /** + * Contains transactions executed by the transaction manager than can be + * undone. + * + * @type {CalTransaction} + */ + undoStack = []; + + /** + * Contains transactions that have been undone by the transaction manager and + * can be redone again later if desired. + * + * @type {CalTransaction} + */ + redoStack = []; + + /** + * Provides a singleton instance of the CalTransactionManager. + * + * @returns {CalTransactionManager} + */ + static getInstance() { + if (!transactionManager) { + transactionManager = new CalTransactionManager(); + } + return transactionManager; + } + + /** + * @typedef {object} ExtResponse + * @property {number} responseMode One of the calIItipItem.autoResponse values. + */ + + /** + * @typedef {"add" | "modify" | "delete"} Action + */ + + /** + * Adds a CalTransaction to the internal stack. The transaction will be + * executed and its resulting Promise returned. + * + * @param {CalTransaction} trn - The CalTransaction to add to the stack and + * execute. + */ + async commit(trn) { + this.undoStack.push(trn); + return trn.doTransaction(); + } + + /** + * Creates and pushes a new CalBatchTransaction onto the internal stack. + * The created transaction is returned and can be used to combine multiple + * transactions into one. + * + * @returns {CalBatchTrasaction} + */ + beginBatch() { + let trn = new CalBatchTransaction(); + this.undoStack.push(trn); + return trn; + } + + /** + * peekUndoStack provides the top transaction on the undo stack (if any) + * without modifying the stack. + * + * @returns {CalTransaction?} + */ + peekUndoStack() { + return this.undoStack.at(-1); + } + + /** + * Undo the transaction at the top of the undo stack. + * + * @throws - NS_ERROR_FAILURE if the undo stack is empty. + */ + async undo() { + if (!this.undoStack.length) { + throw new Components.Exception( + "CalTransactionManager: undo stack is empty!", + Cr.NS_ERROR_FAILURE + ); + } + let trn = this.undoStack.pop(); + this.redoStack.push(trn); + return trn.undoTransaction(); + } + + /** + * Returns true if it is possible to undo the transaction at the top of the + * undo stack. + * + * @returns {boolean} + */ + canUndo() { + let trn = this.peekUndoStack(); + return Boolean(trn?.canWrite()); + } + + /** + * peekRedoStack provides the top transaction on the redo stack (if any) + * without modifying the stack. + * + * @returns {CalTransaction?} + */ + peekRedoStack() { + return this.redoStack.at(-1); + } + + /** + * Redo the transaction at the top of the redo stack. + * + * @throws - NS_ERROR_FAILURE if the redo stack is empty. + */ + async redo() { + if (!this.redoStack.length) { + throw new Components.Exception( + "CalTransactionManager: redo stack is empty!", + Cr.NS_ERROR_FAILURE + ); + } + let trn = this.redoStack.pop(); + this.undoStack.push(trn); + return trn.doTransaction(); + } + + /** + * Returns true if it is possible to redo the transaction at the top of the + * redo stack. + * + * @returns {boolean} + */ + canRedo() { + let trn = this.peekRedoStack(); + return Boolean(trn?.canWrite()); + } +} + +/** + * CalTransaction represents a single, atomic user operation on one or more + * calendar items. + */ +class CalTransaction { + /** + * Indicates whether the calendar of the transaction's target item(s) can be + * written to. + * + * @returns {boolean} + */ + canWrite() { + return false; + } + + /** + * Executes the transaction. + */ + async doTransaction() {} + + /** + * Executes the "undo" action of the transaction. + */ + async undoTransaction() {} +} + +/** + * CalBatchTransaction is used for batch transactions where multiple transactions + * treated as one is desired. For example; where the user selects and deletes + * more than one event. + */ +class CalBatchTransaction extends CalTransaction { + /** + * Stores the transactions that belong to the batch. + * + * @type {CalTransaction[]} + */ + transactions = []; + + /** + * Similar to the CalTransactionManager method except the transaction will be + * added to the batch. + */ + async commit(trn) { + this.transactions.push(trn); + return trn.doTransaction(); + } + + canWrite() { + return Boolean(this.transactions.length && this.transactions.every(trn => trn.canWrite())); + } + + async doTransaction() { + for (let trn of this.transactions) { + await trn.doTransaction(); + } + } + + async undoTransaction() { + for (let trn of this.transactions.slice().reverse()) { + await trn.undoTransaction(); + } + } +} + +/** + * CalBaseTransaction serves as the base for add/modify/delete operations. + */ +class CalBaseTransaction extends CalTransaction { + /** + * @type {calICalendar} + */ + calendar = null; + + /** + * @type {calIItemBase} + */ + item = null; + + /** + * @type {calIItemBase} + */ + oldItem = null; + + /** + * @type {calICalendar} + */ + oldCalendar = null; + + /** + * @type {ExtResponse} + */ + extResponse = null; + + /** + * @private + * @param {calIItemBase} item + * @param {calICalendar} calendar + * @param {calIItemBase?} oldItem + * @param {object?} extResponse + */ + constructor(item, calendar, oldItem, extResponse) { + super(); + this.item = item; + this.calendar = calendar; + this.oldItem = oldItem; + this.extResponse = extResponse; + } + + _dispatch(opType, item, oldItem) { + cal.itip.checkAndSend(opType, item, oldItem, this.extResponse); + } + + canWrite() { + if (itemWritable(this.item)) { + return this instanceof CalModifyTransaction ? itemWritable(this.oldItem) : true; + } + return false; + } +} + +/** + * CalAddTransaction handles additions. + */ +class CalAddTransaction extends CalBaseTransaction { + async doTransaction() { + let item = await this.calendar.addItem(this.item); + this._dispatch(OP_ADD, item, this.oldItem); + this.item = item; + } + + async undoTransaction() { + await this.calendar.deleteItem(this.item); + this._dispatch(OP_DELETE, this.item, this.item); + this.oldItem = this.item; + } +} + +/** + * CalModifyTransaction handles modifications. + */ +class CalModifyTransaction extends CalBaseTransaction { + async doTransaction() { + let item; + if (this.item.calendar.id == this.oldItem.calendar.id) { + item = await this.calendar.modifyItem( + cal.itip.prepareSequence(this.item, this.oldItem), + this.oldItem + ); + this._dispatch(OP_MODIFY, item, this.oldItem); + } else { + this.oldCalendar = this.oldItem.calendar; + item = await this.calendar.addItem(this.item); + this._dispatch(OP_ADD, item, this.oldItem); + await this.oldItem.calendar.deleteItem(this.oldItem); + this._dispatch(OP_DELETE, this.oldItem, this.oldItem); + } + this.item = item; + } + + async undoTransaction() { + if (this.oldItem.calendar.id == this.item.calendar.id) { + await this.calendar.modifyItem(cal.itip.prepareSequence(this.oldItem, this.item), this.item); + this._dispatch(OP_MODIFY, this.oldItem, this.oldItem); + } else { + await this.calendar.deleteItem(this.item); + this._dispatch(OP_DELETE, this.item, this.item); + await this.oldCalendar.addItem(this.oldItem); + this._dispatch(OP_ADD, this.oldItem, this.item); + } + } +} + +/** + * CalDeleteTransaction handles deletions. + */ +class CalDeleteTransaction extends CalBaseTransaction { + async doTransaction() { + await this.calendar.deleteItem(this.item); + this._dispatch(OP_DELETE, this.item, this.oldItem); + } + + async undoTransaction() { + await this.calendar.addItem(this.item); + this._dispatch(OP_ADD, this.item, this.item); + } +} + +/** + * Checks whether an item's calendar can be written to. + * + * @param {calIItemBase} item + */ +function itemWritable(item) { + return ( + item && + item.calendar && + cal.acl.isCalendarWritable(item.calendar) && + cal.acl.userCanAddItemsToCalendar(item.calendar) + ); +} |