Add tgHistory directive.
This is a complete rewrite of the old tgChange directive that has a lot of functions that works only with dom mutability and with unclear name. This new version works in majority of time with immutable operations and only mutates the dom in the last render step.stable
parent
b843916233
commit
5b173463ed
|
@ -0,0 +1,385 @@
|
||||||
|
###
|
||||||
|
# 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/history.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
taiga = @.taiga
|
||||||
|
trim = @.taiga.trim
|
||||||
|
bindOnce = @.taiga.bindOnce
|
||||||
|
|
||||||
|
module = angular.module("taigaCommon")
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
## History Directive (Main)
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
class HistoryController extends taiga.Controller
|
||||||
|
@.$inject = ["$scope", "$tgRepo"]
|
||||||
|
|
||||||
|
constructor: (@scope, @repo) ->
|
||||||
|
|
||||||
|
# TODO: possible move to resources
|
||||||
|
getHistory: (type, objectId) ->
|
||||||
|
return @repo.queryOneRaw("history/#{type}", objectId)
|
||||||
|
|
||||||
|
loadHistory: (type, objectId) ->
|
||||||
|
return @.getHistory(type, objectId).then (history) =>
|
||||||
|
for historyResult in history
|
||||||
|
# If description was modified take only the description_html field
|
||||||
|
if historyResult.values_diff.description?
|
||||||
|
historyResult.values_diff.description = historyResult.values_diff.description_diff
|
||||||
|
|
||||||
|
delete historyResult.values_diff.description_html
|
||||||
|
delete historyResult.values_diff.description_diff
|
||||||
|
|
||||||
|
@scope.history = history
|
||||||
|
@scope.comments = _.filter(history, (item) -> item.comment != "")
|
||||||
|
|
||||||
|
|
||||||
|
HistoryDirective = ($log) ->
|
||||||
|
templateChangeDiff = _.template("""
|
||||||
|
<div class="change-entry">
|
||||||
|
<div class="activity-changed">
|
||||||
|
<span><%- name %></span>
|
||||||
|
</div>
|
||||||
|
<div class="activity-fromto">
|
||||||
|
<p>
|
||||||
|
<span><%= diff %></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""")
|
||||||
|
|
||||||
|
templateChangePoints = _.template("""
|
||||||
|
<% _.each(points, function(point, name) { %>
|
||||||
|
<div class="change-entry">
|
||||||
|
<div class="activity-changed">
|
||||||
|
<span>points (<%- name.toLowerCase() %>)</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>
|
||||||
|
<% }); %>
|
||||||
|
""")
|
||||||
|
|
||||||
|
templateChangeGeneric = _.template("""
|
||||||
|
<div class="change-entry">
|
||||||
|
<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>
|
||||||
|
""")
|
||||||
|
|
||||||
|
templateActivity = _.template("""
|
||||||
|
<div class="activity-single <%- mode %>">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<% if (comment.length > 0) { %>
|
||||||
|
<div class="comment wysiwyg">
|
||||||
|
<%= comment %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if(changes.length > 0) { %>
|
||||||
|
<div class="changes">
|
||||||
|
<% if (mode != "activity") { %>
|
||||||
|
<a class="changes-title" href="" title="Show activity">
|
||||||
|
<span><%- changesText %></span>
|
||||||
|
<span class="icon icon-arrow-up"></span>
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% _.each(changes, function(change) { %>
|
||||||
|
<%= change %>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""")
|
||||||
|
|
||||||
|
templateBaseEntries = _.template("""
|
||||||
|
<% if (showMore > 0) { %>
|
||||||
|
<a href="" title="Show more" class="show-more show-more-comments">
|
||||||
|
+ Show previous entries (<%- showMore %> more)
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
<% _.each(entries, function(entry) { %>
|
||||||
|
<%= entry %>
|
||||||
|
<% }) %>
|
||||||
|
""")
|
||||||
|
|
||||||
|
templateBase = _.template("""
|
||||||
|
<section class="history">
|
||||||
|
<ul class="history-tabs">
|
||||||
|
<li>
|
||||||
|
<a href="#" class="active">
|
||||||
|
<span class="icon icon-comment"></span>
|
||||||
|
<span class="tab-title">Comments</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">
|
||||||
|
<span class="icon icon-issues"></span>
|
||||||
|
<span class="tab-title">Activity</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<section class="history-comments">
|
||||||
|
<div class="comments-list"></div>
|
||||||
|
<div tg-check-permission tg-toggle-comment permission="modify_<%- type %>" class="add-comment">
|
||||||
|
<textarea placeholder="Write here a new commet"
|
||||||
|
ng-model="<%- ngmodel %>.comment" tg-markitup="tg-markitup">
|
||||||
|
</textarea>
|
||||||
|
<% if (mode !== "edit") { %>
|
||||||
|
<a href="" title="Comment" class="button button-green save-comment">Comment</a>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="history-activity hidden">
|
||||||
|
<div class="changes-list"></div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
""")
|
||||||
|
|
||||||
|
link = ($scope, $el, $attrs, $ctrl) ->
|
||||||
|
# Bootstraping
|
||||||
|
type = $attrs.type
|
||||||
|
objectId = null
|
||||||
|
|
||||||
|
showAllComments = false
|
||||||
|
showAllActivity = false
|
||||||
|
|
||||||
|
bindOnce $scope, $attrs.ngModel, (model) ->
|
||||||
|
type = $attrs.type
|
||||||
|
objectId = model.id
|
||||||
|
$ctrl.loadHistory(type, objectId)
|
||||||
|
|
||||||
|
# Helpers
|
||||||
|
|
||||||
|
getUserFullName = (userId) ->
|
||||||
|
return $scope.usersById[userId]?.full_name_display
|
||||||
|
|
||||||
|
getUserAvatar = (userId) ->
|
||||||
|
return $scope.usersById[userId]?.photo
|
||||||
|
|
||||||
|
countChanges = (comment) ->
|
||||||
|
return _.keys(comment.values_diff).length
|
||||||
|
|
||||||
|
formatChange = (change) ->
|
||||||
|
if _.isArray(change)
|
||||||
|
if change.length == 0
|
||||||
|
return "nil"
|
||||||
|
return change.join(", ")
|
||||||
|
|
||||||
|
if change == ""
|
||||||
|
return "nil"
|
||||||
|
|
||||||
|
return change
|
||||||
|
|
||||||
|
# Render into string (operations without mutability)
|
||||||
|
|
||||||
|
renderAttachmentEntry = (field, value) ->
|
||||||
|
attachments = _.map value, (changes, type) ->
|
||||||
|
if type == "new"
|
||||||
|
return _.map changes, (change) ->
|
||||||
|
return templateChangeDiff({name: "New attachment", diff: change.filename})
|
||||||
|
else if type == "deleted"
|
||||||
|
return _.map changes, (change) ->
|
||||||
|
return templateChangeDiff({name: "Deleted attachment", diff: change.filename})
|
||||||
|
else
|
||||||
|
return _.map changes, (change) ->
|
||||||
|
return templateChangeDiff({name: "Updated attachment", diff: change[0].filename})
|
||||||
|
|
||||||
|
return _.flatten(attachments).join("\n")
|
||||||
|
|
||||||
|
renderChangeEntry = (field, value) ->
|
||||||
|
if field == "description"
|
||||||
|
return templateChangeDiff({name: field, diff: value[1]})
|
||||||
|
else if field == "points"
|
||||||
|
return templateChangePoints({points: value})
|
||||||
|
else if field == "attachments"
|
||||||
|
return renderAttachmentEntry(field, value)
|
||||||
|
else if field == "assigned_to"
|
||||||
|
from = formatChange(value[0] or "Unassigned")
|
||||||
|
to = formatChange(value[1] or "Unassigned")
|
||||||
|
return templateChangeGeneric({name:field, from:from, to: to})
|
||||||
|
else
|
||||||
|
from = formatChange(value[0])
|
||||||
|
to = formatChange(value[1])
|
||||||
|
return templateChangeGeneric({name:field, from:from, to: to})
|
||||||
|
|
||||||
|
renderChangeEntries = (change, join=true) ->
|
||||||
|
entries = _.map(change.values_diff, (value, field) -> renderChangeEntry(field, value))
|
||||||
|
if join
|
||||||
|
return entries.join("\n")
|
||||||
|
return entries
|
||||||
|
|
||||||
|
renderChangesHelperText = (change) ->
|
||||||
|
size = countChanges(change)
|
||||||
|
if size == 1
|
||||||
|
return "Made #{size} change" # TODO: i18n
|
||||||
|
return "Made #{size} changes" # TODO: i18n
|
||||||
|
|
||||||
|
renderComment = (comment) ->
|
||||||
|
return templateActivity({
|
||||||
|
avatar: getUserAvatar(comment.user.pk)
|
||||||
|
userFullName: getUserFullName(comment.user.pk)
|
||||||
|
creationDate: moment(comment.created_at).format("DD MMM YYYY HH:mm")
|
||||||
|
comment: comment.comment_html
|
||||||
|
changesText: renderChangesHelperText(comment)
|
||||||
|
changes: renderChangeEntries(comment, false)
|
||||||
|
mode: "comment"
|
||||||
|
})
|
||||||
|
|
||||||
|
renderChange = (change) ->
|
||||||
|
return templateActivity({
|
||||||
|
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
|
||||||
|
changes: renderChangeEntries(change, false)
|
||||||
|
changesText: ""
|
||||||
|
mode: "activity"
|
||||||
|
})
|
||||||
|
|
||||||
|
renderHistory = (entries, totalEntries) ->
|
||||||
|
if entries.length == totalEntries
|
||||||
|
showMore = 0
|
||||||
|
else
|
||||||
|
showMore = totalEntries - entries.length
|
||||||
|
|
||||||
|
return templateBaseEntries({entries: entries, showMore:showMore})
|
||||||
|
|
||||||
|
# Render into DOM (operations with dom mutability)
|
||||||
|
|
||||||
|
renderComments = ->
|
||||||
|
comments = $scope.comments or []
|
||||||
|
totalComments = comments.length
|
||||||
|
if not showAllComments
|
||||||
|
comments = _.last(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 = _.last(changes, 4)
|
||||||
|
|
||||||
|
changes = _.map(changes, (x) -> renderChange(x))
|
||||||
|
html = renderHistory(changes, totalChanges)
|
||||||
|
$el.find(".changes-list").html(html)
|
||||||
|
|
||||||
|
# Watchers
|
||||||
|
|
||||||
|
$scope.$watch("comments", renderComments)
|
||||||
|
$scope.$watch("history", renderActivity)
|
||||||
|
|
||||||
|
$scope.$on "history:reload", ->
|
||||||
|
renderComments()
|
||||||
|
renderActivity()
|
||||||
|
|
||||||
|
# Events
|
||||||
|
|
||||||
|
$el.on "click", ".add-comment a.button-green", (event) ->
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
$el.find(".comment-list").addClass("activeanimation")
|
||||||
|
onSuccess = ->
|
||||||
|
$ctrl.loadHistory(type, objectId)
|
||||||
|
|
||||||
|
onError = ->
|
||||||
|
$confirm.notify("error")
|
||||||
|
|
||||||
|
model = $scope.$eval($attrs.ngModel)
|
||||||
|
$ctrl.repo.save(model).then(onSuccess, onError)
|
||||||
|
|
||||||
|
$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", ".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 li a", (event) ->
|
||||||
|
$el.find(".history-tabs li a").toggleClass("active")
|
||||||
|
$el.find(".history section").toggleClass("hidden")
|
||||||
|
|
||||||
|
$scope.$on "$destroy", ->
|
||||||
|
$el.off()
|
||||||
|
|
||||||
|
templateFn = ($el, $attrs) ->
|
||||||
|
return templateBase({ngmodel: $attrs.ngModel, type: $attrs.type, mode: $attrs.mode})
|
||||||
|
|
||||||
|
return {
|
||||||
|
controller: HistoryController
|
||||||
|
template: templateFn
|
||||||
|
restrict: "AE"
|
||||||
|
link: link
|
||||||
|
# require: ["ngModel", "tgHistory"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.directive("tgHistory", ["$log", HistoryDirective])
|
|
@ -82,7 +82,7 @@ urls = {
|
||||||
"severities": "/api/v1/severities"
|
"severities": "/api/v1/severities"
|
||||||
|
|
||||||
# History
|
# History
|
||||||
"history/userstory": "/api/v1/history/userstory"
|
"history/us": "/api/v1/history/userstory"
|
||||||
"history/issue": "/api/v1/history/issue"
|
"history/issue": "/api/v1/history/issue"
|
||||||
"history/task": "/api/v1/history/task"
|
"history/task": "/api/v1/history/task"
|
||||||
"history/wiki": "/api/v1/history/wiki"
|
"history/wiki": "/api/v1/history/wiki"
|
||||||
|
|
Loading…
Reference in New Issue