commit eb79a0f65946e9ab03c315f98ab2da4d3f34928f parent 6709555dd91c8157f0ad62bb8660c462ab6a7a22 Author: Dan Stillman <dstillman@zotero.org> Date: Wed, 28 Jan 2009 21:25:06 +0000 Addresses #513, Deleted Items folder - Still experimental, but committing for testing - Sync conflicts with deleted items aren't yet supported Unrelated: deprecated ZoteroPane.deleteSelectedItem() in favor of more accurately named deleteSelectedItems() Diffstat:
16 files changed, 359 insertions(+), 57 deletions(-)
diff --git a/chrome/content/zotero/bindings/noteeditor.xml b/chrome/content/zotero/bindings/noteeditor.xml @@ -146,13 +146,17 @@ var parentbox = this._id('citeLabel'); var textbox = this._id('noteField'); + var textboxReadOnly = this._id('noteFieldReadOnly'); var button = this._id('goButton'); if (this.editable) { - textbox.removeAttribute('readonly'); + textbox.hidden = false; + textboxReadOnly.hidden = true; } else { - textbox.setAttribute('readonly', 'true'); + textbox.hidden = true; + textboxReadOnly.hidden = false; + textbox = textboxReadOnly; } //var scrollPos = textbox.inputField.scrollTop; @@ -361,7 +365,10 @@ <content> <xul:vbox xbl:inherits="flex"> <xul:label id="citeLabel"/> - <xul:textbox id="noteField" type="styled" mode="note" timeout="1000" flex="1"/> + <xul:textbox id="noteField" type="styled" mode="note" + timeout="1000" flex="1" hidden="true"/> + <xul:textbox id="noteFieldReadOnly" type="styled" mode="note" + readonly="true" flex="1" hidden="true"/> <xul:hbox id="linksbox" hidden="true"> <xul:linksbox id="links" flex="1"/> </xul:hbox> diff --git a/chrome/content/zotero/itemPane.js b/chrome/content/zotero/itemPane.js @@ -58,6 +58,7 @@ var ZoteroItemPane = new function() { return; } + _deck = document.getElementById('zotero-view-item'); _itemBox = document.getElementById('zotero-editpane-item-box'); _notesList = document.getElementById('zotero-editpane-dynamic-notes'); _notesLabel = document.getElementById('zotero-editpane-notes-label'); @@ -76,7 +77,7 @@ var ZoteroItemPane = new function() { // Force blur() when clicking off a textbox to another item in middle // pane, since for some reason it's not being called automatically if (_itemBeingEdited && _itemBeingEdited != thisItem) { - switch (_tabs.selectedIndex) { + switch (_deck.selectedIndex) { // Info case 0: // TODO: fix @@ -100,7 +101,7 @@ var ZoteroItemPane = new function() { _itemBeingEdited = thisItem; _loaded = {}; - loadPane(_tabs.selectedIndex, mode); + loadPane(_deck.selectedIndex, mode); } diff --git a/chrome/content/zotero/overlay.js b/chrome/content/zotero/overlay.js @@ -52,7 +52,6 @@ var ZoteroPane = new function() this.itemSelected = itemSelected; this.reindexItem = reindexItem; this.duplicateSelectedItem = duplicateSelectedItem; - this.deleteSelectedItem = deleteSelectedItem; this.deleteSelectedCollection = deleteSelectedCollection; this.editSelectedCollection = editSelectedCollection; this.copySelectedItemsToClipboard = copySelectedItemsToClipboard; @@ -552,7 +551,7 @@ var ZoteroPane = new function() event.keyCode == event.DOM_VK_DELETE) { // If Cmd or Ctrl delete, delete from Library (with prompt) var fromDB = event.metaKey || (!Zotero.isMac && event.ctrlKey); - ZoteroPane.deleteSelectedItem(fromDB); + ZoteroPane.deleteSelectedItems(fromDB); event.preventDefault(); return; } @@ -827,11 +826,21 @@ var ZoteroPane = new function() return; } + // Display restore button if items selected in Trash + if (this.itemsView && this.itemsView.selection.count) { + document.getElementById('zotero-item-restore-button').hidden + = !this.itemsView._itemGroup.isTrash(); + } + + var tabs = document.getElementById('zotero-view-tabs'); + if (this.itemsView && this.itemsView.selection.count == 1 && this.itemsView.selection.currentIndex != -1) { var item = this.itemsView._getItemAtRow(this.itemsView.selection.currentIndex); if(item.ref.isNote()) { + tabs.hidden = true; + var noteEditor = document.getElementById('zotero-note-editor'); noteEditor.mode = this.itemsView.readOnly ? 'view' : 'edit'; @@ -846,19 +855,27 @@ var ZoteroPane = new function() noteEditor.enableUndo(); - document.getElementById('zotero-view-note-button').setAttribute('noteID',item.ref.id); - if(item.ref.getSource()) - { - document.getElementById('zotero-view-note-button').setAttribute('sourceID',item.ref.getSource()); + var viewButton = document.getElementById('zotero-view-note-button'); + if (this.itemsView.readOnly) { + viewButton.hidden = true; } - else - { - document.getElementById('zotero-view-note-button').removeAttribute('sourceID'); + else { + viewButton.hidden = false; + viewButton.setAttribute('noteID', item.ref.id); + if (item.ref.getSource()) { + viewButton.setAttribute('sourceID', item.ref.getSource()); + } + else { + viewButton.removeAttribute('sourceID'); + } } + document.getElementById('zotero-item-pane-content').selectedIndex = 2; } else if(item.ref.isAttachment()) { + tabs.hidden = true; + var attachmentBox = document.getElementById('zotero-attachment-box'); attachmentBox.mode = this.itemsView.readOnly ? 'view' : 'edit'; attachmentBox.item = item.ref; @@ -869,12 +886,22 @@ var ZoteroPane = new function() // Regular item else { - ZoteroItemPane.viewItem(item.ref, this.itemsView.readOnly ? 'view' : false); document.getElementById('zotero-item-pane-content').selectedIndex = 1; + if (this.itemsView.readOnly) { + document.getElementById('zotero-view-item').selectedIndex = 0; + ZoteroItemPane.viewItem(item.ref, 'view'); + tabs.hidden = true; + } + else { + ZoteroItemPane.viewItem(item.ref); + tabs.selectedIndex = document.getElementById('zotero-view-item').selectedIndex; + tabs.hidden = false; + } } } else { + tabs.hidden = true; document.getElementById('zotero-item-pane-content').selectedIndex = 0; var label = document.getElementById('zotero-view-selected-label'); @@ -927,11 +954,15 @@ var ZoteroPane = new function() } + this.deleteSelectedItem = function () { + Zotero.debug("ZoteroPane.deleteSelectedItem() is deprecated -- use ZoteroPane.deleteSelectedItems()"); + this.deleteSelectedItems(); + } + /* * _force_ deletes item from DB even if removing from a collection or search */ - function deleteSelectedItem(force) - { + this.deleteSelectedItems = function (force) { if (this.itemsView && this.itemsView.selection.count > 0) { if (!force){ if (this.itemsView._itemGroup.isCollection()) { @@ -997,6 +1028,35 @@ var ZoteroPane = new function() } } + + this.restoreSelectedItems = function () { + var items = this.getSelectedItems(); + if (!items) { + return; + } + + Zotero.DB.beginTransaction(); + for (var i=0; i<items.length; i++) { + items[i].deleted = false; + items[i].save(); + } + Zotero.DB.commitTransaction(); + } + + + this.emptyTrash = function () { + var prompt = Components.classes["@mozilla.org/network/default-prompt;1"] + .getService(Components.interfaces.nsIPrompt); + + var result = prompt.confirm("", + Zotero.getString('pane.collections.emptyTrash') + "\n\n" + + Zotero.getString('general.actionCannotBeUndone')); + if (result) { + Zotero.Items.emptyTrash(); + } + } + + function editSelectedCollection() { if (this.collectionsView.selection.count > 0) { @@ -1253,14 +1313,14 @@ var ZoteroPane = new function() exportCollection: 7, createBibCollection: 8, exportFile: 9, - loadReport: 10 + loadReport: 10, + emptyTrash: 11 }; // Collection if (this.collectionsView.selection.count == 1 && this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex).isCollection()) { - var hide = [m.newCollection, m.newSavedSearch, m.exportFile]; var show = [m.newSubcollection, m.sep1, m.editSelectedCollection, m.removeCollection, m.sep2, m.exportCollection, m.createBibCollection, m.loadReport]; if (this.itemsView.rowCount>0) { @@ -1275,6 +1335,7 @@ var ZoteroPane = new function() var disable = [m.exportCollection, m.createBibCollection, m.loadReport]; } + // Adjust labels menu.childNodes[m.editSelectedCollection].setAttribute('label', Zotero.getString('pane.collections.menu.rename.collection')); menu.childNodes[m.removeCollection].setAttribute('label', Zotero.getString('pane.collections.menu.remove.collection')); menu.childNodes[m.exportCollection].setAttribute('label', Zotero.getString('pane.collections.menu.export.collection')); @@ -1284,7 +1345,6 @@ var ZoteroPane = new function() // Saved Search else if (this.collectionsView.selection.count == 1 && this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex).isSearch()) { - var hide = [m.newCollection, m.newSavedSearch, m.newSubcollection, m.sep1, m.exportFile] var show = [m.editSelectedCollection, m.removeCollection, m.sep2, m.exportCollection, m.createBibCollection, m.loadReport]; @@ -1296,17 +1356,21 @@ var ZoteroPane = new function() var disable = [m.exportCollection, m.createBibCollection, m.loadReport]; } + // Adjust labels menu.childNodes[m.editSelectedCollection].setAttribute('label', Zotero.getString('pane.collections.menu.edit.savedSearch')); menu.childNodes[m.removeCollection].setAttribute('label', Zotero.getString('pane.collections.menu.remove.savedSearch')); menu.childNodes[m.exportCollection].setAttribute('label', Zotero.getString('pane.collections.menu.export.savedSearch')); menu.childNodes[m.createBibCollection].setAttribute('label', Zotero.getString('pane.collections.menu.createBib.savedSearch')); menu.childNodes[m.loadReport].setAttribute('label', Zotero.getString('pane.collections.menu.generateReport.savedSearch')); } + // Trash + else if (this.collectionsView.selection.count == 1 && + this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex).isTrash()) { + var show = [m.emptyTrash]; + } // Library else { - var hide = [m.newSubcollection, m.editSelectedCollection, m.removeCollection, m.sep2, - m.exportCollection, m.createBibCollection, m.loadReport]; var show = [m.newCollection, m.newSavedSearch, m.sep1, m.exportFile]; } @@ -1320,9 +1384,9 @@ var ZoteroPane = new function() menu.childNodes[enable[i]].setAttribute('disabled', false); } - for (var i in hide) - { - menu.childNodes[hide[i]].setAttribute('hidden', true); + // Hide all items by default + for each(var pos in m) { + menu.childNodes[pos].setAttribute('hidden', true); } for (var i in show) diff --git a/chrome/content/zotero/overlay.xul b/chrome/content/zotero/overlay.xul @@ -90,6 +90,7 @@ <menuitem oncommand="Zotero_File_Interface.bibliographyFromCollection();"/> <menuitem label="&zotero.toolbar.export.label;" oncommand="Zotero_File_Interface.exportFile()"/> <menuitem oncommand="Zotero_Report_Interface.loadCollectionReport()"/> + <menuitem label="&zotero.toolbar.emptyTrash.label;" oncommand="ZoteroPane.emptyTrash();"/> </popup> <popup id="zotero-itemmenu" onpopupshowing="ZoteroPane.buildItemContextMenu();"> <menuitem label="&zotero.items.menu.showInLibrary;" oncommand="ZoteroPane.selectItem(this.parentNode.getAttribute('itemID'), true)"/> @@ -99,8 +100,8 @@ <menuitem label="&zotero.items.menu.attach.link;" oncommand="ZoteroPane.addAttachmentFromPage(true, this.parentNode.getAttribute('itemID'));"/> <menuseparator/> <menuitem label="&zotero.items.menu.duplicateItem;" oncommand="ZoteroPane.duplicateSelectedItem();"/> - <menuitem oncommand="ZoteroPane.deleteSelectedItem();"/> - <menuitem oncommand="ZoteroPane.deleteSelectedItem(true);"/> + <menuitem oncommand="ZoteroPane.deleteSelectedItems();"/> + <menuitem oncommand="ZoteroPane.deleteSelectedItems(true);"/> <menuseparator/> <menuitem oncommand="Zotero_File_Interface.exportItems();"/> <menuitem oncommand="Zotero_File_Interface.bibliographyFromItems();"/> @@ -358,6 +359,9 @@ <toolbarbutton id="zotero-tb-fullscreen" tooltiptext="&zotero.toolbar.fullscreen.tooltip;" oncommand="ZoteroPane.fullScreen();"/> <toolbarbutton class="tabs-closebutton" oncommand="ZoteroPane.toggleDisplay()"/> </hbox> + <!-- TODO: localize --> + <button id="zotero-item-restore-button" label="Restore to Library" + oncommand="ZoteroPane.restoreSelectedItems()" hidden="true"/> <groupbox flex="1"> <caption> <tabs id="zotero-view-tabs" onselect="document.getElementById('zotero-view-item').selectedIndex = this.selectedIndex;" hidden="true"> @@ -368,7 +372,7 @@ <tab label="&zotero.tabs.related.label;"/> </tabs> </caption> - <deck id="zotero-item-pane-content" selectedIndex="0" flex="1" onselect="document.getElementById('zotero-view-tabs').setAttribute('hidden',(this.selectedIndex != 1));"> + <deck id="zotero-item-pane-content" selectedIndex="0" flex="1"> <box pack="center" align="center"> <label id="zotero-view-selected-label"/> </box> diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js @@ -164,6 +164,11 @@ Zotero.CollectionTreeView.prototype.refresh = function() } } + var deletedItems = Zotero.Items.getDeleted(); + if (deletedItems) { + this._showItem(new Zotero.ItemGroup('trash', null), 0, this._dataItems.length); + } + this._refreshHashMap(); // Update the treebox's row count @@ -1037,6 +1042,12 @@ Zotero.ItemGroup.prototype.isShare = function() return this.type == 'share'; } +Zotero.ItemGroup.prototype.isTrash = function() +{ + return this.type == 'trash'; +} + + Zotero.ItemGroup.prototype.getName = function() { if (this.isCollection()) { @@ -1051,6 +1062,9 @@ Zotero.ItemGroup.prototype.getName = function() else if (this.isShare()) { return this.ref.name; } + else if (this.isTrash()) { + return Zotero.getString('pane.collections.trash'); + } else { return ""; } @@ -1098,12 +1112,18 @@ Zotero.ItemGroup.prototype.getSearchObject = function() { } includeScopeChildren = true; } + else if (this.isTrash()) { + s.addCondition('deleted', 'true'); + } else if (!this.isSearch()) { throw ('Invalid search mode in Zotero.ItemGroup.getSearchObject()'); } // Create the outer (filter) search var s2 = new Zotero.Search(); + if (this.isTrash()) { + s2.addCondition('deleted', 'true'); + } s2.setScope(s, includeScopeChildren); if (this.searchText) { diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js @@ -75,12 +75,14 @@ Zotero.Item.prototype._init = function () { this._changedPrimaryData = false; this._changedItemData = false; this._changedCreators = false; + this._changedDeleted = false; this._changedNote = false; this._changedSource = false; this._changedAttachmentData = false; this._previousData = null; + this._deleted = null; this._noteTitle = null; this._noteText = null; this._noteAccessTime = null; @@ -341,8 +343,9 @@ Zotero.Item.prototype.loadFromRow = function(row, reload) { Zotero.Item.prototype.hasChanged = function() { return !!(this._changed || this._changedPrimaryData - || this._changedCreators || this._changedItemData + || this._changedCreators + || this._changedDeleted || this._changedNote || this._changedSource || this._changedAttachmentData); @@ -919,6 +922,44 @@ Zotero.Item.prototype.removeCreator = function(orderIndex) { } +Zotero.Item.prototype.__defineGetter__('deleted', function () { + if (this._deleted !== null) { + return this._deleted; + } + + if (!this.id) { + return ''; + } + + var sql = "SELECT COUNT(*) FROM deletedItems WHERE itemID=?"; + var deleted = !!Zotero.DB.valueQuery(sql, this.id); + this._deleted = deleted; + return deleted; +}); + + +Zotero.Item.prototype.__defineSetter__('deleted', function (val) { + Zotero.debug('setting deleted'); + Zotero.debug(val); + if (!this.id) { + Zotero.debug("Deleted state not set on item without id"); + return; + } + + var deleted = !!val; + + if (this.deleted == deleted) { + Zotero.debug("Deleted state hasn't changed for item " + this.id); + return; + } + + if (!this._changedDeleted) { + this._changedDeleted = true; + } + this._deleted = deleted; +}); + + Zotero.Item.prototype.addRelatedItem = function (itemID) { var parsedInt = parseInt(itemID); if (parsedInt != itemID) { @@ -1033,6 +1074,7 @@ Zotero.Item.prototype.save = function() { Zotero.DB.query("UPDATE itemTags SET itemID=? WHERE itemID=?", params); Zotero.DB.query("UPDATE fulltextItemWords SET itemID=? WHERE itemID=?", params); Zotero.DB.query("UPDATE fulltextItems SET itemID=? WHERE itemID=?", params); + Zotero.DB.query("UPDATE deletedItems SET itemID=? WHERE itemID=?", params); Zotero.DB.query("UPDATE annotations SET itemID=? WHERE itemID=?", params); Zotero.DB.query("UPDATE highlights SET itemID=? WHERE itemID=?", params); @@ -1237,6 +1279,17 @@ Zotero.Item.prototype.save = function() { } + if (this._changedDeleted) { + if (this.deleted) { + sql = "REPLACE INTO deletedItems (itemID) VALUES (?)"; + } + else { + sql = "DELETE FROM deletedItems WHERE itemID=?"; + } + Zotero.DB.query(sql, itemID); + } + + // Note if (this.isNote() || this._changedNote) { sql = "INSERT INTO itemNotes " @@ -1393,6 +1446,7 @@ Zotero.Item.prototype.save = function() { Zotero.DB.query(sql, sqlValues); + // // ItemData // @@ -1573,6 +1627,17 @@ Zotero.Item.prototype.save = function() { } + if (this._changedDeleted) { + if (this.deleted) { + sql = "REPLACE INTO deletedItems (itemID) VALUES (?)"; + } + else { + sql = "DELETE FROM deletedItems WHERE itemID=?"; + } + Zotero.DB.query(sql, this.id); + } + + // Note if (this._changedNote) { if (this._noteText === null || this._noteTitle === null) { @@ -1790,6 +1855,13 @@ Zotero.Item.prototype.save = function() { this._key = key; } + if (this._changedDeleted) { + Zotero.Notifier.trigger('refresh', 'collection', 0); + if (this._deleted) { + Zotero.Notifier.trigger('trash', 'item', this.id); + } + } + Zotero.Items.reload(this.id); if (isNew) { @@ -3244,6 +3316,7 @@ Zotero.Item.prototype.erase = function(deleteChildren) { Zotero.DB.query('DELETE FROM annotations WHERE itemID=?', this.id); Zotero.DB.query('DELETE FROM highlights WHERE itemID=?', this.id); + Zotero.DB.query('DELETE FROM deletedItems WHERE itemID=?', this.id); Zotero.DB.query('DELETE FROM itemCreators WHERE itemID=?', this.id); Zotero.DB.query('DELETE FROM itemNotes WHERE itemID=?', this.id); Zotero.DB.query('DELETE FROM itemAttachments WHERE itemID=?', this.id); @@ -3467,6 +3540,10 @@ Zotero.Item.prototype.serialize = function(mode) { } } + // Deleted items flag + if (this.deleted) { + arr.deleted = true; + } if (this.isRegularItem()) { // Creators diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js @@ -106,6 +106,24 @@ Zotero.Items = new function() { } + + /** + * Return items marked as deleted + * + * @param {Boolean} asIDs Return itemIDs instead of + * Zotero.Item objects + * @return {Zotero.Item[]|Integer[]} + */ + this.getDeleted = function (asIDs) { + var sql = "SELECT itemID FROM deletedItems"; + var ids = Zotero.DB.columnQuery(sql); + if (asIDs) { + return ids; + } + return this.get(ids); + } + + /* * Returns all items in the database * @@ -320,6 +338,45 @@ Zotero.Items = new function() { } + this.trash = function (ids) { + ids = Zotero.flattenArguments(ids); + + Zotero.UnresponsiveScriptIndicator.disable(); + try { + Zotero.DB.beginTransaction(); + for each(var id in ids) { + var item = this.get(id); + if (!item) { + Zotero.debug('Item ' + id + ' does not exist in Items.trash()!', 1); + Zotero.Notifier.trigger('delete', 'item', id); + continue; + } + item.deleted = true; + item.save(); + } + Zotero.DB.commitTransaction(); + } + catch (e) { + Zotero.DB.rollbackTransaction(); + throw (e); + } + finally { + Zotero.UnresponsiveScriptIndicator.enable(); + } + } + + + this.emptyTrash = function () { + Zotero.DB.beginTransaction(); + var deletedIDs = this.getDeleted(true); + if (deletedIDs) { + this.erase(deletedIDs, true); + } + Zotero.Notifier.trigger('refresh', 'collection', 0); + Zotero.DB.commitTransaction(); + } + + /** * Delete item(s) from database and clear from internal array * diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js @@ -232,7 +232,7 @@ Zotero.ItemTreeView.prototype.refresh = function() Zotero.ItemTreeView.prototype.__defineGetter__('readOnly', function () { - if (this._itemGroup.isShare()) { + if (this._itemGroup.isTrash() || this._itemGroup.isShare()) { return true; } return false; @@ -309,7 +309,7 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData) } if ((action == 'remove' && !this._itemGroup.isLibrary()) - || action == 'delete' || action == 'id-change') { + || action == 'delete' || action == 'id-change' || action == 'trash') { // We only care about the old ids if (action == 'id-change') { @@ -323,7 +323,7 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData) var rows = []; for(var i=0, len=ids.length; i<len; i++) { - if (action == 'delete' || action == 'id-change' || + if (action == 'delete' || action == 'trash' || action == 'id-change' || !this._itemGroup.ref.hasItem(ids[i])) { // Row might already be gone (e.g. if this is a child and // 'modify' was sent to parent) @@ -350,12 +350,11 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData) madeChanges = true; sort = true; } - } else if (action == 'modify') { - // If saved search, just re-run search - if (this._itemGroup.isSearch()) + // If trash or saved search, just re-run search + if (this._itemGroup.isTrash() || this._itemGroup.isSearch()) { this.refresh(); madeChanges = true; @@ -410,6 +409,11 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData) // modify comes in after a delete continue; } + // Deleted items get a modify that we have to ignore when + // not viewing the trash + if (item.deleted) { + continue; + } if(item.isRegularItem() || !item.getSource()) { //most likely, the note or attachment's parent was removed. @@ -436,9 +440,8 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData) } else if(action == 'add') { - // If saved search, just re-run search - if (this._itemGroup.isSearch()) - { + // If saved search or trash, just re-run search + if (this._itemGroup.isSearch() || this._itemGroup.isTrash()) { this.refresh(); madeChanges = true; sort = true; @@ -1167,10 +1170,11 @@ Zotero.ItemTreeView.prototype.getSelectedItems = function(asIDs) } -/* +/** * Delete the selection * - * _force_ deletes item from DB even if removing from a collection + * @param {Boolean} eraseChildren + * @param {Boolean} force Delete item even if removing from a collection */ Zotero.ItemTreeView.prototype.deleteSelection = function(eraseChildren, force) { @@ -1201,11 +1205,14 @@ Zotero.ItemTreeView.prototype.deleteSelection = function(eraseChildren, force) // Erase item(s) from DB if (this._itemGroup.isLibrary() || force) { - Zotero.Items.erase(ids, eraseChildren); + Zotero.Items.trash(ids); } else if (this._itemGroup.isCollection()) { this._itemGroup.ref.removeItems(ids); } + else if (this._itemGroup.isTrash()) { + Zotero.Items.erase(ids, eraseChildren); + } this._treebox.endUpdateBatch(); } diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js @@ -2146,7 +2146,7 @@ Zotero.Schema = new function(){ } } - // // 1.5 Sync Preview 3.6 + // 1.5 Sync Preview 3.6 if (i==47) { Zotero.DB.query("ALTER TABLE syncDeleteLog RENAME TO syncDeleteLogOld"); Zotero.DB.query("DROP INDEX syncDeleteLog_timestamp"); @@ -2155,6 +2155,11 @@ Zotero.Schema = new function(){ Zotero.DB.query("INSERT OR IGNORE INTO syncDeleteLog SELECT syncObjectTypeID, key, timestamp FROM syncDeleteLogOld ORDER BY timestamp DESC"); Zotero.DB.query("DROP TABLE syncDeleteLogOld"); } + + // + if (i==48) { + Zotero.DB.query("CREATE TABLE deletedItems (\n itemID INTEGER PRIMARY KEY,\n dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL\n);"); + } } _updateDBVersion('userdata', toVersion); diff --git a/chrome/content/zotero/xpcom/search.js b/chrome/content/zotero/xpcom/search.js @@ -928,6 +928,10 @@ Zotero.Search.prototype._buildQuery = function(){ // Handle special conditions else { switch (data['name']){ + case 'deleted': + var deleted = this._conditions[i].operator == 'true'; + continue; + case 'noChildren': var noChildren = this._conditions[i]['operator']=='true'; continue; @@ -971,20 +975,19 @@ Zotero.Search.prototype._buildQuery = function(){ } } + // Exclude deleted items by default + sql += " WHERE itemID " + (deleted ? "" : "NOT ") + "IN " + + "(SELECT itemID FROM deletedItems)"; + if (noChildren){ - sql += " WHERE (itemID NOT IN (SELECT itemID FROM itemNotes " + sql += " AND (itemID NOT IN (SELECT itemID FROM itemNotes " + "WHERE sourceItemID IS NOT NULL) AND itemID NOT IN " + "(SELECT itemID FROM itemAttachments " + "WHERE sourceItemID IS NOT NULL))"; } if (this._hasPrimaryConditions) { - if (noChildren){ - sql += " AND "; - } - else { - sql += " WHERE "; - } + sql += " AND "; for each(var condition in conditions){ var skipOperators = false; @@ -1441,12 +1444,10 @@ Zotero.Search.prototype._buildQuery = function(){ } // Keep non-required conditions separate if in ANY mode else if (!condition['required'] && joinMode == 'ANY') { - var nonQSConditions = true; anySQL += condSQL + ' OR '; anySQLParams = anySQLParams.concat(condSQLParams); } else { - var nonQSConditions = true; condSQL += ' AND '; sql += condSQL; sqlParams = sqlParams.concat(condSQLParams); @@ -1460,11 +1461,8 @@ Zotero.Search.prototype._buildQuery = function(){ sql = sql.substring(0, sql.length-4); // remove last ' OR ' sql += ')'; } - else if (nonQSConditions) { - sql = sql.substring(0, sql.length-5); // remove last ' AND ' - } else { - sql = sql.substring(0, sql.length-7); // remove ' WHERE ' + sql = sql.substring(0, sql.length-5); // remove last ' AND ' } // Add on quicksearch conditions @@ -1601,6 +1599,15 @@ Zotero.SearchConditions = new function(){ // Special conditions // + + { + name: 'deleted', + operators: { + true: true, + false: true + } + }, + // Don't include child items { name: 'noChildren', diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js @@ -2535,6 +2535,11 @@ Zotero.Sync.Server.Data = new function() { xml.field += newField; } + // Deleted item flag + if (item.deleted) { + xml.@deleted = '1'; + } + if (item.primary.itemType == 'note' || item.primary.itemType == 'attachment') { if (item.sourceItemID) { xml.@sourceItemID = item.sourceItemID; @@ -2665,6 +2670,10 @@ Zotero.Sync.Server.Data = new function() { } } + // Deleted item flag + var deleted = xmlItem.@deleted.toString(); + item.deleted = (deleted == 'true' || deleted == "1"); + // Item creators var i = 0; for each(var creator in xmlItem.creator) { diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd @@ -61,6 +61,7 @@ <!ENTITY zotero.toolbar.newCollection.label "New Collection..."> <!ENTITY zotero.toolbar.newSubcollection.label "New Subcollection..."> <!ENTITY zotero.toolbar.newSavedSearch.label "New Saved Search..."> +<!ENTITY zotero.toolbar.emptyTrash.label "Empty Trash"> <!ENTITY zotero.toolbar.tagSelector.label "Show/Hide Tag Selector"> <!ENTITY zotero.toolbar.actions.label "Actions"> <!ENTITY zotero.toolbar.import.label "Import..."> diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties @@ -14,6 +14,7 @@ general.errorHasOccurred = An error has occurred. general.restartFirefox = Please restart Firefox. general.restartFirefoxAndTryAgain = Please restart Firefox and try again. general.checkForUpdate = Check for update +general.actionCannotBeUndone = This action cannot be undone. general.install = Install general.updateAvailable = Update Available general.upgrade = Upgrade @@ -50,12 +51,14 @@ startupError = There was an error starting Zotero. pane.collections.delete = Are you sure you want to delete the selected collection? pane.collections.deleteSearch = Are you sure you want to delete the selected search? +pane.collections.emptyTrash = Are you sure you want to permanently remove items in the Trash? pane.collections.newCollection = New Collection pane.collections.name = Enter a name for this collection: pane.collections.newSavedSeach = New Saved Search pane.collections.savedSearchName = Enter a name for this saved search: pane.collections.rename = Rename collection: pane.collections.library = My Library +pane.collections.trash = Trash pane.collections.untitled = Untitled pane.collections.menu.rename.collection = Rename Collection... diff --git a/chrome/skin/default/zotero/treesource-trash.png b/chrome/skin/default/zotero/treesource-trash.png Binary files differ. diff --git a/triggers.sql b/triggers.sql @@ -1,4 +1,4 @@ --- 3 +-- 4 -- Triggers to validate date field DROP TRIGGER IF EXISTS insert_date_field; @@ -807,6 +807,41 @@ CREATE TRIGGER fku_savedSearches_savedSearchID_savedSearchConditions_savedSearch UPDATE savedSearchConditions SET savedSearchID=NEW.savedSearchID WHERE savedSearchID=OLD.savedSearchID; END; + +-- deletedItems/itemID +-- savedSearchConditions/savedSearchID +DROP TRIGGER IF EXISTS fki_deletedItems_itemID_items_itemID; +CREATE TRIGGER fki_deletedItems_itemID_items_itemID + BEFORE INSERT ON deletedItems + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "deletedItems" violates foreign key constraint "fki_deletedItems_itemID_items_itemID"') + WHERE (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_deletedItems_itemID_items_itemID; +CREATE TRIGGER fku_deletedItems_itemID_items_itemID + BEFORE UPDATE OF itemID ON deletedItems + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "deletedItems" violates foreign key constraint "fku_deletedItems_itemID_items_itemID"') + WHERE (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_deletedItems_itemID_items_itemID; +CREATE TRIGGER fkd_deletedItems_itemID_items_itemID + BEFORE DELETE ON items + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "items" violates foreign key constraint "fkd_deletedItems_itemID_items_itemID"') + WHERE (SELECT COUNT(*) FROM deletedItems WHERE itemID = OLD.itemID) > 0; + END; + +DROP TRIGGER IF EXISTS fku_items_itemID_deletedItems_itemID; +CREATE TRIGGER fku_items_itemID_deletedItems_itemID + AFTER UPDATE OF itemID ON items + FOR EACH ROW BEGIN + UPDATE deletedItems SET itemID=NEW.itemID WHERE itemID=OLD.itemID; + END; + + -- syncDeleteLog/syncObjectTypeID DROP TRIGGER IF EXISTS fki_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID; CREATE TRIGGER fki_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID diff --git a/userdata.sql b/userdata.sql @@ -1,4 +1,4 @@ --- 47 +-- 48 -- This file creates tables containing user-specific data -- any changes made -- here must be mirrored in transition steps in schema.js::_migrateSchema() @@ -171,6 +171,11 @@ CREATE TABLE savedSearchConditions ( FOREIGN KEY (savedSearchID) REFERENCES savedSearches(savedSearchID) ); +CREATE TABLE deletedItems ( + itemID INTEGER PRIMARY KEY, + dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL +); + CREATE TABLE fulltextItems ( itemID INTEGER PRIMARY KEY, version INT,