diff --git a/chrome/content/zotero/xpcom/sync/syncRunner.js b/chrome/content/zotero/xpcom/sync/syncRunner.js index ce65901b5b..6d30dadca4 100644 --- a/chrome/content/zotero/xpcom/sync/syncRunner.js +++ b/chrome/content/zotero/xpcom/sync/syncRunner.js @@ -32,6 +32,7 @@ if (!Zotero.Sync) { // Initialized as Zotero.Sync.Runner in zotero.js Zotero.Sync.Runner_Module = function (options = {}) { const stopOnError = false; + const HTML_NS = 'http://www.w3.org/1999/xhtml'; Zotero.defineProperty(this, 'enabled', { get: () => { @@ -84,7 +85,9 @@ Zotero.Sync.Runner_Module = function (options = {}) { var _lastSyncStatus; var _currentSyncStatusLabel; var _currentLastSyncLabel; + var _currentTooltipMessages; var _errors = []; + var _tooltipMessages = []; Zotero.addShutdownListener(() => this.stop()); @@ -118,6 +121,7 @@ Zotero.Sync.Runner_Module = function (options = {}) { this._sync = Zotero.Promise.coroutine(function* (options) { // Clear message list _errors = []; + _tooltipMessages = []; // Shouldn't be possible because of serial() if (_syncInProgress) { @@ -1033,7 +1037,8 @@ Zotero.Sync.Runner_Module = function (options = {}) { upgrade: 4, // Skip these - animate: -1 + animate: -1, + ignore: -2 }; var state = false; for (let i = 0; i < errors.length; i++) { @@ -1265,20 +1270,31 @@ Zotero.Sync.Runner_Module = function (options = {}) { } // Show warning for unknown data that couldn't be saved else if (e.name && e.name == 'ZoteroInvalidDataError') { - e.message = Zotero.getString( - 'sync.error.invalidDataError', - [ - Zotero.Libraries.get(e.libraryID).name, - Zotero.clientName - ] - ) - + "\n\n" - + Zotero.getString('sync.error.invalidDataError.otherData'); - e.errorType = 'warning'; - e.dialogButtonText = Zotero.getString('general.checkForUpdates'); - e.dialogButtonCallback = () => { - Zotero.openCheckForUpdatesWindow(); - }; + let library = Zotero.Libraries.get(e.libraryID); + let msg = Zotero.getString( + 'sync.error.invalidDataError', + [ + library.name, + Zotero.clientName + ] + ) + + "\n\n" + + Zotero.getString('sync.error.invalidDataError.otherData'); + + // Show warning for My Library + if (library.libraryType == 'user') { + e.message = msg; + e.errorType = 'warning'; + e.dialogButtonText = Zotero.getString('general.checkForUpdates'); + e.dialogButtonCallback = () => { + Zotero.openCheckForUpdatesWindow(); + }; + } + // Otherwise just show in sync button tooltip + else { + _addTooltipMessage(msg); + e.errorType = 'ignore'; + } } }); @@ -1299,6 +1315,7 @@ Zotero.Sync.Runner_Module = function (options = {}) { if (!Array.isArray(errors)) { errors = [errors]; } + errors = errors.filter(o => o.errorType !== 'ignore'); var state = this.getPrimaryErrorType(errors); } @@ -1512,10 +1529,12 @@ Zotero.Sync.Runner_Module = function (options = {}) { if (tooltip) { _currentSyncStatusLabel = tooltip.firstChild.nextSibling; _currentLastSyncLabel = tooltip.firstChild.nextSibling.nextSibling; + _currentTooltipMessages = tooltip.querySelector('.sync-button-tooltip-messages'); } else { _currentSyncStatusLabel = null; _currentLastSyncLabel = null; + _currentTooltipMessages = null; } if (_currentSyncStatusLabel) { _updateSyncStatusLabel(); @@ -1547,7 +1566,12 @@ Zotero.Sync.Runner_Module = function (options = {}) { Zotero.Sync.Data.Local.setAPIKey(); yield client.deleteAPIKey(); }) - + + + function _addTooltipMessage(msg) { + _tooltipMessages.push(msg.replace(/\n+/g, ' ')); + }; + function _updateSyncStatusLabel() { if (_lastSyncStatus) { @@ -1585,6 +1609,19 @@ Zotero.Sync.Runner_Module = function (options = {}) { _currentLastSyncLabel.value = Zotero.getString('sync.status.lastSync') + " " + msg; _currentLastSyncLabel.hidden = false; + + if (_tooltipMessages.length) { + _currentTooltipMessages.textContent = ''; + for (let message of _tooltipMessages) { + let elem = _currentTooltipMessages.ownerDocument.createElementNS(HTML_NS, 'p'); + elem.textContent = message; + _currentTooltipMessages.appendChild(elem); + } + _currentTooltipMessages.hidden = false; + } + else { + _currentTooltipMessages.hidden = true; + } } diff --git a/chrome/content/zotero/zoteroPane.xul b/chrome/content/zotero/zoteroPane.xul index 4d20c0ad9a..c096d56392 100644 --- a/chrome/content/zotero/zoteroPane.xul +++ b/chrome/content/zotero/zoteroPane.xul @@ -214,6 +214,7 @@ +
diff --git a/scss/_zotero-react-client.scss b/scss/_zotero-react-client.scss index 54c74b6657..c99c1f191f 100644 --- a/scss/_zotero-react-client.scss +++ b/scss/_zotero-react-client.scss @@ -27,5 +27,6 @@ @import "components/icons"; @import "components/progressMeter"; @import "components/search"; +@import "components/syncButtonTooltip"; @import "components/tagsBox"; @import "components/tagSelector"; diff --git a/scss/components/_syncButtonTooltip.scss b/scss/components/_syncButtonTooltip.scss new file mode 100644 index 0000000000..b04d3ea2d2 --- /dev/null +++ b/scss/components/_syncButtonTooltip.scss @@ -0,0 +1,8 @@ +.sync-button-tooltip-messages { + max-width: 400px; + + p { + color: gray; + margin: 8px 6px; + } +} \ No newline at end of file diff --git a/test/tests/syncRunnerTest.js b/test/tests/syncRunnerTest.js index 1e9713addf..2dd4dbce0c 100644 --- a/test/tests/syncRunnerTest.js +++ b/test/tests/syncRunnerTest.js @@ -109,6 +109,63 @@ describe("Zotero.Sync.Runner", function () { setHTTPResponse(server, baseURL, response, responses); } + function setDefaultResponses(options = {}) { + var target = options.target || 'users/1'; + var headers = { + "Last-Modified-Version": options.libraryVersion || 5 + }; + var lastLibraryVersion = options.lastLibraryVersion || 4; + setResponse({ + method: "GET", + url: `${target}/settings?since=${lastLibraryVersion}`, + status: 200, + headers, + json: {} + }); + setResponse({ + method: "GET", + url: `${target}/collections?format=versions&since=${lastLibraryVersion}`, + status: 200, + headers, + json: {} + }); + setResponse({ + method: "GET", + url: `${target}/searches?format=versions&since=${lastLibraryVersion}`, + status: 200, + headers, + json: {} + }); + setResponse({ + method: "GET", + url: `${target}/items/top?format=versions&since=${lastLibraryVersion}&includeTrashed=1`, + status: 200, + headers, + json: {} + }); + setResponse({ + method: "GET", + url: `${target}/items?format=versions&since=${lastLibraryVersion}&includeTrashed=1`, + status: 200, + headers, + json: {} + }); + setResponse({ + method: "GET", + url: `${target}/deleted?since=${lastLibraryVersion}`, + status: 200, + headers, + json: {} + }); + setResponse({ + method: "GET", + url: `${target}/fulltext?format=versions`, + status: 200, + headers, + json: {} + }); + } + // // Tests @@ -993,28 +1050,18 @@ describe("Zotero.Sync.Runner", function () { } }); - it("should show the sync error icon on error", function* () { + it("should show the sync error icon on error", async function () { let library = Zotero.Libraries.userLibrary; - library.libraryVersion = 5; - yield library.save(); + library.libraryVersion = 1; + await library.save(); setResponse('keyInfo.fullAccess'); setResponse('userGroups.groupVersionsEmpty'); - // My Library - setResponse({ - method: "GET", - url: "users/1/settings", - status: 200, - headers: { - "Last-Modified-Version": 5 - }, - json: { - INVALID: true // TODO: Find a cleaner error - } - }); + + // No other responses, so settings response will be a 404 spy = sinon.spy(runner, "updateIcons"); - yield runner.sync(); + await runner.sync(); assert.isTrue(spy.calledTwice); assert.isArray(spy.args[1][0]); assert.lengthOf(spy.args[1][0], 1); @@ -1097,6 +1144,118 @@ describe("Zotero.Sync.Runner", function () { }); + it("should show an error for invalid My Library data", async function () { + let library = Zotero.Libraries.userLibrary; + library.libraryVersion = 1; + await library.save(); + + var collection = await createDataObject('collection', { synced: true }); + var json = collection.toResponseJSON(); + json.version = json.data.version = 2; + json.data.INVALID = true; + + setResponse('keyInfo.fullAccess'); + setResponse('userGroups.groupVersionsEmpty'); + setDefaultResponses({ + lastLibraryVersion: 1, + libraryVersion: 2 + }); + setResponse({ + method: "GET", + url: "users/1/collections?format=versions&since=1", + status: 200, + headers: { + "Last-Modified-Version": 2 + }, + json: { + [json.key]: 2 + } + }); + setResponse({ + method: "GET", + url: `users/1/collections?format=json&collectionKey=${json.key}`, + status: 200, + headers: { + "Last-Modified-Version": 2 + }, + json: [json] + }); + + spy = sinon.spy(runner, "updateIcons"); + await runner.sync(); + assert.isTrue(spy.calledTwice); + assert.isArray(spy.args[1][0]); + assert.lengthOf(spy.args[1][0], 1); + // Not an instance of Error for some reason + var error = spy.args[1][0][0]; + assert.equal(Object.getPrototypeOf(error).constructor.name, "Error"); + assert.match(error.message, /^Some data in My Library/); + }); + + + it("should show a warning in the sync button tooltip for invalid group data", async function () { + win = await loadZoteroPane(); + var doc = win.document; + + // Create group with same id and version as groups response + var groupData = responses.groups.memberGroup; + var group = await createGroup({ + id: groupData.json.id, + version: groupData.json.version + }); + group.libraryVersion = 1; + await group.save(); + + var collection = await createDataObject('collection', { synced: true }); + var json = collection.toResponseJSON(); + json.version = json.data.version = 2; + json.data.INVALID = true; + + var target = 'groups/' + group.id; + setResponse('keyInfo.fullAccess'); + setResponse('userGroups.groupVersionsOnlyMemberGroup'); + setResponse('groups.memberGroup'); + setDefaultResponses({ + target, + lastLibraryVersion: 1, + libraryVersion: 2 + }); + setResponse({ + method: "GET", + url: target + '/collections?format=versions&since=1', + status: 200, + headers: { + "Last-Modified-Version": 2 + }, + json: { + [json.key]: 2 + } + }); + setResponse({ + method: "GET", + url: target + `/collections?format=json&collectionKey=${json.key}`, + status: 200, + headers: { + "Last-Modified-Version": 2 + }, + json: [json] + }); + + await runner.sync({ libraries: [group.libraryID] }); + + assert.isTrue(doc.getElementById('zotero-tb-sync-error').hidden); + + // Fake what happens on button mouseover + var tooltip = doc.getElementById('zotero-tb-sync-tooltip'); + runner.registerSyncStatus(tooltip); + + var html = doc.getElementById('zotero-tb-sync-tooltip').innerHTML; + assert.match(html, /Some data in .+\. Other data will continue to sync\./); + + runner.registerSyncStatus(); + }); + + // TODO: Test multiple long tags and tags across libraries describe("Long Tag Fixer", function () { it("should split a tag", function* () {