diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index 01e183a9b..3bf787a9c 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -10,6 +10,8 @@ import React, {PropTypes} from 'react'; import PermalinkButton from 'coral-plugin-permalinks/PermalinkButton'; import AuthorName from 'coral-plugin-author-name/AuthorName'; + +import TagLabel from 'coral-plugin-tag-label/TagLabel'; import Content from 'coral-plugin-commentcontent/CommentContent'; import PubDate from 'coral-plugin-pubdate/PubDate'; import {ReplyBox, ReplyButton} from 'coral-plugin-replies'; @@ -20,6 +22,7 @@ import styles from './Comment.css'; const getActionSummary = (type, comment) => comment.action_summaries .filter((a) => a.__typename === type)[0]; +const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF') ; class Comment extends React.Component { @@ -56,12 +59,16 @@ class Comment extends React.Component { action_summaries: PropTypes.array.isRequired, body: PropTypes.string.isRequired, id: PropTypes.string.isRequired, + tags: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string + }) + ), replies: PropTypes.arrayOf( PropTypes.shape({ body: PropTypes.string.isRequired, id: PropTypes.string.isRequired - }) - ), + })), user: PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired @@ -98,6 +105,9 @@ class Comment extends React.Component {
+ { isStaff(comment.tags) + ? + : null }
diff --git a/client/coral-framework/graphql/fragments/commentView.graphql b/client/coral-framework/graphql/fragments/commentView.graphql index 575434a31..123ce9a09 100644 --- a/client/coral-framework/graphql/fragments/commentView.graphql +++ b/client/coral-framework/graphql/fragments/commentView.graphql @@ -5,6 +5,9 @@ fragment commentView on Comment { body created_at status + tags { + name + } user { id name: displayName diff --git a/client/coral-plugin-stream/Stream.js b/client/coral-plugin-stream/Stream.js index 633a98502..c62ef7081 100644 --- a/client/coral-plugin-stream/Stream.js +++ b/client/coral-plugin-stream/Stream.js @@ -58,6 +58,9 @@ const StreamQuery = gql`fragment commentView on Comment { user { name: displayName } + tags { + name + } actions { type: action_type count diff --git a/client/coral-plugin-tag-label/TagLabel.js b/client/coral-plugin-tag-label/TagLabel.js new file mode 100644 index 000000000..0bceca225 --- /dev/null +++ b/client/coral-plugin-tag-label/TagLabel.js @@ -0,0 +1,9 @@ +import React from 'react'; + +import styles from './styles.css'; + +const TagLabel = ({isStaff}) =>
+ {isStaff ? 'Staff' : ''} +
; + +export default TagLabel; diff --git a/client/coral-plugin-tag-label/styles.css b/client/coral-plugin-tag-label/styles.css new file mode 100644 index 000000000..03dc3dbc8 --- /dev/null +++ b/client/coral-plugin-tag-label/styles.css @@ -0,0 +1,7 @@ +.staff { + background-color: #4C1066; + color: white; + display: inline-block; + margin: 10px 10px; + padding: 5px 5px; +} diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 2f18ae135..6214c9df8 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -36,6 +36,12 @@ const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = } } + if (user.hasRoles('ADMIN')) { + return CommentsService + .addTag(comment.id, 'STAFF', user.id) + .then(() => comment); + } + return comment; }); }; diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index e231bc0c3..ddffcd708 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -5,6 +5,7 @@ # Date represented as an ISO8601 string. scalar Date + ################################################################################ ## Users ################################################################################ @@ -45,6 +46,17 @@ type User { comments(query: CommentsQuery): [Comment] } +type Tag { + # the actual tag for the comment. + name: String! + + # the user that assigned the tag. If NULL then the system automatically tagged it. + assigned_by: String + + # the time when the tag was assigned. + created_at: Date! +} + ################################################################################ ## Comments ################################################################################ @@ -96,6 +108,9 @@ input CommentsQuery { # skip results from the last created_at timestamp. cursor: Date + # filter by a specific tag name. + tag: [String] + # sort the results by created_at. sort: SORT_ORDER = REVERSE_CHRONOLOGICAL } @@ -109,7 +124,10 @@ type Comment { # The actual comment data. body: String! - # The user who authored the comment. + # the tags on the comment + tags: [Tag] + + # the user who authored the comment. user: User # the recent replies made against this comment. diff --git a/models/comment.js b/models/comment.js index b656f9238..ac44d904e 100644 --- a/models/comment.js +++ b/models/comment.js @@ -30,6 +30,24 @@ const StatusSchema = new Schema({ _id: false }); +/** + * The Mongo schema for a Comment Tag. + * @type {Schema} + */ +const TagSchema = new Schema({ + name: String, + + // The User ID of the user that assigned the status. + assigned_by: { + type: String, + default: null + }, + + created_at: Date +}, { + _id: false +}); + /** * The Mongo schema for a Comment. * @type {Schema} @@ -49,6 +67,7 @@ const CommentSchema = new Schema({ author_id: String, status_history: [StatusSchema], status: {type: String, default: null}, + tags: [TagSchema], parent_id: String }, { timestamps: { diff --git a/services/comments.js b/services/comments.js index 5d39a129e..fc05341cb 100644 --- a/services/comments.js +++ b/services/comments.js @@ -3,6 +3,10 @@ const CommentModel = require('../models/comment'); const ActionModel = require('../models/action'); const ActionsService = require('./actions'); +const ALLOWED_TAGS = [ + {name: 'STAFF'} +]; + module.exports = class CommentsService { /** @@ -33,6 +37,7 @@ module.exports = class CommentsService { type: status, created_at: new Date() }] : [], + tags: [], status, author_id }); @@ -40,6 +45,33 @@ module.exports = class CommentsService { return comment.save(); } + /** + * Adds a tag if it doesn't already exist on the comment. + */ + static addTag(id, name, assigned_by) { + + if (ALLOWED_TAGS.find((t) => t.name === name) == null) { + return Promise.reject(new Error('tag not allowed')); + } + + return CommentModel.update({ + id, + tags: { + $ne: { + name + } + } + }, { + $push: { + tags: { + name, + assigned_by, + created_at: new Date() + } + } + }); + } + /** * Finds a comment by the id. * @param {String} id identifier of comment (uuid) diff --git a/test/services/comments.js b/test/services/comments.js index fc2105cb1..c27050ae7 100644 --- a/test/services/comments.js +++ b/test/services/comments.js @@ -70,11 +70,14 @@ describe('services.CommentsService', () => { const users = [{ email: 'stampi@gmail.com', displayName: 'Stampi', - password: '1Coral!!' + password: '1Coral!!', + roles: ['ADMIN'], + _id: '1' }, { email: 'sockmonster@gmail.com', displayName: 'Sockmonster', - password: '2Coral!!' + password: '2Coral!!', + _id : '2' }]; const actions = [{ @@ -256,4 +259,40 @@ describe('services.CommentsService', () => { }); }); + + describe('#tagByStaff()', () => { + + it('creates a new comment by admin', () => { + return UsersService.findLocalUser('stampi@gmail.com', '1Coral!!').then((user) => { + return UsersService.addRoleToUser(user.id, 'ADMIN').then(() => { + UsersService.findById(user.id).then((u) => { + return CommentsService + .publicCreate({ + body: 'This is a comment!', + status: 'ACCEPTED', + author_id: u.id + }).then((c) => { + expect(c).to.not.be.null; + expect(c.tags).to.not.have.length(0); + expect(c.tags[0].name).to.be.equal('STAFF'); + }); + }); + }); + }); + }); + + it('creates a new comment by non admin', () => { + return UsersService.findLocalUser('sockmonster@gmail.com', '2Coral!!').then((user) => { + return CommentsService + .publicCreate({ + body: 'This is a comment!', + status: 'ACCEPTED', + author_id: user.id + }).then((c) => { + expect(c).to.not.be.null; + expect(c.tags).to.have.length(0); + }); + }); + }); + }); });