Commit c41545a1 authored by Robert Knight's avatar Robert Knight

Move tag editor to separate component

As part of an effort to simplify the <annotation> directive and make
testing easier, split this out.
parent 55a5669e
...@@ -152,6 +152,7 @@ module.exports = angular.module('h', [ ...@@ -152,6 +152,7 @@ module.exports = angular.module('h', [
.directive('sortDropdown', require('./directive/sort-dropdown')) .directive('sortDropdown', require('./directive/sort-dropdown'))
.directive('spinner', require('./directive/spinner')) .directive('spinner', require('./directive/spinner'))
.directive('statusButton', require('./directive/status-button')) .directive('statusButton', require('./directive/status-button'))
.directive('tagEditor', require('./directive/tag-editor'))
.directive('timestamp', require('./directive/timestamp')) .directive('timestamp', require('./directive/timestamp'))
.directive('topBar', require('./directive/top-bar')) .directive('topBar', require('./directive/top-bar'))
.directive('windowScroll', require('./directive/window-scroll')) .directive('windowScroll', require('./directive/window-scroll'))
......
...@@ -13,19 +13,6 @@ var isNew = annotationMetadata.isNew; ...@@ -13,19 +13,6 @@ var isNew = annotationMetadata.isNew;
var isReply = annotationMetadata.isReply; var isReply = annotationMetadata.isReply;
var extractDocumentMetadata = annotationMetadata.extractDocumentMetadata; var extractDocumentMetadata = annotationMetadata.extractDocumentMetadata;
/** Return a domainModel tags array from the given vm tags array.
*
* domainModel.tags and vm.form.tags use different formats. This
* function returns a domainModel.tags-formatted copy of the given
* vm.form.tags-formatted array.
*
*/
function domainModelTagsFromViewModelTags(viewModelTags) {
return (viewModelTags || []).map(function(tag) {
return tag.text;
});
}
/** Return a human-readable error message for the given server error. /** Return a human-readable error message for the given server error.
* *
* @param {object} reason The error object from the server. Should have * @param {object} reason The error object from the server. Should have
...@@ -92,16 +79,13 @@ function saveToDrafts(drafts, domainModel, vm) { ...@@ -92,16 +79,13 @@ function saveToDrafts(drafts, domainModel, vm) {
* Copy any properties from vm that might have been modified by the user into * Copy any properties from vm that might have been modified by the user into
* domainModel, overwriting any existing properties in domainModel. * domainModel, overwriting any existing properties in domainModel.
* *
* Additionally, the `tags` property of vm - an array of objects each with a
* `text` string - will become a simple array of strings in domainModel.
*
* @param {object} domainModel The object to copy properties to * @param {object} domainModel The object to copy properties to
* @param {object} vm The object to copy properties from * @param {object} vm The object to copy properties from
* *
*/ */
function updateDomainModel(domainModel, vm, permissions) { function updateDomainModel(domainModel, vm, permissions) {
domainModel.text = vm.form.text; domainModel.text = vm.form.text;
domainModel.tags = domainModelTagsFromViewModelTags(vm.form.tags); domainModel.tags = vm.form.tags;
if (vm.isPrivate) { if (vm.isPrivate) {
domainModel.permissions = permissions.private(); domainModel.permissions = permissions.private();
} else { } else {
...@@ -115,7 +99,7 @@ function updateViewModel($scope, domainModel, ...@@ -115,7 +99,7 @@ function updateViewModel($scope, domainModel,
vm.form = { vm.form = {
text: domainModel.text, text: domainModel.text,
tags: viewModelTagsFromDomainModelTags(domainModel.tags), tags: domainModel.tags,
}; };
if (domainModel.links) { if (domainModel.links) {
...@@ -136,19 +120,6 @@ function updateViewModel($scope, domainModel, ...@@ -136,19 +120,6 @@ function updateViewModel($scope, domainModel,
vm.documentDomain = documentDomain(documentMetadata); vm.documentDomain = documentDomain(documentMetadata);
} }
/** Return a vm tags array from the given domainModel tags array.
*
* domainModel.tags and vm.form.tags use different formats. This
* function returns a vm.form.tags-formatted copy of the given
* domainModel.tags-formatted array.
*
*/
function viewModelTagsFromDomainModelTags(domainModelTags) {
return (domainModelTags || []).map(function(tag) {
return {text: tag};
});
}
/** /**
* @ngdoc type * @ngdoc type
* @name annotation.AnnotationController * @name annotation.AnnotationController
...@@ -158,7 +129,7 @@ function viewModelTagsFromDomainModelTags(domainModelTags) { ...@@ -158,7 +129,7 @@ function viewModelTagsFromDomainModelTags(domainModelTags) {
function AnnotationController( function AnnotationController(
$document, $q, $rootScope, $scope, $timeout, $window, annotationUI, $document, $q, $rootScope, $scope, $timeout, $window, annotationUI,
annotationMapper, drafts, flash, features, groups, permissions, session, annotationMapper, drafts, flash, features, groups, permissions, session,
settings, tags) { settings) {
var vm = this; var vm = this;
var domainModel; var domainModel;
...@@ -554,13 +525,6 @@ function AnnotationController( ...@@ -554,13 +525,6 @@ function AnnotationController(
return Promise.resolve(); return Promise.resolve();
} }
// Update stored tags with the new tags of this annotation.
var newTags = vm.form.tags.filter(function(tag) {
var tags = domainModel.tags || [];
return tags.indexOf(tag.text) === -1;
});
tags.store(newTags);
var saved; var saved;
switch (vm.action) { switch (vm.action) {
case 'create': case 'create':
...@@ -624,16 +588,6 @@ function AnnotationController( ...@@ -624,16 +588,6 @@ function AnnotationController(
vm.isPrivate = (privacy === 'private'); vm.isPrivate = (privacy === 'private');
}; };
/**
* @ngdoc method
* @name annotation.AnnotationController#tagsAutoComplete.
* @returns {Promise} immediately resolved to {string[]} -
* the tags to show in autocomplete.
*/
vm.tagsAutoComplete = function(query) {
return $q.when(tags.filter(query));
};
vm.tagStreamURL = function(tag) { vm.tagStreamURL = function(tag) {
return vm.serviceUrl + 'stream?q=tag:' + encodeURIComponent(tag); return vm.serviceUrl + 'stream?q=tag:' + encodeURIComponent(tag);
}; };
...@@ -677,6 +631,10 @@ function AnnotationController( ...@@ -677,6 +631,10 @@ function AnnotationController(
vm.form.text = text; vm.form.text = text;
}; };
vm.setTags = function (tags) {
vm.form.tags = tags;
};
init(); init();
} }
......
'use strict';
// @ngInject
function TagEditorController(tags) {
this.onTagsChanged = function () {
tags.store(this.tagList);
var newTags = this.tagList.map(function (item) { return item.text; });
this.onEditTags({tags: newTags});
};
this.autocomplete = function (query) {
return Promise.resolve(tags.filter(query));
};
this.$onChanges = function (changes) {
if (changes.tags) {
this.tagList = changes.tags.currentValue.map(function (tag) {
return {text: tag};
});
}
};
}
module.exports = function () {
return {
bindToController: true,
controller: TagEditorController,
controllerAs: 'vm',
restrict: 'E',
scope: {
tags: '<',
onEditTags: '&',
},
template: require('../../../templates/client/tag_editor.html'),
};
};
...@@ -83,20 +83,14 @@ describe('annotation', function() { ...@@ -83,20 +83,14 @@ describe('annotation', function() {
var domainModel = {}; var domainModel = {};
var viewModel = { var viewModel = {
form: { form: {
tags: [ tags: ['foo', 'bar'],
{text: 'foo'},
{text: 'bar'}
]
} }
}; };
updateDomainModel(domainModel, viewModel, fakePermissions(), updateDomainModel(domainModel, viewModel, fakePermissions(),
fakeGroups()); fakeGroups());
assert.deepEqual( assert.deepEqual(domainModel.tags, ['foo', 'bar']);
domainModel.tags, ['foo', 'bar'],
'The array of {tag: "text"} objects in viewModel becomes an array ' +
'of "text" strings in domainModel');
}); });
it('sets domainModel.permissions to private if vm.isPrivate', function() { it('sets domainModel.permissions to private if vm.isPrivate', function() {
...@@ -230,11 +224,6 @@ describe('annotation', function() { ...@@ -230,11 +224,6 @@ describe('annotation', function() {
serviceUrl: 'https://test.hypothes.is/', serviceUrl: 'https://test.hypothes.is/',
}; };
var fakeTags = {
filter: sandbox.stub().returns('a while ago'),
store: sandbox.stub()
};
fakeGroups = { fakeGroups = {
focused: function() { focused: function() {
return {}; return {};
...@@ -250,7 +239,6 @@ describe('annotation', function() { ...@@ -250,7 +239,6 @@ describe('annotation', function() {
$provide.value('permissions', fakePermissions); $provide.value('permissions', fakePermissions);
$provide.value('session', fakeSession); $provide.value('session', fakeSession);
$provide.value('settings', fakeSettings); $provide.value('settings', fakeSettings);
$provide.value('tags', fakeTags);
$provide.value('groups', fakeGroups); $provide.value('groups', fakeGroups);
})); }));
...@@ -700,11 +688,7 @@ describe('annotation', function() { ...@@ -700,11 +688,7 @@ describe('annotation', function() {
controller.form.text = 'bar'; controller.form.text = 'bar';
assert.ok(controller.hasContent()); assert.ok(controller.hasContent());
controller.form.text = ''; controller.form.text = '';
controller.form.tags = [ controller.form.tags = ['foo'];
{
text: 'foo'
}
];
assert.ok(controller.hasContent()); assert.ok(controller.hasContent());
}); });
}); });
...@@ -995,11 +979,7 @@ describe('annotation', function() { ...@@ -995,11 +979,7 @@ describe('annotation', function() {
describe('drafts', function() { describe('drafts', function() {
it('starts editing immediately if there is a draft', function() { it('starts editing immediately if there is a draft', function() {
fakeDrafts.get.returns({ fakeDrafts.get.returns({
tags: [ tags: ['unsaved'],
{
text: 'unsaved'
}
],
text: 'unsaved-text' text: 'unsaved-text'
}); });
var controller = createDirective().controller; var controller = createDirective().controller;
...@@ -1008,15 +988,11 @@ describe('annotation', function() { ...@@ -1008,15 +988,11 @@ describe('annotation', function() {
it('uses the text and tags from the draft if present', function() { it('uses the text and tags from the draft if present', function() {
fakeDrafts.get.returns({ fakeDrafts.get.returns({
tags: [{text: 'unsaved-tag'}], tags: ['unsaved-tag'],
text: 'unsaved-text' text: 'unsaved-text'
}); });
var controller = createDirective().controller; var controller = createDirective().controller;
assert.deepEqual(controller.form.tags, [ assert.deepEqual(controller.form.tags, ['unsaved-tag']);
{
text: 'unsaved-tag'
}
]);
assert.equal(controller.form.text, 'unsaved-text'); assert.equal(controller.form.text, 'unsaved-text');
}); });
...@@ -1096,7 +1072,7 @@ describe('annotation', function() { ...@@ -1096,7 +1072,7 @@ describe('annotation', function() {
it('does not remove the current annotation if it has tags', function () { it('does not remove the current annotation if it has tags', function () {
var annotation = fixtures.newAnnotation(); var annotation = fixtures.newAnnotation();
var parts = createDirective(annotation); var parts = createDirective(annotation);
parts.controller.form.tags = [{text: 'a-tag'}]; parts.controller.form.tags = ['a-tag'];
$rootScope.$emit(events.BEFORE_ANNOTATION_CREATED, $rootScope.$emit(events.BEFORE_ANNOTATION_CREATED,
fixtures.newAnnotation()); fixtures.newAnnotation());
assert.notCalled(fakeDrafts.remove); assert.notCalled(fakeDrafts.remove);
......
'use strict';
var angular = require('angular');
var util = require('./util');
describe('tagEditor', function () {
var fakeTags;
before(function () {
angular.module('app',[])
.directive('tagEditor', require('../tag-editor'));
});
beforeEach(function () {
fakeTags = {
filter: sinon.stub(),
store: sinon.stub(),
};
angular.mock.module('app', {
tags: fakeTags,
});
});
it('converts tags to the form expected by ng-tags-input', function () {
var element = util.createDirective(document, 'tag-editor', {
tags: ['foo', 'bar']
});
assert.deepEqual(element.ctrl.tagList, [{text: 'foo'}, {text: 'bar'}]);
});
describe('when tags are changed', function () {
var element;
var onEditTags;
beforeEach(function () {
onEditTags = sinon.stub();
element = util.createDirective(document, 'tag-editor', {
onEditTags: {args: ['tags'], callback: onEditTags},
tags: ['foo'],
});
element.ctrl.onTagsChanged();
});
it('calls onEditTags handler', function () {
assert.calledWith(onEditTags, sinon.match(['foo']));
});
it('saves tags to the store', function () {
assert.calledWith(fakeTags.store, sinon.match([{text: 'foo'}]));
});
});
describe('#autocomplete', function () {
it('suggests tags using the `tags` service', function () {
var element = util.createDirective(document, 'tag-editor', {tags: []});
element.ctrl.autocomplete('query');
assert.calledWith(fakeTags.filter, 'query');
});
});
});
...@@ -84,24 +84,15 @@ ...@@ -84,24 +84,15 @@
<!-- Tags --> <!-- Tags -->
<div class="annotation-body form-field" ng-if="vm.editing()"> <div class="annotation-body form-field" ng-if="vm.editing()">
<tags-input ng-model="vm.form.tags" <tag-editor tags="vm.form.tags"
name="tags" on-edit-tags="vm.setTags(tags)"></tag-editor>
class="tags"
placeholder="Add tags…"
min-length="1"
replace-spaces-with-dashes="false"
enable-editing-last-tag="true">
<auto-complete source="vm.tagsAutoComplete($query)"
min-length="1"
max-results-to-show="10"></auto-complete>
</tags-input>
</div> </div>
<div class="annotation-body u-layout-row tags tags-read-only" <div class="annotation-body u-layout-row tags tags-read-only"
ng-if="(vm.canCollapseBody || vm.form.tags.length) && !vm.editing()"> ng-if="(vm.canCollapseBody || vm.form.tags.length) && !vm.editing()">
<ul class="tag-list"> <ul class="tag-list">
<li class="tag-item" ng-repeat="tag in vm.form.tags"> <li class="tag-item" ng-repeat="tag in vm.form.tags">
<a ng-href="{{vm.tagStreamURL(tag.text)}}" target="_blank">{{tag.text}}</a> <a ng-href="{{vm.tagStreamURL(tag)}}" target="_blank">{{tag}}</a>
</li> </li>
</ul> </ul>
<div class="u-stretch"></div> <div class="u-stretch"></div>
......
<tags-input ng-model="vm.tagList"
name="tags"
class="tags"
placeholder="Add tags…"
min-length="1"
replace-spaces-with-dashes="false"
enable-editing-last-tag="true"
on-tag-added="vm.onTagsChanged()"
on-tag-removed="vm.onTagsChanged()">
<auto-complete source="vm.autocomplete($query)"
min-length="1"
max-results-to-show="10"></auto-complete>
</tags-input>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment