From 5b173463edb6986775f1434e8143099a7d89c8e7 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 8 Sep 2014 10:20:01 +0200 Subject: [PATCH] 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. --- app/coffee/modules/common/history.coffee | 385 +++++++++++++++++++++++ app/coffee/modules/resources.coffee | 2 +- 2 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 app/coffee/modules/common/history.coffee diff --git a/app/coffee/modules/common/history.coffee b/app/coffee/modules/common/history.coffee new file mode 100644 index 00000000..534239a9 --- /dev/null +++ b/app/coffee/modules/common/history.coffee @@ -0,0 +1,385 @@ +### +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino Garcia +# Copyright (C) 2014 David Barragán Merino +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: modules/common/history.coffee +### + +taiga = @.taiga +trim = @.taiga.trim +bindOnce = @.taiga.bindOnce + +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(""" +
+
+ <%- name %> +
+
+

+ <%= diff %> +

+
+
+ """) + + templateChangePoints = _.template(""" + <% _.each(points, function(point, name) { %> +
+
+ points (<%- name.toLowerCase() %>) +
+
+

+ from
+ <%= point[0] %> +

+

+ to
+ <%= point[1] %> +

+
+
+ <% }); %> + """) + + templateChangeGeneric = _.template(""" +
+
+ <%- name %> +
+
+

+ from
+ <%= from %> +

+

+ to
+ <%= to %> +

+
+
+ """) + + templateActivity = _.template(""" +
+
+ + <%- userFullName %> + +
+
+
+ + <%- userFullName %> + + + <%- creationDate %> + +
+ + <% if (comment.length > 0) { %> +
+ <%= comment %> +
+ <% } %> + + <% if(changes.length > 0) { %> +
+ <% if (mode != "activity") { %> + + <%- changesText %> + + + <% } %> + + <% _.each(changes, function(change) { %> + <%= change %> + <% }) %> +
+ <% } %> +
+
+ """) + + templateBaseEntries = _.template(""" + <% if (showMore > 0) { %> + + + Show previous entries (<%- showMore %> more) + + <% } %> + <% _.each(entries, function(entry) { %> + <%= entry %> + <% }) %> + """) + + templateBase = _.template(""" +
+ +
+
+
+ + <% if (mode !== "edit") { %> + Comment + <% } %> +
+
+ +
+ """) + + 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]) diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee index d56d499d..a886a157 100644 --- a/app/coffee/modules/resources.coffee +++ b/app/coffee/modules/resources.coffee @@ -82,7 +82,7 @@ urls = { "severities": "/api/v1/severities" # History - "history/userstory": "/api/v1/history/userstory" + "history/us": "/api/v1/history/userstory" "history/issue": "/api/v1/history/issue" "history/task": "/api/v1/history/task" "history/wiki": "/api/v1/history/wiki"