diff --git a/CHANGELOG.md b/CHANGELOG.md index 571ade29..d6758b78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.2.0 (Unreleased) ### Features +- US/Task/Issue visualization and edition refactor. Now only one view for both. - Multiple User stories Drag & Drop in the backlog. - Add visual difference to closed USs in backlog panel. - Show crerated date of attachments in the hover of the filename. diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee index 3d298fe5..8e6fbcce 100644 --- a/app/coffee/app.coffee +++ b/app/coffee/app.coffee @@ -52,30 +52,22 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven # User stories $routeProvider.when("/project/:pslug/us/:usref", {templateUrl: "/partials/us-detail.html", resolve: {loader: tgLoaderProvider.add()}}) - $routeProvider.when("/project/:pslug/us/:usref/edit", - {templateUrl: "/partials/us-detail-edit.html"}) # Tasks $routeProvider.when("/project/:pslug/task/:taskref", {templateUrl: "/partials/task-detail.html", resolve: {loader: tgLoaderProvider.add()}}) - $routeProvider.when("/project/:pslug/task/:taskref/edit", - {templateUrl: "/partials/task-detail-edit.html"}) # Wiki $routeProvider.when("/project/:pslug/wiki", {redirectTo: (params) -> "/project/#{params.pslug}/wiki/home"}, ) $routeProvider.when("/project/:pslug/wiki/:slug", {templateUrl: "/partials/wiki.html", resolve: {loader: tgLoaderProvider.add()}}) - $routeProvider.when("/project/:pslug/wiki/:slug/edit", - {templateUrl: "/partials/wiki-edit.html"}) # Issues $routeProvider.when("/project/:pslug/issues", {templateUrl: "/partials/issues.html", resolve: {loader: tgLoaderProvider.add()}}) $routeProvider.when("/project/:pslug/issue/:issueref", - {templateUrl: "/partials/issues-detail.html"}) - $routeProvider.when("/project/:pslug/issue/:issueref/edit", - {templateUrl: "/partials/issues-detail-edit.html"}) + {templateUrl: "/partials/issues-detail.html", resolve: {loader: tgLoaderProvider.add()}}) # Admin $routeProvider.when("/project/:pslug/admin/project-profile/details", diff --git a/app/coffee/modules/base.coffee b/app/coffee/modules/base.coffee index d3dffe7f..8310d95c 100644 --- a/app/coffee/modules/base.coffee +++ b/app/coffee/modules/base.coffee @@ -67,17 +67,13 @@ urls = { "project-search": "/project/:project/search" "project-userstories-detail": "/project/:project/us/:ref" - "project-userstories-detail-edit": "/project/:project/us/:ref/edit" "project-tasks-detail": "/project/:project/task/:ref" - "project-tasks-detail-edit": "/project/:project/task/:ref/edit" "project-issues-detail": "/project/:project/issue/:ref" - "project-issues-detail-edit": "/project/:project/issue/:ref/edit" "project-wiki": "/project/:project/wiki", "project-wiki-page": "/project/:project/wiki/:slug", - "project-wiki-page-edit": "/project/:project/wiki/:slug/edit", # Admin "project-admin-home": "/project/:project/admin/project-profile/details" diff --git a/app/coffee/modules/common/components.coffee b/app/coffee/modules/common/components.coffee index 30d3a35c..6d973390 100644 --- a/app/coffee/modules/common/components.coffee +++ b/app/coffee/modules/common/components.coffee @@ -24,6 +24,7 @@ bindOnce = @.taiga.bindOnce module = angular.module("taigaCommon") + ############################################################################# ## Date Range Directive (used mainly for sprint date range) ############################################################################# @@ -46,6 +47,33 @@ DateRangeDirective = -> module.directive("tgDateRange", DateRangeDirective) +############################################################################# +## Date Selector Directive (using pikaday) +############################################################################# + +DateSelectorDirective =-> + link = ($scope, $el, $attrs, $model) -> + selectedDate = null + $el.picker = new Pikaday({ + field: $el[0] + format: "DD MMM YYYY" + onSelect: (date) => + selectedDate = date + onOpen: => + $el.picker.setDate(selectedDate) if selectedDate? + }) + + $scope.$watch $attrs.ngModel, (val) -> + $el.picker.setDate(val) if val? + + return { + link: link + require: "ngModel" + } + +module.directive("tgDateSelector", DateSelectorDirective) + + ############################################################################# ## Sprint Progress Bar Directive ############################################################################# @@ -76,92 +104,126 @@ module.directive("tgSprintProgressbar", SprintProgressBarDirective) ############################################################################# -## Date Selector Directive (using pikaday) +## Created-by display directive ############################################################################# -DateSelectorDirective =-> - link = ($scope, $el, $attrs, $model) -> - selectedDate = null - $el.picker = new Pikaday({ - field: $el[0] - format: "DD MMM YYYY" - onSelect: (date) => - selectedDate = date - onOpen: => - $el.picker.setDate(selectedDate) if selectedDate? - }) +CreatedByDisplayDirective = -> + # Display the owner information (full name and photo) and the date of + # creation of an object (like USs, tasks and issues). + # + # Example: + # div.us-created-by(tg-created-by-display, ng-model="us") + # + # Requirements: + # - model object must have the attributes 'created_date' and + # 'owner'(ng-model) + # - scope.usersById object is required. - $scope.$watch $attrs.ngModel, (val) -> - $el.picker.setDate(val) if val? + template = _.template(""" +
+ <%- owner.full_name_display %> +
+ +
+ Created by <%- owner.full_name_display %> + <%- date %> +
+ """) # TODO: i18n + + link = ($scope, $el, $attrs) -> + render = (model) -> + html = template({ + owner: $scope.usersById?[model.owner] + date: moment(model.created_date).format("DD MMM YYYY HH:mm") + }) + $el.html(html) + + bindOnce $scope, $attrs.ngModel, (model) -> + render(model) if model? + + $scope.$on "$destroy", -> + $el.off() return { link: link + restrict: "EA" require: "ngModel" } -module.directive("tgDateSelector", DateSelectorDirective) +module.directive("tgCreatedByDisplay", CreatedByDisplayDirective) ############################################################################# ## Watchers directive ############################################################################# -WatchersDirective = ($rootscope, $confirm) -> +WatchersDirective = ($rootscope, $confirm, $repo) -> + # You have to include a div with the tg-lb-watchers directive in the page + # where use this directive + # # TODO: i18n template = _.template(""" + <% if(isEditable){ %>
watchers - <% if (editable) { %> - <% } %>
+ <% } else if(watchers.length > 0){ %> +
+ watchers +
+ <% }; %> <% _.each(watchers, function(watcher) { %>
- + <%- watcher.full_name_display %> - +
- - <%- watcher.full_name_display %> - + <%- watcher.full_name_display %> - <% if (editable) { %> + <% if(isEditable){ %> - <% } %> + <% }; %>
<% }); %> """) link = ($scope, $el, $attrs, $model) -> - editable = $attrs.editable? + isEditable = -> + return $scope.project?.my_permissions?.indexOf($attrs.requiredPerm) != -1 + + save = (model) -> + promise = $repo.save($model.$modelValue) + promise.then -> + $confirm.notify("success") + watchers = _.map(model.watchers, (watcherId) -> $scope.usersById[watcherId]) + renderWatchers(watchers) + $rootscope.$broadcast("history:reload") + promise.then null, -> + model.revert() + $confirm.notify("error") renderWatchers = (watchers) -> - html = template({watchers: watchers, editable:editable}) + ctx = { + watchers: watchers + isEditable: isEditable() + } + html = template(ctx) $el.html(html) - if watchers.length == 0 - if editable - $el.find(".title").text("Add watchers") - $el.find(".watchers-header").addClass("no-watchers") - else - $el.find(".watchers-header").hide() - - $scope.$watch $attrs.ngModel, (item) -> - return if not item? - watchers = _.map(item.watchers, (watcherId) -> $scope.usersById[watcherId]) - renderWatchers(watchers) - - if not editable - $el.find(".add-watcher").remove() + if isEditable() and watchers.length == 0 + $el.find(".title").text("Add watchers") + $el.find(".watchers-header").addClass("no-watchers") $el.on "click", ".icon-delete", (event) -> event.preventDefault() + return if not isEditable() target = angular.element(event.currentTarget) watcherId = target.data("watcher-id") @@ -176,9 +238,11 @@ WatchersDirective = ($rootscope, $confirm) -> item = $model.$modelValue.clone() item.watchers = watcherIds $model.$setViewValue(item) + save(item) $el.on "click", ".add-watcher", (event) -> event.preventDefault() + return if not isEditable() $scope.$apply -> $rootscope.$broadcast("watcher:add", $model.$modelValue) @@ -189,19 +253,30 @@ WatchersDirective = ($rootscope, $confirm) -> item = $model.$modelValue.clone() item.watchers = watchers - $model.$setViewValue(item) + save(item) + + $scope.$watch $attrs.ngModel, (item) -> + return if not item? + watchers = _.map(item.watchers, (watcherId) -> $scope.usersById[watcherId]) + renderWatchers(watchers) + + $scope.$on "$destroy", -> + $el.off() return {link:link, require:"ngModel"} -module.directive("tgWatchers", ["$rootScope", "$tgConfirm", WatchersDirective]) +module.directive("tgWatchers", ["$rootScope", "$tgConfirm", "$tgRepo", WatchersDirective]) ############################################################################# ## Assigned to directive ############################################################################# -AssignedToDirective = ($rootscope, $confirm) -> +AssignedToDirective = ($rootscope, $confirm, $repo, $loading) -> + # You have to include a div with the tg-lb-assignedto directive in the page + # where use this directive + # # TODO: i18n template = _.template(""" <% if (assignedTo) { %> @@ -213,62 +288,360 @@ AssignedToDirective = ($rootscope, $confirm) ->
Assigned to - - <% if (assignedTo) { %> - <%- assignedTo.full_name_display %> - <% } else { %> - Not assigned - <% } %> - <% if (editable) { %> - - <% } %> + + + <% if (assignedTo) { %> + <%- assignedTo.full_name_display %> + <% } else { %> + Not assigned + <% } %> + + <% if(isEditable){ %><% }; %> - <% if (editable && assignedTo!==null) { %> + <% if (assignedTo!==null && isEditable) { %> <% } %>
- """) + """) # TODO: i18n link = ($scope, $el, $attrs, $model) -> - editable = $attrs.editable? + isEditable = -> + return $scope.project?.my_permissions?.indexOf($attrs.requiredPerm) != -1 + + save = (model) -> + $loading.start($el) + + promise = $repo.save($model.$modelValue) + promise.then -> + $loading.finish($el) + $confirm.notify("success") + renderAssignedTo(model) + $rootscope.$broadcast("history:reload") + promise.then null, -> + model.revert() + $confirm.notify("error") + $loading.finish($el) renderAssignedTo = (issue) -> assignedToId = issue?.assigned_to - assignedTo = null - assignedTo = $scope.usersById[assignedToId] if assignedToId? - html = template({assignedTo: assignedTo, editable:editable}) + assignedTo = if assignedToId? then $scope.usersById[assignedToId] else null + + ctx = { + assignedTo: assignedTo + isEditable: isEditable() + } + html = template(ctx) $el.html(html) + $el.on "click", ".user-assigned", (event) -> + event.preventDefault() + return if not isEditable() + $scope.$apply -> + $rootscope.$broadcast("assigned-to:add", $model.$modelValue) + + $el.on "click", ".icon-delete", (event) -> + event.preventDefault() + return if not isEditable() + title = "Are you sure you want to leave it unassigned?" # TODO: i18n + + $confirm.ask(title).then (finish) => + finish() + $model.$modelValue.assigned_to = null + save($model.$modelValue) + + $scope.$on "assigned-to:added", (ctx, userId) -> + $model.$modelValue.assigned_to = userId + save($model.$modelValue) + $scope.$watch $attrs.ngModel, (instance) -> renderAssignedTo(instance) - if editable - $el.on "click", ".user-assigned", (event) -> - event.preventDefault() - $scope.$apply -> - $rootscope.$broadcast("assigned-to:add", $model.$modelValue) - - $el.on "click", ".icon-delete", (event) -> - event.preventDefault() - title = "Delete assignetion" - message = "" - - $confirm.askOnDelete(title, message).then (finish) => - finish() - $model.$modelValue.assigned_to = null - renderAssignedTo($model.$modelValue) - - $scope.$on "assigned-to:added", (ctx, userId) -> - $model.$modelValue.assigned_to = userId - renderAssignedTo($model.$modelValue) + $scope.$on "$destroy", -> + $el.off() return { link:link, require:"ngModel" } +module.directive("tgAssignedTo", ["$rootScope", "$tgConfirm", "$tgRepo", "$tgLoading", AssignedToDirective]) -module.directive("tgAssignedTo", ["$rootScope", "$tgConfirm", AssignedToDirective]) + +############################################################################# +## Block Button directive +############################################################################# + +BlockButtonDirective = ($rootscope, $loading) -> + template = """ + Block + Unblock + """ + + link = ($scope, $el, $attrs, $model) -> + isEditable = -> + return $scope.project.my_permissions.indexOf("modify_us") != -1 + + $scope.$watch $attrs.ngModel, (item) -> + return if not item + + if isEditable() + $el.find('.item-block').addClass('editable') + + if item.is_blocked + $el.find('.item-block').hide() + $el.find('.item-unblock').show() + else + $el.find('.item-block').show() + $el.find('.item-unblock').hide() + + $el.on "click", ".item-block", (event) -> + event.preventDefault() + $rootscope.$broadcast("block", $model.$modelValue) + + $el.on "click", ".item-unblock", (event) -> + event.preventDefault() + $loading.start($el.find(".item-unblock")) + finish = -> + $loading.finish($el.find(".item-unblock")) + + $rootscope.$broadcast("unblock", $model.$modelValue, finish) + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + require: "ngModel" + template: template + } + +module.directive("tgBlockButton", ["$rootScope", "$tgLoading", BlockButtonDirective]) + + +############################################################################# +## Delete Button directive +############################################################################# + +DeleteButtonDirective = ($log, $repo, $confirm, $location) -> + template = """ + Delete + """ #TODO: i18n + + link = ($scope, $el, $attrs, $model) -> + if not $attrs.onDeleteGoToUrl + return $log.error "DeleteButtonDirective requires on-delete-go-to-url set in scope." + if not $attrs.onDeleteTitle + return $log.error "DeleteButtonDirective requires on-delete-title set in scope." + + $el.on "click", ".button", (event) -> + title = $scope.$eval($attrs.onDeleteTitle) + subtitle = $model.$modelValue.subject + + $confirm.askOnDelete(title, subtitle).then (finish) => + promise = $repo.remove($model.$modelValue) + promise.then => + finish() + url = $scope.$eval($attrs.onDeleteGoToUrl) + $location.path(url) + promise.then null, => + finish(false) + $confirm.notify("error") + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + require: "ngModel" + template: template + } + +module.directive("tgDeleteButton", ["$log", "$tgRepo", "$tgConfirm", "$tgLocation", DeleteButtonDirective]) + + +############################################################################# +## Editable subject directive +############################################################################# + +EditableSubjectDirective = ($rootscope, $repo, $confirm, $loading) -> + template = """ +
+ {{ item.subject }} + +
+
+ + + + +
+ """ + + link = ($scope, $el, $attrs, $model) -> + + isEditable = -> + return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1 + + save = -> + $model.$modelValue.subject = $scope.item.subject + $loading.start($el.find('.save-container')) + promise = $repo.save($model.$modelValue) + promise.then -> + $confirm.notify("success") + $rootscope.$broadcast("history:reload") + $el.find('.edit-subject').hide() + $el.find('.view-subject').show() + promise.then null, -> + $confirm.notify("error") + promise.finally -> + $loading.finish($el.find('.save-container')) + + $el.click -> + return if not isEditable() + $el.find('.edit-subject').show() + $el.find('.view-subject').hide() + $el.find('input').focus() + + $el.on "click", ".save", -> + save() + + $el.on "keyup", "input", -> + if event.keyCode == 13 + save() + else if event.keyCode == 27 + $model.$modelValue.revert() + $el.find('div.edit-subject').hide() + $el.find('div.view-subject').show() + + $el.find('div.edit-subject').hide() + $el.find('div.view-subject span.edit').hide() + + + $scope.$watch $attrs.ngModel, (value) -> + return if not value + $scope.item = value + + if not isEditable() + $el.find('.view-subject .edit').remove() + + $scope.$on "$destroy", -> + $el.off() + + + return { + link: link + restrict: "EA" + require: "ngModel" + template: template + } + +module.directive("tgEditableSubject", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", + EditableSubjectDirective]) + + +############################################################################# +## Editable subject directive +############################################################################# + +EditableDescriptionDirective = ($window, $document, $rootscope, $repo, $confirm, $compile, $loading) -> + template = """ +
+
+ +
+
+ + + + +
+ """ # TODO: i18n + noDescriptionMegEditMode = """ +

+ No description yet, why don't you add a good one clicking here? +

+ """ # TODO: i18n + noDescriptionMegReadMode = """ +

+ No description +

+ """ # TODO: i18n + + link = ($scope, $el, $attrs, $model) -> + $el.find('.edit-description').hide() + $el.find('.view-description .edit').hide() + + isEditable = -> + return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1 + + getSelectedText = -> + if $window.getSelection + return $window.getSelection().toString() + else if $document.selection + return $document.selection.createRange().text + return null + + $el.on "mouseup", ".view-description", (event) -> + # We want to dettect the a inside the div so we use the target and + # not the currentTarget + target = angular.element(event.target) + return if not isEditable() + return if target.is('a') + return if getSelectedText() + + $el.find('.edit-description').show() + $el.find('.view-description').hide() + $el.find('textarea').focus() + + $el.on "click", ".save", -> + $model.$modelValue.description = $scope.item.description + + $loading.start($el.find('.save-container')) + promise = $repo.save($model.$modelValue) + promise.then -> + $confirm.notify("success") + $rootscope.$broadcast("history:reload") + $el.find('.edit-description').hide() + $el.find('.view-description').show() + promise.then null, -> + $confirm.notify("error") + promise.finally -> + $loading.finish($el.find('.save-container')) + + $el.on "keyup", "textarea", -> + if event.keyCode == 27 + $scope.item.revert() + $el.find('.edit-description').hide() + $el.find('.view-description').show() + + $scope.$watch $attrs.ngModel, (value) -> + return if not value + $scope.item = value + + if isEditable() + $el.find('.view-description .edit').show() + $el.find('.view-description .us-content').addClass('editable') + $scope.noDescriptionMsg = noDescriptionMegEditMode + else + $scope.noDescriptionMsg = noDescriptionMegReadMode + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + require: "ngModel" + template: template + } + +module.directive("tgEditableDescription", ["$window", "$document", "$rootScope", "$tgRepo", "$tgConfirm", + "$compile", "$tgLoading", EditableDescriptionDirective]) ############################################################################# @@ -377,6 +750,7 @@ ListItemSeverityDirective = -> template: template } + ListItemTypeDirective = -> template = """
diff --git a/app/coffee/modules/common/lightboxes.coffee b/app/coffee/modules/common/lightboxes.coffee index 7bfaa917..87711d4a 100644 --- a/app/coffee/modules/common/lightboxes.coffee +++ b/app/coffee/modules/common/lightboxes.coffee @@ -127,16 +127,33 @@ module.directive("lightbox", ["lightboxService", LightboxDirective]) # Issue/Userstory blocking message lightbox directive. -BlockLightboxDirective = (lightboxService) -> +BlockLightboxDirective = ($rootscope, $tgrepo, $confirm, lightboxService, $loading) -> link = ($scope, $el, $attrs, $model) -> $el.find("h2.title").text($attrs.title) $scope.$on "block", -> + $el.find(".reason").val($model.$modelValue.blocked_note) lightboxService.open($el) - $scope.$on "unblock", -> - $model.$modelValue.is_blocked = false - $model.$modelValue.blocked_note_html = "" + $scope.$on "unblock", (event, model, finishCallback) -> + item = $model.$modelValue.clone() + item.is_blocked = false + item.blocked_note = "" + + promise = $tgrepo.save(item) + promise.then -> + $confirm.notify("success") + $rootscope.$broadcast("history:reload") + $model.$setViewValue(item) + finishCallback() + + promise.then null, -> + $confirm.notify("error") + item.revert() + $model.$setViewValue(item) + + promise.finally -> + finishCallback() $scope.$on "$destroy", -> $el.off() @@ -144,19 +161,34 @@ BlockLightboxDirective = (lightboxService) -> $el.on "click", ".button-green", (event) -> event.preventDefault() - $scope.$apply -> - $model.$modelValue.is_blocked = true - $model.$modelValue.blocked_note = $el.find(".reason").val() + item = $model.$modelValue.clone() + item.is_blocked = true + item.blocked_note = $el.find(".reason").val() + $model.$setViewValue(item) - lightboxService.close($el) + $loading.start($el.find(".button-green")) + + promise = $tgrepo.save($model.$modelValue) + promise.then -> + $confirm.notify("success") + $rootscope.$broadcast("history:reload") + + promise.then null, -> + $confirm.notify("error") + item.revert() + $model.$setViewValue(item) + + promise.finally -> + $loading.finish($el.find(".button-green")) + lightboxService.close($el) return { templateUrl: "/partials/views/modules/lightbox-block.html" - link:link, - require:"ngModel" + link: link + require: "ngModel" } -module.directive("tgLbBlock", ["lightboxService", BlockLightboxDirective]) +module.directive("tgLbBlock", ["$rootScope", "$tgRepo", "$tgConfirm", "lightboxService", "$tgLoading", BlockLightboxDirective]) ############################################################################# @@ -202,10 +234,10 @@ module.directive("tgBlockingMessageInput", ["$log", BlockingMessageInputDirectiv CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService, $loading) -> link = ($scope, $el, attrs) -> - isNew = true + $scope.isNew = true $scope.$on "usform:new", (ctx, projectId, status, statusList) -> - isNew = true + $scope.isNew = true $scope.usStatusList = statusList $scope.us = { @@ -229,7 +261,7 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService, $scope.$on "usform:edit", (ctx, us) -> $scope.us = us - isNew = false + $scope.isNew = false # Update texts for edition $el.find(".button-green span").html("Save") #TODO: i18n @@ -264,7 +296,7 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService, $loading.start(target) - if isNew + if $scope.isNew promise = $repo.create("userstories", $scope.us) broadcastEvent = "usform:new:success" else diff --git a/app/coffee/modules/common/tags.coffee b/app/coffee/modules/common/tags.coffee index 87a6fef9..9dcdc669 100644 --- a/app/coffee/modules/common/tags.coffee +++ b/app/coffee/modules/common/tags.coffee @@ -43,6 +43,9 @@ TagsDirective = -> $ctrl.$formatters.push(formatter) $ctrl.$parsers.push(parser) + $scope.$on "$destroy", -> + $el.off() + return { require: "ngModel" link: link @@ -83,127 +86,133 @@ ColorizeTagsDirective = -> $scope.$watch $attrs.tgColorizeTags, (tags) -> render(tags) if tags? + $scope.$on "$destroy", -> + $el.off() + return {link: link} module.directive("tgColorizeTags", ColorizeTagsDirective) + ############################################################################# -## TagLine (possible should be moved as generic directive) +## TagLine Directive (for Lightboxes) ############################################################################# -TagLineDirective = ($log, $rs) -> - # Main directive template (rendered by angular) +LbTagLineDirective = ($rs) -> + ENTER_KEY = 13 + template = """
-
- """ + + """ # TODO: i18n # Tags template (rendered manually using lodash) templateTags = _.template(""" <% _.each(tags, function(tag) { %> -
+ <%- tag.name %> - <% if (editable) { %> - <% } %> -
- <% }); %>""") - - 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) + + <% }); %> + """) # TODO: i18n link = ($scope, $el, $attrs, $model) -> - editable = if $attrs.editable == "true" then true else false - $el.addClass("tags-block") + ## Render + renderTags = (tags, tagsColors) -> + ctx = { + tags: _.map(tags, (t) -> {name: t, color: tagsColors[t]}) + } + html = templateTags(ctx) + $el.find("div.tags-container").html(html) + showSaveButton = -> $el.find(".save").removeClass("hidden") + hideSaveButton = -> $el.find(".save").addClass("hidden") + + resetInput = -> + $el.find("input").val("") + $el.find("input").autocomplete("close") + + ## Aux methods addValue = (value) -> - value = trim(value) - return if value.length <= 0 + value = trim(value.toLowerCase()) + return if value.length == 0 tags = _.clone($model.$modelValue, false) tags = [] if not tags? - tags.push(value) + tags.push(value) if value not in tags $scope.$apply -> - $model.$setViewValue(normalizeTags(tags)) + $model.$setViewValue(tags) + + deleteValue = (value) -> + value = trim(value.toLowerCase()) + return if value.length == 0 + + tags = _.clone($model.$modelValue, false) + tags = _.pull(tags, value) + + $scope.$apply -> + $model.$setViewValue(tags) saveInputTag = () -> - input = $el.find('input') + value = $el.find("input").val() - addValue(input.val()) - input.val("") - input.autocomplete("close") - $el.find('.save').hide() + addValue(value) + resetInput() + hideSaveButton() - $scope.$watch $attrs.ngModel, (val) -> - tags_colors = if $scope.project?.tags_colors? then $scope.project.tags_colors else [] - renderTags($el, val, editable, tags_colors) + ## Events + $el.on "keypress", "input", (event) -> + return if event.keyCode != ENTER_KEY + event.preventDefault() - bindOnce $scope, "projectId", (projectId) -> - # If not editable, no tags preloading is needed. - return if not editable + $el.on "keyup", "input", (event) -> + target = angular.element(event.currentTarget) + if event.keyCode == ENTER_KEY + saveInputTag() + else + if target.val().length + showSaveButton() + else + hideSaveButton() + + $el.on "click", ".save", (event) -> + event.preventDefault() + saveInputTag() + + $el.on "click", ".icon-delete", (event) -> + event.preventDefault() + target = angular.element(event.currentTarget) + + value = target.siblings(".tag-name").text() + deleteValue(value) + + bindOnce $scope, "project", (project) -> positioningFunction = (position, elements) -> menu = elements.element.element menu.css("width", elements.target.width) menu.css("top", position.top) menu.css("left", position.left) - $rs.projects.tags(projectId).then (data) -> - $el.find("input").autocomplete({ - source: data - position: { - my: "left top", - using: positioningFunction - } - select: (event, ui) -> - addValue(ui.item.value) - ui.item.value = "" - }) + $el.find("input").autocomplete({ + source: _.keys(project.tags_colors) + position: { + my: "left top", + using: positioningFunction + } + select: (event, ui) -> + addValue(ui.item.value) + ui.item.value = "" + }) - if not editable - $el.find("input").remove() + $scope.$watch $attrs.ngModel, (tags) -> + tagsColors = $scope.project?.tags_colors or [] + renderTags(tags, tagsColors) - $el.on "keypress", "input", (event) -> - return if event.keyCode != 13 - event.preventDefault() - - $el.on "keyup", "input", (event) -> - target = angular.element(event.currentTarget) - - if event.keyCode == 13 - saveInputTag() - else if target.val().length - $el.find('.save').show() - else - $el.find('.save').hide() - - $el.on "click", ".save", saveInputTag - - $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)) + $scope.$on "$destroy", -> + $el.off() return { link:link, @@ -211,4 +220,198 @@ TagLineDirective = ($log, $rs) -> template: template } -module.directive("tgTagLine", ["$log", "$tgResources", TagLineDirective]) +module.directive("tgLbTagLine", ["$tgResources", LbTagLineDirective]) + + +############################################################################# +## TagLine Directive (for detail pages) +############################################################################# + +TagLineDirective = ($rootScope, $repo, $rs, $confirm) -> + ENTER_KEY = 13 + ESC_KEY = 27 + + template = """ +
+ + + + """ # TODO: i18n + + # Tags template (rendered manually using lodash) + templateTags = _.template(""" + <% _.each(tags, function(tag) { %> + + <%- tag.name %> + <% if (isEditable) { %> + + <% } %> + + <% }); %> + """) # TODO: i18n + + link = ($scope, $el, $attrs, $model) -> + isEditable = -> + return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1 + + ## Render + renderTags = (tags, tagsColors) -> + ctx = { + tags: _.map(tags, (t) -> {name: t, color: tagsColors[t]}) + isEditable: isEditable() + } + html = templateTags(ctx) + $el.find("div.tags-container").html(html) + + renderInReadModeOnly = -> + $el.find(".add-tag").remove() + $el.find("input").remove() + $el.find(".save").remove() + + showAddTagButton = -> $el.find(".add-tag").removeClass("hidden") + hideAddTagButton = -> $el.find(".add-tag").addClass("hidden") + + showAddTagButtonText = -> $el.find(".add-tag-text").removeClass("hidden") + hideAddTagButtonText = -> $el.find(".add-tag-text").addClass("hidden") + + showSaveButton = -> $el.find(".save").removeClass("hidden") + hideSaveButton = -> $el.find(".save").addClass("hidden") + + showInput = -> $el.find("input").removeClass("hidden") + hideInput = -> $el.find("input").addClass("hidden") + resetInput = -> + $el.find("input").val("") + $el.find("input").autocomplete("close") + + ## Aux methods + addValue = (value) -> + value = trim(value.toLowerCase()) + return if value.length == 0 + + tags = _.clone($model.$modelValue.tags, false) + tags = [] if not tags? + tags.push(value) if value not in tags + + model = $model.$modelValue.clone() + model.tags = tags + $model.$setViewValue(model) + + onSuccess = -> + $rootScope.$broadcast("history:reload") + onError = -> + $confirm.notify("error") + model.revert() + $model.$setViewValue(model) + $repo.save(model).then(onSuccess, onError) + + deleteValue = (value) -> + value = trim(value.toLowerCase()) + return if value.length == 0 + + tags = _.clone($model.$modelValue.tags, false) + tags = _.pull(tags, value) + + model = $model.$modelValue.clone() + model.tags = tags + $model.$setViewValue(model) + + onSuccess = -> + $rootScope.$broadcast("history:reload") + onError = -> + $confirm.notify("error") + model.revert() + $model.$setViewValue(model) + $repo.save(model).then(onSuccess, onError) + + saveInputTag = () -> + value = $el.find("input").val() + + addValue(value) + resetInput() + hideSaveButton() + + ## Events + $el.on "keypress", "input", (event) -> + return if event.keyCode not in [ENTER_KEY, ESC_KEY] + event.preventDefault() + + $el.on "keyup", "input", (event) -> + target = angular.element(event.currentTarget) + + if event.keyCode == ENTER_KEY + saveInputTag() + else if event.keyCode == ESC_KEY + resetInput() + hideInput() + hideSaveButton() + showAddTagButton() + else + if target.val().length + showSaveButton() + else + hideSaveButton() + + $el.on "click", ".save", (event) -> + event.preventDefault() + saveInputTag() + + $el.on "click", ".add-tag", (event) -> + event.preventDefault() + hideAddTagButton() + showInput() + + $el.on "click", ".icon-delete", (event) -> + event.preventDefault() + target = angular.element(event.currentTarget) + + value = target.siblings(".tag-name").text() + deleteValue(value) + + bindOnce $scope, "project", (project) -> + if not isEditable() + renderInReadModeOnly() + return + + showAddTagButton() + + positioningFunction = (position, elements) -> + menu = elements.element.element + menu.css("width", elements.target.width) + menu.css("top", position.top) + menu.css("left", position.left) + + $el.find("input").autocomplete({ + source: _.keys(project.tags_colors) + position: { + my: "left top", + using: positioningFunction + } + select: (event, ui) -> + addValue(ui.item.value) + ui.item.value = "" + }) + + $scope.$watch $attrs.ngModel, (model) -> + return if not model + + if model.tags.length + hideAddTagButtonText() + else + showAddTagButtonText() + + tagsColors = $scope.project?.tags_colors or [] + renderTags(model.tags, tagsColors) + + $scope.$on "$destroy", -> + $el.off() + + return { + link:link, + require:"ngModel" + template: template + } + +module.directive("tgTagLine", ["$rootScope", "$tgRepo", "$tgResources", "$tgConfirm", TagLineDirective]) diff --git a/app/coffee/modules/common/wisiwyg.coffee b/app/coffee/modules/common/wisiwyg.coffee index 2177dab3..cb0f1443 100644 --- a/app/coffee/modules/common/wisiwyg.coffee +++ b/app/coffee/modules/common/wisiwyg.coffee @@ -32,7 +32,7 @@ tgMarkitupDirective = ($rootscope, $rs, $tr) -> previewTemplate = _.template("""
- Edit +
<%= data %> diff --git a/app/coffee/modules/issues/detail.coffee b/app/coffee/modules/issues/detail.coffee index b526a337..a7ce0269 100644 --- a/app/coffee/modules/issues/detail.coffee +++ b/app/coffee/modules/issues/detail.coffee @@ -46,11 +46,12 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin) "$log", "$appTitle", "$tgAnalytics", - "$tgNavUrls" + "$tgNavUrls", + "tgLoader" ] constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, - @log, @appTitle, @analytics, @navUrls) -> + @log, @appTitle, @analytics, @navUrls, tgLoader) -> @scope.issueRef = @params.issueref @scope.sectionName = "Issue Details" @.initializeEventHandlers() @@ -60,11 +61,12 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin) # On Success promise.then => @appTitle.set(@scope.issue.subject + " - " + @scope.project.name) + @.initializeOnDeleteGoToUrl() + tgLoader.pageLoaded() # On Error promise.then null, @.onInitialDataError.bind(@) - initializeEventHandlers: -> @scope.$on "attachment:create", => @rootscope.$broadcast("history:reload") @@ -81,6 +83,13 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @rootscope.$broadcast("history:reload") @.loadIssue() + initializeOnDeleteGoToUrl: -> + ctx = {project: @scope.project.slug} + if @scope.project.is_issues_activated + @scope.onDeleteGoToUrl = @navUrls.resolve("project-issues", ctx) + else + @scope.onDeleteGoToUrl = @navUrls.resolve("project", ctx) + loadProject: -> return @rs.projects.get(@scope.projectId).then (project) => @scope.project = project @@ -130,250 +139,432 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin) .then(=> @.loadUsersAndRoles()) .then(=> @.loadIssue()) - block: -> - @rootscope.$broadcast("block", @scope.issue) - - unblock: -> - @rootscope.$broadcast("unblock", @scope.issue) - - delete: -> - # TODO: i18n - title = "Delete Issue" - message = @scope.issue.subject - - @confirm.askOnDelete(title, message).then (finish) => - promise = @.repo.remove(@scope.issue) - promise.then => - finish() - @location.path(@navUrls.resolve("project-issues", {project: @scope.project.slug})) - promise.then null, => - finish(false) - @confirm.notify("error") - module.controller("IssueDetailController", IssueDetailController) ############################################################################# -## Issue Main Directive +## Issue status display directive ############################################################################# -IssueDirective = ($tgrepo, $log, $location, $confirm, $navUrls, $loading) -> - linkSidebar = ($scope, $el, $attrs, $ctrl) -> +IssueStatusDisplayDirective = -> + # Display if a Issue is open or closed and its issueboard status. + # + # Example: + # tg-issue-status-display(ng-model="issue") + # + # Requirements: + # - Issue object (ng-model) + # - scope.statusById object + + template = _.template(""" + + <% if (status.is_closed) { %> + Closed + <% } else { %> + Open + <% } %> + + + <%= status.name %> + + """) # TODO: i18n link = ($scope, $el, $attrs) -> - $ctrl = $el.controller() - linkSidebar($scope, $el, $attrs, $ctrl) - - if $el.is("form") - form = $el.checksley() - - $el.on "click", ".save-issue", (event) -> - if not form.validate() - return - - onSuccess = -> - $loading.finish(target) - $confirm.notify("success") - ctx = { - project: $scope.project.slug - ref: $scope.issue.ref - } - $location.path($navUrls.resolve("project-issues-detail", ctx)) - - onError = -> - $loading.finish(target) - $confirm.notify("error") - - target = angular.element(event.currentTarget) - $loading.start(target) - $tgrepo.save($scope.issue).then(onSuccess, onError) - - return {link:link} - -module.directive("tgIssueDetail", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm", "$tgNavUrls", - "$tgLoading", IssueDirective]) - - -############################################################################# -## Issue status directive -############################################################################# - -IssueStatusDirective = () -> - # TODO: i18n - template = _.template(""" -

- - <% if (status.is_closed) { %> - Closed - <% } else { %> - Open - <% } %> - - <%= status.name %> -

-
-
- <%- owner.full_name_display %> -
- -
- Created by <%- owner.full_name_display %> - <%- date %> -
-
-
-
- - <%= type.name %> - <% if (editable) { %> - - <% } %> - type -
-
- - <%= severity.name %> - <% if (editable) { %> - - <% } %> - severity -
-
- - <%= priority.name %> - <% if (editable) { %> - - <% } %> - priority -
-
- - <%= status.name %> - <% if (editable) { %> - - <% } %> - status -
-
- """) - selectionTypeTemplate = _.template(""" - - """) - selectionSeverityTemplate = _.template(""" - - """) - selectionPriorityTemplate = _.template(""" - - """) - selectionStatusTemplate = _.template(""" - - """) - - link = ($scope, $el, $attrs, $model) -> - editable = $attrs.editable? - - renderIssuestatus = (issue) -> - owner = $scope.usersById?[issue.owner] - date = moment(issue.created_date).format("DD MMM YYYY HH:mm") - type = $scope.typeById[issue.type] - status = $scope.statusById[issue.status] - severity = $scope.severityById[issue.severity] - priority = $scope.priorityById[issue.priority] + render = (issue) -> html = template({ - owner: owner - date: date - editable: editable - status: status - severity: severity - priority: priority - type: type + status: $scope.statusById[issue.status] }) $el.html(html) - $el.find(".type-data").append(selectionTypeTemplate({types:$scope.typeList})) - $el.find(".severity-data").append(selectionSeverityTemplate({severities:$scope.severityList})) - $el.find(".priority-data").append(selectionPriorityTemplate({priorities:$scope.priorityList})) - $el.find(".status-data").append(selectionStatusTemplate({statuses:$scope.statusList})) $scope.$watch $attrs.ngModel, (issue) -> - if issue? - renderIssuestatus(issue) + render(issue) if issue? - if editable - $el.on "click", ".type-data", (event) -> - event.preventDefault() - event.stopPropagation() - $el.find(".pop-type").popover().open() + $scope.$on "$destroy", -> + $el.off() - $el.on "click", ".type", (event) -> - event.preventDefault() - event.stopPropagation() - target = angular.element(event.currentTarget) - $model.$modelValue.type = target.data("type-id") - renderIssuestatus($model.$modelValue) - $.fn.popover().closeAll() + return { + link: link + restrict: "EA" + require: "ngModel" + } - $el.on "click", ".severity-data", (event) -> - event.preventDefault() - event.stopPropagation() - $el.find(".pop-severity").popover().open() +module.directive("tgIssueStatusDisplay", IssueStatusDisplayDirective) - $el.on "click", ".severity", (event) -> - event.preventDefault() - event.stopPropagation() - target = angular.element(event.currentTarget) - $model.$modelValue.severity = target.data("severity-id") - renderIssuestatus($model.$modelValue) - $.fn.popover().closeAll() - $el.on "click", ".priority-data", (event) -> - event.preventDefault() - event.stopPropagation() - $el.find(".pop-priority").popover().open() +############################################################################# +## Issue status button directive +############################################################################# - $el.on "click", ".priority", (event) -> - event.preventDefault() - event.stopPropagation() - target = angular.element(event.currentTarget) - $model.$modelValue.priority = target.data("priority-id") - renderIssuestatus($model.$modelValue) - $.fn.popover().closeAll() +IssueStatusButtonDirective = ($rootScope, $repo, $confirm, $loading) -> + # Display the status of Issue and you can edit it. + # + # Example: + # tg-issue-status-button(ng-model="issue") + # + # Requirements: + # - Issue object (ng-model) + # - scope.statusById object + # - $scope.project.my_permissions - $el.on "click", ".status-data", (event) -> - event.preventDefault() - event.stopPropagation() - $el.find(".pop-status").popover().open() + template = _.template(""" +
+ + <%= status.name %> + <% if(editable){ %><% }%> + status - $el.on "click", ".status", (event) -> - event.preventDefault() - event.stopPropagation() - target = angular.element(event.currentTarget) - $model.$modelValue.status = target.data("status-id") - renderIssuestatus($model.$modelValue) - $.fn.popover().closeAll() +
    + <% _.each(statuses, function(st) { %> +
  • <%- st.name %>
  • + <% }); %> +
+
+ """) #TODO: i18n - return {link:link, require:"ngModel"} + link = ($scope, $el, $attrs, $model) -> + isEditable = -> + return $scope.project.my_permissions.indexOf("modify_issue") != -1 -module.directive("tgIssueStatus", IssueStatusDirective) + render = (issue) => + status = $scope.statusById[issue.status] + + html = template({ + status: status + statuses: $scope.statusList + editable: isEditable() + }) + $el.html(html) + + $el.on "click", ".status-data", (event) -> + event.preventDefault() + event.stopPropagation() + return if not isEditable() + + $el.find(".pop-status").popover().open() + + $el.on "click", ".status", (event) -> + event.preventDefault() + event.stopPropagation() + return if not isEditable() + + target = angular.element(event.currentTarget) + + $.fn.popover().closeAll() + + issue = $model.$modelValue.clone() + issue.status = target.data("status-id") + $model.$setViewValue(issue) + + $scope.$apply() + + onSuccess = -> + $confirm.notify("success") + $rootScope.$broadcast("history:reload") + $loading.finish($el.find(".level-name")) + onError = -> + $confirm.notify("error") + issue.revert() + $model.$setViewValue(issue) + $loading.finish($el.find(".level-name")) + + $loading.start($el.find(".level-name")) + $repo.save($model.$modelValue).then(onSuccess, onError) + + $scope.$watch $attrs.ngModel, (issue) -> + render(issue) if issue + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + require: "ngModel" + } + +module.directive("tgIssueStatusButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", IssueStatusButtonDirective]) + +############################################################################# +## Issue type button directive +############################################################################# + +IssueTypeButtonDirective = ($rootScope, $repo, $confirm, $loading) -> + # Display the type of Issue and you can edit it. + # + # Example: + # tg-issue-type-button(ng-model="issue") + # + # Requirements: + # - Issue object (ng-model) + # - scope.typeById object + # - $scope.project.my_permissions + + template = _.template(""" +
+ + <%= type.name %> + <% if(editable){ %><% }%> + type + +
    + <% _.each(typees, function(tp) { %> +
  • <%- tp.name %>
  • + <% }); %> +
+
+ """) #TODO: i18n + + link = ($scope, $el, $attrs, $model) -> + isEditable = -> + return $scope.project.my_permissions.indexOf("modify_issue") != -1 + + render = (issue) => + type = $scope.typeById[issue.type] + + html = template({ + type: type + typees: $scope.typeList + editable: isEditable() + }) + $el.html(html) + + $el.on "click", ".type-data", (event) -> + event.preventDefault() + event.stopPropagation() + return if not isEditable() + + $el.find(".pop-type").popover().open() + + $el.on "click", ".type", (event) -> + event.preventDefault() + event.stopPropagation() + return if not isEditable() + + target = angular.element(event.currentTarget) + + $.fn.popover().closeAll() + + issue = $model.$modelValue.clone() + issue.type = target.data("type-id") + $model.$setViewValue(issue) + + $scope.$apply() + + onSuccess = -> + $confirm.notify("success") + $rootScope.$broadcast("history:reload") + $loading.finish($el.find(".level-name")) + onError = -> + $confirm.notify("error") + issue.revert() + $model.$setViewValue(issue) + $loading.finish($el.find(".level-name")) + $loading.start($el.find(".level-name")) + $repo.save($model.$modelValue).then(onSuccess, onError) + + $scope.$watch $attrs.ngModel, (issue) -> + render(issue) if issue + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + require: "ngModel" + } + +module.directive("tgIssueTypeButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", IssueTypeButtonDirective]) + + +############################################################################# +## Issue severity button directive +############################################################################# + +IssueSeverityButtonDirective = ($rootScope, $repo, $confirm, $loading) -> + # Display the severity of Issue and you can edit it. + # + # Example: + # tg-issue-severity-button(ng-model="issue") + # + # Requirements: + # - Issue object (ng-model) + # - scope.severityById object + # - $scope.project.my_permissions + + template = _.template(""" +
+ + <%= severity.name %> + <% if(editable){ %><% }%> + severity + +
    + <% _.each(severityes, function(sv) { %> +
  • <%- sv.name %>
  • + <% }); %> +
+
+ """) #TODO: i18n + + link = ($scope, $el, $attrs, $model) -> + isEditable = -> + return $scope.project.my_permissions.indexOf("modify_issue") != -1 + + render = (issue) => + severity = $scope.severityById[issue.severity] + + html = template({ + severity: severity + severityes: $scope.severityList + editable: isEditable() + }) + $el.html(html) + + $el.on "click", ".severity-data", (event) -> + event.preventDefault() + event.stopPropagation() + return if not isEditable() + + $el.find(".pop-severity").popover().open() + + $el.on "click", ".severity", (event) -> + event.preventDefault() + event.stopPropagation() + return if not isEditable() + + target = angular.element(event.currentTarget) + + $.fn.popover().closeAll() + + issue = $model.$modelValue.clone() + issue.severity = target.data("severity-id") + $model.$setViewValue(issue) + + $scope.$apply() + + onSuccess = -> + $confirm.notify("success") + $rootScope.$broadcast("history:reload") + $loading.finish($el.find(".level-name")) + onError = -> + $confirm.notify("error") + issue.revert() + $model.$setViewValue(issue) + $loading.finish($el.find(".level-name")) + $loading.start($el.find(".level-name")) + $repo.save($model.$modelValue).then(onSuccess, onError) + + $scope.$watch $attrs.ngModel, (issue) -> + render(issue) if issue + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + require: "ngModel" + } + +module.directive("tgIssueSeverityButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", IssueSeverityButtonDirective]) + + +############################################################################# +## Issue priority button directive +############################################################################# + +IssuePriorityButtonDirective = ($rootScope, $repo, $confirm, $loading) -> + # Display the priority of Issue and you can edit it. + # + # Example: + # tg-issue-priority-button(ng-model="issue") + # + # Requirements: + # - Issue object (ng-model) + # - scope.priorityById object + # - $scope.project.my_permissions + + template = _.template(""" +
+ + <%= priority.name %> + <% if(editable){ %><% }%> + priority + +
    + <% _.each(priorityes, function(pr) { %> +
  • <%- pr.name %>
  • + <% }); %> +
+
+ """) #TODO: i18n + + link = ($scope, $el, $attrs, $model) -> + isEditable = -> + return $scope.project.my_permissions.indexOf("modify_issue") != -1 + + render = (issue) => + priority = $scope.priorityById[issue.priority] + + html = template({ + priority: priority + priorityes: $scope.priorityList + editable: isEditable() + }) + $el.html(html) + + $el.on "click", ".priority-data", (event) -> + event.preventDefault() + event.stopPropagation() + return if not isEditable() + + $el.find(".pop-priority").popover().open() + + $el.on "click", ".priority", (event) -> + event.preventDefault() + event.stopPropagation() + return if not isEditable() + + target = angular.element(event.currentTarget) + + $.fn.popover().closeAll() + + issue = $model.$modelValue.clone() + issue.priority = target.data("priority-id") + $model.$setViewValue(issue) + + $scope.$apply() + + onSuccess = -> + $confirm.notify("success") + $rootScope.$broadcast("history:reload") + $loading.finish($el.find(".level-name")) + onError = -> + $confirm.notify("error") + issue.revert() + $model.$setViewValue(issue) + $loading.finish($el.find(".level-name")) + $loading.start($el.find(".level-name")) + $repo.save($model.$modelValue).then(onSuccess, onError) + + $scope.$watch $attrs.ngModel, (issue) -> + render(issue) if issue + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + require: "ngModel" + } + +module.directive("tgIssuePriorityButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", IssuePriorityButtonDirective]) ############################################################################# diff --git a/app/coffee/modules/related-tasks.coffee b/app/coffee/modules/related-tasks.coffee index 8b853704..66d38bef 100644 --- a/app/coffee/modules/related-tasks.coffee +++ b/app/coffee/modules/related-tasks.coffee @@ -53,7 +53,7 @@ RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading) ->
-
+
<% if(perms.modify_task) { %> @@ -166,7 +166,7 @@ RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading) -> return {link:link, require:"ngModel"} -module.directive("tgRelatedTaskRow", ["$tgRepo", "$compile", "$tgConfirm", "$rootScope", "$tgLoading", RelatedTaskRowDirective]) +module.directive("tgRelatedTaskRow", ["$tgRepo", "$compile", "$tgConfirm", "$rootScope", "$tgLoading", "$tgAnalytics", RelatedTaskRowDirective]) RelatedTaskCreateFormDirective = ($repo, $compile, $confirm, $tgmodel, $loading, $analytics) -> template = _.template(""" diff --git a/app/coffee/modules/resources/projects.coffee b/app/coffee/modules/resources/projects.coffee index 19437881..f3e1ab51 100644 --- a/app/coffee/modules/resources/projects.coffee +++ b/app/coffee/modules/resources/projects.coffee @@ -45,9 +45,6 @@ resourceProvider = ($repo) -> service.stats = (projectId) -> return $repo.queryOneRaw("projects", "#{projectId}/stats") - service.tags = (projectId) -> - return $repo.queryOneRaw("projects", "#{projectId}/tags") - service.tagsColors = (id) -> return $repo.queryOne("projects", "#{id}/tags_colors") diff --git a/app/coffee/modules/taskboard/lightboxes.coffee b/app/coffee/modules/taskboard/lightboxes.coffee index 9a28e5da..6a1c09f0 100644 --- a/app/coffee/modules/taskboard/lightboxes.coffee +++ b/app/coffee/modules/taskboard/lightboxes.coffee @@ -25,7 +25,7 @@ debounce = @.taiga.debounce CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, lightboxService) -> link = ($scope, $el, attrs) -> - isNew = true + $scope.isNew = true $scope.$on "taskform:new", (ctx, sprintId, usId) -> $scope.task = { @@ -37,7 +37,7 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, lightboxService) -> assigned_to: null tags: [] } - isNew = true + $scope.isNew = true # Update texts for creation $el.find(".button-green span").html("Create") #TODO: i18n @@ -46,7 +46,7 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, lightboxService) -> $scope.$on "taskform:edit", (ctx, task) -> $scope.task = task - isNew = false + $scope.isNew = false # Update texts for edition $el.find(".button-green span").html("Save") #TODO: i18n @@ -60,7 +60,7 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, lightboxService) -> if not form.validate() return - if isNew + if $scope.isNew promise = $repo.create("tasks", $scope.task) broadcastEvent = "taskform:new:success" else diff --git a/app/coffee/modules/tasks/detail.coffee b/app/coffee/modules/tasks/detail.coffee index de3b715d..5598ccc5 100644 --- a/app/coffee/modules/tasks/detail.coffee +++ b/app/coffee/modules/tasks/detail.coffee @@ -57,6 +57,7 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin) promise.then () => @appTitle.set(@scope.task.subject + " - " + @scope.project.name) + @.initializeOnDeleteGoToUrl() tgLoader.pageLoaded() promise.then null, @.onInitialDataError.bind(@) @@ -70,6 +71,21 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.$on "attachment:delete", => @rootscope.$broadcast("history:reload") + initializeOnDeleteGoToUrl: -> + ctx = {project: @scope.project.slug} + @scope.onDeleteGoToUrl = @navUrls.resolve("project", ctx) + if @scope.project.is_backlog_activated + if @scope.task.milestone + ctx.sprint = @scope.sprint.slug + @scope.onDeleteGoToUrl = @navUrls.resolve("project-taskboard", ctx) + else if @scope.task.us + ctx.ref = @scope.us.ref + @scope.onDeleteGoToUrl = @navUrls.resolve("project-userstories-detail", ctx) + else if @scope.project.is_kanban_activated + if @scope.us + ctx.ref = @scope.us.ref + @scope.onDeleteGoToUrl = @navUrls.resolve("project-userstories-detail", ctx) + loadProject: -> return @rs.projects.get(@scope.projectId).then (project) => @scope.project = project @@ -97,14 +113,19 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin) ref: @scope.task.neighbors.next.ref } @scope.nextUrl = @navUrls.resolve("project-tasks-detail", ctx) + return task - if task.milestone - @rs.sprints.get(task.project, task.milestone).then (sprint) => - @scope.sprint = sprint + loadSprint: -> + if @scope.task.milestone + return @rs.sprints.get(@scope.task.project, @scope.task.milestone).then (sprint) => + @scope.sprint = sprint + return sprint - if task.user_story - @rs.userstories.get(task.project, task.user_story).then (us) => - @scope.us = us + loadUserStory: -> + if @scope.task.user_story + return @rs.userstories.get(@scope.task.project, @scope.task.user_story).then (us) => + @scope.us = us + return us loadInitialData: -> params = { @@ -119,157 +140,216 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin) return promise.then(=> @.loadProject()) .then(=> @.loadUsersAndRoles()) - .then(=> @.loadTask()) - - block: -> - @rootscope.$broadcast("block", @scope.task) - - unblock: -> - @rootscope.$broadcast("unblock", @scope.task) - - delete: -> - #TODO: i18n - title = "Delete Task" - message = @scope.task.subject - - @confirm.askOnDelete(title, message).then (finish) => - promise = @.repo.remove(@scope.task) - promise.then => - finish() - - if @scope.task.milestone - @location.path(@navUrls.resolve("project-taskboard", {project: @scope.project.slug, sprint: @scope.sprint.slug})) - else if @scope.us - @location.path(@navUrls.resolve("project-userstories-detail", {project: @scope.project.slug, ref: @scope.us.ref})) - - promise.then null, => - finish(false) - @confirm.notify("error") + .then(=> @.loadTask().then(=> @q.all([@.loadUserStory(), + @.loadSprint()]))) module.controller("TaskDetailController", TaskDetailController) ############################################################################# -## Task Main Directive +## Task status display directive ############################################################################# -TaskDirective = ($tgrepo, $log, $location, $confirm, $navUrls, $loading) -> - linkSidebar = ($scope, $el, $attrs, $ctrl) -> +TaskStatusDisplayDirective = -> + # Display if a Task is open or closed and its taskboard status. + # + # Example: + # tg-task-status-display(ng-model="task") + # + # Requirements: + # - Task object (ng-model) + # - scope.statusById object + + template = _.template(""" + + <% if (status.is_closed) { %> + Closed + <% } else { %> + Open + <% } %> + + + <%= status.name %> + + """) # TODO: i18n link = ($scope, $el, $attrs) -> - $ctrl = $el.controller() - linkSidebar($scope, $el, $attrs, $ctrl) + render = (task) -> + html = template({ + status: $scope.statusById[task.status] + }) + $el.html(html) - if $el.is("form") - form = $el.checksley() + $scope.$watch $attrs.ngModel, (task) -> + render(task) if task? - $el.on "click", ".save-task", (event) -> - if not form.validate() - return + $scope.$on "$destroy", -> + $el.off() - onSuccess = -> - $loading.finish(target) - $confirm.notify("success") - ctx = { - project: $scope.project.slug - ref: $scope.task.ref - } - $location.path($navUrls.resolve("project-tasks-detail", ctx)) + return { + link: link + restrict: "EA" + require: "ngModel" + } - onError = -> - $loading.finish(target) - $confirm.notify("error") +module.directive("tgTaskStatusDisplay", TaskStatusDisplayDirective) + + +############################################################################# +## Task status button directive +############################################################################# + +TaskStatusButtonDirective = ($rootScope, $repo, $confirm, $loading) -> + # Display the status of Task and you can edit it. + # + # Example: + # tg-task-status-button(ng-model="task") + # + # Requirements: + # - Task object (ng-model) + # - scope.statusById object + # - $scope.project.my_permissions + + template = _.template(""" +
+ + <%= status.name %> + <% if(editable){ %><% }%> + status + +
    + <% _.each(statuses, function(st) { %> +
  • <%- st.name %>
  • + <% }); %> +
+
+ """) #TODO: i18n + + link = ($scope, $el, $attrs, $model) -> + isEditable = -> + return $scope.project.my_permissions.indexOf("modify_task") != -1 + + render = (task) => + status = $scope.statusById[task.status] + + html = template({ + status: status + statuses: $scope.statusList + editable: isEditable() + }) + $el.html(html) + + $el.on "click", ".status-data", (event) -> + event.preventDefault() + event.stopPropagation() + return if not isEditable() + + $el.find(".pop-status").popover().open() + + $el.on "click", ".status", (event) -> + event.preventDefault() + event.stopPropagation() + return if not isEditable() target = angular.element(event.currentTarget) - $loading.start(target) - $tgrepo.save($scope.task).then(onSuccess, onError) - return {link:link} + $.fn.popover().closeAll() -module.directive("tgTaskDetail", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm", "$tgNavUrls", - "$tgLoading", TaskDirective]) + task = $model.$modelValue.clone() + task.status = target.data("status-id") + $model.$setViewValue(task) + + $scope.$apply() + + onSuccess = -> + $confirm.notify("success") + $rootScope.$broadcast("history:reload") + $loading.finish($el.find(".level-name")) + + onError = -> + $confirm.notify("error") + task.revert() + $model.$setViewValue(task) + $loading.finish($el.find(".level-name")) + + $loading.start($el.find(".level-name")) + $repo.save($model.$modelValue).then(onSuccess, onError) + + $scope.$watch $attrs.ngModel, (task) -> + render(task) if task + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + require: "ngModel" + } + +module.directive("tgTaskStatusButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", + TaskStatusButtonDirective]) -############################################################################# -## Task status directive -############################################################################# - -TaskStatusDirective = () -> - #TODO: i18n +TaskIsIocaineButtonDirective = ($rootscope, $tgrepo, $confirm, $loading) -> template = _.template(""" -

- - <% if (status.is_closed) { %> - Closed - <% } else { %> - Open - <% } %> - <%= status.name %> -

-
-
- <%- owner.full_name_display %> -
- -
- Created by <%- owner.full_name_display %> - <%- date %> -
-
-
-
- - <%= status.name %> - <% if (editable) { %> - - <% } %> - status -
-
- """) - selectionStatusTemplate = _.template(""" - +
+ + +
""") link = ($scope, $el, $attrs, $model) -> - editable = $attrs.editable? + isEditable = -> + return $scope.project.my_permissions.indexOf("modify_task") != -1 - renderTaskstatus = (task) -> - owner = $scope.usersById?[task.owner] - date = moment(task.created_date).format("DD MMM YYYY HH:mm") - status = $scope.statusById[task.status] - html = template({ - owner: owner - date: date - editable: editable - status: status - }) + render = (task) -> + if not isEditable() and not task.is_iocaine + $el.html("") + return + + ctx = { + isIocaine: task.is_iocaine + isEditable: isEditable() + } + html = template(ctx) $el.html(html) - $el.find(".status-data").append(selectionStatusTemplate({statuses:$scope.statusList})) + + $el.on "click", ".is-iocaine", (event) -> + return if not isEditable() + + task = $model.$modelValue.clone() + task.is_iocaine = not task.is_iocaine + $model.$setViewValue(task) + $loading.start($el.find('label')) + + promise = $tgrepo.save($model.$modelValue) + promise.then -> + $confirm.notify("success") + $rootscope.$broadcast("history:reload") + + promise.then null, -> + task.revert() + $model.$setViewValue(task) + $confirm.notify("error") + + promise.finally -> + $loading.finish($el.find('label')) $scope.$watch $attrs.ngModel, (task) -> - if task? - renderTaskstatus(task) + render(task) if task - if editable - $el.on "click", ".status-data", (event) -> - event.preventDefault() - event.stopPropagation() - $el.find(".pop-status").popover().open() + $scope.$on "$destroy", -> + $el.off() - $el.on "click", ".status", (event) -> - event.preventDefault() - event.stopPropagation() - target = angular.element(event.currentTarget) - $model.$modelValue.status = target.data("status-id") - renderTaskstatus($model.$modelValue) - $el.find(".popover").popover().close() + return { + link: link + restrict: "EA" + require: "ngModel" + } - return {link:link, require:"ngModel"} - -module.directive("tgTaskStatus", TaskStatusDirective) +module.directive("tgTaskIsIocaineButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", TaskIsIocaineButtonDirective]) diff --git a/app/coffee/modules/userstories/detail.coffee b/app/coffee/modules/userstories/detail.coffee index f3845362..050679b4 100644 --- a/app/coffee/modules/userstories/detail.coffee +++ b/app/coffee/modules/userstories/detail.coffee @@ -59,12 +59,17 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin) # On Success promise.then => @appTitle.set(@scope.us.subject + " - " + @scope.project.name) + @.initializeOnDeleteGoToUrl() tgLoader.pageLoaded() # On Error promise.then null, @.onInitialDataError.bind(@) initializeEventHandlers: -> + @scope.$on "related-tasks:update", => + @.loadUs() + @scope.tasks = _.clone(@scope.tasks, false) + @scope.$on "attachment:create", => @analytics.trackEvent("attachment", "create", "create attachment on userstory", 1) @rootscope.$broadcast("history:reload") @@ -75,6 +80,18 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.$on "attachment:delete", => @rootscope.$broadcast("history:reload") + initializeOnDeleteGoToUrl: -> + ctx = {project: @scope.project.slug} + @scope.onDeleteGoToUrl = @navUrls.resolve("project", ctx) + if @scope.project.is_backlog_activated + if @scope.us.milestone + ctx.sprint = @scope.sprint.slug + @scope.onDeleteGoToUrl = @navUrls.resolve("project-taskboard", ctx) + else + @scope.onDeleteGoToUrl = @navUrls.resolve("project-backlog", ctx) + else if @scope.project.is_kanban_activated + @scope.onDeleteGoToUrl = @navUrls.resolve("project-kanban", ctx) + loadProject: -> return @rs.projects.get(@scope.projectId).then (project) => @scope.project = project @@ -106,12 +123,14 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin) } @scope.nextUrl = @navUrls.resolve("project-userstories-detail", ctx) - if us.milestone - @rs.sprints.get(us.project, us.milestone).then (sprint) => - @scope.sprint = sprint - return us + loadSprint: -> + if @scope.us.milestone + return @rs.sprints.get(@scope.us.project, @scope.us.milestone).then (sprint) => + @scope.sprint = sprint + return sprint + loadTasks: -> return @rs.tasks.list(@scope.projectId, null, @scope.usId).then (tasks) => @scope.tasks = tasks @@ -130,263 +149,128 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin) return promise.then(=> @.loadProject()) .then(=> @.loadUsersAndRoles()) - .then(=> @q.all([@.loadUs(), + .then(=> @q.all([@.loadUs().then(=> @.loadSprint()), @.loadTasks()])) - block: -> - @rootscope.$broadcast("block", @scope.us) - - unblock: -> - @rootscope.$broadcast("unblock", @scope.us) - - delete: -> - #TODO: i18n - title = "Delete User Story" - message = @scope.us.subject - - @confirm.askOnDelete(title, message).then (finish) => - promise = @.repo.remove(@scope.us) - promise.then => - finish() - - if @scope.us.milestone - @location.path(@navUrls.resolve("project-taskboard", {project: @scope.project.slug, sprint: @scope.sprint.slug})) - else if @scope.project.is_backlog_activated - @location.path(@navUrls.resolve("project-backlog", {project: @scope.project.slug})) - else - @location.path(@navUrls.resolve("project-kanban", {project: @scope.project.slug})) - promise.then null, => - finish(false) - $confirm.notify("error") - module.controller("UserStoryDetailController", UserStoryDetailController) + ############################################################################# -## User story Main Directive +## User story status display directive ############################################################################# -UsDirective = ($tgrepo, $log, $location, $confirm, $navUrls, $loading) -> - linkSidebar = ($scope, $el, $attrs, $ctrl) -> +UsStatusDisplayDirective = -> + # Display if a US is open or closed and its kanban status. + # + # Example: + # tg-us-status-display(ng-model="us") + # + # Requirements: + # - US object (ng-model) + # - scope.statusById object + + template = _.template(""" + + <% if (is_closed) { %> + Closed + <% } else { %> + Open + <% } %> + + + <%= status.name %> + + """) # TODO: i18n link = ($scope, $el, $attrs) -> - $ctrl = $el.controller() - linkSidebar($scope, $el, $attrs, $ctrl) - - if $el.is("form") - form = $el.checksley() - - $el.on "click", ".save-us", (event) -> - if not form.validate() - return - - onSuccess = -> - $loading.finish(target) - $confirm.notify("success") - ctx = { - project: $scope.project.slug - ref: $scope.us.ref - } - $location.path($navUrls.resolve("project-userstories-detail", ctx)) - - onError = -> - $loading.finish(target) - $confirm.notify("error") - - target = angular.element(event.currentTarget) - $loading.start(target) - $tgrepo.save($scope.us).then(onSuccess, onError) - - return {link:link} - -module.directive("tgUsDetail", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm", - "$tgNavUrls", "$tgLoading", UsDirective]) - -############################################################################# -## User story status directive -############################################################################# - -UsStatusDetailDirective = () -> - #TODO: i18n - template = _.template(""" -

- - <% if (is_closed) { %> - Closed - <% } else { %> - Open - <% } %> - <%= status.name %> -

- -
-
- - <%- totalClosedTasks %>/<%- totalTasks %> tasks completed - -
- -
-
- <%- owner.full_name_display %> -
- -
- Created by <%- owner.full_name_display %> - <%- date %> -
-
- -
    -
  • - <%- totalPoints %> - total -
  • - <% _.each(rolePoints, function(rolePoint) { %> -
  • - <%- rolePoint.points %> - <%- rolePoint.name %>
  • - <% }); %> -
- -
-
- - <%= status.name %> - <% if (editable) { %> - - <% } %> - status -
-
- """) - selectionStatusTemplate = _.template(""" - - """) - selectionPointsTemplate = _.template(""" - - """) - - link = ($scope, $el, $attrs, $model) -> - editable = $attrs.editable? - updatingSelectedRoleId = null - $ctrl = $el.controller() - - showSelectPoints = (target) -> - us = $model.$modelValue - $el.find(".pop-points-open").remove() - $el.find(target).append(selectionPointsTemplate({ "points": $scope.project.points })) - target.removeClass('active') - $el.find(".pop-points-open a[data-point-id='#{us.points[updatingSelectedRoleId]}']").addClass("active") - # If not showing role selection let's move to the left - $el.find(".pop-points-open").popover().open() - - calculateTotalPoints = (us)-> - values = _.map(us.points, (v, k) -> $scope.pointsById[v].value) - values = _.filter(values, (num) -> num?) - if values.length == 0 - return "?" - - return _.reduce(values, (acc, num) -> acc + num) - - renderUsstatus = (us) -> - owner = $scope.usersById?[us.owner] - date = moment(us.created_date).format("DD MMM YYYY HH:mm") - status = $scope.statusById[us.status] - rolePoints = _.clone(_.filter($scope.project.roles, "computable"), true) - _.map rolePoints, (v, k) -> - name = $scope.pointsById[us.points[v.id]].name - name = "?" if not name? - v.points = name - - totalTasks = $scope.tasks.length - totalClosedTasks = _.filter($scope.tasks, (task) => $scope.taskStatusById[task.status].is_closed).length - usProgress = 0 - usProgress = 100 * totalClosedTasks / totalTasks if totalTasks > 0 + render = (us) -> html = template({ - owner: owner - date: date - editable: editable is_closed: us.is_closed - status: status - totalPoints: us.total_points - rolePoints: rolePoints - totalTasks: totalTasks - totalClosedTasks: totalClosedTasks - usProgress: usProgress + status: $scope.statusById[us.status] }) $el.html(html) - $el.find(".status-data").append(selectionStatusTemplate({statuses:$scope.statusList})) - bindOnce $scope, "tasks", (tasks) -> - $scope.$watch $attrs.ngModel, (us) -> - if us? - renderUsstatus(us) + $scope.$watch $attrs.ngModel, (us) -> + render(us) if us? - $scope.$on "related-tasks:update", -> - us = $scope.$eval $attrs.ngModel - if us? - # Reload the us because the status could have changed - $ctrl.loadUs() - renderUsstatus(us) + $scope.$on "$destroy", -> + $el.off() - if editable - $el.on "click", ".status-data", (event) -> - event.preventDefault() - event.stopPropagation() - $el.find(".pop-status").popover().open() + return { + link: link + restrict: "EA" + require: "ngModel" + } - $el.on "click", ".status", (event) -> - event.preventDefault() - event.stopPropagation() - target = angular.element(event.currentTarget) - $model.$modelValue.status = target.data("status-id") - renderUsstatus($model.$modelValue) - $.fn.popover().closeAll() +module.directive("tgUsStatusDisplay", UsStatusDisplayDirective) - $el.on "click", ".total.clickable", (event) -> - event.preventDefault() - event.stopPropagation() - target = angular.element(event.currentTarget) - updatingSelectedRoleId = target.data("role-id") - target.siblings().removeClass('active') - target.addClass('active') - showSelectPoints(target) - $el.on "click", ".point", (event) -> - event.preventDefault() - event.stopPropagation() +############################################################################# +## User story related tasts progress splay Directive +############################################################################# - target = angular.element(event.currentTarget) - $.fn.popover().closeAll() +UsTasksProgressDisplayDirective = -> + # Display a progress bar with the stats of completed tasks. + # + # Example: + # tg-us-tasks-progress-display(ng-model="tasks") + # + # Requirements: + # - Task object list (ng-model) + # - scope.taskStatusById object - $scope.$apply () -> - us = $model.$modelValue - usPoints = _.clone(us.points, true) - usPoints[updatingSelectedRoleId] = target.data("point-id") - us.points = usPoints - us.total_points = calculateTotalPoints(us) - renderUsstatus(us) + template = _.template(""" +
+ + <%- totalClosedTasks %>/<%- totalTasks %> tasks completed + + """) # TODO: i18n - return {link:link, require:"ngModel"} + link = ($scope, $el, $attrs) -> + render = (tasks) -> + totalTasks = tasks.length + totalClosedTasks = _.filter(tasks, (task) => $scope.taskStatusById[task.status].is_closed).length + + progress = if totalTasks > 0 then 100 * totalClosedTasks / totalTasks else 0 + + html = template({ + totalTasks: totalTasks + totalClosedTasks: totalClosedTasks + progress: progress + }) + $el.html(html) + + $scope.$watch $attrs.ngModel, (tasks) -> + render(tasks) if tasks? + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + require: "ngModel" + } + +module.directive("tgUsTasksProgressDisplay", UsTasksProgressDisplayDirective) -module.directive("tgUsStatusDetail", UsStatusDetailDirective) ############################################################################# ## User story estimation directive ############################################################################# -UsEstimationDirective = ($log) -> +UsEstimationDirective = ($rootScope, $repo, $confirm) -> + # Display the points of a US and you can edit it. + # + # Example: + # tg-us-estimation-progress-bar(ng-model="us") + # + # Requirements: + # - Us object (ng-model) + # - scope.project object + # Optionals: + # - save-after-modify (boolean): save object after modify + mainTemplate = _.template("""
  • @@ -394,7 +278,7 @@ UsEstimationDirective = ($log) -> total
  • <% _.each(roles, function(role) { %> -
  • +
  • <%- role.points %> <%- role.name %>
  • <% }); %> @@ -417,7 +301,14 @@ UsEstimationDirective = ($log) ->
""") - link = ($scope, $el, $attrs) -> + link = ($scope, $el, $attrs, $model) -> + saveAfterModify = $attrs.saveAfterModify or false + + isEditable = -> + if $model.$modelValue.id + return $scope.project.my_permissions.indexOf("modify_us") != -1 + return $scope.project.my_permissions.indexOf("add_us") != -1 + render = (us) -> totalPoints = us.total_points or 0 computableRoles = _.filter($scope.project.roles, "computable") @@ -430,7 +321,12 @@ UsEstimationDirective = ($log) -> role.points = if pointObj? and pointObj.name? then pointObj.name else "?" return role - html = mainTemplate({totalPoints: totalPoints, roles: roles}) + ctx = { + totalPoints: totalPoints + roles: roles + editable: isEditable() + } + html = mainTemplate(ctx) $el.html(html) renderPoints = (target, us, roleId) -> @@ -463,19 +359,15 @@ UsEstimationDirective = ($log) -> return "0" return _.reduce(values, (acc, num) -> acc + num) - $scope.$watch $attrs.ngModel, (us) -> - render(us) if us - - $scope.$on "$destroy", -> - $el.off() - $el.on "click", ".total.clickable", (event) -> event.preventDefault() event.stopPropagation() + return if not isEditable() + target = angular.element(event.currentTarget) roleId = target.data("role-id") - us = $scope.$eval($attrs.ngModel) + us = $model.$modelValue renderPoints(target, us, roleId) target.siblings().removeClass('active') @@ -484,8 +376,7 @@ UsEstimationDirective = ($log) -> $el.on "click", ".point", (event) -> event.preventDefault() event.stopPropagation() - - us = $scope.$eval($attrs.ngModel) + return if not isEditable() target = angular.element(event.currentTarget) roleId = target.data("role-id") @@ -493,17 +384,262 @@ UsEstimationDirective = ($log) -> $el.find(".popover").popover().close() - points = _.clone(us.points, true) + # NOTE: This block of code is strange and, sometimes, repetitive + # but is the only solution I find to update the object + # corectly + us = angular.copy($model.$modelValue) + points = _.clone($model.$modelValue.points, true) points[roleId] = pointId + us.setAttr('points', points) if us.setAttr? + us.points = points + us.total_points = calculateTotalPoints(us) + $model.$setViewValue(us) - $scope.$apply -> - us.points = points - us.total_points = calculateTotalPoints(us) - render(us) + if saveAfterModify + # Edit in the detail page + onSuccess = -> + $confirm.notify("success") + $rootScope.$broadcast("history:reload") + onError = -> + us.revert() + $model.$setViewValue(us) + $confirm.notify("error") + $repo.save($model.$modelValue).then(onSuccess, onError) + else + # Create or eedit in the lightbox + render($model.$modelValue) + + $scope.$watch $attrs.ngModel, (us) -> + render(us) if us + + $scope.$on "$destroy", -> + $el.off() return { link: link restrict: "EA" + require: "ngModel" } -module.directive("tgUsEstimation", UsEstimationDirective) +module.directive("tgUsEstimation", ["$rootScope", "$tgRepo", "$tgConfirm", UsEstimationDirective]) + + +############################################################################# +## User story status button directive +############################################################################# + +UsStatusButtonDirective = ($rootScope, $repo, $confirm, $loading) -> + # Display the status of a US and you can edit it. + # + # Example: + # tg-us-status-button(ng-model="us") + # + # Requirements: + # - Us object (ng-model) + # - scope.statusById object + # - $scope.project.my_permissions + + template = _.template(""" +
+ + <%= status.name %> + <% if(editable){ %><% }%> + status + +
    + <% _.each(statuses, function(st) { %> +
  • <%- st.name %>
  • + <% }); %> +
+
+ """) #TODO: i18n + + link = ($scope, $el, $attrs, $model) -> + isEditable = -> + return $scope.project.my_permissions.indexOf("modify_us") != -1 + + render = (us) => + status = $scope.statusById[us.status] + + html = template({ + status: status + statuses: $scope.statusList + editable: isEditable() + }) + $el.html(html) + + $el.on "click", ".status-data", (event) -> + event.preventDefault() + event.stopPropagation() + return if not isEditable() + + $el.find(".pop-status").popover().open() + + $el.on "click", ".status", (event) -> + event.preventDefault() + event.stopPropagation() + return if not isEditable() + + target = angular.element(event.currentTarget) + + $.fn.popover().closeAll() + + us = $model.$modelValue.clone() + us.status = target.data("status-id") + $model.$setViewValue(us) + + $scope.$apply() + + onSuccess = -> + $confirm.notify("success") + $rootScope.$broadcast("history:reload") + $loading.finish($el.find(".level-name")) + + onError = -> + $confirm.notify("error") + us.revert() + $model.$setViewValue(us) + $loading.finish($el.find(".level-name")) + + $loading.start($el.find(".level-name")) + $repo.save($model.$modelValue).then(onSuccess, onError) + + $scope.$watch $attrs.ngModel, (us) -> + render(us) if us + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + require: "ngModel" + } + +module.directive("tgUsStatusButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", + UsStatusButtonDirective]) + + +############################################################################# +## User story team requirements button directive +############################################################################# + +UsTeamRequirementButtonDirective = ($rootscope, $tgrepo, $confirm, $loading) -> + template = _.template(""" + + + """) #TODO: i18n + + link = ($scope, $el, $attrs, $model) -> + canEdit = -> + return $scope.project.my_permissions.indexOf("modify_us") != -1 + + render = (us) -> + if not canEdit() and not us.team_requirement + $el.html("") + return + + ctx = { + canEdit: canEdit() + isRequired: us.team_requirement + } + html = template(ctx) + $el.html(html) + + $el.on "click", ".team-requirement", (event) -> + return if not canEdit() + + us = $model.$modelValue.clone() + us.team_requirement = not us.team_requirement + $model.$setViewValue(us) + + $loading.start($el.find("label")) + promise = $tgrepo.save($model.$modelValue) + promise.then => + $loading.finish($el.find("label")) + $rootscope.$broadcast("history:reload") + promise.then null, -> + $loading.finish($el.find("label")) + $confirm.notify("error") + us.revert() + $model.$setViewValue(us) + + $scope.$watch $attrs.ngModel, (us) -> + render(us) if us + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + require: "ngModel" + } + +module.directive("tgUsTeamRequirementButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", UsTeamRequirementButtonDirective]) + +############################################################################# +## User story client requirements button directive +############################################################################# + +UsClientRequirementButtonDirective = ($rootscope, $tgrepo, $confirm, $loading) -> + template = _.template(""" + + + """) #TODO: i18n + + link = ($scope, $el, $attrs, $model) -> + canEdit = -> + return $scope.project.my_permissions.indexOf("modify_us") != -1 + + render = (us) -> + if not canEdit() and not us.client_requirement + $el.html("") + return + + ctx = { + canEdit: canEdit() + isRequired: us.client_requirement + } + html = template(ctx) + $el.html(html) + + $el.on "click", ".client-requirement", (event) -> + return if not canEdit() + + us = $model.$modelValue.clone() + us.client_requirement = not us.client_requirement + $model.$setViewValue(us) + + $loading.start($el.find("label")) + promise = $tgrepo.save($model.$modelValue) + promise.then => + $loading.finish($el.find("label")) + $rootscope.$broadcast("history:reload") + promise.then null, -> + $loading.finish($el.find("label")) + $confirm.notify("error") + us.revert() + $model.$setViewValue(us) + + $scope.$watch $attrs.ngModel, (us) -> + render(us) if us + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + require: "ngModel" + } + +module.directive("tgUsClientRequirementButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", + UsClientRequirementButtonDirective]) diff --git a/app/coffee/modules/wiki/main.coffee b/app/coffee/modules/wiki/main.coffee index de58d10e..354822ef 100644 --- a/app/coffee/modules/wiki/main.coffee +++ b/app/coffee/modules/wiki/main.coffee @@ -38,6 +38,7 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin) "$scope", "$rootScope", "$tgRepo", + "$tgModel", "$tgConfirm", "$tgResources", "$routeParams", @@ -51,7 +52,7 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin) "tgLoader" ] - constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, + constructor: (@scope, @rootscope, @repo, @model, @confirm, @rs, @params, @q, @location, @filter, @log, @appTitle, @navUrls, @analytics, tgLoader) -> @scope.projectSlug = @params.pslug @scope.wikiSlug = @params.slug @@ -80,7 +81,15 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.wiki = wiki return wiki - @scope.wiki = {content: ""} + if @scope.project.my_permissions.indexOf("add_wiki_page") == -1 + return null + + data = { + project: @scope.projectId + slug: @scope.wikiSlug + content: "" + } + @scope.wiki = @model.make_model("wiki", data) return @scope.wiki loadWikiLinks: -> @@ -109,28 +118,13 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.wikiId = data.wikipage return prom.then null, (xhr) => - ctx = {project: @params.pslug, slug: @params.slug} - @location.path(@navUrls.resolve("project-wiki-page-edit", ctx)) + @scope.wikiId = null return promise.then(=> @.loadProject()) .then(=> @.loadUsersAndRoles()) .then(=> @q.all([@.loadWikiLinks(), @.loadWiki()])) - edit: -> - ctx = { - project: @scope.projectSlug - slug: @scope.wikiSlug - } - @location.path(@navUrls.resolve("project-wiki-page-edit", ctx)) - - cancel: -> - ctx = { - project: @scope.projectSlug - slug: @scope.wikiSlug - } - @location.path(@navUrls.resolve("project-wiki-page", ctx)) - delete: -> # TODO: i18n title = "Delete Wiki Page" @@ -151,95 +145,181 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin) module.controller("WikiDetailController", WikiDetailController) -############################################################################# -## Wiki Edit Controller -############################################################################# - -class WikiEditController extends WikiDetailController - save: debounce 2000, -> - onSuccess = => - ctx = { - project: @scope.projectSlug - slug: @scope.wiki.slug - } - @location.path(@navUrls.resolve("project-wiki-page", ctx)) - @confirm.notify("success") - - onError = => - @confirm.notify("error") - - if @scope.wiki.id - @repo.save(@scope.wiki).then onSuccess, onError - else - @analytics.trackEvent("wikipage", "create", "create wiki page", 1) - @scope.wiki.project = @scope.projectId - @scope.wiki.slug = @scope.wikiSlug - @repo.create("wiki", @scope.wiki).then onSuccess, onError - -module.controller("WikiEditController", WikiEditController) - ############################################################################# -## Wiki Main Directive +## Wiki Summary Directive ############################################################################# -WikiDirective = ($tgrepo, $log, $location, $confirm) -> - link = ($scope, $el, $attrs) -> - $ctrl = $el.controller() - - return {link:link} - -module.directive("tgWikiDetail", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm", WikiDirective]) - - -############################################################################# -## Wiki Edit Main Directive -############################################################################# - -WikiEditDirective = ($tgrepo, $log, $location, $confirm) -> - link = ($scope, $el, $attrs) -> - $ctrl = $el.controller() - - return {link:link} - -module.directive("tgWikiEdit", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm", WikiEditDirective]) - - -############################################################################# -## Wiki User Info Directive -############################################################################# - -WikiUserInfoDirective = ($log) -> +WikiSummaryDirective = ($log) -> template = _.template(""" -
- <%- name %> -
- last modification - <%- name %> +
    +
  • + <%- totalEditions %> + times
    edited
    +
  • +
  • + <%- lastModifiedDate %> + last
    edit
    +
  • +
  • +
    + <%- user.name %> +
    + last modification + <%- user.name %> +
  • +
""") - link = ($scope, $el, $attrs) -> - if not $attrs.ngModel? - return $log.error "WikiUserDirective: no ng-model attr is defined" - + link = ($scope, $el, $attrs, $model) -> render = (wiki) -> if not $scope.usersById? - $log.error "WikiUserDirective requires userById set in scope." + $log.error "WikiSummaryDirective requires userById set in scope." else user = $scope.usersById[wiki.last_modifier] - if user is undefined - ctx = {name: "unknown", imgurl: "/images/unnamed.png"} - else - ctx = {name: user.full_name_display, imgurl: user.photo} + if user is undefined + user = {name: "unknown", imgUrl: "/images/unnamed.png"} + else + user = {name: user.full_name_display, imgUrl: user.photo} + + ctx = { + totalEditions: wiki.editions + lastModifiedDate: moment(wiki.modified_date).format("DD MMM YYYY HH:mm") + user: user + } html = template(ctx) $el.html(html) - bindOnce($scope, $attrs.ngModel, render) + $scope.$watch $attrs.ngModel, (wikiPage) -> + return if not wikiPage + render(wikiPage) + + $scope.$on "$destroy", -> + $el.off() return { link: link - restrict: "AE" + restrict: "EA" + require: "ngModel" } -module.directive("tgWikiUserInfo", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm", WikiUserInfoDirective]) +module.directive("tgWikiSummary", ["$log", WikiSummaryDirective]) + + +############################################################################# +## Editable Wiki Content Directive +############################################################################# + +EditableWikiContentDirective = ($window, $document, $repo, $confirm, $loading, $location, $navUrls, + $analytics) -> + template = """ +
+
+ +
+ + """ # TODO: i18n + + link = ($scope, $el, $attrs, $model) -> + isEditable = -> + return $scope.project.my_permissions.indexOf("modify_wiki_page") != -1 + + switchToEditMode = -> + $el.find('.edit-wiki-content').show() + $el.find('.view-wiki-content').hide() + $el.find('textarea').focus() + + switchToReadMode = -> + $el.find('.edit-wiki-content').hide() + $el.find('.view-wiki-content').show() + + disableEdition = -> + $el.find(".view-wiki-content .edit").remove() + $el.find(".edit-wiki-content").remove() + + cancelEdition = -> + if $scope.wiki.id + $scope.wiki.revert() + switchToReadMode() + else + ctx = {project: $scope.projectSlug} + $location.path($navUrls.resolve("project-wiki", ctx)) + + getSelectedText = -> + if $window.getSelection + return $window.getSelection().toString() + else if $document.selection + return $document.selection.createRange().text + return null + + $el.on "mouseup", ".view-wiki-content", (event) -> + # We want to dettect the a inside the div so we use the target and + # not the currentTarget + target = angular.element(event.target) + return if not isEditable() + return if target.is('a') + return if getSelectedText() + switchToEditMode() + + $el.on "click", ".save", debounce 2000, -> + onSuccess = (wikiPage) -> + if not $scope.wiki.id? + $analytics.trackEvent("wikipage", "create", "create wiki page", 1) + + $scope.wiki = wikiPage + $model.setModelValue = $scope.wiki + $confirm.notify("success") + switchToReadMode() + + onError = -> + $confirm.notify("error") + + $loading.start($el.find('.save-container')) + if $scope.wiki.id? + promise = $repo.save($scope.wiki).then(onSuccess, onError) + else + promise = $repo.create("wiki", $scope.wiki).then(onSuccess, onError) + promise.finally -> + $loading.finish($el.find('.save-container')) + + $el.on "click", ".cancel", -> + cancelEdition() + + $el.on "keyup", "textarea", -> + if event.keyCode == 27 + cancelEdition() + + $scope.$watch $attrs.ngModel, (wikiPage) -> + return if not wikiPage + $scope.wiki = wikiPage + + if isEditable() + $el.addClass('editable') + if not wikiPage.id? + switchToEditMode() + else + disableEdition() + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + require: "ngModel" + template: template + } + +module.directive("tgEditableWikiContent", ["$window", "$document", "$tgRepo", "$tgConfirm", "$tgLoading", + "$tgLocation", "$tgNavUrls", "$tgAnalytics", + EditableWikiContentDirective]) diff --git a/app/images/markitup/bold.png b/app/images/markitup/bold.png old mode 100755 new mode 100644 index 889ae80e..714fd12b Binary files a/app/images/markitup/bold.png and b/app/images/markitup/bold.png differ diff --git a/app/images/markitup/code.png b/app/images/markitup/code.png index 63fe6cef..a4156e2b 100644 Binary files a/app/images/markitup/code.png and b/app/images/markitup/code.png differ diff --git a/app/images/markitup/italic.png b/app/images/markitup/italic.png old mode 100755 new mode 100644 index 8482ac8c..3793aa56 Binary files a/app/images/markitup/italic.png and b/app/images/markitup/italic.png differ diff --git a/app/images/markitup/link.png b/app/images/markitup/link.png index 25eacb7c..9a610fb5 100755 Binary files a/app/images/markitup/link.png and b/app/images/markitup/link.png differ diff --git a/app/images/markitup/list-bullet.png b/app/images/markitup/list-bullet.png old mode 100755 new mode 100644 index 4a8672bd..1b45081e Binary files a/app/images/markitup/list-bullet.png and b/app/images/markitup/list-bullet.png differ diff --git a/app/images/markitup/list-numeric.png b/app/images/markitup/list-numeric.png index 33b0b8df..e01ac608 100755 Binary files a/app/images/markitup/list-numeric.png and b/app/images/markitup/list-numeric.png differ diff --git a/app/images/markitup/picture.png b/app/images/markitup/picture.png old mode 100755 new mode 100644 index 4a158fef..5e638e1d Binary files a/app/images/markitup/picture.png and b/app/images/markitup/picture.png differ diff --git a/app/images/markitup/preview.png b/app/images/markitup/preview.png index a9925a06..51ae6c3e 100755 Binary files a/app/images/markitup/preview.png and b/app/images/markitup/preview.png differ diff --git a/app/images/markitup/quotes.png b/app/images/markitup/quotes.png index e54ebeba..55620b63 100644 Binary files a/app/images/markitup/quotes.png and b/app/images/markitup/quotes.png differ diff --git a/app/images/markitup/stroke.png b/app/images/markitup/stroke.png old mode 100755 new mode 100644 index 612058a7..1b3971f1 Binary files a/app/images/markitup/stroke.png and b/app/images/markitup/stroke.png differ diff --git a/app/partials/issues-detail-edit.jade b/app/partials/issues-detail-edit.jade deleted file mode 100644 index 4cc4db4c..00000000 --- a/app/partials/issues-detail-edit.jade +++ /dev/null @@ -1,47 +0,0 @@ -extends dummy-layout - -block head - title Taiga Your agile, free, and open source project management tool - -block content - form.wrapper(tg-issue-detail, ng-controller="IssueDetailController as ctrl", - ng-init="section='issues'") - div.main.us-detail - div.us-detail-header.header-with-actions - include views/components/mainTitle - .action-buttons - a.button.button-green.save-issue(href="", title="Save") Save - a.button.button-red.cancel(tg-nav="project-issues-detail:project=project.slug, ref=issue.ref", href="", title="Cancel") Cancel - - section.us-story-main-data - div.us-title(ng-class="{blocked: issue.is_blocked}") - div.us-edit-name-inner - span.us-number(tg-bo-ref="issue.ref") - input(type="text", ng-model="issue.subject", data-required="true", data-maxlength="500") - p.block-desc-container(ng-show="issue.is_blocked") - span.block-description-title Blocked - span.block-description(tg-bind-html="issue.blocked_note || 'This issue is blocked'") - a.unblock(ng-click="ctrl.unblock()", href="", title="Unblock issue") Unblock - - div(tg-tag-line, editable="true", ng-model="issue.tags") - - section.us-content - textarea(placeholder="Write a description of your issue", ng-model="issue.description", tg-markitup) - - tg-attachments(ng-model="issue", type="issue") - tg-history(ng-model="issue", type="issue", mode="edit") - - sidebar.menu-secondary.sidebar - section.us-status(tg-issue-status, ng-model="issue", editable="true") - section.us-assigned-to(tg-assigned-to, ng-model="issue", editable="true") - section.watchers(tg-watchers, ng-model="issue", editable="true") - - section.us-detail-settings - a.button.button-gray.clickable(title="Click to block the issue", ng-show="!issue.is_blocked", ng-click="ctrl.block()") Block - a.button.button-red(title="Click to delete the issue", tg-check-permission="delete_issue", ng-click="ctrl.delete()", href="") Delete - - div.lightbox.lightbox-block.hidden(tg-lb-block, title="Blocking issue", ng-model="issue") - - div.lightbox.lightbox-select-user(tg-lb-assignedto) - - div.lightbox.lightbox-select-user(tg-lb-watchers) diff --git a/app/partials/issues-detail.jade b/app/partials/issues-detail.jade index 15075c3a..c4cdfb72 100644 --- a/app/partials/issues-detail.jade +++ b/app/partials/issues-detail.jade @@ -4,19 +4,17 @@ block head title Taiga Your agile, free, and open source project management tool block content - div.wrapper(tg-issue-detail, ng-controller="IssueDetailController as ctrl", + div.wrapper(ng-controller="IssueDetailController as ctrl", ng-init="section='issues'") div.main.us-detail div.us-detail-header.header-with-actions include views/components/mainTitle - .action-buttons - a.button.button-green(tg-check-permission="modify_issue", href="", title="Edit", tg-nav="project-issues-detail-edit:project=project.slug,ref=issue.ref") Edit section.us-story-main-data div.us-title(ng-class="{blocked: issue.is_blocked}") h2.us-title-text span.us-number(tg-bo-ref="issue.ref") - span.us-name(ng-bind="issue.subject") + span.us-name(tg-editable-subject, ng-model="issue", required-perm="modify_issue") p.us-related-task(ng-if="issue.generated_user_stories") This issue has been promoted to US: a(ng-repeat="us in issue.generated_user_stories", @@ -30,20 +28,40 @@ block content span.block-description(tg-bind-html="issue.blocked_note || 'This issue is blocked'") div.issue-nav - a.icon.icon-arrow-left(ng-show="previousUrl",href="{{ previousUrl }}", title="previous issue") - a.icon.icon-arrow-right(ng-show="nextUrl", href="{{ nextUrl }}", title="next issue") + a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl", + title="previous issue") + a.icon.icon-arrow-right(ng-show="nextUrl", tg-bo-href="nextUrl", + title="next issue") - div(tg-tag-line, ng-model="issue.tags", ng-show="issue.tags") + div.tags-block(tg-tag-line, ng-model="issue", required-perm="modify_issue") - section.us-content.wysiwyg(tg-bind-html="issue.description_html") + section.duty-content.wysiwyg(tg-editable-description, ng-model="issue", required-perm="modify_issue") tg-attachments(ng-model="issue", type="issue") tg-history(ng-model="issue", type="issue") sidebar.menu-secondary.sidebar - section.us-status(tg-issue-status, ng-model="issue") - section.us-assigned-to(tg-assigned-to, ng-model="issue") - section.watchers(tg-watchers, ng-model="issue") + section.us-status + h1(tg-issue-status-display, ng-model="issue") + tg-created-by-display.us-created-by(ng-model="issue") + div.duty-data-container + div.duty-data(tg-issue-type-button, ng-model="issue") + div.duty-data(tg-issue-severity-button, ng-model="issue") + div.duty-data(tg-issue-priority-button, ng-model="issue") + div.duty-data(tg-issue-status-button, ng-model="issue") + + section.duty-assigned-to(tg-assigned-to, ng-model="issue", required-perm="modify_issue") + + section.watchers(tg-watchers, ng-model="issue", required-perm="modify_issue") section.us-detail-settings - tg-promote-issue-to-us-button(ng-model="issue") + tg-promote-issue-to-us-button(tg-check-permission="add_us", ng-model="issue") + tg-block-button(tg-check-permission="modify_issue", ng-model="issue") + tg-delete-button(tg-check-permission="delete_issue", + on-delete-title="'Delete issue'", + on-delete-go-to-url="onDeleteGoToUrl", + ng-model="issue") + + div.lightbox.lightbox-block.hidden(tg-lb-block, title="Blocking issue", ng-model="issue") + div.lightbox.lightbox-select-user(tg-lb-assignedto) + div.lightbox.lightbox-select-user(tg-lb-watchers) diff --git a/app/partials/task-detail-edit.jade b/app/partials/task-detail-edit.jade deleted file mode 100644 index a056b697..00000000 --- a/app/partials/task-detail-edit.jade +++ /dev/null @@ -1,49 +0,0 @@ -extends dummy-layout - -block head - title Taiga Your agile, free, and open source project management tool - -block content - form.wrapper(tg-task-detail, ng-controller="TaskDetailController as ctrl", - ng-init="section='backlog'") - div.main.us-detail - div.us-detail-header.header-with-actions - include views/components/mainTitle - .action-buttons - a.button.button-green.save-task(href="", title="Save") Save - a.button.button-red.cancel(tg-nav="project-tasks-detail:project=project.slug,ref=task.ref", href="", title="Cancel") Cancel - - section.us-story-main-data - div.us-title(ng-class="{blocked: task.is_blocked}") - div.us-edit-name-inner - span.us-number(tg-bo-ref="task.ref") - input(type="text", ng-model="task.subject", data-required="true", data-maxlength="500") - p.block-desc-container(ng-show="task.is_blocked") - span.block-description-title Blocked - span.block-description(tg-bind-html="task.blocked_note || 'This task is blocked'") - a.unblock(ng-click="ctrl.unblock()", href="", title="Unblock task") Unblock - - div(tg-tag-line, editable="true", ng-model="task.tags") - - section.us-content - textarea(placeholder="Write a description of your task", ng-model="task.description", tg-markitup) - - tg-attachments(ng-model="task", type="task") - tg-history(ng-model="task", type="task", mode="edit") - - sidebar.menu-secondary.sidebar - section.us-status(tg-task-status, ng-model="task", editable="true") - section.us-assigned-to(tg-assigned-to, ng-model="task", editable="true") - section.watchers(tg-watchers, ng-model="task", editable="true") - - section.us-detail-settings - fieldset(title="Feeling a bit overwhelmed by a task? Make sure others know about it by clicking on Iocaine when editing a task. It's possible to become immune to this (fictional) deadly poison by consuming small amounts over time just as it's possible to get better at what you do by occasionally taking on extra challenges!") - label.clickable.button.button-gray(for="is-iocaine", ng-class="{'active': task.is_iocaine}") Iocaine - input(ng-model="task.is_iocaine", type="checkbox", id="is-iocaine", name="is-iocaine") - - a.button.button-gray.clickable(ng-show="!task.is_blocked", ng-click="ctrl.block()") Block - a.button.button-red(tg-check-permission="delete_task", ng-click="ctrl.delete()", href="") Delete - - div.lightbox.lightbox-block.hidden(tg-lb-block, title="Blocking task", ng-model="task") - div.lightbox.lightbox-select-user.hidden(tg-lb-assignedto) - div.lightbox.lightbox-select-user.hidden(tg-lb-watchers) diff --git a/app/partials/task-detail.jade b/app/partials/task-detail.jade index 1f9727d3..052648a1 100644 --- a/app/partials/task-detail.jade +++ b/app/partials/task-detail.jade @@ -4,7 +4,7 @@ block head title Taiga Your agile, free, and open source project management tool block content - div.wrapper(tg-task-detail, ng-controller="TaskDetailController as ctrl", + div.wrapper(ng-controller="TaskDetailController as ctrl", ng-init="section='backlog'") div.main.us-detail div.us-detail-header.header-with-actions @@ -15,16 +15,12 @@ block content href="", title="Go to taskboard", tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug", ng-if="sprint && project.is_backlog_activated") Taskboard - a.button.button-green( - tg-check-permission="modify_task", href="", - title="Edit", - tg-nav="project-tasks-detail-edit:project=project.slug,ref=task.ref") Edit section.us-story-main-data div.us-title(ng-class="{blocked: task.is_blocked}") h2.us-title-text span.us-number(tg-bo-ref="task.ref") - span.us-name(ng-bind="task.subject") + span.us-name(tg-editable-subject, ng-model="task", required-perm="modify_task") h3.us-related-task This task belongs to a(tg-check-permission="view_us", href="", title="Go to user story", tg-nav="project-userstories-detail:project=project.slug, ref=us.ref", @@ -35,20 +31,37 @@ block content span.block-description-title Blocked span.block-description(tg-bind-html="task.blocked_note || 'This task is blocked'") div.issue-nav - a.icon.icon-arrow-left(ng-show="previousUrl",href="{{ previousUrl }}", title="previous task") - a.icon.icon-arrow-right(ng-show="nextUrl", href="{{ nextUrl }}", title="next task") + a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl", + title="previous task") + a.icon.icon-arrow-right(ng-show="nextUrl", tg-bo-href="nextUrl", + title="next task") - div(tg-tag-line, ng-model="task.tags", ng-show="task.tags") + div.tags-block(tg-tag-line, ng-model="task", required-perm="modify_task") - section.us-content.wysiwyg(tg-bind-html="task.description_html") + section.duty-content.wysiwyg(tg-editable-description, ng-model="task", required-perm="modify_task") tg-attachments(ng-model="task", type="task") tg-history(ng-model="task", type="task") sidebar.menu-secondary.sidebar - section.us-status(tg-task-status, ng-model="task") - section.us-assigned-to(tg-assigned-to, ng-model="task") - section.watchers(tg-watchers, ng-model="task") + section.us-status + h1(tg-task-status-display, ng-model="task") + div.us-created-by(tg-created-by-display, ng-model="task") + div.duty-data-container + div.duty-data(tg-task-status-button, ng-model="task") + + section.duty-assigned-to(tg-assigned-to, ng-model="task", required-perm="modify_task") + + section.watchers(tg-watchers, ng-model="task", required-perm="modify_task") section.us-detail-settings - span.button.button-gray(href="", ng-class="{'active': task.is_iocaine }", title="Feeling a bit overwhelmed by a task? Make sure others know about it by clicking on Iocaine when editing a task. It's possible to become immune to this (fictional) deadly poison by consuming small amounts over time just as it's possible to get better at what you do by occasionally taking on extra challenges!") Iocaine + tg-task-is-iocaine-button(ng-model="task") + tg-block-button(tg-check-permission="modify_task", ng-model="task") + tg-delete-button(tg-check-permission="delete_task", + on-delete-title="'Delete Task'", + on-delete-go-to-url="onDeleteGoToUrl", + ng-model="task") + + div.lightbox.lightbox-block.hidden(tg-lb-block, title="Blocking task", ng-model="task") + div.lightbox.lightbox-select-user(tg-lb-assignedto) + div.lightbox.lightbox-select-user(tg-lb-watchers) diff --git a/app/partials/us-detail-edit.jade b/app/partials/us-detail-edit.jade deleted file mode 100644 index 43fe65c2..00000000 --- a/app/partials/us-detail-edit.jade +++ /dev/null @@ -1,52 +0,0 @@ -extends dummy-layout - -block head - title Taiga Your agile, free, and open source project management tool - -block content - form.wrapper(tg-us-detail, ng-controller="UserStoryDetailController as ctrl", - ng-init="section='backlog'") - div.main.us-detail - div.us-detail-header.header-with-actions - include views/components/mainTitle - .action-buttons - a.button.button-green.save-us(href="", title="Save") Save - a.button.button-red.cancel(tg-nav="project-userstories-detail:project=project.slug,ref=us.ref", href="", title="Cancel") Cancel - - section.us-story-main-data - div.us-title(ng-class="{blocked: us.is_blocked}") - div.us-edit-name-inner - span.us-number(tg-bo-ref="us.ref") - input(type="text", ng-model="us.subject", data-required="true", data-maxlength="500") - p.block-desc-container(ng-show="us.is_blocked") - span.block-description-title Blocked - span.block-description(tg-bind-html="us.blocked_note || 'This US is blocked'") - a.unblock(ng-click="ctrl.unblock()", href="", title="Unblock US") Unblock - - div(tg-tag-line, editable="true", ng-model="us.tags") - - section.us-content - textarea(placeholder="Write a description of your user story", ng-model="us.description", tg-markitup) - - tg-attachments(ng-model="us", type="us") - tg-history(ng-model="us", type="us", mode="edit") - - sidebar.menu-secondary.sidebar - section.us-status(tg-us-status-detail, ng-model="us", editable="true") - section.us-assigned-to(tg-assigned-to, ng-model="us", editable="true") - section.watchers(tg-watchers, ng-model="us", editable="true") - - section.us-detail-settings - fieldset - label.clickable.button.button-gray(for="client-requirement", ng-class="{'active': us.client_requirement}") Client requirement - input(ng-model="us.client_requirement", type="checkbox", id="client-requirement", name="client-requirement") - fieldset - label.clickable.button.button-gray(for="team-requirement", ng-class="{'active': us.team_requirement}") Team requirement - input(ng-model="us.team_requirement", type="checkbox", id="team-requirement", name="team-requirement") - - a.button.button-gray.clickable(ng-show="!us.is_blocked", ng-click="ctrl.block()") Block - a.button.button-red(tg-check-permission="delete_us", ng-click="ctrl.delete()", href="") Delete - - div.lightbox.lightbox-block.hidden(tg-lb-block, title="Blocking issue", ng-model="us") - div.lightbox.lightbox-select-user.hidden(tg-lb-assignedto) - div.lightbox.lightbox-select-user.hidden(tg-lb-watchers) diff --git a/app/partials/us-detail.jade b/app/partials/us-detail.jade index 2553c7f4..93fe31ce 100644 --- a/app/partials/us-detail.jade +++ b/app/partials/us-detail.jade @@ -4,7 +4,7 @@ block head title Taiga Your agile, free, and open source project management tool block content - div.wrapper(tg-us-detail, ng-controller="UserStoryDetailController as ctrl", + div.wrapper(ng-controller="UserStoryDetailController as ctrl", ng-init="section='backlog'") div.main.us-detail div.us-detail-header.header-with-actions @@ -15,16 +15,12 @@ block content href="", title="Go to taskboard", tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug", ng-if="sprint && project.is_backlog_activated") Taskboard - a.button.button-green( - tg-check-permission="modify_us", href="", - title="Edit", - tg-nav="project-userstories-detail-edit:project=project.slug,ref=us.ref") Edit section.us-story-main-data div.us-title(ng-class="{blocked: us.is_blocked}") h2.us-title-text span.us-number(tg-bo-ref="us.ref") - span.us-name(ng-bind="us.subject") + span.us-name(tg-editable-subject, ng-model="us", required-perm="modify_us") p.us-related-task(ng-if="us.origin_issue") This US has been promoted from Issue a(tg-check-permission="view_us", href="", title="Go to issue", @@ -36,13 +32,14 @@ block content span.block-description-title Blocked span.block-description(tg-bind-html="us.blocked_note || 'This user story is blocked'") div.issue-nav - a.icon.icon-arrow-left(ng-show="previousUrl",href="{{ previousUrl }}", - title="previous user story") - a.icon.icon-arrow-right(ng-show="nextUrl", href="{{ nextUrl }}", title="next user story") + a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl", + title="previous user story") + a.icon.icon-arrow-right(ng-show="nextUrl", tg-bo-href="nextUrl", + title="next user story") - div(tg-tag-line, ng-model="us.tags", ng-show="us.tags") + div.tags-block(tg-tag-line, ng-model="us", required-perm="modify_us") - section.us-content.wysiwyg(tg-bind-html="us.description_html") + section.duty-content.wysiwyg(tg-editable-description, ng-model="us", required-perm="modify_us") include views/modules/related-tasks @@ -50,15 +47,27 @@ block content tg-history(ng-model="us", type="us") sidebar.menu-secondary.sidebar - section.us-status(tg-us-status-detail, ng-model="us") - section.us-assigned-to(tg-assigned-to, ng-model="us") - section.us-created-by(tg-created-by, ng-model="us") - section.watchers(tg-watchers, ng-model="us") + section.us-status + h1(tg-us-status-display, ng-model="us") + div.us-detail-progress-bar(tg-us-tasks-progress-display, ng-model="tasks") + tg-created-by-display.us-created-by(ng-model="us") + tg-us-estimation(ng-model="us", save-after-modify="true") + div.duty-data-container + div.duty-data(tg-us-status-button, ng-model="us") + + section.duty-assigned-to(tg-assigned-to, ng-model="us", required-perm="modify_us") + + section.watchers(tg-watchers, ng-model="us", required-perm="modify_us") section.us-detail-settings - span.button.button-gray(href="", title="Client requirement", - ng-class="{'active': us.client_requirement}") Client requirement - span.button.button-gray(href="", title="Team requirement", - ng-class="{'active': us.team_requirement}") Team requirement + tg-us-team-requirement-button(ng-model="us") + tg-us-client-requirement-button(ng-model="us") + tg-block-button(tg-check-permission="modify_us", ng-model="us") + tg-delete-button(tg-check-permission="delete_us", + on-delete-title="'Delete User Story'", + on-delete-go-to-url="onDeleteGoToUrl", + ng-model="us") + div.lightbox.lightbox-block.hidden(tg-lb-block, title="Blocking us", ng-model="us") div.lightbox.lightbox-select-user.hidden(tg-lb-assignedto) + div.lightbox.lightbox-select-user.hidden(tg-lb-watchers) diff --git a/app/partials/views/modules/lightbox-create-issue.jade b/app/partials/views/modules/lightbox-create-issue.jade index 1a7142de..1dbe545d 100644 --- a/app/partials/views/modules/lightbox-create-issue.jade +++ b/app/partials/views/modules/lightbox-create-issue.jade @@ -14,7 +14,7 @@ form select.severity(ng-model="issue.severity", ng-options="s.id as s.name for s in severityList") fieldset - div(tg-tag-line, editable="true", ng-model="issue.tags") + div.tags-block(tg-lb-tag-line, ng-model="issue.tags") fieldset textarea.description(placeholder="Description", ng-model="issue.description") diff --git a/app/partials/views/modules/lightbox-task-create-edit.jade b/app/partials/views/modules/lightbox-task-create-edit.jade index 5ff14193..96772bd8 100644 --- a/app/partials/views/modules/lightbox-task-create-edit.jade +++ b/app/partials/views/modules/lightbox-task-create-edit.jade @@ -16,7 +16,7 @@ form option(value="") Unassigned fieldset - div(tg-tag-line, editable="true", ng-model="task.tags") + div.tags-block(tg-lb-tag-line, ng-model="task.tags") fieldset textarea.description(placeholder="Type a short description", ng-model="task.description") diff --git a/app/partials/views/modules/lightbox-us-create-edit.jade b/app/partials/views/modules/lightbox-us-create-edit.jade index a916418c..15808251 100644 --- a/app/partials/views/modules/lightbox-us-create-edit.jade +++ b/app/partials/views/modules/lightbox-us-create-edit.jade @@ -8,14 +8,13 @@ form fieldset.estimation tg-us-estimation(ng-model="us") - //- Render by tg-lb-create-edit-userstory fieldset select(name="status", ng-model="us.status", ng-options="s.id as s.name for s in usStatusList", tr="placeholder:common.status") fieldset - div(tg-tag-line, editable="true", ng-model="us.tags") + div.tags-block(tg-lb-tag-line, ng-model="us.tags") fieldset textarea.description(name="description", ng-model="us.description", diff --git a/app/partials/views/modules/wiki-summary.jade b/app/partials/views/modules/wiki-summary.jade deleted file mode 100644 index bcd4bd80..00000000 --- a/app/partials/views/modules/wiki-summary.jade +++ /dev/null @@ -1,9 +0,0 @@ -div.summary.wiki-summary - ul - li - span.number(tg-bo-bind="wiki.editions") - span.description times
edited - li(ng-if="wiki.modified_date") - span.number(tg-bo-bind="wiki.modified_date|momentFormat:'DD MMM YYYY HH:mm'") - span.description last
edit - li.username-edition(tg-wiki-user-info, ng-model='wiki') diff --git a/app/partials/wiki-edit.jade b/app/partials/wiki-edit.jade deleted file mode 100644 index 2d1b04f6..00000000 --- a/app/partials/wiki-edit.jade +++ /dev/null @@ -1,24 +0,0 @@ -extends dummy-layout - -block head - title Taiga Your agile, free, and open source project management tool - -block content - div.wrapper(tg-wiki-edit, ng-controller="WikiEditController as ctrl", - ng-init="section='wiki'") - sidebar.menu-secondary.extrabar(tg-check-permission="view_wiki_links") - section.wiki-nav(tg-wiki-nav, ng-model="wikiLinks") - section.main.wiki - div.header-with-actions - h1 - span(tg-bo-bind="project.name", class="project-name-short") - span.green Wiki - span.wiki-title(tg-bo-bind='wikiSlug|unslugify') - .action-buttons - a.button.button-green.save-wiki(href="", title="Save", ng-click="ctrl.save()") Save - a.button.button-red.cancel-wiki(href="", title="CAncel", ng-click="ctrl.cancel()") Cancel - - section.wysiwyg - textarea(placeholder="Write a your wiki page", ng-model="wiki.content", tg-markitup) - - tg-attachments(ng-model="wiki", type="wiki_page", ng-if="wiki.id") diff --git a/app/partials/wiki.jade b/app/partials/wiki.jade index 27d95235..fb0a7578 100644 --- a/app/partials/wiki.jade +++ b/app/partials/wiki.jade @@ -4,25 +4,22 @@ block head title Taiga Your agile, free, and open source project management tool block content - div.wrapper(tg-wiki-detail, ng-controller="WikiDetailController as ctrl", + div.wrapper(ng-controller="WikiDetailController as ctrl", ng-init="section='wiki'") sidebar.menu-secondary.extrabar(tg-check-permission="view_wiki_links") section.wiki-nav(tg-wiki-nav, ng-model="wikiLinks") section.main.wiki - .header-with-actions + .header h1 - span(tg-bo-bind="project.name", class="project-name-short") + span(tg-bo-bind="project.name") span.green Wiki - span.wiki-title(tg-bo-bind='wiki.slug|unslugify') - .action-buttons - a.button.button-red.delete-wiki(tg-check-permission="delete_wiki_page", - href="", title="Delete", ng-click="ctrl.delete()") Delete + span.wiki-title(tg-bo-bind='wikiSlug|unslugify') - a.button.button-green.edit-wiki(tg-check-permission="modify_wiki_page", - href="", title="Edit", ng-click="ctrl.edit()") Edit + div.summary.wiki-summary(tg-wiki-summary, ng-model="wiki", ng-if="wiki.id") + section.wiki-content(tg-editable-wiki-content, ng-model="wiki") - include views/modules/wiki-summary + tg-attachments(ng-model="wiki", type="wiki_page", ng-if="wiki.id") - section.wiki-content.wysiwyg(tg-bind-html="wiki.html") - - tg-attachments(ng-model="wiki", type="wiki_page") + a.remove(href="", ng-click="ctrl.delete()", ng-if="wiki.id", title="Remove this wiki page") + span.icon.icon-delete + span Remove this wiki page diff --git a/app/styles/components/markitup.scss b/app/styles/components/markitup.scss new file mode 100644 index 00000000..eb70f415 --- /dev/null +++ b/app/styles/components/markitup.scss @@ -0,0 +1,38 @@ +.markItUpHeader { + ul { + background: $whitish; + padding: .3rem; + li { + display: inline-block; + float: none; + a { + opacity: .8; + &:hover { + @include transition(opacity .2s linear); + opacity: .3; + } + } + } + .preview-icon { + position: absolute; + right: 2.5rem; + } + } +} + +.markItUpContainer { + padding: 0; +} + +.markdown { + position: relative; +} + +.preview { + .actions { + background: $whitish; + margin-top: .5rem; + min-height: 2rem; + padding: .3rem; + } +} diff --git a/app/styles/components/tag.scss b/app/styles/components/tag.scss index 3797a2e7..41ea7899 100644 --- a/app/styles/components/tag.scss +++ b/app/styles/components/tag.scss @@ -30,10 +30,8 @@ .tags-block { .tags-container { display: inline-block; - vertical-align: middle; } input { - display: inline-block; padding: .4rem; width: 14rem; } @@ -42,7 +40,18 @@ margin: 0 .5rem .5rem 0; padding: .5rem; } - .save { - display: none; + .add-tag { + color: $gray-light; + &:hover { + color: $fresh-taiga; + } + } + .icon-plus { + @extend %large; + } + .add-tag-text { + @extend %small; } } + + diff --git a/app/styles/components/watchers.scss b/app/styles/components/watchers.scss index 63afee95..303fa7f9 100644 --- a/app/styles/components/watchers.scss +++ b/app/styles/components/watchers.scss @@ -1,5 +1,5 @@ .watchers { - margin-top: 2rem; + margin-top: 1rem; .watchers-header { border-bottom: 2px solid $gray-light; padding: .5rem; diff --git a/app/styles/layout/elements.scss b/app/styles/layout/elements.scss index 44f9c1da..c545653b 100644 --- a/app/styles/layout/elements.scss +++ b/app/styles/layout/elements.scss @@ -39,6 +39,10 @@ sup { vertical-align: middle; } +.icon-spinner { + @include animation (spin 1s linear infinite); +} + .clickable { cursor: pointer; } diff --git a/app/styles/layout/us-detail.scss b/app/styles/layout/us-detail.scss index a9c813d6..dd2c489e 100644 --- a/app/styles/layout/us-detail.scss +++ b/app/styles/layout/us-detail.scss @@ -57,6 +57,12 @@ display: flex; margin-bottom: 0; max-width: 94%; + &:hover { + .icon-edit { + @include transition(opacity .3s linear); + opacity: 1; + } + } } .us-number { @extend %xlarge; @@ -72,6 +78,17 @@ display: inline-block; line-height: 2.2rem; padding-right: 1rem; + width: 100%; + } + .icon-edit, + .icon-floppy, + .icon-spinner { + @extend %large; + color: $gray-light; + margin-left: .5rem; + } + .icon-edit { + opacity: 0; } .us-related-task { @extend %small; @@ -133,12 +150,74 @@ } } -.us-content { +.duty-content { + position: relative; + &:hover { + .view-description { + .edit { + @include transition(all .2s linear); + opacity: 1; + top: -1.5rem; + } + .editable { + background: $whitish; + cursor: pointer; + .no-description { + color: $grayer; + } + } + } + } + &.wysiwyg { + overflow: visible; + } + .no-description { + color: $gray-light; + } textarea { background: $white; height: 10rem; margin-bottom: 2rem; } + .save-container { + position: absolute; + right: 1rem; + top: .2rem; + .save { + color: $blackish; + opacity: .6; + top: 0; + } + &:hover { + @include transition(opacity .2s linear); + opacity: .3; + } + } + .edit { + color: $grayer; + } + .view-description { + .edit { + @include transition(all .2s linear); + background: $whitish; + left: 0; + opacity: 0; + padding: .2rem .5rem; + position: absolute; + top: 0; + } + } + .edit-description { + .save { + top: .4rem; + } + .edit { + @include transition(all .2s linear); + position: absolute; + right: 2.5rem; + top: .4rem; + } + } } .comment-list { @@ -230,23 +309,25 @@ } } -.issue-data { +.duty-data-container { @extend %small; - div { - @include clearfix(); - @include transition(background .2s ease-in); - background: darken($whitish, 5%); + margin-bottom: 1rem; + .duty-data { margin-bottom: .5rem; - padding: .5rem; - padding-right: 1rem; &:last-child { margin: 0; } - } - .clickable { - &:hover { + div { @include transition(background .2s ease-in); - background: darken($whitish, 10%); + background: darken($whitish, 5%); + padding: .5rem; + padding-right: 1rem; + } + .clickable { + &:hover { + @include transition(background .2s ease-in); + background: darken($whitish, 10%); + } } } .level { @@ -257,6 +338,10 @@ .level-name { color: darken($whitish, 20%); float: right; + &.loading span { + @include animation (loading .5s linear); + @include animation (spin 1s linear infinite); + } } } @@ -271,9 +356,25 @@ } .button-gray { background: $gray-light; - &:hover, + &:hover { + background: $gray-light; + } + &.editable { + &:hover { + background: $grayer; + cursor: pointer; + } + } &.active { - background: $grayer; + background: $green-taiga; + } + } + .item-block { + &.editable { + &:hover { + background: $red; + cursor: pointer; + } } } .button-red { @@ -283,7 +384,9 @@ } } label { - cursor: pointer; + &.editable { + cursor: pointer; + } +input { display: none; } diff --git a/app/styles/layout/wiki.scss b/app/styles/layout/wiki.scss index d8cfd8fb..4c0eae1e 100644 --- a/app/styles/layout/wiki.scss +++ b/app/styles/layout/wiki.scss @@ -1,3 +1,70 @@ +.wiki { + .remove { + @extend %small; + color: $gray-light; + &:hover { + span { + @include transition(color .2s linear); + color: $grayer; + } + .icon { + @include transition(color .2s linear); + color: $red; + } + } + .icon { + color: $gray-light; + margin-right: .3rem; + } + } +} + .wiki-content { margin-bottom: 2rem; + position: relative; + .view-wiki-content { + &:hover { + .wysiwyg { + background: $whitish; + cursor: pointer; + } + .edit { + @include transition(all .2s linear); + opacity: 1; + top: -1.5rem; + } + } + .edit { + @include transition(all .2s linear); + background: $whitish; + left: 0; + opacity: 0; + padding: .2rem .5rem; + position: absolute; + top: 0; + } + } + .edit-wiki-content { + .icon { + &:hover { + @include transition(all .2s linear); + color: $grayer; + opacity: .3; + } + } + .preview-icon { + position: absolute; + right: 3.5rem; + } + .action-container { + position: absolute; + right: 1rem; + top: .3rem; + } + .edit { + position: absolute; + right: 3.5rem; + top: .4rem; + } + } } diff --git a/app/styles/modules/common/assigned-to.scss b/app/styles/modules/common/assigned-to.scss index 4b7be68b..9fc78665 100644 --- a/app/styles/modules/common/assigned-to.scss +++ b/app/styles/modules/common/assigned-to.scss @@ -1,7 +1,26 @@ -.us-assigned-to { +.duty-assigned-to { @include table-flex(); margin-top: 1rem; position: relative; + &:hover { + .assigned-to { + .icon-delete { + @include transition (opacity .3s linear); + opacity: 1; + } + } + } + &.loading { + width: 100%; + span { + font-size: 30px; + padding: 20px 0; + text-align: center; + width: 100%; + @include animation (loading .5s linear); + @include animation (spin 1s linear infinite); + } + } .user-avatar { @include table-flex-child(1, 0); img { @@ -22,13 +41,21 @@ @extend %large; color: $green-taiga; cursor: default; + line-height: 1.5rem; &.editable { cursor: pointer; } + .icon { + vertical-align: top; + } + } + .assigned-name { + @include ellipsis(80%); + display: inline-block; } .icon-delete { color: $gray-light; - opacity: 1; + opacity: 0; position: absolute; right: 0; top: 0; diff --git a/app/styles/modules/common/attachments.scss b/app/styles/modules/common/attachments.scss index b49b67e0..92050f74 100644 --- a/app/styles/modules/common/attachments.scss +++ b/app/styles/modules/common/attachments.scss @@ -98,7 +98,7 @@ } .icon-edit, .icon-floppy { - right: 4rem; + right: 3.5rem; } .icon-delete { right: 2rem; diff --git a/app/styles/modules/common/history.scss b/app/styles/modules/common/history.scss index 0e5e68d2..920d4582 100644 --- a/app/styles/modules/common/history.scss +++ b/app/styles/modules/common/history.scss @@ -1,6 +1,5 @@ .history { margin-bottom: 1rem; - padding: 0 1rem; } .changes-title { display: block; @@ -66,7 +65,22 @@ } .add-comment { @include clearfix; + &.active { + .button-green { + display: block; + } + textarea { + @include transition(height .3s ease-in); + height: 6rem; + } + .preview-icon { + opacity: 1; + position: absolute; + right: 1rem; + } + } textarea { + background: $white; float: left; height: 41px; margin-bottom: .5rem; @@ -79,14 +93,13 @@ .button-green { display: none; } - &.active { - .button-green { - display: block; - } - textarea { - @include transition(height .3s ease-in); - height: 6rem; - } + .edit, + .preview-icon { + position: absolute; + right: 1rem; + } + .preview-icon { + opacity: 0; } } a.show-more-comments { diff --git a/app/styles/modules/common/related-tasks.scss b/app/styles/modules/common/related-tasks.scss index 37936812..bf4dfb8b 100644 --- a/app/styles/modules/common/related-tasks.scss +++ b/app/styles/modules/common/related-tasks.scss @@ -61,6 +61,17 @@ .status { position: relative; text-align: left; + &:hover { + .icon { + @include transition (opacity .2s ease-in); + opacity: 1; + } + } + .not-clickable { + &:hover { + color: $grayer; + } + } .popover { a { text-align: left; @@ -73,6 +84,7 @@ .icon { color: $gray-light; margin-left: .2rem; + opacity: 0; } } .pop-status { @@ -160,8 +172,10 @@ text-align: left; } .task-assignedto { - cursor: pointer; position: relative; + &.editable { + cursor: pointer; + } &:hover { .icon { @include transition(opacity .3s linear); diff --git a/main-sass.js b/main-sass.js index 5f77ff33..6c1b7bdb 100644 --- a/main-sass.js +++ b/main-sass.js @@ -53,6 +53,8 @@ exports.files = function () { 'components/spinner', 'components/help-notion-button', 'components/beta', + 'components/markitup', + //################################################# // Modules