diff --git a/CHANGELOG.md b/CHANGELOG.md index 285860e1..ed9988bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,24 @@ # Changelog # -## 1.2.0 (Unreleased) +## 1.2.0 Picea obovata (2014-11-04) ### Features + +- US/Task/Issue visualization and edition refactor. Now only one view for both. - Multiple User stories Drag & Drop in the backlog. -- Added beta ribbon. +- Add visual difference to closed USs in backlog panel. +- Show crerated date of attachments in the hover of the filename. +- Show info about maximun size allowed for avatar and attachments files. +- Add beta ribbon. +- Support for custom text when inviting users. ### Misc -- Lots of small and not so small bugfixes + +- TAIGA loves Movember! The logo has a beautiful mustache this month. +- Lots of small and not so small bugfixes. -## 1.1.0 (2014-10-13) - -### Misc ### - -- Fix bug related to stange behavior of browser autofill and angularjs on login page. -- Fix bug on userstories ordering on sprints. -- Fix bug of projects list visualization on project nav on first page loading. +## 1.1.0 Alnus maximowiczii (2014-10-13) ### Features ### @@ -24,14 +26,21 @@ - Changed configuration format from coffeescript file to json. - Add builtin analytics support. -## 1.0.0 (2014-10-07) - ### Misc ### -- Lots of small and not so small bugfixes +- Fix bug related to stange behavior of browser autofill and angularjs on login page. +- Fix bug on userstories ordering on sprints. +- Fix bug of projects list visualization on project nav on first page loading. + + +## 1.0.0 (2014-10-07) ### Features ### - Redesign for taskboard and backlog summaries - Allow feedback for users from the platform - Real time changes for backlog, taskboard, kanban and issues + +### Misc ### + +- Lots of small and not so small bugfixes diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee index e007c11d..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", @@ -136,6 +128,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven {templateUrl: "/partials/error.html"}) $routeProvider.when("/not-found", {templateUrl: "/partials/not-found.html"}) + $routeProvider.when("/permission-denied", + {templateUrl: "/partials/permission-denied.html"}) $routeProvider.otherwise({redirectTo: '/not-found'}) $locationProvider.html5Mode(true) diff --git a/app/coffee/classes.coffee b/app/coffee/classes.coffee index 14ffc5ec..7e995c09 100644 --- a/app/coffee/classes.coffee +++ b/app/coffee/classes.coffee @@ -22,6 +22,16 @@ class TaigaBase class TaigaService extends TaigaBase class TaigaController extends TaigaBase + onInitialDataError: (xhr) => + if xhr + if xhr.status == 404 + @location.path(@navUrls.resolve("not-found")) + @location.replace() + else if xhr.status == 403 + @location.path(@navUrls.resolve("permission-denied")) + @location.replace() + + return @q.reject(xhr) @.taiga.Base = TaigaBase @.taiga.Service = TaigaService diff --git a/app/coffee/modules/admin/lightboxes.coffee b/app/coffee/modules/admin/lightboxes.coffee index 2cf83e74..bf30904a 100644 --- a/app/coffee/modules/admin/lightboxes.coffee +++ b/app/coffee/modules/admin/lightboxes.coffee @@ -24,13 +24,19 @@ debounce = @.taiga.debounce module = angular.module("taigaKanban") -MAX_MEMBERSHIP_FIELDSETS = 6 +MAX_MEMBERSHIP_FIELDSETS = 4 ############################################################################# ## Create Members Lightbox Directive ############################################################################# CreateMembersDirective = ($rs, $rootScope, $confirm, lightboxService) -> + extraTextTemplate = """ +
+ +
+ """ + template = _.template("""
@@ -53,11 +59,14 @@ CreateMembersDirective = ($rs, $rootScope, $confirm, lightboxService) -> return template(ctx) resetForm = -> - $el.find("form > .add-member-wrapper").remove() + $el.find("form textarea").remove("") + $el.find("form .add-member-wrapper").remove() + + invitations = $el.find(".add-member-forms") + invitations.html(extraTextTemplate) - title = $el.find("h2") fieldSet = createFieldSet() - title.after(fieldSet) + invitations.prepend(fieldSet) $scope.$on "membersform:new", -> resetForm() @@ -91,7 +100,7 @@ CreateMembersDirective = ($rs, $rootScope, $confirm, lightboxService) -> fieldSet.after(newFieldSet) if $el.find(".add-member-wrapper").length == MAX_MEMBERSHIP_FIELDSETS - $el.find("fieldset:last > a").removeClass("icon-plus add-fieldset") + $el.find(".add-member-wrapper fieldset:last > a").removeClass("icon-plus add-fieldset") .addClass("icon-delete delete-fieldset") $el.on "click", ".button-green", debounce 2000, (event) -> @@ -112,12 +121,10 @@ CreateMembersDirective = ($rs, $rootScope, $confirm, lightboxService) -> #checksley find new fields form.destroy() form.initialize() - if not form.validate() return - memberWrappers = $el.find("form > .add-member-wrapper") - + memberWrappers = $el.find("form .add-member-wrapper") memberWrappers = _.filter memberWrappers, (mw) -> angular.element(mw).find("input").hasClass('checksley-ok') @@ -132,7 +139,9 @@ CreateMembersDirective = ($rs, $rootScope, $confirm, lightboxService) -> } if invitations.length - $rs.memberships.bulkCreateMemberships($scope.project.id, invitations).then(onSuccess, onError) + invitation_extra_text = $el.find("form textarea").val() + + $rs.memberships.bulkCreateMemberships($scope.project.id, invitations, invitation_extra_text).then(onSuccess, onError) return {link: link} diff --git a/app/coffee/modules/admin/memberships.coffee b/app/coffee/modules/admin/memberships.coffee index a7435ae9..c337fbbe 100644 --- a/app/coffee/modules/admin/memberships.coffee +++ b/app/coffee/modules/admin/memberships.coffee @@ -58,11 +58,7 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai promise.then => @appTitle.set("Membership - " + @scope.project.name) - promise.then null, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) + promise.then null, @.onInitialDataError.bind(@) @scope.$on "membersform:new:success", => @.loadMembers() @@ -119,11 +115,11 @@ paginatorTemplate = """ <% } %> <% _.each(pages, function(item) { %> -
  • +
  • <% if (item.type === "page") { %> - <%= item.num %> + <%- item.num %> <% } else if (item.type === "page-active") { %> - <%= item.num %> + <%- item.num %> <% } else { %> ... <% } %> @@ -237,7 +233,7 @@ module.directive("tgMemberships", MembershipsDirective) MembershipsRowAvatarDirective = ($log) -> template = _.template("""
    - <%- full_name %> + <%- full_name %>
    <%- full_name %> @@ -432,18 +428,18 @@ MembershipsRowActionsDirective = ($log, $repo, $rs, $confirm) -> event.preventDefault() title = "Delete member" # TODO: i18n - subtitle = if member.user then member.full_name else "the invitation to #{member.email}" # TODO: i18n + message = if member.user then member.full_name else "the invitation to #{member.email}" # TODO: i18n - $confirm.ask(title, subtitle).then (finish) -> + $confirm.askOnDelete(title, message).then (finish) -> onSuccess = -> finish() $ctrl.loadMembers() - $confirm.notify("success", null, "We've deleted #{subtitle}.") # TODO: i18n + $confirm.notify("success", null, "We've deleted #{message}.") # TODO: i18n onError = -> finish(false) # TODO: i18in - $confirm.notify("error", null, "We have not been able to delete #{subtitle}.") + $confirm.notify("error", null, "We have not been able to delete #{message}.") $repo.remove(member).then(onSuccess, onError) diff --git a/app/coffee/modules/admin/project-profile.coffee b/app/coffee/modules/admin/project-profile.coffee index 14b3087d..3c1b3505 100644 --- a/app/coffee/modules/admin/project-profile.coffee +++ b/app/coffee/modules/admin/project-profile.coffee @@ -57,11 +57,7 @@ class ProjectProfileController extends mixOf(taiga.Controller, taiga.PageMixin) promise.then => @appTitle.set("Project profile - " + @scope.sectionName + " - " + @scope.project.name) - promise.then null, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) + promise.then null, @.onInitialDataError.bind(@) @scope.$on "project:loaded", => @appTitle.set("Project profile - " + @scope.sectionName + " - " + @scope.project.name) @@ -96,7 +92,7 @@ module.controller("ProjectProfileController", ProjectProfileController) ## Project Profile Directive ############################################################################# -ProjectProfileDirective = ($repo, $confirm, $loading) -> +ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location) -> link = ($scope, $el, $attrs) -> form = $el.find("form").checksley({"onlyOneErrorElement": true}) submit = (target) => @@ -108,6 +104,8 @@ ProjectProfileDirective = ($repo, $confirm, $loading) -> promise.then -> $loading.finish(target) $confirm.notify("success") + newUrl = $navurls.resolve("project-admin-project-profile-details", {project: $scope.project.slug}) + $location.path(newUrl) $scope.$emit("project:loaded", $scope.project) promise.then null, (data) -> @@ -132,7 +130,7 @@ ProjectProfileDirective = ($repo, $confirm, $loading) -> return {link:link} -module.directive("tgProjectProfile", ["$tgRepo", "$tgConfirm", "$tgLoading", ProjectProfileDirective]) +module.directive("tgProjectProfile", ["$tgRepo", "$tgConfirm", "$tgLoading", "$tgNavUrls", "$tgLocation", ProjectProfileDirective]) ############################################################################# diff --git a/app/coffee/modules/admin/project-values.coffee b/app/coffee/modules/admin/project-values.coffee index e86ee0b1..fdadaf35 100644 --- a/app/coffee/modules/admin/project-values.coffee +++ b/app/coffee/modules/admin/project-values.coffee @@ -57,11 +57,7 @@ class ProjectValuesController extends mixOf(taiga.Controller, taiga.PageMixin) promise.then () => @appTitle.set("Project values - " + @scope.sectionName + " - " + @scope.project.name) - promise.then null, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) + promise.then null, @.onInitialDataError.bind(@) @scope.$on("admin:project-values:move", @.moveValue) @@ -170,8 +166,8 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) -> promise = $repo.save(value) promise.then => row = target.parents(".row.table-main") - row.hide() - row.siblings(".visualization").css("display": "flex") + row.addClass("hidden") + row.siblings(".visualization").removeClass('hidden') promise.then null, (data) -> $confirm.notify("error") @@ -181,9 +177,9 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) -> row = target.parents(".row.table-main") value = target.scope().value $scope.$apply -> - row.hide() + row.addClass("hidden") value.revert() - row.siblings(".visualization").css("display": "flex") + row.siblings(".visualization").removeClass('hidden') $el.on "submit", "form", (event) -> event.preventDefault() @@ -195,7 +191,7 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) -> $el.on "click", ".show-add-new", (event) -> event.preventDefault() - $el.find(".new-value").css('display': 'flex') + $el.find(".new-value").removeClass('hidden') goToBottomList(true) @@ -231,9 +227,10 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) -> target = angular.element(event.currentTarget) row = target.parents(".row.table-main") - row.hide() + row.addClass("hidden") + editionRow = row.siblings(".edition") - editionRow.css("display": "flex") + editionRow.removeClass('hidden') editionRow.find('input:visible').first().focus().select() $el.on "keyup", ".edition input", (event) -> @@ -264,12 +261,13 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) -> choices[option.id] = option.name #TODO: i18n - title = "Delete" + title = "Delete value" subtitle = value.name + replacement = "All items with this value will be changed to" if _.keys(choices).length == 0 return $confirm.error("You can't delete all values.") - return $confirm.askChoice(title, subtitle, choices).then (response) -> + return $confirm.askChoice(title, subtitle, choices, replacement).then (response) -> onSucces = -> $ctrl.loadValues().finally -> response.finish() diff --git a/app/coffee/modules/admin/roles.coffee b/app/coffee/modules/admin/roles.coffee index b3c64464..cf2d6bd7 100644 --- a/app/coffee/modules/admin/roles.coffee +++ b/app/coffee/modules/admin/roles.coffee @@ -58,11 +58,7 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil promise.then () => @appTitle.set("Roles - " + @scope.project.name) - promise.then null, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) + promise.then null, @.onInitialDataError.bind(@) loadProject: -> return @rs.projects.get(@scope.projectId).then (project) => @@ -93,8 +89,10 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil delete: -> # TODO: i18n - title = "Delete Role" + title = "Delete Role" # TODO: i18n subtitle = @scope.role.name + replacement = "All the users with this role will be moved to" # TODO: i18n + warning = "Be careful, all role estimations will be removed" # TODO: i18n choices = {} for role in @scope.roles @@ -102,9 +100,9 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil choices[role.id] = role.name if _.keys(choices).length == 0 - return @confirm.error("You can't delete all values.") + return @confirm.error("You can't delete all values.") # TODO: i18n - return @confirm.askChoice(title, subtitle, choices).then (response) => + return @confirm.askChoice(title, subtitle, choices, replacement, warning).then (response) => promise = @repo.remove(@scope.role, {moveTo: response.selected}) promise.then => @.loadProject() @@ -127,6 +125,47 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil module.controller("RolesController", RolesController) +EditRoleDirective = ($repo, $confirm) -> + link = ($scope, $el, $attrs) -> + toggleView = -> + $el.find('.total').toggle() + $el.find('.edit-role').toggle() + + submit = () -> + $scope.role.name = $el.find("input").val() + + promise = $repo.save($scope.role) + + promise.then -> + $confirm.notify("success") + + promise.then null, (data) -> + $confirm.notify("error") + + toggleView() + + $el.on "click", "a.icon-edit", -> + toggleView() + $el.find("input").focus() + + $el.on "click", "a.save", submit + + $el.on "keyup", "input", -> + if event.keyCode == 13 # Enter key + submit() + else if event.keyCode == 27 # ESC key + toggleView() + + $scope.$on "role:changed", -> + if $el.find('.edit-role').is(":visible") + toggleView() + + $scope.$on "$destroy", -> + $el.off() + + return {link:link} + +module.directive("tgEditRole", ["$tgRepo", "$tgConfirm", EditRoleDirective]) RolesDirective = -> link = ($scope, $el, $attrs) -> diff --git a/app/coffee/modules/auth.coffee b/app/coffee/modules/auth.coffee index 939d290e..42a8ce12 100644 --- a/app/coffee/modules/auth.coffee +++ b/app/coffee/modules/auth.coffee @@ -20,6 +20,7 @@ ### taiga = @.taiga +debounce = @.taiga.debounce module = angular.module("taigaAuth", ["taigaResources"]) @@ -156,7 +157,7 @@ PublicRegisterMessageDirective = ($config, $navUrls) -> template = _.template(""" """) templateFn = -> @@ -225,7 +226,7 @@ RegisterDirective = ($auth, $confirm, $location, $navUrls, $config, $analytics) $location.replace() $scope.data = {} - form = $el.find("form").checksley() + form = $el.find("form").checksley({onlyOneErrorElement: true}) onSuccessSubmit = (response) -> $analytics.trackEvent("auth", "register", "user registration", 1) @@ -238,7 +239,7 @@ RegisterDirective = ($auth, $confirm, $location, $navUrls, $config, $analytics) form.setErrors(response.data) - submit = -> + submit = debounce 2000, => if not form.validate() return @@ -278,7 +279,7 @@ ForgotPasswordDirective = ($auth, $confirm, $location, $navUrls) -> $confirm.notify("light-error", "According to our Oompa Loompas, your are not registered yet.") #TODO: i18n - submit = -> + submit = debounce 2000, => if not form.validate() return @@ -323,7 +324,7 @@ ChangePasswordFromRecoveryDirective = ($auth, $confirm, $location, $params, $nav $confirm.notify("light-error", "One of our Oompa Loompas say '#{response.data._error_message}'.") #TODO: i18n - submit = -> + submit = debounce 2000, => if not form.validate() return @@ -362,7 +363,7 @@ InvitationDirective = ($auth, $confirm, $location, $params, $navUrls, $analytics # Login form $scope.dataLogin = {token: token} - loginForm = $el.find("form.login-form").checksley() + loginForm = $el.find("form.login-form").checksley({onlyOneErrorElement: true}) onSuccessSubmitLogin = (response) -> $analytics.trackEvent("auth", "invitationAccept", "invitation accept with existing user", 1) @@ -374,7 +375,7 @@ InvitationDirective = ($auth, $confirm, $location, $params, $navUrls, $analytics $confirm.notify("light-error", "According to our Oompa Loompas, your are not registered yet or typed an invalid password.") #TODO: i18n - submitLogin = -> + submitLogin = debounce 2000, => if not loginForm.validate() return @@ -403,7 +404,7 @@ InvitationDirective = ($auth, $confirm, $location, $params, $navUrls, $analytics $confirm.notify("light-error", "According to our Oompa Loompas, that username or email is already in use.") #TODO: i18n - submitRegister = -> + submitRegister = debounce 2000, => if not registerForm.validate() return diff --git a/app/coffee/modules/backlog/filters.coffee b/app/coffee/modules/backlog/filters.coffee index 6ce39e8e..d6445965 100644 --- a/app/coffee/modules/backlog/filters.coffee +++ b/app/coffee/modules/backlog/filters.coffee @@ -40,18 +40,18 @@ BacklogFiltersDirective = ($log, $location) -> <% _.each(filters, function(f) { %> <% if (f.selected) { %> - style="border-left: 3px solid <%= f.color %>;"<% } %>> + data-type="<%- f.type %>" + data-id="<%- f.id %>"> + style="border-left: 3px solid <%- f.color %>;"<% } %>> <%- f.name %> <%- f.count %> <% } else { %> - style="border-left: 3px solid <%= f.color %>;"<% } %>> + data-type="<%- f.type %>" + data-id="<%- f.id %>"> + style="border-left: 3px solid <%- f.color %>;"<% } %>> <%- f.name %> <%- f.count %> @@ -63,9 +63,9 @@ BacklogFiltersDirective = ($log, $location) -> templateSelected = _.template(""" <% _.each(filters, function(f) { %> - style="border-left: 3px solid <%= f.color %>;"<% } %>> + data-type="<%- f.type %>" + data-id="<%- f.id %>"> + style="border-left: 3px solid <%- f.color %>;"<% } %>> <%- f.name %> @@ -79,14 +79,14 @@ BacklogFiltersDirective = ($log, $location) -> showFilters = (title, type) -> $el.find(".filters-cats").hide() - $el.find(".filter-list").show() + $el.find(".filter-list").removeClass("hidden") $el.find("h2.breadcrumb").removeClass("hidden") $el.find("h2 a.subfilter span.title").html(title) $el.find("h2 a.subfilter span.title").prop("data-type", type) showCategories = -> $el.find(".filters-cats").show() - $el.find(".filter-list").hide() + $el.find(".filter-list").addClass("hidden") $el.find("h2.breadcrumb").addClass("hidden") initializeSelectedFilters = (filters) -> diff --git a/app/coffee/modules/backlog/lightboxes.coffee b/app/coffee/modules/backlog/lightboxes.coffee index 00ba58ba..d0a31b46 100644 --- a/app/coffee/modules/backlog/lightboxes.coffee +++ b/app/coffee/modules/backlog/lightboxes.coffee @@ -86,9 +86,9 @@ CreateEditSprint = ($repo, $confirm, $rs, $rootscope, lightboxService, $loading) remove = -> #TODO: i18n title = "Delete sprint" - subtitle = $scope.sprint.name + message = $scope.sprint.name - $confirm.ask(title, subtitle).then (finish) => + $confirm.askOnDelete(title, message).then (finish) => onSuccess = -> finish() $scope.milestonesCounter -= 1 diff --git a/app/coffee/modules/backlog/main.coffee b/app/coffee/modules/backlog/main.coffee index 8201b4f8..85c59b1a 100644 --- a/app/coffee/modules/backlog/main.coffee +++ b/app/coffee/modules/backlog/main.coffee @@ -74,11 +74,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F tgLoader.pageLoaded() # On Error - promise.then null, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) + promise.then null, @.onInitialDataError.bind(@) initializeEventHandlers: -> @scope.$on "usform:bulk:success", => @@ -431,6 +427,8 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F @scope.filters = {} plainTags = _.flatten(_.filter(_.map(@scope.userstories, "tags"))) + plainTags.sort() + @scope.filters.tags = _.map _.countBy(plainTags), (v, k) => obj = { id: k, @@ -469,9 +467,9 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F deleteUserStory: (us) -> #TODO: i18n title = "Delete User Story" - subtitle = us.subject + message = us.subject - @confirm.ask(title, subtitle).then (finish) => + @confirm.askOnDelete(title, message).then (finish) => # We modify the userstories in scope so the user doesn't see the removed US for a while @scope.userstories = _.without(@scope.userstories, us) @filterVisibleUserstories() diff --git a/app/coffee/modules/backlog/sprints.coffee b/app/coffee/modules/backlog/sprints.coffee index 5dba9b92..fcdcd8b6 100644 --- a/app/coffee/modules/backlog/sprints.coffee +++ b/app/coffee/modules/backlog/sprints.coffee @@ -21,44 +21,25 @@ taiga = @.taiga -mixOf = @.taiga.mixOf -toggleText = @.taiga.toggleText -scopeDefer = @.taiga.scopeDefer -bindOnce = @.taiga.bindOnce -groupBy = @.taiga.groupBy - module = angular.module("taigaBacklog") + ############################################################################# -## Sprint Directive +## Sprint Actions Directive ############################################################################# BacklogSprintDirective = ($repo, $rootscope) -> - ## Common parts - linkCommon = ($scope, $el, $attrs, $ctrl) -> - sprint = $scope.$eval($attrs.tgBacklogSprint) - if $scope.$first - $el.addClass("sprint-current") - $el.find(".sprint-table").addClass('open') - - else if sprint.closed - $el.addClass("sprint-closed") - - else if not $scope.$first and not sprint.closed - $el.addClass("sprint-old-open") - - # Update progress bars - $scope.$watch $attrs.tgBacklogSprint, (value) -> + link = ($scope, $el, $attrs) -> + $scope.$watch $attrs.tgBacklogSprint, (sprint) -> sprint = $scope.$eval($attrs.tgBacklogSprint) - if sprint.total_points - progressPercentage = Math.round(100 * (sprint.closed_points / sprint.total_points)) - else - progressPercentage = 0 - - $el.find(".current-progress").css("width", "#{progressPercentage}%") - - $el.find(".sprint-table").disableSelection() + if $scope.$first + $el.addClass("sprint-current") + $el.find(".sprint-table").addClass('open') + else if sprint.closed + $el.addClass("sprint-closed") + else if not $scope.$first and not sprint.closed + $el.addClass("sprint-old-open") # Event Handlers $el.on "click", ".sprint-name > .icon-arrow-up", (event) -> @@ -67,15 +48,92 @@ BacklogSprintDirective = ($repo, $rootscope) -> $el.find(".sprint-table").toggleClass('open') $el.on "click", ".sprint-name > .icon-edit", (event) -> + sprint = $scope.$eval($attrs.tgBacklogSprint) $rootscope.$broadcast("sprintform:edit", sprint) - link = ($scope, $el, $attrs) -> - $ctrl = $el.closest("div.wrapper").controller() - linkCommon($scope, $el, $attrs, $ctrl) - $scope.$on "$destroy", -> $el.off() return {link: link} module.directive("tgBacklogSprint", ["$tgRepo", "$rootScope", BacklogSprintDirective]) + + +############################################################################# +## Sprint Header Directive +############################################################################# + +BacklogSprintHeaderDirective = ($navUrls) -> + template = _.template(""" +
    + + + <% if(isVisible){ %> + + <%- name %> + + <% } %> + + <% if(isEditable){ %> + + <% } %> +
    + +
    +
    <%- estimatedDateRange %>
    +
      +
    • + <%- closedPoints %> + closed +
    • +
    • + <%- totalPoints %> + total +
    • +
    +
    + """) + + link = ($scope, $el, $attrs, $model) -> + isEditable = -> + return $scope.project.my_permissions.indexOf("modify_milestone") != -1 + + isVisible = -> + return $scope.project.my_permissions.indexOf("view_milestones") != -1 + + render = (sprint) -> + taskboardUrl = $navUrls.resolve("project-taskboard", + {project: $scope.project.slug, sprint: sprint.slug}) + + start = moment(sprint.estimated_start).format("DD MMM YYYY") + finish = moment(sprint.estimated_finish).format("DD MMM YYYY") + estimatedDateRange = "#{start}-#{finish}" + + ctx = { + name: sprint.name + taskboardUrl: taskboardUrl + estimatedDateRange: estimatedDateRange + closedPoints: sprint.closed_points or 0 + totalPoints: sprint.total_points or 0 + isVisible: isVisible() + isEditable: isEditable() + } + $el.html(template(ctx)) + + + $scope.$watch $attrs.ngModel, (sprint) -> + render(sprint) + + $scope.$on "sprintform:edit:success", -> + render($model.$modelValue) + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + require: "ngModel" + } + +module.directive("tgBacklogSprintHeader", ["$tgNavUrls", "$tgRepo", "$rootScope", BacklogSprintHeaderDirective]) diff --git a/app/coffee/modules/base.coffee b/app/coffee/modules/base.coffee index 9a4a309f..8310d95c 100644 --- a/app/coffee/modules/base.coffee +++ b/app/coffee/modules/base.coffee @@ -46,6 +46,7 @@ urls = { "home": "/" "error": "/error" "not-found": "/not-found" + "permission-denied": "/permission-denied" "login": "/login" "forgot-password": "/forgot-password" @@ -66,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/attachments.coffee b/app/coffee/modules/common/attachments.coffee index 1f928845..05951831 100644 --- a/app/coffee/modules/common/attachments.coffee +++ b/app/coffee/modules/common/attachments.coffee @@ -72,10 +72,12 @@ class AttachmentsController extends taiga.Controller @.attachments.push(data) @rootscope.$broadcast("attachment:create") - promise = promise.then null, (data) -> + promise = promise.then null, (data) => + @scope.$emit("attachments:size-error") if data.status == 413 index = @.uploadingAttachments.indexOf(attachment) @.uploadingAttachments.splice(index, 1) - @confirm.notify("error", null, "We have not been able to upload '#{attachment.name}'.") + @confirm.notify("error", "We have not been able to upload '#{attachment.name}'. + #{data.data._error_message}") return @q.reject(data) return promise @@ -109,7 +111,8 @@ class AttachmentsController extends taiga.Controller @.updateCounters() @rootscope.$broadcast("attachment:edit") - onError = => + onError = (response) => + $scope.$emit("attachments:size-error") if response.status == 413 @confirm.notify("error") return @q.reject() @@ -127,9 +130,9 @@ class AttachmentsController extends taiga.Controller # Remove one concrete attachment. removeAttachment: (attachment) -> title = "Delete attachment" #TODO: i18in - subtitle = "the attachment '#{attachment.name}'" #TODO: i18in + message = "the attachment '#{attachment.name}'" #TODO: i18in - return @confirm.ask(title, subtitle).then (finish) => + return @confirm.askOnDelete(title, message).then (finish) => onSuccess = => finish() index = @.attachments.indexOf(attachment) @@ -139,7 +142,7 @@ class AttachmentsController extends taiga.Controller onError = => finish(false) - @confirm.notify("error", null, "We have not been able to delete #{subtitle}.") + @confirm.notify("error", null, "We have not been able to delete #{message}.") return @q.reject() return @repo.remove(attachment).then(onSuccess, onError) @@ -151,7 +154,7 @@ class AttachmentsController extends taiga.Controller return not item.is_deprecated -AttachmentsDirective = ($confirm) -> +AttachmentsDirective = ($config, $confirm) -> template = _.template("""
    @@ -159,7 +162,11 @@ AttachmentsDirective = ($confirm) -> attachments -
    +
    + <% if (maxFileSize){ %> + + <% }; %>
    @@ -195,7 +202,6 @@ AttachmentsDirective = ($confirm) ->
    """) - link = ($scope, $el, $attrs, $ctrls) -> $ctrl = $ctrls[0] $model = $ctrls[1] @@ -222,6 +228,12 @@ AttachmentsDirective = ($confirm) -> $ctrl.reorderAttachment(attachment, newIndex) $ctrl.saveAttachments() + showSizeInfo = -> + $el.find(".size-info").removeClass("hidden") + + $scope.$on "attachments:size-error", -> + showSizeInfo() + $el.on "change", ".attachments-header input", (event) -> files = _.toArray(event.target.files) return if files.length < 1 @@ -249,7 +261,16 @@ AttachmentsDirective = ($confirm) -> $el.off() templateFn = ($el, $attrs) -> - return template({type: $attrs.type}) + maxFileSize = $config.get("maxUploadFileSize", null) + maxFileSize = sizeFormat(maxFileSize) if maxFileSize + maxFileSizeMsg = if maxFileSize then "Maximum upload size is #{maxFileSize}" else "" # TODO: i18n + + ctx = { + type: $attrs.type + maxFileSize: maxFileSize + maxFileSizeMsg: maxFileSizeMsg + } + return template(ctx) return { require: ["tgAttachments", "ngModel"] @@ -261,13 +282,13 @@ AttachmentsDirective = ($confirm) -> template: templateFn } -module.directive("tgAttachments", ["$tgConfirm", AttachmentsDirective]) +module.directive("tgAttachments", ["$tgConfig", "$tgConfirm", AttachmentsDirective]) AttachmentDirective = -> template = _.template("""
    - + <%- name %> @@ -291,7 +312,7 @@ AttachmentDirective = -> templateEdit = _.template("""
    <%- size %> @@ -320,6 +341,7 @@ AttachmentDirective = -> ctx = { id: attachment.id name: attachment.name + created_date: moment(attachment.created_date).format("DD MMM YYYY [at] hh:mm") #TODO: i18n url: attachment.url size: sizeFormat(attachment.size) description: attachment.description diff --git a/app/coffee/modules/common/components.coffee b/app/coffee/modules/common/components.coffee index 4c429a87..9a483475 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,99 +104,133 @@ 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 %> - + <%- watcher.full_name_display %> - <% if (editable) { %> + <% if(isEditable){ %> + data-watcher-id="<%- watcher.id %>" href="" title="delete-watcher"> - <% } %> + <% }; %>
    <% }); %> """) 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") - title = "Remove watcher" - subtitle = $scope.usersById[watcherId].full_name_display + title = "Delete watcher" + message = $scope.usersById[watcherId].full_name_display - $confirm.ask(title, subtitle).then (finish) => + $confirm.askOnDelete(title, message).then (finish) => finish() watcherIds = _.clone($model.$modelValue.watchers, false) watcherIds = _.pull(watcherIds, watcherId) @@ -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,86 +253,398 @@ 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) { %>
    - <%- assignedTo.full_name_display %> + <%- assignedTo.full_name_display %>
    <% } %> - """) + """) # 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, item) -> + return if item.id != $model.$modelValue.id + $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 = "Remove assigned to" - subtitle = "" - - $confirm.ask(title, subtitle).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 = """ +

    + Empty space is so boring... + go on be descriptive... + A rose by any other name would smell as sweet... +

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

    + No description yet. +

    + """ # 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]) ############################################################################# @@ -308,7 +684,7 @@ ListItemUsStatusDirective = -> ListItemAssignedtoDirective = -> template = _.template("""
    - <%- name %> + <%- name %>
    <%- name %>
    """) @@ -377,6 +753,7 @@ ListItemSeverityDirective = -> template: template } + ListItemTypeDirective = -> template = """
    diff --git a/app/coffee/modules/common/confirm.coffee b/app/coffee/modules/common/confirm.coffee index dbae0425..81679f42 100644 --- a/app/coffee/modules/common/confirm.coffee +++ b/app/coffee/modules/common/confirm.coffee @@ -50,14 +50,13 @@ class ConfirmService extends taiga.Service el.off(".confirm-dialog") - ask: (title, subtitle, message=null, lightboxSelector=".lightbox-confirm-delete") -> + ask: (title, subtitle, message, lightboxSelector=".lightbox-generic-ask") -> el = angular.element(lightboxSelector) # Render content el.find("h2.title").html(title) el.find("span.subtitle").html(subtitle) - if message - el.find("span.delete-question").html(message) + el.find("span.message").html(message) defered = @q.defer() @@ -80,13 +79,27 @@ class ConfirmService extends taiga.Service return defered.promise - askChoice: (title, subtitle, choices, lightboxSelector=".lightbox-ask-choice") -> + askOnDelete: (title, message) -> + return @.ask(title, "Are you sure you want to delete?", message) #TODO: i18n + + askChoice: (title, subtitle, choices, replacement, warning, lightboxSelector=".lightbox-ask-choice") -> el = angular.element(lightboxSelector) # Render content - el.find("h2.title").html(title) - el.find("span.subtitle").html(subtitle) - choicesField = el.find("select.choices") + el.find(".title").html(title) + el.find(".subtitle").html(subtitle) + + if replacement + el.find(".replacement").html(replacement) + else + el.find(".replacement").remove() + + if warning + el.find(".warning").html(warning) + else + el.find(".warning").remove() + + choicesField = el.find(".choices") choicesField.html('') _.each choices, (value, key) -> choicesField.append(angular.element("")) @@ -166,9 +179,9 @@ class ConfirmService extends taiga.Service el = angular.element(selector) if title - el.find("h4").html(title) + el.find("h4").html(title) else - el.find("h4").html(NOTIFICATION_MSG[type].title) + el.find("h4").html(NOTIFICATION_MSG[type].title) if message el.find("p").html(message) diff --git a/app/coffee/modules/common/history.coffee b/app/coffee/modules/common/history.coffee index 5918f71c..b0a52545 100644 --- a/app/coffee/modules/common/history.coffee +++ b/app/coffee/modules/common/history.coffee @@ -84,11 +84,11 @@ HistoryDirective = ($log, $loading) ->

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

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

    @@ -103,11 +103,11 @@ HistoryDirective = ($log, $loading) ->

    from
    - <%= from %> + <%- from %>

    to
    - <%= to %> + <%- to %>

    @@ -120,12 +120,12 @@ HistoryDirective = ($log, $loading) ->
    <% _.each(diff, function(change) { %>

    - <%= change.name %> from
    - <%= change.from %> + <%- change.name %> from
    + <%- change.from %>

    - <%= change.name %> to
    - <%= change.to %> + <%- change.name %> to
    + <%- change.to %>

    <% }) %>
    diff --git a/app/coffee/modules/common/lightboxes.coffee b/app/coffee/modules/common/lightboxes.coffee index 7bfaa917..eb9f1512 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 @@ -358,7 +390,7 @@ usersTemplate = _.template("""
    @@ -372,7 +404,7 @@ usersTemplate = _.template("""
    diff --git a/app/coffee/modules/common/raven-logger.coffee b/app/coffee/modules/common/raven-logger.coffee new file mode 100644 index 00000000..f0f9d415 --- /dev/null +++ b/app/coffee/modules/common/raven-logger.coffee @@ -0,0 +1,41 @@ +### +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino Garcia +# Copyright (C) 2014 David Barragán Merino +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: modules/common/raven-logger.coffee +### + + +taiga = @.taiga + +module = angular.module("taigaCommon") + +ExceptionHandlerFactory = ($log, @config) -> + ravenConfig = @config.get("ravenConfig", null) + if ravenConfig + $log.debug "Using the RavenJS exception handler." + Raven.config(ravenConfig).install() + return (exception, cause) -> + $log.error.apply($log, arguments) + Raven.captureException(exception) + + else + $log.debug "Using the default logging exception handler." + return (exception, cause) -> + $log.error.apply($log, arguments) + +module.factory("$exceptionHandler", ["$log", "$tgConfig", ExceptionHandlerFactory]) diff --git a/app/coffee/modules/common/tags.coffee b/app/coffee/modules/common/tags.coffee index 87a6fef9..6caa1735 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 @@ -73,6 +76,7 @@ ColorizeTagsDirective = -> link = ($scope, $el, $attrs, $ctrl) -> render = (srcTags) -> template = templates[$attrs.tgColorizeTagsType] + srcTags.sort() tags = _.map srcTags, (tag) -> color = $scope.project.tags_colors[tag] return {name: tag, color: color} @@ -83,127 +87,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 +221,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").focus() + hideInput = -> $el.find("input").addClass("hidden").blur() + 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 3abb17b3..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 %> @@ -96,18 +96,24 @@ tgMarkitupDirective = ($rootscope, $rs, $tr) -> onEnter: keepDefault: false replaceWith: (data) => - lines = data.textarea.value[0..(data.caretPosition - 1)].split("\n") - lastLine = lines[lines.length - 1] + lines = data.textarea.value.split("\n") + cursorLine = data.textarea.value[0..(data.caretPosition - 1)].split("\n").length + newLineContent = data.textarea.value[data.caretPosition..].split("\n")[0] + lastLine = lines[cursorLine - 1] # unordered list - match = lastLine.match /^(\s*- ).*/ + if match emptyListItem = lastLine.match /^(\s*)\-\s$/ if emptyListItem markdownCaretPositon = removeEmptyLine(data.textarea, lines.length - 1, data.caretPosition) else - return "\n#{match[1]}" if match + breakLineAtBeginning = newLineContent.match /^(\s*)\-\s/ + + if !breakLineAtBeginning + return "\n#{match[1]}" if match # unordered list * match = lastLine.match /^(\s*\* ).*/ @@ -118,7 +124,10 @@ tgMarkitupDirective = ($rootscope, $rs, $tr) -> if emptyListItem markdownCaretPositon = removeEmptyLine(data.textarea, lines.length - 1, data.caretPosition) else - return "\n#{match[1]}" if match + breakLineAtBeginning = newLineContent.match /^(\s*)\*\s/ + + if !breakLineAtBeginning + return "\n#{match[1]}" if match # ordered list match = lastLine.match /^(\s*)(\d+)\.\s/ @@ -129,7 +138,10 @@ tgMarkitupDirective = ($rootscope, $rs, $tr) -> if emptyListItem markdownCaretPositon = removeEmptyLine(data.textarea, lines.length - 1, data.caretPosition) else - return "\n#{match[1] + (parseInt(match[2], 10) + 1)}. " + breakLineAtBeginning = newLineContent.match /^(\s*)(\d+)\.\s/ + + if !breakLineAtBeginning + return "\n#{match[1] + (parseInt(match[2], 10) + 1)}. " return "\n" diff --git a/app/coffee/modules/issues/detail.coffee b/app/coffee/modules/issues/detail.coffee index ac36f08e..cb5f7aa4 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,14 +61,11 @@ 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, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) - + promise.then null, @.onInitialDataError.bind(@) initializeEventHandlers: -> @scope.$on "attachment:create", => @@ -85,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 @@ -134,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" - subtitle = @scope.issue.subject - - @confirm.ask(title, subtitle).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]) ############################################################################# @@ -386,7 +573,7 @@ module.directive("tgIssueStatus", IssueStatusDirective) PromoteIssueToUsButtonDirective = ($rootScope, $repo, $confirm) -> template = _.template(""" - + Promote to User Story """) # TODO: i18n diff --git a/app/coffee/modules/issues/list.coffee b/app/coffee/modules/issues/list.coffee index b2aab722..eb5e677a 100644 --- a/app/coffee/modules/issues/list.coffee +++ b/app/coffee/modules/issues/list.coffee @@ -74,11 +74,7 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi tgLoader.pageLoaded() # On Error - promise.then null, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) + promise.then null, @.onInitialDataError.bind(@) @scope.$on "issueform:new:success", => @analytics.trackEvent("issue", "create", "create issue on issues list", 1) @@ -323,11 +319,11 @@ paginatorTemplate = """ <% } %> <% _.each(pages, function(item) { %> -
  • +
  • <% if (item.type === "page") { %> - <%= item.num %> + <%- item.num %> <% } else if (item.type === "page-active") { %> - <%= item.num %> + <%- item.num %> <% } else { %> ... <% } %> @@ -473,8 +469,8 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading) -> <% _.each(filters, function(f) { %> <% if (!f.selected) { %> + data-type="<%- f.type %>" + data-id="<%- f.id %>"> style="border-left: 3px solid <%- f.color %>;"<% } %>> <%- f.name %> @@ -495,9 +491,9 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading) -> templateSelected = _.template(""" <% _.each(filters, function(f) { %> - style="border-left: 3px solid <%= f.color %>;"<% } %>> + data-type="<%- f.type %>" + data-id="<%- f.id %>"> + style="border-left: 3px solid <%- f.color %>;"<% } %>> <%- f.name %> @@ -512,14 +508,14 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading) -> showFilters = (title, type) -> $el.find(".filters-cats").hide() - $el.find(".filter-list").show() + $el.find(".filter-list").removeClass("hidden") $el.find("h2.breadcrumb").removeClass("hidden") $el.find("h2 a.subfilter span.title").html(title) $el.find("h2 a.subfilter span.title").prop("data-type", type) showCategories = -> $el.find(".filters-cats").show() - $el.find(".filter-list").hide() + $el.find(".filter-list").addClass("hidden") $el.find("h2.breadcrumb").addClass("hidden") initializeSelectedFilters = (filters) -> @@ -555,9 +551,10 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading) -> initializeSelectedFilters($scope.filters) return null - filters = $scope.filters[type] - filter = _.find(filters, {id:id}) + filterId = if type == 'tags' then taiga.toString(id) else id + filter = _.find(filters, {id: filterId}) + filter.selected = (not filter.selected) # Convert id to null as string for properly @@ -642,9 +639,9 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading) -> target = angular.element(event.currentTarget) customFilterName = target.parent().data('id') title = "Delete custom filter" # TODO: i18n - subtitle = "the custom filter '#{customFilterName}'" # TODO: i18n + message = "the custom filter '#{customFilterName}'" # TODO: i18n - $confirm.ask(title, subtitle).then (finish) -> + $confirm.askOnDelete(title, message).then (finish) -> promise = $ctrl.deleteMyFilter(customFilterName) promise.then -> promise = $ctrl.loadMyFilters() @@ -791,7 +788,7 @@ module.directive("tgIssueStatusInlineEdition", ["$tgRepo", IssueStatusInlineEdit IssueAssignedToInlineEditionDirective = ($repo, $rootscope, popoverService) -> template = _.template(""" - <%- name %> + <%- name %>
    <%- name %>
    """) diff --git a/app/coffee/modules/kanban/main.coffee b/app/coffee/modules/kanban/main.coffee index 1446fb39..3ab7a514 100644 --- a/app/coffee/modules/kanban/main.coffee +++ b/app/coffee/modules/kanban/main.coffee @@ -78,12 +78,7 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi tgLoader.pageLoaded() # On Error - promise.then null, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) - + promise.then null, @.onInitialDataError.bind(@) initializeEventHandlers: -> @scope.$on "usform:new:success", => @@ -397,7 +392,7 @@ KanbanUserDirective = ($log) -> template = _.template("""
    class="not-clickable"<% } %>> - <%- name %> + <%- name %>
    """) # TODO: i18n diff --git a/app/coffee/modules/nav.coffee b/app/coffee/modules/nav.coffee index b3e35b1a..d834c5f4 100644 --- a/app/coffee/modules/nav.coffee +++ b/app/coffee/modules/nav.coffee @@ -272,7 +272,7 @@ ProjectMenuDirective = ($log, $compile, $auth, $rootscope, $tgAuth, $location, $
  • Logout
  • - <%= user.full_name_display %> + <%- user.full_name_display %>
    @@ -281,41 +281,30 @@ ProjectMenuDirective = ($log, $compile, $auth, $rootscope, $tgAuth, $location, $ mainTemplate = _.template(""" diff --git a/app/coffee/modules/projects/lightboxes.coffee b/app/coffee/modules/projects/lightboxes.coffee index e88aaa35..9d6cb8c0 100644 --- a/app/coffee/modules/projects/lightboxes.coffee +++ b/app/coffee/modules/projects/lightboxes.coffee @@ -26,7 +26,7 @@ debounce = @.taiga.debounce module = angular.module("taigaProject") -CreateProject = ($rootscope, $repo, $confirm, $location, $navurls, $rs, $projectUrl, lightboxService) -> +CreateProject = ($rootscope, $repo, $confirm, $location, $navurls, $rs, $projectUrl, lightboxService, $cacheFactory) -> link = ($scope, $el, attrs) -> $scope.data = {} $scope.templates = [] @@ -34,6 +34,11 @@ CreateProject = ($rootscope, $repo, $confirm, $location, $navurls, $rs, $project form = $el.find("form").checksley({"onlyOneErrorElement": true}) onSuccessSubmit = (response) -> + # remove all $http cache + # This is necessary when a project is created with the same name + # than another deleted in the same session + $cacheFactory.get('$http').removeAll() + $rootscope.$broadcast("projects:reload") $confirm.notify("success", "Success") #TODO: i18n $location.url($projectUrl.get(response)) @@ -116,7 +121,7 @@ CreateProject = ($rootscope, $repo, $confirm, $location, $navurls, $rs, $project return {link:link} module.directive("tgLbCreateProject", ["$rootScope", "$tgRepo", "$tgConfirm", "$location", "$tgNavUrls", - "$tgResources", "$projectUrl", "lightboxService", CreateProject]) + "$tgResources", "$projectUrl", "lightboxService", "$cacheFactory", CreateProject]) ############################################################################# diff --git a/app/coffee/modules/projects/main.coffee b/app/coffee/modules/projects/main.coffee index 86b438f9..db64d8d2 100644 --- a/app/coffee/modules/projects/main.coffee +++ b/app/coffee/modules/projects/main.coffee @@ -53,11 +53,7 @@ class ProjectsController extends taiga.Controller @scope.$emit("projects:loaded") @tgLoader.pageLoaded() - promise.then null, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) + promise.then null, @.onInitialDataError.bind(@) loadInitialData: -> return @rs.projects.list().then (projects) => @@ -96,11 +92,7 @@ class ProjectController extends taiga.Controller promise.then () => @appTitle.set(@scope.project.name) - promise.then null, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) + promise.then null, @.onInitialDataError.bind(@) loadInitialData: -> # Resolve project slug diff --git a/app/coffee/modules/related-tasks.coffee b/app/coffee/modules/related-tasks.coffee index 1e142421..c7acbffe 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) { %> @@ -139,9 +139,9 @@ RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading) -> #TODO: i18n task = $model.$modelValue title = "Delete Task" - subtitle = task.subject + message = task.subject - $confirm.ask(title, subtitle).then (finish) -> + $confirm.askOnDelete(title, message).then (finish) -> promise = $repo.remove(task) promise.then -> finish() @@ -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(""" @@ -310,7 +310,7 @@ module.directive("tgRelatedTasks", ["$tgRepo", "$tgResources", "$rootScope", Rel RelatedTaskAssignedToInlineEditionDirective = ($repo, $rootscope, popoverService) -> template = _.template(""" - <%- name %> + <%- name %>
    <%- name %>
    """) diff --git a/app/coffee/modules/resources/attachments.coffee b/app/coffee/modules/resources/attachments.coffee index f56e82fd..327d10a4 100644 --- a/app/coffee/modules/resources/attachments.coffee +++ b/app/coffee/modules/resources/attachments.coffee @@ -24,7 +24,7 @@ taiga = @.taiga sizeFormat = @.taiga.sizeFormat -resourceProvider = ($rootScope, $urls, $model, $repo, $auth, $q) -> +resourceProvider = ($rootScope, $config, $urls, $model, $repo, $auth, $q) -> service = {} service.list = (urlName, objectId, projectId) -> @@ -38,6 +38,16 @@ resourceProvider = ($rootScope, $urls, $model, $repo, $auth, $q) -> defered.reject(null) return defered.promise + maxFileSize = $config.get("maxUploadFileSize", null) + if maxFileSize and file.size > maxFileSize + response = { + status: 413, + data: _error_message: "'#{file.name}' (#{sizeFormat(file.size)}) is too heavy for our oompa + loompas, try it with a smaller than (#{sizeFormat(maxFileSize)})" + } + defered.reject(response) + return defered.promise + uploadProgress = (evt) => $rootScope.$apply => file.status = "in-progress" @@ -83,5 +93,5 @@ resourceProvider = ($rootScope, $urls, $model, $repo, $auth, $q) -> module = angular.module("taigaResources") -module.factory("$tgAttachmentsResourcesProvider", ["$rootScope", "$tgUrls", "$tgModel", "$tgRepo", "$tgAuth", - "$q", resourceProvider]) +module.factory("$tgAttachmentsResourcesProvider", ["$rootScope", "$tgConfig", "$tgUrls", "$tgModel", "$tgRepo", + "$tgAuth", "$q", resourceProvider]) diff --git a/app/coffee/modules/resources/memberships.coffee b/app/coffee/modules/resources/memberships.coffee index de569993..6a2763b7 100644 --- a/app/coffee/modules/resources/memberships.coffee +++ b/app/coffee/modules/resources/memberships.coffee @@ -42,9 +42,9 @@ resourceProvider = ($repo, $http, $urls) -> url = $urls.resolve("memberships") return $http.post("#{url}/#{id}/resend_invitation", {}) - service.bulkCreateMemberships = (projectId, data) -> + service.bulkCreateMemberships = (projectId, data, invitation_extra_text) -> url = $urls.resolve("bulk-create-memberships") - params = {project_id: projectId, bulk_memberships: data} + params = {project_id: projectId, bulk_memberships: data, invitation_extra_text: invitation_extra_text} return $http.post(url, params) return (instance) -> 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/resources/user-settings.coffee b/app/coffee/modules/resources/user-settings.coffee index c9bfe0b5..54a374cc 100644 --- a/app/coffee/modules/resources/user-settings.coffee +++ b/app/coffee/modules/resources/user-settings.coffee @@ -21,13 +21,26 @@ taiga = @.taiga +sizeFormat = @.taiga.sizeFormat -resourceProvider = ($repo, $http, $urls) -> + +resourceProvider = ($config, $repo, $http, $urls, $q) -> service = {} - service.changeAvatar = (attachmentModel) -> + service.changeAvatar = (file) -> + maxFileSize = $config.get("maxUploadFileSize", null) + if maxFileSize and file.size > maxFileSize + response = { + status: 413, + data: _error_message: "'#{file.name}' (#{sizeFormat(file.size)}) is too heavy for our oompa + loompas, try it with a smaller than (#{sizeFormat(maxFileSize)})" + } + defered = $q.defer() + defered.reject(response) + return defered.promise + data = new FormData() - data.append('avatar', attachmentModel) + data.append('avatar', file) options = { transformRequest: angular.identity, headers: {'Content-Type': undefined} @@ -52,4 +65,5 @@ resourceProvider = ($repo, $http, $urls) -> module = angular.module("taigaResources") -module.factory("$tgUserSettingsResourcesProvider", ["$tgRepo", "$tgHttp", "$tgUrls", resourceProvider]) +module.factory("$tgUserSettingsResourcesProvider", ["$tgConfig", "$tgRepo", "$tgHttp", "$tgUrls", "$q", + resourceProvider]) diff --git a/app/coffee/modules/search.coffee b/app/coffee/modules/search.coffee index fa5b390e..e22c787d 100644 --- a/app/coffee/modules/search.coffee +++ b/app/coffee/modules/search.coffee @@ -55,11 +55,7 @@ class SearchController extends mixOf(taiga.Controller, taiga.PageMixin) promise.then () => @appTitle.set("Search") - promise.then null, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) + promise.then null, @.onInitialDataError.bind(@) # Search input watcher @scope.searchTerm = "" 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/taskboard/main.coffee b/app/coffee/modules/taskboard/main.coffee index 8cd2dc37..331a69b2 100644 --- a/app/coffee/modules/taskboard/main.coffee +++ b/app/coffee/modules/taskboard/main.coffee @@ -66,11 +66,7 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin) tgLoader.pageLoaded() # On Error - promise.then null, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) + promise.then null, @.onInitialDataError.bind(@) initializeEventHandlers: -> # TODO: Reload entire taskboard after create/edit tasks seems @@ -320,7 +316,7 @@ TaskboardUserDirective = ($log) -> template = _.template("""
    class="not-clickable"<% } %>> - <%- name %> + <%- name %>
    """) # TODO: i18n diff --git a/app/coffee/modules/tasks/detail.coffee b/app/coffee/modules/tasks/detail.coffee index ea532357..0dfef32a 100644 --- a/app/coffee/modules/tasks/detail.coffee +++ b/app/coffee/modules/tasks/detail.coffee @@ -57,13 +57,10 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin) promise.then () => @appTitle.set(@scope.task.subject + " - " + @scope.project.name) + @.initializeOnDeleteGoToUrl() tgLoader.pageLoaded() - promise.then null, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) + promise.then null, @.onInitialDataError.bind(@) initializeEventHandlers: -> @scope.$on "attachment:create", => @@ -74,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 @@ -101,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 = { @@ -123,152 +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" - subtitle = @scope.task.subject - - @confirm.ask(title, subtitle).then (finish) => - promise = @.repo.remove(@scope.task) - promise.then => - finish() - @location.path(@navUrls.resolve("project-backlog", {project: @scope.project.slug})) - 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/user-settings/change-password.coffee b/app/coffee/modules/user-settings/change-password.coffee index 3db6c237..54aeb0de 100644 --- a/app/coffee/modules/user-settings/change-password.coffee +++ b/app/coffee/modules/user-settings/change-password.coffee @@ -51,11 +51,7 @@ class UserChangePasswordController extends mixOf(taiga.Controller, taiga.PageMix promise = @.loadInitialData() - promise.then null, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) + promise.then null, @.onInitialDataError.bind(@) loadProject: -> return @rs.projects.get(@scope.projectId).then (project) => diff --git a/app/coffee/modules/user-settings/main.coffee b/app/coffee/modules/user-settings/main.coffee index 6137264f..c356df27 100644 --- a/app/coffee/modules/user-settings/main.coffee +++ b/app/coffee/modules/user-settings/main.coffee @@ -21,6 +21,7 @@ taiga = @.taiga mixOf = @.taiga.mixOf +sizeFormat = @.taiga.sizeFormat module = angular.module("taigaUserSettings") @@ -32,6 +33,7 @@ class UserSettingsController extends mixOf(taiga.Controller, taiga.PageMixin) @.$inject = [ "$scope", "$rootScope", + "$tgConfig", "$tgRepo", "$tgConfirm", "$tgResources", @@ -42,18 +44,18 @@ class UserSettingsController extends mixOf(taiga.Controller, taiga.PageMixin) "$tgAuth" ] - constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @auth) -> + constructor: (@scope, @rootscope, @config, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @auth) -> @scope.sectionName = "User Profile" #i18n @scope.project = {} @scope.user = @auth.getUser() + maxFileSize = @config.get("maxUploadFileSize", null) + if maxFileSize + @scope.maxFileSizeMsg = "[Max, size: #{sizeFormat(maxFileSize)}" # TODO: i18n + promise = @.loadInitialData() - promise.then null, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) + promise.then null, @.onInitialDataError.bind(@) loadProject: -> return @rs.projects.get(@scope.projectId).then (project) => @@ -115,6 +117,9 @@ module.directive("tgUserProfile", ["$tgConfirm", "$tgAuth", "$tgRepo", UserProf UserAvatarDirective = ($auth, $model, $rs, $confirm) -> link = ($scope, $el, $attrs) -> + showSizeInfo = -> + $el.find(".size-info").removeClass("hidden") + onSuccess = (response) -> user = $model.make_model("users", response.data) $auth.setUser(user) @@ -124,6 +129,7 @@ UserAvatarDirective = ($auth, $model, $rs, $confirm) -> $confirm.notify('success') onError = (response) -> + showSizeInfo() if response.status == 413 $el.find('.overlay').hide() $confirm.notify('error', response.data._error_message) diff --git a/app/coffee/modules/user-settings/notifications.coffee b/app/coffee/modules/user-settings/notifications.coffee index b6e392f0..6af3c428 100644 --- a/app/coffee/modules/user-settings/notifications.coffee +++ b/app/coffee/modules/user-settings/notifications.coffee @@ -51,11 +51,7 @@ class UserNotificationsController extends mixOf(taiga.Controller, taiga.PageMixi promise = @.loadInitialData() - promise.then null, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) + promise.then null, @.onInitialDataError.bind(@) loadProject: -> return @rs.projects.get(@scope.projectId).then (project) => diff --git a/app/coffee/modules/userstories/detail.coffee b/app/coffee/modules/userstories/detail.coffee index e81f4481..71e7a8a7 100644 --- a/app/coffee/modules/userstories/detail.coffee +++ b/app/coffee/modules/userstories/detail.coffee @@ -59,16 +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, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) + 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") @@ -79,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 @@ -110,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 @@ -134,257 +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" - subtitle = @scope.us.subject - - @confirm.ask(title, subtitle).then (finish) => - promise = @.repo.remove(@scope.us) - promise.then => - finish() - @location.path(@navUrls.resolve("project-backlog", {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("""
    • @@ -392,7 +278,7 @@ UsEstimationDirective = ($log) -> total
    • <% _.each(roles, function(role) { %> -
    • +
    • <%- role.points %> <%- role.name %>
    • <% }); %> @@ -415,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") @@ -428,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) -> @@ -461,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') @@ -482,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") @@ -491,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 7a45338e..3c6a1179 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 @@ -65,11 +66,7 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin) tgLoader.pageLoaded() # On Error - promise.then null, (xhr) => - if xhr and xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() - return @q.reject(xhr) + promise.then null, @.onInitialDataError.bind(@) loadProject: -> return @rs.projects.get(@scope.projectId).then (project) => @@ -84,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: -> @@ -113,34 +118,19 @@ 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" - subtitle = unslugify(@scope.wiki.slug) + message = unslugify(@scope.wiki.slug) - @confirm.ask(title, subtitle).then (finish) => + @confirm.askOnDelete(title, message).then (finish) => onSuccess = => finish() ctx = {project: @scope.projectSlug} @@ -155,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/coffee/modules/wiki/nav.coffee b/app/coffee/modules/wiki/nav.coffee index 2f4530ee..4daf00bd 100644 --- a/app/coffee/modules/wiki/nav.coffee +++ b/app/coffee/modules/wiki/nav.coffee @@ -107,9 +107,9 @@ WikiNavDirective = ($tgrepo, $log, $location, $confirm, $navUrls, $analytics, $l # TODO: i18n title = "Delete Wiki Link" - subtitle = $scope.wikiLinks[linkId].title + message = $scope.wikiLinks[linkId].title - $confirm.ask(title, subtitle).then (finish) => + $confirm.askOnDelete(title, message).then (finish) => promise = $tgrepo.remove($scope.wikiLinks[linkId]) promise.then -> promise = $ctrl.loadWikiLinks() diff --git a/app/fonts/OpenSans-CondLight.eot b/app/fonts/OpenSans-CondLight.eot index b2c34dc5..b8d5568a 100644 Binary files a/app/fonts/OpenSans-CondLight.eot and b/app/fonts/OpenSans-CondLight.eot differ diff --git a/app/fonts/OpenSans-CondLight.svg b/app/fonts/OpenSans-CondLight.svg index a23ca9ec..d08e94d5 100644 --- a/app/fonts/OpenSans-CondLight.svg +++ b/app/fonts/OpenSans-CondLight.svg @@ -6,12 +6,11 @@ + - - @@ -205,49 +204,933 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -295,8 +1178,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -345,8 +1406,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -368,9 +1541,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -394,8 +1697,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -417,7 +1757,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -434,7 +1807,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -448,7 +1846,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -468,6 +1899,23 @@ + + + + + + + + + + + + + + + + + @@ -478,7 +1926,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -492,8 +1965,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -519,7 +2043,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -538,6 +2095,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -551,7 +2128,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -570,10 +2180,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -640,6 +2382,23 @@ + + + + + + + + + + + + + + + + + @@ -651,8 +2410,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -711,8 +2591,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -769,7 +2770,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -783,8 +2809,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -842,7 +2992,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -861,6 +3036,13 @@ + + + + + + + @@ -875,6 +3057,13 @@ + + + + + + + @@ -891,7 +3080,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -919,6 +3142,13 @@ + + + + + + + @@ -929,6 +3159,13 @@ + + + + + + + @@ -939,7 +3176,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -989,7 +3281,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1019,8 +3345,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1042,8 +3405,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1065,8 +3465,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1088,8 +3525,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1111,8 +3585,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1134,8 +3645,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1156,7 +3704,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1174,7 +3747,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1193,7 +3799,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1212,7 +3851,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1231,7 +3903,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1250,7 +3955,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1269,7 +4007,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1288,7 +4059,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1307,6 +4111,23 @@ + + + + + + + + + + + + + + + + + @@ -1318,6 +4139,23 @@ + + + + + + + + + + + + + + + + + @@ -1329,6 +4167,23 @@ + + + + + + + + + + + + + + + + + @@ -1340,6 +4195,23 @@ + + + + + + + + + + + + + + + + + @@ -1351,8 +4223,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1412,6 +4408,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -1449,6 +4465,13 @@ + + + + + + + @@ -1459,6 +4482,13 @@ + + + + + + + @@ -1469,6 +4499,13 @@ + + + + + + + @@ -1479,6 +4516,13 @@ + + + + + + + @@ -1489,6 +4533,13 @@ + + + + + + + @@ -1499,6 +4550,13 @@ + + + + + + + @@ -1509,6 +4567,13 @@ + + + + + + + @@ -1519,6 +4584,13 @@ + + + + + + + @@ -1533,6 +4605,13 @@ + + + + + + + @@ -1552,6 +4631,13 @@ + + + + + + + @@ -1569,11 +4655,2813 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1631,10 +7519,11353 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1682,8 +18913,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1731,8 +19140,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1754,8 +19275,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1803,8 +19502,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1826,5 +19637,13 @@ + + + + + + + + \ No newline at end of file diff --git a/app/fonts/OpenSans-CondLight.ttf b/app/fonts/OpenSans-CondLight.ttf index 044dc1f9..f74e27ec 100644 Binary files a/app/fonts/OpenSans-CondLight.ttf and b/app/fonts/OpenSans-CondLight.ttf differ diff --git a/app/fonts/OpenSans-CondLight.woff b/app/fonts/OpenSans-CondLight.woff index 7564c664..73632c70 100644 Binary files a/app/fonts/OpenSans-CondLight.woff and b/app/fonts/OpenSans-CondLight.woff differ diff --git a/app/fonts/taiga.eot b/app/fonts/taiga.eot index 7a6a04e8..4d7ef8bb 100644 Binary files a/app/fonts/taiga.eot and b/app/fonts/taiga.eot differ diff --git a/app/fonts/taiga.svg b/app/fonts/taiga.svg index 3c640f2f..ff41af91 100644 --- a/app/fonts/taiga.svg +++ b/app/fonts/taiga.svg @@ -45,4 +45,5 @@ + diff --git a/app/fonts/taiga.ttf b/app/fonts/taiga.ttf index fac99541..8fb9b030 100644 Binary files a/app/fonts/taiga.ttf and b/app/fonts/taiga.ttf differ diff --git a/app/fonts/taiga.woff b/app/fonts/taiga.woff index 1c12ca6a..14d2f8f5 100644 Binary files a/app/fonts/taiga.woff and b/app/fonts/taiga.woff differ 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/index.jade b/app/index.jade index bd7e4d4f..3751ee53 100644 --- a/app/index.jade +++ b/app/index.jade @@ -21,13 +21,13 @@ html(lang="en", ng-app="taiga") div.master(ng-view) - div.hidden.lightbox.lightbox-confirm-delete - include partials/views/modules/lightbox-confirm-delete - div.hidden.lightbox.lightbox-ask-choice + div.lightbox.lightbox-generic-ask + include partials/views/modules/lightbox-generic-ask + div.lightbox.lightbox-ask-choice include partials/views/modules/lightbox-ask-choice - div.hidden.lightbox.lightbox-generic-success + div.lightbox.lightbox-generic-success include partials/views/modules/lightbox-generic-success - div.hidden.lightbox.lightbox-generic-error + div.lightbox.lightbox-generic-error include partials/views/modules/lightbox-generic-error div.lightbox.lightbox-search(tg-search-box) include partials/views/modules/lightbox-search diff --git a/app/partials/admin-memberships.jade b/app/partials/admin-memberships.jade index 8eb2f94e..4ba6109a 100644 --- a/app/partials/admin-memberships.jade +++ b/app/partials/admin-memberships.jade @@ -22,5 +22,5 @@ block content div.paginator.memberships-paginator - div.lightbox.lightbox-add-member.hidden(tg-lb-create-members) + div.lightbox.lightbox-add-member(tg-lb-create-members) include views/modules/lightbox-add-member diff --git a/app/partials/admin-project-default-values.jade b/app/partials/admin-project-default-values.jade index ef74b073..e56f9089 100644 --- a/app/partials/admin-project-default-values.jade +++ b/app/partials/admin-project-default-values.jade @@ -12,7 +12,7 @@ block content sidebar.menu-tertiary.sidebar(tg-admin-navigation="default-values") include views/modules/admin-submenu-project-profile - section.main.admin-roles + section.main.admin-common header include views/components/mainTitle diff --git a/app/partials/admin-project-profile.jade b/app/partials/admin-project-profile.jade index 39349595..cf4c7917 100644 --- a/app/partials/admin-project-profile.jade +++ b/app/partials/admin-project-profile.jade @@ -58,5 +58,5 @@ block content a.button.button-green(href="") Save a.delete-project(href="", title="Delete this project", ng-click="ctrl.openDeleteLightbox()") Delete this project - div.lightbox.lightbox-delete-project.hidden(tg-lb-delete-project) + div.lightbox.lightbox-delete-project(tg-lb-delete-project) include views/modules/lightbox-delete-project diff --git a/app/partials/admin-project-values-issue-priorities.jade b/app/partials/admin-project-values-issue-priorities.jade index d84446c2..d0e06689 100644 --- a/app/partials/admin-project-values-issue-priorities.jade +++ b/app/partials/admin-project-values-issue-priorities.jade @@ -13,7 +13,7 @@ block content sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-priorities") include views/modules/admin-submenu-project-values - section.main.admin-roles + section.main.admin-common include views/components/mainTitle p.admin-subtitle Specify the priority levels users can assign to issues diff --git a/app/partials/admin-project-values-issue-severities.jade b/app/partials/admin-project-values-issue-severities.jade index 800ee06d..db23a4cb 100644 --- a/app/partials/admin-project-values-issue-severities.jade +++ b/app/partials/admin-project-values-issue-severities.jade @@ -13,7 +13,7 @@ block content sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-severities") include views/modules/admin-submenu-project-values - section.main.admin-roles + section.main.admin-common include views/components/mainTitle p.admin-subtitle Specify the severity level users can select to classify issues diff --git a/app/partials/admin-project-values-issue-status.jade b/app/partials/admin-project-values-issue-status.jade index d8b5dbf1..be1440e1 100644 --- a/app/partials/admin-project-values-issue-status.jade +++ b/app/partials/admin-project-values-issue-status.jade @@ -13,7 +13,7 @@ block content sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-issue-status") include views/modules/admin-submenu-project-values - section.main.admin-roles + section.main.admin-common include views/components/mainTitle p.admin-subtitle Specify the column headers that you will use to classify Issues diff --git a/app/partials/admin-project-values-issue-types.jade b/app/partials/admin-project-values-issue-types.jade index 4aac095b..5aa01057 100644 --- a/app/partials/admin-project-values-issue-types.jade +++ b/app/partials/admin-project-values-issue-types.jade @@ -13,7 +13,7 @@ block content sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-issue-types") include views/modules/admin-submenu-project-values - section.main.admin-roles + section.main.admin-common include views/components/mainTitle p.admin-subtitle Specify the categories users can select to classify issues diff --git a/app/partials/admin-project-values-task-status.jade b/app/partials/admin-project-values-task-status.jade index ec22b611..5f06174d 100644 --- a/app/partials/admin-project-values-task-status.jade +++ b/app/partials/admin-project-values-task-status.jade @@ -13,7 +13,7 @@ block content sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-task-status") include views/modules/admin-submenu-project-values - section.main.admin-roles + section.main.admin-common include views/components/mainTitle p.admin-subtitle Specify the column headers that you will use to classify Tasks related to each User Stories diff --git a/app/partials/admin-project-values-us-points.jade b/app/partials/admin-project-values-us-points.jade index bba828bb..6cf8d83d 100644 --- a/app/partials/admin-project-values-us-points.jade +++ b/app/partials/admin-project-values-us-points.jade @@ -13,7 +13,7 @@ block content sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-us-points") include views/modules/admin-submenu-project-values - section.main.admin-roles + section.main.admin-common include views/components/mainTitle p.admin-subtitle Specify the numerical system you will use to indicate the level of difficulty for each User Story @@ -26,5 +26,5 @@ block content include views/modules/admin/project-points - div.hidden.lightbox.lightbox-generic-notion.notion-admin-project-values-us-points(id="notion-admin-project-values-us-points", tg-lb-notion) + div.lightbox.lightbox-generic-notion.notion-admin-project-values-us-points(id="notion-admin-project-values-us-points", tg-lb-notion) include views/modules/help-notions/lightbox-notion-admin-project-values-us-points diff --git a/app/partials/admin-project-values-us-status.jade b/app/partials/admin-project-values-us-status.jade index 85387e32..3fc886ed 100644 --- a/app/partials/admin-project-values-us-status.jade +++ b/app/partials/admin-project-values-us-status.jade @@ -13,7 +13,7 @@ block content sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-us-status") include views/modules/admin-submenu-project-values - section.main.admin-roles + section.main.admin-common include views/components/mainTitle p.admin-subtitle Specify the column headers that you will use to classify User Stories diff --git a/app/partials/admin-roles.jade b/app/partials/admin-roles.jade index 900332e5..cb9010b2 100644 --- a/app/partials/admin-roles.jade +++ b/app/partials/admin-roles.jade @@ -11,15 +11,21 @@ block content sidebar.menu-tertiary.sidebar include views/modules/admin-submenu-roles - section.main.admin-roles + section.main.admin-roles.admin-common .header-with-actions include views/components/mainTitle .action-buttons a.button.button-red.delete-role(href="", title="Delete", ng-click="ctrl.delete()") Delete - p.total - | {{ role.name }} - span ({{ role.members_count }} members with this role) + + div(tg-edit-role) + .edit-role + input(type="text", value="{{ role.name }}") + a.save.icon.icon-floppy(href="", title="Save") + + p.total + span.role-name(title="{{ role.members_count }} members with this role") {{ role.name }} + a.edit-value.icon.icon-edit div.any-computable-role(ng-hide="anyComputableRole") Be careful, no role in your project will be able to estimate the point value for user stories diff --git a/app/partials/forgot-password.jade b/app/partials/forgot-password.jade index f5504c25..0fd58df7 100644 --- a/app/partials/forgot-password.jade +++ b/app/partials/forgot-password.jade @@ -8,7 +8,7 @@ block content div.wrapper div.login-main div.login-container - img.logo-svg(src="/svg/logo.svg", alt="TAIGA") + img.logo-svg(src="/svg/logo.svg", alt="TAIGA loves Movember!") h1.logo Taiga h2.tagline LOVE YOUR PROJECT 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..91448c44 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", @@ -27,23 +25,43 @@ block content 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'") + span.block-description(ng-bind="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(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/kanban.jade b/app/partials/kanban.jade index 85e36fda..b875221d 100644 --- a/app/partials/kanban.jade +++ b/app/partials/kanban.jade @@ -26,4 +26,4 @@ block content div.lightbox.lightbox-generic-bulk(tg-lb-create-bulk-userstories) include views/modules/lightbox-us-bulk - div.lightbox.lightbox-select-user.hidden(tg-lb-assignedto) + div.lightbox.lightbox-select-user(tg-lb-assignedto) diff --git a/app/partials/login.jade b/app/partials/login.jade index 8883998b..96457b86 100644 --- a/app/partials/login.jade +++ b/app/partials/login.jade @@ -8,7 +8,7 @@ block content div.wrapper div.login-main div.login-container - img.logo-svg(src="/svg/logo.svg", alt="TAIGA") + img.logo-svg(src="/svg/logo.svg", alt="TAIGA loves Movember!") h1.logo Taiga h2.tagline LOVE YOUR PROJECT diff --git a/app/partials/mail-notifications.jade b/app/partials/mail-notifications.jade index 9e8d8fd2..e03c6b6f 100644 --- a/app/partials/mail-notifications.jade +++ b/app/partials/mail-notifications.jade @@ -9,7 +9,7 @@ block content sidebar.menu-secondary.sidebar(tg-user-settings-navigation="mail-notifications") include views/modules/user-settings-menu - section.main.admin-roles + section.main.admin-common header h1 span.green(tg-bo-html="sectionName") diff --git a/app/partials/permission-denied.jade b/app/partials/permission-denied.jade new file mode 100644 index 00000000..a855c2af --- /dev/null +++ b/app/partials/permission-denied.jade @@ -0,0 +1,7 @@ +div.error-main + div.error-container + object.logo-svg(type="image/svg+xml", data="/svg/logo.svg") + img(src="/images/logo.png", alt="TAIGA") + h1.logo Permission denied + p.error-text Error 403. + a(href="/", title="") Take me home diff --git a/app/partials/register.jade b/app/partials/register.jade index 9ba5521e..a42c56be 100644 --- a/app/partials/register.jade +++ b/app/partials/register.jade @@ -8,8 +8,7 @@ block content div.wrapper div.login-main div.login-container - object.logo-svg(type="image/svg+xml", data="/svg/logo.svg") - img(src="/images/logo.png", alt="TAIGA") + img.logo-svg(src="/svg/logo.svg", alt="TAIGA loves Movember!") h1.logo Taiga h2.tagline LOVE YOUR PROJECT 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..f1bb79c6 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", @@ -33,22 +29,39 @@ block content span(tg-bo-bind="us.subject") 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'") + span.block-description(ng-bind="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(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..14f05a05 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", @@ -34,15 +30,16 @@ block content p.block-desc-container(ng-show="us.is_blocked") span.block-description-title Blocked - span.block-description(tg-bind-html="us.blocked_note || 'This user story is blocked'") + span.block-description(ng-bind="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-select-user.hidden(tg-lb-assignedto) + div.lightbox.lightbox-block(tg-lb-block, title="Blocking us", ng-model="us") + div.lightbox.lightbox-select-user(tg-lb-assignedto) + div.lightbox.lightbox-select-user(tg-lb-watchers) diff --git a/app/partials/user-change-password.jade b/app/partials/user-change-password.jade index 97c6ba02..9196c655 100644 --- a/app/partials/user-change-password.jade +++ b/app/partials/user-change-password.jade @@ -27,6 +27,3 @@ block content fieldset input(type="submit", class="hidden") a.button.button-green(href="", ng-click="ctrl.save()") Save - - div.lightbox.lightbox-delete-account.hidden - include views/modules/lightbox-delete-account diff --git a/app/partials/user-profile.jade b/app/partials/user-profile.jade index 0826e8d8..894479b5 100644 --- a/app/partials/user-profile.jade +++ b/app/partials/user-profile.jade @@ -24,9 +24,9 @@ block content span.icon.icon-spinner input(type="file", id="avatar-field", class="hidden", tg-avatar-model="avatarAttachment") - - p The image will be cropped to 80x80 size. - a.button.button-green.change Change + p The image will be cropped to 80x80px.
    + span.size-info.hidden(tg-bo-html="maxFileSizeMsg") + a.button.button-green.change(tg-bo-title="'Change photo. ' + maxFileSizeMsg") Change a.use-gravatar Use gravatar image div.data @@ -60,7 +60,4 @@ block content a.delete-account(href="", title="Delete Taiga account", ng-click="ctrl.openDeleteLightbox()") Delete Taiga account - div.lightbox.lightbox-delete-account.hidden(tg-lb-delete-user) - - div.lightbox.lightbox-confirm-use-gravatar.hidden - include views/modules/lightbox-use-gravatar + div.lightbox.lightbox-delete-account(tg-lb-delete-user) diff --git a/app/partials/views/components/kanban-task.jade b/app/partials/views/components/kanban-task.jade index 55012439..522616b3 100644 --- a/app/partials/views/components/kanban-task.jade +++ b/app/partials/views/components/kanban-task.jade @@ -4,10 +4,10 @@ div.kanban-task-inner div.task-text a.task-assigned(href="", title="Assign User Story") span.task-num(tg-bo-ref="us.ref") - a.task-name(href="", tg-bo-title="us.subject", tg-bind-html="us.subject", + a.task-name(href="", tg-bo-title="us.subject", ng-bind="us.subject", tg-nav="project-userstories-detail:project=project.slug,ref=us.ref") a.task-points(href="", title="Total Us points") - span(tg-bind-html="us.total_points") -- + span(ng-bind="us.total_points") -- span points a.icon.icon-edit(tg-check-permission="modify_us", href="", title="Edit") a.icon.icon-drag-h(tg-check-permission="modify_us", href="", title="Drag&Drop") diff --git a/app/partials/views/components/sprint-summary.jade b/app/partials/views/components/sprint-summary.jade index 71c4e124..585f7b25 100644 --- a/app/partials/views/components/sprint-summary.jade +++ b/app/partials/views/components/sprint-summary.jade @@ -2,7 +2,7 @@ div.summary.large-summary div div.summary-progress-bar(tg-progress-bar="stats.completedPercentage") div.data - span.number(tg-bind-html="stats.completedPercentage + '%'") + span.number(ng-bind="stats.completedPercentage + '%'") ul li diff --git a/app/partials/views/components/summary.jade b/app/partials/views/components/summary.jade index f8031e8e..9af07112 100644 --- a/app/partials/views/components/summary.jade +++ b/app/partials/views/components/summary.jade @@ -2,17 +2,17 @@ div.summary div.summary-progress-bar(tg-backlog-progress-bar="stats") div.data - span.number(tg-bind-html="stats.completedPercentage + '%'") + span.number(ng-bind="stats.completedPercentage + '%'") ul li - span.number(tg-bind-html="stats.total_points") -- + span.number(ng-bind="stats.total_points") -- span.description project
    points li - span.number(tg-bind-html="stats.defined_points") -- + span.number(ng-bind="stats.defined_points") -- span.description defined
    points li - span.number(tg-bind-html="stats.closed_points") -- + span.number(ng-bind="stats.closed_points") -- span.description closed
    points li - span.number(tg-bind-html="stats.speed | number:0") -- + span.number(ng-bind="stats.speed | number:0") -- span.description points /
    sprint diff --git a/app/partials/views/modules/admin/project-points.jade b/app/partials/views/modules/admin/project-points.jade index 211b255b..5117b528 100644 --- a/app/partials/views/modules/admin/project-points.jade +++ b/app/partials/views/modules/admin/project-points.jade @@ -33,8 +33,8 @@ section.project-values-table data-type="number") div.project-values-settings - a.save.icon.icon-floppy(href="", title="Add") - a.cancel.icon.icon-delete(href="", title="Delete") + a.save.icon.icon-floppy(href="", title="Save changes") + a.cancel.icon.icon-delete(href="", title="Cancel") form div.project-values-row.new-value.hidden @@ -48,4 +48,4 @@ section.project-values-table div.project-values-settings a.add-new.icon.icon-floppy(href="", title="Add") - a.delete-new.icon.icon-delete(href="", title="Delete") + a.delete-new.icon.icon-delete(href="", title="Cancel") diff --git a/app/partials/views/modules/admin/project-status.jade b/app/partials/views/modules/admin/project-status.jade index 5333df57..aa303a39 100644 --- a/app/partials/views/modules/admin/project-status.jade +++ b/app/partials/views/modules/admin/project-status.jade @@ -38,8 +38,8 @@ section.colors-table ng-options="e.id as e.name for e in [{'id':true, 'name':'Yes'},{'id':false, 'name': 'No'}]") div.options-column - a.save.icon.icon-floppy(href="", title="Add") - a.cancel.icon.icon-delete(href="", title="Delete") + a.save.icon.icon-floppy(href="", title="Save changes") + a.cancel.icon.icon-delete(href="", title="Cancel") form div.row.table-main.new-value.hidden @@ -57,4 +57,4 @@ section.colors-table div.options-column a.add-new.icon.icon-floppy(href="", title="Add") - a.delete-new.icon.icon-delete(href="", title="Delete") + a.delete-new.icon.icon-delete(href="", title="Cancel") diff --git a/app/partials/views/modules/admin/project-types.jade b/app/partials/views/modules/admin/project-types.jade index d58f29f8..4f504e38 100644 --- a/app/partials/views/modules/admin/project-types.jade +++ b/app/partials/views/modules/admin/project-types.jade @@ -31,8 +31,8 @@ section.colors-table ng-model="value.name", data-required="true", data-maxlength="255") div.options-column - a.save.icon.icon-floppy(href="", title="Add") - a.cancel.icon.icon-delete(href="", title="Delete") + a.save.icon.icon-floppy(href="", title="Save changes") + a.cancel.icon.icon-delete(href="", title="Cancel") form div.row.table-main.new-value.hidden @@ -46,4 +46,4 @@ section.colors-table div.options-column a.add-new.icon.icon-floppy(href="", title="Add") - a.delete-new.icon.icon-delete(href="", title="Delete") + a.delete-new.icon.icon-delete(href="", title="Cancel") diff --git a/app/partials/views/modules/admin/project-us-status.jade b/app/partials/views/modules/admin/project-us-status.jade index ca457484..a20c8c68 100644 --- a/app/partials/views/modules/admin/project-us-status.jade +++ b/app/partials/views/modules/admin/project-us-status.jade @@ -46,8 +46,8 @@ section.colors-table ng-model="value.wip_limit", data-type="digits") div.options-column - a.save.icon.icon-floppy(href="", title="Add") - a.cancel.icon.icon-delete(href="", title="Delete") + a.save.icon.icon-floppy(href="", title="Save changes") + a.cancel.icon.icon-delete(href="", title="Cancel") form div.row.table-main.new-value.hidden @@ -69,4 +69,4 @@ section.colors-table div.options-column a.add-new.icon.icon-floppy(href="", title="Add") - a.delete-new.icon.icon-delete(href="", title="Delete") + a.delete-new.icon.icon-delete(href="", title="Cancel") diff --git a/app/partials/views/modules/help-notions/lightbox-generic-notion.jade b/app/partials/views/modules/help-notions/lightbox-generic-notion.jade index 6e4222e7..00ce3a93 100644 --- a/app/partials/views/modules/help-notions/lightbox-generic-notion.jade +++ b/app/partials/views/modules/help-notions/lightbox-generic-notion.jade @@ -7,6 +7,6 @@ section block content - div.delete-options + div.options a.button.button-green(href="", title="Accept") span Accept diff --git a/app/partials/views/modules/lightbox-add-member.jade b/app/partials/views/modules/lightbox-add-member.jade index 7540b0a8..8bff3b80 100644 --- a/app/partials/views/modules/lightbox-add-member.jade +++ b/app/partials/views/modules/lightbox-add-member.jade @@ -4,6 +4,7 @@ form h2.title New Member //- Form is set in a directive + .add-member-forms a.button.button-green(href="", title="Save") span Create diff --git a/app/partials/views/modules/lightbox-ask-choice.jade b/app/partials/views/modules/lightbox-ask-choice.jade index f248c246..4e69be3b 100644 --- a/app/partials/views/modules/lightbox-ask-choice.jade +++ b/app/partials/views/modules/lightbox-ask-choice.jade @@ -1,13 +1,14 @@ a.close(href="", title="close") span.icon.icon-delete form - h2.title Delete User Story - p - span.delete-question Are you sure you want to delete? - span.subtitle #125 Crear el perfil de usuario senior en el admin - span.replacement What value do you want to use as replacement? - select.choices - div.delete-options + h2.title + p.question + p.subtitle + p.replacement + select.choices + p.warning + + div.options a.button.button-green(href="", title="Accept") span Accept a.button.button-red(href="", title="Delete") diff --git a/app/partials/views/modules/lightbox-confirm-delete.jade b/app/partials/views/modules/lightbox-confirm-delete.jade deleted file mode 100644 index 388a7160..00000000 --- a/app/partials/views/modules/lightbox-confirm-delete.jade +++ /dev/null @@ -1,12 +0,0 @@ -a.close(href="", title="close") - span.icon.icon-delete -form - h2.title Delete User Story - p - span.delete-question Are you sure you want to delete? - span.subtitle #125 Crear el perfil de usuario senior en el admin - div.delete-options - a.button.button-green(href="", title="Accept") - span Accept - a.button.button-red(href="", title="Delete") - span Cancel \ No newline at end of file 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-delete-account.jade b/app/partials/views/modules/lightbox-delete-account.jade index 0fb4ab2b..bd7c15af 100644 --- a/app/partials/views/modules/lightbox-delete-account.jade +++ b/app/partials/views/modules/lightbox-delete-account.jade @@ -3,9 +3,9 @@ a.close(href="", title="close") form h2.title Delete Taiga Account p - span.delete-question Are you sure you want to delete your Taiga account? + span.question Are you sure you want to delete your Taiga account? span.subtitle We're going to miss you! :-( - div.delete-options + div.options a.button.button-green(href="", title="Accept") span Accept a.button.button-red(href="", title="Cancel") diff --git a/app/partials/views/modules/lightbox-delete-project.jade b/app/partials/views/modules/lightbox-delete-project.jade index 63749499..b05fb346 100644 --- a/app/partials/views/modules/lightbox-delete-project.jade +++ b/app/partials/views/modules/lightbox-delete-project.jade @@ -3,9 +3,9 @@ a.close(href="", title="close") form h2.title Delete project p - span.delete-question Are you sure you want to delete this project? + span.question Are you sure you want to delete this project? span.subtitle All project data US/Tasks/Issues/Sprints/WikiPages will be lost! :-( - div.delete-options + div.options a.button.button-green(href="", title="Yes, I'm really sure") span Yes, I'm really sure a.button.button-red(href="", title="Cancel") diff --git a/app/partials/views/modules/lightbox-use-gravatar.jade b/app/partials/views/modules/lightbox-generic-ask.jade similarity index 64% rename from app/partials/views/modules/lightbox-use-gravatar.jade rename to app/partials/views/modules/lightbox-generic-ask.jade index 464d9e9e..af346d15 100644 --- a/app/partials/views/modules/lightbox-use-gravatar.jade +++ b/app/partials/views/modules/lightbox-generic-ask.jade @@ -1,9 +1,10 @@ a.close(href="", title="close") span.icon.icon-delete form - h2.title Use gravatar as image + h2.title p - span.subtitle You really want to delete your current photo and use the gravatar image? + span.subtitle + span.message div.options a.button.button-green(href="", title="Accept") span Accept diff --git a/app/partials/views/modules/lightbox-generic-error.jade b/app/partials/views/modules/lightbox-generic-error.jade index db878226..e8277d96 100644 --- a/app/partials/views/modules/lightbox-generic-error.jade +++ b/app/partials/views/modules/lightbox-generic-error.jade @@ -2,6 +2,6 @@ a.close(href="", title="close") span.icon.icon-delete section h2.title - div.delete-options + div.options a.button.button-green(href="", title="Accept") span Accept diff --git a/app/partials/views/modules/lightbox-generic-success.jade b/app/partials/views/modules/lightbox-generic-success.jade index db878226..e8277d96 100644 --- a/app/partials/views/modules/lightbox-generic-success.jade +++ b/app/partials/views/modules/lightbox-generic-success.jade @@ -2,6 +2,6 @@ a.close(href="", title="close") span.icon.icon-delete section h2.title - div.delete-options + div.options a.button.button-green(href="", title="Accept") span Accept 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/sprints.jade b/app/partials/views/modules/sprints.jade index 4f4ab30e..d850e14d 100644 --- a/app/partials/views/modules/sprints.jade +++ b/app/partials/views/modules/sprints.jade @@ -4,7 +4,7 @@ section.sprints div.summary ul li - span.number(tg-bind-html="sprintsCounter") -- + span.number(ng-bind="sprintsCounter") -- span.description
    sprints div.new-sprint a.button.button-green(href="", title="Add New sprint", @@ -12,35 +12,23 @@ section.sprints tg-check-permission="add_milestone") span.text + New sprint - section.sprint(ng-repeat="sprint in sprints track by sprint.id" - tg-backlog-sprint="sprint") - header - div.sprint-name - a.icon.icon-arrow-up(href="", title="Compact Sprint") - span {{ sprint.name }} - a.icon.icon-edit(tg-check-permission="modify_milestone", href="", title="Edit Sprint") - div.sprint-summary - div.sprint-date(tg-date-range="sprint.estimated_start,sprint.estimated_finish") - ul - li - span.number(ng-bind="sprint.closed_points|default:''") -- - span.description closed - li - span.number(ng-bind="sprint.total_points|default:''") - span.description total + section.sprint(ng-repeat="sprint in sprints track by sprint.id" tg-backlog-sprint="sprint") + header(tg-backlog-sprint-header, ng-model="sprint") - div.sprint-progress-bar(tg-progress-bar="100 * sprint.closed_points / total_points") + div.sprint-progress-bar(tg-progress-bar="100 * sprint.closed_points / sprint.total_points") div.sprint-table(tg-sprint-sortable) div.row.milestone-us-item-row(ng-repeat="us in sprint.user_stories track by us.id") div.column-us.width-8 a.us-name.clickable(tg-nav="project-userstories-detail:project=project.slug,ref=us.ref", - tg-bo-title="'#' + us.ref + ' ' + us.subject") + tg-bo-title="'#' + us.ref + ' ' + us.subject", + ng-class="{closed: us.is_closed}") span(tg-bo-ref="us.ref") span(tg-bo-bind="us.subject") - div.column-points.width-1(tg-bo-bind="us.total_points") + div.column-points.width-1(tg-bo-bind="us.total_points", ng-class="{closed: us.is_closed}") - a.button.button-gray(href="", tg-bo-title="'Go to Taskboard of ' + sprint.name", + a.button.button-gray(tg-bo-title="'Go to Taskboard of ' + sprint.name", tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug", tg-check-permission="view_milestones") + span Sprint Taskboard 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/buttons.scss b/app/styles/components/buttons.scss index 957ebaa0..f5a060e1 100755 --- a/app/styles/components/buttons.scss +++ b/app/styles/components/buttons.scss @@ -13,26 +13,8 @@ } } -%button, .button { - @extend %medium; - @extend %title; - @include transition (background .3s linear); - display: inline-block; - padding: 7px 40px 6px; - text-transform: uppercase; - &:hover { - @include transition (background .3s linear); - } - &.loading { - span { - @include animation (loading .5s linear); - @include animation (spin 1s linear infinite); - } - } - .icon { - margin-right: .3rem; - } + @extend %button; } a.button-green { 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/popover.scss b/app/styles/components/popover.scss deleted file mode 100644 index 88175da3..00000000 --- a/app/styles/components/popover.scss +++ /dev/null @@ -1,41 +0,0 @@ -@mixin popover($width, $top: '', $left: '', $bottom: '', $right: '', $arrow-width: 0, $arrow-top: '', $arrow-left: '', $arrow-bottom: '') { - @extend %text; - background: $blackish; - bottom: #{$bottom}; - color: $white; - display: none; - left: #{$left}; - list-style-type: none; - margin: 0; - padding: 10px; - position: absolute; - right: #{$right}; - top: #{$top}; - width: $width; - z-index: 99; - a { - @extend %small; - border-bottom: 1px solid $grayer; - color: $white; - display: block; - padding: 10px 2px; - &:last-child { - border: 0; - } - &:hover { - color: $fresh-taiga; - @include transition (color .3s linear); - } - } - &:after { - @include transform(rotate(45deg)); - background: $blackish; - bottom: #{$arrow-bottom}; - content: ''; - height: 15px; - left: #{$arrow-left}; - position: absolute; - top: #{$arrow-top}; - width: #{$arrow-width}; - } -} diff --git a/app/styles/components/spinner.scss b/app/styles/components/spinner.scss deleted file mode 100644 index 8b137891..00000000 --- a/app/styles/components/spinner.scss +++ /dev/null @@ -1 +0,0 @@ - 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..5c08a5d2 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; @@ -13,7 +13,6 @@ @extend %large; position: absolute; right: 1rem; - top: 4px; } &.no-watchers { border-bottom: 0; diff --git a/app/styles/dependencies/helpers.scss b/app/styles/dependencies/helpers.scss index 667f4ab3..79ae56fd 100644 --- a/app/styles/dependencies/helpers.scss +++ b/app/styles/dependencies/helpers.scss @@ -1,8 +1,3 @@ -// FTW -* { - box-sizing: border-box; -} - //Flexbox FTW %table-flex { align-content: stretch; @@ -20,132 +15,94 @@ width: 300px; } -// #Reset & Basics (Inspired by E. Meyers) -//================================================== -a, -abbr, -acronym, -address, -applet, -article, -aside, -audio, -b, -big, -blockquote, -body, -canvas, -caption, -center, -cite, -code, -dd, -del, -details, -dfn, -div, -dl, -dt, -em, -embed, -fieldset, -figcaption, -figure, -footer, -form, -h1, -h2, -h3, -h4, -h5, -h6, -header, -hgroup, -html, -i, -iframe, -img, -ins, -kbd, -label, -legend, -li, -mark, -menu, -nav, -object, -ol, -output, -p, -pre, -q, -ruby, -s, -samp, -section, -small, -span, -strike, -strong, -sub, -summary, -sup, -table, -tbody, -td, -tfoot, -th, -thead, -time, -tr, -tt, -u, -ul, -var, -video { - border: 0; - font: inherit; - font-size: 100%; - margin: 0; - padding: 0; - vertical-align: baseline; + +// __Font Sizes__ // +%xsmall {font-size: .5rem;} +%small {font-size: .8rem;} +%medium {font-size: 1rem;} +%large {font-size: 1.2rem;} +%larger {font-size: 1.6rem;} +%xlarge {font-size: 2rem;} +%xxlarge {font-size: 3rem;} + +// __Font Types__ // +%title {font-family: 'OpenSans-CondLight', Arial, Helvetica, sans-serif;} +%text {font-family: 'opensans-regular', Arial, Helvetica, sans-serif; line-height: 1.3rem;} +%bold {font-family: 'opensans-semibold', Arial, Helvetica, sans-serif;} +%taiga {font-family: 'taiga';} + +%lightbox { + background: rgba($white, .95); + bottom: 0; + display: none; + left: 0; + opacity: 0; + position: fixed; + right: 0; + top: 0; + z-index: 99910; + .close { + @extend %large; + position: absolute; + right: 2rem; + top: 2rem; + } + &.open { + @include table-flex(center, center, flex, row, wrap, center); + @include transition (opacity .3s ease); + opacity: 1; + } + &.close { + @include transition (opacity .3s ease); + opacity: 0; + } + .title { + text-align: center; + } + input, + textarea, + select { + margin-bottom: 1rem; + } + textarea { + resize: vertical; + } + .button-green, + .button-gray { + display: block; + padding: 12px; + text-align: center; + } } -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -menu, -nav, -section { - display: block; -} -body { - line-height: 1; +%button { + @extend %medium; + @extend %title; + @include transition (background .3s linear); + display: inline-block; + padding: 7px 40px 6px; + text-transform: uppercase; + &:hover { + @include transition (background .3s linear); + } + &.loading { + span { + @include animation (loading .5s linear); + @include animation (spin 1s linear infinite); + } + } + .icon { + margin-right: .3rem; + } } -ol, -ul { - list-style: none; +// Background +%triangled-bg { + background: url('/images/bg.png') no-repeat center center; + background-size: cover; } -blockquote, -q { - quotes: none; -} - -blockquote:before, -blockquote:after, -q:before, -q:after { - content: ''; -} - -table { - border-collapse: collapse; - border-spacing: 0; +%background-taiga { + background: url('/images/invitation_bg.jpg') no-repeat center center; + background-size: cover; } diff --git a/app/styles/dependencies/mixins.scss b/app/styles/dependencies/mixins.scss index b58588c9..28ce7fc5 100644 --- a/app/styles/dependencies/mixins.scss +++ b/app/styles/dependencies/mixins.scss @@ -5,18 +5,14 @@ text-overflow: ellipsis; } -@mixin background-opacity($color: $white, $opacity: .3) { - background: rgba($color, $opacity); -} - // Table Flex - http://devbryce.com/site/flexbox/ @mixin table-flex($align-content: stretch, $align-items: stretch, $display: flex, $flex-direction: row, $flex-wrap: wrap, $justify-content: flex-start) { @include display($display); - @include align-content($align-content); - @include align-items($align-items); - @include flex-direction($flex-direction); - @include flex-wrap($flex-wrap); - @include justify-content($justify-content); + @include align-content($align-content); //flex-start | flex-end | center | space-between | space-around | stretch + @include align-items($align-items); //flex-start | flex-end | center | baseline | stretch + @include flex-direction($flex-direction); //row | row-reverse | column | column-reverse + @include flex-wrap($flex-wrap); //nowrap | wrap | wrap-reverse + @include justify-content($justify-content); //flex-start | flex-end | center | space-between | space-around } @mixin table-flex-child($flex-grow: 1, $flex-basis: 300px, $flex-shrink: 0, $width:'') { @@ -54,3 +50,45 @@ @mixin background($red: 255, $green: 255, $blue: 255, $opacity: 1) { background: rgba($red, $green, $blue, $opacity); } + +@mixin popover($width, $top: '', $left: '', $bottom: '', $right: '', $arrow-width: 0, $arrow-top: '', $arrow-left: '', $arrow-bottom: '') { + @extend %text; + background: $blackish; + bottom: #{$bottom}; + color: $white; + display: none; + left: #{$left}; + list-style-type: none; + margin: 0; + padding: 10px; + position: absolute; + right: #{$right}; + top: #{$top}; + width: $width; + z-index: 99; + a { + @extend %small; + border-bottom: 1px solid $grayer; + color: $white; + display: block; + padding: 10px 2px; + &:last-child { + border: 0; + } + &:hover { + color: $fresh-taiga; + @include transition (color .3s linear); + } + } + &:after { + @include transform(rotate(45deg)); + background: $blackish; + bottom: #{$arrow-bottom}; + content: ''; + height: 15px; + left: #{$arrow-left}; + position: absolute; + top: #{$arrow-top}; + width: #{$arrow-width}; + } +} diff --git a/app/styles/extras/dependencies.scss b/app/styles/extras/dependencies.scss new file mode 100644 index 00000000..89a7026d --- /dev/null +++ b/app/styles/extras/dependencies.scss @@ -0,0 +1,16 @@ +// Bourbon +$prefix-for-webkit: true; +$prefix-for-mozilla: true; +$prefix-for-microsoft: true; +$prefix-for-opera: true; +$prefix-for-spec: true; +@import '../bourbon/bourbon'; + +//################################################# +// dependencies +//################################################# + +@import '../dependencies/colors'; +@import '../dependencies/mixins'; +@import '../dependencies/helpers'; +@import '../dependencies/responsive'; diff --git a/app/styles/dependencies/animation.scss b/app/styles/layout/animation.scss similarity index 100% rename from app/styles/dependencies/animation.scss rename to app/styles/layout/animation.scss diff --git a/app/styles/layout/base.scss b/app/styles/layout/base.scss index 9dc54351..aa4f9a73 100644 --- a/app/styles/layout/base.scss +++ b/app/styles/layout/base.scss @@ -98,6 +98,21 @@ body { @include table-flex-child(1, 260px, 0, 260px); background: $whitish; padding: 2rem 1rem; + &.filters-bar { + @include table-flex-child(0, 1px, 0, 1px); + @include transition(all .2s linear); + padding: 0; + &.active { + @include table-flex-child(1, 260px, 0, 260px); + @include transition(all .2s linear); + padding: 2em 1em; + + .filters-inner { + @include transition (all .4s ease-in); + opacity: 1; + } + } + } } .menu-tertiary { @@ -121,7 +136,7 @@ body { } .hidden { - display: none; + display: none !important; } .header-with-actions { @@ -129,7 +144,14 @@ body { margin-bottom: 1rem; .action-buttons { @include flex-shrink(0); - padding-left: 1rem; + } + .button { + color: $white; + float: right; + margin-left: 10px; + &:hover { + color: $white; + } } h1 { margin-bottom: 0; diff --git a/app/styles/dependencies/elements.scss b/app/styles/layout/elements.scss similarity index 88% rename from app/styles/dependencies/elements.scss rename to app/styles/layout/elements.scss index 77fd94f7..c545653b 100644 --- a/app/styles/dependencies/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; } @@ -51,18 +55,6 @@ sup { cursor: move; } -// Background -%triangled-bg { - background: url('/images/bg.png') no-repeat center center; - background-size: cover; -} - -%background-taiga { - background: url('/images/invitation_bg.jpg') no-repeat center center; - background-size: cover; -} - - //Datepicker .pika-single { z-index: 999999; diff --git a/app/styles/dependencies/forms.scss b/app/styles/layout/forms.scss similarity index 91% rename from app/styles/dependencies/forms.scss rename to app/styles/layout/forms.scss index c53cd4db..3d2b3895 100644 --- a/app/styles/dependencies/forms.scss +++ b/app/styles/layout/forms.scss @@ -16,9 +16,9 @@ input[type="number"], input[type="password"], input[type="email"], input[type="date"], +input[type="password"], select, textarea { - @extend %large; @extend %title; background: $whitish; border: 1px solid $gray-light; @@ -30,9 +30,6 @@ textarea { color: $gray-light; } } -input[type="password"] { - @extend %title; -} textarea { min-height: 10rem; diff --git a/app/styles/layout/mail-notifications.scss b/app/styles/layout/mail-notifications.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/app/styles/layout/reset.scss b/app/styles/layout/reset.scss new file mode 100644 index 00000000..99deddc2 --- /dev/null +++ b/app/styles/layout/reset.scss @@ -0,0 +1,135 @@ +// FTW +* { + box-sizing: border-box; +} + + +// #Reset & Basics (Inspired by E. Meyers) +//================================================== +a, +abbr, +acronym, +address, +applet, +article, +aside, +audio, +b, +big, +blockquote, +body, +canvas, +caption, +center, +cite, +code, +dd, +del, +details, +dfn, +div, +dl, +dt, +em, +embed, +fieldset, +figcaption, +figure, +footer, +form, +h1, +h2, +h3, +h4, +h5, +h6, +header, +hgroup, +html, +i, +iframe, +img, +ins, +kbd, +label, +legend, +li, +mark, +menu, +nav, +object, +ol, +output, +p, +pre, +q, +ruby, +s, +samp, +section, +small, +span, +strike, +strong, +sub, +summary, +sup, +table, +tbody, +td, +tfoot, +th, +thead, +time, +tr, +tt, +u, +ul, +var, +video { + border: 0; + font: inherit; + font-size: 100%; + margin: 0; + padding: 0; + vertical-align: baseline; +} + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { + display: block; +} +body { + line-height: 1; +} + +ol, +ul { + list-style: none; +} + +blockquote, +q { + quotes: none; +} + +blockquote:before, +blockquote:after, +q:before, +q:after { + content: ''; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/app/styles/dependencies/typography.scss b/app/styles/layout/typography.scss similarity index 90% rename from app/styles/dependencies/typography.scss rename to app/styles/layout/typography.scss index 92dc71b8..6ff57c9e 100755 --- a/app/styles/dependencies/typography.scss +++ b/app/styles/layout/typography.scss @@ -25,21 +25,6 @@ h6 { } } -// __Font Sizes__ // -%xsmall {font-size: .5rem;} -%small {font-size: .8rem;} -%medium {font-size: 1rem;} -%large {font-size: 1.2rem;} -%larger {font-size: 1.6rem;} -%xlarge {font-size: 2rem;} -%xxlarge {font-size: 3rem;} - -// __Font Types__ // -%title {font-family: 'OpenSans-CondLight';} -%text {font-family: 'opensans-regular'; line-height: 1.3rem;} -%bold {font-family: 'opensans-semibold';} -%taiga {font-family: 'taiga';} - h1 { @extend %xxlarge; @extend %title; @@ -255,4 +240,6 @@ a:visited { .icon-stats:before { content: 'L'; } - +.icon-copy:before { + content: 'M'; +} diff --git a/app/styles/layout/us-detail.scss b/app/styles/layout/us-detail.scss index 98bebef4..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; @@ -95,6 +112,7 @@ .block-description-title { @extend %bold; color: $white; + margin-right: .5rem; } .block-description { color: $white; @@ -132,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 { @@ -229,19 +309,21 @@ } } -.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 { + div { + @include transition(background .2s ease-in); + background: darken($whitish, 5%); + padding: .5rem; + padding-right: 1rem; + } + .clickable { &:hover { @include transition(background .2s ease-in); background: darken($whitish, 10%); @@ -256,6 +338,10 @@ .level-name { color: darken($whitish, 20%); float: right; + &.loading span { + @include animation (loading .5s linear); + @include animation (spin 1s linear infinite); + } } } @@ -270,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 { @@ -282,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 743f82da..4c0eae1e 100644 --- a/app/styles/layout/wiki.scss +++ b/app/styles/layout/wiki.scss @@ -1,13 +1,70 @@ -.wiki-content { - margin-bottom: 2rem; -} -.action-buttons { - .button { - color: $white; - float: right; - margin-left: 10px; +.wiki { + .remove { + @extend %small; + color: $gray-light; &:hover { - color: $white; + 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/main.scss b/app/styles/main.scss deleted file mode 100755 index 43c1aee0..00000000 --- a/app/styles/main.scss +++ /dev/null @@ -1,149 +0,0 @@ -// THIS IS THE MAIN INCLUDES SASS FILE - -// Bourbon -$prefix-for-webkit: true; -$prefix-for-mozilla: true; -$prefix-for-microsoft: true; -$prefix-for-opera: true; -$prefix-for-spec: true; -@import 'bourbon/bourbon'; - -// Codehilite -@import 'vendor/codehilite.github'; - -//################################################# -// dependencies -//################################################# - -@import 'dependencies/helpers'; -@import 'dependencies/colors'; -@import 'dependencies/typography'; -@import 'dependencies/elements'; -@import 'dependencies/mixins'; -@import 'dependencies/responsive'; -@import 'dependencies/forms'; -@import 'dependencies/animation'; - -//################################################# -// components -//################################################# - -@import 'components/buttons'; -@import 'components/avatar'; -@import 'components/summary'; -@import 'components/popover'; -@import 'components/tag'; -@import 'components/filter'; -@import 'components/taskboard-task'; -@import 'components/kanban-task'; -@import 'components/notification-message'; -@import 'components/basic-table'; -@import 'components/paginator'; -@import 'components/watchers'; -@import 'components/level'; -@import 'components/created-by'; -@import 'components/wysiwyg'; -@import 'components/select-color'; -@import 'components/loader'; -@import 'components/spinner'; -@import 'components/help-notion-button'; -@import 'components/beta'; - -//################################################# -// Modules -//################################################# - -//Common modules -@import 'modules/common/assigned-to'; -@import 'modules/common/nav'; -@import 'modules/common/projects-nav'; -@import 'modules/common/lightbox'; -@import 'modules/common/colors-table'; -@import 'modules/common/category-config'; -@import 'modules/common/attachments'; -@import 'modules/common/related-tasks'; -@import 'modules/common/history'; -@import 'modules/common/wizard'; - -//Project modules -@import 'modules/home-projects-list'; -@import 'modules/home-project'; -@import 'modules/create-project'; - -//Issues modules -@import 'modules/issues/issues-table'; - -//Kanban modules -@import 'modules/kanban/kanban-table'; - -//Search modules -@import 'modules/search/search-filter'; -@import 'modules/search/search-result-table'; -@import 'modules/search/search-in'; - -//Filters modules -@import 'modules/filters/filters'; -@import 'modules/filters/list-filters'; -@import 'modules/filters/filter-tags'; - -//Backlog modules -@import 'modules/backlog/sprints'; -@import 'modules/backlog/burndown'; -@import 'modules/backlog/backlog-table'; -@import 'modules/backlog/taskboard-table'; - -//Login modules -@import 'modules/auth/login-form'; -@import 'modules/auth/register-form'; -@import 'modules/auth/forgot-form'; -@import 'modules/auth/change-password-from-recovery'; - -//Wiki modules -@import 'modules/wiki/wiki-nav'; -@import 'modules/wiki/wiki-summary'; - -//modules admin -@import 'modules/admin/admin-menu'; -@import 'modules/admin/admin-submenu'; -@import 'modules/admin/admin-submenu-roles'; -@import 'modules/admin/admin-roles'; -@import 'modules/admin/admin-functionalities'; -@import 'modules/admin/admin-membership-table'; -@import 'modules/admin/admin-project-profile'; -@import 'modules/admin/default-values'; -@import 'modules/admin/project-values'; - -//Modules user Settings -@import 'modules/user-settings/user-profile'; -@import 'modules/user-settings/user-change-password'; -@import 'modules/user-settings/mail-notifications-table'; - -//################################################# -// Layout -//################################################# - -@import 'layout/base'; -@import 'layout/login'; -@import 'layout/invitation'; -@import 'layout/not-found'; -@import 'layout/backlog'; -@import 'layout/taskboard'; -@import 'layout/us-detail'; -@import 'layout/admin-memberships'; -@import 'layout/admin-project-values'; -@import 'layout/project-colors'; -@import 'layout/kanban'; -@import 'layout/issues'; -@import 'layout/wiki'; -@import 'layout/wiki-edit'; - -//################################################# -// Help -//################################################# -@import 'modules/help/lightbox-generic-notion'; - -//################################################# -// Shame -//################################################# - -@import 'shame/shame'; diff --git a/app/styles/modules/admin/admin-common.scss b/app/styles/modules/admin/admin-common.scss new file mode 100644 index 00000000..13756fe2 --- /dev/null +++ b/app/styles/modules/admin/admin-common.scss @@ -0,0 +1,24 @@ +.admin-common { + header { + h1 { + margin-bottom: 0; + } + } + .admin-subtitle { + color: $gray-light; + margin: 0; + } + .total { + @extend %large; + @extend %title; + background-color: $whitish; + color: $grayer; + padding: 1rem; + &:hover { + .edit-value { + @include transition(opacity .3s linear); + opacity: 1; + } + } + } +} diff --git a/app/styles/modules/admin/admin-membership-table.scss b/app/styles/modules/admin/admin-membership-table.scss index 43a1a646..1c354b20 100644 --- a/app/styles/modules/admin/admin-membership-table.scss +++ b/app/styles/modules/admin/admin-membership-table.scss @@ -6,6 +6,13 @@ @include table-flex(stretch, center, flex, row, wrap, flex-start); figcaption { margin-left: 1rem; + width: 75%; + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; + } } img { @include table-flex-child(1, 35px, 0); @@ -23,29 +30,6 @@ color: $gray-light; } } - .active, - .pending { - padding: 8px; - width: 115px; - .icon { - @extend %large; - } - } - .active { - background-color: $whitish; - } - .pending { - background-color: $red-light; - color: $white; - .icon { - float: right; - } - &:hover { - @include transition (background-color .3s linear); - background-color: $red; - color: $white; - } - } .header-role, .header-status { padding-left: .5rem; @@ -54,16 +38,32 @@ padding-right: 1rem; } .row-status { - @include table-flex(); - .delete { - @extend %large; - @include table-flex(stretch, center, flex, row, wrap, flex-start); - color: $gray-light; - margin-left: 15px; - padding: 0 15px; - &:hover { - color: $red; - } + @include table-flex($justify-content: space-between, $align-items: center); + } + .active, + .pending { + padding: 8px; + } + .active { + background-color: $whitish; + } + .pending { + background-color: $red-light; + color: $white; + .icon { + float: rsdsdfdvsdvight; + } + &:hover { + @include transition (background-color .3s linear); + background-color: $red; + color: $white; + } + } + .delete { + @extend %large; + color: $gray-light; + &:hover { + color: $red; } } .row-admin { @@ -78,14 +78,17 @@ .row-role, .header-member, .header-role { - @include table-flex-child(3, 35px, 0); + @include table-flex-child(4, 0, 0); + } + .row-admin, + .header-admin { + @include table-flex-child(1, 0, 0); } .row-status, - .row-admin, - .header-admin, .header-status { @include table-flex-child(1, 50px, 0); } + .check { background-color: darken($whitish, 10%); border-radius: 2px; diff --git a/app/styles/modules/admin/admin-menu.scss b/app/styles/modules/admin/admin-menu.scss index bc6bd31a..99ab5cbf 100644 --- a/app/styles/modules/admin/admin-menu.scss +++ b/app/styles/modules/admin/admin-menu.scss @@ -11,7 +11,6 @@ a { display: block; padding: 1rem 0 1rem 1rem; - &.active, &:hover { .icon { @include transition (opacity .3s linear); @@ -19,6 +18,10 @@ } } } + .active { + @include transition (opacity .3s linear); + opacity: 1; + } .icon { color: $blackish; float: right; diff --git a/app/styles/modules/admin/admin-roles.scss b/app/styles/modules/admin/admin-roles.scss index 1ae92901..29199cc9 100644 --- a/app/styles/modules/admin/admin-roles.scss +++ b/app/styles/modules/admin/admin-roles.scss @@ -1,14 +1,35 @@ .admin-roles { - .total { - @extend %large; + .role-name { + @extend %xlarge; @extend %title; - background-color: $whitish; color: $grayer; - padding: .5rem 1rem; - span { - @extend %medium; - @extend %text; - padding-left: .5rem; + } + .edit-value { + @include transition(opacity .3s linear); + @extend %medium; + color: $gray-light; + cursor: pointer; + margin-left: .5rem; + opacity: 0; + } + .edit-role { + @include table-flex(stretch, left, center, row, wrap); + background-color: $whitish; + display: none; + margin-bottom: 1rem; + padding: .5rem; + input { + background-color: $white; + width: 50%; + } + .icon-floppy { + @include transition(color.3s linear); + color: $gray-light; + margin-left: .5rem; + &:hover { + @include transition(color.3s linear); + color: $green-taiga; + } } } .any-computable-role { diff --git a/app/styles/modules/admin/project-values.scss b/app/styles/modules/admin/project-values.scss index 1c1f8571..46fabb7e 100644 --- a/app/styles/modules/admin/project-values.scss +++ b/app/styles/modules/admin/project-values.scss @@ -56,7 +56,7 @@ &:hover { @include transition(color .3s linear); color: $green-taiga; - &.icon-delete { + .icon-delete { color: $red; } } diff --git a/app/styles/modules/backlog/backlog-table.scss b/app/styles/modules/backlog/backlog-table.scss index 90bc279c..3c8e7212 100644 --- a/app/styles/modules/backlog/backlog-table.scss +++ b/app/styles/modules/backlog/backlog-table.scss @@ -14,7 +14,6 @@ padding: .5rem 0 .5rem .5rem; text-align: left; width: 100%; - } .backlog-table-title, .backlog-table-subtitle, @@ -23,7 +22,8 @@ background: transparent; } .user-stories { - @include table-flex-child(20, 365px, 0); + width: 100%; + } .status { @include table-flex-child(0, 150px, 0); @@ -149,8 +149,9 @@ background: lighten($green-taiga, 60%); } .user-story-name { - @include table-flex(); + @include table-flex($flex-wrap: nowrap); input { + @include flex-shrink(0); margin-right: 1rem; vertical-align: super; &:checked { diff --git a/app/styles/modules/backlog/sprints.scss b/app/styles/modules/backlog/sprints.scss index b36c4a83..979c8f64 100644 --- a/app/styles/modules/backlog/sprints.scss +++ b/app/styles/modules/backlog/sprints.scss @@ -16,7 +16,7 @@ } } .sprint-name { - span { + a { @extend %large; @extend %title; @include ellipsis($width: 90%); @@ -153,10 +153,16 @@ @include table-flex-child(1, 0, 0); padding: 0 4px; text-align: right; + &.closed { + color: lighten($gray-light, 5%); + } } .us-name { @include ellipsis(250px); display: block; + &.closed { + color: lighten($gray-light, 5%); + } } } .button-gray { 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 56a2fdc9..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; @@ -169,4 +169,8 @@ input { display: none; } + span { + @extend %small; + color: $gray-light; + } } 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/lightbox.scss b/app/styles/modules/common/lightbox.scss index d3e57bbb..247a34b2 100644 --- a/app/styles/modules/common/lightbox.scss +++ b/app/styles/modules/common/lightbox.scss @@ -1,46 +1,5 @@ -.lightbox, -%lightbox { - @include background-opacity($white, .95); - bottom: 0; - display: none; - left: 0; - opacity: 0; - position: fixed; - right: 0; - top: 0; - z-index: 99910; - .close { - @extend %large; - position: absolute; - right: 2rem; - top: 2rem; - } - &.open { - @include table-flex(center, center, flex, row, wrap, center); - @include transition (opacity .3s ease); - opacity: 1; - } - &.close { - @include transition (opacity .3s ease); - opacity: 0; - } - .title { - text-align: center; - } - input, - textarea, - select { - margin-bottom: 1rem; - } - textarea { - resize: vertical; - } - .button-green, - .button-gray { - display: block; - padding: 12px; - text-align: center; - } +.lightbox { + @extend %lightbox; } .markdown-preview { @@ -191,16 +150,23 @@ .lightbox-add-member { .add-member-wrapper { @include table-flex(); - } - fieldset { - position: relative; - &:first-child { - @include table-flex-child(3, 400px); - } + margin-bottom: .5rem; &:last-child { - @include table-flex-child(1, 200px); - margin-left: .5rem; + margin-bottom: 0; } + fieldset { + position: relative; + &:first-child { + @include table-flex-child(3, 400px); + } + &:last-child { + @include table-flex-child(1, 200px); + margin-left: .5rem; + } + } + } + .extra-text { + margin-top: 1rem; } input[type=email], select { @@ -283,70 +249,12 @@ } } -.lightbox-confirm-delete { +.lightbox-generic-ask { form { @include table-flex-child(0, 420px, 0, 420px); } - .delete-question, - .subtitle { - display: block; - line-height: 2rem; - text-align: center; - } - .subtitle { - @extend %large; - @extend %title; - } - .delete-options { - @include table-flex(); - a { - @include table-flex-child(1, 0, 0); - padding: 8px 0; - text-align: center; - &:first-child { - margin-right: .5rem; - } - } - } -} - -.lightbox-ask-choice { - form { - @include table-flex-child(0, 420px, 0, 420px); - } - .delete-question, - .subtitle { - display: block; - line-height: 2rem; - text-align: center; - } - .subtitle { - @extend %large; - @extend %title; - } - .replacement { - display: block; - text-align: center; - } - .delete-options { - @include table-flex(); - a { - @include table-flex-child(1, 0, 0); - padding: 8px 0; - text-align: center; - &:first-child { - margin-right: .5rem; - } - } - } -} - -.lightbox-confirm-use-gravatar { - form { - @include table-flex-child(0, 420px, 0, 420px); - } - .delete-question, - .subtitle { + .subtitle, + .message { display: block; line-height: 2rem; text-align: center; @@ -368,11 +276,45 @@ } } +.lightbox-ask-choice { + text-align: center; + form { + @include table-flex-child(0, 420px, 0); + } + .question, + .subtitle { + display: block; + line-height: 1.5rem; + text-align: center; + } + .subtitle { + @extend %large; + @extend %title; + } + .replacement { + display: block; + span { + display: block; + } + } + .options { + @include table-flex(); + a { + @include table-flex-child(1, 0, 0); + padding: 8px 0; + text-align: center; + &:first-child { + margin-right: .5rem; + } + } + } +} + .lightbox-delete-account { form { @include table-flex-child(0, 420px, 0, 420px); } - .delete-question, + .question, .subtitle { display: block; line-height: 2rem; @@ -382,7 +324,7 @@ @extend %large; @extend %title; } - .newsletter-delete { + .newsletter { margin-top: 1rem; text-align: center; input { @@ -393,7 +335,7 @@ } } } - .delete-options { + .options { @include table-flex(); a { @include table-flex-child(1, 0, 0); @@ -410,7 +352,7 @@ form { @include table-flex-child(0, 420px, 0, 420px); } - .delete-question, + .question, .subtitle { display: block; line-height: 2rem; @@ -420,7 +362,7 @@ @extend %large; @extend %title; } - .delete-options { + .options { @include table-flex(); a { @include table-flex-child(1, 0, 0); diff --git a/app/styles/modules/common/projects-nav.scss b/app/styles/modules/common/projects-nav.scss index 355e9460..b8e62c6f 100644 --- a/app/styles/modules/common/projects-nav.scss +++ b/app/styles/modules/common/projects-nav.scss @@ -120,8 +120,9 @@ width: 150px; } p { - @extend %large; + @extend %medium; color: $fresh-taiga; padding-top: 20px; + text-align: center; } } 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/app/styles/modules/filters/filters.scss b/app/styles/modules/filters/filters.scss index 015df762..a98e3218 100644 --- a/app/styles/modules/filters/filters.scss +++ b/app/styles/modules/filters/filters.scss @@ -1,21 +1,3 @@ -.menu-secondary { - &.filters-bar { - @include table-flex-child(0, 1px, 0, 1px); - @include transition(all .2s linear); - padding: 0; - &.active { - @include table-flex-child(1, 260px, 0, 260px); - @include transition(all .2s linear); - padding: 2em 1em; - - .filters-inner { - @include transition (all .4s ease-in); - opacity: 1; - } - } - } -} - .filters { h1 { vertical-align: baseline; @@ -47,13 +29,11 @@ right: .7rem; top: .7rem; } - } .filters-inner { opacity: 0; @include transition (all .1s ease-in); - .loading { background: $grayer; border: 1px solid #b8b8b8; diff --git a/app/styles/modules/filters/list-filters.scss b/app/styles/modules/filters/list-filters.scss index 06e0a234..29753c6c 100644 --- a/app/styles/modules/filters/list-filters.scss +++ b/app/styles/modules/filters/list-filters.scss @@ -15,7 +15,6 @@ @extend %large; @extend %title; opacity: .4; - &.active, &:hover { @include transition (opacity .3s linear); color: $blackish; @@ -23,6 +22,11 @@ } } + .active { + @include transition (opacity .3s linear); + color: $blackish; + opacity: 1; + } .icon { padding-right: .5rem; } diff --git a/app/styles/modules/help/notion-admin-project-values-us-points.scss b/app/styles/modules/help/notion-admin-project-values-us-points.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/app/styles/modules/search/search-filter.scss b/app/styles/modules/search/search-filter.scss index 6ef3ab9c..b9aa3314 100644 --- a/app/styles/modules/search/search-filter.scss +++ b/app/styles/modules/search/search-filter.scss @@ -11,18 +11,21 @@ @extend %large; @extend %title; opacity: .2; - &.active, &:hover { @include transition (opacity .3s linear); color: $blackish; opacity: 1; } } + .active { + @include transition (opacity .3s linear); + color: $blackish; + opacity: 1; + } .icon { margin-right: .4rem; } - - span.name { + .name { padding-left: 5px; } } diff --git a/app/styles/modules/user-settings/user-profile.scss b/app/styles/modules/user-settings/user-profile.scss index a720aa64..21c1a219 100644 --- a/app/styles/modules/user-settings/user-profile.scss +++ b/app/styles/modules/user-settings/user-profile.scss @@ -36,9 +36,13 @@ } p { @extend %xsmall; - margin-bottom: 0; + line-height: .8rem; + margin-bottom: .3rem; text-align: center; } + span { + @extend %bold; + } .use-gravatar { @extend %small; cursor: pointer; diff --git a/app/svg/logo.svg b/app/svg/logo.svg index 346d8115..4ca67830 100644 --- a/app/svg/logo.svg +++ b/app/svg/logo.svg @@ -1,23 +1,25 @@ - + - - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/app/svg/logo_back.svg b/app/svg/logo_back.svg new file mode 100644 index 00000000..346d8115 --- /dev/null +++ b/app/svg/logo_back.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/bower.json b/bower.json index d01e3285..10a70219 100644 --- a/bower.json +++ b/bower.json @@ -72,7 +72,8 @@ "favico.js": "0.3.4", "Sortable": "~0.1.8", "pikaday": "~1.2.0", - "malihu-custom-scrollbar-plugin": "~3.0.4" + "malihu-custom-scrollbar-plugin": "~3.0.4", + "raven-js": "~1.1.16" }, "resolutions": { "lodash": "~2.4.1", diff --git a/conf/main.example.json b/conf/main.example.json index b71f9e52..5d382492 100644 --- a/conf/main.example.json +++ b/conf/main.example.json @@ -5,5 +5,6 @@ "publicRegisterEnabled": true, "feedbackEnabled": true, "privacyPolicyUrl": null, - "termsOfServiceUrl": null + "termsOfServiceUrl": null, + "maxUploadFileSize": null } diff --git a/gulpfile.coffee b/gulpfile.coffee index 56127693..85d7788b 100644 --- a/gulpfile.coffee +++ b/gulpfile.coffee @@ -1,6 +1,6 @@ gulp = require("gulp") jade = require("gulp-jade") - +gutil = require("gulp-util") coffee = require("gulp-coffee") concat = require("gulp-concat") uglify = require("gulp-uglify") @@ -8,6 +8,7 @@ plumber = require("gulp-plumber") wrap = require("gulp-wrap") rename = require("gulp-rename") flatten = require('gulp-flatten') +gulpif = require('gulp-if') minifyHTML = require("gulp-minify-html") sass = require("gulp-ruby-sass") @@ -19,11 +20,20 @@ scsslint = require("gulp-scss-lint") newer = require("gulp-newer") cache = require("gulp-cached") jadeInheritance = require('gulp-jade-inheritance') +sourcemaps = require('gulp-sourcemaps') +insert = require("gulp-insert") +runSequence = require('run-sequence') +lazypipe = require('lazypipe') +rimraf = require('rimraf') + +mainSass = require("./main-sass").files paths = {} paths.app = "app/" paths.dist = "dist/" paths.tmp = "tmp/" +paths.tmpStyles = paths.tmp + "styles/" +paths.tmpStylesExtras = "#{paths.tmpStyles}/taiga-front-extras/**/*.css" paths.extras = "extras/" paths.jade = [ @@ -37,9 +47,11 @@ paths.svg = paths.app + "svg/**/*" paths.css = paths.app + "styles/vendor/*.css" paths.locales = paths.app + "locales/**/*.json" paths.sass = [ - paths.app + "styles/**/*.scss" + "#{paths.app}/styles/**/*.scss" + "#{paths.app}/plugins/**/*.scss" "!#{paths.app}/styles/bourbon/**/*.scss" - paths.app + "plugins/**/*.scss" + "!#{paths.app}/styles/dependencies/**/*.scss" + "!#{paths.app}/styles/extras/**/*.scss" ] paths.coffee = [ @@ -85,12 +97,15 @@ paths.js = [ paths.app + "vendor/jquery-textcomplete/jquery.textcomplete.js", paths.app + "vendor/markitup/markitup/jquery.markitup.js", paths.app + "vendor/malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.concat.min.js", + paths.app + "vendor/raven-js/dist/raven.js", paths.app + "js/jquery.ui.git-custom.js", paths.app + "js/jquery-ui.drag-multiple-custom.js", paths.app + "js/sha1-custom.js", paths.app + "plugins/**/*.js" ] +isDeploy = process.argv[process.argv.length - 1] == 'deploy' + ############################################################################ # Layout/CSS Related tasks ############################################################################## @@ -116,38 +131,54 @@ gulp.task "templates", -> .pipe(jade({pretty: true, locals:{v:(new Date()).getTime()}})) .pipe(gulp.dest(paths.dist)) +############################################################################## +# CSS Related tasks +############################################################################## + gulp.task "sass-lint", -> gulp.src(paths.sass) .pipe(cache("sasslint")) - .pipe(scsslint({config: "scsslint.yml"})) + .pipe(gulpif(!isDeploy, scsslint({config: "scsslint.yml"}))) -gulp.task "sass-watch", ["sass-lint"], -> - gulp.src(["#{paths.app}/styles/main.scss", "#{paths.app}/plugins/**/*.scss"]) +gulp.task "sass-compile", ["sass-lint"], -> + gulp.src(paths.sass) .pipe(plumber()) - .pipe(concat("all.scss")) - .pipe(sass()) - .pipe(rename("app.css")) + .pipe(cache("scss")) + .pipe(insert.prepend('@import "dependencies";')) + .pipe(sass({ + 'sourcemap=none': true, + loadPath: [ + "#{paths.app}styles/extras/" + ] + })) + .pipe(gulp.dest(paths.tmpStyles)) + +csslintChannel = lazypipe() + .pipe(csslint, "csslintrc.json") + .pipe(csslint.reporter) + +gulp.task "css-lint-app", -> + gulp.src(mainSass.concat([paths.tmpStylesExtras])) + .pipe(cache("csslint")) + .pipe(gulpif(!isDeploy, csslintChannel())) + +gulp.task "css-join", ["css-lint-app"], -> + gulp.src(mainSass.concat([paths.tmpStylesExtras])) + .pipe(concat("app.css")) .pipe(gulp.dest(paths.tmp)) -gulp.task "sass-deploy", -> - gulp.src(["#{paths.app}/styles/main.scss", "#{paths.app}/plugins/**/*.scss"]) - .pipe(plumber()) - .pipe(concat("all.scss")) - .pipe(sass()) - .pipe(rename("app.css")) - .pipe(gulp.dest(paths.tmp)) +gulp.task "css-app", (cb) -> + runSequence("sass-compile", "css-join", cb) gulp.task "css-vendor", -> gulp.src(paths.css) .pipe(concat("vendor.css")) .pipe(gulp.dest(paths.tmp)) -gulp.task "css-lint-app", ["sass-watch"], -> - gulp.src(paths.tmp + "app.css") - .pipe(csslint("csslintrc.json")) - .pipe(csslint.reporter()) +gulp.task "delete-tmp-styles", (cb) -> + rimraf(paths.tmpStyles, cb) -gulp.task "styles-watch", ["sass-watch", "css-vendor", "css-lint-app"], -> +gulp.task "styles-watch", ["css-app", "css-vendor"], -> _paths = [ paths.tmp + "vendor.css", paths.tmp + "app.css" @@ -155,18 +186,11 @@ gulp.task "styles-watch", ["sass-watch", "css-vendor", "css-lint-app"], -> gulp.src(_paths) .pipe(concat("main.css")) + .pipe(gulpif(isDeploy, minifyCSS({noAdvanced: true}))) .pipe(gulp.dest(paths.dist + "styles/")) -gulp.task "styles-deploy", ["sass-deploy", "css-vendor"], -> - _paths = [ - paths.tmp + "vendor.css", - paths.tmp + "app.css" - ] - - gulp.src(_paths) - .pipe(concat("main.css")) - .pipe(minifyCSS()) - .pipe(gulp.dest(paths.dist + "styles/")) +gulp.task "styles", ["delete-tmp-styles"], -> + gulp.start("styles-watch") ############################################################################## # JS Related tasks @@ -200,8 +224,10 @@ gulp.task "jslibs-watch", -> gulp.task "jslibs-deploy", -> gulp.src(paths.js) .pipe(plumber()) + .pipe(sourcemaps.init()) .pipe(concat("libs.js")) .pipe(uglify({mangle:false, preserveComments: false})) + .pipe(sourcemaps.write('./')) .pipe(gulp.dest("dist/js/")) gulp.task "app-watch", ["coffee", "conf", "locales"], -> @@ -223,8 +249,10 @@ gulp.task "app-deploy", ["coffee", "conf", "locales"], -> ] gulp.src(_paths) - .pipe(concat("app.js")) - .pipe(uglify({mangle:false, preserveComments: false})) + .pipe(sourcemaps.init()) + .pipe(concat("app.js")) + .pipe(uglify({mangle:false, preserveComments: false})) + .pipe(sourcemaps.write('./')) .pipe(gulp.dest(paths.dist + "js/")) ############################################################################## @@ -291,19 +319,21 @@ gulp.task "watch", -> gulp.task "deploy", [ + "delete-tmp-styles", "templates", "copy", "jade-deploy", "app-deploy", "jslibs-deploy", - "styles-deploy" + "styles" ] # The default task (called when you run gulp from cli) gulp.task "default", [ + "delete-tmp-styles", "copy", "templates", - "styles-watch", + "styles", "app-watch", "jslibs-watch", "jade-deploy", diff --git a/main-sass.js b/main-sass.js new file mode 100644 index 00000000..b3ebd1da --- /dev/null +++ b/main-sass.js @@ -0,0 +1,147 @@ +exports.files = function () { + var base = process.cwd() + "/tmp/styles/"; + + var files = [ + // Codehilite + 'vendor/codehilite.github', + + //################################################# + // Layout + //################################################# + + 'layout/reset', + 'layout/base', + 'layout/animation', + 'layout/typography', + 'layout/login', + 'layout/invitation', + 'layout/elements', + 'layout/forms', + 'layout/not-found', + 'layout/backlog', + 'layout/taskboard', + 'layout/us-detail', + 'layout/admin-memberships', + 'layout/admin-project-values', + 'layout/project-colors', + 'layout/kanban', + 'layout/issues', + 'layout/wiki', + 'layout/wiki-edit', + + //################################################# + // components + //################################################# + + 'components/buttons', + 'components/avatar', + 'components/summary', + 'components/popover', + 'components/tag', + 'components/filter', + 'components/taskboard-task', + 'components/kanban-task', + 'components/notification-message', + 'components/basic-table', + 'components/paginator', + 'components/watchers', + 'components/level', + 'components/created-by', + 'components/wysiwyg', + 'components/select-color', + 'components/loader', + 'components/spinner', + 'components/help-notion-button', + 'components/beta', + 'components/markitup', + + + //################################################# + // Modules + //################################################# + + //Common modules + 'modules/common/assigned-to', + 'modules/common/nav', + 'modules/common/projects-nav', + 'modules/common/lightbox', + 'modules/common/colors-table', + 'modules/common/category-config', + 'modules/common/attachments', + 'modules/common/related-tasks', + 'modules/common/history', + 'modules/common/wizard', + + //Project modules + 'modules/home-projects-list', + 'modules/home-project', + 'modules/create-project', + + //Issues modules + 'modules/issues/issues-table', + + //Kanban modules + 'modules/kanban/kanban-table', + + //Search modules + 'modules/search/search-filter', + 'modules/search/search-result-table', + 'modules/search/search-in', + + //Filters modules + 'modules/filters/filters', + 'modules/filters/list-filters', + 'modules/filters/filter-tags', + + //Backlog modules + 'modules/backlog/sprints', + 'modules/backlog/burndown', + 'modules/backlog/backlog-table', + 'modules/backlog/taskboard-table', + + //Login modules + 'modules/auth/login-form', + 'modules/auth/register-form', + 'modules/auth/forgot-form', + 'modules/auth/change-password-from-recovery', + + //Wiki modules + 'modules/wiki/wiki-nav', + 'modules/wiki/wiki-summary', + + //modules admin + 'modules/admin/admin-menu', + 'modules/admin/admin-common', + 'modules/admin/admin-submenu', + 'modules/admin/admin-submenu-roles', + 'modules/admin/admin-roles', + 'modules/admin/admin-functionalities', + 'modules/admin/admin-membership-table', + 'modules/admin/admin-project-profile', + 'modules/admin/default-values', + 'modules/admin/project-values', + + //Modules user Settings + 'modules/user-settings/user-profile', + 'modules/user-settings/user-change-password', + 'modules/user-settings/mail-notifications-table', + + //################################################# + // Help + //################################################# + + 'modules/help/lightbox-generic-notion', + + //################################################# + // Shame + //################################################# + + 'shame/shame', + ]; + + files = files.map(function (file) { + return base + file + ".css"; + }); + + return files; +}(); diff --git a/package.json b/package.json index 47a2c2ee..3f930598 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "gulp-csslint": "^0.1.5", "gulp-flatten": "0.0.4", "gulp-if": "0.0.5", + "gulp-insert": "^0.4.0", + "gulp-intermediate": "^3.0.1", "gulp-jade": "^0.5.0", "gulp-jade-inheritance": "0.0.4", "gulp-minify-css": "^0.3.1", @@ -37,15 +39,20 @@ "gulp-notify": "^1.2.5", "gulp-plumber": "^0.6.2", "gulp-rename": "^1.2.0", - "gulp-ruby-sass": "^0.4.3", + "gulp-ruby-sass": "^0.7.1", "gulp-scss-lint": "0.1.1", + "gulp-sourcemaps": "^1.2.4", "gulp-styledocco": "0.0.1", "gulp-template": "^0.1.1", "gulp-uglify": "~0.2.0", "gulp-util": "~2.2.14", "gulp-watch": "^0.5.4", "gulp-wrap": "^0.3.0", + "lazypipe": "^0.2.2", "readable-stream": "~1.0.31", + "rimraf": "^2.2.8", + "run-sequence": "^1.0.1", + "gulp-if": "^1.2.5", "through2": "~0.6.1" } }