taiga-front/app/coffee/modules/common.coffee

470 lines
16 KiB
CoffeeScript

###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com>
#
# 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.coffee
###
taiga = @.taiga
trim = @.taiga.trim
typeIsArray = @.taiga.typeIsArray
module = angular.module("taigaCommon", [])
#############################################################################
## TagLine (possible should be moved as generic directive)
#############################################################################
TagLineDirective = ($log) ->
# Main directive template (rendered by angular)
template = """
<div class="tags-container"></div>
<input type="text" placeholder="Write tag..." class="hidden"/>
"""
# Tags template (rendered manually using lodash)
templateTags = _.template("""
<% _.each(tags, function(tag) { %>
<div class="tag" style="border-left: 5px solid <%- tag.color %>;">
<span class="tag-name"><%- tag.name %></span>
<% if (editable) { %>
<a href="" title="delete tag" class="icon icon-delete"></a>
<% } %>
</div>
<% }); %>""")
renderTags = ($el, tags, editable, tagsColors) ->
ctx = {
tags: _.map(tags, (t) -> {name: t, color: tagsColors[t]})
editable: editable
}
html = templateTags(ctx)
$el.find("div.tags-container").html(html)
normalizeTags = (tags) ->
tags = _.map(tags, trim)
tags = _.map(tags, (x) -> x.toLowerCase())
return _.uniq(tags)
link = ($scope, $el, $attrs, $model) ->
editable = if $attrs.editable == "true" then true else false
$scope.$watch $attrs.ngModel, (val) ->
return if not val
renderTags($el, val, editable, $scope.project.tags_colors)
$el.find("input").remove() if not editable
$el.on "keyup", "input", (event) ->
return if event.keyCode != 13
target = angular.element(event.currentTarget)
value = trim(target.val())
if value.length <= 0
return
tags = _.clone($model.$modelValue, false)
tags = [] if not tags?
tags.push(value)
target.val("")
$scope.$apply ->
$model.$setViewValue(normalizeTags(tags))
$el.on "click", ".icon-delete", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
value = trim(target.siblings(".tag-name").text())
if value.length <= 0
return
tags = _.clone($model.$modelValue, false)
tags = _.pull(tags, value)
$scope.$apply ->
$model.$setViewValue(normalizeTags(tags))
return {
link:link,
require:"ngModel"
template: template
}
module.directive("tgTagLine", ["$log", TagLineDirective])
#############################################################################
## Change (comment and history mode) directive
#############################################################################
ChangeDirective = ->
# TODO: i18n
commentBaseTemplate = _.template("""
<div class="comment-user activity-comment">
<a class="avatar" href="" title="<%- userFullName %>">
<img src="<%- avatar %>" alt="<%- userFullName %>">
</a>
</div>
<div class="comment-content">
<a class="username" href="" title="<%- userFullName %>">
<%- userFullName %>
</a>
<% if(hasChanges){ %>
<div class="us-activity">
<a class="activity-title" href="" title="Show activity">
<span>
<%- changesText %>
</span>
<span class="icon icon-arrow-up">
</span>
</a>
</div>
<% } %>
<div class="comment wysiwyg">
<%= comment %>
</div>
<div class="date">
<%- creationDate %>
</div>
</div>
""")
changeBaseTemplate = _.template("""
<div class="activity-user">
<a class="avatar" href="" title="<%- userFullName %>">
<img src="<%- avatar %>" alt="<%- userFullName %>">
</a>
</div>
<div class="activity-content">
<div class="activity-username">
<a class="username" href="" title="<%- userFullName %>">
<%- userFullName %>
</a>
<span class="date">
<%- creationDate %>
</span>
</div>
<div class="comment wysiwyg">
<%= comment %>
</div>
</div>
""")
standardChangeFromToTemplate = _.template("""
<div class="activity-inner">
<div class="activity-changed">
<span><%- name %></span>
</div>
<div class="activity-fromto">
<p>
<strong> from </strong> <br />
<span><%= from %></span>
</p>
<p>
<strong> to </strong> <br />
<span><%= to %></span>
</p>
</div>
</div>
""")
descriptionChangeTemplate = _.template("""
<div class="activity-inner">
<div class="activity-changed">
<span><%- name %></span>
</div>
<div class="activity-fromto">
<p>
<span><%= diff %></span>
</p>
</div>
</div>
""")
pointsChangeTemplate = _.template("""
<% _.each(points, function(point, name) { %>
<div class="activity-inner">
<div class="activity-changed">
<span><%- name %> points</span>
</div>
<div class="activity-fromto">
<p>
<strong> from </strong> <br />
<span><%= point[0] %></span>
</p>
<p>
<strong> to </strong> <br />
<span><%= point[1] %></span>
</p>
</div>
</div>
<% }); %>
""")
attachmentTemplate = _.template("""
<div class="activity-inner">
<div class="activity-changed">
<span><%- name %></span>
</div>
<div class="activity-fromto">
<%- description %>
</div>
</div>
""")
link = ($scope, $el, $attrs, $model) ->
countChanges = (comment) ->
return _.keys(comment.values_diff).length
buildChangesText = (comment) ->
size = countChanges(comment)
# TODO: i18n
if size == 1
return "Made #{size} change"
return "Made #{size} changes"
renderEntries = (change, parentDomNode) ->
_.each change.values_diff, (modification, name) ->
if name == "description"
parentDomNode.append(descriptionChangeTemplate({
name: name
diff: modification[1]
}))
else if name == "points"
parentDomNode.append(pointsChangeTemplate({
points: modification
}))
else if name == "attachments"
_.each modification, (attachmentChanges, attachmentType) ->
if attachmentType == "new"
_.each attachmentChanges, (attachmentChange) ->
parentDomNode.append(attachmentTemplate({
name: "New attachment"
description: attachmentChange.filename
}))
else if attachmentType == "deleted"
_.each attachmentChanges, (attachmentChange) ->
parentDomNode.append(attachmentTemplate({
name: "Deleted attachment"
description: attachmentChange.filename
}))
else
name = "Updated attachment"
_.each attachmentChanges, (attachmentChange) ->
parentDomNode.append(attachmentTemplate({
name: "Updated attachment"
description: attachmentChange[0].filename
}))
else
parentDomNode.append(standardChangeFromToTemplate({
name: name
from: prettyPrintModification(modification[0])
to: prettyPrintModification(modification[1])
}))
renderComment = (comment) ->
html = commentBaseTemplate({
avatar: getUserAvatar(comment.user.pk)
userFullName: getUserFullName(comment.user.pk)
creationDate: moment(comment.created_at).format("DD MMM YYYY HH:mm")
comment: taiga.nl2br(comment.comment_html)
changesText: buildChangesText(comment)
hasChanges: countChanges(comment) > 0
})
$el.html(html)
activityContentDom = $el.find(".comment-content .us-activity")
renderEntries(comment, activityContentDom)
renderChange = (change) ->
html = changeBaseTemplate({
avatar: getUserAvatar(change.user.pk)
userFullName: getUserFullName(change.user.pk)
creationDate: moment(change.created_at).format("DD MMM YYYY HH:mm")
comment: change.comment_html
})
$el.html(html)
activityContentDom = $el.find(".activity-content")
renderEntries(change, activityContentDom)
getUserFullName = (userId) ->
return $scope.usersById[userId]?.full_name_display
getUserAvatar = (userId) ->
return $scope.usersById[userId]?.photo
prettyPrintModification = (value) ->
if typeIsArray(value)
if value.length == 0
#TODO i18n
return "None"
return value.join(", ")
if value == ""
#TODO i18n
return "None"
return value
$scope.$watch $attrs.ngModel, (change) ->
if not change?
return
if $attrs.mode == "comment"
renderComment(change)
else
renderChange(change)
$el.on "click", ".activity-title", (event) ->
event.preventDefault()
$el.find(".activity-inner").toggleClass("active")
$scope.$on "$destroy", ->
$el.off()
return {link:link, require:"ngModel"}
module.directive("tgChange", ChangeDirective)
#############################################################################
## Permission directive, hide elements when necessary
#############################################################################
CheckPermissionDirective = ->
showElementIfPermission = (element, permission, project) ->
element.show() if project.my_permissions.indexOf(permission) > -1
link = ($scope, $el, $attrs) ->
$el.hide()
permission = $attrs.permission
#Sometimes this directive from a self included html template
if $scope.project?
showElementIfPermission($el, permission, $scope.project)
$scope.$on "project:loaded", (ctx, project) ->
showElementIfPermission($el, permission, project)
$scope.$on "$destroy", ->
$el.off()
return {link:link}
module.directive("tgCheckPermission", CheckPermissionDirective)
#############################################################################
## Animation frame service, apply css changes in the next render frame
#############################################################################
AnimationFrame = () ->
animationFrame =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame
performAnimation = (time) =>
fn = tail.shift()
fn()
if (tail.length)
animationFrame(performAnimation)
tail = []
add = () ->
for fn in arguments
tail.push(fn)
if tail.length == 1
animationFrame(performAnimation)
return {add: add}
module.factory("animationFrame", AnimationFrame)
#############################################################################
## Open/close comment
#############################################################################
ToggleCommentDirective = () ->
link = ($scope, $el, $attrs) ->
$el.find("textarea").on "focus", () ->
$el.addClass("active")
return {link:link}
module.directive("tgToggleComment", ToggleCommentDirective)
#############################################################################
## Set the page title
#############################################################################
AppTitle = () ->
set = (text) ->
$("title").text(text)
return {set: set}
module.factory("$appTitle", AppTitle)
#############################################################################
## Get the appropiate section url for a project
## according to his enabled features and user permisions
#############################################################################
ProjectUrl = ($navurls) ->
get = (project) ->
if project.is_backlog_activated and project.my_permissions.indexOf("view_us")>-1
url = $navurls.resolve("project-backlog")
else if project.is_kanban_activated and project.my_permissions.indexOf("view_us")>-1
url = $navurls.resolve("project-kanban")
else if project.is_wiki_activated and project.my_permissions.indexOf("view_wiki_pages")>-1
url = $navurls.resolve("project-wiki")
else if project.is_issues_activated and project.my_permissions.indexOf("view_issues")>-1
url = $navurls.resolve("project-issues")
else
url = $navurls.resolve("project")
return $navurls.formatUrl(url, {'project': project.slug})
return {get: get}
module.factory("$projectUrl", ["$tgNavUrls", ProjectUrl])
#############################################################################
## Limite line size in a text area
#############################################################################
LimitLineLengthDirective = () ->
link = ($scope, $el, $attrs) ->
maxColsPerLine = parseInt($el.attr("cols"))
$el.on "keyup", (event) ->
code = event.keyCode
lines = $el.val().split("\n")
_.each lines, (line, index) ->
lines[index] = line.substring(0, maxColsPerLine - 2)
$el.val(lines.join("\n"))
return {link:link}
module.directive("tgLimitLineLength", LimitLineLengthDirective)