diff --git a/apps/comments/css/comments.css b/apps/comments/css/comments.css index 5e247aaeb71..b86ed38efe7 100644 --- a/apps/comments/css/comments.css +++ b/apps/comments/css/comments.css @@ -50,6 +50,33 @@ right: 0; } +#commentsTabView .comment .action { + opacity: 0; + vertical-align: middle; + display: inline-block; +} + +#commentsTabView .comment:hover .action { + opacity: 0.3; +} + +#commentsTabView .comment .action:hover { + opacity: 1; +} + +#commentsTabView .comment .action.delete { + position: absolute; + right: 0; +} + +#commentsTabView .comment.disabled { + opacity: 0.3; +} + +#commentsTabView .comment.disabled .action { + visibility: hidden; +} + .app-files .action-comment>img { margin-right: 5px; } diff --git a/apps/comments/js/commentmodel.js b/apps/comments/js/commentmodel.js index ba04fd61de3..89492707b61 100644 --- a/apps/comments/js/commentmodel.js +++ b/apps/comments/js/commentmodel.js @@ -39,8 +39,17 @@ }, parse: function(data) { - data.isUnread = (data.isUnread === 'true'); - return data; + return { + id: data.id, + message: data.message, + actorType: data.actorType, + actorId: data.actorId, + actorDisplayName: data.actorDisplayName, + creationDateTime: data.creationDateTime, + objectType: data.objectType, + objectId: data.objectId, + isUnread: (data.isUnread === 'true') + }; } }); diff --git a/apps/comments/js/commentstabview.js b/apps/comments/js/commentstabview.js index 188d8c5943c..2c5e9414751 100644 --- a/apps/comments/js/commentstabview.js +++ b/apps/comments/js/commentstabview.js @@ -8,28 +8,38 @@ * */ +/* global Handlebars */ + (function(OC, OCA) { var TEMPLATE = - '
' + - '
' + - ' {{#if avatarEnabled}}' + - '
' + - ' {{/if}}' + - '
{{userDisplayName}}
' + - '
' + - '
' + - ' ' + - ' ' + - ' '+ - '
' + - ' ' + - '
' + + '' + '' + '' + ''; + var EDIT_COMMENT_TEMPLATE = + '
' + + '
' + + ' {{#if avatarEnabled}}' + + '
' + + ' {{/if}}' + + '
{{actorDisplayName}}
' + + '{{#if isEditMode}}' + + ' ' + + '{{/if}}' + + '
' + + '
' + + ' ' + + ' ' + + '{{#if isEditMode}}' + + ' ' + + '{{/if}}' + + ' '+ + '
' + + '
'; + var COMMENT_TEMPLATE = '
  • ' + '
    ' + @@ -37,6 +47,9 @@ '
    ' + ' {{/if}}' + '
    {{actorDisplayName}}
    ' + + '{{#if isUserAuthor}}' + + ' ' + + '{{/if}}' + '
    {{date}}
    ' + '
    ' + '
    {{{formattedMessage}}}
    ' + @@ -52,7 +65,10 @@ events: { 'submit .newCommentForm': '_onSubmitComment', - 'click .showMore': '_onClickShowMore' + 'click .showMore': '_onClickShowMore', + 'click .action.edit': '_onClickEditComment', + 'click .action.delete': '_onClickDeleteComment', + 'click .cancel': '_onClickCloseComment' }, initialize: function() { @@ -65,7 +81,6 @@ this._avatarsEnabled = !!OC.config.enable_avatars; // TODO: error handling - _.bindAll(this, '_onSubmitComment'); }, template: function(params) { @@ -75,10 +90,24 @@ var currentUser = OC.getCurrentUser(); return this._template(_.extend({ avatarEnabled: this._avatarsEnabled, - userId: currentUser.uid, - userDisplayName: currentUser.displayName, + actorId: currentUser.uid, + actorDisplayName: currentUser.displayName + }, params)); + }, + + editCommentTemplate: function(params) { + if (!this._editCommentTemplate) { + this._editCommentTemplate = Handlebars.compile(EDIT_COMMENT_TEMPLATE); + } + var currentUser = OC.getCurrentUser(); + return this._editCommentTemplate(_.extend({ + avatarEnabled: this._avatarsEnabled, + actorId: currentUser.uid, + actorDisplayName: currentUser.displayName, newMessagePlaceholder: t('comments', 'Type in a new comment...'), - submitText: t('comments', 'Post') + deleteTooltip: t('comments', 'Delete comment'), + submitText: t('comments', 'Post'), + cancelText: t('comments', 'Cancel') }, params)); }, @@ -87,7 +116,9 @@ this._commentTemplate = Handlebars.compile(COMMENT_TEMPLATE); } return this._commentTemplate(_.extend({ - avatarEnabled: this._avatarsEnabled + avatarEnabled: this._avatarsEnabled, + editTooltip: t('comments', 'Edit comment'), + isUserAuthor: OC.getCurrentUser().uid === params.actorId }, params)); }, @@ -115,6 +146,7 @@ emptyResultLabel: t('comments', 'No other comments available'), moreLabel: t('comments', 'More comments...') })); + this.$el.find('.comments').before(this.editCommentTemplate({})); this.$el.find('.has-tooltip').tooltip(); this.$container = this.$el.find('ul.comments'); this.$el.find('.avatar').avatar(OC.getCurrentUser().uid, 28); @@ -136,9 +168,11 @@ this.$el.find('.loading').toggleClass('hidden', !state); }, - _onRequest: function() { - this._toggleLoading(true); - this.$el.find('.showMore').addClass('hidden'); + _onRequest: function(type) { + if (type === 'REPORT') { + this._toggleLoading(true); + this.$el.find('.showMore').addClass('hidden'); + } }, _onEndRequest: function(type) { @@ -203,13 +237,69 @@ this.collection.fetchNext(); }, + _onClickEditComment: function(ev) { + ev.preventDefault(); + var $comment = $(ev.target).closest('.comment'); + var commentId = $comment.data('id'); + var commentToEdit = this.collection.get(commentId); + var $formRow = $(this.editCommentTemplate(_.extend({ + isEditMode: true, + submitText: t('comments', 'Save') + }, commentToEdit.attributes))); + + $comment.addClass('hidden'); + // spawn form + $comment.after($formRow); + $formRow.data('commentEl', $comment); + + // copy avatar element from original to avoid flickering + $formRow.find('.avatar').replaceWith($comment.find('.avatar').clone()); + $formRow.find('.has-tooltip').tooltip(); + + return false; + }, + + _onClickCloseComment: function(ev) { + ev.preventDefault(); + var $row = $(ev.target).closest('.comment'); + $row.data('commentEl').removeClass('hidden'); + $row.remove(); + return false; + }, + + _onClickDeleteComment: function(ev) { + ev.preventDefault(); + var $comment = $(ev.target).closest('.comment'); + var commentId = $comment.data('id'); + var $loading = $comment.find('.submitLoading'); + + $comment.addClass('disabled'); + $loading.removeClass('hidden'); + this.collection.get(commentId).destroy({ + success: function() { + $comment.data('commentEl').remove(); + $comment.remove(); + }, + error: function(msg) { + $loading.addClass('hidden'); + $comment.removeClass('disabled'); + OC.Notification.showTemporary(msg); + } + }); + + + return false; + }, + _onClickShowMore: function(ev) { ev.preventDefault(); this.nextPage(); }, _onSubmitComment: function(e) { + var self = this; var $form = $(e.target); + var commentId = $form.closest('.comment').data('id'); var currentUser = OC.getCurrentUser(); var $submit = $form.find('.submit'); var $loading = $form.find('.submitLoading'); @@ -225,28 +315,56 @@ $submit.addClass('hidden'); $loading.removeClass('hidden'); - this.collection.create({ - actorId: currentUser.uid, - actorDisplayName: currentUser.displayName, - actorType: 'users', - verb: 'comment', - message: $textArea.val(), - creationDateTime: (new Date()).toUTCString() - }, { - at: 0, - success: function() { - $submit.removeClass('hidden'); - $loading.addClass('hidden'); - $textArea.val('').prop('disabled', false); - }, - error: function(msg) { - $submit.removeClass('hidden'); - $loading.addClass('hidden'); - $textArea.prop('disabled', false); + if (commentId) { + // edit mode + var comment = this.collection.get(commentId); + comment.save({ + message: $textArea.val() + }, { + success: function(model) { + var $row = $form.closest('.comment'); + $submit.removeClass('hidden'); + $loading.addClass('hidden'); + $row.data('commentEl') + .removeClass('hidden') + .find('.message') + .html(self._formatMessage(model.get('message'))); + $row.remove(); + }, + error: function(msg) { + $submit.removeClass('hidden'); + $loading.addClass('hidden'); + $textArea.prop('disabled', false); - OC.Notification.showTemporary(msg); - } - }); + OC.Notification.showTemporary(msg); + } + }); + } else { + this.collection.create({ + actorId: currentUser.uid, + actorDisplayName: currentUser.displayName, + actorType: 'users', + verb: 'comment', + message: $textArea.val(), + creationDateTime: (new Date()).toUTCString() + }, { + at: 0, + // wait for real creation before adding + wait: true, + success: function() { + $submit.removeClass('hidden'); + $loading.addClass('hidden'); + $textArea.val('').prop('disabled', false); + }, + error: function(msg) { + $submit.removeClass('hidden'); + $loading.addClass('hidden'); + $textArea.prop('disabled', false); + + OC.Notification.showTemporary(msg); + } + }); + } return false; } diff --git a/apps/comments/tests/js/commentstabviewSpec.js b/apps/comments/tests/js/commentstabviewSpec.js index 432fa5ddc4c..4c3d38290ba 100644 --- a/apps/comments/tests/js/commentstabviewSpec.js +++ b/apps/comments/tests/js/commentstabviewSpec.js @@ -195,6 +195,140 @@ describe('OCA.Comments.CommentsTabView tests', function() { }); }); + describe('editing comments', function() { + var saveStub; + var currentUserStub; + + beforeEach(function() { + saveStub = sinon.stub(OCA.Comments.CommentModel.prototype, 'save'); + currentUserStub = sinon.stub(OC, 'getCurrentUser'); + currentUserStub.returns({ + uid: 'testuser', + displayName: 'Test User' + }); + view.collection.add({ + id: 1, + actorId: 'testuser', + actorDisplayName: 'Test User', + actorType: 'users', + verb: 'comment', + message: 'New message', + creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString() + }); + view.collection.add({ + id: 2, + actorId: 'anotheruser', + actorDisplayName: 'Another User', + actorType: 'users', + verb: 'comment', + message: 'New message from another user', + creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString() + }); + }); + afterEach(function() { + saveStub.restore(); + currentUserStub.restore(); + }); + + it('shows edit link for owner comments', function() { + var $comment = view.$el.find('.comment[data-id=1]'); + expect($comment.length).toEqual(1); + expect($comment.find('.action.edit').length).toEqual(1); + }); + + it('does not show edit link for other user\'s comments', function() { + var $comment = view.$el.find('.comment[data-id=2]'); + expect($comment.length).toEqual(1); + expect($comment.find('.action.edit').length).toEqual(0); + }); + + it('shows edit form when clicking edit', function() { + var $comment = view.$el.find('.comment[data-id=1]'); + $comment.find('.action.edit').click(); + + expect($comment.hasClass('hidden')).toEqual(true); + var $formRow = view.$el.find('.newCommentRow.comment[data-id=1]'); + expect($formRow.length).toEqual(1); + }); + + it('saves message and updates comment item when clicking save', function() { + var $comment = view.$el.find('.comment[data-id=1]'); + $comment.find('.action.edit').click(); + + var $formRow = view.$el.find('.newCommentRow.comment[data-id=1]'); + expect($formRow.length).toEqual(1); + + $formRow.find('textarea').val('modified\nmessage'); + $formRow.find('form').submit(); + + expect(saveStub.calledOnce).toEqual(true); + expect(saveStub.lastCall.args[0]).toEqual({ + message: 'modified\nmessage' + }); + + var model = view.collection.get(1); + // simulate the fact that save sets the attribute + model.set('message', 'modified\nmessage'); + saveStub.yieldTo('success', model); + + // original comment element is visible again + expect($comment.hasClass('hidden')).toEqual(false); + // and its message was updated + expect($comment.find('.message').html()).toEqual('modified
    message'); + + // form row is gone + $formRow = view.$el.find('.newCommentRow.comment[data-id=1]'); + expect($formRow.length).toEqual(0); + }); + + it('restores original comment when cancelling', function() { + var $comment = view.$el.find('.comment[data-id=1]'); + $comment.find('.action.edit').click(); + + var $formRow = view.$el.find('.newCommentRow.comment[data-id=1]'); + expect($formRow.length).toEqual(1); + + $formRow.find('textarea').val('modified\nmessage'); + $formRow.find('.cancel').click(); + + expect(saveStub.notCalled).toEqual(true); + + // original comment element is visible again + expect($comment.hasClass('hidden')).toEqual(false); + // and its message was not updated + expect($comment.find('.message').html()).toEqual('New message'); + + // form row is gone + $formRow = view.$el.find('.newCommentRow.comment[data-id=1]'); + expect($formRow.length).toEqual(0); + }); + + it('destroys model when clicking delete', function() { + var destroyStub = sinon.stub(OCA.Comments.CommentModel.prototype, 'destroy'); + var $comment = view.$el.find('.comment[data-id=1]'); + $comment.find('.action.edit').click(); + + var $formRow = view.$el.find('.newCommentRow.comment[data-id=1]'); + expect($formRow.length).toEqual(1); + + $formRow.find('.delete').click(); + + expect(destroyStub.calledOnce).toEqual(true); + expect(destroyStub.thisValues[0].id).toEqual(1); + + destroyStub.yieldTo('success'); + + // original comment element is gone + $comment = view.$el.find('.comment[data-id=1]'); + expect($comment.length).toEqual(0); + + // form row is gone + $formRow = view.$el.find('.newCommentRow.comment[data-id=1]'); + expect($formRow.length).toEqual(0); + + destroyStub.restore(); + }); + }); describe('read marker', function() { var updateMarkerStub;