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) { ...@@ -60,6 +60,7 @@ function analytics($analytics, $window, settings) {
events: { events: {
ANNOTATION_CREATED: 'annotationCreated', ANNOTATION_CREATED: 'annotationCreated',
ANNOTATION_DELETED: 'annotationDeleted', ANNOTATION_DELETED: 'annotationDeleted',
ANNOTATION_FLAGGED: 'annotationFlagged',
ANNOTATION_UPDATED: 'annotationUpdated', ANNOTATION_UPDATED: 'annotationUpdated',
HIGHLIGHT_CREATED: 'highlightCreated', HIGHLIGHT_CREATED: 'highlightCreated',
HIGHLIGHT_UPDATED: 'highlightUpdated', HIGHLIGHT_UPDATED: 'highlightUpdated',
......
...@@ -54,11 +54,21 @@ function annotationMapper($rootScope, annotationUI, store) { ...@@ -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 { return {
loadAnnotations: loadAnnotations, loadAnnotations: loadAnnotations,
unloadAnnotations: unloadAnnotations, unloadAnnotations: unloadAnnotations,
createAnnotation: createAnnotation, createAnnotation: createAnnotation,
deleteAnnotation: deleteAnnotation, deleteAnnotation: deleteAnnotation,
flagAnnotation: flagAnnotation,
}; };
} }
......
...@@ -220,6 +220,22 @@ function AnnotationController( ...@@ -220,6 +220,22 @@ function AnnotationController(
return permissions.permits(action, vm.annotation, session.state.userid); 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 * @ngdoc method
* @name annotation.AnnotationController#delete * @name annotation.AnnotationController#delete
......
...@@ -138,6 +138,7 @@ describe('annotation', function() { ...@@ -138,6 +138,7 @@ describe('annotation', function() {
}, },
}), }),
deleteAnnotation: sandbox.stub(), deleteAnnotation: sandbox.stub(),
flagAnnotation: sandbox.stub(),
}; };
var fakeAnnotationUI = {}; var fakeAnnotationUI = {};
...@@ -689,6 +690,47 @@ describe('annotation', function() { ...@@ -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 () { describe('#documentMeta()', function () {
it('returns the domain, title link and text for the annotation', function () { it('returns the domain, title link and text for the annotation', function () {
var annot = fixtures.defaultAnnotation(); var annot = fixtures.defaultAnnotation();
......
...@@ -31,6 +31,9 @@ module.exports = { ...@@ -31,6 +31,9 @@ module.exports = {
/** An annotation was either deleted or unloaded. */ /** An annotation was either deleted or unloaded. */
ANNOTATION_DELETED: 'annotationDeleted', ANNOTATION_DELETED: 'annotationDeleted',
/** An annotation was flagged. */
ANNOTATION_FLAGGED: 'annotationFlagged',
/** An annotation has been updated. */ /** An annotation has been updated. */
ANNOTATION_UPDATED: 'annotationUpdated', ANNOTATION_UPDATED: 'annotationUpdated',
......
...@@ -143,6 +143,9 @@ function store($http, $q, auth, settings) { ...@@ -143,6 +143,9 @@ function store($http, $q, auth, settings) {
get: apiCall('annotation.read'), get: apiCall('annotation.read'),
update: apiCall('annotation.update'), update: apiCall('annotation.update'),
}, },
flag: {
create: apiCall('flag.create'),
},
profile: { profile: {
read: apiCall('profile.read'), read: apiCall('profile.read'),
update: apiCall('profile.update'), update: apiCall('profile.update'),
......
...@@ -185,6 +185,23 @@ ...@@ -185,6 +185,23 @@
on-close="vm.showShareDialog = false"> on-close="vm.showShareDialog = false">
</annotation-share-dialog> </annotation-share-dialog>
</span> </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> </div>
</footer> </footer>
</div> </div>
...@@ -17,6 +17,9 @@ describe('annotationMapper', function() { ...@@ -17,6 +17,9 @@ describe('annotationMapper', function() {
annotation: { annotation: {
delete: sinon.stub().returns(Promise.resolve({})), delete: sinon.stub().returns(Promise.resolve({})),
}, },
flag: {
create: sinon.stub().returns(Promise.resolve({})),
},
}; };
angular.module('app', []) angular.module('app', [])
.service('annotationMapper', require('../annotation-mapper')) .service('annotationMapper', require('../annotation-mapper'))
...@@ -124,6 +127,24 @@ describe('annotationMapper', function() { ...@@ -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 () { describe('#createAnnotation()', function () {
it('creates a new annotation resource', function () { it('creates a new annotation resource', function () {
var ann = {}; var ann = {};
......
...@@ -67,6 +67,12 @@ describe('store', function () { ...@@ -67,6 +67,12 @@ describe('store', function () {
url: 'http://example.com/api/annotations/:id', url: 'http://example.com/api/annotations/:id',
}, },
}, },
flag: {
create: {
method: 'POST',
url: 'http://example.com/api/flags',
},
},
search: { search: {
method: 'GET', method: 'GET',
url: 'http://example.com/api/search', url: 'http://example.com/api/search',
...@@ -123,6 +129,18 @@ describe('store', function () { ...@@ -123,6 +129,18 @@ describe('store', function () {
$httpBackend.flush(); $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) { it('removes internal properties before sending data to the server', function (done) {
var annotation = { var annotation = {
$highlight: true, $highlight: true,
......
...@@ -261,3 +261,8 @@ ...@@ -261,3 +261,8 @@
margin-bottom: $layout-h-margin - 3px; 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-face {
font-family: 'h'; font-family: 'h';
src: url('../fonts/h.woff') format('woff'); src: url('../fonts/h.woff') format('woff');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
[class^="h-icon-"], [class*=" h-icon-"] { [class^="h-icon-"], [class*=" h-icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */ /* use !important to prevent issues with browser extensions that change fonts */
font-family: 'h' !important; font-family: 'h' !important;
speak: none; speak: none;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
font-variant: normal; font-variant: normal;
text-transform: none; text-transform: none;
line-height: 1; line-height: 1;
/* Better Font Rendering =========== */ /* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.h-icon-annotation-flag:before {
content: "\e90d";
}
.h-icon-clipboard:before { .h-icon-clipboard:before {
content: "\e90c"; content: "\e90c";
} }
.h-icon-hypothesis-logo:before { .h-icon-hypothesis-logo:before {
content: "\e90b"; content: "\e90b";
} }
.h-icon-annotation-edit:before { .h-icon-annotation-edit:before {
content: "\e907"; content: "\e907";
} }
.h-icon-annotation-reply:before { .h-icon-annotation-reply:before {
content: "\e90a"; content: "\e90a";
} }
.h-icon-annotation-delete:before { .h-icon-annotation-delete:before {
content: "\e908"; content: "\e908";
} }
.h-icon-annotation-share:before { .h-icon-annotation-share:before {
content: "\e909"; content: "\e909";
} }
.h-icon-google-plus:before { .h-icon-google-plus:before {
content: "\e906"; content: "\e906";
} }
.h-icon-annotate:before { .h-icon-annotate:before {
content: "\e903"; content: "\e903";
} }
.h-icon-highlight:before { .h-icon-highlight:before {
content: "\e904"; content: "\e904";
} }
.h-icon-note:before { .h-icon-note:before {
content: "\e905"; content: "\e905";
} }
.h-icon-account:before { .h-icon-account:before {
content: "\e800"; content: "\e800";
} }
.h-icon-sort:before { .h-icon-sort:before {
content: "\e801"; content: "\e801";
} }
.h-icon-group:before { .h-icon-group:before {
content: "\e61e"; content: "\e61e";
} }
.h-icon-cancel-outline:before { .h-icon-cancel-outline:before {
content: "\e619"; content: "\e619";
} }
.h-icon-google-plus-old:before { .h-icon-google-plus-old:before {
content: "\ea88"; content: "\ea88";
} }
.h-icon-facebook:before { .h-icon-facebook:before {
content: "\ea8d"; content: "\ea8d";
} }
.h-icon-twitter:before { .h-icon-twitter:before {
content: "\ea91"; content: "\ea91";
} }
.h-icon-github:before { .h-icon-github:before {
content: "\e900"; content: "\e900";
} }
.h-icon-feed:before { .h-icon-feed:before {
content: "\e901"; content: "\e901";
} }
.h-icon-cc-by:before { .h-icon-cc-by:before {
content: "\e61f"; content: "\e61f";
} }
.h-icon-cc-logo:before { .h-icon-cc-logo:before {
content: "\e620"; content: "\e620";
} }
.h-icon-cc-zero:before { .h-icon-cc-zero:before {
content: "\e621"; content: "\e621";
} }
.h-icon-markdown:before { .h-icon-markdown:before {
content: "\e60b"; content: "\e60b";
} }
.h-icon-move:before { .h-icon-move:before {
content: "\e902"; content: "\e902";
} }
.h-icon-arrow-right:before { .h-icon-arrow-right:before {
content: "\e61d"; content: "\e61d";
} }
.h-icon-arrow-drop-down:before { .h-icon-arrow-drop-down:before {
content: "\e629"; content: "\e629";
} }
.h-icon-link:before { .h-icon-link:before {
content: "\e628"; content: "\e628";
} }
.h-icon-create:before { .h-icon-create:before {
content: "\e627"; content: "\e627";
} }
.h-icon-delete:before { .h-icon-delete:before {
content: "\e624"; content: "\e624";
} }
.h-icon-remove:before { .h-icon-remove:before {
content: "\e625"; content: "\e625";
} }
.h-icon-edit:before { .h-icon-edit:before {
content: "\e626"; content: "\e626";
} }
.h-icon-bookmark:before { .h-icon-bookmark:before {
content: "\e600"; content: "\e600";
} }
.h-icon-done:before { .h-icon-done:before {
content: "\e601"; content: "\e601";
} }
.h-icon-lock:before { .h-icon-lock:before {
content: "\e602"; content: "\e602";
} }
.h-icon-search:before { .h-icon-search:before {
content: "\e603"; content: "\e603";
} }
.h-icon-settings:before { .h-icon-settings:before {
content: "\e604"; content: "\e604";
} }
.h-icon-visibility:before { .h-icon-visibility:before {
content: "\e605"; content: "\e605";
} }
.h-icon-visibility-off:before { .h-icon-visibility-off:before {
content: "\e606"; content: "\e606";
} }
.h-icon-add:before { .h-icon-add:before {
content: "\e608"; content: "\e608";
} }
.h-icon-clear:before { .h-icon-clear:before {
content: "\e609"; content: "\e609";
} }
.h-icon-content-copy:before { .h-icon-content-copy:before {
content: "\e60a"; content: "\e60a";
} }
.h-icon-flag:before { .h-icon-flag:before {
content: "\e60c"; content: "\e60c";
} }
.h-icon-reply:before { .h-icon-reply:before {
content: "\e60d"; content: "\e60d";
} }
.h-icon-border-color:before { .h-icon-border-color:before {
content: "\e60e"; content: "\e60e";
} }
.h-icon-format-bold:before { .h-icon-format-bold:before {
content: "\e60f"; content: "\e60f";
} }
.h-icon-format-italic:before { .h-icon-format-italic:before {
content: "\e610"; content: "\e610";
} }
.h-icon-format-list-bulleted:before { .h-icon-format-list-bulleted:before {
content: "\e611"; content: "\e611";
} }
.h-icon-format-list-numbered:before { .h-icon-format-list-numbered:before {
content: "\e612"; content: "\e612";
} }
.h-icon-format-quote:before { .h-icon-format-quote:before {
content: "\e613"; content: "\e613";
} }
.h-icon-functions:before { .h-icon-functions:before {
content: "\e614"; content: "\e614";
} }
.h-icon-insert-comment:before { .h-icon-insert-comment:before {
content: "\e617"; content: "\e617";
} }
.h-icon-insert-link:before { .h-icon-insert-link:before {
content: "\e615"; content: "\e615";
} }
.h-icon-insert-photo:before { .h-icon-insert-photo:before {
content: "\e616"; content: "\e616";
} }
.h-icon-cancel:before { .h-icon-cancel:before {
content: "\e61a"; content: "\e61a";
} }
.h-icon-check:before { .h-icon-check:before {
content: "\e61b"; content: "\e61b";
} }
.h-icon-chevron-left:before { .h-icon-chevron-left:before {
content: "\e607"; content: "\e607";
} }
.h-icon-chevron-right:before { .h-icon-chevron-right:before {
content: "\e618"; content: "\e618";
} }
.h-icon-close:before { .h-icon-close:before {
content: "\e61c"; content: "\e61c";
} }
.h-icon-public:before { .h-icon-public:before {
content: "\e622"; content: "\e622";
} }
.h-icon-share:before { .h-icon-share:before {
content: "\e623"; content: "\e623";
} }
.h-icon-mail:before { .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