Merge pull request #1020 from taigaio/us/4186-2929/add_and_edit_comment_permission

US #4186 #2929: Add and edit comment permission
stable
Juanfran 2016-06-15 08:38:20 +02:00 committed by GitHub
commit 4a2ae30d30
73 changed files with 2078 additions and 1006 deletions

View File

@ -10,6 +10,8 @@
- Attachments image slider - Attachments image slider
- New admin area to edit the tag colors used in your project - 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)) - 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 ### Misc
- Lots of small and not so small bugfixes. - Lots of small and not so small bugfixes.

View File

@ -771,6 +771,7 @@ modules = [
"taigaUserTimeline", "taigaUserTimeline",
"taigaExternalApps", "taigaExternalApps",
"taigaDiscover", "taigaDiscover",
"taigaHistory",
# template cache # template cache
"templates", "templates",

View File

@ -367,6 +367,7 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm, $compile) ->
{ key: "view_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.VIEW_USER_STORIES"} { 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: "add_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.ADD_USER_STORIES"}
{ key: "modify_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.MODIFY_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"} { key: "delete_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.DELETE_USER_STORIES"}
] ]
categories.push({ categories.push({
@ -378,6 +379,7 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm, $compile) ->
{ key: "view_tasks", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.VIEW_TASKS"} { key: "view_tasks", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.VIEW_TASKS"}
{ key: "add_task", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.ADD_TASKS"} { key: "add_task", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.ADD_TASKS"}
{ key: "modify_task", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.MODIFY_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"} { key: "delete_task", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.DELETE_TASKS"}
] ]
categories.push({ categories.push({
@ -389,6 +391,7 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm, $compile) ->
{ key: "view_issues", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.VIEW_ISSUES"} { key: "view_issues", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.VIEW_ISSUES"}
{ key: "add_issue", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.ADD_ISSUES"} { key: "add_issue", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.ADD_ISSUES"}
{ key: "modify_issue", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.MODIFY_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"} { key: "delete_issue", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.DELETE_ISSUES"}
] ]
categories.push({ categories.push({

View File

@ -1,507 +0,0 @@
###
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán Merino <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# Copyright (C) 2014-2016 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
# Copyright (C) 2014-2016 Xavi Julian <xavier.julian@kaleidos.net>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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])

View File

@ -83,7 +83,7 @@ MarkitupDirective = ($rootscope, $rs, $selectedText, $template, $compile, $trans
markdownDomNode = element.parents(".markdown") markdownDomNode = element.parents(".markdown")
markItUpDomNode = element.parents(".markItUp") 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 = previewTemplate({data: data.data})
html = $compile(html)($scope) html = $compile(html)($scope)

View File

@ -31,6 +31,25 @@ resourceProvider = ($repo, $http, $urls) ->
service.get = (type, objectId) -> service.get = (type, objectId) ->
return $repo.queryOneRaw("history/#{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) -> service.deleteComment = (type, objectId, activityId) ->
url = $urls.resolve("history/#{type}") url = $urls.resolve("history/#{type}")
url = "#{url}/#{objectId}/delete_comment" url = "#{url}/#{objectId}/delete_comment"

View File

@ -244,6 +244,7 @@
"VIEW_USER_STORIES": "View user stories", "VIEW_USER_STORIES": "View user stories",
"ADD_USER_STORIES": "Add user stories", "ADD_USER_STORIES": "Add user stories",
"MODIFY_USER_STORIES": "Modify user stories", "MODIFY_USER_STORIES": "Modify user stories",
"COMMENT_USER_STORIES": "Comment user stories",
"DELETE_USER_STORIES": "Delete user stories" "DELETE_USER_STORIES": "Delete user stories"
}, },
"TASKS": { "TASKS": {
@ -251,6 +252,7 @@
"VIEW_TASKS": "View tasks", "VIEW_TASKS": "View tasks",
"ADD_TASKS": "Add tasks", "ADD_TASKS": "Add tasks",
"MODIFY_TASKS": "Modify tasks", "MODIFY_TASKS": "Modify tasks",
"COMMENT_TASKS": "Comment tasks",
"DELETE_TASKS": "Delete tasks" "DELETE_TASKS": "Delete tasks"
}, },
"ISSUES": { "ISSUES": {
@ -258,6 +260,7 @@
"VIEW_ISSUES": "View issues", "VIEW_ISSUES": "View issues",
"ADD_ISSUES": "Add issues", "ADD_ISSUES": "Add issues",
"MODIFY_ISSUES": "Modify issues", "MODIFY_ISSUES": "Modify issues",
"COMMENT_ISSUES": "Comment issues",
"DELETE_ISSUES": "Delete issues" "DELETE_ISSUES": "Delete issues"
}, },
"WIKI": { "WIKI": {
@ -1018,28 +1021,47 @@
} }
}, },
"COMMENTS": { "COMMENTS": {
"DELETED_INFO": "Comment deleted by {{user}} on {{date}}", "DELETED_INFO": "Comment deleted by {{user}}",
"TITLE": "Comments", "TITLE": "Comments",
"COMMENTS_COUNT": "{{comments}} Comments",
"ORDER": "Order",
"OLDER_FIRST": "Older first",
"RECENT_FIRST": "Recent first",
"COMMENT": "Comment", "COMMENT": "Comment",
"EDIT_COMMENT": "Edit comment",
"EDITED_COMMENT": "Edited:",
"SHOW_HISTORY": "View historic",
"TYPE_NEW_COMMENT": "Type a new comment here", "TYPE_NEW_COMMENT": "Type a new comment here",
"SHOW_DELETED": "Show deleted comment", "SHOW_DELETED": "Show deleted comment",
"HIDE_DELETED": "Hide deleted comment", "HIDE_DELETED": "Hide deleted comment",
"DELETE": "Delete comment", "DELETE": "Delete comment",
"RESTORE": "Restore comment" "RESTORE": "Restore comment",
"HISTORY": {
"TITLE": "Activity"
}
}, },
"ACTIVITY": { "ACTIVITY": {
"SHOW_ACTIVITY": "Show activity", "SHOW_ACTIVITY": "Show activity",
"DATETIME": "DD MMM YYYY HH:mm", "DATETIME": "DD MMM YYYY HH:mm",
"SHOW_MORE": "+ Show previous entries ({{showMore}} more)", "SHOW_MORE": "+ Show previous entries ({{showMore}} more)",
"TITLE": "Activity", "TITLE": "Activity",
"ACTIVITIES_COUNT": "{{activities}} Activities",
"REMOVED": "removed", "REMOVED": "removed",
"ADDED": "added", "ADDED": "added",
"US_POINTS": "US points ({{name}})", "TAGS_ADDED": "tags added:",
"NEW_ATTACHMENT": "new attachment", "TAGS_REMOVED": "tags removed:",
"DELETED_ATTACHMENT": "deleted attachment", "US_POINTS": "{{role}} points",
"UPDATED_ATTACHMENT": "updated attachment {{filename}}", "NEW_ATTACHMENT": "new attachment:",
"DELETED_CUSTOM_ATTRIBUTE": "deleted custom attribute", "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}}", "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": { "VALUES": {
"YES": "yes", "YES": "yes",
"NO": "no", "NO": "no",
@ -1071,6 +1093,7 @@
"TAGS": "tags", "TAGS": "tags",
"ATTACHMENTS": "attachments", "ATTACHMENTS": "attachments",
"IS_DEPRECATED": "is deprecated", "IS_DEPRECATED": "is deprecated",
"IS_NOT_DEPRECATED": "is not deprecated",
"ORDER": "order", "ORDER": "order",
"BACKLOG_ORDER": "backlog order", "BACKLOG_ORDER": "backlog order",
"SPRINT_ORDER": "sprint order", "SPRINT_ORDER": "sprint order",

View File

@ -0,0 +1,64 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,134 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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

View File

@ -0,0 +1,44 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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"
)

View File

@ -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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
#
# File: comments.controller.coffee
###
module = angular.module("taigaHistory")
class CommentsController
@.$inject = []
constructor: () ->
initializePermissions: () ->
@.canAddCommentPermission = 'comment_' + @.name
module.controller("CommentsCtrl", CommentsController)

View File

@ -0,0 +1,41 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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")

View File

@ -0,0 +1,48 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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"
)

View File

@ -0,0 +1,37 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,63 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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()

View File

@ -0,0 +1,40 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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"
)

View File

@ -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;
}
}

View File

@ -0,0 +1,31 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# File: comment.directive.coffee
###
module = angular.module('taigaHistory')
HistoryEntryDirective = (lightboxService) ->
return {
scope: {
entry: "<"
},
templateUrl:"history/history-lightbox/history-entry.html",
}
module.directive("tgHistoryEntry", HistoryEntryDirective)

View File

@ -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"
)

View File

@ -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%;
}
}
}
}

View File

@ -0,0 +1,37 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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"
)

View File

@ -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);
}
}

View File

@ -0,0 +1,91 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,214 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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

View File

@ -0,0 +1,42 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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"
)

View File

@ -0,0 +1,20 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# File: history.module.coffee
###
angular.module("taigaHistory", [])

View File

@ -0,0 +1,34 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,43 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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')

View File

@ -0,0 +1,38 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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

View File

@ -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]}}

View File

@ -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]}}

View File

@ -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]}}

View File

@ -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}}

View File

@ -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]}}

View File

@ -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]"
)

View File

@ -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") ...

View File

@ -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]}}

View File

@ -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]}}

View File

@ -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]}}

View File

@ -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}}

View File

@ -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;
}
}
}

View File

@ -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]}}

View File

@ -0,0 +1,33 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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'
)

View File

@ -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;
}
}
}
}

View File

@ -19,7 +19,7 @@
taiga = @.taiga taiga = @.taiga
class ChekcPermissionsService class CheckPermissionsService
@.$inject = [ @.$inject = [
"tgProjectService" "tgProjectService"
] ]
@ -31,4 +31,4 @@ class ChekcPermissionsService
return @projectService.project.get('my_permissions').indexOf(permission) != -1 return @projectService.project.get('my_permissions').indexOf(permission) != -1
angular.module("taigaCommon").service("tgCheckPermissionsService", ChekcPermissionsService) angular.module("taigaCommon").service("tgCheckPermissionsService", CheckPermissionsService)

View File

@ -1,5 +1,5 @@
mixin wysihelp 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")
span.drag-drop-help(ng-if="!wiki.id", translate="COMMON.WYSIWYG.ATTACH_FILE_HELP_SAVE_FIRST") span.drag-drop-help(ng-if="!wiki.id", translate="COMMON.WYSIWYG.ATTACH_FILE_HELP_SAVE_FIRST")
a.help-markdown( a.help-markdown(

View File

@ -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 %>
<% }) %>
<% } %>

View File

@ -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 %>
<% }) %>

View File

@ -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

View File

@ -1,16 +0,0 @@
.change-entry
.activity-changed
span <%- name %>
.activity-fromto
<% _.each(diff, function(change) { %>
p
strong <%- change.name %>&nbsp;
strong(translate="COMMON.FROM")
br
span <%- change.from %>
p
strong <%- change.name %>&nbsp;
strong(translate="COMMON.TO")
br
span <%- change.to %>
<% }) %>

View File

@ -1,6 +0,0 @@
.change-entry
.activity-changed
span <%- name %>
.activity-fromto
p
span <%= diff %>

View File

@ -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 %>

View File

@ -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 %>
<% } %>

View File

@ -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] %>
<% }); %>

View File

@ -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")
<% } %>

View File

@ -92,10 +92,12 @@ div.wrapper(
project-id="projectId" project-id="projectId"
edit-permission = "modify_issue" edit-permission = "modify_issue"
) )
tg-history( tg-history-section(
ng-model="issue" ng-if="issue"
type="issue" type="issue"
name="issue"
id="issue.id"
) )
sidebar.menu-secondary.sidebar.ticket-data sidebar.menu-secondary.sidebar.ticket-data

View File

@ -94,8 +94,13 @@ div.wrapper(
project-id="projectId" project-id="projectId"
edit-permission = "modify_task" 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 sidebar.menu-secondary.sidebar.ticket-data

View File

@ -90,9 +90,12 @@ div.wrapper(
edit-permission = "modify_us" edit-permission = "modify_us"
) )
tg-history( tg-history-section(
ng-model="us" ng-if="us"
type="us" type="us"
name="us"
id="us.id"
project-id="projectId"
) )
sidebar.menu-secondary.sidebar.ticket-data sidebar.menu-secondary.sidebar.ticket-data

View File

@ -40,6 +40,10 @@
margin-bottom: 0; margin-bottom: 0;
margin-top: 0; margin-top: 0;
padding-left: 2em; padding-left: 2em;
ul,
ol {
padding-left: 1rem;
}
} }
ul { ul {
list-style-type: disc; list-style-type: disc;

View File

@ -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;
}
}
}

View File

@ -16,8 +16,9 @@ helper.title = function() {
el.$('.edit-subject input').clear().sendKeys(title); el.$('.edit-subject input').clear().sendKeys(title);
}, },
save: function() { save: async function() {
el.$('.save').click(); el.$('.save').click();
await browser.waitForAngular();
} }
}; };
@ -144,17 +145,37 @@ helper.assignedTo = function() {
return obj; 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() { helper.history = function() {
let el = $('section.history'); let el = $('section.history');
let obj = { let obj = {
el:el, el:el,
selectCommentsTab: function() { selectCommentsTab: async function() {
el.$$('.history-tabs li a').first().click(); el.$('.e2e-comments-tab').click();
await browser.waitForAngular();
}, },
selectActivityTab: function() { selectActivityTab: async function() {
el.$$('.history-tabs li a').last().click(); el.$('.e2e-activity-tab').click();
await browser.waitForAngular();
}, },
addComment: async function(comment) { addComment: async function(comment) {
@ -168,46 +189,65 @@ helper.history = function() {
}, },
countComments: async function() { countComments: async function() {
let moreComments = el.$('.comments-list .show-more-comments'); let comments = await el.$$(".comment-wrapper");
let moreCommentsIsPresent = await moreComments.isPresent();
if (moreCommentsIsPresent){
moreComments.click();
}
await browser.waitForAngular();
let comments = await el.$$(".activity-single.comment");
return comments.length; return comments.length;
}, },
countActivities: async function() { countActivities: async function() {
let moreActivities = el.$('.changes-list .show-more-comments'); let activities = await el.$$(".activity");
let selectActivityTabIsPresent = await moreActivities.isPresent();
if (selectActivityTabIsPresent){
utils.common.link(moreActivities);
// moreActivities.click();
}
await browser.waitForAngular();
let activities = await el.$$(".activity-single.activity");
return activities.length; return activities.length;
}, },
countDeletedComments: async function() { countDeletedComments: async function() {
let moreComments = el.$('.comments-list .show-more-comments'); let comments = await el.$$(".deleted-comment-wrapper");
let moreCommentsIsPresent = await moreComments.isPresent();
if (moreCommentsIsPresent){
moreComments.click();
}
await browser.waitForAngular();
let comments = await el.$$(".activity-single.comment.deleted-comment");
return comments.length; 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() { 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(); await browser.waitForAngular();
}, },
restoreLastComment: async function() { restoreLastComment: async function() {
el.$$(".activity-single.comment.deleted-comment .comment-restore").last().click(); el.$$(".deleted-comment-wrapper .restore-comment").last().click();
await browser.waitForAngular(); await browser.waitForAngular();
} }
} }

View File

@ -1,4 +1,5 @@
var path = require('path'); var path = require('path');
var utils = require('../utils');
var detailHelper = require('../helpers').detail; var detailHelper = require('../helpers').detail;
var commonHelper = require('../helpers').common; var commonHelper = require('../helpers').common;
var customFieldsHelper = require('../helpers/custom-fields-helper'); 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(); let historyHelper = detailHelper.history();
//Adding a comment //Adding a comment
historyHelper.selectCommentsTab(); historyHelper.selectCommentsTab();
await utils.common.takeScreenshot(screenshotsFolder, "show comments tab");
let commentsCounter = await historyHelper.countComments(); let commentsCounter = await historyHelper.countComments();
let date = Date.now(); 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); 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 //Deleting last comment
let deletedCommentsCounter = await historyHelper.countDeletedComments(); let deletedCommentsCounter = await historyHelper.countDeletedComments();
await historyHelper.deleteLastComment(); await historyHelper.deleteLastComment();
let newDeletedCommentsCounter = await historyHelper.countDeletedComments(); let newDeletedCommentsCounter = await historyHelper.countDeletedComments();
expect(newDeletedCommentsCounter).to.be.equal(deletedCommentsCounter+1); expect(newDeletedCommentsCounter).to.be.equal(deletedCommentsCounter+1);
await utils.common.takeScreenshot(screenshotsFolder, "deleted comment");
//Restore last comment //Restore last comment
deletedCommentsCounter = await historyHelper.countDeletedComments(); deletedCommentsCounter = await historyHelper.countDeletedComments();
await historyHelper.restoreLastComment(); await historyHelper.restoreLastComment();
newDeletedCommentsCounter = await historyHelper.countDeletedComments(); newDeletedCommentsCounter = await historyHelper.countDeletedComments();
expect(newDeletedCommentsCounter).to.be.equal(deletedCommentsCounter-1); expect(newDeletedCommentsCounter).to.be.equal(deletedCommentsCounter-1);
await utils.common.takeScreenshot(screenshotsFolder, "restored comment");
//Store comment with a modification //Store comment with a modification
commentsCounter = await historyHelper.countComments(); commentsCounter = await historyHelper.countComments();
@ -221,18 +242,19 @@ shared.historyTesting = async function() {
historyHelper.writeComment("New comment " + date); historyHelper.writeComment("New comment " + date);
let title = detailHelper.title(); let title = detailHelper.title();
title.setTitle('changed'); title.setTitle('changed');
title.save(); await title.save();
newCommentsCounter = await historyHelper.countComments(); newCommentsCounter = await historyHelper.countComments();
expect(newCommentsCounter).to.be.equal(commentsCounter+1); expect(newCommentsCounter).to.be.equal(commentsCounter+1);
//Check activity //Check activity
await historyHelper.selectActivityTab(); await historyHelper.selectActivityTab();
await utils.common.takeScreenshot(screenshotsFolder, "show activity tab");
let activitiesCounter = await historyHelper.countActivities(); let activitiesCounter = await historyHelper.countActivities();
expect(activitiesCounter).to.be.least(newCommentsCounter); expect(newCommentsCounter).to.be.least(activitiesCounter);
} }
shared.blockTesting = async function() { shared.blockTesting = async function() {

View File

@ -37,7 +37,7 @@ describe('Issue detail', async function(){
describe('watchers edition', sharedDetail.watchersTesting); describe('watchers edition', sharedDetail.watchersTesting);
it('history', sharedDetail.historyTesting); it('history', sharedDetail.historyTesting.bind(this, "issues"));
it('block', sharedDetail.blockTesting); it('block', sharedDetail.blockTesting);
@ -57,4 +57,5 @@ describe('Issue detail', async function(){
expect(url).not.to.be.equal(issueUrl); expect(url).not.to.be.equal(issueUrl);
}); });
}); });
}); });

View File

@ -53,7 +53,7 @@ describe('Task detail', function(){
expect(newIsIocaine).to.be.equal(isIocaine); expect(newIsIocaine).to.be.equal(isIocaine);
}); });
it('history', sharedDetail.historyTesting); it('history', sharedDetail.historyTesting.bind(this, "tasks"));
it('block', sharedDetail.blockTesting); it('block', sharedDetail.blockTesting);

View File

@ -68,7 +68,7 @@ describe('User story detail', function(){
describe('watchers edition', sharedDetail.watchersTesting); describe('watchers edition', sharedDetail.watchersTesting);
it('history', sharedDetail.historyTesting); it('history', sharedDetail.historyTesting.bind(this, "user-stories"));
it('block', sharedDetail.blockTesting); it('block', sharedDetail.blockTesting);