diff --git a/frontend/vre/annotation/annotation.model.js b/frontend/vre/annotation/annotation.model.js
index e9e4864f..43fe255e 100644
--- a/frontend/vre/annotation/annotation.model.js
+++ b/frontend/vre/annotation/annotation.model.js
@@ -91,7 +91,7 @@ export var Annotation = JsonLdModel.extend({
var field = selector['edpopcol:field'],
text = selector['edpopcol:originalText'];
if (field) flat['edpopcol:field'] = field['@id'];
- if (text) flat['edpopcol:originalText'] = text;
+ if (text != null) flat['edpopcol:originalText'] = text;
}
}
return _.assign(flat, anno);
@@ -110,7 +110,7 @@ export var Annotation = JsonLdModel.extend({
flatField = jsonld['edpopcol:field'],
flatText = jsonld['edpopcol:originalText'];
if (flatField) selector['edpopcol:field'] = {'@id': flatField};
- if (flatText) selector['edpopcol:originalText'] = flatText;
+ if (flatText != null) selector['edpopcol:originalText'] = flatText;
if (flatField || flatText) var flatSelector = true;
if (flatSource) target['oa:hasSource'] = {'@id': flatSource};
if (flatSelector) target['oa:hasSelector'] = selector;
diff --git a/frontend/vre/annotation/annotation.model.test.js b/frontend/vre/annotation/annotation.model.test.js
index b3f7de99..3133b1ff 100644
--- a/frontend/vre/annotation/annotation.model.test.js
+++ b/frontend/vre/annotation/annotation.model.test.js
@@ -118,6 +118,14 @@ describe('Annotation model', () => {
'@id': 'edpopcol:incorrectFieldValue'
});
});
+
+ it('preserves empty originalText', () => {
+ model.set(flatAnnotation);
+ model.set('edpopcol:originalText', '');
+ const storedAttributes = model.toJSON();
+ model.clear().set(model.parse(storedAttributes));
+ assert(model.get('edpopcol:originalText') === '');
+ });
});
describe('Annotations collection', () => {
diff --git a/frontend/vre/css/style.css b/frontend/vre/css/style.css
index c060a208..dccdcddc 100644
--- a/frontend/vre/css/style.css
+++ b/frontend/vre/css/style.css
@@ -10,6 +10,12 @@ h2 {
text-decoration: none;
}
+.vre-record-fields .original-text:empty::before,
+.popover .relink-option:empty::before {
+ content: "(empty value)";
+ color: #888;
+}
+
.vre-record-fields del, .authored-by {
font-size: 0.8em;
color: #888;
diff --git a/frontend/vre/field/field-value.view.js b/frontend/vre/field/field-value.view.js
index 4d2918e1..c02871f0 100644
--- a/frontend/vre/field/field-value.view.js
+++ b/frontend/vre/field/field-value.view.js
@@ -32,6 +32,7 @@ export var FieldValueView = View.extend(/**
events: {
'click .fa-plus, .fa-pen': bubble('edit'),
'click .fa-xmark': bubble('discard'),
+ 'click': 'mockDanglingEdit',
},
initialize: function(options) {
@@ -72,10 +73,12 @@ export var FieldValueView = View.extend(/**
edit = attributes.edit,
updatedDate = edit && edit.getUpdatedDate(),
originalText = attributes.originalText,
+ hasOriginalText = (originalText != null),
payload = _.extend({}, attributes, {
fieldInfo,
linkedRecordUri,
updatedDate,
+ hasOriginalText,
});
if (edit && originalText && originalText.length > 20) {
payload.shortOriginalText = (
@@ -104,4 +107,11 @@ export var FieldValueView = View.extend(/**
edit.set('edpopcol:originalText', event.target.textContent);
edit.save();
},
+
+ mockDanglingEdit: function(event) {
+ if (!event.shiftKey) return;
+ var edit = this.model.get('edit');
+ if (!edit || !edit.has('edpopcol:originalText')) return;
+ edit.set('edpopcol:originalText', 'simulated dangle');
+ },
});
diff --git a/frontend/vre/field/field-value.view.mustache b/frontend/vre/field/field-value.view.mustache
index f8285374..95f7fe74 100644
--- a/frontend/vre/field/field-value.view.mustache
+++ b/frontend/vre/field/field-value.view.mustache
@@ -12,16 +12,16 @@
{{/edit}}
class="vre-field-value"
>
-{{#originalText}}
+{{#hasOriginalText}}
{{#uncorrected}}
{{#linkedRecordUri}}
{{originalText}}
+ >{{originalText}}
{{/linkedRecordUri}}
{{^linkedRecordUri}}
- {{originalText}}
+ {{originalText}}
{{/linkedRecordUri}}
{{/uncorrected}}
{{^uncorrected}}
@@ -29,10 +29,10 @@
{{.}}
{{/shortOriginalText}}
{{^shortOriginalText}}
- {{.}}
+ {{originalText}}
{{/shortOriginalText}}
{{/uncorrected}}
-{{/originalText}}
+{{/hasOriginalText}}
{{#correctedText}}
{{.}}
{{/correctedText}}
diff --git a/frontend/vre/field/field.model.test.js b/frontend/vre/field/field.model.test.js
index f053d29b..5d02c4c0 100644
--- a/frontend/vre/field/field.model.test.js
+++ b/frontend/vre/field/field.model.test.js
@@ -6,6 +6,7 @@ import Backbone from 'backbone';
import { Record } from '../record/record.model.js';
import { Annotations } from '../annotation/annotation.model.js';
+import { properties } from '../utils/record-ontology.js';
import { presentableContents, fieldEntryTag } from './field.model.js';
// Quick string-generating function to emulate the backend's bnode: IRIs.
@@ -73,6 +74,12 @@ var mockRecord = {
'@type': 'edpoprec:Field',
'edpoprec:originalText': contributor.value4.original,
}],
+ // field with empty string value
+ 'edpoprec:publisherOrPrinter': {
+ '@id': bnode(),
+ '@type': 'edpoprec:Field',
+ 'edpoprec:originalText': ''
+ },
// field with just a single corrected value
'edpoprec:genre': {
'@id': bnode(),
@@ -253,6 +260,11 @@ var expectedCompoundData = [sinon.match({
id: 'edpoprec:publisherOrPrinter',
field: sinon.match.truthy,
content: [sinon.match({
+ id: '',
+ uncorrected: true,
+ order: fieldEntryTag.originalValue,
+ isFirst: true,
+ }), sinon.match({
id: 'edpoprec:publisherOrPrinter',
field: true,
order: fieldEntryTag.wholeField
@@ -422,6 +434,10 @@ function disableRecordFieldListeners(recordField) {
describe('presentableContents', function() {
var record, contents;
+ before(() => {
+ properties.once('update', () => done());
+ });
+
beforeEach(() => {
record = new Record(mockRecord);
record.annotations = new Annotations(mockAnnotations);
@@ -947,5 +963,77 @@ describe('presentableContents', function() {
});
});
});
+
+ describe('empty string original values', () => {
+ var content, fieldName = 'edpoprec:publisherOrPrinter';
+
+ beforeEach(() => {
+ model = contents.get(fieldName);
+ content = model.content;
+ });
+
+ afterEach(() => {
+ content = null;
+ });
+
+ it('can be corrected', () => {
+ model.annotations.underlying.add({
+ '@id': bnode(),
+ '@type': 'oa:Annotation',
+ 'edpopcol:field': fieldName,
+ 'edpopcol:originalText': '',
+ 'oa:hasBody': 'nonempty',
+ });
+ sinon.assert.match(
+ recordField2json(model),
+ sinon.match({
+ id: 'edpoprec:publisherOrPrinter',
+ field: sinon.match.truthy,
+ content: [sinon.match({
+ id: ' → nonempty',
+ correction: true,
+ order: fieldEntryTag.correction,
+ original: sinon.match.truthy,
+ originalText: '',
+ correctedText: 'nonempty',
+ isFirst: true,
+ }), sinon.match({
+ id: 'edpoprec:publisherOrPrinter',
+ field: true,
+ order: fieldEntryTag.wholeField
+ })],
+ })
+ );
+ });
+
+ it('can be deleted', () => {
+ model.annotations.underlying.add({
+ '@id': bnode(),
+ '@type': 'oa:Annotation',
+ 'edpopcol:field': fieldName,
+ 'edpopcol:originalText': '',
+ marksDeletion: true,
+ });
+ sinon.assert.match(
+ recordField2json(model),
+ sinon.match({
+ id: 'edpoprec:publisherOrPrinter',
+ field: sinon.match.truthy,
+ content: [sinon.match({
+ id: ' →',
+ deletion: true,
+ order: fieldEntryTag.deletion,
+ original: sinon.match.truthy,
+ originalText: '',
+ isFirst: true,
+ }), sinon.match({
+ id: 'edpoprec:publisherOrPrinter',
+ field: true,
+ order: fieldEntryTag.wholeField
+ })],
+ })
+ );
+ });
+ });
});
});
diff --git a/frontend/vre/field/field.view.js b/frontend/vre/field/field.view.js
index 8b34fe24..05ee17f6 100644
--- a/frontend/vre/field/field.view.js
+++ b/frontend/vre/field/field.view.js
@@ -65,7 +65,7 @@ export var FieldView = AggregateView.extend(/**
newEdit = edit.clone();
} else {
newEdit = this.makeEdit(originalText);
- correctedText && newEdit.set('oa:hasBody', correctedText);
+ correctedText != null && newEdit.set('oa:hasBody', correctedText);
}
var editor = new AnnotationEditView({
model: newEdit,
@@ -90,7 +90,7 @@ export var FieldView = AggregateView.extend(/**
'edpopcol:field': this.model.id,
'motivation': 'oa:editing',
});
- originalText && newEdit.set('edpopcol:originalText', originalText);
+ originalText != null && newEdit.set('edpopcol:originalText', originalText);
return newEdit;
},
diff --git a/frontend/vre/field/field.view.test.js b/frontend/vre/field/field.view.test.js
index 4b0b24cb..d841c03f 100644
--- a/frontend/vre/field/field.view.test.js
+++ b/frontend/vre/field/field.view.test.js
@@ -1,11 +1,50 @@
import assert from 'assert';
import sinon from 'sinon';
+import _ from 'lodash';
+import { Model, Collection } from 'backbone';
+
+import { vreChannel } from '../radio.js';
import { FieldView } from './field.view';
describe('FieldView', function() {
- // This space intentionally left blank after view redesign. The view has
- // become more declarative and some of its former functionality has moved
- // into the new FieldValueView subview, which is even more declarative.
- // Tests may be added again when regressions are encountered.
+ var model, view, collection;
+
+ before(() => {
+ vreChannel.reply('projects:current', _.constant({id: 1}));
+ });
+
+ beforeEach(() => {
+ model = new Model({id: 1, record: {id: 2}});
+ collection = new Collection;
+ view = new FieldView({model, collection});
+ });
+
+ afterEach(() => {
+ view.remove();
+ collection.off().reset();
+ model.off().clear();
+ });
+
+ after(() => {
+ vreChannel.stopReplying('projects:current');
+ });
+
+ describe('makeEdit', () => {
+ it('includes non-null originalText', () => {
+ var text = 'something'
+ var edit = view.makeEdit(text);
+ assert(edit.get('edpopcol:originalText') === text);
+ });
+
+ it('omits nullish originalText', () => {
+ var edit = view.makeEdit();
+ assert(!edit.has('edpopcol:originalText'));
+ });
+
+ it('includes non-null originalText even if empty', () => {
+ var edit = view.makeEdit('');
+ assert(edit.get('edpopcol:originalText') === '');
+ });
+ });
});