Commit a762a135 authored by Sheetal Umesh Kumar's avatar Sheetal Umesh Kumar Committed by GitHub

Implement moderation UI for group reader for flagging an annotation. (#296)

https://github.com/hypothesis/product-backlog/issues/181
parent 3030bf40
......@@ -60,6 +60,7 @@ function analytics($analytics, $window, settings) {
events: {
ANNOTATION_CREATED: 'annotationCreated',
ANNOTATION_DELETED: 'annotationDeleted',
ANNOTATION_FLAGGED: 'annotationFlagged',
ANNOTATION_UPDATED: 'annotationUpdated',
HIGHLIGHT_CREATED: 'highlightCreated',
HIGHLIGHT_UPDATED: 'highlightUpdated',
......
......@@ -54,11 +54,21 @@ function annotationMapper($rootScope, annotationUI, store) {
});
}
function flagAnnotation(annot) {
return store.flag.create(null,{
annotation: annot.id,
}).then(function () {
$rootScope.$broadcast(events.ANNOTATION_FLAGGED, annot);
return annot;
});
}
return {
loadAnnotations: loadAnnotations,
unloadAnnotations: unloadAnnotations,
createAnnotation: createAnnotation,
deleteAnnotation: deleteAnnotation,
flagAnnotation: flagAnnotation,
};
}
......
......@@ -220,6 +220,22 @@ function AnnotationController(
return permissions.permits(action, vm.annotation, session.state.userid);
};
/**
* @ngdoc method
* @name annotation.AnnotationController#flag
* @description Flag the annotation.
*/
vm.flag = function() {
var onRejected = function(reason) {
flash.error(
errorMessage(reason), 'Flagging annotation failed');
};
annotationMapper.flagAnnotation(vm.annotation).then(function(){
analytics.track(analytics.events.ANNOTATION_FLAGGED);
vm.isFlagged = true;
}, onRejected);
};
/**
* @ngdoc method
* @name annotation.AnnotationController#delete
......
......@@ -138,6 +138,7 @@ describe('annotation', function() {
},
}),
deleteAnnotation: sandbox.stub(),
flagAnnotation: sandbox.stub(),
};
var fakeAnnotationUI = {};
......@@ -689,6 +690,47 @@ describe('annotation', function() {
});
});
describe('#flag()', function() {
beforeEach(function() {
fakeAnnotationMapper.flagAnnotation = sandbox.stub();
});
it(
'calls annotationMapper.flag() when an annotation is flagged',
function(done) {
var parts = createDirective();
fakeAnnotationMapper.flagAnnotation.returns($q.resolve());
parts.controller.flag();
assert.calledWith(fakeAnnotationMapper.flagAnnotation,
parts.annotation);
done();
}
);
it('flashes an error if the flag fails', function(done) {
var controller = createDirective().controller;
fakeAnnotationMapper.flagAnnotation.returns(Promise.reject({
status: 500,
statusText: 'Server error',
}));
controller.flag();
setTimeout(function () {
assert.calledWith(fakeFlash.error, '500 Server error', 'Flagging annotation failed');
done();
}, 0);
});
it('doesn\'t flash an error if the flag succeeds', function(done) {
var controller = createDirective().controller;
fakeAnnotationMapper.flagAnnotation.returns($q.resolve());
controller.flag();
setTimeout(function () {
assert.notCalled(fakeFlash.error);
done();
}, 0);
});
});
describe('#documentMeta()', function () {
it('returns the domain, title link and text for the annotation', function () {
var annot = fixtures.defaultAnnotation();
......
......@@ -31,6 +31,9 @@ module.exports = {
/** An annotation was either deleted or unloaded. */
ANNOTATION_DELETED: 'annotationDeleted',
/** An annotation was flagged. */
ANNOTATION_FLAGGED: 'annotationFlagged',
/** An annotation has been updated. */
ANNOTATION_UPDATED: 'annotationUpdated',
......
......@@ -143,6 +143,9 @@ function store($http, $q, auth, settings) {
get: apiCall('annotation.read'),
update: apiCall('annotation.update'),
},
flag: {
create: apiCall('flag.create'),
},
profile: {
read: apiCall('profile.read'),
update: apiCall('profile.update'),
......
......@@ -185,6 +185,23 @@
on-close="vm.showShareDialog = false">
</annotation-share-dialog>
</span>
<span ng-if="vm.isThirdPartyUser()">
<button class="btn btn-clean annotation-action-btn"
ng-if="!vm.isFlagged"
ng-click="vm.flag()"
ng-disabled="vm.isDeleted()"
aria-label="Flag"
h-tooltip>
<i class="h-icon-annotation-flag btn-icon"></i>
</button>
<button class="btn btn-clean annotation-action-btn"
ng-if="vm.isFlagged"
ng-disabled="vm.isDeleted()"
aria-label="Annotation has been flagged"
h-tooltip>
<i class="h-icon-annotation-flag annotation--flagged btn-icon"></i>
</button>
</span>
</div>
</footer>
</div>
......@@ -17,6 +17,9 @@ describe('annotationMapper', function() {
annotation: {
delete: sinon.stub().returns(Promise.resolve({})),
},
flag: {
create: sinon.stub().returns(Promise.resolve({})),
},
};
angular.module('app', [])
.service('annotationMapper', require('../annotation-mapper'))
......@@ -124,6 +127,24 @@ describe('annotationMapper', function() {
});
});
describe('#flagAnnotation()', function () {
it('flags an annotation', function () {
var ann = {id: 'test-id'};
annotationMapper.flagAnnotation(ann);
assert.calledOnce(fakeStore.flag.create);
assert.calledWith(fakeStore.flag.create, null, {annotation: ann.id});
});
it('emits the "annotationFlagged" event', function (done) {
sandbox.stub($rootScope, '$broadcast');
var ann = {id: 'test-id'};
annotationMapper.flagAnnotation(ann).then(function () {
assert.calledWith($rootScope.$broadcast,
events.ANNOTATION_FLAGGED, ann);
}).then(done, done);
});
});
describe('#createAnnotation()', function () {
it('creates a new annotation resource', function () {
var ann = {};
......
......@@ -67,6 +67,12 @@ describe('store', function () {
url: 'http://example.com/api/annotations/:id',
},
},
flag: {
create: {
method: 'POST',
url: 'http://example.com/api/flags',
},
},
search: {
method: 'GET',
url: 'http://example.com/api/search',
......@@ -123,6 +129,18 @@ describe('store', function () {
$httpBackend.flush();
});
it('flags an annotation', function (done) {
store.flag.create(null, {annotation: 'an-id'}).then(function () {
done();
});
$httpBackend.expectPOST('http://example.com/api/flags')
.respond(function () {
return [204, {}, {}];
});
$httpBackend.flush();
});
it('removes internal properties before sending data to the server', function (done) {
var annotation = {
$highlight: true,
......
......@@ -261,3 +261,8 @@
margin-bottom: $layout-h-margin - 3px;
}
}
.annotation--flagged {
color: $brand-color;
cursor: default;
}
This source diff could not be displayed because it is too large. You can view the blob instead.
@font-face {
font-family: 'h';
font-family: 'h';
src: url('../fonts/h.woff') format('woff');
font-weight: normal;
font-style: normal;
font-weight: normal;
font-style: normal;
}
[class^="h-icon-"], [class*=" h-icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'h' !important;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'h' !important;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.h-icon-annotation-flag:before {
content: "\e90d";
}
.h-icon-clipboard:before {
content: "\e90c";
content: "\e90c";
}
.h-icon-hypothesis-logo:before {
content: "\e90b";
content: "\e90b";
}
.h-icon-annotation-edit:before {
content: "\e907";
content: "\e907";
}
.h-icon-annotation-reply:before {
content: "\e90a";
content: "\e90a";
}
.h-icon-annotation-delete:before {
content: "\e908";
content: "\e908";
}
.h-icon-annotation-share:before {
content: "\e909";
content: "\e909";
}
.h-icon-google-plus:before {
content: "\e906";
content: "\e906";
}
.h-icon-annotate:before {
content: "\e903";
content: "\e903";
}
.h-icon-highlight:before {
content: "\e904";
content: "\e904";
}
.h-icon-note:before {
content: "\e905";
content: "\e905";
}
.h-icon-account:before {
content: "\e800";
content: "\e800";
}
.h-icon-sort:before {
content: "\e801";
content: "\e801";
}
.h-icon-group:before {
content: "\e61e";
content: "\e61e";
}
.h-icon-cancel-outline:before {
content: "\e619";
content: "\e619";
}
.h-icon-google-plus-old:before {
content: "\ea88";
content: "\ea88";
}
.h-icon-facebook:before {
content: "\ea8d";
content: "\ea8d";
}
.h-icon-twitter:before {
content: "\ea91";
content: "\ea91";
}
.h-icon-github:before {
content: "\e900";
content: "\e900";
}
.h-icon-feed:before {
content: "\e901";
content: "\e901";
}
.h-icon-cc-by:before {
content: "\e61f";
content: "\e61f";
}
.h-icon-cc-logo:before {
content: "\e620";
content: "\e620";
}
.h-icon-cc-zero:before {
content: "\e621";
content: "\e621";
}
.h-icon-markdown:before {
content: "\e60b";
content: "\e60b";
}
.h-icon-move:before {
content: "\e902";
content: "\e902";
}
.h-icon-arrow-right:before {
content: "\e61d";
content: "\e61d";
}
.h-icon-arrow-drop-down:before {
content: "\e629";
content: "\e629";
}
.h-icon-link:before {
content: "\e628";
content: "\e628";
}
.h-icon-create:before {
content: "\e627";
content: "\e627";
}
.h-icon-delete:before {
content: "\e624";
content: "\e624";
}
.h-icon-remove:before {
content: "\e625";
content: "\e625";
}
.h-icon-edit:before {
content: "\e626";
content: "\e626";
}
.h-icon-bookmark:before {
content: "\e600";
content: "\e600";
}
.h-icon-done:before {
content: "\e601";
content: "\e601";
}
.h-icon-lock:before {
content: "\e602";
content: "\e602";
}
.h-icon-search:before {
content: "\e603";
content: "\e603";
}
.h-icon-settings:before {
content: "\e604";
content: "\e604";
}
.h-icon-visibility:before {
content: "\e605";
content: "\e605";
}
.h-icon-visibility-off:before {
content: "\e606";
content: "\e606";
}
.h-icon-add:before {
content: "\e608";
content: "\e608";
}
.h-icon-clear:before {
content: "\e609";
content: "\e609";
}
.h-icon-content-copy:before {
content: "\e60a";
content: "\e60a";
}
.h-icon-flag:before {
content: "\e60c";
content: "\e60c";
}
.h-icon-reply:before {
content: "\e60d";
content: "\e60d";
}
.h-icon-border-color:before {
content: "\e60e";
content: "\e60e";
}
.h-icon-format-bold:before {
content: "\e60f";
content: "\e60f";
}
.h-icon-format-italic:before {
content: "\e610";
content: "\e610";
}
.h-icon-format-list-bulleted:before {
content: "\e611";
content: "\e611";
}
.h-icon-format-list-numbered:before {
content: "\e612";
content: "\e612";
}
.h-icon-format-quote:before {
content: "\e613";
content: "\e613";
}
.h-icon-functions:before {
content: "\e614";
content: "\e614";
}
.h-icon-insert-comment:before {
content: "\e617";
content: "\e617";
}
.h-icon-insert-link:before {
content: "\e615";
content: "\e615";
}
.h-icon-insert-photo:before {
content: "\e616";
content: "\e616";
}
.h-icon-cancel:before {
content: "\e61a";
content: "\e61a";
}
.h-icon-check:before {
content: "\e61b";
content: "\e61b";
}
.h-icon-chevron-left:before {
content: "\e607";
content: "\e607";
}
.h-icon-chevron-right:before {
content: "\e618";
content: "\e618";
}
.h-icon-close:before {
content: "\e61c";
content: "\e61c";
}
.h-icon-public:before {
content: "\e622";
content: "\e622";
}
.h-icon-share:before {
content: "\e623";
content: "\e623";
}
.h-icon-mail:before {
content: "\e62a";
content: "\e62a";
}
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