From e02945b591b17ab9705771c3817c68e762e1b6ac Mon Sep 17 00:00:00 2001 From: Aurimas Vinckevicius Date: Fri, 14 Nov 2014 02:46:31 -0600 Subject: [PATCH] Add a centralized, modular .save() method to DataObject .save calls ._initSave(), _saveData(), _finalizeSave() internally passing `env` object to each to act as an environment for passing around variables * _initSave should determine if the save is possible and return a promise for either `true` or `false`. It should also set up the environment, e.g. determine if this `isNew` * _saveData performs the actual saving to the database, but should not do any terminal steps in the save process so that any extending classes could extend this method to write additional data to the database * _finalizeSave should perform any finalization before the data is committed to the database. _recoverFromSaveError is called with `env` and an error that occurred. This method should perform any recovery steps, e.g. discarding the save and reloading the item from the database into the cache. --- .../content/zotero/xpcom/data/collection.js | 272 ++-- .../content/zotero/xpcom/data/dataObject.js | 92 ++ chrome/content/zotero/xpcom/data/item.js | 1154 ++++++++--------- chrome/content/zotero/xpcom/search.js | 240 ++-- 4 files changed, 872 insertions(+), 886 deletions(-) diff --git a/chrome/content/zotero/xpcom/data/collection.js b/chrome/content/zotero/xpcom/data/collection.js index db205fb4b..1ae5aa4b1 100644 --- a/chrome/content/zotero/xpcom/data/collection.js +++ b/chrome/content/zotero/xpcom/data/collection.js @@ -279,168 +279,136 @@ Zotero.Collection.prototype.getChildItems = function (asIDs, includeDeleted) { return objs; } +Zotero.Collection.prototype._initSave = Zotero.Promise.coroutine(function* (env) { + if (!this.name) { + throw new Error('Collection name is empty'); + } + + return Zotero.Collection._super.prototype._initSave.apply(this, arguments); +}); -Zotero.Collection.prototype.save = Zotero.Promise.coroutine(function* () { - try { - Zotero.Collections.editCheck(this); +Zotero.Collection.prototype._saveData = Zotero.Promise.coroutine(function* (env) { + var isNew = env.isNew; + + var collectionID = env.id = this._id = this.id ? this.id : yield Zotero.ID.get('collections'); + var libraryID = env.libraryID = this.libraryID; + var key = env.key = this._key = this.key ? this.key : this._generateKey(); + + Zotero.debug("Saving collection " + this.id); + + // Verify parent + if (this._parentKey) { + let newParent = Zotero.Collections.getByLibraryAndKey( + this.libraryID, this._parentKey + ); - if (!this.name) { - throw new Error('Collection name is empty'); + if (!newParent) { + throw new Error("Cannot set parent to invalid collection " + this._parentKey); } - if (Zotero.Utilities.isEmpty(this._changed)) { - Zotero.debug("Collection " + this.id + " has not changed"); - return false; + if (newParent.id == this.id) { + throw new Error('Cannot move collection into itself!'); } - var isNew = !this.id; - - // Register this item's identifiers in Zotero.DataObjects on transaction commit, - // before other callbacks run - var collectionID, libraryID, key; - if (isNew) { - var transactionOptions = { - onCommit: function () { - Zotero.Collections.registerIdentifiers(collectionID, libraryID, key); - } - }; - } - else { - var transactionOptions = null; + if (this.id && (yield this.hasDescendent('collection', newParent.id))) { + throw ('Cannot move collection "' + this.name + '" into one of its own descendents'); } - return Zotero.DB.executeTransaction(function* () { - // how to know if date modified changed (in server code too?) - - collectionID = this._id = this.id ? this.id : yield Zotero.ID.get('collections'); - libraryID = this.libraryID; - key = this._key = this.key ? this.key : this._generateKey(); - - Zotero.debug("Saving collection " + this.id); - - // Verify parent - if (this._parentKey) { - let newParent = Zotero.Collections.getByLibraryAndKey( - this.libraryID, this._parentKey - ); - - if (!newParent) { - throw new Error("Cannot set parent to invalid collection " + this._parentKey); - } - - if (newParent.id == this.id) { - throw new Error('Cannot move collection into itself!'); - } - - if (this.id && (yield this.hasDescendent('collection', newParent.id))) { - throw ('Cannot move collection "' + this.name + '" into one of its own descendents'); - } - - var parent = newParent.id; - } - else { - var parent = null; - } - - var columns = [ - 'collectionID', - 'collectionName', - 'parentCollectionID', - 'clientDateModified', - 'libraryID', - 'key', - 'version', - 'synced' - ]; - var sqlValues = [ - collectionID ? { int: collectionID } : null, - { string: this.name }, - parent ? parent : null, - Zotero.DB.transactionDateTime, - this.libraryID ? this.libraryID : 0, - key, - this.version ? this.version : 0, - this.synced ? 1 : 0 - ]; - if (isNew) { - var placeholders = columns.map(function () '?').join(); - - var sql = "REPLACE INTO collections (" + columns.join(', ') + ") " - + "VALUES (" + placeholders + ")"; - var insertID = yield Zotero.DB.queryAsync(sql, sqlValues); - if (!collectionID) { - collectionID = insertID; - } - } - else { - columns.shift(); - sqlValues.push(sqlValues.shift()); - let sql = 'UPDATE collections SET ' - + columns.map(function (x) x + '=?').join(', ') - + ' WHERE collectionID=?'; - yield Zotero.DB.queryAsync(sql, sqlValues); - } - - if (this._changed.parentKey) { - var parentIDs = []; - if (this.id && this._previousData.parentKey) { - parentIDs.push(Zotero.Collections.getIDFromLibraryAndKey( - this.libraryID, this._previousData.parentKey - )); - } - if (this.parentKey) { - parentIDs.push(Zotero.Collections.getIDFromLibraryAndKey( - this.libraryID, this.parentKey - )); - } - if (this.id) { - Zotero.Notifier.trigger('move', 'collection', this.id); - } - } - - if (isNew && this.libraryID) { - var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID); - var group = Zotero.Groups.get(groupID); - group.clearCollectionCache(); - } - - if (isNew) { - Zotero.Notifier.trigger('add', 'collection', this.id); - } - else { - Zotero.Notifier.trigger('modify', 'collection', this.id, this._previousData); - } - - // Invalidate cached child collections - if (parentIDs) { - Zotero.Collections.refreshChildCollections(parentIDs); - } - - // New collections have to be reloaded via Zotero.Collections.get(), so mark them as disabled - if (isNew) { - var id = this.id; - this._disabled = true; - return id; - } - - yield this.reload(); - this._clearChanged(); - - return true; - }.bind(this), transactionOptions); + var parent = newParent.id; } - catch (e) { - try { - yield this.reload(); - this._clearChanged(); - } - catch (e2) { - Zotero.debug(e2, 1); - } - - Zotero.debug(e, 1); - throw e; + else { + var parent = null; } + + var columns = [ + 'collectionID', + 'collectionName', + 'parentCollectionID', + 'clientDateModified', + 'libraryID', + 'key', + 'version', + 'synced' + ]; + var sqlValues = [ + collectionID ? { int: collectionID } : null, + { string: this.name }, + parent ? parent : null, + Zotero.DB.transactionDateTime, + this.libraryID ? this.libraryID : 0, + key, + this.version ? this.version : 0, + this.synced ? 1 : 0 + ]; + if (isNew) { + var placeholders = columns.map(function () '?').join(); + + var sql = "REPLACE INTO collections (" + columns.join(', ') + ") " + + "VALUES (" + placeholders + ")"; + var insertID = yield Zotero.DB.queryAsync(sql, sqlValues); + if (!collectionID) { + collectionID = env.id = insertID; + } + } + else { + columns.shift(); + sqlValues.push(sqlValues.shift()); + let sql = 'UPDATE collections SET ' + + columns.map(function (x) x + '=?').join(', ') + + ' WHERE collectionID=?'; + yield Zotero.DB.queryAsync(sql, sqlValues); + } + + if (this._changed.parentKey) { + var parentIDs = []; + if (this.id && this._previousData.parentKey) { + parentIDs.push(Zotero.Collections.getIDFromLibraryAndKey( + this.libraryID, this._previousData.parentKey + )); + } + if (this.parentKey) { + parentIDs.push(Zotero.Collections.getIDFromLibraryAndKey( + this.libraryID, this.parentKey + )); + } + if (this.id) { + Zotero.Notifier.trigger('move', 'collection', this.id); + } + env.parentIDs = parentIDs; + } +}); + +Zotero.Collection.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) { + var isNew = env.isNew; + if (isNew && this.libraryID) { + var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID); + var group = Zotero.Groups.get(groupID); + group.clearCollectionCache(); + } + + if (isNew) { + Zotero.Notifier.trigger('add', 'collection', this.id); + } + else { + Zotero.Notifier.trigger('modify', 'collection', this.id, this._previousData); + } + + // Invalidate cached child collections + if (env.parentIDs) { + Zotero.Collections.refreshChildCollections(env.parentIDs); + } + + // New collections have to be reloaded via Zotero.Collections.get(), so mark them as disabled + if (isNew) { + var id = this.id; + this._disabled = true; + return id; + } + + yield this.reload(); + this._clearChanged(); + + return true; }); diff --git a/chrome/content/zotero/xpcom/data/dataObject.js b/chrome/content/zotero/xpcom/data/dataObject.js index d7d928548..a563f7e67 100644 --- a/chrome/content/zotero/xpcom/data/dataObject.js +++ b/chrome/content/zotero/xpcom/data/dataObject.js @@ -425,6 +425,98 @@ Zotero.DataObject.prototype._clearFieldChange = function (field) { delete this._previousData[field]; } + +Zotero.DataObject.prototype.isEditable = function () { + return Zotero.Libraries.isEditable(this.libraryID); +} + + +Zotero.DataObject.prototype.editCheck = function () { + if (!Zotero.Sync.Server.updatesInProgress && !Zotero.Sync.Storage.updatesInProgress && !this.isEditable()) { + throw ("Cannot edit " + this._objectType + " in read-only Zotero library"); + } +} + +/** + * Save changes to database + * + * @return {Promise} Promise for itemID of new item, + * TRUE on item update, or FALSE if item was unchanged + */ +Zotero.DataObject.prototype.save = Zotero.Promise.coroutine(function* (options) { + var env = { + arguments: arguments, + transactionOptions: null, + options: options || {} + }; + + var proceed = yield this._initSave(env); + if (!proceed) return false; + + if (env.isNew) { + Zotero.debug('Saving data for new ' + this._objectType + ' to database', 4); + } + else { + Zotero.debug('Updating database with new ' + this._objectType + ' data', 4); + } + + return Zotero.DB.executeTransaction(function* () { + yield this._saveData(env); + return yield this._finalizeSave(env); + }.bind(this), env.transactionOptions) + .catch(e => { + return this._recoverFromSaveError(env, e) + .catch(function(e2) { + Zotero.debug(e2, 1); + }) + .then(function() { + Zotero.debug(e, 1); + throw e; + }) + }); +}); + +Zotero.DataObject.prototype.hasChanged = function() { + Zotero.debug(this._changed); + return !!Object.keys(this._changed).filter(dataType => this._changed[dataType]).length +} + +Zotero.DataObject.prototype._saveData = function() { + throw new Error("Zotero.DataObject.prototype._saveData is an abstract method"); +} + +Zotero.DataObject.prototype._finalizeSave = function() { + throw new Error("Zotero.DataObject.prototype._finalizeSave is an abstract method"); +} + +Zotero.DataObject.prototype._recoverFromSaveError = Zotero.Promise.coroutine(function* () { + yield this.reload(null, true); + this._clearChanged(); +}); + +Zotero.DataObject.prototype._initSave = Zotero.Promise.coroutine(function* (env) { + env.isNew = !this.id; + + this.editCheck(); + + if (!this.hasChanged()) { + Zotero.debug(this._ObjectType + ' ' + this.id + ' has not changed', 4); + return false; + } + + // Register this object's identifiers in Zotero.DataObjects on transaction commit, + // before other callbacks run + if (env.isNew) { + env.transactionOptions = { + onCommit: () => { + this.ObjectsClass.registerIdentifiers(env.id, env.libraryID, env.key); + } + }; + } + + return true; +}); + /** * Generates data object key * @return {String} key diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js index fecc926fb..f3c57bbe0 100644 --- a/chrome/content/zotero/xpcom/data/item.js +++ b/chrome/content/zotero/xpcom/data/item.js @@ -503,15 +503,6 @@ Zotero.Item.prototype.loadFromRow = function(row, reload) { } -/* - * Check if any data fields have changed since last save - */ -Zotero.Item.prototype.hasChanged = function() { - Zotero.debug(this._changed); - return !!Object.keys(this._changed).filter((dataType) => this._changed[dataType]).length -} - - /* * Set or change the item's type */ @@ -1173,609 +1164,572 @@ Zotero.Item.prototype.removeRelatedItem = Zotero.Promise.coroutine(function* (it }); -/** - * Save changes to database - * - * @return {Promise} Promise for itemID of new item, - * TRUE on item update, or FALSE if item was unchanged - */ -Zotero.Item.prototype.save = Zotero.Promise.coroutine(function* (options) { - try { - if (!options) { - options = {}; - } +Zotero.Item.prototype.isEditable = function() { + var editable = Zotero.Item._super.prototype.isEditable.apply(this); + if (!editable) return false; + + // Check if we're allowed to save attachments + if (this.isAttachment() + && (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL || + this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE) + && !Zotero.Libraries.isFilesEditable(this.libraryID) + ) { + return false; + } + + return true; +} + +Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) { + var isNew = env.isNew; + var options = env.options; + + var itemTypeID = this.itemTypeID; - var isNew = !this.id; - - Zotero.Items.editCheck(this); - - if (!this.hasChanged()) { - Zotero.debug('Item ' + this.id + ' has not changed', 4); - return false; - } - - // Register this item's identifiers in Zotero.DataObjects on transaction commit, - // before other callbacks run - var itemID, libraryID, key; - if (isNew) { - var transactionOptions = { - onCommit: function () { - Zotero.Items.registerIdentifiers(itemID, libraryID, key); + var sqlColumns = []; + var sqlValues = []; + var reloadParentChildItems = {}; + + // + // Primary fields + // + // If available id value, use it -- otherwise we'll use autoincrement + var itemID = env.id = this._id = this.id ? this.id : yield Zotero.ID.get('items'); + Zotero.debug('='); + var libraryID = env.libraryID = this.libraryID; + var key = env.key = this._key = this.key ? this.key : this._generateKey(); + + sqlColumns.push( + 'itemTypeID', + 'dateAdded', + 'libraryID', + 'key', + 'version', + 'synced' + ); + + sqlValues.push( + { int: itemTypeID }, + this.dateAdded ? this.dateAdded : Zotero.DB.transactionDateTime, + this.libraryID ? this.libraryID : 0, + key, + this.version ? this.version : 0, + this.synced ? 1 : 0 + ); + + if (isNew) { + sqlColumns.push('dateModified', 'clientDateModified'); + sqlValues.push(Zotero.DB.transactionDateTime, Zotero.DB.transactionDateTime); + } + else { + for each (let field in ['dateModified', 'clientDateModified']) { + switch (field) { + case 'dateModified': + case 'clientDateModified': + let skipFlag = "skip" + field[0].toUpperCase() + field.substr(1) + "Update"; + if (!options[skipFlag]) { + sqlColumns.push(field); + sqlValues.push(Zotero.DB.transactionDateTime); } - }; + break; + } + } + } + + if (isNew) { + sqlColumns.unshift('itemID'); + sqlValues.unshift(parseInt(itemID)); + + var sql = "INSERT INTO items (" + sqlColumns.join(", ") + ") " + + "VALUES (" + sqlValues.map(function () "?").join() + ")"; + var insertID = yield Zotero.DB.queryAsync(sql, sqlValues); + if (!itemID) { + itemID = env.id = insertID; + } + + Zotero.Notifier.trigger('add', 'item', itemID); + } + else { + var sql = "UPDATE items SET " + sqlColumns.join("=?, ") + "=? WHERE itemID=?"; + sqlValues.push(parseInt(itemID)); + yield Zotero.DB.queryAsync(sql, sqlValues); + + var notifierData = {}; + notifierData[itemID] = { changed: this._previousData }; + Zotero.Notifier.trigger('modify', 'item', itemID, notifierData); + } + + // + // ItemData + // + if (this._changed.itemData) { + let del = []; + + let valueSQL = "SELECT valueID FROM itemDataValues WHERE value=?"; + let insertValueSQL = "INSERT INTO itemDataValues VALUES (?,?)"; + let replaceSQL = "REPLACE INTO itemData VALUES (?,?,?)"; + + for (let fieldID in this._changed.itemData) { + fieldID = parseInt(fieldID); + let value = this.getField(fieldID, true); + + // If field changed and is empty, mark row for deletion + if (!value) { + del.push(fieldID); + continue; + } + + if (Zotero.ItemFields.getID('accessDate') == fieldID + && (this.getField(fieldID)) == 'CURRENT_TIMESTAMP') { + value = Zotero.DB.transactionDateTime; + } + + let valueID = yield Zotero.DB.valueQueryAsync(valueSQL, [value], { debug: true }) + if (!valueID) { + valueID = yield Zotero.ID.get('itemDataValues'); + yield Zotero.DB.queryAsync(insertValueSQL, [valueID, value], { debug: false }); + } + + yield Zotero.DB.queryAsync(replaceSQL, [itemID, fieldID, valueID], { debug: false }); + } + + // Delete blank fields + if (del.length) { + sql = 'DELETE from itemData WHERE itemID=? AND ' + + 'fieldID IN (' + del.map(function () '?').join() + ')'; + yield Zotero.DB.queryAsync(sql, [itemID].concat(del)); + } + } + + // + // Creators + // + if (this._changed.creators) { + for (let orderIndex in this._changed.creators) { + orderIndex = parseInt(orderIndex); + + if (isNew) { + Zotero.debug('Adding creator in position ' + orderIndex, 4); + } + else { + Zotero.debug('Creator ' + orderIndex + ' has changed', 4); + } + + let creatorData = this.getCreator(orderIndex); + // If no creator in this position, just remove the item-creator association + if (!creatorData) { + let sql = "DELETE FROM itemCreators WHERE itemID=? AND orderIndex=?"; + yield Zotero.DB.queryAsync(sql, [itemID, orderIndex]); + Zotero.Prefs.set('purge.creators', true); + continue; + } + + let previousCreatorID = this._previousData.creators[orderIndex] + ? this._previousData.creators[orderIndex].id + : false; + let newCreatorID = yield Zotero.Creators.getIDFromData(creatorData, true); + + // If there was previously a creator at this position and it's different from + // the new one, the old one might need to be purged. + if (previousCreatorID && previousCreatorID != newCreatorID) { + Zotero.Prefs.set('purge.creators', true); + } + + let sql = "INSERT OR REPLACE INTO itemCreators " + + "(itemID, creatorID, creatorTypeID, orderIndex) VALUES (?, ?, ?, ?)"; + yield Zotero.DB.queryAsync( + sql, + [ + itemID, + newCreatorID, + creatorData.creatorTypeID, + orderIndex + ] + ); + } + } + + // Parent item + let parentItem = this.parentKey; + parentItem = parentItem ? Zotero.Items.getByLibraryAndKey(this.libraryID, parentItem) : null; + if (this._changed.parentKey) { + if (isNew) { + if (!parentItem) { + // TODO: clear caches? + let msg = this._parentKey + " is not a valid item key"; + throw new Zotero.Error(msg, "MISSING_OBJECT"); + } + + let newParentItemNotifierData = {}; + //newParentItemNotifierData[newParentItem.id] = {}; + Zotero.Notifier.trigger('modify', 'item', parentItem.id, newParentItemNotifierData); + + switch (Zotero.ItemTypes.getName(itemTypeID)) { + case 'note': + case 'attachment': + reloadParentChildItems[parentItem.id] = true; + break; + } } else { - var transactionOptions = null; + let type = Zotero.ItemTypes.getName(itemTypeID); + let Type = type[0].toUpperCase() + type.substr(1); + + if (this._parentKey) { + if (!parentItem) { + // TODO: clear caches + let msg = "Cannot set source to invalid item " + this._parentKey; + throw new Zotero.Error(msg, "MISSING_OBJECT"); + } + + let newParentItemNotifierData = {}; + //newParentItemNotifierData[newParentItem.id] = {}; + Zotero.Notifier.trigger('modify', 'item', parentItem.id, newParentItemNotifierData); + } + + var oldParentKey = this._previousData.parentKey; + if (oldParentKey) { + var oldParentItem = Zotero.Items.getByLibraryAndKey(this.libraryID, oldParentKey); + if (oldParentItem) { + let oldParentItemNotifierData = {}; + //oldParentItemNotifierData[oldParentItem.id] = {}; + Zotero.Notifier.trigger('modify', 'item', oldParentItem.id, oldParentItemNotifierData); + } + else { + Zotero.debug("Old source item " + oldParentKey + + " didn't exist in Zotero.Item.save()", 2); + } + } + + // If this was an independent item, remove from any collections + // where it existed previously and add parent instead + if (!oldParentKey) { + let sql = "SELECT collectionID FROM collectionItems WHERE itemID=?"; + let changedCollections = yield Zotero.DB.columnQueryAsync(sql, this.id); + if (changedCollections) { + for (let i=0; i wrapper if not present - if (!noteText.match(/^
[\s\S]*<\/div>$/)) { - // Keep consistent with getNote() - noteText = '
' + noteText + '
'; - } - - let params = [ - parent ? parent : null, - noteText, - this._noteTitle ? this._noteTitle : '' - ]; - let sql = "SELECT COUNT(*) FROM itemNotes WHERE itemID=?"; - if (yield Zotero.DB.valueQueryAsync(sql, itemID)) { - sql = "UPDATE itemNotes SET parentItemID=?, note=?, title=? WHERE itemID=?"; - params.push(itemID); - } - else { - sql = "INSERT INTO itemNotes " - + "(itemID, parentItemID, note, title) VALUES (?,?,?,?)"; - params.unshift(itemID); - } - yield Zotero.DB.queryAsync(sql, params); - - if (parentItem) { - reloadParentChildItems[parentItem.id] = true; - } - } - - // - // Attachment - // - if (!isNew) { - // If attachment title changes, update parent attachments - if (this._changed.itemData && this._changed.itemData[110] && this.isAttachment() && parentItem) { - reloadParentChildItems[parentItem.id] = true; - } - } - - if (this.isAttachment() || this._changed.attachmentData) { - let sql = "REPLACE INTO itemAttachments (itemID, parentItemID, linkMode, " - + "contentType, charsetID, path, syncState) VALUES (?,?,?,?,?,?,?)"; - let parent = this.parentID; - let linkMode = this.attachmentLinkMode; - let contentType = this.attachmentContentType; - let charsetID = Zotero.CharacterSets.getID(this.attachmentCharset); - let path = this.attachmentPath; - let syncState = this.attachmentSyncState; - - if (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) { - // Save attachment within attachment base directory as relative path - if (Zotero.Prefs.get('saveRelativeAttachmentPath')) { - path = Zotero.Attachments.getBaseDirectoryRelativePath(path); - } - // If possible, convert relative path to absolute - else { - let file = Zotero.Attachments.resolveRelativePath(path); - if (file) { - path = file.persistentDescriptor; - } - } - } - - let params = [ - itemID, - parent ? parent : null, - { int: linkMode }, - contentType ? { string: contentType } : null, - charsetID ? { int: charsetID } : null, - path ? { string: path } : null, - syncState ? { int: syncState } : 0 - ]; - yield Zotero.DB.queryAsync(sql, params); - - // Clear cached child attachments of the parent - if (!isNew && parentItem) { - reloadParentChildItems[parentItem.id] = true; - } - } - - // Tags - if (this._changed.tags) { - let oldTags = this._previousData.tags; - let newTags = this._tags; - - // Convert to individual JSON objects, diff, and convert back - let oldTagsJSON = oldTags.map(function (x) JSON.stringify(x)); - let newTagsJSON = newTags.map(function (x) JSON.stringify(x)); - let toAdd = Zotero.Utilities.arrayDiff(newTagsJSON, oldTagsJSON) - .map(function (x) JSON.parse(x)); - let toRemove = Zotero.Utilities.arrayDiff(oldTagsJSON, newTagsJSON) - .map(function (x) JSON.parse(x));; - - for (let i=0; i wrapper if not present + if (!noteText.match(/^
[\s\S]*<\/div>$/)) { + // Keep consistent with getNote() + noteText = '
' + noteText + '
'; + } + + let params = [ + parent ? parent : null, + noteText, + this._noteTitle ? this._noteTitle : '' + ]; + let sql = "SELECT COUNT(*) FROM itemNotes WHERE itemID=?"; + if (yield Zotero.DB.valueQueryAsync(sql, itemID)) { + sql = "UPDATE itemNotes SET parentItemID=?, note=?, title=? WHERE itemID=?"; + params.push(itemID); + } + else { + sql = "INSERT INTO itemNotes " + + "(itemID, parentItemID, note, title) VALUES (?,?,?,?)"; + params.unshift(itemID); + } + yield Zotero.DB.queryAsync(sql, params); + + if (parentItem) { + reloadParentChildItems[parentItem.id] = true; + } + } + + // + // Attachment + // + if (!isNew) { + // If attachment title changes, update parent attachments + if (this._changed.itemData && this._changed.itemData[110] && this.isAttachment() && parentItem) { + reloadParentChildItems[parentItem.id] = true; + } + } + + if (this.isAttachment() || this._changed.attachmentData) { + let sql = "REPLACE INTO itemAttachments (itemID, parentItemID, linkMode, " + + "contentType, charsetID, path, syncState) VALUES (?,?,?,?,?,?,?)"; + let parent = this.parentID; + let linkMode = this.attachmentLinkMode; + let contentType = this.attachmentContentType; + let charsetID = Zotero.CharacterSets.getID(this.attachmentCharset); + let path = this.attachmentPath; + let syncState = this.attachmentSyncState; + + if (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) { + // Save attachment within attachment base directory as relative path + if (Zotero.Prefs.get('saveRelativeAttachmentPath')) { + path = Zotero.Attachments.getBaseDirectoryRelativePath(path); + } + // If possible, convert relative path to absolute + else { + let file = Zotero.Attachments.resolveRelativePath(path); + if (file) { + path = file.persistentDescriptor; + } + } + } + + let params = [ + itemID, + parent ? parent : null, + { int: linkMode }, + contentType ? { string: contentType } : null, + charsetID ? { int: charsetID } : null, + path ? { string: path } : null, + syncState ? { int: syncState } : 0 + ]; + yield Zotero.DB.queryAsync(sql, params); + + // Clear cached child attachments of the parent + if (!isNew && parentItem) { + reloadParentChildItems[parentItem.id] = true; + } + } + + // Tags + if (this._changed.tags) { + let oldTags = this._previousData.tags; + let newTags = this._tags; + + // Convert to individual JSON objects, diff, and convert back + let oldTagsJSON = oldTags.map(function (x) JSON.stringify(x)); + let newTagsJSON = newTags.map(function (x) JSON.stringify(x)); + let toAdd = Zotero.Utilities.arrayDiff(newTagsJSON, oldTagsJSON) + .map(function (x) JSON.parse(x)); + let toRemove = Zotero.Utilities.arrayDiff(oldTagsJSON, newTagsJSON) + .map(function (x) JSON.parse(x));; + + for (let i=0; i