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') === ''); + }); + }); });