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 %>
<%- email %>
@@ -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("""
Not registered yet?
- create your free account here
+ create your free account here
""")
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("""