diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f9d9827..61c8a86f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ - Attachments image slider - New admin area to edit the tag colors used in your project - Display the current user (me) at first in assignment lightbox (thanks to [@mikaoelitiana](https://github.com/mikaoelitiana)) +- Add a new permissions to allow add comments instead of use the existent modify permission for this purpose. +- Ability to edit comments, view edition history and redesign comments module UI ### Misc - Lots of small and not so small bugfixes. diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee index 9db8b891..e5d828bf 100644 --- a/app/coffee/app.coffee +++ b/app/coffee/app.coffee @@ -771,6 +771,7 @@ modules = [ "taigaUserTimeline", "taigaExternalApps", "taigaDiscover", + "taigaHistory", # template cache "templates", diff --git a/app/coffee/modules/admin/roles.coffee b/app/coffee/modules/admin/roles.coffee index a8142907..6262fcbc 100644 --- a/app/coffee/modules/admin/roles.coffee +++ b/app/coffee/modules/admin/roles.coffee @@ -367,6 +367,7 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm, $compile) -> { key: "view_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.VIEW_USER_STORIES"} { key: "add_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.ADD_USER_STORIES"} { key: "modify_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.MODIFY_USER_STORIES"} + { key: "comment_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.COMMENT_USER_STORIES"} { key: "delete_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.DELETE_USER_STORIES"} ] categories.push({ @@ -378,6 +379,7 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm, $compile) -> { key: "view_tasks", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.VIEW_TASKS"} { key: "add_task", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.ADD_TASKS"} { key: "modify_task", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.MODIFY_TASKS"} + { key: "comment_task", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.COMMENT_TASKS"} { key: "delete_task", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.DELETE_TASKS"} ] categories.push({ @@ -389,6 +391,7 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm, $compile) -> { key: "view_issues", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.VIEW_ISSUES"} { key: "add_issue", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.ADD_ISSUES"} { key: "modify_issue", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.MODIFY_ISSUES"} + { key: "comment_issue", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.COMMENT_ISSUES"} { key: "delete_issue", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.DELETE_ISSUES"} ] categories.push({ diff --git a/app/coffee/modules/common/history.coffee b/app/coffee/modules/common/history.coffee deleted file mode 100644 index c8017ed2..00000000 --- a/app/coffee/modules/common/history.coffee +++ /dev/null @@ -1,507 +0,0 @@ -### -# Copyright (C) 2014-2016 Andrey Antukh -# Copyright (C) 2014-2016 Jesús Espino Garcia -# Copyright (C) 2014-2016 David Barragán Merino -# Copyright (C) 2014-2016 Alejandro Alonso -# Copyright (C) 2014-2016 Juan Francisco Alcántara -# Copyright (C) 2014-2016 Xavi Julian -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -# File: modules/common/history.coffee -### - -taiga = @.taiga -trim = @.taiga.trim -bindOnce = @.taiga.bindOnce -debounce = @.taiga.debounce - -module = angular.module("taigaCommon") - -IGNORED_FIELDS = { - "userstories.userstory": [ - "watchers", "kanban_order", "backlog_order", "sprint_order", "finish_date", "tribe_gig" - ], - "tasks.task": [ - "watchers", "us_order", "taskboard_order" - ], - "issues.issue": [ - "watchers" - ] -} - -############################################################################# -## History Directive (Main) -############################################################################# - - -class HistoryController extends taiga.Controller - @.$inject = ["$scope", "$tgRepo", "$tgResources"] - - constructor: (@scope, @repo, @rs) -> - - initialize: (type, objectId) -> - @.type = type - @.objectId = objectId - - loadHistory: (type, objectId) -> - return @rs.history.get(type, objectId).then (history) => - for historyResult in history - # If description was modified take only the description_html field - if historyResult.values_diff.description_diff? - historyResult.values_diff.description = historyResult.values_diff.description_diff - - delete historyResult.values_diff.description_html - delete historyResult.values_diff.description_diff - - # If block note was modified take only the blocked_note_html field - if historyResult.values_diff.blocked_note_diff? - historyResult.values_diff.blocked_note = historyResult.values_diff.blocked_note_diff - - delete historyResult.values_diff.blocked_note_html - delete historyResult.values_diff.blocked_note_diff - - for historyEntry in history - changeModel = historyEntry.key.split(":")[0] - if IGNORED_FIELDS[changeModel]? - historyEntry.values_diff = _.removeKeys(historyEntry.values_diff, IGNORED_FIELDS[changeModel]) - - @scope.history = _.filter(history, (item) -> Object.keys(item.values_diff).length > 0) - - @scope.comments = _.filter(history, (item) -> item.comment != "") - - deleteComment: (type, objectId, activityId) -> - return @rs.history.deleteComment(type, objectId, activityId).then => @.loadHistory(type, objectId) - - undeleteComment: (type, objectId, activityId) -> - return @rs.history.undeleteComment(type, objectId, activityId).then => @.loadHistory(type, objectId) - - -HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $compile, $navUrls, $rootScope, checkPermissionsService) -> - templateChangeDiff = $template.get("common/history/history-change-diff.html", true) - templateChangePoints = $template.get("common/history/history-change-points.html", true) - templateChangeGeneric = $template.get("common/history/history-change-generic.html", true) - templateChangeAttachment = $template.get("common/history/history-change-attachment.html", true) - templateChangeList = $template.get("common/history/history-change-list.html", true) - templateDeletedComment = $template.get("common/history/history-deleted-comment.html", true) - templateActivity = $template.get("common/history/history-activity.html", true) - templateBaseEntries = $template.get("common/history/history-base-entries.html", true) - templateBase = $template.get("common/history/history-base.html", true) - - link = ($scope, $el, $attrs, $ctrl) -> - # Bootstraping - type = $attrs.type - objectId = null - - showAllComments = false - showAllActivity = false - - getPrettyDateFormat = -> - return $translate.instant("ACTIVITY.DATETIME") - - bindOnce $scope, $attrs.ngModel, (model) -> - type = $attrs.type - objectId = model.id - - $ctrl.initialize(type, objectId) - $ctrl.loadHistory(type, objectId) - - # Helpers - getHumanizedFieldName = (field) -> - humanizedFieldNames = { - subject : $translate.instant("ACTIVITY.FIELDS.SUBJECT") - name: $translate.instant("ACTIVITY.FIELDS.NAME") - description : $translate.instant("ACTIVITY.FIELDS.DESCRIPTION") - content: $translate.instant("ACTIVITY.FIELDS.CONTENT") - status: $translate.instant("ACTIVITY.FIELDS.STATUS") - is_closed : $translate.instant("ACTIVITY.FIELDS.IS_CLOSED") - finish_date : $translate.instant("ACTIVITY.FIELDS.FINISH_DATE") - type: $translate.instant("ACTIVITY.FIELDS.TYPE") - priority: $translate.instant("ACTIVITY.FIELDS.PRIORITY") - severity: $translate.instant("ACTIVITY.FIELDS.SEVERITY") - assigned_to : $translate.instant("ACTIVITY.FIELDS.ASSIGNED_TO") - watchers : $translate.instant("ACTIVITY.FIELDS.WATCHERS") - milestone : $translate.instant("ACTIVITY.FIELDS.MILESTONE") - user_story: $translate.instant("ACTIVITY.FIELDS.USER_STORY") - project: $translate.instant("ACTIVITY.FIELDS.PROJECT") - is_blocked: $translate.instant("ACTIVITY.FIELDS.IS_BLOCKED") - blocked_note: $translate.instant("ACTIVITY.FIELDS.BLOCKED_NOTE") - points: $translate.instant("ACTIVITY.FIELDS.POINTS") - client_requirement : $translate.instant("ACTIVITY.FIELDS.CLIENT_REQUIREMENT") - team_requirement : $translate.instant("ACTIVITY.FIELDS.TEAM_REQUIREMENT") - is_iocaine: $translate.instant("ACTIVITY.FIELDS.IS_IOCAINE") - tags: $translate.instant("ACTIVITY.FIELDS.TAGS") - attachments : $translate.instant("ACTIVITY.FIELDS.ATTACHMENTS") - is_deprecated: $translate.instant("ACTIVITY.FIELDS.IS_DEPRECATED") - blocked_note: $translate.instant("ACTIVITY.FIELDS.BLOCKED_NOTE") - is_blocked: $translate.instant("ACTIVITY.FIELDS.IS_BLOCKED") - order: $translate.instant("ACTIVITY.FIELDS.ORDER") - backlog_order: $translate.instant("ACTIVITY.FIELDS.BACKLOG_ORDER") - sprint_order: $translate.instant("ACTIVITY.FIELDS.SPRINT_ORDER") - kanban_order: $translate.instant("ACTIVITY.FIELDS.KANBAN_ORDER") - taskboard_order: $translate.instant("ACTIVITY.FIELDS.TASKBOARD_ORDER") - us_order: $translate.instant("ACTIVITY.FIELDS.US_ORDER") - } - - return humanizedFieldNames[field] or field - - countChanges = (comment) -> - return _.keys(comment.values_diff).length - - formatChange = (change) -> - if _.isArray(change) - if change.length == 0 - return $translate.instant("ACTIVITY.VALUES.EMPTY") - return change.join(", ") - - if change == "" - return $translate.instant("ACTIVITY.VALUES.EMPTY") - - if not change? or change == false - return $translate.instant("ACTIVITY.VALUES.NO") - - if change == true - return $translate.instant("ACTIVITY.VALUES.YES") - - return change - - # Render into string (operations without mutability) - - renderAttachmentEntry = (value) -> - attachments = _.map value, (changes, type) -> - if type == "new" - return _.map changes, (change) -> - return templateChangeDiff({ - name: $translate.instant("ACTIVITY.NEW_ATTACHMENT"), - diff: change.filename - }) - else if type == "deleted" - return _.map changes, (change) -> - return templateChangeDiff({ - name: $translate.instant("ACTIVITY.DELETED_ATTACHMENT"), - diff: change.filename - }) - else - return _.map changes, (change) -> - name = $translate.instant("ACTIVITY.UPDATED_ATTACHMENT", {filename: change.filename}) - - diff = _.map change.changes, (values, name) -> - return { - name: getHumanizedFieldName(name) - from: formatChange(values[0]) - to: formatChange(values[1]) - } - - return templateChangeAttachment({name: name, diff: diff}) - - return _.flatten(attachments).join("\n") - - renderCustomAttributesEntry = (value) -> - customAttributes = _.map value, (changes, type) -> - if type == "new" - return _.map changes, (change) -> - html = templateChangeGeneric({ - name: change.name, - from: formatChange(""), - to: formatChange(change.value) - }) - - html = $compile(html)($scope) - - return html[0].outerHTML - else if type == "deleted" - return _.map changes, (change) -> - return templateChangeDiff({ - name: $translate.instant("ACTIVITY.DELETED_CUSTOM_ATTRIBUTE") - diff: change.name - }) - else - return _.map changes, (change) -> - customAttrsChanges = _.map change.changes, (values) -> - return templateChangeGeneric({ - name: change.name - from: formatChange(values[0]) - to: formatChange(values[1]) - }) - return _.flatten(customAttrsChanges).join("\n") - - return _.flatten(customAttributes).join("\n") - - renderChangeEntry = (field, value) -> - if field == "description" - return templateChangeDiff({name: getHumanizedFieldName("description"), diff: value[1]}) - else if field == "blocked_note" - return templateChangeDiff({name: getHumanizedFieldName("blocked_note"), diff: value[1]}) - else if field == "points" - html = templateChangePoints({points: value}) - - html = $compile(html)($scope) - - return html[0].outerHTML - else if field == "attachments" - return renderAttachmentEntry(value) - else if field == "custom_attributes" - return renderCustomAttributesEntry(value) - else if field in ["tags", "watchers"] - name = getHumanizedFieldName(field) - removed = _.difference(value[0], value[1]) - added = _.difference(value[1], value[0]) - html = templateChangeList({name:name, removed:removed, added: added}) - - html = $compile(html)($scope) - - return html[0].outerHTML - else if field == "assigned_to" - name = getHumanizedFieldName(field) - from = formatChange(value[0] or $translate.instant("ACTIVITY.VALUES.UNASSIGNED")) - to = formatChange(value[1] or $translate.instant("ACTIVITY.VALUES.UNASSIGNED")) - return templateChangeGeneric({name:name, from:from, to: to}) - else - name = getHumanizedFieldName(field) - from = formatChange(value[0]) - to = formatChange(value[1]) - return templateChangeGeneric({name:name, from:from, to: to}) - - renderChangeEntries = (change) -> - return _.map(change.values_diff, (value, field) -> renderChangeEntry(field, value)) - - renderChangesHelperText = (change) -> - size = countChanges(change) - return $translate.instant("ACTIVITY.SIZE_CHANGE", {size: size}, 'messageformat') - - renderComment = (comment) -> - if (comment.delete_comment_date or comment.delete_comment_user?.name) - html = templateDeletedComment({ - deleteCommentDate: moment(comment.delete_comment_date).format(getPrettyDateFormat()) if comment.delete_comment_date - deleteCommentUser: comment.delete_comment_user.name - deleteComment: comment.comment_html - activityId: comment.id - canRestoreComment: ($scope.user and - (comment.delete_comment_user.pk == $scope.user.id or - $scope.project.my_permissions.indexOf("modify_project") > -1)) - }) - - html = $compile(html)($scope) - - return html[0].outerHTML - - html = templateActivity({ - avatar: comment.user.photo - userFullName: comment.user.name - userProfileUrl: if comment.user.is_active then $navUrls.resolve("user-profile", {username: comment.user.username}) else "" - creationDate: moment(comment.created_at).format(getPrettyDateFormat()) - comment: comment.comment_html - changesText: renderChangesHelperText(comment) - changes: renderChangeEntries(comment) - mode: "comment" - deleteCommentActionTitle: $translate.instant("COMMENTS.DELETE") - deleteCommentDate: moment(comment.delete_comment_date).format(getPrettyDateFormat()) if comment.delete_comment_date - deleteCommentUser: comment.delete_comment_user.name if comment.delete_comment_user?.name - activityId: comment.id - canDeleteComment: comment.user.pk == $scope.user?.id or $scope.project.my_permissions.indexOf("modify_project") > -1 - }) - - html = $compile(html)($scope) - - return html[0].outerHTML - - renderChange = (change) -> - return templateActivity({ - avatar: change.user.photo - userFullName: change.user.name - userProfileUrl: if change.user.is_active then $navUrls.resolve("user-profile", {username: change.user.username}) else "" - creationDate: moment(change.created_at).format(getPrettyDateFormat()) - comment: change.comment_html - changes: renderChangeEntries(change) - changesText: "" - mode: "activity" - deleteCommentDate: moment(change.delete_comment_date).format(getPrettyDateFormat()) if change.delete_comment_date - deleteCommentUser: change.delete_comment_user.name if change.delete_comment_user?.name - activityId: change.id - }) - - renderHistory = (entries, totalEntries) -> - if entries.length == totalEntries - showMore = 0 - else - showMore = totalEntries - entries.length - - html = templateBaseEntries({entries: entries, showMore:showMore}) - html = $compile(html)($scope) - return html - - # Render into DOM (operations with dom mutability) - - renderBase = -> - comments = $scope.comments or [] - changes = $scope.history or [] - - historyVisible = !!changes.length - commentsVisible = (!!comments.length) || checkPermissionsService.check('modify_' + $attrs.type) - - html = templateBase({ - ngmodel: $attrs.ngModel, - type: $attrs.type, - mode: $attrs.mode, - historyVisible: historyVisible, - commentsVisible: commentsVisible - }) - - html = $compile(html)($scope) - - $el.html(html) - - rerender = -> - renderBase() - renderComments() - renderActivity() - - renderComments = -> - comments = $scope.comments or [] - totalComments = comments.length - - if not showAllComments - comments = _.takeRight(comments, 4) - - comments = _.map comments, (x) -> renderComment(x) - - html = renderHistory(comments, totalComments) - $el.find(".comments-list").html(html) - - renderActivity = -> - changes = $scope.history or [] - totalChanges = changes.length - if not showAllActivity - changes = _.takeRight(changes, 4) - - changes = _.map(changes, (x) -> renderChange(x)) - html = renderHistory(changes, totalChanges) - $el.find(".changes-list").html(html) - - save = $qqueue.bindAdd (target) => - $scope.$broadcast("markdown-editor:submit") - - $el.find(".comment-list").addClass("activeanimation") - - currentLoading = $loading() - .target(target) - .start() - - onSuccess = -> - $rootScope.$broadcast("comment:new") - - $ctrl.loadHistory(type, objectId).finally -> - currentLoading.finish() - - onError = -> - currentLoading.finish() - $confirm.notify("error") - - model = $scope.$eval($attrs.ngModel) - - $ctrl.repo.save(model).then(onSuccess, onError) - - # Watchers - - $scope.$watch("comments", rerender) - $scope.$watch("history", rerender) - - $scope.$on("object:updated", -> $ctrl.loadHistory(type, objectId)) - - # Events - - $el.on "click", ".add-comment .button-green", debounce 2000, (event) -> - event.preventDefault() - - target = angular.element(event.currentTarget) - save(target) - - $el.on "click", "a", (event) -> - target = angular.element(event.target) - href = target.attr('href') - if href && href.indexOf("#") == 0 - event.preventDefault() - $('body').scrollTop($(href).offset().top) - - $el.on "click", ".show-more", (event) -> - event.preventDefault() - - target = angular.element(event.currentTarget) - if target.parent().is(".changes-list") - showAllActivity = not showAllActivity - renderActivity() - else - showAllComments = not showAllComments - renderComments() - - $el.on "click", ".show-deleted-comment", (event) -> - event.preventDefault() - target = angular.element(event.currentTarget) - target.parents('.activity-single').find('.hide-deleted-comment').show() - target.parents('.activity-single').find('.show-deleted-comment').hide() - target.parents('.activity-single').find('.comment-body').show() - - $el.on "click", ".hide-deleted-comment", (event) -> - event.preventDefault() - target = angular.element(event.currentTarget) - target.parents('.activity-single').find('.hide-deleted-comment').hide() - target.parents('.activity-single').find('.show-deleted-comment').show() - target.parents('.activity-single').find('.comment-body').hide() - - $el.on "click", ".changes-title", (event) -> - event.preventDefault() - target = angular.element(event.currentTarget) - target.parent().find(".change-entry").toggleClass("active") - - $el.on "focus", ".add-comment textarea", (event) -> - $(this).addClass('active') - - $el.on "click", ".history-tabs a", (event) -> - target = angular.element(event.currentTarget) - - $el.find(".history-tabs li").removeClass("active") - target.parent().addClass("active") - - $el.find(".history section").addClass("hidden") - $el.find(".history section.#{target.data('section-class')}").removeClass("hidden") - - $el.on "click", ".comment-delete", debounce 2000, (event) -> - event.preventDefault() - - target = angular.element(event.currentTarget) - activityId = target.data('activity-id') - $ctrl.deleteComment(type, objectId, activityId) - - $el.on "click", ".comment-restore", debounce 2000, (event) -> - event.preventDefault() - - target = angular.element(event.currentTarget) - activityId = target.data('activity-id') - $ctrl.undeleteComment(type, objectId, activityId) - - $scope.$on "$destroy", -> - $el.off() - - renderBase() - - return { - controller: HistoryController - restrict: "AE" - link: link - # require: ["ngModel", "tgHistory"] - } - - -module.directive("tgHistory", ["$log", "$tgLoading", "$tgQqueue", "$tgTemplate", "$tgConfirm", "$translate", - "$compile", "$tgNavUrls", "$rootScope", "tgCheckPermissionsService", HistoryDirective]) diff --git a/app/coffee/modules/common/wisiwyg.coffee b/app/coffee/modules/common/wisiwyg.coffee index e6fe3b0c..40f53264 100644 --- a/app/coffee/modules/common/wisiwyg.coffee +++ b/app/coffee/modules/common/wisiwyg.coffee @@ -83,7 +83,7 @@ MarkitupDirective = ($rootscope, $rs, $selectedText, $template, $compile, $trans markdownDomNode = element.parents(".markdown") markItUpDomNode = element.parents(".markItUp") - $rs.mdrender.render($scope.projectId, $model.$modelValue).then (data) -> + $rs.mdrender.render($scope.projectId || $scope.vm.projectId, $model.$modelValue).then (data) -> html = previewTemplate({data: data.data}) html = $compile(html)($scope) diff --git a/app/coffee/modules/resources/history.coffee b/app/coffee/modules/resources/history.coffee index 5ea6886b..6e0942b1 100644 --- a/app/coffee/modules/resources/history.coffee +++ b/app/coffee/modules/resources/history.coffee @@ -31,6 +31,25 @@ resourceProvider = ($repo, $http, $urls) -> service.get = (type, objectId) -> return $repo.queryOneRaw("history/#{type}", objectId) + service.editComment = (type, objectId, activityId, comment) -> + url = $urls.resolve("history/#{type}") + url = "#{url}/#{objectId}/edit_comment" + params = { + id: activityId + } + commentData = { + comment: comment + } + return $http.post(url, commentData, params).then (data) => + return data.data + + service.getCommentHistory = (type, objectId, activityId) -> + url = $urls.resolve("history/#{type}") + url = "#{url}/#{objectId}/comment_versions" + params = {id: activityId} + return $http.get(url, params).then (data) => + return data.data + service.deleteComment = (type, objectId, activityId) -> url = $urls.resolve("history/#{type}") url = "#{url}/#{objectId}/delete_comment" diff --git a/app/locales/taiga/locale-en.json b/app/locales/taiga/locale-en.json index a5176289..0263a4f1 100644 --- a/app/locales/taiga/locale-en.json +++ b/app/locales/taiga/locale-en.json @@ -244,6 +244,7 @@ "VIEW_USER_STORIES": "View user stories", "ADD_USER_STORIES": "Add user stories", "MODIFY_USER_STORIES": "Modify user stories", + "COMMENT_USER_STORIES": "Comment user stories", "DELETE_USER_STORIES": "Delete user stories" }, "TASKS": { @@ -251,6 +252,7 @@ "VIEW_TASKS": "View tasks", "ADD_TASKS": "Add tasks", "MODIFY_TASKS": "Modify tasks", + "COMMENT_TASKS": "Comment tasks", "DELETE_TASKS": "Delete tasks" }, "ISSUES": { @@ -258,6 +260,7 @@ "VIEW_ISSUES": "View issues", "ADD_ISSUES": "Add issues", "MODIFY_ISSUES": "Modify issues", + "COMMENT_ISSUES": "Comment issues", "DELETE_ISSUES": "Delete issues" }, "WIKI": { @@ -1018,28 +1021,47 @@ } }, "COMMENTS": { - "DELETED_INFO": "Comment deleted by {{user}} on {{date}}", + "DELETED_INFO": "Comment deleted by {{user}}", "TITLE": "Comments", + "COMMENTS_COUNT": "{{comments}} Comments", + "ORDER": "Order", + "OLDER_FIRST": "Older first", + "RECENT_FIRST": "Recent first", "COMMENT": "Comment", + "EDIT_COMMENT": "Edit comment", + "EDITED_COMMENT": "Edited:", + "SHOW_HISTORY": "View historic", "TYPE_NEW_COMMENT": "Type a new comment here", "SHOW_DELETED": "Show deleted comment", "HIDE_DELETED": "Hide deleted comment", "DELETE": "Delete comment", - "RESTORE": "Restore comment" + "RESTORE": "Restore comment", + "HISTORY": { + "TITLE": "Activity" + } }, "ACTIVITY": { "SHOW_ACTIVITY": "Show activity", "DATETIME": "DD MMM YYYY HH:mm", "SHOW_MORE": "+ Show previous entries ({{showMore}} more)", "TITLE": "Activity", + "ACTIVITIES_COUNT": "{{activities}} Activities", "REMOVED": "removed", "ADDED": "added", - "US_POINTS": "US points ({{name}})", - "NEW_ATTACHMENT": "new attachment", - "DELETED_ATTACHMENT": "deleted attachment", - "UPDATED_ATTACHMENT": "updated attachment {{filename}}", - "DELETED_CUSTOM_ATTRIBUTE": "deleted custom attribute", + "TAGS_ADDED": "tags added:", + "TAGS_REMOVED": "tags removed:", + "US_POINTS": "{{role}} points", + "NEW_ATTACHMENT": "new attachment:", + "DELETED_ATTACHMENT": "deleted attachment:", + "UPDATED_ATTACHMENT": "updated attachment ({{filename}}): ", + "CREATED_CUSTOM_ATTRIBUTE": "created custom attribute", + "UPDATED_CUSTOM_ATTRIBUTE": "updated custom attribute", "SIZE_CHANGE": "Made {size, plural, one{one change} other{# changes}}", + "BECAME_DEPRECATED": "became deprecated", + "BECAME_UNDEPRECATED": "became undeprecated", + "TEAM_REQUIREMENT": "Team Requirement", + "CLIENT_REQUIREMENT": "Client Requirement", + "BLOCKED": "Blocked", "VALUES": { "YES": "yes", "NO": "no", @@ -1071,6 +1093,7 @@ "TAGS": "tags", "ATTACHMENTS": "attachments", "IS_DEPRECATED": "is deprecated", + "IS_NOT_DEPRECATED": "is not deprecated", "ORDER": "order", "BACKLOG_ORDER": "backlog order", "SPRINT_ORDER": "sprint order", diff --git a/app/modules/history/comments/comment.controller.coffee b/app/modules/history/comments/comment.controller.coffee new file mode 100644 index 00000000..c137af11 --- /dev/null +++ b/app/modules/history/comments/comment.controller.coffee @@ -0,0 +1,64 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: history.controller.coffee +### + +module = angular.module("taigaHistory") + +class CommentController + @.$inject = [ + "tgCurrentUserService", + "tgCheckPermissionsService", + "tgLightboxFactory" + ] + + constructor: (@currentUserService, @permissionService, @lightboxFactory) -> + @.hiddenDeletedComment = true + @.toggleEditComment = false + @.commentContent = angular.copy(@.comment) + + showDeletedComment: () -> + @.hiddenDeletedComment = false + + hideDeletedComment: () -> + @.hiddenDeletedComment = true + + toggleCommentEditor: () -> + @.toggleEditComment = !@.toggleEditComment + + checkCancelComment: (event) -> + if event.keyCode == 27 + @.toggleCommentEditor() + + canEditDeleteComment: () -> + if @currentUserService.getUser() + @.user = @currentUserService.getUser() + return @.user.get('id') == @.comment.user.pk || @permissionService.check('modify_project') + + displayCommentHistory: () -> + @lightboxFactory.create('tg-lb-display-historic', { + "class": "lightbox lightbox-display-historic" + "comment": "comment" + "name": "name" + "object": "object" + }, { + "comment": @.comment + "name": @.name + "object": @.object + }) + +module.controller("CommentCtrl", CommentController) diff --git a/app/modules/history/comments/comment.controller.spec.coffee b/app/modules/history/comments/comment.controller.spec.coffee new file mode 100644 index 00000000..9765fe1f --- /dev/null +++ b/app/modules/history/comments/comment.controller.spec.coffee @@ -0,0 +1,134 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: subscriptions.controller.spec.coffee +### + +describe "CommentController", -> + provide = null + controller = null + mocks = {} + + _mockTgCurrentUserService = () -> + mocks.tgCurrentUserService = { + getUser: sinon.stub() + } + + provide.value "tgCurrentUserService", mocks.tgCurrentUserService + + _mockTgCheckPermissionsService = () -> + mocks.tgCheckPermissionsService = { + check: sinon.stub() + } + + provide.value "tgCheckPermissionsService", mocks.tgCheckPermissionsService + + _mockTgLightboxFactory = () -> + mocks.tgLightboxFactory = { + create: sinon.stub() + } + + provide.value "tgLightboxFactory", mocks.tgLightboxFactory + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgCurrentUserService() + _mockTgCheckPermissionsService() + _mockTgLightboxFactory() + return null + + beforeEach -> + module "taigaHistory" + _mocks() + + inject ($controller) -> + controller = $controller + + commentsCtrl = controller "CommentCtrl" + + commentsCtrl.comment = "comment" + commentsCtrl.hiddenDeletedComment = true + commentsCtrl.toggleEditComment = false + commentsCtrl.commentContent = commentsCtrl.comment + + it "show deleted Comment", () -> + commentsCtrl = controller "CommentCtrl" + commentsCtrl.showDeletedComment() + expect(commentsCtrl.hiddenDeletedComment).to.be.false + + it "hide deleted Comment", () -> + commentsCtrl = controller "CommentCtrl" + + commentsCtrl.hiddenDeletedComment = false + commentsCtrl.hideDeletedComment() + expect(commentsCtrl.hiddenDeletedComment).to.be.true + + it "toggle deleted Comment", () -> + commentsCtrl = controller "CommentCtrl" + + commentsCtrl.toggleEditComment = false + commentsCtrl.toggleCommentEditor() + expect(commentsCtrl.toggleEditComment).to.be.true + + it "cancel comment on keyup", () -> + commentsCtrl = controller "CommentCtrl" + commentsCtrl.toggleCommentEditor = sinon.stub() + event = { + keyCode: 27 + } + commentsCtrl.checkCancelComment(event) + expect(commentsCtrl.toggleCommentEditor).have.been.called + + it "can Edit Comment", () -> + commentsCtrl = controller "CommentCtrl" + + commentsCtrl.user = Immutable.fromJS({ + id: 7 + }) + + mocks.tgCurrentUserService.getUser.returns(commentsCtrl.user) + + commentsCtrl.comment = { + user: { + pk: 7 + } + } + + mocks.tgCheckPermissionsService.check.withArgs('modify_project').returns(true) + + canEdit = commentsCtrl.canEditDeleteComment() + expect(canEdit).to.be.true + + it "cannot Edit Comment", () -> + commentsCtrl = controller "CommentCtrl" + + commentsCtrl.user = Immutable.fromJS({ + id: 8 + }) + + mocks.tgCurrentUserService.getUser.returns(commentsCtrl.user) + + commentsCtrl.comment = { + user: { + pk: 7 + } + } + + mocks.tgCheckPermissionsService.check.withArgs('modify_project').returns(false) + + canEdit = commentsCtrl.canEditDeleteComment() + expect(canEdit).to.be.false diff --git a/app/modules/history/comments/comment.directive.coffee b/app/modules/history/comments/comment.directive.coffee new file mode 100644 index 00000000..cb3c2bb4 --- /dev/null +++ b/app/modules/history/comments/comment.directive.coffee @@ -0,0 +1,44 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: comment.directive.coffee +### + +module = angular.module('taigaHistory') + +CommentDirective = () -> + + return { + scope: { + name: "@", + object: "@", + comment: "<", + type: "<", + loading: "<", + editing: "<", + deleting: "<", + objectId: "<", + onDeleteComment: "&", + onRestoreDeletedComment: "&", + onEditComment: "&" + }, + templateUrl:"history/comments/comment.html", + bindToController: true, + controller: 'CommentCtrl', + controllerAs: "vm", + } + +module.directive("tgComment", CommentDirective) diff --git a/app/modules/history/comments/comment.jade b/app/modules/history/comments/comment.jade new file mode 100644 index 00000000..38d8a2a3 --- /dev/null +++ b/app/modules/history/comments/comment.jade @@ -0,0 +1,108 @@ +include ../../../partials/common/components/wysiwyg.jade + +.comment-wrapper(ng-if="!vm.comment.delete_comment_date") + img.comment-avatar( + ng-src="{{vm.comment.user.photo}}" + ng-alt="{{vm.comment.user.name}}" + ) + .comment-main + .comment-data + span.comment-creator {{vm.comment.user.name}} + span.comment-date {{vm.comment.created_at | momentFormat:'DD MMM YYYY HH:mm'}} + .comment-edited(ng-if="vm.comment.edit_comment_date") + span(translate="COMMENTS.EDITED_COMMENT") + span {{vm.comment.edit_comment_date | momentFormat:'DD MMM YYYY HH:mm'}} + span.separator - + a( + href="" + title="COMMENTS.SHOW_HISTORY" + ng-click="vm.displayCommentHistory()" + ) + span(translate="COMMENTS.SHOW_HISTORY") + tg-svg(svg-icon="icon-bulk") + .comment-container + .comment-text.wysiwyg( + ng-if="!vm.toggleEditComment" + ng-bind-html="vm.comment.comment_html" + ) + .comment-editor( + ng-if="vm.toggleEditComment" + ng-keyup="vm.checkCancelComment($event)" + ) + .edit-comment(ng-model="vm.type") + textarea( + ng-model="vm.commentContent.comment" + ) + .save-comment-wrapper + button.button-green.save-comment( + type="button" + title="{{'COMMENTS.EDIT_COMMENT' | translate}}" + translate="COMMENTS.EDIT_COMMENT" + ng-disabled="!vm.commentContent.comment.length || vm.editing" + ng-click="vm.onEditComment({commentId: vm.comment.id, commentData: vm.commentContent.comment})" + tg-loading="vm.editing" + ) + .comment-options(ng-if="::vm.canEditDeleteComment()") + tg-svg.comment-option( + svg-icon="icon-edit" + svg-title-translate="COMMON.EDIT" + ng-click="vm.toggleCommentEditor()" + ng-if="!vm.toggleEditComment" + ) + tg-svg.comment-option( + svg-icon="icon-close" + svg-title-translate="COMMON.CANCEL" + ng-click="vm.toggleCommentEditor()" + ng-if="vm.toggleEditComment" + ) + tg-svg.comment-option( + svg-icon="icon-trash" + svg-title-translate="COMMON.DELETE" + ng-click="vm.onDeleteComment({commentId: vm.comment.id})" + ng-if="!vm.toggleEditComment" + tg-loading="vm.deleting" + ) + +.deleted-comment-wrapper( + ng-if="vm.comment.delete_comment_date" +) + .deleted-comment-main + span( + translate="COMMENTS.DELETED_INFO" + translate-values="{user: vm.comment.delete_comment_user.name }" + ) + span - {{vm.comment.delete_comment_date | momentFormat:'DD MMM YYYY HH:mm'}} + a.toggle-deleted-comment( + href="" + ng-click="vm.showDeletedComment()" + ng-if="vm.hiddenDeletedComment" + ) + span(translate="COMMENTS.SHOW_DELETED") + tg-svg( + svg-icon="icon-arrow-down" + svg-title-translate="COMMENTS.SHOW_DELETED" + ) + a.toggle-deleted-comment( + href="" + ng-click="vm.hideDeletedComment()" + ng-if="!vm.hiddenDeletedComment" + ) + span(translate="COMMENTS.HIDE_DELETED") + tg-svg( + svg-icon="icon-arrow-up" + svg-title-translate="COMMENTS.HIDE_DELETED" + ) + a.restore-comment( + href="" + ng-click="vm.onRestoreDeletedComment({commentId: vm.comment.id})" + tg-loading="vm.editing" + ) + tg-svg( + svg-icon="icon-reload" + svg-title-translate="COMMENTS.RESTORE" + ) + span(translate="COMMENTS.RESTORE") + p.deleted-comment-comment( + ng-if="!vm.hiddenDeletedComment" + ng-bind-html="vm.comment.comment_html" + ) diff --git a/app/modules/history/comments/comment.scss b/app/modules/history/comments/comment.scss new file mode 100644 index 00000000..c5a68912 --- /dev/null +++ b/app/modules/history/comments/comment.scss @@ -0,0 +1,159 @@ +.comments { + clear: both; + .add-comment { + margin-top: 1rem; + textarea { + height: 3rem; + } + .preview-icon, + .edit { + position: absolute; + right: 1rem; + } + } + .save-comment-wrapper { + align-items: flex-end; + display: flex; + flex-direction: column; + } + .save-comment { + margin-top: 1rem; + padding: .5rem 4rem; + } + +} +.comment { + display: block; + .comment-wrapper { + align-items: flex-start; + border-bottom: 1px solid $whitish; + display: flex; + padding: 2rem 0; + &:hover { + .comment-option { + opacity: 1; + } + } + } + .comment-main { + width: 100%; + } + .comment-avatar { + flex-basis: 50px; + flex-shrink: 0; + margin-right: 1.5rem; + } + .comment-data { + align-items: center; + display: flex; + justify-content: flex-start; + margin-bottom: 1rem; + } + .comment-creator { + color: $primary; + margin-right: .5rem; + } + .comment-date { + @include font-size(small); + color: $gray-light; + } + .comment-edited { + @include font-size(small); + background: $whitish; + margin: 0 .5rem; + padding: .25rem; + .separator { + margin: 0 .25rem; + } + a { + color: $primary; + fill: $primary; + } + svg { + @include svg-size(.75rem); + margin: 0 0 0 .25rem; + } + } + .comment-options { + align-items: center; + align-self: stretch; + display: flex; + flex-basis: 50px; + flex-shrink: 0; + margin-left: 1.5rem; + .comment-option { + cursor: pointer; + opacity: 0; + } + .icon-edit { + fill: $gray-light; + margin-right: .5rem; + &:hover { + fill: $gray; + } + } + .icon-close { + fill: $gray-light; + margin-right: .5rem; + &:hover { + fill: $red; + } + } + .icon-trash { + fill: $red-light; + &:hover { + fill: $red; + } + } + } + .deleted-comment-wrapper { + border-bottom: 1px solid $whitish; + padding: 1rem 0; + width: 100%; + } + .deleted-comment-main { + @include font-size(xsmall); + color: $gray-light; + display: flex; + width: 100%; + } + .toggle-deleted-comment { + color: $primary; + fill: $primary; + margin: 0 1rem; + transition: none; + .icon-arrow-down, + .icon-arrow-up { + @include svg-size(.8rem); + margin-left: .25rem; + } + } + .restore-comment { + margin-left: auto; + transition: all .2s; + &:hover { + color: $primary; + fill: $primary; + } + .icon-reload { + @include svg-size(.8rem); + margin-right: .25rem; + } + } + .deleted-comment-comment { + margin-top: 1rem; + } + .comment-editor { + textarea { + height: 5rem; + min-height: 5rem; + } + } +} + +.comment-text { + &.wysiwyg { + margin-bottom: 0; + padding: 0; + } +} diff --git a/app/modules/history/comments/comments.controller.coffee b/app/modules/history/comments/comments.controller.coffee new file mode 100644 index 00000000..6a986321 --- /dev/null +++ b/app/modules/history/comments/comments.controller.coffee @@ -0,0 +1,28 @@ +### +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: comments.controller.coffee +### + +module = angular.module("taigaHistory") + +class CommentsController + @.$inject = [] + + constructor: () -> + + initializePermissions: () -> + @.canAddCommentPermission = 'comment_' + @.name + +module.controller("CommentsCtrl", CommentsController) diff --git a/app/modules/history/comments/comments.controller.spec.coffee b/app/modules/history/comments/comments.controller.spec.coffee new file mode 100644 index 00000000..dff1233a --- /dev/null +++ b/app/modules/history/comments/comments.controller.spec.coffee @@ -0,0 +1,41 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: comments.controller.spec.coffee +### + +describe "CommentsController", -> + provide = null + controller = null + mocks = {} + + _mocks = () -> + module ($provide) -> + provide = $provide + return null + + beforeEach -> + module "taigaHistory" + _mocks() + + inject ($controller) -> + controller = $controller + + it "set can add comment permission", () -> + commentsCtrl = controller "CommentsCtrl" + commentsCtrl.name = "us" + commentsCtrl.initializePermissions() + expect(commentsCtrl.canAddCommentPermission).to.be.equal("comment_us") diff --git a/app/modules/history/comments/comments.directive.coffee b/app/modules/history/comments/comments.directive.coffee new file mode 100644 index 00000000..cb14c234 --- /dev/null +++ b/app/modules/history/comments/comments.directive.coffee @@ -0,0 +1,48 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: comments.directive.coffee +### + +module = angular.module('taigaHistory') + +CommentsDirective = () -> + link = (scope, el, attrs, ctrl) -> + ctrl.initializePermissions() + + return { + scope: { + type: "<", + name: "@", + object: "@", + comments: "<", + onDeleteComment: "&", + onRestoreDeletedComment: "&", + onAddComment: "&", + onEditComment: "&", + loading: "<", + deleting: "<", + editing: "<", + projectId: "=" + }, + templateUrl:"history/comments/comments.html", + bindToController: true, + controller: 'CommentsCtrl', + controllerAs: "vm" + link: link + } + +module.directive("tgComments", CommentsDirective) diff --git a/app/modules/history/comments/comments.jade b/app/modules/history/comments/comments.jade new file mode 100644 index 00000000..00d2f4f5 --- /dev/null +++ b/app/modules/history/comments/comments.jade @@ -0,0 +1,37 @@ +include ../../../partials/common/components/wysiwyg.jade + +section.comments + .comments-wrapper + tg-comment.comment( + ng-repeat="comment in vm.comments track by comment.id" + ng-class="{'deleted-comment': comment.delete_comment_date}" + comment="comment" + name="{{vm.name}}" + loading="vm.loading" + editing="vm.editing" + deleting="vm.deleting" + object="{{vm.object}}" + on-delete-comment="vm.onDeleteComment({commentId: commentId})" + on-restore-deleted-comment="vm.onRestoreDeletedComment({commentId: commentId})" + on-edit-comment="vm.onEditComment({commentId: commentId, commentData: commentData})" + ) + tg-editable-wysiwyg.add-comment( + ng-model="vm.type" + tg-check-permission="{{::vm.canAddCommentPermission}}" + tg-toggle-comment + ) + textarea( + ng-attr-placeholder="{{'COMMENTS.TYPE_NEW_COMMENT' | translate}}" + tg-markitup="tg-markitup" + ng-model="vm.type.comment" + ) + +wysihelp + .save-comment-wrapper + button.button-green.save-comment( + type="button" + title="{{'COMMENTS.COMMENT' | translate}}" + translate="COMMENTS.COMMENT" + ng-disabled="!vm.type.comment.length || vm.loading" + ng-click="vm.onAddComment()" + tg-loading="vm.loading" + ) diff --git a/app/modules/history/history-lightbox/comment-history-lightbox.controller.coffee b/app/modules/history/history-lightbox/comment-history-lightbox.controller.coffee new file mode 100644 index 00000000..93f014f3 --- /dev/null +++ b/app/modules/history/history-lightbox/comment-history-lightbox.controller.coffee @@ -0,0 +1,37 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: history.controller.coffee +### + +module = angular.module("taigaHistory") + +class LightboxDisplayHistoricController + @.$inject = [ + "$tgResources", + ] + + constructor: (@rs) -> + + _loadHistoric: () -> + type = @.name + objectId = @.object + activityId = @.comment.id + + @rs.history.getCommentHistory(type, objectId, activityId).then (data) => + @.commentHistoryEntries = data + +module.controller("LightboxDisplayHistoricCtrl", LightboxDisplayHistoricController) diff --git a/app/modules/history/history-lightbox/comment-history-lightbox.controller.spec.coffee b/app/modules/history/history-lightbox/comment-history-lightbox.controller.spec.coffee new file mode 100644 index 00000000..92167c96 --- /dev/null +++ b/app/modules/history/history-lightbox/comment-history-lightbox.controller.spec.coffee @@ -0,0 +1,63 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: subscriptions.controller.spec.coffee +### + +describe "LightboxDisplayHistoricController", -> + provide = null + controller = null + mocks = {} + + _mockTgResources = () -> + mocks.tgResources = { + history: { + getCommentHistory: sinon.stub() + } + } + + provide.value "$tgResources", mocks.tgResources + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgResources() + return null + + beforeEach -> + module "taigaHistory" + _mocks() + + inject ($controller) -> + controller = $controller + + it "load historic", (done) -> + historicLbCtrl = controller "LightboxDisplayHistoricCtrl" + + historicLbCtrl.name = "type" + historicLbCtrl.object = 1 + historicLbCtrl.comment = {} + historicLbCtrl.comment.id = 1 + + type = historicLbCtrl.name + objectId = historicLbCtrl.object + activityId = historicLbCtrl.comment.id + + promise = mocks.tgResources.history.getCommentHistory.withArgs(type, objectId, activityId).promise().resolve() + + historicLbCtrl._loadHistoric().then (data) -> + expect(historicLbCtrl.commentHistoryEntries).is.equal(data) + done() diff --git a/app/modules/history/history-lightbox/comment-history-lightbox.directive.coffee b/app/modules/history/history-lightbox/comment-history-lightbox.directive.coffee new file mode 100644 index 00000000..b09c99a0 --- /dev/null +++ b/app/modules/history/history-lightbox/comment-history-lightbox.directive.coffee @@ -0,0 +1,40 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: comment.directive.coffee +### + +module = angular.module('taigaHistory') + +LightboxDisplayHistoricDirective = (lightboxService) -> + link = (scope, el, attrs, ctrl) -> + ctrl._loadHistoric() + lightboxService.open(el) + + return { + scope: {}, + bindToController: { + name: '=', + object: '=', + comment: '=' + }, + templateUrl:"history/history-lightbox/comment-history-lightbox.html", + controller: "LightboxDisplayHistoricCtrl", + controllerAs: "vm", + link: link + } + +module.directive("tgLbDisplayHistoric", LightboxDisplayHistoricDirective) diff --git a/app/modules/history/history-lightbox/comment-history-lightbox.jade b/app/modules/history/history-lightbox/comment-history-lightbox.jade new file mode 100644 index 00000000..32127f9b --- /dev/null +++ b/app/modules/history/history-lightbox/comment-history-lightbox.jade @@ -0,0 +1,9 @@ +tg-lightbox-close + +.history-container + h2.title(translate="COMMENTS.HISTORY.TITLE") + .history-wrapper + tg-history-entry.entry( + ng-repeat="entry in vm.commentHistoryEntries" + entry="entry" + ) diff --git a/app/modules/history/history-lightbox/comment-history-lightbox.scss b/app/modules/history/history-lightbox/comment-history-lightbox.scss new file mode 100644 index 00000000..47e79868 --- /dev/null +++ b/app/modules/history/history-lightbox/comment-history-lightbox.scss @@ -0,0 +1,13 @@ +.lightbox-display-historic { + display: none; + .history-container { + max-width: 800px; + width: 90%; + } + .history-wrapper { + max-height: 600px; + overflow-x: hidden; + overflow-y: auto; + padding: 2rem; + } +} diff --git a/app/modules/history/history-lightbox/history-entry.directive.coffee b/app/modules/history/history-lightbox/history-entry.directive.coffee new file mode 100644 index 00000000..42814d29 --- /dev/null +++ b/app/modules/history/history-lightbox/history-entry.directive.coffee @@ -0,0 +1,31 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: comment.directive.coffee +### + +module = angular.module('taigaHistory') + +HistoryEntryDirective = (lightboxService) -> + + return { + scope: { + entry: "<" + }, + templateUrl:"history/history-lightbox/history-entry.html", + } + +module.directive("tgHistoryEntry", HistoryEntryDirective) diff --git a/app/modules/history/history-lightbox/history-entry.jade b/app/modules/history/history-lightbox/history-entry.jade new file mode 100644 index 00000000..4df2d67b --- /dev/null +++ b/app/modules/history/history-lightbox/history-entry.jade @@ -0,0 +1,19 @@ +.entry-wrapper + img.entry-avatar( + ng-src="{{entry.user.photo}}" + ng-alt="{{entry.user.name}}" + ) + .entry-main + .entry-data + span.entry-creator {{entry.user.full_name_display}} + span.entry-date {{entry.date | momentFormat:'DD MMM YYYY HH:mm'}} + tg-svg.display-full-entry( + svg-icon="icon-arrow-down" + ng-class="{'inactive': !displayFullEntry}" + ng-click="displayFullEntry=!displayFullEntry" + ng-show="entry.comment.length >= 75" + ) + .entry-text( + ng-class="{'ellipsed': !displayFullEntry && entry.comment.length >= 75, 'blurry': entry.comment.length >= 75 && !displayFullEntry}" + ng-bind-html="entry.comment_html" + ) diff --git a/app/modules/history/history-lightbox/history-entry.scss b/app/modules/history/history-lightbox/history-entry.scss new file mode 100644 index 00000000..763921aa --- /dev/null +++ b/app/modules/history/history-lightbox/history-entry.scss @@ -0,0 +1,62 @@ +.entry { + display: block; + .entry-wrapper { + align-items: flex-start; + border-bottom: 1px solid $whitish; + display: flex; + padding: 2rem 0; + } + .entry-avatar { + flex-basis: 50px; + flex-grow: 0; + flex-shrink: 0; + margin-right: 1.5rem; + } + .entry-main { + flex: 1; + max-width: calc(100% - 100px); + } + .entry-data { + align-items: flex-start; + display: flex; + margin-bottom: 1rem; + } + .entry-creator { + color: $primary; + margin-right: .5rem; + } + .entry-date { + @include font-size(small); + color: $gray-light; + } + .display-full-entry { + @include svg-size(1.25rem); + cursor: pointer; + fill: $primary; + margin-left: auto; + transform: rotate(0); + transition: transform .2s; + &.inactive { + transform: rotate(180deg); + } + } + .entry-text { + margin-bottom: 0; + &.ellipsed { + max-height: 3rem; + overflow: hidden; + } + &.blurry { + position: relative; + &::after { + background-image: linear-gradient(to top, $white, transparent); + content: ''; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + } + } + } +} diff --git a/app/modules/history/history-tabs/history-tabs.directive.coffee b/app/modules/history/history-tabs/history-tabs.directive.coffee new file mode 100644 index 00000000..fdadb250 --- /dev/null +++ b/app/modules/history/history-tabs/history-tabs.directive.coffee @@ -0,0 +1,37 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: history-tabs.directive.coffee +### + +module = angular.module('taigaHistory') + +HistoryTabsDirective = () -> + + return { + templateUrl:"history/history-tabs/history-tabs.html", + scope: { + onActiveComments: "&", + onActiveActivities: "&", + onOrderComments: "&" + activeTab: "<", + commentsNum: "<", + activitiesNum: "<", + onReverse: "<" + } + } + +module.directive("tgHistoryTabs", HistoryTabsDirective) diff --git a/app/modules/history/history-tabs/history-tabs.jade b/app/modules/history/history-tabs/history-tabs.jade new file mode 100644 index 00000000..8b0c175a --- /dev/null +++ b/app/modules/history/history-tabs/history-tabs.jade @@ -0,0 +1,41 @@ +nav.history-tabs + a.history-tab.e2e-comments-tab( + href="" + title="Comments" + ng-click="onActiveComments()" + ng-class="{active: activeTab}" + translate="COMMENTS.COMMENTS_COUNT" + translate-values="{comments: commentsNum}" + ) + a.history-tab.e2e-activity-tab( + href="" + title="Activities" + ng-click="onActiveActivities()" + ng-class="{active: !activeTab}" + translate="ACTIVITY.ACTIVITIES_COUNT" + translate-values="{activities: activitiesNum}" + ) + a.order-comments( + href="" + title="Order Comments" + ng-click="onOrderComments()" + ng-class="{'new-first': top, 'old-first': !top}" + ng-if="commentsNum > 1 && activeTab" + ) + + span( + translate="COMMENTS.OLDER_FIRST" + ng-if="onReverse" + ) + tg-svg( + svg-icon="icon-arrow-down" + ng-if="onReverse" + ) + span( + translate="COMMENTS.RECENT_FIRST" + ng-if="!onReverse" + ) + tg-svg( + svg-icon="icon-arrow-up" + ng-if="!onReverse" + ) diff --git a/app/modules/history/history-tabs/history-tabs.scss b/app/modules/history/history-tabs/history-tabs.scss new file mode 100644 index 00000000..48cb8471 --- /dev/null +++ b/app/modules/history/history-tabs/history-tabs.scss @@ -0,0 +1,32 @@ +.history-tabs { + background: $whitish; + display: flex; + flex-direction: row; + a { + color: $grayer; + display: inline-block; + padding: .75rem 1rem; + &:hover { + color: $primary; + } + } + .history-tab { + @include font-type(bold); + border-bottom: 3px solid transparent; + color: $gray-light; + transition: all .1s linear; + &.active { + border-bottom: 3px solid $grayer; + color: $grayer; + } + } + .order-comments { + @include font-type(light); + margin-left: auto; + transition: none; + } + .icon-arrow-up, + .icon-arrow-down { + @include svg-size(.75rem); + } +} diff --git a/app/modules/history/history.controller.coffee b/app/modules/history/history.controller.coffee new file mode 100644 index 00000000..3eff5130 --- /dev/null +++ b/app/modules/history/history.controller.coffee @@ -0,0 +1,91 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: history.controller.coffee +### + +module = angular.module("taigaHistory") + +class HistorySectionController + @.$inject = [ + "$tgResources", + "$tgRepo", + "$tgStorage", + ] + + constructor: (@rs, @repo, @storage) -> + @.viewComments = true + @._loadHistory() + @.reverse = @storage.get("orderComments") + + _loadHistory: () -> + @rs.history.get(@.name, @.id).then (history) => + @._getComments(history) + @._getActivities(history) + + _getComments: (comments) -> + @.comments = _.filter(comments, (item) -> item.comment != "") + if @.reverse + @.comments - _.reverse(@.comments) + @.commentsNum = @.comments.length + + _getActivities: (activities) -> + @.activities = _.filter(activities, (item) -> Object.keys(item.values_diff).length > 0) + @.activitiesNum = @.activities.length + + onActiveHistoryTab: (active) -> + @.viewComments = active + + deleteComment: (commentId) -> + type = @.name + objectId = @.id + activityId = commentId + @.deleting = true + return @rs.history.deleteComment(type, objectId, activityId).then => + @._loadHistory() + @.deleting = false + + editComment: (commentId, comment) -> + type = @.name + objectId = @.id + activityId = commentId + @.editing = true + return @rs.history.editComment(type, objectId, activityId, comment).then => + @._loadHistory() + @.editing = false + + restoreDeletedComment: (commentId) -> + type = @.name + objectId = @.id + activityId = commentId + @.editing = true + return @rs.history.undeleteComment(type, objectId, activityId).then => + @._loadHistory() + @.editing = false + + addComment: () -> + type = @.type + @.loading = true + @repo.save(@.type).then => + @._loadHistory() + @.loading = false + + onOrderComments: () -> + @.reverse = !@.reverse + @storage.set("orderComments", @.reverse) + @._loadHistory() + +module.controller("HistorySection", HistorySectionController) diff --git a/app/modules/history/history.controller.spec.coffee b/app/modules/history/history.controller.spec.coffee new file mode 100644 index 00000000..bd77887f --- /dev/null +++ b/app/modules/history/history.controller.spec.coffee @@ -0,0 +1,214 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: subscriptions.controller.spec.coffee +### + +describe "HistorySection", -> + provide = null + controller = null + mocks = {} + + _mockTgResources = () -> + mocks.tgResources = { + history: { + get: sinon.stub() + deleteComment: sinon.stub() + undeleteComment: sinon.stub() + editComment: sinon.stub() + } + } + + provide.value "$tgResources", mocks.tgResources + + _mockTgRepo = () -> + mocks.tgRepo = { + save: sinon.stub() + } + + provide.value "$tgRepo", mocks.tgRepo + + _mocktgStorage = () -> + mocks.tgStorage = { + get: sinon.stub() + set: sinon.stub() + } + provide.value "$tgStorage", mocks.tgStorage + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgResources() + _mockTgRepo() + _mocktgStorage() + return null + + beforeEach -> + module "taigaHistory" + + _mocks() + + inject ($controller) -> + controller = $controller + promise = mocks.tgResources.history.get.promise().resolve() + + it "load historic", (done) -> + historyCtrl = controller "HistorySection" + + historyCtrl._getComments = sinon.stub() + historyCtrl._getActivities = sinon.stub() + + name = "name" + id = 4 + + promise = mocks.tgResources.history.get.withArgs(name, id).promise().resolve() + historyCtrl._loadHistory().then (data) -> + expect(historyCtrl._getComments).have.been.calledWith(data) + expect(historyCtrl._getActivities).have.been.calledWith(data) + done() + + it "get Comments older first", () -> + historyCtrl = controller "HistorySection" + + comments = ['comment3', 'comment2', 'comment1'] + historyCtrl.reverse = false + + historyCtrl._getComments(comments) + expect(historyCtrl.comments).to.be.eql(['comment3', 'comment2', 'comment1']) + expect(historyCtrl.commentsNum).to.be.equal(3) + + it "get Comments newer first", () -> + historyCtrl = controller "HistorySection" + + comments = ['comment3', 'comment2', 'comment1'] + historyCtrl.reverse = true + + historyCtrl._getComments(comments) + expect(historyCtrl.comments).to.be.eql(['comment1', 'comment2', 'comment3']) + expect(historyCtrl.commentsNum).to.be.equal(3) + + it "get activities", () -> + historyCtrl = controller "HistorySection" + activities = { + 'activity1': { + 'values_diff': {"k1": [0, 1]} + }, + 'activity2': { + 'values_diff': {"k2": [0, 1]} + }, + 'activity3': { + 'values_diff': {"k3": [0, 1]} + }, + } + + historyCtrl._getActivities(activities) + + historyCtrl.activities = activities + expect(historyCtrl.activitiesNum).to.be.equal(3) + + it "on active history tab", () -> + historyCtrl = controller "HistorySection" + active = true + historyCtrl.onActiveHistoryTab(active) + expect(historyCtrl.viewComments).to.be.true + + it "on inactive history tab", () -> + historyCtrl = controller "HistorySection" + active = false + historyCtrl.onActiveHistoryTab(active) + expect(historyCtrl.viewComments).to.be.false + + it "delete comment", () -> + historyCtrl = controller "HistorySection" + historyCtrl._loadHistory = sinon.stub() + + historyCtrl.name = "type" + historyCtrl.id = 1 + + type = historyCtrl.name + objectId = historyCtrl.id + commentId = 7 + + promise = mocks.tgResources.history.deleteComment.withArgs(type, objectId, commentId).promise().resolve() + + historyCtrl.deleting = true + historyCtrl.deleteComment(commentId).then () -> + expect(historyCtrl._loadHistory).have.been.called + expect(historyCtrl.deleting).to.be.false + + it "edit comment", () -> + historyCtrl = controller "HistorySection" + historyCtrl._loadHistory = sinon.stub() + + historyCtrl.name = "type" + historyCtrl.id = 1 + activityId = 7 + comment = "blablabla" + + type = historyCtrl.name + objectId = historyCtrl.id + commentId = activityId + + promise = mocks.tgResources.history.editComment.withArgs(type, objectId, activityId, comment).promise().resolve() + + historyCtrl.editing = true + historyCtrl.editComment(commentId, comment).then () -> + expect(historyCtrl._loadHistory).has.been.called + expect(historyCtrl.editing).to.be.false + + it "restore comment", () -> + historyCtrl = controller "HistorySection" + historyCtrl._loadHistory = sinon.stub() + + historyCtrl.name = "type" + historyCtrl.id = 1 + activityId = 7 + + type = historyCtrl.name + objectId = historyCtrl.id + commentId = activityId + + promise = mocks.tgResources.history.undeleteComment.withArgs(type, objectId, activityId).promise().resolve() + + historyCtrl.editing = true + historyCtrl.restoreDeletedComment(commentId).then () -> + expect(historyCtrl._loadHistory).has.been.called + expect(historyCtrl.editing).to.be.false + + it "add comment", () -> + historyCtrl = controller "HistorySection" + historyCtrl._loadHistory = sinon.stub() + + historyCtrl.type = "type" + type = historyCtrl.type + historyCtrl.loading = true + + promise = mocks.tgRepo.save.withArgs(type).promise().resolve() + + historyCtrl.addComment().then () -> + expect(historyCtrl._loadHistory).has.been.called + expect(historyCtrl.loading).to.be.false + + it "order comments", () -> + historyCtrl = controller "HistorySection" + historyCtrl._loadHistory = sinon.stub() + + historyCtrl.reverse = false + + historyCtrl.onOrderComments() + expect(historyCtrl.reverse).to.be.true + expect(mocks.tgStorage.set).has.been.calledWith("orderComments", historyCtrl.reverse) + expect(historyCtrl._loadHistory).has.been.called diff --git a/app/modules/history/history.directive.coffee b/app/modules/history/history.directive.coffee new file mode 100644 index 00000000..eebaf063 --- /dev/null +++ b/app/modules/history/history.directive.coffee @@ -0,0 +1,42 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: history.directive.coffee +### + +module = angular.module('taigaHistory') + +HistorySectionDirective = () -> + link = (scope, el, attr, ctrl) -> + scope.$on "object:updated", -> ctrl._loadHistory(scope.type, scope.id) + + return { + link: link, + templateUrl:"history/history.html", + controller: "HistorySection", + controllerAs: "vm", + bindToController: true, + scope: { + type: "=", + name: "@", + id: "=", + projectId: "=" + } + } + +HistorySectionDirective.$inject = [] + +module.directive("tgHistorySection", HistorySectionDirective) diff --git a/app/modules/history/history.jade b/app/modules/history/history.jade new file mode 100644 index 00000000..e5fb19d3 --- /dev/null +++ b/app/modules/history/history.jade @@ -0,0 +1,29 @@ +section.history + tg-history-tabs( + on-active-comments="vm.onActiveHistoryTab(true)" + on-active-activities="vm.onActiveHistoryTab(false)" + active-tab="vm.viewComments", + on-order-comments="vm.onOrderComments()" + comments-num="vm.commentsNum" + activities-num="vm.activitiesNum" + on-reverse="vm.reverse" + ) + tg-comments( + ng-if="vm.viewComments" + comments="vm.comments" + on-delete-comment="vm.deleteComment(commentId)" + on-restore-deleted-comment="vm.restoreDeletedComment(commentId)" + on-add-comment="vm.addComment()" + on-edit-comment="vm.editComment(commentId, commentData)" + object="{{vm.id}}" + type="vm.type" + name="{{vm.name}}" + loading="vm.loading" + editing="vm.editing" + deleting="vm.deleting" + project-id="vm.projectId" + ) + tg-history( + ng-if="!vm.viewComments" + activities="vm.activities" + ) diff --git a/app/modules/history/history.module.coffee b/app/modules/history/history.module.coffee new file mode 100644 index 00000000..6089087a --- /dev/null +++ b/app/modules/history/history.module.coffee @@ -0,0 +1,20 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: history.module.coffee +### + +angular.module("taigaHistory", []) diff --git a/app/modules/history/history/history-diff.controller.coffee b/app/modules/history/history/history-diff.controller.coffee new file mode 100644 index 00000000..4096f44d --- /dev/null +++ b/app/modules/history/history/history-diff.controller.coffee @@ -0,0 +1,34 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: history.controller.coffee +### + +module = angular.module("taigaHistory") + +class ActivitiesDiffController + @.$inject = [ + ] + + constructor: () -> + + diffTags: () -> + if @.type == 'tags' + @.diffRemoveTags = _.difference(@.diff[0], @.diff[1]).toString() + @.diffAddTags = _.difference(@.diff[1], @.diff[0]).toString() + + +module.controller("ActivitiesDiffCtrl", ActivitiesDiffController) diff --git a/app/modules/history/history/history-diff.controller.spec.coffee b/app/modules/history/history/history-diff.controller.spec.coffee new file mode 100644 index 00000000..c04900d9 --- /dev/null +++ b/app/modules/history/history/history-diff.controller.spec.coffee @@ -0,0 +1,43 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: subscriptions.controller.spec.coffee +### + +describe "ActivitiesDiffController", -> + provide = null + controller = null + mocks = {} + + beforeEach -> + module "taigaHistory" + + inject ($controller) -> + controller = $controller + + it "Check diff between tags", () -> + activitiesDiffCtrl = controller "ActivitiesDiffCtrl" + + activitiesDiffCtrl.type = "tags" + + activitiesDiffCtrl.diff = [ + ["architecto", "perspiciatis", "testafo"], + ["architecto", "perspiciatis", "testafo", "fasto"] + ] + + activitiesDiffCtrl.diffTags() + expect(activitiesDiffCtrl.diffRemoveTags).to.be.equal('') + expect(activitiesDiffCtrl.diffAddTags).to.be.equal('fasto') diff --git a/app/modules/history/history/history-diff.directive.coffee b/app/modules/history/history/history-diff.directive.coffee new file mode 100644 index 00000000..481c27ec --- /dev/null +++ b/app/modules/history/history/history-diff.directive.coffee @@ -0,0 +1,38 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: history.directive.coffee +### + +module = angular.module('taigaHistory') + +HistoryDiffDirective = () -> + link = (scope, el, attrs, ctrl) -> + ctrl.diffTags() + + return { + scope: { + type: "<", + diff: "<" + }, + templateUrl:"history/history/history-diff.html", + controller: "ActivitiesDiffCtrl", + controllerAs: 'vm', + bindToController: true, + link: link + } + +module.directive("tgHistoryDiff", HistoryDiffDirective) diff --git a/app/modules/history/history/history-diff.jade b/app/modules/history/history/history-diff.jade new file mode 100644 index 00000000..aa6d53c8 --- /dev/null +++ b/app/modules/history/history/history-diff.jade @@ -0,0 +1,61 @@ +.diff-wrapper( + ng-if="vm.type == 'points'" +) + include history-templates/history-points + +.diff-wrapper( + ng-if="vm.type == 'attachments'" +) + include history-templates/history-attachments + +.diff-wrapper( + ng-if="vm.type == 'milestone'" +) + include history-templates/history-milestone + +.diff-wrapper( + ng-if="vm.type == 'status'" +) + include history-templates/history-status + +.diff-wrapper( + ng-if="vm.type == 'subject'" +) + include history-templates/history-subject + +.diff-wrapper( + ng-if="vm.type == 'description_diff'" +) + include history-templates/history-description + +.diff-wrapper( + ng-if="vm.type == 'assigned_to'" +) + include history-templates/history-assigned + +.diff-wrapper( + ng-if="vm.type == 'tags'" +) + include history-templates/history-tags + +.diff-wrapper( + ng-if="vm.type == 'custom_attributes'" +) + include history-templates/history-custom-attributes + +.diff-wrapper( + ng-if="vm.type == 'team_requirement'" +) + include history-templates/team-requirement + +.diff-wrapper( + ng-if="vm.type == 'client_requirement'" +) + include history-templates/client-requirement + +.diff-wrapper( + ng-if="vm.type == 'is_blocked'" +) + include history-templates/blocked + + diff --git a/app/modules/history/history/history-templates/blocked.jade b/app/modules/history/history/history-templates/blocked.jade new file mode 100644 index 00000000..7a2fb580 --- /dev/null +++ b/app/modules/history/history/history-templates/blocked.jade @@ -0,0 +1,9 @@ +.diff-status-wrapper + span.key( + translate="ACTIVITY.BLOCKED" + ) + span.diff {{vm.diff[0]}} + tg-svg( + svg-icon="icon-arrow-right" + ) + span.diff {{vm.diff[1]}} diff --git a/app/modules/history/history/history-templates/client-requirement.jade b/app/modules/history/history/history-templates/client-requirement.jade new file mode 100644 index 00000000..10649a6a --- /dev/null +++ b/app/modules/history/history/history-templates/client-requirement.jade @@ -0,0 +1,9 @@ +.diff-status-wrapper + span.key( + translate="ACTIVITY.CLIENT_REQUIREMENT" + ) + span.diff {{vm.diff[0]}} + tg-svg( + svg-icon="icon-arrow-right" + ) + span.diff {{vm.diff[1]}} diff --git a/app/modules/history/history/history-templates/history-assigned.jade b/app/modules/history/history/history-templates/history-assigned.jade new file mode 100644 index 00000000..ab57ac18 --- /dev/null +++ b/app/modules/history/history/history-templates/history-assigned.jade @@ -0,0 +1,9 @@ +.diff-status-wrapper + span.key( + translate="ACTIVITY.FIELDS.ASSIGNED_TO" + ) + span.diff(ng-if="vm.diff[0]") {{vm.diff[0]}} + tg-svg( + svg-icon="icon-arrow-right" + ) + span.diff(ng-if="vm.diff[1]") {{vm.diff[1]}} diff --git a/app/modules/history/history/history-templates/history-attachments.jade b/app/modules/history/history/history-templates/history-attachments.jade new file mode 100644 index 00000000..6deebc32 --- /dev/null +++ b/app/modules/history/history/history-templates/history-attachments.jade @@ -0,0 +1,37 @@ +.diff-attachments-new( + ng-if="vm.diff.new.length" + ng-repeat="newAttachment in vm.diff.new" +) + span.key(translate="ACTIVITY.NEW_ATTACHMENT") + span.diff {{newAttachment.filename}} +.diff-attachments-update( + ng-if="vm.diff.changed.length" + ng-repeat="editAttachment in vm.diff.changed" +) + span.key( + translate="ACTIVITY.UPDATED_ATTACHMENT" + translate-values="{filename: editAttachment.filename}" + ) + span.diff(ng-if="editAttachment.changes.is_deprecated") + span( + ng-if="editAttachment.changes.is_deprecated[1] == false" + translate="ACTIVITY.BECAME_UNDEPRECATED" + ) + span( + ng-if="editAttachment.changes.is_deprecated[1] == true" + translate="ACTIVITY.BECAME_DEPRECATED" + ) + span.diff(ng-if="editAttachment.changes.description") + span(ng-if='editAttachment.changes.description[0].length') {{editAttachment.changes.description[0]}} + span(ng-if='!editAttachment.changes.description[0].length') ... + tg-svg( + svg-icon="icon-arrow-right" + ) + span {{editAttachment.changes.description[1]}} + +.diff-attachments-deleted( + ng-if="vm.diff.deleted.length" + ng-repeat="deletedAttachment in vm.diff.deleted" +) + span.key(translate="ACTIVITY.DELETED_ATTACHMENT") + span.diff {{deletedAttachment.filename}} diff --git a/app/modules/history/history/history-templates/history-custom-attributes.jade b/app/modules/history/history/history-templates/history-custom-attributes.jade new file mode 100644 index 00000000..69d6a1bf --- /dev/null +++ b/app/modules/history/history/history-templates/history-custom-attributes.jade @@ -0,0 +1,19 @@ +.diff-custom-new( + ng-if="vm.diff.new.length" + ng-repeat="newCustom in vm.diff.new" +) + span.key(translate="ACTIVITY.CREATED_CUSTOM_ATTRIBUTE") + span.diff ({{newCustom.name}}) + span.diff {{newCustom.value}} + +.diff-custom-new( + ng-if="vm.diff.changed.length" + ng-repeat="changeCustom in vm.diff.changed" +) + span.key(translate="ACTIVITY.UPDATED_CUSTOM_ATTRIBUTE") + span.diff ({{changeCustom.name}}) + span.diff {{changeCustom.changes.value[0]}} + tg-svg( + svg-icon="icon-arrow-right" + ) + span.diff {{changeCustom.changes.value[1]}} diff --git a/app/modules/history/history/history-templates/history-description.jade b/app/modules/history/history/history-templates/history-description.jade new file mode 100644 index 00000000..9c4dafb8 --- /dev/null +++ b/app/modules/history/history/history-templates/history-description.jade @@ -0,0 +1,12 @@ +.diff-status-wrapper + p.key( + translate="ACTIVITY.FIELDS.DESCRIPTION" + ) + p.diff( + ng-if="vm.diff[0]" + ng-bind-html="vm.diff[0]" + ) + p.diff( + ng-if="vm.diff[1]" + ng-bind-html="vm.diff[1]" + ) diff --git a/app/modules/history/history/history-templates/history-milestone.jade b/app/modules/history/history/history-templates/history-milestone.jade new file mode 100644 index 00000000..61b78d3d --- /dev/null +++ b/app/modules/history/history/history-templates/history-milestone.jade @@ -0,0 +1,11 @@ +.diff-milestone-wrapper + span.key( + translate="ACTIVITY.FIELDS.MILESTONE" + ) + span.diff(ng-if="vm.diff[0] != null") {{vm.diff[0]}} + span.diff(ng-if="vm.diff[0] == null") ... + tg-svg( + svg-icon="icon-arrow-right" + ) + span.diff(ng-if="vm.diff[1] != null") {{vm.diff[1]}} + span.diff(ng-if="vm.diff[1] == null") ... diff --git a/app/modules/history/history/history-templates/history-points.jade b/app/modules/history/history/history-templates/history-points.jade new file mode 100644 index 00000000..85c99704 --- /dev/null +++ b/app/modules/history/history/history-templates/history-points.jade @@ -0,0 +1,11 @@ +.diff-points-wrapper(ng-repeat="(key, diff) in vm.diff") + span.key( + translate="ACTIVITY.US_POINTS" + translate-values="{role: vm.diff.key}" + ) + span.diff {{diff[0]}} + tg-svg.comment-option( + svg-icon="icon-arrow-right" + svg-title-translate="COMMON.EDIT" + ) + span.diff {{diff[1]}} diff --git a/app/modules/history/history/history-templates/history-status.jade b/app/modules/history/history/history-templates/history-status.jade new file mode 100644 index 00000000..33af2ea2 --- /dev/null +++ b/app/modules/history/history/history-templates/history-status.jade @@ -0,0 +1,9 @@ +.diff-status-wrapper + span.key( + translate="ACTIVITY.FIELDS.STATUS" + ) + span.diff(ng-if="vm.diff[0]") {{vm.diff[0]}} + tg-svg( + svg-icon="icon-arrow-right" + ) + span.diff(ng-if="vm.diff[1]") {{vm.diff[1]}} diff --git a/app/modules/history/history/history-templates/history-subject.jade b/app/modules/history/history/history-templates/history-subject.jade new file mode 100644 index 00000000..e038ba01 --- /dev/null +++ b/app/modules/history/history/history-templates/history-subject.jade @@ -0,0 +1,9 @@ +.diff-subject-wrapper + span.key( + translate="ACTIVITY.FIELDS.SUBJECT" + ) + span.diff(ng-if="vm.diff[0]") {{vm.diff[0]}} + tg-svg( + svg-icon="icon-arrow-right" + ) + span.diff(ng-if="vm.diff[1]") {{vm.diff[1]}} diff --git a/app/modules/history/history/history-templates/history-tags.jade b/app/modules/history/history/history-templates/history-tags.jade new file mode 100644 index 00000000..32ec884b --- /dev/null +++ b/app/modules/history/history/history-templates/history-tags.jade @@ -0,0 +1,8 @@ +.diff-tags-wrapper + p(ng-if="vm.diffRemoveTags") + span.key(translate="ACTIVITY.TAGS_REMOVED") + span.diff {{vm.diffRemoveTags}} + + p(ng-if="vm.diffAddTags") + span.key(translate="ACTIVITY.TAGS_ADDED") + span.diff {{vm.diffAddTags}} diff --git a/app/modules/history/history/history-templates/history-templates.scss b/app/modules/history/history/history-templates/history-templates.scss new file mode 100644 index 00000000..69773c26 --- /dev/null +++ b/app/modules/history/history/history-templates/history-templates.scss @@ -0,0 +1,25 @@ +.activity-diff { + .key { + @include font-type(bold); + background: $whitish; + margin-right: .5rem; + padding: .25rem; + } + .diff { + line-height: 1.6; + } + .icon-arrow-right { + @include svg-size(.75rem); + fill: $gray-light; + margin: 0 .5rem; + } + .diff-status-wrapper { + p { + display: inline-block; + } + del { + background: lighten(rgba($primary-light, .3), 20%); + text-decoration: underline; + } + } +} diff --git a/app/modules/history/history/history-templates/team-requirement.jade b/app/modules/history/history/history-templates/team-requirement.jade new file mode 100644 index 00000000..592d1e50 --- /dev/null +++ b/app/modules/history/history/history-templates/team-requirement.jade @@ -0,0 +1,9 @@ +.diff-status-wrapper + span.key( + translate="ACTIVITY.TEAM_REQUIREMENT" + ) + span.diff {{vm.diff[0]}} + tg-svg( + svg-icon="icon-arrow-right" + ) + span.diff {{vm.diff[1]}} diff --git a/app/modules/history/history/history.directive.coffee b/app/modules/history/history/history.directive.coffee new file mode 100644 index 00000000..40862178 --- /dev/null +++ b/app/modules/history/history/history.directive.coffee @@ -0,0 +1,33 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: history.directive.coffee +### + +module = angular.module('taigaHistory') + +HistoryDirective = () -> + link = (scope, el, attrs) -> + + return { + scope: { + activities: "<" + }, + templateUrl:"history/history/history.html", + link: link + } + +module.directive("tgHistory", HistoryDirective) diff --git a/app/modules/history/history/history.jade b/app/modules/history/history/history.jade new file mode 100644 index 00000000..d53d022c --- /dev/null +++ b/app/modules/history/history/history.jade @@ -0,0 +1,19 @@ +section.activities + .activities-wrapper + .activity(ng-repeat="activity in activities track by activity.id") + img.activity-avatar( + ng-src="{{activity.user.photo}}" + ng-alt="{{activity.user.name}}" + ) + .activity-main + .activity-data + span.activity-creator {{activity.user.name}} + span.activity-date {{activity.created_at | momentFormat:'DD MMM YYYY HH:mm'}} + p.activity-text(ng-if="activity.comment") {{activity.comment}} + + .activity-diff( + ng-repeat="(key, diff) in activity.values_diff" + tg-history-diff + type='key' + diff='diff' + ) diff --git a/app/modules/history/history/history.scss b/app/modules/history/history/history.scss new file mode 100644 index 00000000..840e79f2 --- /dev/null +++ b/app/modules/history/history/history.scss @@ -0,0 +1,49 @@ +.activities { + .activity { + align-items: flex-start; + border-bottom: 1px solid $whitish; + display: flex; + padding: 2rem 0; + } + .activity-avatar { + flex-basis: 50px; + flex-shrink: 0; + margin-right: 1.5rem; + } + .activity-data { + margin-bottom: 1rem; + } + .activity-creator { + color: $primary; + margin-right: .5rem; + } + .activity-date { + color: $gray-light; + } + .comment-options { + align-items: center; + align-self: stretch; + display: flex; + flex-basis: 50px; + flex-shrink: 0; + margin-left: 1.5rem; + .comment-option { + cursor: pointer; + opacity: 0; + transition: opacity .2s; + } + .icon-edit { + fill: $gray-light; + margin-right: .5rem; + &:hover { + fill: $gray; + } + } + .icon-trash { + fill: $red-light; + &:hover { + fill: $red; + } + } + } +} diff --git a/app/modules/services/check-permissions.service.coffee b/app/modules/services/check-permissions.service.coffee index 5ca652e3..ad0cd7e9 100644 --- a/app/modules/services/check-permissions.service.coffee +++ b/app/modules/services/check-permissions.service.coffee @@ -19,7 +19,7 @@ taiga = @.taiga -class ChekcPermissionsService +class CheckPermissionsService @.$inject = [ "tgProjectService" ] @@ -31,4 +31,4 @@ class ChekcPermissionsService return @projectService.project.get('my_permissions').indexOf(permission) != -1 -angular.module("taigaCommon").service("tgCheckPermissionsService", ChekcPermissionsService) +angular.module("taigaCommon").service("tgCheckPermissionsService", CheckPermissionsService) diff --git a/app/partials/common/components/wysiwyg.jade b/app/partials/common/components/wysiwyg.jade index 97a62681..f68d4467 100644 --- a/app/partials/common/components/wysiwyg.jade +++ b/app/partials/common/components/wysiwyg.jade @@ -1,5 +1,5 @@ mixin wysihelp - div.wysiwyg-help + .wysiwyg-help span.drag-drop-help(ng-if="wiki.id", translate="COMMON.WYSIWYG.ATTACH_FILE_HELP") span.drag-drop-help(ng-if="!wiki.id", translate="COMMON.WYSIWYG.ATTACH_FILE_HELP_SAVE_FIRST") a.help-markdown( diff --git a/app/partials/common/history/history-activity.jade b/app/partials/common/history/history-activity.jade deleted file mode 100644 index 3b58f956..00000000 --- a/app/partials/common/history/history-activity.jade +++ /dev/null @@ -1,41 +0,0 @@ -.activity-single(class!="<%- mode %>") - .activity-user - a.avatar(href!="<%- userProfileUrl %>", title!="<%- userFullName %>") - img(src!="<%- avatar %>", alt!="<%- userFullName %>") - .activity-content - .activity-username - a.username(href!="<%- userProfileUrl %>", title!="<%- userFullName %>") - | <%- userFullName %> - span.date - | <%- creationDate %> - - <% if (comment.length > 0) { %> - <% if ((deleteCommentDate || deleteCommentUser)) { %> - .deleted-comment - span(translate="COMMENTS.DELETED_INFO", - translate-values!="{ user: '<%- deleteCommentUser %>', date: '<%- deleteCommentDate %>'}") - <% } %> - .comment.wysiwyg - div(ng-non-bindable) - | <%= comment %> - <% if (!deleteCommentDate && mode !== "activity" && canDeleteComment) { %> - a.comment-delete( - href="", - title!="<%- deleteCommentActionTitle %>", - data-activity-id!="<%- activityId %>" - ) - tg-svg(svg-icon="icon-trash") - <% } %> - <% } %> - - <% if(changes.length > 0) { %> - .changes - <% if (mode != "activity") { %> - a.changes-title(href="", title="{{'ACTIVITY.SHOW_ACTIVITY' | translate}}") - span <%- changesText %> - tg-svg(svg-icon="icon-arrow-right") - <% } %> - <% _.each(changes, function(change) { %> - | <%= change %> - <% }) %> - <% } %> diff --git a/app/partials/common/history/history-base-entries.jade b/app/partials/common/history/history-base-entries.jade deleted file mode 100644 index fe662b98..00000000 --- a/app/partials/common/history/history-base-entries.jade +++ /dev/null @@ -1,6 +0,0 @@ -<% if (showMore > 0) { %> -a(href="" title="{{ 'ACTIVITY.SHOW_MORE' | translate}}" class="show-more show-more-comments", translate="ACTIVITY.SHOW_MORE", translate-values!="{showMore: '<%- showMore %>'}") -<% } %> -<% _.each(entries, function(entry) { %> -<%= entry %> -<% }) %> \ No newline at end of file diff --git a/app/partials/common/history/history-base.jade b/app/partials/common/history/history-base.jade deleted file mode 100644 index 1fddd9c4..00000000 --- a/app/partials/common/history/history-base.jade +++ /dev/null @@ -1,35 +0,0 @@ -include ../components/wysiwyg.jade - -section.history - <% if (commentsVisible || historyVisible) { %> - ul.history-tabs - <% if (commentsVisible) { %> - li.active - a( - href="", - data-section-class="history-comments" - ) - tg-svg(svg-icon="icon-writer") - span.tab-title(translate="COMMENTS.TITLE") - <% } %> - <% if (historyVisible) { %> - li - a( - href="", - data-section-class="history-activity" - ) - tg-svg(svg-icon="icon-timeline") - span.tab-title(translate="ACTIVITY.TITLE") - <% } %> - <% } %> - section.history-comments - .comments-list - div(tg-editable-wysiwyg, ng-model!="<%- ngmodel %>") - div(tg-check-permission!="modify_<%- type %>", tg-toggle-comment, class="add-comment") - textarea(ng-attr-placeholder="{{'COMMENTS.TYPE_NEW_COMMENT' | translate}}", ng-model!="<%- ngmodel %>.comment", tg-markitup="tg-markitup") - <% if (mode !== "edit") { %> - +wysihelp - button(type="button", ng-disabled!="!<%- ngmodel %>.comment.length" title="{{'COMMENTS.COMMENT' | translate}}", translate="COMMENTS.COMMENT", class="button button-green save-comment") - <% } %> - section.history-activity.hidden - .changes-list diff --git a/app/partials/common/history/history-change-attachment.jade b/app/partials/common/history/history-change-attachment.jade deleted file mode 100644 index 6b12816f..00000000 --- a/app/partials/common/history/history-change-attachment.jade +++ /dev/null @@ -1,16 +0,0 @@ -.change-entry - .activity-changed - span <%- name %> - .activity-fromto - <% _.each(diff, function(change) { %> - p - strong <%- change.name %>  - strong(translate="COMMON.FROM") - br - span <%- change.from %> - p - strong <%- change.name %>  - strong(translate="COMMON.TO") - br - span <%- change.to %> - <% }) %> diff --git a/app/partials/common/history/history-change-diff.jade b/app/partials/common/history/history-change-diff.jade deleted file mode 100644 index 13e137bd..00000000 --- a/app/partials/common/history/history-change-diff.jade +++ /dev/null @@ -1,6 +0,0 @@ -.change-entry - .activity-changed - span <%- name %> - .activity-fromto - p - span <%= diff %> diff --git a/app/partials/common/history/history-change-generic.jade b/app/partials/common/history/history-change-generic.jade deleted file mode 100644 index 8c29b5c1..00000000 --- a/app/partials/common/history/history-change-generic.jade +++ /dev/null @@ -1,12 +0,0 @@ -.change-entry - .activity-changed - span <%- name %> - .activity-fromto - p - strong(translate="COMMON.FROM") - br - span <%- from %> - p - strong(translate="COMMON.TO") - br - span <%- to %> diff --git a/app/partials/common/history/history-change-list.jade b/app/partials/common/history/history-change-list.jade deleted file mode 100644 index 038cfc88..00000000 --- a/app/partials/common/history/history-change-list.jade +++ /dev/null @@ -1,17 +0,0 @@ -.change-entry - .activity-changed - span <%- name %> - .activity-fromto - <% if (removed.length > 0) { %> - p - strong(translate="ACTIVITY.REMOVED") - br - span <%- removed %> - <% } %> - - <% if (added.length > 0) { %> - p - strong(translate="ACTIVITY.ADDED") - br - span <%- added %> - <% } %> diff --git a/app/partials/common/history/history-change-points.jade b/app/partials/common/history/history-change-points.jade deleted file mode 100644 index 89429a93..00000000 --- a/app/partials/common/history/history-change-points.jade +++ /dev/null @@ -1,14 +0,0 @@ -<% _.each(points, function(point, name) { %> -.change-entry - .activity-changed - span(translate="ACTIVITY.US_POINTS", translate-values!="{name: '<%- name %>'}") - .activity-fromto - p - strong(translate="COMMON.FROM") - br - span <%- point[0] %> - p - strong(translate="COMMON.TO") - br - span <%- point[1] %> -<% }); %> diff --git a/app/partials/common/history/history-deleted-comment.jade b/app/partials/common/history/history-deleted-comment.jade deleted file mode 100644 index 73a2f2e2..00000000 --- a/app/partials/common/history/history-deleted-comment.jade +++ /dev/null @@ -1,18 +0,0 @@ -.activity-single.comment.deleted-comment - div - span(translate="COMMENTS.DELETED_INFO", - translate-values!="{user: '<%- deleteCommentUser %>', date: '<%- deleteCommentDate %>'}") - a(href="", title="{{'COMMENTS.SHOW_DELETED' | translate}}", - class="show-deleted-comment", translate="COMMENTS.SHOW_DELETED") - a(href="", title="{{'COMMENTS.HIDE_DELETED' | translate}}", - class="hide-deleted-comment hidden", translate="COMMENTS.HIDE_DELETED") - .comment-body.wysiwyg <%= deleteComment %> - <% if (canRestoreComment) { %> - a.comment-restore( - href="" - data-activity-id!="<%- activityId %>" - title="{{ 'COMMENTS.RESTORE' | translate }}" - ) - tg-svg(svg-icon="icon-reload") - span(translate="COMMENTS.RESTORE") - <% } %> diff --git a/app/partials/issue/issues-detail.jade b/app/partials/issue/issues-detail.jade index aa8dfefa..ab8bce2c 100644 --- a/app/partials/issue/issues-detail.jade +++ b/app/partials/issue/issues-detail.jade @@ -92,10 +92,12 @@ div.wrapper( project-id="projectId" edit-permission = "modify_issue" ) - - tg-history( - ng-model="issue" + + tg-history-section( + ng-if="issue" type="issue" + name="issue" + id="issue.id" ) sidebar.menu-secondary.sidebar.ticket-data diff --git a/app/partials/task/task-detail.jade b/app/partials/task/task-detail.jade index a0d00f8d..68d7dff9 100644 --- a/app/partials/task/task-detail.jade +++ b/app/partials/task/task-detail.jade @@ -94,8 +94,13 @@ div.wrapper( project-id="projectId" edit-permission = "modify_task" ) - - tg-history(ng-model="task", type="task") + + tg-history-section( + ng-if="task" + type="task" + name="task" + id="task.id" + ) sidebar.menu-secondary.sidebar.ticket-data diff --git a/app/partials/us/us-detail.jade b/app/partials/us/us-detail.jade index 0896a232..54976f35 100644 --- a/app/partials/us/us-detail.jade +++ b/app/partials/us/us-detail.jade @@ -90,9 +90,12 @@ div.wrapper( edit-permission = "modify_us" ) - tg-history( - ng-model="us" + tg-history-section( + ng-if="us" type="us" + name="us" + id="us.id" + project-id="projectId" ) sidebar.menu-secondary.sidebar.ticket-data diff --git a/app/styles/components/wysiwyg.scss b/app/styles/components/wysiwyg.scss index d091a699..dbf1220d 100644 --- a/app/styles/components/wysiwyg.scss +++ b/app/styles/components/wysiwyg.scss @@ -40,6 +40,10 @@ margin-bottom: 0; margin-top: 0; padding-left: 2em; + ul, + ol { + padding-left: 1rem; + } } ul { list-style-type: disc; diff --git a/app/styles/modules/common/history.scss b/app/styles/modules/common/history.scss deleted file mode 100644 index 52736c9f..00000000 --- a/app/styles/modules/common/history.scss +++ /dev/null @@ -1,278 +0,0 @@ -.history { - margin-bottom: 1rem; -} -.changes-title { - display: block; - padding: .5rem; - &:hover { - .icon { - color: $primary; - transform: rotate(90deg); - transition: all .2s linear; - } - } - .icon { - color: $grayer; - float: right; - transform: rotate(0); - transition: all .2s linear; - } -} -.change-entry { - border-bottom: 1px solid $gray-light; - display: flex; - padding: .5rem; - &:last-child { - border-bottom: 0; - } - .activity-changed, - .activity-fromto { - flex-basis: 50px; - flex-grow: 1; - } - .activity-changed { - @include font-type(bold); - } - .activity-fromto { - @include font-size(small); - word-wrap: break-word; - } -} -.history-tabs { - @include font-type(light); - border-bottom: 1px solid $whitish; - border-top: 1px solid $whitish; - margin-bottom: 0; - li { - background: $white; - display: inline-block; - position: relative; - &.active { - border-left: 1px solid $whitish; - border-right: 1px solid $whitish; - color: $primary; - top: 1px; - } - &:hover { - color: $grayer; - transition: color .2s ease-in; - } - } - a { - color: $gray-light; - display: block; - padding: .5rem 2rem; - transition: color .2s ease-in; - } - .icon { - fill: currentColor; - height: .75rem; - margin-right: .5rem; - width: .75rem; - } -} -.add-comment { - @include cursor-progress; - @include clearfix; - margin-top: 1rem; - &.active { - .button-green { - display: block; - margin-top: .5rem; - } - textarea { - height: 6rem; - transition: height .3s ease-in; - } - .help-markdown { - opacity: 1; - transition: opacity .3s linear; - } - .preview-icon { - opacity: 1; - position: absolute; - right: 1rem; - } - } - textarea { - background: $white; - height: 5rem; - min-height: 41px; - } - .help-markdown { - opacity: 0; - } - .save-comment { - color: $white; - float: right; - } - .button-green { - display: none; - } - .edit, - .preview-icon { - position: absolute; - right: 1rem; - top: .5rem; - } - .edit { - fill: $gray-light; - &:hover { - cursor: pointer; - fill: $primary; - } - } - .preview-icon { - opacity: 0; - } -} -.show-more-comments { - @include font-size(small); - border-bottom: 1px solid $gray-light; - border-top: 1px solid $gray-light; - color: $gray-light; - display: block; - padding: 1rem 0 1rem 1rem; - &:hover { - background: lighten($primary, 60%); - transition: background .2s ease-in; - } -} -.comment-list { - &.activeanimation { - .comment-single.ng-enter:last-child, - .comment-single.ng-leave:last-child { - transition: all .3s ease-in; - } - .comment-single.ng-enter:last-child, - .comment-single.ng-leave.ng-leave-active:last-child { - opacity: 0; - } - .comment-single.ng-leave:last-child, - .comment-single.ng-enter.ng-enter-active:last-child { - opacity: 1; - } - } -} -.activity-single { - border-bottom: 1px solid $gray-light; - display: flex; - padding: 2rem 0; - position: relative; - &:hover { - .comment-delete { - opacity: 1; - transition: opacity .2s linear; - } - .comment-restore { - opacity: 1; - transition: opacity .2s linear; - } - } - &:first-child { - margin-top: 0; - } - &:last-child { - border-bottom: 0; - } - &.deleted-comment, - .deleted-comment { - @include font-size(small); - color: $gray-light; - padding: .5rem; - a { - color: $gray-light; - margin-left: .3rem; - &:hover { - color: $primary; - transition: color .2s linear; - } - } - img { - filter: grayscale(100%); - opacity: .5; - } - .comment-body { - display: none; - margin: .2rem 0 .5rem; - p { - @include font-size(medium); - } - } - } - .comment-restore { - @include font-size(small); - color: $gray-light; - display: block; - position: absolute; - right: 0; - top: .4rem; - .icon { - vertical-align: baseline; - } - &:hover { - color: $primary; - transition: color .2s linear; - } - } - .username { - color: $primary; - margin-bottom: .5rem; - } - .activity-user { - flex-basis: 60px; - flex-shrink: 0; - margin-right: 1rem; - img { - width: 100%; - } - } - .activity-username { - color: $primary; - margin-bottom: .5rem; - } - .activity-content { - flex-shrink: 0; - width: calc(100% - 80px); - } - .changes { - background: $mass-white; - .change-entry { - display: none; - &.active { - display: flex; - } - } - } - .date { - @include font-size(small); - color: $gray-light; - margin-left: 1rem; - } - .wysiwyg { - margin-bottom: 0; - } - .comment-delete { - cursor: pointer; - display: block; - opacity: 0; - position: absolute; - right: .5rem; - top: 2rem; - svg { - fill: $red-light; - transition: all .2s linear; - } - &:hover { - svg { - fill: $red; - transition: color .2s linear; - } - } - } - &.activity { - .change-entry { - display: flex; - } - } -} diff --git a/e2e/helpers/detail-helper.js b/e2e/helpers/detail-helper.js index eb6c77f9..d0d33f7a 100644 --- a/e2e/helpers/detail-helper.js +++ b/e2e/helpers/detail-helper.js @@ -16,8 +16,9 @@ helper.title = function() { el.$('.edit-subject input').clear().sendKeys(title); }, - save: function() { + save: async function() { el.$('.save').click(); + await browser.waitForAngular(); } }; @@ -144,17 +145,37 @@ helper.assignedTo = function() { return obj; }; +helper.editComment = function() { + let el = $('.comment-editor'); + let obj = { + el:el, + + updateText: function (text) { + el.$('textarea').sendKeys(text); + }, + + saveComment: async function () { + el.$('.save-comment').click() + await browser.waitForAngular(); + } + } + return obj; + +}; + helper.history = function() { let el = $('section.history'); let obj = { el:el, - selectCommentsTab: function() { - el.$$('.history-tabs li a').first().click(); + selectCommentsTab: async function() { + el.$('.e2e-comments-tab').click(); + await browser.waitForAngular(); }, - selectActivityTab: function() { - el.$$('.history-tabs li a').last().click(); + selectActivityTab: async function() { + el.$('.e2e-activity-tab').click(); + await browser.waitForAngular(); }, addComment: async function(comment) { @@ -168,46 +189,65 @@ helper.history = function() { }, countComments: async function() { - let moreComments = el.$('.comments-list .show-more-comments'); - let moreCommentsIsPresent = await moreComments.isPresent(); - if (moreCommentsIsPresent){ - moreComments.click(); - } - await browser.waitForAngular(); - let comments = await el.$$(".activity-single.comment"); + let comments = await el.$$(".comment-wrapper"); return comments.length; }, countActivities: async function() { - let moreActivities = el.$('.changes-list .show-more-comments'); - let selectActivityTabIsPresent = await moreActivities.isPresent(); - if (selectActivityTabIsPresent){ - utils.common.link(moreActivities); - // moreActivities.click(); - } - await browser.waitForAngular(); - let activities = await el.$$(".activity-single.activity"); + let activities = await el.$$(".activity"); return activities.length; }, countDeletedComments: async function() { - let moreComments = el.$('.comments-list .show-more-comments'); - let moreCommentsIsPresent = await moreComments.isPresent(); - if (moreCommentsIsPresent){ - moreComments.click(); - } - await browser.waitForAngular(); - let comments = await el.$$(".activity-single.comment.deleted-comment"); + let comments = await el.$$(".deleted-comment-wrapper"); return comments.length; }, + editLastComment: async function() { + let lastComment = el.$$(".comment-wrapper").last() + browser + .actions() + .mouseMove(lastComment) + .perform(); + + lastComment.$$(".comment-option").first().click(); + await browser.waitForAngular(); + }, + deleteLastComment: async function() { - el.$$(".activity-single.comment .comment-delete").last().click(); + let lastComment = el.$$(".comment-wrapper").last() + browser + .actions() + .mouseMove(lastComment) + .perform(); + + lastComment.$$(".comment-option").last().click(); + await browser.waitForAngular(); + }, + + showVersionsLastComment: async function() { + el.$$(".comment-edited a").last().click(); + await browser.waitForAngular(); + }, + + closeVersionsLastComment: async function() { + $(".lightbox-display-historic .close").click(); + await browser.waitForAngular(); + }, + + enableEditModeLastComment: async function() { + let lastComment = el.$$(".comment-wrapper").last() + browser + .actions() + .mouseMove(lastComment) + .perform(); + + lastComment.$$(".comment-option").last().click(); await browser.waitForAngular(); }, restoreLastComment: async function() { - el.$$(".activity-single.comment.deleted-comment .comment-restore").last().click(); + el.$$(".deleted-comment-wrapper .restore-comment").last().click(); await browser.waitForAngular(); } } diff --git a/e2e/shared/detail.js b/e2e/shared/detail.js index 43f2525f..abfde5d2 100644 --- a/e2e/shared/detail.js +++ b/e2e/shared/detail.js @@ -1,4 +1,5 @@ var path = require('path'); +var utils = require('../utils'); var detailHelper = require('../helpers').detail; var commonHelper = require('../helpers').common; var customFieldsHelper = require('../helpers/custom-fields-helper'); @@ -191,29 +192,49 @@ shared.assignedToTesting = function() { }); } -shared.historyTesting = async function() { +shared.historyTesting = async function(screenshotsFolder) { let historyHelper = detailHelper.history(); + + //Adding a comment historyHelper.selectCommentsTab(); + await utils.common.takeScreenshot(screenshotsFolder, "show comments tab"); let commentsCounter = await historyHelper.countComments(); let date = Date.now(); - await historyHelper.addComment("New comment " + date); - let newCommentsCounter = await historyHelper.countComments(); + await historyHelper.addComment("New comment " + date); + await utils.common.takeScreenshot(screenshotsFolder, "new coment"); + + let newCommentsCounter = await historyHelper.countComments(); expect(newCommentsCounter).to.be.equal(commentsCounter+1); + //Edit last comment + historyHelper.editLastComment(); + let editComment = detailHelper.editComment(); + editComment.updateText("This is the new and updated text"); + editComment.saveComment(); + await utils.common.takeScreenshot(screenshotsFolder, "edit comment"); + + //Show versions from last comment edited + historyHelper.showVersionsLastComment(); + await utils.common.takeScreenshot(screenshotsFolder, "show comment versions"); + + historyHelper.closeVersionsLastComment(); + //Deleting last comment let deletedCommentsCounter = await historyHelper.countDeletedComments(); await historyHelper.deleteLastComment(); let newDeletedCommentsCounter = await historyHelper.countDeletedComments(); expect(newDeletedCommentsCounter).to.be.equal(deletedCommentsCounter+1); + await utils.common.takeScreenshot(screenshotsFolder, "deleted comment"); //Restore last comment deletedCommentsCounter = await historyHelper.countDeletedComments(); await historyHelper.restoreLastComment(); newDeletedCommentsCounter = await historyHelper.countDeletedComments(); expect(newDeletedCommentsCounter).to.be.equal(deletedCommentsCounter-1); + await utils.common.takeScreenshot(screenshotsFolder, "restored comment"); //Store comment with a modification commentsCounter = await historyHelper.countComments(); @@ -221,18 +242,19 @@ shared.historyTesting = async function() { historyHelper.writeComment("New comment " + date); let title = detailHelper.title(); title.setTitle('changed'); - title.save(); - + await title.save(); newCommentsCounter = await historyHelper.countComments(); expect(newCommentsCounter).to.be.equal(commentsCounter+1); //Check activity await historyHelper.selectActivityTab(); + await utils.common.takeScreenshot(screenshotsFolder, "show activity tab"); let activitiesCounter = await historyHelper.countActivities(); - expect(activitiesCounter).to.be.least(newCommentsCounter); + expect(newCommentsCounter).to.be.least(activitiesCounter); + } shared.blockTesting = async function() { diff --git a/e2e/suites/issues/issue-detail.e2e.js b/e2e/suites/issues/issue-detail.e2e.js index af466e1c..a1674ccf 100644 --- a/e2e/suites/issues/issue-detail.e2e.js +++ b/e2e/suites/issues/issue-detail.e2e.js @@ -37,7 +37,7 @@ describe('Issue detail', async function(){ describe('watchers edition', sharedDetail.watchersTesting); - it('history', sharedDetail.historyTesting); + it('history', sharedDetail.historyTesting.bind(this, "issues")); it('block', sharedDetail.blockTesting); @@ -57,4 +57,5 @@ describe('Issue detail', async function(){ expect(url).not.to.be.equal(issueUrl); }); }); + }); diff --git a/e2e/suites/tasks/task-detail.e2e.js b/e2e/suites/tasks/task-detail.e2e.js index 74fab211..31dca89c 100644 --- a/e2e/suites/tasks/task-detail.e2e.js +++ b/e2e/suites/tasks/task-detail.e2e.js @@ -53,7 +53,7 @@ describe('Task detail', function(){ expect(newIsIocaine).to.be.equal(isIocaine); }); - it('history', sharedDetail.historyTesting); + it('history', sharedDetail.historyTesting.bind(this, "tasks")); it('block', sharedDetail.blockTesting); diff --git a/e2e/suites/user-stories/user-story-detail.e2e.js b/e2e/suites/user-stories/user-story-detail.e2e.js index e3e21339..9727efa6 100644 --- a/e2e/suites/user-stories/user-story-detail.e2e.js +++ b/e2e/suites/user-stories/user-story-detail.e2e.js @@ -68,7 +68,7 @@ describe('User story detail', function(){ describe('watchers edition', sharedDetail.watchersTesting); - it('history', sharedDetail.historyTesting); + it('history', sharedDetail.historyTesting.bind(this, "user-stories")); it('block', sharedDetail.blockTesting);