Merge pull request #1020 from taigaio/us/4186-2929/add_and_edit_comment_permission
US #4186 #2929: Add and edit comment permissionstable
commit
4a2ae30d30
|
@ -10,6 +10,8 @@
|
|||
- Attachments image slider
|
||||
- New admin area to edit the tag colors used in your project
|
||||
- Display the current user (me) at first in assignment lightbox (thanks to [@mikaoelitiana](https://github.com/mikaoelitiana))
|
||||
- Add a new permissions to allow add comments instead of use the existent modify permission for this purpose.
|
||||
- Ability to edit comments, view edition history and redesign comments module UI
|
||||
|
||||
### Misc
|
||||
- Lots of small and not so small bugfixes.
|
||||
|
|
|
@ -771,6 +771,7 @@ modules = [
|
|||
"taigaUserTimeline",
|
||||
"taigaExternalApps",
|
||||
"taigaDiscover",
|
||||
"taigaHistory",
|
||||
|
||||
# template cache
|
||||
"templates",
|
||||
|
|
|
@ -367,6 +367,7 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm, $compile) ->
|
|||
{ key: "view_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.VIEW_USER_STORIES"}
|
||||
{ key: "add_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.ADD_USER_STORIES"}
|
||||
{ key: "modify_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.MODIFY_USER_STORIES"}
|
||||
{ key: "comment_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.COMMENT_USER_STORIES"}
|
||||
{ key: "delete_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.DELETE_USER_STORIES"}
|
||||
]
|
||||
categories.push({
|
||||
|
@ -378,6 +379,7 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm, $compile) ->
|
|||
{ key: "view_tasks", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.VIEW_TASKS"}
|
||||
{ key: "add_task", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.ADD_TASKS"}
|
||||
{ key: "modify_task", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.MODIFY_TASKS"}
|
||||
{ key: "comment_task", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.COMMENT_TASKS"}
|
||||
{ key: "delete_task", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.DELETE_TASKS"}
|
||||
]
|
||||
categories.push({
|
||||
|
@ -389,6 +391,7 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm, $compile) ->
|
|||
{ key: "view_issues", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.VIEW_ISSUES"}
|
||||
{ key: "add_issue", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.ADD_ISSUES"}
|
||||
{ key: "modify_issue", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.MODIFY_ISSUES"}
|
||||
{ key: "comment_issue", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.COMMENT_ISSUES"}
|
||||
{ key: "delete_issue", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.DELETE_ISSUES"}
|
||||
]
|
||||
categories.push({
|
||||
|
|
|
@ -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])
|
|
@ -83,7 +83,7 @@ MarkitupDirective = ($rootscope, $rs, $selectedText, $template, $compile, $trans
|
|||
markdownDomNode = element.parents(".markdown")
|
||||
markItUpDomNode = element.parents(".markItUp")
|
||||
|
||||
$rs.mdrender.render($scope.projectId, $model.$modelValue).then (data) ->
|
||||
$rs.mdrender.render($scope.projectId || $scope.vm.projectId, $model.$modelValue).then (data) ->
|
||||
html = previewTemplate({data: data.data})
|
||||
html = $compile(html)($scope)
|
||||
|
||||
|
|
|
@ -31,6 +31,25 @@ resourceProvider = ($repo, $http, $urls) ->
|
|||
service.get = (type, objectId) ->
|
||||
return $repo.queryOneRaw("history/#{type}", objectId)
|
||||
|
||||
service.editComment = (type, objectId, activityId, comment) ->
|
||||
url = $urls.resolve("history/#{type}")
|
||||
url = "#{url}/#{objectId}/edit_comment"
|
||||
params = {
|
||||
id: activityId
|
||||
}
|
||||
commentData = {
|
||||
comment: comment
|
||||
}
|
||||
return $http.post(url, commentData, params).then (data) =>
|
||||
return data.data
|
||||
|
||||
service.getCommentHistory = (type, objectId, activityId) ->
|
||||
url = $urls.resolve("history/#{type}")
|
||||
url = "#{url}/#{objectId}/comment_versions"
|
||||
params = {id: activityId}
|
||||
return $http.get(url, params).then (data) =>
|
||||
return data.data
|
||||
|
||||
service.deleteComment = (type, objectId, activityId) ->
|
||||
url = $urls.resolve("history/#{type}")
|
||||
url = "#{url}/#{objectId}/delete_comment"
|
||||
|
|
|
@ -244,6 +244,7 @@
|
|||
"VIEW_USER_STORIES": "View user stories",
|
||||
"ADD_USER_STORIES": "Add user stories",
|
||||
"MODIFY_USER_STORIES": "Modify user stories",
|
||||
"COMMENT_USER_STORIES": "Comment user stories",
|
||||
"DELETE_USER_STORIES": "Delete user stories"
|
||||
},
|
||||
"TASKS": {
|
||||
|
@ -251,6 +252,7 @@
|
|||
"VIEW_TASKS": "View tasks",
|
||||
"ADD_TASKS": "Add tasks",
|
||||
"MODIFY_TASKS": "Modify tasks",
|
||||
"COMMENT_TASKS": "Comment tasks",
|
||||
"DELETE_TASKS": "Delete tasks"
|
||||
},
|
||||
"ISSUES": {
|
||||
|
@ -258,6 +260,7 @@
|
|||
"VIEW_ISSUES": "View issues",
|
||||
"ADD_ISSUES": "Add issues",
|
||||
"MODIFY_ISSUES": "Modify issues",
|
||||
"COMMENT_ISSUES": "Comment issues",
|
||||
"DELETE_ISSUES": "Delete issues"
|
||||
},
|
||||
"WIKI": {
|
||||
|
@ -1018,28 +1021,47 @@
|
|||
}
|
||||
},
|
||||
"COMMENTS": {
|
||||
"DELETED_INFO": "Comment deleted by {{user}} on {{date}}",
|
||||
"DELETED_INFO": "Comment deleted by {{user}}",
|
||||
"TITLE": "Comments",
|
||||
"COMMENTS_COUNT": "{{comments}} Comments",
|
||||
"ORDER": "Order",
|
||||
"OLDER_FIRST": "Older first",
|
||||
"RECENT_FIRST": "Recent first",
|
||||
"COMMENT": "Comment",
|
||||
"EDIT_COMMENT": "Edit comment",
|
||||
"EDITED_COMMENT": "Edited:",
|
||||
"SHOW_HISTORY": "View historic",
|
||||
"TYPE_NEW_COMMENT": "Type a new comment here",
|
||||
"SHOW_DELETED": "Show deleted comment",
|
||||
"HIDE_DELETED": "Hide deleted comment",
|
||||
"DELETE": "Delete comment",
|
||||
"RESTORE": "Restore comment"
|
||||
"RESTORE": "Restore comment",
|
||||
"HISTORY": {
|
||||
"TITLE": "Activity"
|
||||
}
|
||||
},
|
||||
"ACTIVITY": {
|
||||
"SHOW_ACTIVITY": "Show activity",
|
||||
"DATETIME": "DD MMM YYYY HH:mm",
|
||||
"SHOW_MORE": "+ Show previous entries ({{showMore}} more)",
|
||||
"TITLE": "Activity",
|
||||
"ACTIVITIES_COUNT": "{{activities}} Activities",
|
||||
"REMOVED": "removed",
|
||||
"ADDED": "added",
|
||||
"US_POINTS": "US points ({{name}})",
|
||||
"NEW_ATTACHMENT": "new attachment",
|
||||
"DELETED_ATTACHMENT": "deleted attachment",
|
||||
"UPDATED_ATTACHMENT": "updated attachment {{filename}}",
|
||||
"DELETED_CUSTOM_ATTRIBUTE": "deleted custom attribute",
|
||||
"TAGS_ADDED": "tags added:",
|
||||
"TAGS_REMOVED": "tags removed:",
|
||||
"US_POINTS": "{{role}} points",
|
||||
"NEW_ATTACHMENT": "new attachment:",
|
||||
"DELETED_ATTACHMENT": "deleted attachment:",
|
||||
"UPDATED_ATTACHMENT": "updated attachment ({{filename}}): ",
|
||||
"CREATED_CUSTOM_ATTRIBUTE": "created custom attribute",
|
||||
"UPDATED_CUSTOM_ATTRIBUTE": "updated custom attribute",
|
||||
"SIZE_CHANGE": "Made {size, plural, one{one change} other{# changes}}",
|
||||
"BECAME_DEPRECATED": "became deprecated",
|
||||
"BECAME_UNDEPRECATED": "became undeprecated",
|
||||
"TEAM_REQUIREMENT": "Team Requirement",
|
||||
"CLIENT_REQUIREMENT": "Client Requirement",
|
||||
"BLOCKED": "Blocked",
|
||||
"VALUES": {
|
||||
"YES": "yes",
|
||||
"NO": "no",
|
||||
|
@ -1071,6 +1093,7 @@
|
|||
"TAGS": "tags",
|
||||
"ATTACHMENTS": "attachments",
|
||||
"IS_DEPRECATED": "is deprecated",
|
||||
"IS_NOT_DEPRECATED": "is not deprecated",
|
||||
"ORDER": "order",
|
||||
"BACKLOG_ORDER": "backlog order",
|
||||
"SPRINT_ORDER": "sprint order",
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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"
|
||||
)
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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")
|
|
@ -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)
|
|
@ -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"
|
||||
)
|
|
@ -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)
|
|
@ -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()
|
|
@ -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)
|
|
@ -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"
|
||||
)
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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"
|
||||
)
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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"
|
||||
)
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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"
|
||||
)
|
|
@ -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", [])
|
|
@ -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)
|
|
@ -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')
|
|
@ -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)
|
|
@ -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
|
||||
|
||||
|
|
@ -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]}}
|
|
@ -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]}}
|
|
@ -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]}}
|
|
@ -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}}
|
|
@ -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]}}
|
|
@ -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]"
|
||||
)
|
|
@ -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") ...
|
|
@ -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]}}
|
|
@ -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]}}
|
|
@ -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]}}
|
|
@ -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}}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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]}}
|
|
@ -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)
|
|
@ -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'
|
||||
)
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
taiga = @.taiga
|
||||
|
||||
class ChekcPermissionsService
|
||||
class CheckPermissionsService
|
||||
@.$inject = [
|
||||
"tgProjectService"
|
||||
]
|
||||
|
@ -31,4 +31,4 @@ class ChekcPermissionsService
|
|||
|
||||
return @projectService.project.get('my_permissions').indexOf(permission) != -1
|
||||
|
||||
angular.module("taigaCommon").service("tgCheckPermissionsService", ChekcPermissionsService)
|
||||
angular.module("taigaCommon").service("tgCheckPermissionsService", CheckPermissionsService)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
mixin wysihelp
|
||||
div.wysiwyg-help
|
||||
.wysiwyg-help
|
||||
span.drag-drop-help(ng-if="wiki.id", translate="COMMON.WYSIWYG.ATTACH_FILE_HELP")
|
||||
span.drag-drop-help(ng-if="!wiki.id", translate="COMMON.WYSIWYG.ATTACH_FILE_HELP_SAVE_FIRST")
|
||||
a.help-markdown(
|
||||
|
|
|
@ -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 %>
|
||||
<% }) %>
|
||||
<% } %>
|
|
@ -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 %>
|
||||
<% }) %>
|
|
@ -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
|
|
@ -1,16 +0,0 @@
|
|||
.change-entry
|
||||
.activity-changed
|
||||
span <%- name %>
|
||||
.activity-fromto
|
||||
<% _.each(diff, function(change) { %>
|
||||
p
|
||||
strong <%- change.name %>
|
||||
strong(translate="COMMON.FROM")
|
||||
br
|
||||
span <%- change.from %>
|
||||
p
|
||||
strong <%- change.name %>
|
||||
strong(translate="COMMON.TO")
|
||||
br
|
||||
span <%- change.to %>
|
||||
<% }) %>
|
|
@ -1,6 +0,0 @@
|
|||
.change-entry
|
||||
.activity-changed
|
||||
span <%- name %>
|
||||
.activity-fromto
|
||||
p
|
||||
span <%= diff %>
|
|
@ -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 %>
|
|
@ -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 %>
|
||||
<% } %>
|
|
@ -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] %>
|
||||
<% }); %>
|
|
@ -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")
|
||||
<% } %>
|
|
@ -93,9 +93,11 @@ div.wrapper(
|
|||
edit-permission = "modify_issue"
|
||||
)
|
||||
|
||||
tg-history(
|
||||
ng-model="issue"
|
||||
tg-history-section(
|
||||
ng-if="issue"
|
||||
type="issue"
|
||||
name="issue"
|
||||
id="issue.id"
|
||||
)
|
||||
|
||||
sidebar.menu-secondary.sidebar.ticket-data
|
||||
|
|
|
@ -95,7 +95,12 @@ div.wrapper(
|
|||
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
|
||||
|
||||
|
|
|
@ -90,9 +90,12 @@ div.wrapper(
|
|||
edit-permission = "modify_us"
|
||||
)
|
||||
|
||||
tg-history(
|
||||
ng-model="us"
|
||||
tg-history-section(
|
||||
ng-if="us"
|
||||
type="us"
|
||||
name="us"
|
||||
id="us.id"
|
||||
project-id="projectId"
|
||||
)
|
||||
|
||||
sidebar.menu-secondary.sidebar.ticket-data
|
||||
|
|
|
@ -40,6 +40,10 @@
|
|||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
padding-left: 2em;
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,8 +16,9 @@ helper.title = function() {
|
|||
el.$('.edit-subject input').clear().sendKeys(title);
|
||||
},
|
||||
|
||||
save: function() {
|
||||
save: async function() {
|
||||
el.$('.save').click();
|
||||
await browser.waitForAngular();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -144,17 +145,37 @@ helper.assignedTo = function() {
|
|||
return obj;
|
||||
};
|
||||
|
||||
helper.editComment = function() {
|
||||
let el = $('.comment-editor');
|
||||
let obj = {
|
||||
el:el,
|
||||
|
||||
updateText: function (text) {
|
||||
el.$('textarea').sendKeys(text);
|
||||
},
|
||||
|
||||
saveComment: async function () {
|
||||
el.$('.save-comment').click()
|
||||
await browser.waitForAngular();
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
|
||||
};
|
||||
|
||||
helper.history = function() {
|
||||
let el = $('section.history');
|
||||
let obj = {
|
||||
el:el,
|
||||
|
||||
selectCommentsTab: function() {
|
||||
el.$$('.history-tabs li a').first().click();
|
||||
selectCommentsTab: async function() {
|
||||
el.$('.e2e-comments-tab').click();
|
||||
await browser.waitForAngular();
|
||||
},
|
||||
|
||||
selectActivityTab: function() {
|
||||
el.$$('.history-tabs li a').last().click();
|
||||
selectActivityTab: async function() {
|
||||
el.$('.e2e-activity-tab').click();
|
||||
await browser.waitForAngular();
|
||||
},
|
||||
|
||||
addComment: async function(comment) {
|
||||
|
@ -168,46 +189,65 @@ helper.history = function() {
|
|||
},
|
||||
|
||||
countComments: async function() {
|
||||
let moreComments = el.$('.comments-list .show-more-comments');
|
||||
let moreCommentsIsPresent = await moreComments.isPresent();
|
||||
if (moreCommentsIsPresent){
|
||||
moreComments.click();
|
||||
}
|
||||
await browser.waitForAngular();
|
||||
let comments = await el.$$(".activity-single.comment");
|
||||
let comments = await el.$$(".comment-wrapper");
|
||||
return comments.length;
|
||||
},
|
||||
|
||||
countActivities: async function() {
|
||||
let moreActivities = el.$('.changes-list .show-more-comments');
|
||||
let selectActivityTabIsPresent = await moreActivities.isPresent();
|
||||
if (selectActivityTabIsPresent){
|
||||
utils.common.link(moreActivities);
|
||||
// moreActivities.click();
|
||||
}
|
||||
await browser.waitForAngular();
|
||||
let activities = await el.$$(".activity-single.activity");
|
||||
let activities = await el.$$(".activity");
|
||||
return activities.length;
|
||||
},
|
||||
|
||||
countDeletedComments: async function() {
|
||||
let moreComments = el.$('.comments-list .show-more-comments');
|
||||
let moreCommentsIsPresent = await moreComments.isPresent();
|
||||
if (moreCommentsIsPresent){
|
||||
moreComments.click();
|
||||
}
|
||||
await browser.waitForAngular();
|
||||
let comments = await el.$$(".activity-single.comment.deleted-comment");
|
||||
let comments = await el.$$(".deleted-comment-wrapper");
|
||||
return comments.length;
|
||||
},
|
||||
|
||||
editLastComment: async function() {
|
||||
let lastComment = el.$$(".comment-wrapper").last()
|
||||
browser
|
||||
.actions()
|
||||
.mouseMove(lastComment)
|
||||
.perform();
|
||||
|
||||
lastComment.$$(".comment-option").first().click();
|
||||
await browser.waitForAngular();
|
||||
},
|
||||
|
||||
deleteLastComment: async function() {
|
||||
el.$$(".activity-single.comment .comment-delete").last().click();
|
||||
let lastComment = el.$$(".comment-wrapper").last()
|
||||
browser
|
||||
.actions()
|
||||
.mouseMove(lastComment)
|
||||
.perform();
|
||||
|
||||
lastComment.$$(".comment-option").last().click();
|
||||
await browser.waitForAngular();
|
||||
},
|
||||
|
||||
showVersionsLastComment: async function() {
|
||||
el.$$(".comment-edited a").last().click();
|
||||
await browser.waitForAngular();
|
||||
},
|
||||
|
||||
closeVersionsLastComment: async function() {
|
||||
$(".lightbox-display-historic .close").click();
|
||||
await browser.waitForAngular();
|
||||
},
|
||||
|
||||
enableEditModeLastComment: async function() {
|
||||
let lastComment = el.$$(".comment-wrapper").last()
|
||||
browser
|
||||
.actions()
|
||||
.mouseMove(lastComment)
|
||||
.perform();
|
||||
|
||||
lastComment.$$(".comment-option").last().click();
|
||||
await browser.waitForAngular();
|
||||
},
|
||||
|
||||
restoreLastComment: async function() {
|
||||
el.$$(".activity-single.comment.deleted-comment .comment-restore").last().click();
|
||||
el.$$(".deleted-comment-wrapper .restore-comment").last().click();
|
||||
await browser.waitForAngular();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
var path = require('path');
|
||||
var utils = require('../utils');
|
||||
var detailHelper = require('../helpers').detail;
|
||||
var commonHelper = require('../helpers').common;
|
||||
var customFieldsHelper = require('../helpers/custom-fields-helper');
|
||||
|
@ -191,29 +192,49 @@ shared.assignedToTesting = function() {
|
|||
});
|
||||
}
|
||||
|
||||
shared.historyTesting = async function() {
|
||||
shared.historyTesting = async function(screenshotsFolder) {
|
||||
let historyHelper = detailHelper.history();
|
||||
|
||||
|
||||
//Adding a comment
|
||||
historyHelper.selectCommentsTab();
|
||||
await utils.common.takeScreenshot(screenshotsFolder, "show comments tab");
|
||||
|
||||
let commentsCounter = await historyHelper.countComments();
|
||||
let date = Date.now();
|
||||
await historyHelper.addComment("New comment " + date);
|
||||
let newCommentsCounter = await historyHelper.countComments();
|
||||
|
||||
await historyHelper.addComment("New comment " + date);
|
||||
await utils.common.takeScreenshot(screenshotsFolder, "new coment");
|
||||
|
||||
let newCommentsCounter = await historyHelper.countComments();
|
||||
expect(newCommentsCounter).to.be.equal(commentsCounter+1);
|
||||
|
||||
//Edit last comment
|
||||
historyHelper.editLastComment();
|
||||
let editComment = detailHelper.editComment();
|
||||
editComment.updateText("This is the new and updated text");
|
||||
editComment.saveComment();
|
||||
await utils.common.takeScreenshot(screenshotsFolder, "edit comment");
|
||||
|
||||
//Show versions from last comment edited
|
||||
historyHelper.showVersionsLastComment();
|
||||
await utils.common.takeScreenshot(screenshotsFolder, "show comment versions");
|
||||
|
||||
historyHelper.closeVersionsLastComment();
|
||||
|
||||
//Deleting last comment
|
||||
let deletedCommentsCounter = await historyHelper.countDeletedComments();
|
||||
await historyHelper.deleteLastComment();
|
||||
let newDeletedCommentsCounter = await historyHelper.countDeletedComments();
|
||||
expect(newDeletedCommentsCounter).to.be.equal(deletedCommentsCounter+1);
|
||||
await utils.common.takeScreenshot(screenshotsFolder, "deleted comment");
|
||||
|
||||
//Restore last comment
|
||||
deletedCommentsCounter = await historyHelper.countDeletedComments();
|
||||
await historyHelper.restoreLastComment();
|
||||
newDeletedCommentsCounter = await historyHelper.countDeletedComments();
|
||||
expect(newDeletedCommentsCounter).to.be.equal(deletedCommentsCounter-1);
|
||||
await utils.common.takeScreenshot(screenshotsFolder, "restored comment");
|
||||
|
||||
//Store comment with a modification
|
||||
commentsCounter = await historyHelper.countComments();
|
||||
|
@ -221,18 +242,19 @@ shared.historyTesting = async function() {
|
|||
historyHelper.writeComment("New comment " + date);
|
||||
let title = detailHelper.title();
|
||||
title.setTitle('changed');
|
||||
title.save();
|
||||
|
||||
await title.save();
|
||||
newCommentsCounter = await historyHelper.countComments();
|
||||
|
||||
expect(newCommentsCounter).to.be.equal(commentsCounter+1);
|
||||
|
||||
//Check activity
|
||||
await historyHelper.selectActivityTab();
|
||||
await utils.common.takeScreenshot(screenshotsFolder, "show activity tab");
|
||||
|
||||
let activitiesCounter = await historyHelper.countActivities();
|
||||
|
||||
expect(activitiesCounter).to.be.least(newCommentsCounter);
|
||||
expect(newCommentsCounter).to.be.least(activitiesCounter);
|
||||
|
||||
}
|
||||
|
||||
shared.blockTesting = async function() {
|
||||
|
|
|
@ -37,7 +37,7 @@ describe('Issue detail', async function(){
|
|||
|
||||
describe('watchers edition', sharedDetail.watchersTesting);
|
||||
|
||||
it('history', sharedDetail.historyTesting);
|
||||
it('history', sharedDetail.historyTesting.bind(this, "issues"));
|
||||
|
||||
it('block', sharedDetail.blockTesting);
|
||||
|
||||
|
@ -57,4 +57,5 @@ describe('Issue detail', async function(){
|
|||
expect(url).not.to.be.equal(issueUrl);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -53,7 +53,7 @@ describe('Task detail', function(){
|
|||
expect(newIsIocaine).to.be.equal(isIocaine);
|
||||
});
|
||||
|
||||
it('history', sharedDetail.historyTesting);
|
||||
it('history', sharedDetail.historyTesting.bind(this, "tasks"));
|
||||
|
||||
it('block', sharedDetail.blockTesting);
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ describe('User story detail', function(){
|
|||
|
||||
describe('watchers edition', sharedDetail.watchersTesting);
|
||||
|
||||
it('history', sharedDetail.historyTesting);
|
||||
it('history', sharedDetail.historyTesting.bind(this, "user-stories"));
|
||||
|
||||
it('block', sharedDetail.blockTesting);
|
||||
|
||||
|
|
Loading…
Reference in New Issue