Show annotation widget in conflict resolution window
This adds a very basic reimplementation of the annotation widget for use in the CR window. It's not pixel-perfect, but it's close enough that most people probably won't notice. We'll want to replace this with a real version that's shared between the PDF reader and the rest of the codebase. Image annotations currently show an "[image not shown]" placeholder. Showing images is tricky: we don't have the remote cache image, so if the remote position has changed, we could only show an image by rendering it from the file, and only if the file itself hasn't changed. Just for a better user experience, we could use the local image as long as the position and file are both the same, but that would take some rejiggering of the CR window. Tags aren't shown because they're not shown for CR at all, though that could be changed in the future.
This commit is contained in:
parent
7889cd5d39
commit
06b28194da
7 changed files with 307 additions and 5 deletions
|
@ -67,6 +67,7 @@
|
||||||
case 'item':
|
case 'item':
|
||||||
case 'attachment':
|
case 'attachment':
|
||||||
case 'note':
|
case 'note':
|
||||||
|
case 'annotation':
|
||||||
case 'file':
|
case 'file':
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -225,8 +226,11 @@
|
||||||
Zotero.debug(obj, 1);
|
Zotero.debug(obj, 1);
|
||||||
throw new Error("obj is not item JSON");
|
throw new Error("obj is not item JSON");
|
||||||
}
|
}
|
||||||
if (obj.itemType == 'attachment' || obj.itemType == 'note') {
|
switch (obj.itemType) {
|
||||||
return obj.itemType;
|
case 'attachment':
|
||||||
|
case 'note':
|
||||||
|
case 'annotation':
|
||||||
|
return obj.itemType;
|
||||||
}
|
}
|
||||||
return 'item';
|
return 'item';
|
||||||
]]>
|
]]>
|
||||||
|
@ -340,11 +344,21 @@
|
||||||
elementName = useOld ? 'oldzoteronoteeditor' : 'zoteronoteeditor';
|
elementName = useOld ? 'oldzoteronoteeditor' : 'zoteronoteeditor';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'annotation':
|
||||||
|
elementName = 'div';
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error("Object type '" + this.type + "' not supported");
|
throw new Error("Object type '" + this.type + "' not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
var objbox = document.createElement(elementName);
|
if (elementName == 'div') {
|
||||||
|
let HTML_NS = 'http://www.w3.org/1999/xhtml';
|
||||||
|
var objbox = document.createElementNS(HTML_NS, elementName);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var objbox = document.createElement(elementName);
|
||||||
|
}
|
||||||
|
|
||||||
var parentRow = this._id('parent-row');
|
var parentRow = this._id('parent-row');
|
||||||
if (val.parentItem) {
|
if (val.parentItem) {
|
||||||
|
@ -389,7 +403,16 @@
|
||||||
var item = new Zotero.Item(val.itemType);
|
var item = new Zotero.Item(val.itemType);
|
||||||
item.libraryID = this.libraryID;
|
item.libraryID = this.libraryID;
|
||||||
item.fromJSON(displayJSON);
|
item.fromJSON(displayJSON);
|
||||||
objbox.item = item;
|
|
||||||
|
if (item.isAnnotation()) {
|
||||||
|
Zotero.Annotations.toJSON(item)
|
||||||
|
.then((data) => {
|
||||||
|
Zotero.AnnotationBox.render(objbox, { data });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
objbox.item = item;
|
||||||
|
}
|
||||||
]]>
|
]]>
|
||||||
</setter>
|
</setter>
|
||||||
</property>
|
</property>
|
||||||
|
|
79
chrome/content/zotero/components/annotation.jsx
Normal file
79
chrome/content/zotero/components/annotation.jsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
***** BEGIN LICENSE BLOCK *****
|
||||||
|
|
||||||
|
Copyright © 2021 Corporation for Digital Scholarship
|
||||||
|
Vienna, Virginia, USA
|
||||||
|
https://digitalscholar.org
|
||||||
|
|
||||||
|
This file is part of Zotero.
|
||||||
|
|
||||||
|
Zotero is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Zotero is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
***** END LICENSE BLOCK *****
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { IntlProvider, FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
// This is a quick reimplementation of the annotation for use in the conflict resolution window.
|
||||||
|
// We'll want to replace this with a single component shared between the PDF reader and the rest
|
||||||
|
// of the codebase.
|
||||||
|
function AnnotationBox({ data }) {
|
||||||
|
var textStyle = {
|
||||||
|
borderLeft: "2px solid " + data.color
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntlProvider
|
||||||
|
locale={ Zotero.locale }
|
||||||
|
messages={ Zotero.Intl.strings }
|
||||||
|
>
|
||||||
|
<div className="AnnotationBox">
|
||||||
|
<div className="title">{Zotero.getString('itemTypes.annotation')}</div>
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<div>{Zotero.getString('citation.locator.page')} {data.pageLabel}</div>
|
||||||
|
</div>
|
||||||
|
{data.text !== undefined
|
||||||
|
? <div className="text" style={textStyle}>{data.text}</div>
|
||||||
|
: ''}
|
||||||
|
{data.type == 'image'
|
||||||
|
// TODO: Localize
|
||||||
|
// TODO: Render from PDF based on position, if file is the same? Or don't
|
||||||
|
// worry about it?
|
||||||
|
? <div className="image-placeholder">[image not shown]</div>
|
||||||
|
: ''}
|
||||||
|
{data.comment !== undefined
|
||||||
|
? <div className="comment">{data.comment}</div>
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</IntlProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.AnnotationBox = memo(AnnotationBox);
|
||||||
|
|
||||||
|
Zotero.AnnotationBox.render = (domEl, props) => {
|
||||||
|
ReactDOM.render(<AnnotationBox { ...props } />, domEl);
|
||||||
|
};
|
||||||
|
|
||||||
|
Zotero.AnnotationBox.destroy = (domEl) => {
|
||||||
|
ReactDOM.unmountComponentAtNode(domEl);
|
||||||
|
};
|
|
@ -27,6 +27,7 @@
|
||||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||||
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
|
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
|
||||||
<?xml-stylesheet href="chrome://zotero/skin/merge.css" type="text/css"?>
|
<?xml-stylesheet href="chrome://zotero/skin/merge.css" type="text/css"?>
|
||||||
|
<?xml-stylesheet href="chrome://zotero-platform/content/zotero-react-client.css"?>
|
||||||
|
|
||||||
<!DOCTYPE window SYSTEM "chrome://zotero/locale/zotero.dtd">
|
<!DOCTYPE window SYSTEM "chrome://zotero/locale/zotero.dtd">
|
||||||
|
|
||||||
|
@ -40,6 +41,7 @@
|
||||||
|
|
||||||
<script src="include.js"/>
|
<script src="include.js"/>
|
||||||
<script src="merge.js"/>
|
<script src="merge.js"/>
|
||||||
|
<script src="components/annotation.js"/>
|
||||||
|
|
||||||
<wizardpage onpageshow="Zotero_Merge_Window.init()"
|
<wizardpage onpageshow="Zotero_Merge_Window.init()"
|
||||||
onpagerewound="Zotero_Merge_Window.onBack(); return false"
|
onpagerewound="Zotero_Merge_Window.onBack(); return false"
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
// Components
|
// Components
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
@import "components/annotation";
|
||||||
@import "components/autosuggest";
|
@import "components/autosuggest";
|
||||||
@import "components/button";
|
@import "components/button";
|
||||||
@import "components/createParent";
|
@import "components/createParent";
|
||||||
|
|
42
scss/components/_annotation.scss
Normal file
42
scss/components/_annotation.scss
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
.AnnotationBox {
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 5px;
|
||||||
|
border: 1px solid #d7dad7;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 4px;
|
||||||
|
border-bottom: 1px solid #d7dad7;
|
||||||
|
padding: 0 8px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text, .image-placeholder, .comment {
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text, .image-placeholder {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
padding-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -943,7 +943,10 @@ async function createAnnotation(type, parentItem, options = {}) {
|
||||||
annotation.annotationText = Zotero.Utilities.randomString();
|
annotation.annotationText = Zotero.Utilities.randomString();
|
||||||
}
|
}
|
||||||
annotation.annotationComment = Zotero.Utilities.randomString();
|
annotation.annotationComment = Zotero.Utilities.randomString();
|
||||||
var page = Zotero.Utilities.rand(1, 100).toString().padStart(5, '0');
|
annotation.annotationColor = '#ffd400';
|
||||||
|
var page = Zotero.Utilities.rand(1, 100);
|
||||||
|
annotation.annotationPageLabel = `${page}`;
|
||||||
|
page = page.toString().padStart(5, '0');
|
||||||
var pos = Zotero.Utilities.rand(1, 10000).toString().padStart(6, '0');
|
var pos = Zotero.Utilities.rand(1, 10000).toString().padStart(6, '0');
|
||||||
annotation.annotationSortIndex = `${page}|${pos}|00000`;
|
annotation.annotationSortIndex = `${page}|${pos}|00000`;
|
||||||
annotation.annotationPosition = JSON.stringify({
|
annotation.annotationPosition = JSON.stringify({
|
||||||
|
|
|
@ -3624,6 +3624,158 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
assert.lengthOf(keys, 0);
|
assert.lengthOf(keys, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("should show conflict resolution window on annotation conflicts", async function () {
|
||||||
|
var libraryID = Zotero.Libraries.userLibraryID;
|
||||||
|
({ engine, client, caller } = await setup());
|
||||||
|
var values = [];
|
||||||
|
var dateAdded = Date.now() - 86400000;
|
||||||
|
var responseJSON = [];
|
||||||
|
|
||||||
|
var attachment = await importFileAttachment('test.pdf');
|
||||||
|
var annotation1 = await createAnnotation('highlight', attachment);
|
||||||
|
var annotation2 = await createAnnotation('note', attachment);
|
||||||
|
var annotation3 = await createAnnotation('image', attachment);
|
||||||
|
var objects = [annotation1, annotation2, annotation3];
|
||||||
|
|
||||||
|
for (let i = 0; i < objects.length; i++) {
|
||||||
|
values.push({
|
||||||
|
left: {},
|
||||||
|
right: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
let item = objects[i];
|
||||||
|
item.version = 10;
|
||||||
|
item.dateAdded = Zotero.Date.dateToSQL(new Date(dateAdded), true);
|
||||||
|
// Set Date Modified values one minute apart to enforce order
|
||||||
|
item.dateModified = Zotero.Date.dateToSQL(
|
||||||
|
new Date(dateAdded + (i * 60000)), true
|
||||||
|
);
|
||||||
|
await item.saveTx();
|
||||||
|
|
||||||
|
let jsonData = item.toJSON();
|
||||||
|
jsonData.key = item.key;
|
||||||
|
jsonData.version = 10;
|
||||||
|
let json = {
|
||||||
|
key: item.key,
|
||||||
|
version: jsonData.version,
|
||||||
|
data: jsonData
|
||||||
|
};
|
||||||
|
// Save original version in cache
|
||||||
|
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json]);
|
||||||
|
|
||||||
|
// Make conflicting local and remote changes
|
||||||
|
if (i == 0) {
|
||||||
|
values[i].left.annotationText
|
||||||
|
= item.annotationText = Zotero.Utilities.randomString();
|
||||||
|
values[i].right.annotationText
|
||||||
|
= jsonData.annotationText = Zotero.Utilities.randomString();
|
||||||
|
}
|
||||||
|
else if (i == 1) {
|
||||||
|
values[i].left.annotationComment
|
||||||
|
= item.annotationComment = Zotero.Utilities.randomString();
|
||||||
|
values[i].right.annotationComment
|
||||||
|
= jsonData.annotationComment = Zotero.Utilities.randomString();
|
||||||
|
}
|
||||||
|
else if (i == 2) {
|
||||||
|
values[i].left.annotationColor
|
||||||
|
= item.annotationColor = '#000000';
|
||||||
|
values[i].right.annotationColor = jsonData.annotationColor = '#ffffff';
|
||||||
|
}
|
||||||
|
await item.saveTx({
|
||||||
|
skipDateModifiedUpdate: true
|
||||||
|
});
|
||||||
|
|
||||||
|
values[i].left.version = item.version;
|
||||||
|
values[i].right.version = json.version = jsonData.version = 15;
|
||||||
|
responseJSON.push(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
setResponse({
|
||||||
|
method: "GET",
|
||||||
|
url: `users/1/items?itemKey=${objects.map(x => x.key).join('%2C')}`
|
||||||
|
+ `&includeTrashed=1`,
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Last-Modified-Version": 15
|
||||||
|
},
|
||||||
|
json: responseJSON
|
||||||
|
});
|
||||||
|
|
||||||
|
var crPromise = waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
|
||||||
|
var doc = dialog.document;
|
||||||
|
var wizard = doc.documentElement;
|
||||||
|
var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
|
||||||
|
|
||||||
|
// TODO: Make this function async and verify that annotation widgets show up here
|
||||||
|
// after rendering. This may not be possible as long as this is within XBL.
|
||||||
|
|
||||||
|
// 1 (remote)
|
||||||
|
// Remote version should be selected by default
|
||||||
|
assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
|
||||||
|
wizard.getButton('next').click();
|
||||||
|
|
||||||
|
// 2 (local)
|
||||||
|
assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
|
||||||
|
// Select local object
|
||||||
|
mergeGroup.leftpane.click();
|
||||||
|
assert.equal(mergeGroup.leftpane.getAttribute('selected'), 'true');
|
||||||
|
wizard.getButton('next').click();
|
||||||
|
|
||||||
|
// 2 (remote)
|
||||||
|
assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
|
||||||
|
|
||||||
|
if (Zotero.isMac) {
|
||||||
|
assert.isTrue(wizard.getButton('next').hidden);
|
||||||
|
assert.isFalse(wizard.getButton('finish').hidden);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
wizard.getButton('finish').click();
|
||||||
|
});
|
||||||
|
await engine._downloadObjects('item', objects.map(o => o.key));
|
||||||
|
await crPromise;
|
||||||
|
|
||||||
|
assert.equal(objects[0].annotationText, values[0].right.annotationText);
|
||||||
|
assert.equal(objects[1].annotationComment, values[1].left.annotationComment);
|
||||||
|
assert.equal(objects[2].annotationColor, values[2].right.annotationColor);
|
||||||
|
|
||||||
|
assert.equal(objects[0].version, values[0].right.version);
|
||||||
|
assert.equal(objects[1].version, values[1].right.version);
|
||||||
|
assert.equal(objects[2].version, values[2].right.version);
|
||||||
|
assert.isTrue(objects[0].synced);
|
||||||
|
assert.isFalse(objects[1].synced);
|
||||||
|
assert.isTrue(objects[2].synced);
|
||||||
|
|
||||||
|
// Cache versions should match remote
|
||||||
|
for (let i = 0; i < objects.length; i++) {
|
||||||
|
let cacheJSON = await Zotero.Sync.Data.Local.getCacheObject(
|
||||||
|
'item', libraryID, objects[i].key, values[i].right.version
|
||||||
|
);
|
||||||
|
assert.propertyVal(cacheJSON, 'version', values[i].right.version);
|
||||||
|
if (i == 0) {
|
||||||
|
assert.nestedPropertyVal(
|
||||||
|
cacheJSON, 'data.annotationText', values[i].right.annotationText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (i == 1) {
|
||||||
|
assert.nestedPropertyVal(
|
||||||
|
cacheJSON, 'data.annotationComment', values[i].right.annotationComment
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (i == 2) {
|
||||||
|
assert.nestedPropertyVal(
|
||||||
|
cacheJSON, 'data.annotationColor', values[i].right.annotationColor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
||||||
|
assert.lengthOf(keys, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
it("should resolve all remaining conflicts with local version", async function () {
|
it("should resolve all remaining conflicts with local version", async function () {
|
||||||
var libraryID = Zotero.Libraries.userLibraryID;
|
var libraryID = Zotero.Libraries.userLibraryID;
|
||||||
({ engine, client, caller } = await setup());
|
({ engine, client, caller } = await setup());
|
||||||
|
|
Loading…
Reference in a new issue