Merge branch 'master' into stable (v. 1.2.0)

stable
David Barragán Merino 2014-11-04 10:40:20 +01:00
commit ccda53bada
154 changed files with 21560 additions and 2218 deletions

View File

@ -1,22 +1,24 @@
# Changelog # # Changelog #
## 1.2.0 (Unreleased) ## 1.2.0 Picea obovata (2014-11-04)
### Features ### Features
- US/Task/Issue visualization and edition refactor. Now only one view for both.
- Multiple User stories Drag & Drop in the backlog. - 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 ### 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) ## 1.1.0 Alnus maximowiczii (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.
### Features ### ### Features ###
@ -24,14 +26,21 @@
- Changed configuration format from coffeescript file to json. - Changed configuration format from coffeescript file to json.
- Add builtin analytics support. - Add builtin analytics support.
## 1.0.0 (2014-10-07)
### Misc ### ### 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 ### ### Features ###
- Redesign for taskboard and backlog summaries - Redesign for taskboard and backlog summaries
- Allow feedback for users from the platform - Allow feedback for users from the platform
- Real time changes for backlog, taskboard, kanban and issues - Real time changes for backlog, taskboard, kanban and issues
### Misc ###
- Lots of small and not so small bugfixes

View File

@ -52,30 +52,22 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
# User stories # User stories
$routeProvider.when("/project/:pslug/us/:usref", $routeProvider.when("/project/:pslug/us/:usref",
{templateUrl: "/partials/us-detail.html", resolve: {loader: tgLoaderProvider.add()}}) {templateUrl: "/partials/us-detail.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/us/:usref/edit",
{templateUrl: "/partials/us-detail-edit.html"})
# Tasks # Tasks
$routeProvider.when("/project/:pslug/task/:taskref", $routeProvider.when("/project/:pslug/task/:taskref",
{templateUrl: "/partials/task-detail.html", resolve: {loader: tgLoaderProvider.add()}}) {templateUrl: "/partials/task-detail.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/task/:taskref/edit",
{templateUrl: "/partials/task-detail-edit.html"})
# Wiki # Wiki
$routeProvider.when("/project/:pslug/wiki", $routeProvider.when("/project/:pslug/wiki",
{redirectTo: (params) -> "/project/#{params.pslug}/wiki/home"}, ) {redirectTo: (params) -> "/project/#{params.pslug}/wiki/home"}, )
$routeProvider.when("/project/:pslug/wiki/:slug", $routeProvider.when("/project/:pslug/wiki/:slug",
{templateUrl: "/partials/wiki.html", resolve: {loader: tgLoaderProvider.add()}}) {templateUrl: "/partials/wiki.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/wiki/:slug/edit",
{templateUrl: "/partials/wiki-edit.html"})
# Issues # Issues
$routeProvider.when("/project/:pslug/issues", $routeProvider.when("/project/:pslug/issues",
{templateUrl: "/partials/issues.html", resolve: {loader: tgLoaderProvider.add()}}) {templateUrl: "/partials/issues.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/issue/:issueref", $routeProvider.when("/project/:pslug/issue/:issueref",
{templateUrl: "/partials/issues-detail.html"}) {templateUrl: "/partials/issues-detail.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/issue/:issueref/edit",
{templateUrl: "/partials/issues-detail-edit.html"})
# Admin # Admin
$routeProvider.when("/project/:pslug/admin/project-profile/details", $routeProvider.when("/project/:pslug/admin/project-profile/details",
@ -136,6 +128,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
{templateUrl: "/partials/error.html"}) {templateUrl: "/partials/error.html"})
$routeProvider.when("/not-found", $routeProvider.when("/not-found",
{templateUrl: "/partials/not-found.html"}) {templateUrl: "/partials/not-found.html"})
$routeProvider.when("/permission-denied",
{templateUrl: "/partials/permission-denied.html"})
$routeProvider.otherwise({redirectTo: '/not-found'}) $routeProvider.otherwise({redirectTo: '/not-found'})
$locationProvider.html5Mode(true) $locationProvider.html5Mode(true)

View File

@ -22,6 +22,16 @@
class TaigaBase class TaigaBase
class TaigaService extends TaigaBase class TaigaService extends TaigaBase
class TaigaController 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.Base = TaigaBase
@.taiga.Service = TaigaService @.taiga.Service = TaigaService

View File

@ -24,13 +24,19 @@ debounce = @.taiga.debounce
module = angular.module("taigaKanban") module = angular.module("taigaKanban")
MAX_MEMBERSHIP_FIELDSETS = 6 MAX_MEMBERSHIP_FIELDSETS = 4
############################################################################# #############################################################################
## Create Members Lightbox Directive ## Create Members Lightbox Directive
############################################################################# #############################################################################
CreateMembersDirective = ($rs, $rootScope, $confirm, lightboxService) -> CreateMembersDirective = ($rs, $rootScope, $confirm, lightboxService) ->
extraTextTemplate = """
<fieldset class="extra-text">
<textarea placeholder="(Optional) Add a personalized text to the invitation. Tell something lovely to your new members ;-)"></textarea>
</fieldset>
"""
template = _.template(""" template = _.template("""
<div class="add-member-wrapper"> <div class="add-member-wrapper">
<fieldset> <fieldset>
@ -53,11 +59,14 @@ CreateMembersDirective = ($rs, $rootScope, $confirm, lightboxService) ->
return template(ctx) return template(ctx)
resetForm = -> 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() fieldSet = createFieldSet()
title.after(fieldSet) invitations.prepend(fieldSet)
$scope.$on "membersform:new", -> $scope.$on "membersform:new", ->
resetForm() resetForm()
@ -91,7 +100,7 @@ CreateMembersDirective = ($rs, $rootScope, $confirm, lightboxService) ->
fieldSet.after(newFieldSet) fieldSet.after(newFieldSet)
if $el.find(".add-member-wrapper").length == MAX_MEMBERSHIP_FIELDSETS 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") .addClass("icon-delete delete-fieldset")
$el.on "click", ".button-green", debounce 2000, (event) -> $el.on "click", ".button-green", debounce 2000, (event) ->
@ -112,12 +121,10 @@ CreateMembersDirective = ($rs, $rootScope, $confirm, lightboxService) ->
#checksley find new fields #checksley find new fields
form.destroy() form.destroy()
form.initialize() form.initialize()
if not form.validate() if not form.validate()
return return
memberWrappers = $el.find("form > .add-member-wrapper") memberWrappers = $el.find("form .add-member-wrapper")
memberWrappers = _.filter memberWrappers, (mw) -> memberWrappers = _.filter memberWrappers, (mw) ->
angular.element(mw).find("input").hasClass('checksley-ok') angular.element(mw).find("input").hasClass('checksley-ok')
@ -132,7 +139,9 @@ CreateMembersDirective = ($rs, $rootScope, $confirm, lightboxService) ->
} }
if invitations.length 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} return {link: link}

View File

@ -58,11 +58,7 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai
promise.then => promise.then =>
@appTitle.set("Membership - " + @scope.project.name) @appTitle.set("Membership - " + @scope.project.name)
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
@scope.$on "membersform:new:success", => @scope.$on "membersform:new:success", =>
@.loadMembers() @.loadMembers()
@ -119,11 +115,11 @@ paginatorTemplate = """
<% } %> <% } %>
<% _.each(pages, function(item) { %> <% _.each(pages, function(item) { %>
<li class="<%= item.classes %>"> <li class="<%- item.classes %>">
<% if (item.type === "page") { %> <% if (item.type === "page") { %>
<a href="" data-pagenum="<%= item.num %>"><%= item.num %></a> <a href="" data-pagenum="<%- item.num %>"><%- item.num %></a>
<% } else if (item.type === "page-active") { %> <% } else if (item.type === "page-active") { %>
<span class="active"><%= item.num %></span> <span class="active"><%- item.num %></span>
<% } else { %> <% } else { %>
<span>...</span> <span>...</span>
<% } %> <% } %>
@ -237,7 +233,7 @@ module.directive("tgMemberships", MembershipsDirective)
MembershipsRowAvatarDirective = ($log) -> MembershipsRowAvatarDirective = ($log) ->
template = _.template(""" template = _.template("""
<figure class="avatar"> <figure class="avatar">
<img src="<%= imgurl %>" alt="<%- full_name %>"> <img src="<%- imgurl %>" alt="<%- full_name %>">
<figcaption> <figcaption>
<span class="name"><%- full_name %></span> <span class="name"><%- full_name %></span>
<span class="email"><%- email %></span> <span class="email"><%- email %></span>
@ -432,18 +428,18 @@ MembershipsRowActionsDirective = ($log, $repo, $rs, $confirm) ->
event.preventDefault() event.preventDefault()
title = "Delete member" # TODO: i18n 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 = -> onSuccess = ->
finish() finish()
$ctrl.loadMembers() $ctrl.loadMembers()
$confirm.notify("success", null, "We've deleted #{subtitle}.") # TODO: i18n $confirm.notify("success", null, "We've deleted #{message}.") # TODO: i18n
onError = -> onError = ->
finish(false) finish(false)
# TODO: i18in # 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) $repo.remove(member).then(onSuccess, onError)

View File

@ -57,11 +57,7 @@ class ProjectProfileController extends mixOf(taiga.Controller, taiga.PageMixin)
promise.then => promise.then =>
@appTitle.set("Project profile - " + @scope.sectionName + " - " + @scope.project.name) @appTitle.set("Project profile - " + @scope.sectionName + " - " + @scope.project.name)
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
@scope.$on "project:loaded", => @scope.$on "project:loaded", =>
@appTitle.set("Project profile - " + @scope.sectionName + " - " + @scope.project.name) @appTitle.set("Project profile - " + @scope.sectionName + " - " + @scope.project.name)
@ -96,7 +92,7 @@ module.controller("ProjectProfileController", ProjectProfileController)
## Project Profile Directive ## Project Profile Directive
############################################################################# #############################################################################
ProjectProfileDirective = ($repo, $confirm, $loading) -> ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location) ->
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
form = $el.find("form").checksley({"onlyOneErrorElement": true}) form = $el.find("form").checksley({"onlyOneErrorElement": true})
submit = (target) => submit = (target) =>
@ -108,6 +104,8 @@ ProjectProfileDirective = ($repo, $confirm, $loading) ->
promise.then -> promise.then ->
$loading.finish(target) $loading.finish(target)
$confirm.notify("success") $confirm.notify("success")
newUrl = $navurls.resolve("project-admin-project-profile-details", {project: $scope.project.slug})
$location.path(newUrl)
$scope.$emit("project:loaded", $scope.project) $scope.$emit("project:loaded", $scope.project)
promise.then null, (data) -> promise.then null, (data) ->
@ -132,7 +130,7 @@ ProjectProfileDirective = ($repo, $confirm, $loading) ->
return {link:link} return {link:link}
module.directive("tgProjectProfile", ["$tgRepo", "$tgConfirm", "$tgLoading", ProjectProfileDirective]) module.directive("tgProjectProfile", ["$tgRepo", "$tgConfirm", "$tgLoading", "$tgNavUrls", "$tgLocation", ProjectProfileDirective])
############################################################################# #############################################################################

View File

@ -57,11 +57,7 @@ class ProjectValuesController extends mixOf(taiga.Controller, taiga.PageMixin)
promise.then () => promise.then () =>
@appTitle.set("Project values - " + @scope.sectionName + " - " + @scope.project.name) @appTitle.set("Project values - " + @scope.sectionName + " - " + @scope.project.name)
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
@scope.$on("admin:project-values:move", @.moveValue) @scope.$on("admin:project-values:move", @.moveValue)
@ -170,8 +166,8 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
promise = $repo.save(value) promise = $repo.save(value)
promise.then => promise.then =>
row = target.parents(".row.table-main") row = target.parents(".row.table-main")
row.hide() row.addClass("hidden")
row.siblings(".visualization").css("display": "flex") row.siblings(".visualization").removeClass('hidden')
promise.then null, (data) -> promise.then null, (data) ->
$confirm.notify("error") $confirm.notify("error")
@ -181,9 +177,9 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
row = target.parents(".row.table-main") row = target.parents(".row.table-main")
value = target.scope().value value = target.scope().value
$scope.$apply -> $scope.$apply ->
row.hide() row.addClass("hidden")
value.revert() value.revert()
row.siblings(".visualization").css("display": "flex") row.siblings(".visualization").removeClass('hidden')
$el.on "submit", "form", (event) -> $el.on "submit", "form", (event) ->
event.preventDefault() event.preventDefault()
@ -195,7 +191,7 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
$el.on "click", ".show-add-new", (event) -> $el.on "click", ".show-add-new", (event) ->
event.preventDefault() event.preventDefault()
$el.find(".new-value").css('display': 'flex') $el.find(".new-value").removeClass('hidden')
goToBottomList(true) goToBottomList(true)
@ -231,9 +227,10 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
row = target.parents(".row.table-main") row = target.parents(".row.table-main")
row.hide() row.addClass("hidden")
editionRow = row.siblings(".edition") editionRow = row.siblings(".edition")
editionRow.css("display": "flex") editionRow.removeClass('hidden')
editionRow.find('input:visible').first().focus().select() editionRow.find('input:visible').first().focus().select()
$el.on "keyup", ".edition input", (event) -> $el.on "keyup", ".edition input", (event) ->
@ -264,12 +261,13 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
choices[option.id] = option.name choices[option.id] = option.name
#TODO: i18n #TODO: i18n
title = "Delete" title = "Delete value"
subtitle = value.name subtitle = value.name
replacement = "All items with this value will be changed to"
if _.keys(choices).length == 0 if _.keys(choices).length == 0
return $confirm.error("You can't delete all values.") 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 = -> onSucces = ->
$ctrl.loadValues().finally -> $ctrl.loadValues().finally ->
response.finish() response.finish()

View File

@ -58,11 +58,7 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil
promise.then () => promise.then () =>
@appTitle.set("Roles - " + @scope.project.name) @appTitle.set("Roles - " + @scope.project.name)
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
loadProject: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
@ -93,8 +89,10 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil
delete: -> delete: ->
# TODO: i18n # TODO: i18n
title = "Delete Role" title = "Delete Role" # TODO: i18n
subtitle = @scope.role.name subtitle = @scope.role.name
replacement = "All the users with this role will be moved to" # TODO: i18n
warning = "<strong>Be careful, all role estimations will be removed</strong>" # TODO: i18n
choices = {} choices = {}
for role in @scope.roles for role in @scope.roles
@ -102,9 +100,9 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil
choices[role.id] = role.name choices[role.id] = role.name
if _.keys(choices).length == 0 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 = @repo.remove(@scope.role, {moveTo: response.selected})
promise.then => promise.then =>
@.loadProject() @.loadProject()
@ -127,6 +125,47 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil
module.controller("RolesController", RolesController) 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 = -> RolesDirective = ->
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->

View File

@ -20,6 +20,7 @@
### ###
taiga = @.taiga taiga = @.taiga
debounce = @.taiga.debounce
module = angular.module("taigaAuth", ["taigaResources"]) module = angular.module("taigaAuth", ["taigaResources"])
@ -156,7 +157,7 @@ PublicRegisterMessageDirective = ($config, $navUrls) ->
template = _.template(""" template = _.template("""
<p class="login-text"> <p class="login-text">
<span>Not registered yet?</span> <span>Not registered yet?</span>
<a href="<%= url %>" tg-nav="register" title="Register"> create your free account here</a> <a href="<%- url %>" tg-nav="register" title="Register"> create your free account here</a>
</p>""") </p>""")
templateFn = -> templateFn = ->
@ -225,7 +226,7 @@ RegisterDirective = ($auth, $confirm, $location, $navUrls, $config, $analytics)
$location.replace() $location.replace()
$scope.data = {} $scope.data = {}
form = $el.find("form").checksley() form = $el.find("form").checksley({onlyOneErrorElement: true})
onSuccessSubmit = (response) -> onSuccessSubmit = (response) ->
$analytics.trackEvent("auth", "register", "user registration", 1) $analytics.trackEvent("auth", "register", "user registration", 1)
@ -238,7 +239,7 @@ RegisterDirective = ($auth, $confirm, $location, $navUrls, $config, $analytics)
form.setErrors(response.data) form.setErrors(response.data)
submit = -> submit = debounce 2000, =>
if not form.validate() if not form.validate()
return return
@ -278,7 +279,7 @@ ForgotPasswordDirective = ($auth, $confirm, $location, $navUrls) ->
$confirm.notify("light-error", "According to our Oompa Loompas, $confirm.notify("light-error", "According to our Oompa Loompas,
your are not registered yet.") #TODO: i18n your are not registered yet.") #TODO: i18n
submit = -> submit = debounce 2000, =>
if not form.validate() if not form.validate()
return return
@ -323,7 +324,7 @@ ChangePasswordFromRecoveryDirective = ($auth, $confirm, $location, $params, $nav
$confirm.notify("light-error", "One of our Oompa Loompas say $confirm.notify("light-error", "One of our Oompa Loompas say
'#{response.data._error_message}'.") #TODO: i18n '#{response.data._error_message}'.") #TODO: i18n
submit = -> submit = debounce 2000, =>
if not form.validate() if not form.validate()
return return
@ -362,7 +363,7 @@ InvitationDirective = ($auth, $confirm, $location, $params, $navUrls, $analytics
# Login form # Login form
$scope.dataLogin = {token: token} $scope.dataLogin = {token: token}
loginForm = $el.find("form.login-form").checksley() loginForm = $el.find("form.login-form").checksley({onlyOneErrorElement: true})
onSuccessSubmitLogin = (response) -> onSuccessSubmitLogin = (response) ->
$analytics.trackEvent("auth", "invitationAccept", "invitation accept with existing user", 1) $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 $confirm.notify("light-error", "According to our Oompa Loompas, your are not registered yet or
typed an invalid password.") #TODO: i18n typed an invalid password.") #TODO: i18n
submitLogin = -> submitLogin = debounce 2000, =>
if not loginForm.validate() if not loginForm.validate()
return return
@ -403,7 +404,7 @@ InvitationDirective = ($auth, $confirm, $location, $params, $navUrls, $analytics
$confirm.notify("light-error", "According to our Oompa Loompas, that $confirm.notify("light-error", "According to our Oompa Loompas, that
username or email is already in use.") #TODO: i18n username or email is already in use.") #TODO: i18n
submitRegister = -> submitRegister = debounce 2000, =>
if not registerForm.validate() if not registerForm.validate()
return return

View File

@ -40,18 +40,18 @@ BacklogFiltersDirective = ($log, $location) ->
<% _.each(filters, function(f) { %> <% _.each(filters, function(f) { %>
<% if (f.selected) { %> <% if (f.selected) { %>
<a class="single-filter active" <a class="single-filter active"
data-type="<%= f.type %>" data-type="<%- f.type %>"
data-id="<%= f.id %>"> data-id="<%- f.id %>">
<span class="name" <% if (f.color){ %>style="border-left: 3px solid <%= f.color %>;"<% } %>> <span class="name" <% if (f.color){ %>style="border-left: 3px solid <%- f.color %>;"<% } %>>
<%- f.name %> <%- f.name %>
</span> </span>
<span class="number"><%- f.count %></span> <span class="number"><%- f.count %></span>
</a> </a>
<% } else { %> <% } else { %>
<a class="single-filter" <a class="single-filter"
data-type="<%= f.type %>" data-type="<%- f.type %>"
data-id="<%= f.id %>"> data-id="<%- f.id %>">
<span class="name" <% if (f.color){ %>style="border-left: 3px solid <%= f.color %>;"<% } %>> <span class="name" <% if (f.color){ %>style="border-left: 3px solid <%- f.color %>;"<% } %>>
<%- f.name %> <%- f.name %>
</span> </span>
<span class="number"><%- f.count %></span> <span class="number"><%- f.count %></span>
@ -63,9 +63,9 @@ BacklogFiltersDirective = ($log, $location) ->
templateSelected = _.template(""" templateSelected = _.template("""
<% _.each(filters, function(f) { %> <% _.each(filters, function(f) { %>
<a class="single-filter selected" <a class="single-filter selected"
data-type="<%= f.type %>" data-type="<%- f.type %>"
data-id="<%= f.id %>"> data-id="<%- f.id %>">
<span class="name" <% if (f.color){ %>style="border-left: 3px solid <%= f.color %>;"<% } %>> <span class="name" <% if (f.color){ %>style="border-left: 3px solid <%- f.color %>;"<% } %>>
<%- f.name %></span> <%- f.name %></span>
<span class="icon icon-delete"></span> <span class="icon icon-delete"></span>
</a> </a>
@ -79,14 +79,14 @@ BacklogFiltersDirective = ($log, $location) ->
showFilters = (title, type) -> showFilters = (title, type) ->
$el.find(".filters-cats").hide() $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.breadcrumb").removeClass("hidden")
$el.find("h2 a.subfilter span.title").html(title) $el.find("h2 a.subfilter span.title").html(title)
$el.find("h2 a.subfilter span.title").prop("data-type", type) $el.find("h2 a.subfilter span.title").prop("data-type", type)
showCategories = -> showCategories = ->
$el.find(".filters-cats").show() $el.find(".filters-cats").show()
$el.find(".filter-list").hide() $el.find(".filter-list").addClass("hidden")
$el.find("h2.breadcrumb").addClass("hidden") $el.find("h2.breadcrumb").addClass("hidden")
initializeSelectedFilters = (filters) -> initializeSelectedFilters = (filters) ->

View File

@ -86,9 +86,9 @@ CreateEditSprint = ($repo, $confirm, $rs, $rootscope, lightboxService, $loading)
remove = -> remove = ->
#TODO: i18n #TODO: i18n
title = "Delete sprint" title = "Delete sprint"
subtitle = $scope.sprint.name message = $scope.sprint.name
$confirm.ask(title, subtitle).then (finish) => $confirm.askOnDelete(title, message).then (finish) =>
onSuccess = -> onSuccess = ->
finish() finish()
$scope.milestonesCounter -= 1 $scope.milestonesCounter -= 1

View File

@ -74,11 +74,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
tgLoader.pageLoaded() tgLoader.pageLoaded()
# On Error # On Error
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
initializeEventHandlers: -> initializeEventHandlers: ->
@scope.$on "usform:bulk:success", => @scope.$on "usform:bulk:success", =>
@ -431,6 +427,8 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
@scope.filters = {} @scope.filters = {}
plainTags = _.flatten(_.filter(_.map(@scope.userstories, "tags"))) plainTags = _.flatten(_.filter(_.map(@scope.userstories, "tags")))
plainTags.sort()
@scope.filters.tags = _.map _.countBy(plainTags), (v, k) => @scope.filters.tags = _.map _.countBy(plainTags), (v, k) =>
obj = { obj = {
id: k, id: k,
@ -469,9 +467,9 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
deleteUserStory: (us) -> deleteUserStory: (us) ->
#TODO: i18n #TODO: i18n
title = "Delete User Story" 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 # We modify the userstories in scope so the user doesn't see the removed US for a while
@scope.userstories = _.without(@scope.userstories, us) @scope.userstories = _.without(@scope.userstories, us)
@filterVisibleUserstories() @filterVisibleUserstories()

View File

@ -21,45 +21,26 @@
taiga = @.taiga taiga = @.taiga
mixOf = @.taiga.mixOf
toggleText = @.taiga.toggleText
scopeDefer = @.taiga.scopeDefer
bindOnce = @.taiga.bindOnce
groupBy = @.taiga.groupBy
module = angular.module("taigaBacklog") module = angular.module("taigaBacklog")
############################################################################# #############################################################################
## Sprint Directive ## Sprint Actions Directive
############################################################################# #############################################################################
BacklogSprintDirective = ($repo, $rootscope) -> BacklogSprintDirective = ($repo, $rootscope) ->
## Common parts link = ($scope, $el, $attrs) ->
linkCommon = ($scope, $el, $attrs, $ctrl) -> $scope.$watch $attrs.tgBacklogSprint, (sprint) ->
sprint = $scope.$eval($attrs.tgBacklogSprint) sprint = $scope.$eval($attrs.tgBacklogSprint)
if $scope.$first if $scope.$first
$el.addClass("sprint-current") $el.addClass("sprint-current")
$el.find(".sprint-table").addClass('open') $el.find(".sprint-table").addClass('open')
else if sprint.closed else if sprint.closed
$el.addClass("sprint-closed") $el.addClass("sprint-closed")
else if not $scope.$first and not sprint.closed else if not $scope.$first and not sprint.closed
$el.addClass("sprint-old-open") $el.addClass("sprint-old-open")
# Update progress bars
$scope.$watch $attrs.tgBacklogSprint, (value) ->
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()
# Event Handlers # Event Handlers
$el.on "click", ".sprint-name > .icon-arrow-up", (event) -> $el.on "click", ".sprint-name > .icon-arrow-up", (event) ->
target = $(event.currentTarget) target = $(event.currentTarget)
@ -67,15 +48,92 @@ BacklogSprintDirective = ($repo, $rootscope) ->
$el.find(".sprint-table").toggleClass('open') $el.find(".sprint-table").toggleClass('open')
$el.on "click", ".sprint-name > .icon-edit", (event) -> $el.on "click", ".sprint-name > .icon-edit", (event) ->
sprint = $scope.$eval($attrs.tgBacklogSprint)
$rootscope.$broadcast("sprintform:edit", sprint) $rootscope.$broadcast("sprintform:edit", sprint)
link = ($scope, $el, $attrs) ->
$ctrl = $el.closest("div.wrapper").controller()
linkCommon($scope, $el, $attrs, $ctrl)
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()
return {link: link} return {link: link}
module.directive("tgBacklogSprint", ["$tgRepo", "$rootScope", BacklogSprintDirective]) module.directive("tgBacklogSprint", ["$tgRepo", "$rootScope", BacklogSprintDirective])
#############################################################################
## Sprint Header Directive
#############################################################################
BacklogSprintHeaderDirective = ($navUrls) ->
template = _.template("""
<div class="sprint-name">
<a class="icon icon-arrow-up" href="" title="Compact Sprint"></a>
<% if(isVisible){ %>
<a href="<%- taskboardUrl %>" title="'Go to the taskboard of '<%- name %>'">
<span><%- name %></span>
</a>
<% } %>
<% if(isEditable){ %>
<a class="icon icon-edit" href="" title="Edit Sprint"></a>
<% } %>
</div>
<div class="sprint-summary">
<div class="sprint-date"><%- estimatedDateRange %></div>
<ul>
<li>
<span class="number"><%- closedPoints %></span>
<span class="description">closed</span>
</li>
<li>
<span class="number"><%- totalPoints %></span>
<span class="description">total</span>
</li>
</ul>
</div>
""")
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])

View File

@ -46,6 +46,7 @@ urls = {
"home": "/" "home": "/"
"error": "/error" "error": "/error"
"not-found": "/not-found" "not-found": "/not-found"
"permission-denied": "/permission-denied"
"login": "/login" "login": "/login"
"forgot-password": "/forgot-password" "forgot-password": "/forgot-password"
@ -66,17 +67,13 @@ urls = {
"project-search": "/project/:project/search" "project-search": "/project/:project/search"
"project-userstories-detail": "/project/:project/us/:ref" "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": "/project/:project/task/:ref"
"project-tasks-detail-edit": "/project/:project/task/:ref/edit"
"project-issues-detail": "/project/:project/issue/:ref" "project-issues-detail": "/project/:project/issue/:ref"
"project-issues-detail-edit": "/project/:project/issue/:ref/edit"
"project-wiki": "/project/:project/wiki", "project-wiki": "/project/:project/wiki",
"project-wiki-page": "/project/:project/wiki/:slug", "project-wiki-page": "/project/:project/wiki/:slug",
"project-wiki-page-edit": "/project/:project/wiki/:slug/edit",
# Admin # Admin
"project-admin-home": "/project/:project/admin/project-profile/details" "project-admin-home": "/project/:project/admin/project-profile/details"

View File

@ -72,10 +72,12 @@ class AttachmentsController extends taiga.Controller
@.attachments.push(data) @.attachments.push(data)
@rootscope.$broadcast("attachment:create") @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) index = @.uploadingAttachments.indexOf(attachment)
@.uploadingAttachments.splice(index, 1) @.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 @q.reject(data)
return promise return promise
@ -109,7 +111,8 @@ class AttachmentsController extends taiga.Controller
@.updateCounters() @.updateCounters()
@rootscope.$broadcast("attachment:edit") @rootscope.$broadcast("attachment:edit")
onError = => onError = (response) =>
$scope.$emit("attachments:size-error") if response.status == 413
@confirm.notify("error") @confirm.notify("error")
return @q.reject() return @q.reject()
@ -127,9 +130,9 @@ class AttachmentsController extends taiga.Controller
# Remove one concrete attachment. # Remove one concrete attachment.
removeAttachment: (attachment) -> removeAttachment: (attachment) ->
title = "Delete attachment" #TODO: i18in 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 = => onSuccess = =>
finish() finish()
index = @.attachments.indexOf(attachment) index = @.attachments.indexOf(attachment)
@ -139,7 +142,7 @@ class AttachmentsController extends taiga.Controller
onError = => onError = =>
finish(false) 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 @q.reject()
return @repo.remove(attachment).then(onSuccess, onError) return @repo.remove(attachment).then(onSuccess, onError)
@ -151,7 +154,7 @@ class AttachmentsController extends taiga.Controller
return not item.is_deprecated return not item.is_deprecated
AttachmentsDirective = ($confirm) -> AttachmentsDirective = ($config, $confirm) ->
template = _.template(""" template = _.template("""
<section class="attachments"> <section class="attachments">
<div class="attachments-header"> <div class="attachments-header">
@ -159,7 +162,11 @@ AttachmentsDirective = ($confirm) ->
<span class="attachments-num" tg-bind-html="ctrl.attachmentsCount"></span> <span class="attachments-num" tg-bind-html="ctrl.attachmentsCount"></span>
<span class="attachments-text">attachments</span> <span class="attachments-text">attachments</span>
</h3> </h3>
<div tg-check-permission="modify_<%- type %>" title="Add new attachment" class="add-attach"> <div tg-check-permission="modify_<%- type %>" class="add-attach"
title="Add new attachment. <%- maxFileSizeMsg %>">
<% if (maxFileSize){ %>
<span class="size-info hidden">[Max. size: <%- maxFileSize %>]</span>
<% }; %>
<label for="add-attach" class="icon icon-plus related-tasks-buttons"></label> <label for="add-attach" class="icon icon-plus related-tasks-buttons"></label>
<input id="add-attach" type="file" multiple="multiple"/> <input id="add-attach" type="file" multiple="multiple"/>
</div> </div>
@ -195,7 +202,6 @@ AttachmentsDirective = ($confirm) ->
</div> </div>
</section>""") </section>""")
link = ($scope, $el, $attrs, $ctrls) -> link = ($scope, $el, $attrs, $ctrls) ->
$ctrl = $ctrls[0] $ctrl = $ctrls[0]
$model = $ctrls[1] $model = $ctrls[1]
@ -222,6 +228,12 @@ AttachmentsDirective = ($confirm) ->
$ctrl.reorderAttachment(attachment, newIndex) $ctrl.reorderAttachment(attachment, newIndex)
$ctrl.saveAttachments() $ctrl.saveAttachments()
showSizeInfo = ->
$el.find(".size-info").removeClass("hidden")
$scope.$on "attachments:size-error", ->
showSizeInfo()
$el.on "change", ".attachments-header input", (event) -> $el.on "change", ".attachments-header input", (event) ->
files = _.toArray(event.target.files) files = _.toArray(event.target.files)
return if files.length < 1 return if files.length < 1
@ -249,7 +261,16 @@ AttachmentsDirective = ($confirm) ->
$el.off() $el.off()
templateFn = ($el, $attrs) -> 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 { return {
require: ["tgAttachments", "ngModel"] require: ["tgAttachments", "ngModel"]
@ -261,13 +282,13 @@ AttachmentsDirective = ($confirm) ->
template: templateFn template: templateFn
} }
module.directive("tgAttachments", ["$tgConfirm", AttachmentsDirective]) module.directive("tgAttachments", ["$tgConfig", "$tgConfirm", AttachmentsDirective])
AttachmentDirective = -> AttachmentDirective = ->
template = _.template(""" template = _.template("""
<div class="attachment-name"> <div class="attachment-name">
<a href="<%- url %>" title="<%- name %>" target="_blank"> <a href="<%- url %>" title="<%- name %> uploaded on <%- created_date %>" target="_blank">
<span class="icon icon-documents"></span> <span class="icon icon-documents"></span>
<span><%- name %><span> <span><%- name %><span>
</a> </a>
@ -291,7 +312,7 @@ AttachmentDirective = ->
templateEdit = _.template(""" templateEdit = _.template("""
<div class="attachment-name"> <div class="attachment-name">
<span class="icon.icon-document"></span> <span class="icon.icon-document"></span>
<a href="<%- url %>" title="<%- name %>" target="_blank"><%- name %></a> <a href="<%- url %>" title="<%- name %> uploaded on <%- created_date %>" target="_blank"><%- name %></a>
</div> </div>
<div class="attachment-size"> <div class="attachment-size">
<span><%- size %></span> <span><%- size %></span>
@ -320,6 +341,7 @@ AttachmentDirective = ->
ctx = { ctx = {
id: attachment.id id: attachment.id
name: attachment.name name: attachment.name
created_date: moment(attachment.created_date).format("DD MMM YYYY [at] hh:mm") #TODO: i18n
url: attachment.url url: attachment.url
size: sizeFormat(attachment.size) size: sizeFormat(attachment.size)
description: attachment.description description: attachment.description

View File

@ -24,6 +24,7 @@ bindOnce = @.taiga.bindOnce
module = angular.module("taigaCommon") module = angular.module("taigaCommon")
############################################################################# #############################################################################
## Date Range Directive (used mainly for sprint date range) ## Date Range Directive (used mainly for sprint date range)
############################################################################# #############################################################################
@ -46,6 +47,33 @@ DateRangeDirective = ->
module.directive("tgDateRange", 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 ## Sprint Progress Bar Directive
############################################################################# #############################################################################
@ -76,99 +104,133 @@ module.directive("tgSprintProgressbar", SprintProgressBarDirective)
############################################################################# #############################################################################
## Date Selector Directive (using pikaday) ## Created-by display directive
############################################################################# #############################################################################
DateSelectorDirective =-> CreatedByDisplayDirective = ->
link = ($scope, $el, $attrs, $model) -> # Display the owner information (full name and photo) and the date of
selectedDate = null # creation of an object (like USs, tasks and issues).
$el.picker = new Pikaday({ #
field: $el[0] # Example:
format: "DD MMM YYYY" # div.us-created-by(tg-created-by-display, ng-model="us")
onSelect: (date) => #
selectedDate = date # Requirements:
onOpen: => # - model object must have the attributes 'created_date' and
$el.picker.setDate(selectedDate) if selectedDate? # 'owner'(ng-model)
}) # - scope.usersById object is required.
$scope.$watch $attrs.ngModel, (val) -> template = _.template("""
$el.picker.setDate(val) if val? <div class="user-avatar">
<img src="<%- owner.photo %>" alt="<%- owner.full_name_display %>" />
</div>
<div class="created-by">
<span class="created-title">Created by <%- owner.full_name_display %></span>
<span class="created-date"><%- date %></span>
</div>
""") # 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 { return {
link: link link: link
restrict: "EA"
require: "ngModel" require: "ngModel"
} }
module.directive("tgDateSelector", DateSelectorDirective) module.directive("tgCreatedByDisplay", CreatedByDisplayDirective)
############################################################################# #############################################################################
## Watchers directive ## 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 # TODO: i18n
template = _.template(""" template = _.template("""
<% if(isEditable){ %>
<div class="watchers-header"> <div class="watchers-header">
<span class="title">watchers</span> <span class="title">watchers</span>
<% if (editable) { %>
<a href="" title="Add watcher" class="icon icon-plus add-watcher"></a> <a href="" title="Add watcher" class="icon icon-plus add-watcher"></a>
<% } %>
</div> </div>
<% } else if(watchers.length > 0){ %>
<div class="watchers-header">
<span class="title">watchers</span>
</div>
<% }; %>
<% _.each(watchers, function(watcher) { %> <% _.each(watchers, function(watcher) { %>
<div class="watcher-single"> <div class="watcher-single">
<div class="watcher-avatar"> <div class="watcher-avatar">
<a class="avatar" href="" title="Assigned to"> <span class="avatar" href="" title="<%- watcher.full_name_display %>">
<img src="<%= watcher.photo %>" alt="<%- watcher.full_name_display %>"> <img src="<%- watcher.photo %>" alt="<%- watcher.full_name_display %>">
</a> </span>
</div> </div>
<div class="watcher-name"> <div class="watcher-name">
<span> <span><%- watcher.full_name_display %></span>
<%- watcher.full_name_display %>
</span>
<% if (editable) { %> <% if(isEditable){ %>
<a class="icon icon-delete" <a class="icon icon-delete"
data-watcher-id="<%= watcher.id %>" href="" title="delete-watcher"> data-watcher-id="<%- watcher.id %>" href="" title="delete-watcher">
</a> </a>
<% } %> <% }; %>
</div> </div>
</div> </div>
<% }); %> <% }); %>
""") """)
link = ($scope, $el, $attrs, $model) -> 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) -> renderWatchers = (watchers) ->
html = template({watchers: watchers, editable:editable}) ctx = {
watchers: watchers
isEditable: isEditable()
}
html = template(ctx)
$el.html(html) $el.html(html)
if watchers.length == 0 if isEditable() and watchers.length == 0
if editable
$el.find(".title").text("Add watchers") $el.find(".title").text("Add watchers")
$el.find(".watchers-header").addClass("no-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()
$el.on "click", ".icon-delete", (event) -> $el.on "click", ".icon-delete", (event) ->
event.preventDefault() event.preventDefault()
return if not isEditable()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
watcherId = target.data("watcher-id") watcherId = target.data("watcher-id")
title = "Remove watcher" title = "Delete watcher"
subtitle = $scope.usersById[watcherId].full_name_display message = $scope.usersById[watcherId].full_name_display
$confirm.ask(title, subtitle).then (finish) => $confirm.askOnDelete(title, message).then (finish) =>
finish() finish()
watcherIds = _.clone($model.$modelValue.watchers, false) watcherIds = _.clone($model.$modelValue.watchers, false)
watcherIds = _.pull(watcherIds, watcherId) watcherIds = _.pull(watcherIds, watcherId)
@ -176,9 +238,11 @@ WatchersDirective = ($rootscope, $confirm) ->
item = $model.$modelValue.clone() item = $model.$modelValue.clone()
item.watchers = watcherIds item.watchers = watcherIds
$model.$setViewValue(item) $model.$setViewValue(item)
save(item)
$el.on "click", ".add-watcher", (event) -> $el.on "click", ".add-watcher", (event) ->
event.preventDefault() event.preventDefault()
return if not isEditable()
$scope.$apply -> $scope.$apply ->
$rootscope.$broadcast("watcher:add", $model.$modelValue) $rootscope.$broadcast("watcher:add", $model.$modelValue)
@ -189,86 +253,398 @@ WatchersDirective = ($rootscope, $confirm) ->
item = $model.$modelValue.clone() item = $model.$modelValue.clone()
item.watchers = watchers item.watchers = watchers
$model.$setViewValue(item) $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"} return {link:link, require:"ngModel"}
module.directive("tgWatchers", ["$rootScope", "$tgConfirm", WatchersDirective]) module.directive("tgWatchers", ["$rootScope", "$tgConfirm", "$tgRepo", WatchersDirective])
############################################################################# #############################################################################
## Assigned to directive ## 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 # TODO: i18n
template = _.template(""" template = _.template("""
<% if (assignedTo) { %> <% if (assignedTo) { %>
<div class="user-avatar"> <div class="user-avatar">
<img src="<%= assignedTo.photo %>" alt="<%- assignedTo.full_name_display %>" /> <img src="<%- assignedTo.photo %>" alt="<%- assignedTo.full_name_display %>" />
</div> </div>
<% } %> <% } %>
<div class="assigned-to"> <div class="assigned-to">
<span class="assigned-title">Assigned to</span> <span class="assigned-title">Assigned to</span>
<a href="" title="edit assignment" class="user-assigned <% if (editable) { %> editable <% } %>"> <a href="" title="edit assignment" class="user-assigned <% if(isEditable){ %>editable<% }; %>">
<span class="assigned-name">
<% if (assignedTo) { %> <% if (assignedTo) { %>
<%- assignedTo.full_name_display %> <%- assignedTo.full_name_display %>
<% } else { %> <% } else { %>
Not assigned Not assigned
<% } %> <% } %>
<% if (editable) { %> </span>
<span class="icon icon-arrow-bottom"></span> <% if(isEditable){ %><span class="icon icon-arrow-bottom"></span><% }; %>
<% } %>
</a> </a>
<% if (editable && assignedTo!==null) { %> <% if (assignedTo!==null && isEditable) { %>
<a href="" title="delete assignment" class="icon icon-delete"></a> <a href="" title="delete assignment" class="icon icon-delete"></a>
<% } %> <% } %>
</div> </div>
""") """) # TODO: i18n
link = ($scope, $el, $attrs, $model) -> 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) -> renderAssignedTo = (issue) ->
assignedToId = issue?.assigned_to assignedToId = issue?.assigned_to
assignedTo = null assignedTo = if assignedToId? then $scope.usersById[assignedToId] else null
assignedTo = $scope.usersById[assignedToId] if assignedToId?
html = template({assignedTo: assignedTo, editable:editable}) ctx = {
assignedTo: assignedTo
isEditable: isEditable()
}
html = template(ctx)
$el.html(html) $el.html(html)
$scope.$watch $attrs.ngModel, (instance) ->
renderAssignedTo(instance)
if editable
$el.on "click", ".user-assigned", (event) -> $el.on "click", ".user-assigned", (event) ->
event.preventDefault() event.preventDefault()
return if not isEditable()
$scope.$apply -> $scope.$apply ->
$rootscope.$broadcast("assigned-to:add", $model.$modelValue) $rootscope.$broadcast("assigned-to:add", $model.$modelValue)
$el.on "click", ".icon-delete", (event) -> $el.on "click", ".icon-delete", (event) ->
event.preventDefault() event.preventDefault()
title = "Remove assigned to" return if not isEditable()
subtitle = "" title = "Are you sure you want to leave it unassigned?" # TODO: i18n
$confirm.ask(title, subtitle).then (finish) => $confirm.ask(title).then (finish) =>
finish() finish()
$model.$modelValue.assigned_to = null $model.$modelValue.assigned_to = null
renderAssignedTo($model.$modelValue) save($model.$modelValue)
$scope.$on "assigned-to:added", (ctx, userId) -> $scope.$on "assigned-to:added", (ctx, userId, item) ->
return if item.id != $model.$modelValue.id
$model.$modelValue.assigned_to = userId $model.$modelValue.assigned_to = userId
renderAssignedTo($model.$modelValue) save($model.$modelValue)
$scope.$watch $attrs.ngModel, (instance) ->
renderAssignedTo(instance)
$scope.$on "$destroy", ->
$el.off()
return { return {
link:link, link:link,
require:"ngModel" require:"ngModel"
} }
module.directive("tgAssignedTo", ["$rootScope", "$tgConfirm", "$tgRepo", "$tgLoading", AssignedToDirective])
module.directive("tgAssignedTo", ["$rootScope", "$tgConfirm", AssignedToDirective])
#############################################################################
## Block Button directive
#############################################################################
BlockButtonDirective = ($rootscope, $loading) ->
template = """
<a href="#" class="button button-gray item-block">Block</a>
<a href="#" class="button button-red item-unblock">Unblock</a>
"""
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 = """
<a href="" class="button button-red">Delete</a>
""" #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 = """
<div class="view-subject">
{{ item.subject }}
<a class="edit icon icon-edit" href="" title="Edit" />
</div>
<div class="edit-subject">
<input type="text" ng-model="item.subject" data-required="true" data-maxlength="500"/>
<span class="save-container">
<a class="save icon icon-floppy" href="" title="Save" />
</span>
</div>
"""
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 = """
<div class="view-description">
<section class="us-content wysiwyg"
tg-bind-html="item.description_html || noDescriptionMsg"></section>
<span class="edit icon icon-edit" href="" title="Edit" />
</div>
<div class="edit-description">
<textarea placeholder="Empty space is so boring... go on be descriptive... A rose by any other name would smell as sweet..."
ng-model="item.description"
tg-markitup="tg-markitup"></textarea>
<span class="save-container">
<a class="save icon icon-floppy" href="" title="Save" />
</span>
</div>
""" # TODO: i18n
noDescriptionMegEditMode = """
<p class="no-description editable">
Empty space is so boring...
go on be descriptive...
A rose by any other name would smell as sweet...
</p>
""" # TODO: i18n
noDescriptionMegReadMode = """
<p class="no-description">
No description yet.
</p>
""" # 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 = -> ListItemAssignedtoDirective = ->
template = _.template(""" template = _.template("""
<figure class="avatar"> <figure class="avatar">
<img src="<%= imgurl %>" alt="<%- name %>"/> <img src="<%- imgurl %>" alt="<%- name %>"/>
<figcaption><%- name %></figcaption> <figcaption><%- name %></figcaption>
</figure> </figure>
""") """)
@ -377,6 +753,7 @@ ListItemSeverityDirective = ->
template: template template: template
} }
ListItemTypeDirective = -> ListItemTypeDirective = ->
template = """ template = """
<div class="level"></div> <div class="level"></div>

View File

@ -50,14 +50,13 @@ class ConfirmService extends taiga.Service
el.off(".confirm-dialog") 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) el = angular.element(lightboxSelector)
# Render content # Render content
el.find("h2.title").html(title) el.find("h2.title").html(title)
el.find("span.subtitle").html(subtitle) el.find("span.subtitle").html(subtitle)
if message el.find("span.message").html(message)
el.find("span.delete-question").html(message)
defered = @q.defer() defered = @q.defer()
@ -80,13 +79,27 @@ class ConfirmService extends taiga.Service
return defered.promise 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) el = angular.element(lightboxSelector)
# Render content # Render content
el.find("h2.title").html(title) el.find(".title").html(title)
el.find("span.subtitle").html(subtitle) el.find(".subtitle").html(subtitle)
choicesField = el.find("select.choices")
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('') choicesField.html('')
_.each choices, (value, key) -> _.each choices, (value, key) ->
choicesField.append(angular.element("<option value='#{key}'>#{value}</option>")) choicesField.append(angular.element("<option value='#{key}'>#{value}</option>"))

View File

@ -84,11 +84,11 @@ HistoryDirective = ($log, $loading) ->
<div class="activity-fromto"> <div class="activity-fromto">
<p> <p>
<strong> from </strong> <br /> <strong> from </strong> <br />
<span><%= point[0] %></span> <span><%- point[0] %></span>
</p> </p>
<p> <p>
<strong> to </strong> <br /> <strong> to </strong> <br />
<span><%= point[1] %></span> <span><%- point[1] %></span>
</p> </p>
</div> </div>
</div> </div>
@ -103,11 +103,11 @@ HistoryDirective = ($log, $loading) ->
<div class="activity-fromto"> <div class="activity-fromto">
<p> <p>
<strong> from </strong> <br /> <strong> from </strong> <br />
<span><%= from %></span> <span><%- from %></span>
</p> </p>
<p> <p>
<strong> to </strong> <br /> <strong> to </strong> <br />
<span><%= to %></span> <span><%- to %></span>
</p> </p>
</div> </div>
</div> </div>
@ -120,12 +120,12 @@ HistoryDirective = ($log, $loading) ->
<div class="activity-fromto"> <div class="activity-fromto">
<% _.each(diff, function(change) { %> <% _.each(diff, function(change) { %>
<p> <p>
<strong><%= change.name %> from </strong> <br /> <strong><%- change.name %> from </strong> <br />
<span><%= change.from %></span> <span><%- change.from %></span>
</p> </p>
<p> <p>
<strong><%= change.name %> to </strong> <br /> <strong><%- change.name %> to </strong> <br />
<span><%= change.to %></span> <span><%- change.to %></span>
</p> </p>
<% }) %> <% }) %>
</div> </div>

View File

@ -127,16 +127,33 @@ module.directive("lightbox", ["lightboxService", LightboxDirective])
# Issue/Userstory blocking message lightbox directive. # Issue/Userstory blocking message lightbox directive.
BlockLightboxDirective = (lightboxService) -> BlockLightboxDirective = ($rootscope, $tgrepo, $confirm, lightboxService, $loading) ->
link = ($scope, $el, $attrs, $model) -> link = ($scope, $el, $attrs, $model) ->
$el.find("h2.title").text($attrs.title) $el.find("h2.title").text($attrs.title)
$scope.$on "block", -> $scope.$on "block", ->
$el.find(".reason").val($model.$modelValue.blocked_note)
lightboxService.open($el) lightboxService.open($el)
$scope.$on "unblock", -> $scope.$on "unblock", (event, model, finishCallback) ->
$model.$modelValue.is_blocked = false item = $model.$modelValue.clone()
$model.$modelValue.blocked_note_html = "" 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", -> $scope.$on "$destroy", ->
$el.off() $el.off()
@ -144,19 +161,34 @@ BlockLightboxDirective = (lightboxService) ->
$el.on "click", ".button-green", (event) -> $el.on "click", ".button-green", (event) ->
event.preventDefault() event.preventDefault()
$scope.$apply -> item = $model.$modelValue.clone()
$model.$modelValue.is_blocked = true item.is_blocked = true
$model.$modelValue.blocked_note = $el.find(".reason").val() item.blocked_note = $el.find(".reason").val()
$model.$setViewValue(item)
$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) lightboxService.close($el)
return { return {
templateUrl: "/partials/views/modules/lightbox-block.html" templateUrl: "/partials/views/modules/lightbox-block.html"
link:link, link: link
require:"ngModel" 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) -> CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService, $loading) ->
link = ($scope, $el, attrs) -> link = ($scope, $el, attrs) ->
isNew = true $scope.isNew = true
$scope.$on "usform:new", (ctx, projectId, status, statusList) -> $scope.$on "usform:new", (ctx, projectId, status, statusList) ->
isNew = true $scope.isNew = true
$scope.usStatusList = statusList $scope.usStatusList = statusList
$scope.us = { $scope.us = {
@ -229,7 +261,7 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
$scope.$on "usform:edit", (ctx, us) -> $scope.$on "usform:edit", (ctx, us) ->
$scope.us = us $scope.us = us
isNew = false $scope.isNew = false
# Update texts for edition # Update texts for edition
$el.find(".button-green span").html("Save") #TODO: i18n $el.find(".button-green span").html("Save") #TODO: i18n
@ -264,7 +296,7 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
$loading.start(target) $loading.start(target)
if isNew if $scope.isNew
promise = $repo.create("userstories", $scope.us) promise = $repo.create("userstories", $scope.us)
broadcastEvent = "usform:new:success" broadcastEvent = "usform:new:success"
else else
@ -358,7 +390,7 @@ usersTemplate = _.template("""
<div class="watcher-single active"> <div class="watcher-single active">
<div class="watcher-avatar"> <div class="watcher-avatar">
<a href="" title="Assigned to" class="avatar"> <a href="" title="Assigned to" class="avatar">
<img src="<%= selected.photo %>"/> <img src="<%- selected.photo %>"/>
</a> </a>
</div> </div>
<a href="" title="<%- selected.full_name_display %>" class="watcher-name"> <a href="" title="<%- selected.full_name_display %>" class="watcher-name">
@ -372,7 +404,7 @@ usersTemplate = _.template("""
<div class="watcher-single" data-user-id="<%- user.id %>"> <div class="watcher-single" data-user-id="<%- user.id %>">
<div class="watcher-avatar"> <div class="watcher-avatar">
<a href="#" title="Assigned to" class="avatar"> <a href="#" title="Assigned to" class="avatar">
<img src="<%= user.photo %>" /> <img src="<%- user.photo %>" />
</a> </a>
</div> </div>
<a href="" title="<%- user.full_name_display %>" class="watcher-name"> <a href="" title="<%- user.full_name_display %>" class="watcher-name">

View File

@ -0,0 +1,41 @@
###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: modules/common/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])

View File

@ -43,6 +43,9 @@ TagsDirective = ->
$ctrl.$formatters.push(formatter) $ctrl.$formatters.push(formatter)
$ctrl.$parsers.push(parser) $ctrl.$parsers.push(parser)
$scope.$on "$destroy", ->
$el.off()
return { return {
require: "ngModel" require: "ngModel"
link: link link: link
@ -73,6 +76,7 @@ ColorizeTagsDirective = ->
link = ($scope, $el, $attrs, $ctrl) -> link = ($scope, $el, $attrs, $ctrl) ->
render = (srcTags) -> render = (srcTags) ->
template = templates[$attrs.tgColorizeTagsType] template = templates[$attrs.tgColorizeTagsType]
srcTags.sort()
tags = _.map srcTags, (tag) -> tags = _.map srcTags, (tag) ->
color = $scope.project.tags_colors[tag] color = $scope.project.tags_colors[tag]
return {name: tag, color: color} return {name: tag, color: color}
@ -83,86 +87,118 @@ ColorizeTagsDirective = ->
$scope.$watch $attrs.tgColorizeTags, (tags) -> $scope.$watch $attrs.tgColorizeTags, (tags) ->
render(tags) if tags? render(tags) if tags?
$scope.$on "$destroy", ->
$el.off()
return {link: link} return {link: link}
module.directive("tgColorizeTags", ColorizeTagsDirective) module.directive("tgColorizeTags", ColorizeTagsDirective)
############################################################################# #############################################################################
## TagLine (possible should be moved as generic directive) ## TagLine Directive (for Lightboxes)
############################################################################# #############################################################################
TagLineDirective = ($log, $rs) -> LbTagLineDirective = ($rs) ->
# Main directive template (rendered by angular) ENTER_KEY = 13
template = """ template = """
<div class="tags-container"></div> <div class="tags-container"></div>
<input type="text" placeholder="Write tag..." class="tag-input" /> <input type="text" placeholder="I'm it! Tag me..." class="tag-input" />
<a href="" title="Save" class="save icon icon-floppy"></a> <a href="" title="Save" class="save icon icon-floppy hidden"></a>
""" """ # TODO: i18n
# Tags template (rendered manually using lodash) # Tags template (rendered manually using lodash)
templateTags = _.template(""" templateTags = _.template("""
<% _.each(tags, function(tag) { %> <% _.each(tags, function(tag) { %>
<div class="tag" style="border-left: 5px solid <%- tag.color %>;"> <span class="tag" style="border-left: 5px solid <%- tag.color %>;">
<span class="tag-name"><%- tag.name %></span> <span class="tag-name"><%- tag.name %></span>
<% if (editable) { %>
<a href="" title="delete tag" class="icon icon-delete"></a> <a href="" title="delete tag" class="icon icon-delete"></a>
<% } %> </span>
</div> <% }); %>
<% }); %>""") """) # TODO: i18n
renderTags = ($el, tags, editable, tagsColors) -> link = ($scope, $el, $attrs, $model) ->
## Render
renderTags = (tags, tagsColors) ->
ctx = { ctx = {
tags: _.map(tags, (t) -> {name: t, color: tagsColors[t]}) tags: _.map(tags, (t) -> {name: t, color: tagsColors[t]})
editable: editable
} }
html = templateTags(ctx) html = templateTags(ctx)
$el.find("div.tags-container").html(html) $el.find("div.tags-container").html(html)
normalizeTags = (tags) -> showSaveButton = -> $el.find(".save").removeClass("hidden")
tags = _.map(tags, trim) hideSaveButton = -> $el.find(".save").addClass("hidden")
tags = _.map(tags, (x) -> x.toLowerCase())
return _.uniq(tags)
link = ($scope, $el, $attrs, $model) -> resetInput = ->
editable = if $attrs.editable == "true" then true else false $el.find("input").val("")
$el.addClass("tags-block") $el.find("input").autocomplete("close")
## Aux methods
addValue = (value) -> addValue = (value) ->
value = trim(value) value = trim(value.toLowerCase())
return if value.length <= 0 return if value.length == 0
tags = _.clone($model.$modelValue, false) tags = _.clone($model.$modelValue, false)
tags = [] if not tags? tags = [] if not tags?
tags.push(value) tags.push(value) if value not in tags
$scope.$apply -> $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 = () -> saveInputTag = () ->
input = $el.find('input') value = $el.find("input").val()
addValue(input.val()) addValue(value)
input.val("") resetInput()
input.autocomplete("close") hideSaveButton()
$el.find('.save').hide()
$scope.$watch $attrs.ngModel, (val) -> ## Events
tags_colors = if $scope.project?.tags_colors? then $scope.project.tags_colors else [] $el.on "keypress", "input", (event) ->
renderTags($el, val, editable, tags_colors) return if event.keyCode != ENTER_KEY
event.preventDefault()
bindOnce $scope, "projectId", (projectId) -> $el.on "keyup", "input", (event) ->
# If not editable, no tags preloading is needed. target = angular.element(event.currentTarget)
return if not editable
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) -> positioningFunction = (position, elements) ->
menu = elements.element.element menu = elements.element.element
menu.css("width", elements.target.width) menu.css("width", elements.target.width)
menu.css("top", position.top) menu.css("top", position.top)
menu.css("left", position.left) menu.css("left", position.left)
$rs.projects.tags(projectId).then (data) ->
$el.find("input").autocomplete({ $el.find("input").autocomplete({
source: data source: _.keys(project.tags_colors)
position: { position: {
my: "left top", my: "left top",
using: positioningFunction using: positioningFunction
@ -172,38 +208,12 @@ TagLineDirective = ($log, $rs) ->
ui.item.value = "" ui.item.value = ""
}) })
if not editable $scope.$watch $attrs.ngModel, (tags) ->
$el.find("input").remove() tagsColors = $scope.project?.tags_colors or []
renderTags(tags, tagsColors)
$el.on "keypress", "input", (event) -> $scope.$on "$destroy", ->
return if event.keyCode != 13 $el.off()
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))
return { return {
link:link, link:link,
@ -211,4 +221,198 @@ TagLineDirective = ($log, $rs) ->
template: template 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 = """
<div class="tags-container"></div>
<a href="#" class="add-tag hidden" title="Add tag">
<span class="icon icon-plus"></span>
<span class="add-tag-text">Add tag</span>
</a>
<input type="text" placeholder="I'm it! Tag me..." class="tag-input hidden" />
<a href="" title="Save" class="save icon icon-floppy hidden"></a>
""" # TODO: i18n
# Tags template (rendered manually using lodash)
templateTags = _.template("""
<% _.each(tags, function(tag) { %>
<span class="tag" style="border-left: 5px solid <%- tag.color %>;">
<span class="tag-name"><%- tag.name %></span>
<% if (isEditable) { %>
<a href="" title="delete tag" class="icon icon-delete"></a>
<% } %>
</span>
<% }); %>
""") # 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])

View File

@ -32,7 +32,7 @@ tgMarkitupDirective = ($rootscope, $rs, $tr) ->
previewTemplate = _.template(""" previewTemplate = _.template("""
<div class="preview"> <div class="preview">
<div class="actions"> <div class="actions">
<a href="#" title="Edit">Edit</a> <a href="#" title="Edit" class="icon icon-edit edit"></a>
</div> </div>
<div class="content wysiwyg"> <div class="content wysiwyg">
<%= data %> <%= data %>
@ -96,17 +96,23 @@ tgMarkitupDirective = ($rootscope, $rs, $tr) ->
onEnter: onEnter:
keepDefault: false keepDefault: false
replaceWith: (data) => replaceWith: (data) =>
lines = data.textarea.value[0..(data.caretPosition - 1)].split("\n") lines = data.textarea.value.split("\n")
lastLine = lines[lines.length - 1] 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 - # unordered list -
match = lastLine.match /^(\s*- ).*/ match = lastLine.match /^(\s*- ).*/
if match if match
emptyListItem = lastLine.match /^(\s*)\-\s$/ emptyListItem = lastLine.match /^(\s*)\-\s$/
if emptyListItem if emptyListItem
markdownCaretPositon = removeEmptyLine(data.textarea, lines.length - 1, data.caretPosition) markdownCaretPositon = removeEmptyLine(data.textarea, lines.length - 1, data.caretPosition)
else else
breakLineAtBeginning = newLineContent.match /^(\s*)\-\s/
if !breakLineAtBeginning
return "\n#{match[1]}" if match return "\n#{match[1]}" if match
# unordered list * # unordered list *
@ -118,6 +124,9 @@ tgMarkitupDirective = ($rootscope, $rs, $tr) ->
if emptyListItem if emptyListItem
markdownCaretPositon = removeEmptyLine(data.textarea, lines.length - 1, data.caretPosition) markdownCaretPositon = removeEmptyLine(data.textarea, lines.length - 1, data.caretPosition)
else else
breakLineAtBeginning = newLineContent.match /^(\s*)\*\s/
if !breakLineAtBeginning
return "\n#{match[1]}" if match return "\n#{match[1]}" if match
# ordered list # ordered list
@ -129,6 +138,9 @@ tgMarkitupDirective = ($rootscope, $rs, $tr) ->
if emptyListItem if emptyListItem
markdownCaretPositon = removeEmptyLine(data.textarea, lines.length - 1, data.caretPosition) markdownCaretPositon = removeEmptyLine(data.textarea, lines.length - 1, data.caretPosition)
else else
breakLineAtBeginning = newLineContent.match /^(\s*)(\d+)\.\s/
if !breakLineAtBeginning
return "\n#{match[1] + (parseInt(match[2], 10) + 1)}. " return "\n#{match[1] + (parseInt(match[2], 10) + 1)}. "
return "\n" return "\n"

View File

@ -46,11 +46,12 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
"$log", "$log",
"$appTitle", "$appTitle",
"$tgAnalytics", "$tgAnalytics",
"$tgNavUrls" "$tgNavUrls",
"tgLoader"
] ]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location,
@log, @appTitle, @analytics, @navUrls) -> @log, @appTitle, @analytics, @navUrls, tgLoader) ->
@scope.issueRef = @params.issueref @scope.issueRef = @params.issueref
@scope.sectionName = "Issue Details" @scope.sectionName = "Issue Details"
@.initializeEventHandlers() @.initializeEventHandlers()
@ -60,14 +61,11 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
# On Success # On Success
promise.then => promise.then =>
@appTitle.set(@scope.issue.subject + " - " + @scope.project.name) @appTitle.set(@scope.issue.subject + " - " + @scope.project.name)
@.initializeOnDeleteGoToUrl()
tgLoader.pageLoaded()
# On Error # On Error
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
initializeEventHandlers: -> initializeEventHandlers: ->
@scope.$on "attachment:create", => @scope.$on "attachment:create", =>
@ -85,6 +83,13 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@rootscope.$broadcast("history:reload") @rootscope.$broadcast("history:reload")
@.loadIssue() @.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: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
@scope.project = project @scope.project = project
@ -134,78 +139,24 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
.then(=> @.loadUsersAndRoles()) .then(=> @.loadUsersAndRoles())
.then(=> @.loadIssue()) .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) module.controller("IssueDetailController", IssueDetailController)
############################################################################# #############################################################################
## Issue Main Directive ## Issue status display directive
############################################################################# #############################################################################
IssueDirective = ($tgrepo, $log, $location, $confirm, $navUrls, $loading) -> IssueStatusDisplayDirective = ->
linkSidebar = ($scope, $el, $attrs, $ctrl) -> # 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
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(""" template = _.template("""
<h1>
<span> <span>
<% if (status.is_closed) { %> <% if (status.is_closed) { %>
Closed Closed
@ -213,171 +164,407 @@ IssueStatusDirective = () ->
Open Open
<% } %> <% } %>
</span> </span>
<span class="us-detail-status" style="color:<%= status.color %>"><%= status.name %></span> <span class="us-detail-status" style="color:<%- status.color %>">
</h1> <%- status.name %>
<div class="us-created-by"> </span>
<div class="user-avatar"> """) # TODO: i18n
<img src="<%= owner.photo %>" alt="<%- owner.full_name_display %>" />
</div>
<div class="created-by"> link = ($scope, $el, $attrs) ->
<span class="created-title">Created by <%- owner.full_name_display %></span> render = (issue) ->
<span class="created-date"><%- date %></span>
</div>
</div>
<div class="issue-data">
<div class="type-data <% if (editable) { %>clickable<% } %>">
<span class="level" style="background-color:<%= type.color %>"></span>
<span class="type-status"><%= type.name %></span>
<% if (editable) { %>
<span class="icon icon-arrow-bottom"></span>
<% } %>
<span class="level-name">type</span>
</div>
<div class="severity-data <% if (editable) { %>clickable<% } %>">
<span class="level" style="background-color:<%= severity.color %>"></span>
<span class="severity-status"><%= severity.name %></span>
<% if (editable) { %>
<span class="icon icon-arrow-bottom"></span>
<% } %>
<span class="level-name">severity</span>
</div>
<div class="priority-data <% if (editable) { %>clickable<% } %>">
<span class="level" style="background-color:<%= priority.color %>"></span>
<span class="priority-status"><%= priority.name %></span>
<% if (editable) { %>
<span class="icon icon-arrow-bottom"></span>
<% } %>
<span class="level-name">priority</span>
</div>
<div class="status-data <% if (editable) { %>clickable<% } %>">
<span class="level" style="background-color:<%= status.color %>"></span>
<span class="status-status"><%= status.name %></span>
<% if (editable) { %>
<span class="icon icon-arrow-bottom"></span>
<% } %>
<span class="level-name">status</span>
</div>
</div>
""")
selectionTypeTemplate = _.template("""
<ul class="popover pop-type">
<% _.each(types, function(type) { %>
<li><a href="" class="type" title="<%- type.name %>"
data-type-id="<%- type.id %>"><%- type.name %></a></li>
<% }); %>
</ul>
""")
selectionSeverityTemplate = _.template("""
<ul class="popover pop-severity">
<% _.each(severities, function(severity) { %>
<li><a href="" class="severity" title="<%- severity.name %>"
data-severity-id="<%- severity.id %>"><%- severity.name %></a></li>
<% }); %>
</ul>
""")
selectionPriorityTemplate = _.template("""
<ul class="popover pop-priority">
<% _.each(priorities, function(priority) { %>
<li><a href="" class="priority" title="<%- priority.name %>"
data-priority-id="<%- priority.id %>"><%- priority.name %></a></li>
<% }); %>
</ul>
""")
selectionStatusTemplate = _.template("""
<ul class="popover pop-status">
<% _.each(statuses, function(status) { %>
<li><a href="" class="status" title="<%- status.name %>"
data-status-id="<%- status.id %>"><%- status.name %></a></li>
<% }); %>
</ul>
""")
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]
html = template({ html = template({
owner: owner status: $scope.statusById[issue.status]
date: date
editable: editable
status: status
severity: severity
priority: priority
type: type
}) })
$el.html(html) $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) -> $scope.$watch $attrs.ngModel, (issue) ->
if issue? render(issue) if issue?
renderIssuestatus(issue)
if editable $scope.$on "$destroy", ->
$el.on "click", ".type-data", (event) -> $el.off()
event.preventDefault()
event.stopPropagation()
$el.find(".pop-type").popover().open()
$el.on "click", ".type", (event) -> return {
event.preventDefault() link: link
event.stopPropagation() restrict: "EA"
target = angular.element(event.currentTarget) require: "ngModel"
$model.$modelValue.type = target.data("type-id") }
renderIssuestatus($model.$modelValue)
$.fn.popover().closeAll()
$el.on "click", ".severity-data", (event) -> module.directive("tgIssueStatusDisplay", IssueStatusDisplayDirective)
event.preventDefault()
event.stopPropagation()
$el.find(".pop-severity").popover().open()
$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() ## Issue status button directive
event.stopPropagation() #############################################################################
$el.find(".pop-priority").popover().open()
$el.on "click", ".priority", (event) -> IssueStatusButtonDirective = ($rootScope, $repo, $confirm, $loading) ->
event.preventDefault() # Display the status of Issue and you can edit it.
event.stopPropagation() #
target = angular.element(event.currentTarget) # Example:
$model.$modelValue.priority = target.data("priority-id") # tg-issue-status-button(ng-model="issue")
renderIssuestatus($model.$modelValue) #
$.fn.popover().closeAll() # Requirements:
# - Issue object (ng-model)
# - scope.statusById object
# - $scope.project.my_permissions
template = _.template("""
<div class="status-data <% if(editable){ %>clickable<% }%>">
<span class="level" style="background-color:<%- status.color %>"></span>
<span class="status-status"><%- status.name %></span>
<% if(editable){ %><span class="icon icon-arrow-bottom"></span><% }%>
<span class="level-name">status</span>
<ul class="popover pop-status">
<% _.each(statuses, function(st) { %>
<li><a href="" class="status" title="<%- st.name %>"
data-status-id="<%- st.id %>"><%- st.name %></a></li>
<% }); %>
</ul>
</div>
""") #TODO: i18n
link = ($scope, $el, $attrs, $model) ->
isEditable = ->
return $scope.project.my_permissions.indexOf("modify_issue") != -1
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) -> $el.on "click", ".status-data", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
return if not isEditable()
$el.find(".pop-status").popover().open() $el.find(".pop-status").popover().open()
$el.on "click", ".status", (event) -> $el.on "click", ".status", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
return if not isEditable()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
$model.$modelValue.status = target.data("status-id")
renderIssuestatus($model.$modelValue)
$.fn.popover().closeAll() $.fn.popover().closeAll()
return {link:link, require:"ngModel"} issue = $model.$modelValue.clone()
issue.status = target.data("status-id")
$model.$setViewValue(issue)
module.directive("tgIssueStatus", IssueStatusDirective) $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("""
<div class="type-data <% if(editable){ %>clickable<% }%>">
<span class="level" style="background-color:<%- type.color %>"></span>
<span class="type-type"><%- type.name %></span>
<% if(editable){ %><span class="icon icon-arrow-bottom"></span><% }%>
<span class="level-name">type</span>
<ul class="popover pop-type">
<% _.each(typees, function(tp) { %>
<li><a href="" class="type" title="<%- tp.name %>"
data-type-id="<%- tp.id %>"><%- tp.name %></a></li>
<% }); %>
</ul>
</div>
""") #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("""
<div class="severity-data <% if(editable){ %>clickable<% }%>">
<span class="level" style="background-color:<%- severity.color %>"></span>
<span class="severity-severity"><%- severity.name %></span>
<% if(editable){ %><span class="icon icon-arrow-bottom"></span><% }%>
<span class="level-name">severity</span>
<ul class="popover pop-severity">
<% _.each(severityes, function(sv) { %>
<li><a href="" class="severity" title="<%- sv.name %>"
data-severity-id="<%- sv.id %>"><%- sv.name %></a></li>
<% }); %>
</ul>
</div>
""") #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("""
<div class="priority-data <% if(editable){ %>clickable<% }%>">
<span class="level" style="background-color:<%- priority.color %>"></span>
<span class="priority-priority"><%- priority.name %></span>
<% if(editable){ %><span class="icon icon-arrow-bottom"></span><% }%>
<span class="level-name">priority</span>
<ul class="popover pop-priority">
<% _.each(priorityes, function(pr) { %>
<li><a href="" class="priority" title="<%- pr.name %>"
data-priority-id="<%- pr.id %>"><%- pr.name %></a></li>
<% }); %>
</ul>
</div>
""") #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) -> PromoteIssueToUsButtonDirective = ($rootScope, $repo, $confirm) ->
template = _.template(""" template = _.template("""
<a class="button button-gray clickable" tg-check-permission="add_us"> <a class="button button-gray editable" tg-check-permission="add_us">
Promote to User Story Promote to User Story
</a> </a>
""") # TODO: i18n """) # TODO: i18n

View File

@ -74,11 +74,7 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
tgLoader.pageLoaded() tgLoader.pageLoaded()
# On Error # On Error
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
@scope.$on "issueform:new:success", => @scope.$on "issueform:new:success", =>
@analytics.trackEvent("issue", "create", "create issue on issues list", 1) @analytics.trackEvent("issue", "create", "create issue on issues list", 1)
@ -323,11 +319,11 @@ paginatorTemplate = """
<% } %> <% } %>
<% _.each(pages, function(item) { %> <% _.each(pages, function(item) { %>
<li class="<%= item.classes %>"> <li class="<%- item.classes %>">
<% if (item.type === "page") { %> <% if (item.type === "page") { %>
<a href="" data-pagenum="<%= item.num %>"><%= item.num %></a> <a href="" data-pagenum="<%- item.num %>"><%- item.num %></a>
<% } else if (item.type === "page-active") { %> <% } else if (item.type === "page-active") { %>
<span class="active"><%= item.num %></span> <span class="active"><%- item.num %></span>
<% } else { %> <% } else { %>
<span>...</span> <span>...</span>
<% } %> <% } %>
@ -473,8 +469,8 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading) ->
<% _.each(filters, function(f) { %> <% _.each(filters, function(f) { %>
<% if (!f.selected) { %> <% if (!f.selected) { %>
<a class="single-filter" <a class="single-filter"
data-type="<%= f.type %>" data-type="<%- f.type %>"
data-id="<%= f.id %>"> data-id="<%- f.id %>">
<span class="name" <% if (f.color){ %>style="border-left: 3px solid <%- f.color %>;"<% } %>> <span class="name" <% if (f.color){ %>style="border-left: 3px solid <%- f.color %>;"<% } %>>
<%- f.name %> <%- f.name %>
</span> </span>
@ -495,9 +491,9 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading) ->
templateSelected = _.template(""" templateSelected = _.template("""
<% _.each(filters, function(f) { %> <% _.each(filters, function(f) { %>
<a class="single-filter selected" <a class="single-filter selected"
data-type="<%= f.type %>" data-type="<%- f.type %>"
data-id="<%= f.id %>"> data-id="<%- f.id %>">
<span class="name" <% if (f.color){ %>style="border-left: 3px solid <%= f.color %>;"<% } %>> <span class="name" <% if (f.color){ %>style="border-left: 3px solid <%- f.color %>;"<% } %>>
<%- f.name %> <%- f.name %>
</span> </span>
<span class="icon icon-delete"></span> <span class="icon icon-delete"></span>
@ -512,14 +508,14 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading) ->
showFilters = (title, type) -> showFilters = (title, type) ->
$el.find(".filters-cats").hide() $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.breadcrumb").removeClass("hidden")
$el.find("h2 a.subfilter span.title").html(title) $el.find("h2 a.subfilter span.title").html(title)
$el.find("h2 a.subfilter span.title").prop("data-type", type) $el.find("h2 a.subfilter span.title").prop("data-type", type)
showCategories = -> showCategories = ->
$el.find(".filters-cats").show() $el.find(".filters-cats").show()
$el.find(".filter-list").hide() $el.find(".filter-list").addClass("hidden")
$el.find("h2.breadcrumb").addClass("hidden") $el.find("h2.breadcrumb").addClass("hidden")
initializeSelectedFilters = (filters) -> initializeSelectedFilters = (filters) ->
@ -555,9 +551,10 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading) ->
initializeSelectedFilters($scope.filters) initializeSelectedFilters($scope.filters)
return null return null
filters = $scope.filters[type] 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) filter.selected = (not filter.selected)
# Convert id to null as string for properly # Convert id to null as string for properly
@ -642,9 +639,9 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading) ->
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
customFilterName = target.parent().data('id') customFilterName = target.parent().data('id')
title = "Delete custom filter" # TODO: i18n 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 = $ctrl.deleteMyFilter(customFilterName)
promise.then -> promise.then ->
promise = $ctrl.loadMyFilters() promise = $ctrl.loadMyFilters()
@ -791,7 +788,7 @@ module.directive("tgIssueStatusInlineEdition", ["$tgRepo", IssueStatusInlineEdit
IssueAssignedToInlineEditionDirective = ($repo, $rootscope, popoverService) -> IssueAssignedToInlineEditionDirective = ($repo, $rootscope, popoverService) ->
template = _.template(""" template = _.template("""
<img src="<%= imgurl %>" alt="<%- name %>"/> <img src="<%- imgurl %>" alt="<%- name %>"/>
<figcaption><%- name %></figcaption> <figcaption><%- name %></figcaption>
""") """)

View File

@ -78,12 +78,7 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
tgLoader.pageLoaded() tgLoader.pageLoaded()
# On Error # On Error
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
initializeEventHandlers: -> initializeEventHandlers: ->
@scope.$on "usform:new:success", => @scope.$on "usform:new:success", =>
@ -397,7 +392,7 @@ KanbanUserDirective = ($log) ->
template = _.template(""" template = _.template("""
<figure class="avatar"> <figure class="avatar">
<a href="#" title="Assign User Story" <% if (!clickable) {%>class="not-clickable"<% } %>> <a href="#" title="Assign User Story" <% if (!clickable) {%>class="not-clickable"<% } %>>
<img src="<%= imgurl %>" alt="<%- name %>" class="avatar"> <img src="<%- imgurl %>" alt="<%- name %>" class="avatar">
</a> </a>
</figure> </figure>
""") # TODO: i18n """) # TODO: i18n

View File

@ -272,7 +272,7 @@ ProjectMenuDirective = ($log, $compile, $auth, $rootscope, $tgAuth, $location, $
<li><a href="" title="Logout" class="logout">Logout</a></li> <li><a href="" title="Logout" class="logout">Logout</a></li>
</ul> </ul>
<a href="" title="User preferences" class="avatar" id="nav-user-settings"> <a href="" title="User preferences" class="avatar" id="nav-user-settings">
<img src="<%= user.photo %>" alt="<%= user.full_name_display %>" /> <img src="<%- user.photo %>" alt="<%- user.full_name_display %>" />
</a> </a>
</div> </div>
</div> </div>
@ -281,41 +281,30 @@ ProjectMenuDirective = ($log, $compile, $auth, $rootscope, $tgAuth, $location, $
mainTemplate = _.template(""" mainTemplate = _.template("""
<div class="logo-container logo"> <div class="logo-container logo">
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" <svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" version="1.1" preserveAspectRatio="xMidYMid meet">
viewBox="0 0 134.2 134.3" version="1.1" preserveAspectRatio="xMidYMid meet" shape-rendering="geometricPrecision">
<style> <style>
svg {
transform: scale(.99);
}
path { path {
fill:#f5f5f5; fill:#f5f5f5;
opacity:0.7; opacity:0.7;
} }
.moustache {
fill:#000;
}
</style> </style>
<g transform="translate(-307.87667,-465.22863)"> <g transform="translate(0,-52.362183)">
<g class="bottom"> <g transform="matrix(1.1783562,0,0,1.1783562,2450.4425,-1298.9778)">
<path transform="matrix(-0.14066483,0.99005727,-0.99005727,0.14066483,0,0)" <path transform="matrix(-0.1406648,0.99005728,-0.99005728,0.14066481,0,0)" d="m1114.3 1212.6 263 0 0 263-263 0z" />
d="m561.8-506.6 42 0 0 42-42 0z" /> <path transform="matrix(0.1406648,-0.99005728,0.99005728,-0.14066481,0,0)" d="m-1640.3-1738.9 263 0 0 263-263 0z" />
<path transform="matrix(0.14066483,-0.99005727,0.99005727,-0.14066483,0,0)" <path transform="matrix(0.99005728,0.1406648,0.14066481,0.99005728,0,0)" d="m-2199.2 1599.4 263 0 0 263-263 0z" />
d="m-645.7 422.6 42 0 0 42-42 0z" /> <path transform="matrix(-0.99005728,-0.1406648,-0.14066481,-0.99005728,0,0)" d="m1673.2-2125.1 263 0 0 263-263 0z" />
<path transform="matrix(0.99005727,0.14066483,0.14066483,0.99005727,0,0)" <path transform="matrix(-0.60061118,-0.79954125,0.60061117,-0.79954125,0,0)" d="m132.1-2623.1 263 0 0 263-263 0z" />
d="m266.6 451.9 42 0 0 42-42 0z" /> <path transform="matrix(-0.79954125,0.60061118,-0.79954125,-0.60061117,0,0)" d="m2079.7-535.4 263 0 0 263-263 0z" />
<path transform="matrix(-0.99005727,-0.14066483,-0.14066483,-0.99005727,0,0)" <path transform="matrix(0.60061118,0.79954125,-0.60061118,0.79954125,0,0)" d="m-658.4 2097.4 263 0 0 263-263 0z" />
d="m-350.6-535.9 42 0 0 42-42 0z" /> <path transform="matrix(0.79954125,-0.60061118,0.79954125,0.60061117,0,0)" d="m-2606.2 10.1 263 0 0 263-263 0z" />
</g> <path transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" d="m-130.2 2210.7 141.4 0 0 141.4-141.4 0z" />
<g class="top">
<path transform="matrix(-0.60061118,-0.79954125,0.60061118,-0.79954125,0,0)"
d="m-687.1-62.7 42 0 0 42-42 0z" />
<path transform="matrix(-0.79954125,0.60061118,-0.79954125,-0.60061118,0,0)"
d="m166.6-719.6 42 0 0 42-42 0z" />
<path transform="matrix(0.60061118,0.79954125,-0.60061118,0.79954125,0,0)"
d="m603.1-21.3 42 0 0 42-42 0z" />
<path transform="matrix(0.79954125,-0.60061118,0.79954125,0.60061118,0,0)"
d="m-250.7 635.8 42 0 0 42-42 0z" />
<path transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
d="m630.3 100 22.6 0 0 22.6-22.6 0z" />
</g> </g>
</g> </g>
<path class="moustache" d="m197.1 669.9c0 0-11.5 83.8 58.5 113.8 60.9 26.1 109.3 13 144.4 0.7 35.1-12.4 98.9-66.3 98.9-66.3 0 0 92.3 74.1 154.1 80.6 61.8 6.5 100.4-9.5 119-28 45.1-44.9 34.5-102.8 34.5-102.8 0 0-43.6 31.9-88.4 11.7-44.9-20.2-63.7-73.6-109.8-90.5-46.2-16.9-78.1 0.8-91.8 13.1-13.7 12.4-15.6 16.9-15.6 16.9 0 0-28-31.2-63.1-33.8-35.1-2.6-59.8 15.6-91 46.2-31.2 30.6-48.1 54-83.9 55.3-35.8 1.3-65.7-16.9-65.7-16.9z" />
</svg> </svg>
<span class="item">taiga<sup>[beta]</sup></span> <span class="item">taiga<sup>[beta]</sup></span>
</div> </div>

View File

@ -26,7 +26,7 @@ debounce = @.taiga.debounce
module = angular.module("taigaProject") 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) -> link = ($scope, $el, attrs) ->
$scope.data = {} $scope.data = {}
$scope.templates = [] $scope.templates = []
@ -34,6 +34,11 @@ CreateProject = ($rootscope, $repo, $confirm, $location, $navurls, $rs, $project
form = $el.find("form").checksley({"onlyOneErrorElement": true}) form = $el.find("form").checksley({"onlyOneErrorElement": true})
onSuccessSubmit = (response) -> 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") $rootscope.$broadcast("projects:reload")
$confirm.notify("success", "Success") #TODO: i18n $confirm.notify("success", "Success") #TODO: i18n
$location.url($projectUrl.get(response)) $location.url($projectUrl.get(response))
@ -116,7 +121,7 @@ CreateProject = ($rootscope, $repo, $confirm, $location, $navurls, $rs, $project
return {link:link} return {link:link}
module.directive("tgLbCreateProject", ["$rootScope", "$tgRepo", "$tgConfirm", "$location", "$tgNavUrls", module.directive("tgLbCreateProject", ["$rootScope", "$tgRepo", "$tgConfirm", "$location", "$tgNavUrls",
"$tgResources", "$projectUrl", "lightboxService", CreateProject]) "$tgResources", "$projectUrl", "lightboxService", "$cacheFactory", CreateProject])
############################################################################# #############################################################################

View File

@ -53,11 +53,7 @@ class ProjectsController extends taiga.Controller
@scope.$emit("projects:loaded") @scope.$emit("projects:loaded")
@tgLoader.pageLoaded() @tgLoader.pageLoaded()
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
loadInitialData: -> loadInitialData: ->
return @rs.projects.list().then (projects) => return @rs.projects.list().then (projects) =>
@ -96,11 +92,7 @@ class ProjectController extends taiga.Controller
promise.then () => promise.then () =>
@appTitle.set(@scope.project.name) @appTitle.set(@scope.project.name)
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
loadInitialData: -> loadInitialData: ->
# Resolve project slug # Resolve project slug

View File

@ -53,7 +53,7 @@ RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading) ->
</a> </a>
</div> </div>
<div tg-related-task-assigned-to-inline-edition="task" class="assigned-to"> <div tg-related-task-assigned-to-inline-edition="task" class="assigned-to">
<div title="Assigned to" class="task-assignedto"> <div title="Assigned to" class="task-assignedto <% if(perms.modify_task) { %>editable<% } %>">
<figure class="avatar"></figure> <figure class="avatar"></figure>
<% if(perms.modify_task) { %> <% if(perms.modify_task) { %>
<span class="icon icon-arrow-bottom"></span> <span class="icon icon-arrow-bottom"></span>
@ -139,9 +139,9 @@ RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading) ->
#TODO: i18n #TODO: i18n
task = $model.$modelValue task = $model.$modelValue
title = "Delete Task" 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 = $repo.remove(task)
promise.then -> promise.then ->
finish() finish()
@ -166,7 +166,7 @@ RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading) ->
return {link:link, require:"ngModel"} 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) -> RelatedTaskCreateFormDirective = ($repo, $compile, $confirm, $tgmodel, $loading, $analytics) ->
template = _.template(""" template = _.template("""
@ -310,7 +310,7 @@ module.directive("tgRelatedTasks", ["$tgRepo", "$tgResources", "$rootScope", Rel
RelatedTaskAssignedToInlineEditionDirective = ($repo, $rootscope, popoverService) -> RelatedTaskAssignedToInlineEditionDirective = ($repo, $rootscope, popoverService) ->
template = _.template(""" template = _.template("""
<img src="<%= imgurl %>" alt="<%- name %>"/> <img src="<%- imgurl %>" alt="<%- name %>"/>
<figcaption><%- name %></figcaption> <figcaption><%- name %></figcaption>
""") """)

View File

@ -24,7 +24,7 @@ taiga = @.taiga
sizeFormat = @.taiga.sizeFormat sizeFormat = @.taiga.sizeFormat
resourceProvider = ($rootScope, $urls, $model, $repo, $auth, $q) -> resourceProvider = ($rootScope, $config, $urls, $model, $repo, $auth, $q) ->
service = {} service = {}
service.list = (urlName, objectId, projectId) -> service.list = (urlName, objectId, projectId) ->
@ -38,6 +38,16 @@ resourceProvider = ($rootScope, $urls, $model, $repo, $auth, $q) ->
defered.reject(null) defered.reject(null)
return defered.promise 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) => uploadProgress = (evt) =>
$rootScope.$apply => $rootScope.$apply =>
file.status = "in-progress" file.status = "in-progress"
@ -83,5 +93,5 @@ resourceProvider = ($rootScope, $urls, $model, $repo, $auth, $q) ->
module = angular.module("taigaResources") module = angular.module("taigaResources")
module.factory("$tgAttachmentsResourcesProvider", ["$rootScope", "$tgUrls", "$tgModel", "$tgRepo", "$tgAuth", module.factory("$tgAttachmentsResourcesProvider", ["$rootScope", "$tgConfig", "$tgUrls", "$tgModel", "$tgRepo",
"$q", resourceProvider]) "$tgAuth", "$q", resourceProvider])

View File

@ -42,9 +42,9 @@ resourceProvider = ($repo, $http, $urls) ->
url = $urls.resolve("memberships") url = $urls.resolve("memberships")
return $http.post("#{url}/#{id}/resend_invitation", {}) return $http.post("#{url}/#{id}/resend_invitation", {})
service.bulkCreateMemberships = (projectId, data) -> service.bulkCreateMemberships = (projectId, data, invitation_extra_text) ->
url = $urls.resolve("bulk-create-memberships") 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 $http.post(url, params)
return (instance) -> return (instance) ->

View File

@ -45,9 +45,6 @@ resourceProvider = ($repo) ->
service.stats = (projectId) -> service.stats = (projectId) ->
return $repo.queryOneRaw("projects", "#{projectId}/stats") return $repo.queryOneRaw("projects", "#{projectId}/stats")
service.tags = (projectId) ->
return $repo.queryOneRaw("projects", "#{projectId}/tags")
service.tagsColors = (id) -> service.tagsColors = (id) ->
return $repo.queryOne("projects", "#{id}/tags_colors") return $repo.queryOne("projects", "#{id}/tags_colors")

View File

@ -21,13 +21,26 @@
taiga = @.taiga taiga = @.taiga
sizeFormat = @.taiga.sizeFormat
resourceProvider = ($repo, $http, $urls) ->
resourceProvider = ($config, $repo, $http, $urls, $q) ->
service = {} 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 = new FormData()
data.append('avatar', attachmentModel) data.append('avatar', file)
options = { options = {
transformRequest: angular.identity, transformRequest: angular.identity,
headers: {'Content-Type': undefined} headers: {'Content-Type': undefined}
@ -52,4 +65,5 @@ resourceProvider = ($repo, $http, $urls) ->
module = angular.module("taigaResources") module = angular.module("taigaResources")
module.factory("$tgUserSettingsResourcesProvider", ["$tgRepo", "$tgHttp", "$tgUrls", resourceProvider]) module.factory("$tgUserSettingsResourcesProvider", ["$tgConfig", "$tgRepo", "$tgHttp", "$tgUrls", "$q",
resourceProvider])

View File

@ -55,11 +55,7 @@ class SearchController extends mixOf(taiga.Controller, taiga.PageMixin)
promise.then () => promise.then () =>
@appTitle.set("Search") @appTitle.set("Search")
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
# Search input watcher # Search input watcher
@scope.searchTerm = "" @scope.searchTerm = ""

View File

@ -25,7 +25,7 @@ debounce = @.taiga.debounce
CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, lightboxService) -> CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, lightboxService) ->
link = ($scope, $el, attrs) -> link = ($scope, $el, attrs) ->
isNew = true $scope.isNew = true
$scope.$on "taskform:new", (ctx, sprintId, usId) -> $scope.$on "taskform:new", (ctx, sprintId, usId) ->
$scope.task = { $scope.task = {
@ -37,7 +37,7 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, lightboxService) ->
assigned_to: null assigned_to: null
tags: [] tags: []
} }
isNew = true $scope.isNew = true
# Update texts for creation # Update texts for creation
$el.find(".button-green span").html("Create") #TODO: i18n $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.$on "taskform:edit", (ctx, task) ->
$scope.task = task $scope.task = task
isNew = false $scope.isNew = false
# Update texts for edition # Update texts for edition
$el.find(".button-green span").html("Save") #TODO: i18n $el.find(".button-green span").html("Save") #TODO: i18n
@ -60,7 +60,7 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, lightboxService) ->
if not form.validate() if not form.validate()
return return
if isNew if $scope.isNew
promise = $repo.create("tasks", $scope.task) promise = $repo.create("tasks", $scope.task)
broadcastEvent = "taskform:new:success" broadcastEvent = "taskform:new:success"
else else

View File

@ -66,11 +66,7 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
tgLoader.pageLoaded() tgLoader.pageLoaded()
# On Error # On Error
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
initializeEventHandlers: -> initializeEventHandlers: ->
# TODO: Reload entire taskboard after create/edit tasks seems # TODO: Reload entire taskboard after create/edit tasks seems
@ -320,7 +316,7 @@ TaskboardUserDirective = ($log) ->
template = _.template(""" template = _.template("""
<figure class="avatar"> <figure class="avatar">
<a href="#" title="Assign task" <% if (!clickable) {%>class="not-clickable"<% } %>> <a href="#" title="Assign task" <% if (!clickable) {%>class="not-clickable"<% } %>>
<img src="<%= imgurl %>" alt="<%- name %>"> <img src="<%- imgurl %>" alt="<%- name %>">
</a> </a>
</figure> </figure>
""") # TODO: i18n """) # TODO: i18n

View File

@ -57,13 +57,10 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
promise.then () => promise.then () =>
@appTitle.set(@scope.task.subject + " - " + @scope.project.name) @appTitle.set(@scope.task.subject + " - " + @scope.project.name)
@.initializeOnDeleteGoToUrl()
tgLoader.pageLoaded() tgLoader.pageLoaded()
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
initializeEventHandlers: -> initializeEventHandlers: ->
@scope.$on "attachment:create", => @scope.$on "attachment:create", =>
@ -74,6 +71,21 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.$on "attachment:delete", => @scope.$on "attachment:delete", =>
@rootscope.$broadcast("history:reload") @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: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
@scope.project = project @scope.project = project
@ -101,14 +113,19 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
ref: @scope.task.neighbors.next.ref ref: @scope.task.neighbors.next.ref
} }
@scope.nextUrl = @navUrls.resolve("project-tasks-detail", ctx) @scope.nextUrl = @navUrls.resolve("project-tasks-detail", ctx)
return task
if task.milestone loadSprint: ->
@rs.sprints.get(task.project, task.milestone).then (sprint) => if @scope.task.milestone
return @rs.sprints.get(@scope.task.project, @scope.task.milestone).then (sprint) =>
@scope.sprint = sprint @scope.sprint = sprint
return sprint
if task.user_story loadUserStory: ->
@rs.userstories.get(task.project, task.user_story).then (us) => if @scope.task.user_story
return @rs.userstories.get(@scope.task.project, @scope.task.user_story).then (us) =>
@scope.us = us @scope.us = us
return us
loadInitialData: -> loadInitialData: ->
params = { params = {
@ -123,152 +140,216 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
return promise.then(=> @.loadProject()) return promise.then(=> @.loadProject())
.then(=> @.loadUsersAndRoles()) .then(=> @.loadUsersAndRoles())
.then(=> @.loadTask()) .then(=> @.loadTask().then(=> @q.all([@.loadUserStory(),
@.loadSprint()])))
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")
module.controller("TaskDetailController", TaskDetailController) module.controller("TaskDetailController", TaskDetailController)
############################################################################# #############################################################################
## Task Main Directive ## Task status display directive
############################################################################# #############################################################################
TaskDirective = ($tgrepo, $log, $location, $confirm, $navUrls, $loading) -> TaskStatusDisplayDirective = ->
linkSidebar = ($scope, $el, $attrs, $ctrl) -> # 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
link = ($scope, $el, $attrs) ->
$ctrl = $el.controller()
linkSidebar($scope, $el, $attrs, $ctrl)
if $el.is("form")
form = $el.checksley()
$el.on "click", ".save-task", (event) ->
if not form.validate()
return
onSuccess = ->
$loading.finish(target)
$confirm.notify("success")
ctx = {
project: $scope.project.slug
ref: $scope.task.ref
}
$location.path($navUrls.resolve("project-tasks-detail", ctx))
onError = ->
$loading.finish(target)
$confirm.notify("error")
target = angular.element(event.currentTarget)
$loading.start(target)
$tgrepo.save($scope.task).then(onSuccess, onError)
return {link:link}
module.directive("tgTaskDetail", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm", "$tgNavUrls",
"$tgLoading", TaskDirective])
#############################################################################
## Task status directive
#############################################################################
TaskStatusDirective = () ->
#TODO: i18n
template = _.template(""" template = _.template("""
<h1>
<span> <span>
<% if (status.is_closed) { %> <% if (status.is_closed) { %>
Closed Closed
<% } else { %> <% } else { %>
Open Open
<% } %> <% } %>
<span class="us-detail-status" style="color:<%= status.color %>"><%= status.name %></span> </span>
</h1> <span class="us-detail-status" style="color:<%- status.color %>">
<div class="us-created-by"> <%- status.name %>
<div class="user-avatar"> </span>
<img src="<%= owner.photo %>" alt="<%- owner.full_name_display %>" /> """) # TODO: i18n
</div>
<div class="created-by"> link = ($scope, $el, $attrs) ->
<span class="created-title">Created by <%- owner.full_name_display %></span> render = (task) ->
<span class="created-date"><%- date %></span>
</div>
</div>
<div class="issue-data">
<div class="status-data <% if (editable) { %>clickable<% } %>">
<span class="level" style="background-color:<%= status.color %>"></span>
<span class="status-status"><%= status.name %></span>
<% if (editable) { %>
<span class="icon icon-arrow-bottom"></span>
<% } %>
<span class="level-name">status</span>
</div>
</div>
""")
selectionStatusTemplate = _.template("""
<ul class="popover pop-status">
<% _.each(statuses, function(status) { %>
<li><a href="" class="status" title="<%- status.name %>"
data-status-id="<%- status.id %>"><%- status.name %></a></li>
<% }); %>
</ul>
""")
link = ($scope, $el, $attrs, $model) ->
editable = $attrs.editable?
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({ html = template({
owner: owner status: $scope.statusById[task.status]
date: date
editable: editable
status: status
}) })
$el.html(html) $el.html(html)
$el.find(".status-data").append(selectionStatusTemplate({statuses:$scope.statusList}))
$scope.$watch $attrs.ngModel, (task) -> $scope.$watch $attrs.ngModel, (task) ->
if task? render(task) if task?
renderTaskstatus(task)
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
}
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("""
<div class="status-data <% if(editable){ %>clickable<% }%>">
<span class="level" style="background-color:<%- status.color %>"></span>
<span class="status-status"><%- status.name %></span>
<% if(editable){ %><span class="icon icon-arrow-bottom"></span><% }%>
<span class="level-name">status</span>
<ul class="popover pop-status">
<% _.each(statuses, function(st) { %>
<li><a href="" class="status" title="<%- st.name %>"
data-status-id="<%- st.id %>"><%- st.name %></a></li>
<% }); %>
</ul>
</div>
""") #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)
if editable
$el.on "click", ".status-data", (event) -> $el.on "click", ".status-data", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
return if not isEditable()
$el.find(".pop-status").popover().open() $el.find(".pop-status").popover().open()
$el.on "click", ".status", (event) -> $el.on "click", ".status", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
return if not isEditable()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
$model.$modelValue.status = target.data("status-id")
renderTaskstatus($model.$modelValue)
$el.find(".popover").popover().close()
return {link:link, require:"ngModel"} $.fn.popover().closeAll()
module.directive("tgTaskStatus", TaskStatusDirective) 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])
TaskIsIocaineButtonDirective = ($rootscope, $tgrepo, $confirm, $loading) ->
template = _.template("""
<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 for="is-iocaine"
class="button button-gray is-iocaine <% if(isEditable){ %>editable<% }; %> <% if(isIocaine){ %>active<% }; %>">
Iocaine
</label>
<input type="checkbox" id="is-iocaine" name="is-iocaine"/>
</fieldset>
""")
link = ($scope, $el, $attrs, $model) ->
isEditable = ->
return $scope.project.my_permissions.indexOf("modify_task") != -1
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.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) ->
render(task) if task
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgTaskIsIocaineButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", TaskIsIocaineButtonDirective])

View File

@ -51,11 +51,7 @@ class UserChangePasswordController extends mixOf(taiga.Controller, taiga.PageMix
promise = @.loadInitialData() promise = @.loadInitialData()
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
loadProject: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>

View File

@ -21,6 +21,7 @@
taiga = @.taiga taiga = @.taiga
mixOf = @.taiga.mixOf mixOf = @.taiga.mixOf
sizeFormat = @.taiga.sizeFormat
module = angular.module("taigaUserSettings") module = angular.module("taigaUserSettings")
@ -32,6 +33,7 @@ class UserSettingsController extends mixOf(taiga.Controller, taiga.PageMixin)
@.$inject = [ @.$inject = [
"$scope", "$scope",
"$rootScope", "$rootScope",
"$tgConfig",
"$tgRepo", "$tgRepo",
"$tgConfirm", "$tgConfirm",
"$tgResources", "$tgResources",
@ -42,18 +44,18 @@ class UserSettingsController extends mixOf(taiga.Controller, taiga.PageMixin)
"$tgAuth" "$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.sectionName = "User Profile" #i18n
@scope.project = {} @scope.project = {}
@scope.user = @auth.getUser() @scope.user = @auth.getUser()
maxFileSize = @config.get("maxUploadFileSize", null)
if maxFileSize
@scope.maxFileSizeMsg = "[Max, size: #{sizeFormat(maxFileSize)}" # TODO: i18n
promise = @.loadInitialData() promise = @.loadInitialData()
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
loadProject: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
@ -115,6 +117,9 @@ module.directive("tgUserProfile", ["$tgConfirm", "$tgAuth", "$tgRepo", UserProf
UserAvatarDirective = ($auth, $model, $rs, $confirm) -> UserAvatarDirective = ($auth, $model, $rs, $confirm) ->
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
showSizeInfo = ->
$el.find(".size-info").removeClass("hidden")
onSuccess = (response) -> onSuccess = (response) ->
user = $model.make_model("users", response.data) user = $model.make_model("users", response.data)
$auth.setUser(user) $auth.setUser(user)
@ -124,6 +129,7 @@ UserAvatarDirective = ($auth, $model, $rs, $confirm) ->
$confirm.notify('success') $confirm.notify('success')
onError = (response) -> onError = (response) ->
showSizeInfo() if response.status == 413
$el.find('.overlay').hide() $el.find('.overlay').hide()
$confirm.notify('error', response.data._error_message) $confirm.notify('error', response.data._error_message)

View File

@ -51,11 +51,7 @@ class UserNotificationsController extends mixOf(taiga.Controller, taiga.PageMixi
promise = @.loadInitialData() promise = @.loadInitialData()
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
loadProject: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>

View File

@ -59,16 +59,17 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
# On Success # On Success
promise.then => promise.then =>
@appTitle.set(@scope.us.subject + " - " + @scope.project.name) @appTitle.set(@scope.us.subject + " - " + @scope.project.name)
@.initializeOnDeleteGoToUrl()
tgLoader.pageLoaded() tgLoader.pageLoaded()
# On Error # On Error
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
initializeEventHandlers: -> initializeEventHandlers: ->
@scope.$on "related-tasks:update", =>
@.loadUs()
@scope.tasks = _.clone(@scope.tasks, false)
@scope.$on "attachment:create", => @scope.$on "attachment:create", =>
@analytics.trackEvent("attachment", "create", "create attachment on userstory", 1) @analytics.trackEvent("attachment", "create", "create attachment on userstory", 1)
@rootscope.$broadcast("history:reload") @rootscope.$broadcast("history:reload")
@ -79,6 +80,18 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.$on "attachment:delete", => @scope.$on "attachment:delete", =>
@rootscope.$broadcast("history:reload") @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: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
@scope.project = project @scope.project = project
@ -110,12 +123,14 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
} }
@scope.nextUrl = @navUrls.resolve("project-userstories-detail", ctx) @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 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: -> loadTasks: ->
return @rs.tasks.list(@scope.projectId, null, @scope.usId).then (tasks) => return @rs.tasks.list(@scope.projectId, null, @scope.usId).then (tasks) =>
@scope.tasks = tasks @scope.tasks = tasks
@ -134,257 +149,128 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
return promise.then(=> @.loadProject()) return promise.then(=> @.loadProject())
.then(=> @.loadUsersAndRoles()) .then(=> @.loadUsersAndRoles())
.then(=> @q.all([@.loadUs(), .then(=> @q.all([@.loadUs().then(=> @.loadSprint()),
@.loadTasks()])) @.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) module.controller("UserStoryDetailController", UserStoryDetailController)
#############################################################################
## User story Main Directive
#############################################################################
UsDirective = ($tgrepo, $log, $location, $confirm, $navUrls, $loading) ->
linkSidebar = ($scope, $el, $attrs, $ctrl) ->
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 ## User story status display directive
############################################################################# #############################################################################
UsStatusDetailDirective = () -> UsStatusDisplayDirective = ->
#TODO: i18n # 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(""" template = _.template("""
<h1>
<span> <span>
<% if (is_closed) { %> <% if (is_closed) { %>
Closed Closed
<% } else { %> <% } else { %>
Open Open
<% } %> <% } %>
<span class="us-detail-status" style="color:<%= status.color %>"><%= status.name %></span> </span>
</h1> <span class="us-detail-status" style="color:<%- status.color %>">
<%- status.name %>
</span>
""") # TODO: i18n
<div class="us-detail-progress-bar"> link = ($scope, $el, $attrs) ->
<div class="current-progress" style="width:<%- usProgress %>%"/> render = (us) ->
html = template({
is_closed: us.is_closed
status: $scope.statusById[us.status]
})
$el.html(html)
$scope.$watch $attrs.ngModel, (us) ->
render(us) if us?
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgUsStatusDisplay", UsStatusDisplayDirective)
#############################################################################
## User story related tasts progress splay Directive
#############################################################################
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
template = _.template("""
<div class="current-progress" style="width:<%- progress %>%" />
<span clasS="tasks-completed"> <span clasS="tasks-completed">
<%- totalClosedTasks %>/<%- totalTasks %> tasks completed <%- totalClosedTasks %>/<%- totalTasks %> tasks completed
</span> </span>
</div> """) # TODO: i18n
<div class="us-created-by"> link = ($scope, $el, $attrs) ->
<div class="user-avatar"> render = (tasks) ->
<img src="<%= owner.photo %>" alt="<%- owner.full_name_display %>" /> totalTasks = tasks.length
</div> totalClosedTasks = _.filter(tasks, (task) => $scope.taskStatusById[task.status].is_closed).length
<div class="created-by"> progress = if totalTasks > 0 then 100 * totalClosedTasks / totalTasks else 0
<span class="created-title">Created by <%- owner.full_name_display %></span>
<span class="created-date"><%- date %></span>
</div>
</div>
<ul class="points-per-role">
<li class="total">
<span class="points"><%- totalPoints %></span>
<span class="role">total</span>
</li>
<% _.each(rolePoints, function(rolePoint) { %>
<li class="total <% if (editable) { %>clickable<% } %>" data-role-id="<%- rolePoint.id %>">
<span class="points"><%- rolePoint.points %></span>
<span class="role"><%- rolePoint.name %></span></li>
<% }); %>
</ul>
<div class="issue-data">
<div class="status-data <% if (editable) { %>clickable<% } %>">
<span class="level" style="background-color:<%= status.color %>"></span>
<span class="status-status"><%= status.name %></span>
<% if (editable) { %>
<span class="icon icon-arrow-bottom"></span>
<% } %>
<span class="level-name">status</span>
</div>
</div>
""")
selectionStatusTemplate = _.template("""
<ul class="popover pop-status">
<% _.each(statuses, function(status) { %>
<li><a href="" class="status" title="<%- status.name %>"
data-status-id="<%- status.id %>"><%- status.name %></a></li>
<% }); %>
</ul>
""")
selectionPointsTemplate = _.template("""
<ul class="popover pop-points-open">
<% _.each(points, function(point) { %>
<li><a href="" class="point" title="<%- point.name %>"
data-point-id="<%- point.id %>"><%- point.name %></a>
</li>
<% }); %>
</ul>
""")
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
html = template({ html = template({
owner: owner
date: date
editable: editable
is_closed: us.is_closed
status: status
totalPoints: us.total_points
rolePoints: rolePoints
totalTasks: totalTasks totalTasks: totalTasks
totalClosedTasks: totalClosedTasks totalClosedTasks: totalClosedTasks
usProgress: usProgress progress: progress
}) })
$el.html(html) $el.html(html)
$el.find(".status-data").append(selectionStatusTemplate({statuses:$scope.statusList}))
bindOnce $scope, "tasks", (tasks) -> $scope.$watch $attrs.ngModel, (tasks) ->
$scope.$watch $attrs.ngModel, (us) -> render(tasks) if tasks?
if us?
renderUsstatus(us)
$scope.$on "related-tasks:update", -> $scope.$on "$destroy", ->
us = $scope.$eval $attrs.ngModel $el.off()
if us?
# Reload the us because the status could have changed
$ctrl.loadUs()
renderUsstatus(us)
if editable return {
$el.on "click", ".status-data", (event) -> link: link
event.preventDefault() restrict: "EA"
event.stopPropagation() require: "ngModel"
$el.find(".pop-status").popover().open() }
$el.on "click", ".status", (event) -> module.directive("tgUsTasksProgressDisplay", UsTasksProgressDisplayDirective)
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
$model.$modelValue.status = target.data("status-id")
renderUsstatus($model.$modelValue)
$.fn.popover().closeAll()
$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()
target = angular.element(event.currentTarget)
$.fn.popover().closeAll()
$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)
return {link:link, require:"ngModel"}
module.directive("tgUsStatusDetail", UsStatusDetailDirective)
############################################################################# #############################################################################
## User story estimation directive ## 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(""" mainTemplate = _.template("""
<ul class="points-per-role"> <ul class="points-per-role">
<li class="total"> <li class="total">
@ -392,7 +278,7 @@ UsEstimationDirective = ($log) ->
<span class="role">total</span> <span class="role">total</span>
</li> </li>
<% _.each(roles, function(role) { %> <% _.each(roles, function(role) { %>
<li class="total clickable" data-role-id="<%- role.id %>"> <li class="total <% if(editable){ %>clickable<% } %>" data-role-id="<%- role.id %>">
<span class="points"><%- role.points %></span> <span class="points"><%- role.points %></span>
<span class="role"><%- role.name %></span></li> <span class="role"><%- role.name %></span></li>
<% }); %> <% }); %>
@ -415,7 +301,14 @@ UsEstimationDirective = ($log) ->
</ul> </ul>
""") """)
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) -> render = (us) ->
totalPoints = us.total_points or 0 totalPoints = us.total_points or 0
computableRoles = _.filter($scope.project.roles, "computable") computableRoles = _.filter($scope.project.roles, "computable")
@ -428,7 +321,12 @@ UsEstimationDirective = ($log) ->
role.points = if pointObj? and pointObj.name? then pointObj.name else "?" role.points = if pointObj? and pointObj.name? then pointObj.name else "?"
return role return role
html = mainTemplate({totalPoints: totalPoints, roles: roles}) ctx = {
totalPoints: totalPoints
roles: roles
editable: isEditable()
}
html = mainTemplate(ctx)
$el.html(html) $el.html(html)
renderPoints = (target, us, roleId) -> renderPoints = (target, us, roleId) ->
@ -461,19 +359,15 @@ UsEstimationDirective = ($log) ->
return "0" return "0"
return _.reduce(values, (acc, num) -> acc + num) 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) -> $el.on "click", ".total.clickable", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
return if not isEditable()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
roleId = target.data("role-id") roleId = target.data("role-id")
us = $scope.$eval($attrs.ngModel) us = $model.$modelValue
renderPoints(target, us, roleId) renderPoints(target, us, roleId)
target.siblings().removeClass('active') target.siblings().removeClass('active')
@ -482,8 +376,7 @@ UsEstimationDirective = ($log) ->
$el.on "click", ".point", (event) -> $el.on "click", ".point", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
return if not isEditable()
us = $scope.$eval($attrs.ngModel)
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
roleId = target.data("role-id") roleId = target.data("role-id")
@ -491,17 +384,262 @@ UsEstimationDirective = ($log) ->
$el.find(".popover").popover().close() $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 points[roleId] = pointId
us.setAttr('points', points) if us.setAttr?
$scope.$apply ->
us.points = points us.points = points
us.total_points = calculateTotalPoints(us) us.total_points = calculateTotalPoints(us)
render(us) $model.$setViewValue(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 { return {
link: link link: link
restrict: "EA" 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("""
<div class="status-data <% if(editable){ %>clickable<% }%>">
<span class="level" style="background-color:<%- status.color %>"></span>
<span class="status-status"><%- status.name %></span>
<% if(editable){ %><span class="icon icon-arrow-bottom"></span><% }%>
<span class="level-name">status</span>
<ul class="popover pop-status">
<% _.each(statuses, function(st) { %>
<li><a href="" class="status" title="<%- st.name %>"
data-status-id="<%- st.id %>"><%- st.name %></a></li>
<% }); %>
</ul>
</div>
""") #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("""
<label for="team-requirement"
class="button button-gray team-requirement <% if(canEdit){ %>editable<% }; %> <% if(isRequired){ %>active<% }; %>">
Team requirement
</label>
<input type="checkbox" id="team-requirement" name="team-requirement"/>
""") #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("""
<label for="client-requirement"
class="button button-gray client-requirement <% if(canEdit){ %>editable<% }; %> <% if(isRequired){ %>active<% }; %>">
Client requirement
</label>
<input type="checkbox" id="client-requirement" name="client-requirement"/>
""") #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])

View File

@ -38,6 +38,7 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
"$scope", "$scope",
"$rootScope", "$rootScope",
"$tgRepo", "$tgRepo",
"$tgModel",
"$tgConfirm", "$tgConfirm",
"$tgResources", "$tgResources",
"$routeParams", "$routeParams",
@ -51,7 +52,7 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
"tgLoader" "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) -> @filter, @log, @appTitle, @navUrls, @analytics, tgLoader) ->
@scope.projectSlug = @params.pslug @scope.projectSlug = @params.pslug
@scope.wikiSlug = @params.slug @scope.wikiSlug = @params.slug
@ -65,11 +66,7 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
tgLoader.pageLoaded() tgLoader.pageLoaded()
# On Error # On Error
promise.then null, (xhr) => promise.then null, @.onInitialDataError.bind(@)
if xhr and xhr.status == 404
@location.path(@navUrls.resolve("not-found"))
@location.replace()
return @q.reject(xhr)
loadProject: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
@ -84,7 +81,15 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.wiki = wiki @scope.wiki = wiki
return 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 return @scope.wiki
loadWikiLinks: -> loadWikiLinks: ->
@ -113,34 +118,19 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.wikiId = data.wikipage @scope.wikiId = data.wikipage
return prom.then null, (xhr) => return prom.then null, (xhr) =>
ctx = {project: @params.pslug, slug: @params.slug} @scope.wikiId = null
@location.path(@navUrls.resolve("project-wiki-page-edit", ctx))
return promise.then(=> @.loadProject()) return promise.then(=> @.loadProject())
.then(=> @.loadUsersAndRoles()) .then(=> @.loadUsersAndRoles())
.then(=> @q.all([@.loadWikiLinks(), .then(=> @q.all([@.loadWikiLinks(),
@.loadWiki()])) @.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: -> delete: ->
# TODO: i18n # TODO: i18n
title = "Delete Wiki Page" 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 = => onSuccess = =>
finish() finish()
ctx = {project: @scope.projectSlug} ctx = {project: @scope.projectSlug}
@ -155,95 +145,181 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
module.controller("WikiDetailController", WikiDetailController) 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) -> WikiSummaryDirective = ($log) ->
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) ->
template = _.template(""" template = _.template("""
<ul>
<li>
<span class="number"><%- totalEditions %></span>
<span class="description">times <br />edited</span>
</li>
<li>
<span class="number"><%- lastModifiedDate %></span>
<span class="description"> last <br />edit</span>
</li>
<li class="username-edition">
<figure class="avatar"> <figure class="avatar">
<img src="<%= imgurl %>" alt="<%- name %>"> <img src="<%- user.imgUrl %>" alt="<%- user.name %>">
</figure> </figure>
<span class="description">last modification</span> <span class="description">last modification</span>
<span class="username"><%- name %></span> <span class="username"><%- user.name %></span>
</li>
</ul>
""") """)
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs, $model) ->
if not $attrs.ngModel?
return $log.error "WikiUserDirective: no ng-model attr is defined"
render = (wiki) -> render = (wiki) ->
if not $scope.usersById? if not $scope.usersById?
$log.error "WikiUserDirective requires userById set in scope." $log.error "WikiSummaryDirective requires userById set in scope."
else else
user = $scope.usersById[wiki.last_modifier] 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) html = template(ctx)
$el.html(html) $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 { return {
link: link 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 = """
<div class="view-wiki-content">
<section class="wysiwyg"
tg-bind-html="wiki.html"></section>
<span class="edit icon icon-edit" title="Edit"></span>
</div>
<div class="edit-wiki-content" style="display: none;">
<textarea placeholder="Write your wiki page here"
ng-model="wiki.content"
tg-markitup="tg-markitup"></textarea>
<span class="action-container">
<a class="save icon icon-floppy" href="" title="Save" />
<a class="cancel icon icon-delete" href="" title="Cancel" />
</span>
</div>
""" # 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])

View File

@ -107,9 +107,9 @@ WikiNavDirective = ($tgrepo, $log, $location, $confirm, $navUrls, $analytics, $l
# TODO: i18n # TODO: i18n
title = "Delete Wiki Link" 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 = $tgrepo.remove($scope.wikiLinks[linkId])
promise.then -> promise.then ->
promise = $ctrl.loadWikiLinks() promise = $ctrl.loadWikiLinks()

Binary file not shown.

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -45,4 +45,5 @@
<glyph unicode="&#74;" d="M184 54l24 145c1 3 0 5-2 7 0 0 0 0 0 0-2 2-5 3-7 3l-145-25c-3 0-5-2-6-5-1-3 0-7 2-9l28-28-76-76c-3-3-3-8 0-11l53-53c3-3 8-3 11 0l76 76 28-28c3-2 6-3 9-2 3 1 5 3 5 6z m144 404l-24-145c-1-3 0-5 2-7 0 0 0 0 0 0 2-2 5-3 7-3l145 25c3 0 5 2 6 5 1 3 0 7-2 9l-28 28 76 76c3 3 3 8 0 11l-53 53c-3 3-8 3-11 0l-76-76-28 28c-3 2-6 3-9 2-3-1-5-3-5-6z m130-274l-145 24c-3 1-5 0-7-2 0 0 0 0 0 0-2-2-3-5-3-7l25-145c0-3 2-5 5-6 3-1 7 0 9 2l28 28 76-76c3-3 8-3 11 0l53 53c3 3 3 8 0 11l-76 76 28 28c2 3 3 6 2 9-1 3-3 5-6 5z m-404 144l145-24c3-1 5 0 7 2 0 0 0 0 0 0 2 2 3 5 3 7l-25 145c0 3-2 5-5 6-3 1-6 0-9-2l-28-28-76 76c-3 3-8 3-11 0l-53-53c-3-3-3-8 0-11l76-76-28-28c-2-3-3-6-2-9 1-3 3-5 6-5z"/> <glyph unicode="&#74;" d="M184 54l24 145c1 3 0 5-2 7 0 0 0 0 0 0-2 2-5 3-7 3l-145-25c-3 0-5-2-6-5-1-3 0-7 2-9l28-28-76-76c-3-3-3-8 0-11l53-53c3-3 8-3 11 0l76 76 28-28c3-2 6-3 9-2 3 1 5 3 5 6z m144 404l-24-145c-1-3 0-5 2-7 0 0 0 0 0 0 2-2 5-3 7-3l145 25c3 0 5 2 6 5 1 3 0 7-2 9l-28 28 76 76c3 3 3 8 0 11l-53 53c-3 3-8 3-11 0l-76-76-28 28c-3 2-6 3-9 2-3-1-5-3-5-6z m130-274l-145 24c-3 1-5 0-7-2 0 0 0 0 0 0-2-2-3-5-3-7l25-145c0-3 2-5 5-6 3-1 7 0 9 2l28 28 76-76c3-3 8-3 11 0l53 53c3 3 3 8 0 11l-76 76 28 28c2 3 3 6 2 9-1 3-3 5-6 5z m-404 144l145-24c3-1 5 0 7 2 0 0 0 0 0 0 2 2 3 5 3 7l-25 145c0 3-2 5-5 6-3 1-6 0-9-2l-28-28-76 76c-3 3-8 3-11 0l-53-53c-3-3-3-8 0-11l76-76-28-28c-2-3-3-6-2-9 1-3 3-5 6-5z"/>
<glyph unicode="&#75;" d="M24 154l-24-144c0-3 1-6 2-8 0 0 0 0 0 0 2-1 5-2 8-2l144 25c3 0 6 2 6 5 1 3 1 6-2 8l-28 29 76 75c3 4 3 9 0 12l-52 52c-3 4-9 4-12 0l-76-75-28 28c-2 2-5 3-8 2-3-1-5-4-6-7z m464 204l24 144c0 3-1 6-2 8 0 0 0 0 0 0-2 1-5 2-8 2l-144-25c-3 0-6-2-6-5-1-3-1-6 2-8l28-29-76-75c-3-4-3-9 0-12l52-52c3-4 9-4 12 0l76 75 28-28c2-2 5-3 8-2 3 1 5 4 6 7z m-130-334l144-24c3 0 6 1 8 2 0 0 0 0 0 0 1 2 2 5 2 8l-25 144c0 3-2 6-5 6-3 1-6 1-8-2l-29-28-75 76c-4 3-9 3-12 0l-52-52c-4-3-4-9 0-12l75-76-28-28c-2-2-3-5-2-8 1-3 4-5 7-6z m-204 464l-144 24c-3 0-6-1-8-2 0 0 0 0 0 0-1-2-2-5-2-8l25-144c0-3 2-6 5-6 3-1 6-1 8 2l29 28 75-76c4-3 9-3 12 0l52 52c4 3 4 9 0 12l-75 76 28 28c2 2 3 5 2 8-1 3-4 5-7 6z"/> <glyph unicode="&#75;" d="M24 154l-24-144c0-3 1-6 2-8 0 0 0 0 0 0 2-1 5-2 8-2l144 25c3 0 6 2 6 5 1 3 1 6-2 8l-28 29 76 75c3 4 3 9 0 12l-52 52c-3 4-9 4-12 0l-76-75-28 28c-2 2-5 3-8 2-3-1-5-4-6-7z m464 204l24 144c0 3-1 6-2 8 0 0 0 0 0 0-2 1-5 2-8 2l-144-25c-3 0-6-2-6-5-1-3-1-6 2-8l28-29-76-75c-3-4-3-9 0-12l52-52c3-4 9-4 12 0l76 75 28-28c2-2 5-3 8-2 3 1 5 4 6 7z m-130-334l144-24c3 0 6 1 8 2 0 0 0 0 0 0 1 2 2 5 2 8l-25 144c0 3-2 6-5 6-3 1-6 1-8-2l-29-28-75 76c-4 3-9 3-12 0l-52-52c-4-3-4-9 0-12l75-76-28-28c-2-2-3-5-2-8 1-3 4-5 7-6z m-204 464l-144 24c-3 0-6-1-8-2 0 0 0 0 0 0-1-2-2-5-2-8l25-144c0-3 2-6 5-6 3-1 6-1 8 2l29 28 75-76c4-3 9-3 12 0l52 52c4 3 4 9 0 12l-75 76 28 28c2 2 3 5 2 8-1 3-4 5-7 6z"/>
<glyph unicode="&#76;" d="M435 486c8 0 14-2 19-7 4-5 7-11 7-18 0 0 0-435 0-435 0 0-103 0-103 0 0 0 0 435 0 435 0 17 7 25 21 25 0 0 56 0 56 0m-153-153c7 0 13-3 18-8 5-5 7-11 7-18 0 0 0-281 0-281 0 0-102 0-102 0 0 0 0 281 0 281 0 17 7 26 20 26 0 0 57 0 57 0m-154-154c8 0 14-2 18-7 5-6 8-12 8-18 0 0 0-128 0-128 0 0-103 0-103 0 0 0 0 128 0 128 0 17 7 25 21 25 0 0 56 0 56 0"/> <glyph unicode="&#76;" d="M435 486c8 0 14-2 19-7 4-5 7-11 7-18 0 0 0-435 0-435 0 0-103 0-103 0 0 0 0 435 0 435 0 17 7 25 21 25 0 0 56 0 56 0m-153-153c7 0 13-3 18-8 5-5 7-11 7-18 0 0 0-281 0-281 0 0-102 0-102 0 0 0 0 281 0 281 0 17 7 26 20 26 0 0 57 0 57 0m-154-154c8 0 14-2 18-7 5-6 8-12 8-18 0 0 0-128 0-128 0 0-103 0-103 0 0 0 0 128 0 128 0 17 7 25 21 25 0 0 56 0 56 0"/>
<glyph unicode="&#77;" d="M384 64l-320 0 0 288 320 0 0-96 32 0 0 160c0 17-14 32-32 32l-96 0c0 35-28 64-64 64-35 0-64-29-64-64l-96 0c-17 0-32-15-32-32l0-352c0-18 15-32 32-32l320 0c18 0 32 14 32 32l0 64-32 0z m-256 352c15 0 15 0 32 0 18 0 32 14 32 32 0 17 15 32 32 32 18 0 32-15 32-32 0-18 16-32 32-32 16 0 17 0 32 0 16 0 32-15 32-32l-256 0c0 19 14 32 32 32z m-32-256l64 0 0 32-64 0z m224 64l0 64-128-96 128-96 0 64 160 0 0 64z m-224-128l96 0 0 32-96 0z m160 224l-160 0 0-32 160 0z m-96-64l-64 0 0-32 64 0z"/>
</font></defs></svg> </font></defs></svg>

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Binary file not shown.

BIN
app/images/markitup/bold.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 B

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 859 B

After

Width:  |  Height:  |  Size: 370 B

BIN
app/images/markitup/italic.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 B

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 B

After

Width:  |  Height:  |  Size: 365 B

BIN
app/images/markitup/list-bullet.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 B

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 B

After

Width:  |  Height:  |  Size: 321 B

BIN
app/images/markitup/picture.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 B

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 537 B

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 743 B

After

Width:  |  Height:  |  Size: 375 B

BIN
app/images/markitup/stroke.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 343 B

View File

@ -21,13 +21,13 @@ html(lang="en", ng-app="taiga")
div.master(ng-view) div.master(ng-view)
div.hidden.lightbox.lightbox-confirm-delete div.lightbox.lightbox-generic-ask
include partials/views/modules/lightbox-confirm-delete include partials/views/modules/lightbox-generic-ask
div.hidden.lightbox.lightbox-ask-choice div.lightbox.lightbox-ask-choice
include partials/views/modules/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 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 include partials/views/modules/lightbox-generic-error
div.lightbox.lightbox-search(tg-search-box) div.lightbox.lightbox-search(tg-search-box)
include partials/views/modules/lightbox-search include partials/views/modules/lightbox-search

View File

@ -22,5 +22,5 @@ block content
div.paginator.memberships-paginator 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 include views/modules/lightbox-add-member

View File

@ -12,7 +12,7 @@ block content
sidebar.menu-tertiary.sidebar(tg-admin-navigation="default-values") sidebar.menu-tertiary.sidebar(tg-admin-navigation="default-values")
include views/modules/admin-submenu-project-profile include views/modules/admin-submenu-project-profile
section.main.admin-roles section.main.admin-common
header header
include views/components/mainTitle include views/components/mainTitle

View File

@ -58,5 +58,5 @@ block content
a.button.button-green(href="") Save a.button.button-green(href="") Save
a.delete-project(href="", title="Delete this project", ng-click="ctrl.openDeleteLightbox()") Delete this project 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 include views/modules/lightbox-delete-project

View File

@ -13,7 +13,7 @@ block content
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-priorities") sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-priorities")
include views/modules/admin-submenu-project-values include views/modules/admin-submenu-project-values
section.main.admin-roles section.main.admin-common
include views/components/mainTitle include views/components/mainTitle
p.admin-subtitle Specify the priority levels users can assign to issues p.admin-subtitle Specify the priority levels users can assign to issues

View File

@ -13,7 +13,7 @@ block content
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-severities") sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-severities")
include views/modules/admin-submenu-project-values include views/modules/admin-submenu-project-values
section.main.admin-roles section.main.admin-common
include views/components/mainTitle include views/components/mainTitle
p.admin-subtitle Specify the severity level users can select to classify issues p.admin-subtitle Specify the severity level users can select to classify issues

View File

@ -13,7 +13,7 @@ block content
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-issue-status") sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-issue-status")
include views/modules/admin-submenu-project-values include views/modules/admin-submenu-project-values
section.main.admin-roles section.main.admin-common
include views/components/mainTitle include views/components/mainTitle
p.admin-subtitle Specify the column headers that you will use to classify Issues p.admin-subtitle Specify the column headers that you will use to classify Issues

View File

@ -13,7 +13,7 @@ block content
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-issue-types") sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-issue-types")
include views/modules/admin-submenu-project-values include views/modules/admin-submenu-project-values
section.main.admin-roles section.main.admin-common
include views/components/mainTitle include views/components/mainTitle
p.admin-subtitle Specify the categories users can select to classify issues p.admin-subtitle Specify the categories users can select to classify issues

View File

@ -13,7 +13,7 @@ block content
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-task-status") sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-task-status")
include views/modules/admin-submenu-project-values include views/modules/admin-submenu-project-values
section.main.admin-roles section.main.admin-common
include views/components/mainTitle include views/components/mainTitle
p.admin-subtitle Specify the column headers that you will use to classify Tasks related to each User Stories p.admin-subtitle Specify the column headers that you will use to classify Tasks related to each User Stories

View File

@ -13,7 +13,7 @@ block content
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-us-points") sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-us-points")
include views/modules/admin-submenu-project-values include views/modules/admin-submenu-project-values
section.main.admin-roles section.main.admin-common
include views/components/mainTitle include views/components/mainTitle
p.admin-subtitle Specify the numerical system you will use to indicate the level of difficulty for each User Story 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 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 include views/modules/help-notions/lightbox-notion-admin-project-values-us-points

View File

@ -13,7 +13,7 @@ block content
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-us-status") sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-us-status")
include views/modules/admin-submenu-project-values include views/modules/admin-submenu-project-values
section.main.admin-roles section.main.admin-common
include views/components/mainTitle include views/components/mainTitle
p.admin-subtitle Specify the column headers that you will use to classify User Stories p.admin-subtitle Specify the column headers that you will use to classify User Stories

View File

@ -11,15 +11,21 @@ block content
sidebar.menu-tertiary.sidebar sidebar.menu-tertiary.sidebar
include views/modules/admin-submenu-roles include views/modules/admin-submenu-roles
section.main.admin-roles section.main.admin-roles.admin-common
.header-with-actions .header-with-actions
include views/components/mainTitle include views/components/mainTitle
.action-buttons .action-buttons
a.button.button-red.delete-role(href="", title="Delete", ng-click="ctrl.delete()") Delete a.button.button-red.delete-role(href="", title="Delete", ng-click="ctrl.delete()") Delete
div(tg-edit-role)
.edit-role
input(type="text", value="{{ role.name }}")
a.save.icon.icon-floppy(href="", title="Save")
p.total p.total
| {{ role.name }} span.role-name(title="{{ role.members_count }} members with this role") {{ role.name }}
span ({{ role.members_count }} members with this role) 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 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

View File

@ -8,7 +8,7 @@ block content
div.wrapper div.wrapper
div.login-main div.login-main
div.login-container 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 h1.logo Taiga
h2.tagline LOVE YOUR PROJECT h2.tagline LOVE YOUR PROJECT

View File

@ -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)

View File

@ -4,19 +4,17 @@ block head
title Taiga Your agile, free, and open source project management tool title Taiga Your agile, free, and open source project management tool
block content block content
div.wrapper(tg-issue-detail, ng-controller="IssueDetailController as ctrl", div.wrapper(ng-controller="IssueDetailController as ctrl",
ng-init="section='issues'") ng-init="section='issues'")
div.main.us-detail div.main.us-detail
div.us-detail-header.header-with-actions div.us-detail-header.header-with-actions
include views/components/mainTitle 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 section.us-story-main-data
div.us-title(ng-class="{blocked: issue.is_blocked}") div.us-title(ng-class="{blocked: issue.is_blocked}")
h2.us-title-text h2.us-title-text
span.us-number(tg-bo-ref="issue.ref") 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: 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", a(ng-repeat="us in issue.generated_user_stories",
@ -27,23 +25,43 @@ block content
p.block-desc-container(ng-show="issue.is_blocked") p.block-desc-container(ng-show="issue.is_blocked")
span.block-description-title 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 div.issue-nav
a.icon.icon-arrow-left(ng-show="previousUrl",href="{{ previousUrl }}", title="previous issue") a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl",
a.icon.icon-arrow-right(ng-show="nextUrl", href="{{ nextUrl }}", title="next issue") 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-attachments(ng-model="issue", type="issue")
tg-history(ng-model="issue", type="issue") tg-history(ng-model="issue", type="issue")
sidebar.menu-secondary.sidebar sidebar.menu-secondary.sidebar
section.us-status(tg-issue-status, ng-model="issue") section.us-status
section.us-assigned-to(tg-assigned-to, ng-model="issue") h1(tg-issue-status-display, ng-model="issue")
section.watchers(tg-watchers, 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 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)

View File

@ -26,4 +26,4 @@ block content
div.lightbox.lightbox-generic-bulk(tg-lb-create-bulk-userstories) div.lightbox.lightbox-generic-bulk(tg-lb-create-bulk-userstories)
include views/modules/lightbox-us-bulk include views/modules/lightbox-us-bulk
div.lightbox.lightbox-select-user.hidden(tg-lb-assignedto) div.lightbox.lightbox-select-user(tg-lb-assignedto)

View File

@ -8,7 +8,7 @@ block content
div.wrapper div.wrapper
div.login-main div.login-main
div.login-container 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 h1.logo Taiga
h2.tagline LOVE YOUR PROJECT h2.tagline LOVE YOUR PROJECT

View File

@ -9,7 +9,7 @@ block content
sidebar.menu-secondary.sidebar(tg-user-settings-navigation="mail-notifications") sidebar.menu-secondary.sidebar(tg-user-settings-navigation="mail-notifications")
include views/modules/user-settings-menu include views/modules/user-settings-menu
section.main.admin-roles section.main.admin-common
header header
h1 h1
span.green(tg-bo-html="sectionName") span.green(tg-bo-html="sectionName")

View File

@ -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

View File

@ -8,8 +8,7 @@ block content
div.wrapper div.wrapper
div.login-main div.login-main
div.login-container div.login-container
object.logo-svg(type="image/svg+xml", data="/svg/logo.svg") img.logo-svg(src="/svg/logo.svg", alt="TAIGA loves Movember!")
img(src="/images/logo.png", alt="TAIGA")
h1.logo Taiga h1.logo Taiga
h2.tagline LOVE YOUR PROJECT h2.tagline LOVE YOUR PROJECT

View File

@ -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)

View File

@ -4,7 +4,7 @@ block head
title Taiga Your agile, free, and open source project management tool title Taiga Your agile, free, and open source project management tool
block content block content
div.wrapper(tg-task-detail, ng-controller="TaskDetailController as ctrl", div.wrapper(ng-controller="TaskDetailController as ctrl",
ng-init="section='backlog'") ng-init="section='backlog'")
div.main.us-detail div.main.us-detail
div.us-detail-header.header-with-actions div.us-detail-header.header-with-actions
@ -15,16 +15,12 @@ block content
href="", title="Go to taskboard", href="", title="Go to taskboard",
tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug", tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug",
ng-if="sprint && project.is_backlog_activated") Taskboard 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 section.us-story-main-data
div.us-title(ng-class="{blocked: task.is_blocked}") div.us-title(ng-class="{blocked: task.is_blocked}")
h2.us-title-text h2.us-title-text
span.us-number(tg-bo-ref="task.ref") 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 h3.us-related-task This task belongs to
a(tg-check-permission="view_us", href="", title="Go to user story", a(tg-check-permission="view_us", href="", title="Go to user story",
tg-nav="project-userstories-detail:project=project.slug, ref=us.ref", tg-nav="project-userstories-detail:project=project.slug, ref=us.ref",
@ -33,22 +29,39 @@ block content
span(tg-bo-bind="us.subject") span(tg-bo-bind="us.subject")
p.block-desc-container(ng-show="task.is_blocked") p.block-desc-container(ng-show="task.is_blocked")
span.block-description-title 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 div.issue-nav
a.icon.icon-arrow-left(ng-show="previousUrl",href="{{ previousUrl }}", title="previous task") a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl",
a.icon.icon-arrow-right(ng-show="nextUrl", href="{{ nextUrl }}", title="next task") 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-attachments(ng-model="task", type="task")
tg-history(ng-model="task", type="task") tg-history(ng-model="task", type="task")
sidebar.menu-secondary.sidebar sidebar.menu-secondary.sidebar
section.us-status(tg-task-status, ng-model="task") section.us-status
section.us-assigned-to(tg-assigned-to, ng-model="task") h1(tg-task-status-display, ng-model="task")
section.watchers(tg-watchers, 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 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)

View File

@ -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)

View File

@ -4,7 +4,7 @@ block head
title Taiga Your agile, free, and open source project management tool title Taiga Your agile, free, and open source project management tool
block content block content
div.wrapper(tg-us-detail, ng-controller="UserStoryDetailController as ctrl", div.wrapper(ng-controller="UserStoryDetailController as ctrl",
ng-init="section='backlog'") ng-init="section='backlog'")
div.main.us-detail div.main.us-detail
div.us-detail-header.header-with-actions div.us-detail-header.header-with-actions
@ -15,16 +15,12 @@ block content
href="", title="Go to taskboard", href="", title="Go to taskboard",
tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug", tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug",
ng-if="sprint && project.is_backlog_activated") Taskboard 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 section.us-story-main-data
div.us-title(ng-class="{blocked: us.is_blocked}") div.us-title(ng-class="{blocked: us.is_blocked}")
h2.us-title-text h2.us-title-text
span.us-number(tg-bo-ref="us.ref") 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 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", 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") p.block-desc-container(ng-show="us.is_blocked")
span.block-description-title 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 div.issue-nav
a.icon.icon-arrow-left(ng-show="previousUrl",href="{{ previousUrl }}", a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl",
title="previous user story") title="previous user story")
a.icon.icon-arrow-right(ng-show="nextUrl", href="{{ nextUrl }}", title="next 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 include views/modules/related-tasks
@ -50,15 +47,27 @@ block content
tg-history(ng-model="us", type="us") tg-history(ng-model="us", type="us")
sidebar.menu-secondary.sidebar sidebar.menu-secondary.sidebar
section.us-status(tg-us-status-detail, ng-model="us") section.us-status
section.us-assigned-to(tg-assigned-to, ng-model="us") h1(tg-us-status-display, ng-model="us")
section.us-created-by(tg-created-by, ng-model="us") div.us-detail-progress-bar(tg-us-tasks-progress-display, ng-model="tasks")
section.watchers(tg-watchers, ng-model="us") 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 section.us-detail-settings
span.button.button-gray(href="", title="Client requirement", tg-us-team-requirement-button(ng-model="us")
ng-class="{'active': us.client_requirement}") Client requirement tg-us-client-requirement-button(ng-model="us")
span.button.button-gray(href="", title="Team requirement", tg-block-button(tg-check-permission="modify_us", ng-model="us")
ng-class="{'active': us.team_requirement}") Team requirement 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)

View File

@ -27,6 +27,3 @@ block content
fieldset fieldset
input(type="submit", class="hidden") input(type="submit", class="hidden")
a.button.button-green(href="", ng-click="ctrl.save()") Save a.button.button-green(href="", ng-click="ctrl.save()") Save
div.lightbox.lightbox-delete-account.hidden
include views/modules/lightbox-delete-account

View File

@ -24,9 +24,9 @@ block content
span.icon.icon-spinner span.icon.icon-spinner
input(type="file", id="avatar-field", class="hidden", input(type="file", id="avatar-field", class="hidden",
tg-avatar-model="avatarAttachment") tg-avatar-model="avatarAttachment")
p The image will be cropped to 80x80px.<br>
p The image will be cropped to 80x80 size. span.size-info.hidden(tg-bo-html="maxFileSizeMsg")
a.button.button-green.change Change a.button.button-green.change(tg-bo-title="'Change photo. ' + maxFileSizeMsg") Change
a.use-gravatar Use gravatar image a.use-gravatar Use gravatar image
div.data div.data
@ -60,7 +60,4 @@ block content
a.delete-account(href="", title="Delete Taiga account", a.delete-account(href="", title="Delete Taiga account",
ng-click="ctrl.openDeleteLightbox()") Delete Taiga account ng-click="ctrl.openDeleteLightbox()") Delete Taiga account
div.lightbox.lightbox-delete-account.hidden(tg-lb-delete-user) div.lightbox.lightbox-delete-account(tg-lb-delete-user)
div.lightbox.lightbox-confirm-use-gravatar.hidden
include views/modules/lightbox-use-gravatar

View File

@ -4,10 +4,10 @@ div.kanban-task-inner
div.task-text div.task-text
a.task-assigned(href="", title="Assign User Story") a.task-assigned(href="", title="Assign User Story")
span.task-num(tg-bo-ref="us.ref") 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") tg-nav="project-userstories-detail:project=project.slug,ref=us.ref")
a.task-points(href="", title="Total Us points") a.task-points(href="", title="Total Us points")
span(tg-bind-html="us.total_points") -- span(ng-bind="us.total_points") --
span points span points
a.icon.icon-edit(tg-check-permission="modify_us", href="", title="Edit") 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") a.icon.icon-drag-h(tg-check-permission="modify_us", href="", title="Drag&Drop")

View File

@ -2,7 +2,7 @@ div.summary.large-summary
div div
div.summary-progress-bar(tg-progress-bar="stats.completedPercentage") div.summary-progress-bar(tg-progress-bar="stats.completedPercentage")
div.data div.data
span.number(tg-bind-html="stats.completedPercentage + '%'") span.number(ng-bind="stats.completedPercentage + '%'")
ul ul
li li

View File

@ -2,17 +2,17 @@ div.summary
div.summary-progress-bar(tg-backlog-progress-bar="stats") div.summary-progress-bar(tg-backlog-progress-bar="stats")
div.data div.data
span.number(tg-bind-html="stats.completedPercentage + '%'") span.number(ng-bind="stats.completedPercentage + '%'")
ul ul
li li
span.number(tg-bind-html="stats.total_points") -- span.number(ng-bind="stats.total_points") --
span.description project<br />points span.description project<br />points
li li
span.number(tg-bind-html="stats.defined_points") -- span.number(ng-bind="stats.defined_points") --
span.description defined<br />points span.description defined<br />points
li li
span.number(tg-bind-html="stats.closed_points") -- span.number(ng-bind="stats.closed_points") --
span.description closed<br />points span.description closed<br />points
li li
span.number(tg-bind-html="stats.speed | number:0") -- span.number(ng-bind="stats.speed | number:0") --
span.description points /<br />sprint span.description points /<br />sprint

View File

@ -33,8 +33,8 @@ section.project-values-table
data-type="number") data-type="number")
div.project-values-settings div.project-values-settings
a.save.icon.icon-floppy(href="", title="Add") a.save.icon.icon-floppy(href="", title="Save changes")
a.cancel.icon.icon-delete(href="", title="Delete") a.cancel.icon.icon-delete(href="", title="Cancel")
form form
div.project-values-row.new-value.hidden div.project-values-row.new-value.hidden
@ -48,4 +48,4 @@ section.project-values-table
div.project-values-settings div.project-values-settings
a.add-new.icon.icon-floppy(href="", title="Add") 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")

View File

@ -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'}]") ng-options="e.id as e.name for e in [{'id':true, 'name':'Yes'},{'id':false, 'name': 'No'}]")
div.options-column div.options-column
a.save.icon.icon-floppy(href="", title="Add") a.save.icon.icon-floppy(href="", title="Save changes")
a.cancel.icon.icon-delete(href="", title="Delete") a.cancel.icon.icon-delete(href="", title="Cancel")
form form
div.row.table-main.new-value.hidden div.row.table-main.new-value.hidden
@ -57,4 +57,4 @@ section.colors-table
div.options-column div.options-column
a.add-new.icon.icon-floppy(href="", title="Add") 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")

View File

@ -31,8 +31,8 @@ section.colors-table
ng-model="value.name", data-required="true", data-maxlength="255") ng-model="value.name", data-required="true", data-maxlength="255")
div.options-column div.options-column
a.save.icon.icon-floppy(href="", title="Add") a.save.icon.icon-floppy(href="", title="Save changes")
a.cancel.icon.icon-delete(href="", title="Delete") a.cancel.icon.icon-delete(href="", title="Cancel")
form form
div.row.table-main.new-value.hidden div.row.table-main.new-value.hidden
@ -46,4 +46,4 @@ section.colors-table
div.options-column div.options-column
a.add-new.icon.icon-floppy(href="", title="Add") 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")

View File

@ -46,8 +46,8 @@ section.colors-table
ng-model="value.wip_limit", data-type="digits") ng-model="value.wip_limit", data-type="digits")
div.options-column div.options-column
a.save.icon.icon-floppy(href="", title="Add") a.save.icon.icon-floppy(href="", title="Save changes")
a.cancel.icon.icon-delete(href="", title="Delete") a.cancel.icon.icon-delete(href="", title="Cancel")
form form
div.row.table-main.new-value.hidden div.row.table-main.new-value.hidden
@ -69,4 +69,4 @@ section.colors-table
div.options-column div.options-column
a.add-new.icon.icon-floppy(href="", title="Add") 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")

View File

@ -7,6 +7,6 @@ section
block content block content
div.delete-options div.options
a.button.button-green(href="", title="Accept") a.button.button-green(href="", title="Accept")
span Accept span Accept

View File

@ -4,6 +4,7 @@ form
h2.title New Member h2.title New Member
//- Form is set in a directive //- Form is set in a directive
.add-member-forms
a.button.button-green(href="", title="Save") a.button.button-green(href="", title="Save")
span Create span Create

View File

@ -1,13 +1,14 @@
a.close(href="", title="close") a.close(href="", title="close")
span.icon.icon-delete span.icon.icon-delete
form form
h2.title Delete User Story h2.title
p p.question
span.delete-question Are you sure you want to delete? p.subtitle
span.subtitle #125 Crear el perfil de usuario senior en el admin p.replacement
span.replacement What value do you want to use as replacement?
select.choices select.choices
div.delete-options p.warning
div.options
a.button.button-green(href="", title="Accept") a.button.button-green(href="", title="Accept")
span Accept span Accept
a.button.button-red(href="", title="Delete") a.button.button-red(href="", title="Delete")

View File

@ -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

View File

@ -14,7 +14,7 @@ form
select.severity(ng-model="issue.severity", ng-options="s.id as s.name for s in severityList") select.severity(ng-model="issue.severity", ng-options="s.id as s.name for s in severityList")
fieldset fieldset
div(tg-tag-line, editable="true", ng-model="issue.tags") div.tags-block(tg-lb-tag-line, ng-model="issue.tags")
fieldset fieldset
textarea.description(placeholder="Description", ng-model="issue.description") textarea.description(placeholder="Description", ng-model="issue.description")

View File

@ -3,9 +3,9 @@ a.close(href="", title="close")
form form
h2.title Delete Taiga Account h2.title Delete Taiga Account
p 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! :-( span.subtitle We're going to miss you! :-(
div.delete-options div.options
a.button.button-green(href="", title="Accept") a.button.button-green(href="", title="Accept")
span Accept span Accept
a.button.button-red(href="", title="Cancel") a.button.button-red(href="", title="Cancel")

Some files were not shown because too many files have changed in this diff Show More