Merge branch 'master' into stable

Conflicts:
	app/styles/layout/us-detail.scss
stable
Jesús Espino 2015-03-17 10:57:47 +01:00
commit 3be01527da
237 changed files with 3304 additions and 2307 deletions

View File

@ -7,14 +7,22 @@ The PRIMARY AUTHORS are:
- Alejandro Alonso <alejandro.alonso@kaleidos.net> - Alejandro Alonso <alejandro.alonso@kaleidos.net>
- Anler Hernández <hello@anler.me> - Anler Hernández <hello@anler.me>
- Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net> - Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
- Esther Moreno Riesco <esther.moreno@kaleidos.net>
Special thanks to Kaleidos Open Source S.L. for provide time for taiga Special thanks to Kaleidos Open Source S.L. for provide time for Taiga
development. development.
And here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS -- And here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS --
people who have submitted patches, reported bugs, added translations, helped people who have submitted patches, reported bugs, added translations, helped
answer newbie questions, and generally made taiga that much better: answer newbie questions, and generally made Taiga that much better:
- Pilar Esteban <pilar.esteban@gmail.com> - Pilar Esteban <pilar.esteban@gmail.com>
- Guilhem Got <guilhem.got@gmail.com> - Guilhem Got <guilhem.got@gmail.com>
... - Ramiro Sánchez <ramiro.sanzhez@kaleidos.net>
- Miguel de la Cruz <miguel.delacruz@kaleidos.net>
- Andrea Stagi <stagi.andrea@gmail.com>
- Jordan Rinke
- Wil Wade
- Daniel Koch
- Florian Bezagu
- Ryan Swanstrom

View File

@ -1,5 +1,19 @@
# Changelog # # Changelog #
## 1.6.0 Abies Bifolia (2015-03-17)
### Features
- Added custom fields per project for user stories, tasks and issues.
- Add to the Admin Panel the export to CSV sections.
- Reorganized the Admin Panel.
### Misc
- New contrib plugin for hipchat (by Δndrea Stagi)
- Plugin based authentication.
- Added Taiga Style Guide in support Pages to enhance open source design.
- Lots of small and not so small bugfixes.
## 1.5.0 Betula Pendula - FOSDEM 2015 (2015-01-29) ## 1.5.0 Betula Pendula - FOSDEM 2015 (2015-01-29)
### Features ### Features

View File

@ -1,6 +1,7 @@
# Taiga Front # # Taiga Front #
![Kaleidos Project](http://kaleidos.net/static/img/badge.png "Kaleidos Project") ![Kaleidos Project](http://kaleidos.net/static/img/badge.png "Kaleidos Project")
[![Managed with Taiga](https://taiga.io/media/support/attachments/article-22/banner-gh.png)](https://taiga.io "Managed with Taiga")
## Get the compiled version ## ## Get the compiled version ##

View File

@ -8,7 +8,6 @@ window.taigaConfig = {
"privacyPolicyUrl": null, "privacyPolicyUrl": null,
"termsOfServiceUrl": null, "termsOfServiceUrl": null,
"maxUploadFileSize": null, "maxUploadFileSize": null,
"gitHubClientId": null,
"contribPlugins": [] "contribPlugins": []
} }

View File

@ -36,20 +36,26 @@ taiga.generateUniqueSessionIdentifier = ->
taiga.sessionId = taiga.generateUniqueSessionIdentifier() taiga.sessionId = taiga.generateUniqueSessionIdentifier()
configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEventsProvider, tgLoaderProvider) -> configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEventsProvider, tgLoaderProvider, $compileProvider) ->
$routeProvider.when("/", $routeProvider.when("/",
{templateUrl: "project/projects.html", resolve: {loader: tgLoaderProvider.add()}}) {templateUrl: "project/projects.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/", $routeProvider.when("/project/:pslug/",
{templateUrl: "project/project.html"}) {templateUrl: "project/project.html"})
$routeProvider.when("/project/:pslug/backlog",
{templateUrl: "backlog/backlog.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/taskboard/:sslug",
{templateUrl: "taskboard/taskboard.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/search", $routeProvider.when("/project/:pslug/search",
{templateUrl: "search/search.html", reloadOnSearch: false}) {templateUrl: "search/search.html", reloadOnSearch: false})
$routeProvider.when("/project/:pslug/backlog",
{templateUrl: "backlog/backlog.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/kanban", $routeProvider.when("/project/:pslug/kanban",
{templateUrl: "kanban/kanban.html", resolve: {loader: tgLoaderProvider.add()}}) {templateUrl: "kanban/kanban.html", resolve: {loader: tgLoaderProvider.add()}})
# Milestone
$routeProvider.when("/project/:pslug/taskboard/:sslug",
{templateUrl: "taskboard/taskboard.html", resolve: {loader: tgLoaderProvider.add()}})
# User stories # User stories
$routeProvider.when("/project/:pslug/us/:usref", $routeProvider.when("/project/:pslug/us/:usref",
{templateUrl: "us/us-detail.html", resolve: {loader: tgLoaderProvider.add()}}) {templateUrl: "us/us-detail.html", resolve: {loader: tgLoaderProvider.add()}})
@ -74,7 +80,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
$routeProvider.when("/project/:pslug/issue/:issueref", $routeProvider.when("/project/:pslug/issue/:issueref",
{templateUrl: "issue/issues-detail.html", resolve: {loader: tgLoaderProvider.add()}}) {templateUrl: "issue/issues-detail.html", resolve: {loader: tgLoaderProvider.add()}})
# Admin # Admin - Project Profile
$routeProvider.when("/project/:pslug/admin/project-profile/details", $routeProvider.when("/project/:pslug/admin/project-profile/details",
{templateUrl: "admin/admin-project-profile.html"}) {templateUrl: "admin/admin-project-profile.html"})
$routeProvider.when("/project/:pslug/admin/project-profile/default-values", $routeProvider.when("/project/:pslug/admin/project-profile/default-values",
@ -83,24 +89,28 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
{templateUrl: "admin/admin-project-modules.html"}) {templateUrl: "admin/admin-project-modules.html"})
$routeProvider.when("/project/:pslug/admin/project-profile/export", $routeProvider.when("/project/:pslug/admin/project-profile/export",
{templateUrl: "admin/admin-project-export.html"}) {templateUrl: "admin/admin-project-export.html"})
$routeProvider.when("/project/:pslug/admin/project-values/us-status", $routeProvider.when("/project/:pslug/admin/project-profile/reports",
{templateUrl: "admin/admin-project-values-us-status.html"}) {templateUrl: "admin/admin-project-reports.html"})
$routeProvider.when("/project/:pslug/admin/project-values/us-points",
{templateUrl: "admin/admin-project-values-us-points.html"}) $routeProvider.when("/project/:pslug/admin/project-values/status",
$routeProvider.when("/project/:pslug/admin/project-values/task-status", {templateUrl: "admin/admin-project-values-status.html"})
{templateUrl: "admin/admin-project-values-task-status.html"}) $routeProvider.when("/project/:pslug/admin/project-values/points",
$routeProvider.when("/project/:pslug/admin/project-values/issue-status", {templateUrl: "admin/admin-project-values-points.html"})
{templateUrl: "admin/admin-project-values-issue-status.html"}) $routeProvider.when("/project/:pslug/admin/project-values/priorities",
$routeProvider.when("/project/:pslug/admin/project-values/issue-types", {templateUrl: "admin/admin-project-values-priorities.html"})
{templateUrl: "admin/admin-project-values-issue-types.html"}) $routeProvider.when("/project/:pslug/admin/project-values/severities",
$routeProvider.when("/project/:pslug/admin/project-values/issue-priorities", {templateUrl: "admin/admin-project-values-severities.html"})
{templateUrl: "admin/admin-project-values-issue-priorities.html"}) $routeProvider.when("/project/:pslug/admin/project-values/types",
$routeProvider.when("/project/:pslug/admin/project-values/issue-severities", {templateUrl: "admin/admin-project-values-types.html"})
{templateUrl: "admin/admin-project-values-issue-severities.html"}) $routeProvider.when("/project/:pslug/admin/project-values/custom-fields",
{templateUrl: "admin/admin-project-values-custom-fields.html"})
$routeProvider.when("/project/:pslug/admin/memberships", $routeProvider.when("/project/:pslug/admin/memberships",
{templateUrl: "admin/admin-memberships.html"}) {templateUrl: "admin/admin-memberships.html"})
# Admin - Roles
$routeProvider.when("/project/:pslug/admin/roles", $routeProvider.when("/project/:pslug/admin/roles",
{templateUrl: "admin/admin-roles.html"}) {templateUrl: "admin/admin-roles.html"})
# Admin - Third Parties
$routeProvider.when("/project/:pslug/admin/third-parties/webhooks", $routeProvider.when("/project/:pslug/admin/third-parties/webhooks",
{templateUrl: "admin/admin-third-parties-webhooks.html"}) {templateUrl: "admin/admin-third-parties-webhooks.html"})
$routeProvider.when("/project/:pslug/admin/third-parties/github", $routeProvider.when("/project/:pslug/admin/third-parties/github",
@ -109,6 +119,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
{templateUrl: "admin/admin-third-parties-gitlab.html"}) {templateUrl: "admin/admin-third-parties-gitlab.html"})
$routeProvider.when("/project/:pslug/admin/third-parties/bitbucket", $routeProvider.when("/project/:pslug/admin/third-parties/bitbucket",
{templateUrl: "admin/admin-third-parties-bitbucket.html"}) {templateUrl: "admin/admin-third-parties-bitbucket.html"})
# Admin - Contrib Plugins
$routeProvider.when("/project/:pslug/admin/contrib/:plugin", $routeProvider.when("/project/:pslug/admin/contrib/:plugin",
{templateUrl: "contrib/main.html"}) {templateUrl: "contrib/main.html"})
@ -223,6 +234,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
linewidth: "The subject must have a maximum size of %s" linewidth: "The subject must have a maximum size of %s"
}) })
$compileProvider.debugInfoEnabled(window.taigaConfig.debugInfo || false)
init = ($log, $i18n, $config, $rootscope, $auth, $events, $analytics) -> init = ($log, $i18n, $config, $rootscope, $auth, $events, $analytics) ->
$i18n.initialize($config.get("defaultLanguage")) $i18n.initialize($config.get("defaultLanguage"))
$log.debug("Initialize application") $log.debug("Initialize application")
@ -280,6 +293,7 @@ module.config([
"$provide", "$provide",
"$tgEventsProvider", "$tgEventsProvider",
"tgLoaderProvider", "tgLoaderProvider",
"$compileProvider",
configure configure
]) ])

View File

@ -33,7 +33,9 @@ MAX_MEMBERSHIP_FIELDSETS = 4
CreateMembersDirective = ($rs, $rootScope, $confirm, $loading ,lightboxService) -> CreateMembersDirective = ($rs, $rootScope, $confirm, $loading ,lightboxService) ->
extraTextTemplate = """ extraTextTemplate = """
<fieldset class="extra-text"> <fieldset class="extra-text">
<textarea placeholder="(Optional) Add a personalized text to the invitation. Tell something lovely to your new members ;-)"></textarea> <textarea placeholder="(Optional) Add a personalized text to the invitation. Tell something lovely to your new members ;-)"
maxlength="255">
</textarea>
</fieldset> </fieldset>
""" """
@ -150,7 +152,6 @@ CreateMembersDirective = ($rs, $rootScope, $confirm, $loading ,lightboxService)
submitButton = $el.find(".submit-button") submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link: link} return {link: link}

View File

@ -67,6 +67,9 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai
loadProject: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
if not project.i_am_owner
@location.path(@navUrls.resolve("permission-denied"))
@scope.project = project @scope.project = project
@scope.$emit('project:loaded', project) @scope.$emit('project:loaded', project)
return project return project
@ -307,7 +310,7 @@ MembershipsRowRoleSelectorDirective = ($log, $repo, $confirm) ->
member = $scope.$eval($attrs.tgMembershipsRowRoleSelector) member = $scope.$eval($attrs.tgMembershipsRowRoleSelector)
html = render(member) html = render(member)
$el.on "click", "select", (event) => $el.on "change", "select", (event) =>
onSuccess = -> onSuccess = ->
$confirm.notify("success") $confirm.notify("success")

View File

@ -65,6 +65,9 @@ class ProjectProfileController extends mixOf(taiga.Controller, taiga.PageMixin)
loadProject: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
if not project.i_am_owner
@location.path(@navUrls.resolve("permission-denied"))
@scope.project = project @scope.project = project
@scope.pointsList = _.sortBy(project.points, "order") @scope.pointsList = _.sortBy(project.points, "order")
@scope.usStatusList = _.sortBy(project.us_statuses, "order") @scope.usStatusList = _.sortBy(project.us_statuses, "order")
@ -120,7 +123,6 @@ ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location) ->
submitButton = $el.find(".submit-button") submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link} return {link:link}
@ -154,7 +156,6 @@ ProjectDefaultValuesDirective = ($repo, $confirm, $loading) ->
submitButton = $el.find(".submit-button") submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()
@ -233,7 +234,7 @@ ProjectExportDirective = ($window, $rs, $confirm) ->
resultTitleEl = $el.find(".result-title") resultTitleEl = $el.find(".result-title")
setLoadingTitle = -> resultTitleEl.html("We are generating your dump file") # TODO: i18n setLoadingTitle = -> resultTitleEl.html("We are generating your dump file") # TODO: i18n
setAsyncTitle = -> resultTitleEl.html("We are generating your dump file") # TODO: i18n setAsyncTitle = -> resultTitleEl.html("We are generating your dump file") # TODO: i18n
setSyncTitle = -> resultTitleEl.html("Your dump file ir ready!") # TODO: i18n setSyncTitle = -> resultTitleEl.html("Your dump file is ready!") # TODO: i18n
resultMessageEl = $el.find(".result-message ") resultMessageEl = $el.find(".result-message ")
setLoadingMessage = -> resultMessageEl.html("Please don't close this page.") # TODO: i18n setLoadingMessage = -> resultMessageEl.html("Please don't close this page.") # TODO: i18n
@ -296,3 +297,67 @@ ProjectExportDirective = ($window, $rs, $confirm) ->
return {link:link} return {link:link}
module.directive("tgProjectExport", ["$window", "$tgResources", "$tgConfirm", ProjectExportDirective]) module.directive("tgProjectExport", ["$window", "$tgResources", "$tgConfirm", ProjectExportDirective])
#############################################################################
## CSV Export Controllers
#############################################################################
class CsvExporterController extends taiga.Controller
@.$inject = [
"$scope",
"$rootScope",
"$tgUrls",
"$tgConfirm",
"$tgResources",
]
constructor: (@scope, @rootscope, @urls, @confirm, @rs) ->
@rootscope.$on("project:loaded", @.setCsvUuid)
@scope.$watch "csvUuid", (value) =>
if value
@scope.csvUrl = @urls.resolveAbsolute("#{@.type}-csv", value)
else
@scope.csvUrl = ""
setCsvUuid: =>
@scope.csvUuid = @scope.project["#{@.type}_csv_uuid"]
_generateUuid: (finish) =>
promise = @rs.projects["regenerate_#{@.type}_csv_uuid"](@scope.projectId)
promise.then (data) =>
@scope.csvUuid = data.data?.uuid
promise.then null, =>
@confirm.notify("error")
promise.finally ->
finish()
return promise
regenerateUuid: ->
#TODO: i18n
if @scope.csvUuid
title = "Change URL"
subtitle = "You going to change the CSV data access url. The previous url will be disabled. Are you sure?"
@confirm.ask(title, subtitle).then @._generateUuid
else
@._generateUuid(_.identity)
class CsvExporterUserstoriesController extends CsvExporterController
type: "userstories"
class CsvExporterTasksController extends CsvExporterController
type: "tasks"
class CsvExporterIssuesController extends CsvExporterController
type: "issues"
module.controller("CsvExporterUserstoriesController", CsvExporterUserstoriesController)
module.controller("CsvExporterTasksController", CsvExporterTasksController)
module.controller("CsvExporterIssuesController", CsvExporterIssuesController)

View File

@ -32,10 +32,10 @@ debounce = @.taiga.debounce
module = angular.module("taigaAdmin") module = angular.module("taigaAdmin")
############################################################################# #############################################################################
## Project values Controller ## Project values section Controller
############################################################################# #############################################################################
class ProjectValuesController extends mixOf(taiga.Controller, taiga.PageMixin) class ProjectValuesSectionController extends mixOf(taiga.Controller, taiga.PageMixin)
@.$inject = [ @.$inject = [
"$scope", "$scope",
"$rootScope", "$rootScope",
@ -59,29 +59,47 @@ class ProjectValuesController extends mixOf(taiga.Controller, taiga.PageMixin)
promise.then null, @.onInitialDataError.bind(@) promise.then null, @.onInitialDataError.bind(@)
@scope.$on("admin:project-values:move", @.moveValue)
loadProject: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
if not project.i_am_owner
@location.path(@navUrls.resolve("permission-denied"))
@scope.project = project @scope.project = project
@scope.$emit('project:loaded', project) @scope.$emit('project:loaded', project)
return project return project
loadValues: ->
return @rs[@scope.resource].listValues(@scope.projectId, @scope.type).then (values) =>
@scope.values = values
@scope.maxValueOrder = _.max(values, "order").order
return values
loadInitialData: -> loadInitialData: ->
promise = @repo.resolve({pslug: @params.pslug}).then (data) => promise = @repo.resolve({pslug: @params.pslug}).then (data) =>
@scope.projectId = data.project @scope.projectId = data.project
return data return data
return promise.then( => @q.all([ return promise.then => @.loadProject()
@.loadProject(),
@.loadValues(),
])) module.controller("ProjectValuesSectionController", ProjectValuesSectionController)
#############################################################################
## Project values Controller
#############################################################################
class ProjectValuesController extends taiga.Controller
@.$inject = [
"$scope",
"$rootScope",
"$tgRepo",
"$tgConfirm",
"$tgResources",
]
constructor: (@scope, @rootscope, @repo, @confirm, @rs) ->
@scope.$on("admin:project-values:move", @.moveValue)
@rootscope.$on("project:loaded", @.loadValues)
loadValues: =>
return @rs[@scope.resource].listValues(@scope.projectId, @scope.type).then (values) =>
@scope.values = values
@scope.maxValueOrder = _.max(values, "order").order
return values
moveValue: (ctx, itemValue, itemIndex) => moveValue: (ctx, itemValue, itemIndex) =>
values = @scope.values values = @scope.values
@ -147,21 +165,14 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
$(document.body).scrollTop(table.offset().top + table.height()) $(document.body).scrollTop(table.offset().top + table.height())
if focus if focus
$(".new-value input").focus() $el.find(".new-value input:visible").first().focus()
submit = debounce 2000, => saveValue = (target) ->
promise = $repo.save($scope.project) formEl = target.parents("form")
promise.then -> form = formEl.checksley()
$confirm.notify("success")
promise.then null, (data) ->
$confirm.notify("error", data._error_message)
saveValue = debounce 2000, (target) ->
form = target.parents("form").checksley()
return if not form.validate() return if not form.validate()
value = target.scope().value value = formEl.scope().value
promise = $repo.save(value) promise = $repo.save(value)
promise.then => promise.then =>
row = target.parents(".row.table-main") row = target.parents(".row.table-main")
@ -169,25 +180,37 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
row.siblings(".visualization").removeClass('hidden') row.siblings(".visualization").removeClass('hidden')
promise.then null, (data) -> promise.then null, (data) ->
$confirm.notify("error") form.setErrors(data)
saveNewValue = (target) ->
formEl = target.parents("form")
form = formEl.checksley()
return if not form.validate()
$scope.newValue.project = $scope.project.id
$scope.newValue.order = if $scope.maxValueOrder then $scope.maxValueOrder + 1 else 1
promise = $repo.create(valueType, $scope.newValue)
promise.then (data) =>
target.addClass("hidden")
$scope.values.push(data)
$scope.maxValueOrder = data.order
initializeNewValue()
promise.then null, (data) ->
form.setErrors(data) form.setErrors(data)
cancel = (target) -> cancel = (target) ->
row = target.parents(".row.table-main") row = target.parents(".row.table-main")
value = target.scope().value formEl = target.parents("form")
value = formEl.scope().value
$scope.$apply -> $scope.$apply ->
row.addClass("hidden") row.addClass("hidden")
value.revert() value.revert()
row.siblings(".visualization").removeClass('hidden') row.siblings(".visualization").removeClass('hidden')
$el.on "submit", "form", (event) ->
event.preventDefault()
submit()
$el.on "click", "form a.button-green", (event) ->
event.preventDefault()
submit()
$el.on "click", ".show-add-new", (event) -> $el.on "click", ".show-add-new", (event) ->
event.preventDefault() event.preventDefault()
$el.find(".new-value").removeClass('hidden') $el.find(".new-value").removeClass('hidden')
@ -196,29 +219,12 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
$el.on "click", ".add-new", debounce 2000, (event) -> $el.on "click", ".add-new", debounce 2000, (event) ->
event.preventDefault() event.preventDefault()
form = $el.find(".new-value").parents("form").checksley() target = $el.find(".new-value")
return if not form.validate() saveNewValue(target)
$scope.newValue.project = $scope.project.id
$scope.newValue.order = if $scope.maxValueOrder then $scope.maxValueOrder + 1 else 1
promise = $repo.create(valueType, $scope.newValue)
promise.then =>
$ctrl.loadValues().then ->
animationFrame.add () ->
goToBottomList()
$el.find(".new-value").addClass("hidden")
initializeNewValue()
promise.then null, (data) ->
$confirm.notify("error")
form.setErrors(data)
$el.on "click", ".delete-new", (event) -> $el.on "click", ".delete-new", (event) ->
event.preventDefault() event.preventDefault()
$el.find(".new-value").hide() $el.find(".new-value").addClass("hidden")
initializeNewValue() initializeNewValue()
$el.on "click", ".edit-value", (event) -> $el.on "click", ".edit-value", (event) ->
@ -240,6 +246,14 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
cancel(target) cancel(target)
$el.on "keyup", ".new-value input", (event) ->
if event.keyCode == 13
target = $el.find(".new-value")
saveNewValue(target)
else if event.keyCode == 27
$el.find(".new-value").addClass("hidden")
initializeNewValue()
$el.on "click", ".save", (event) -> $el.on "click", ".save", (event) ->
event.preventDefault() event.preventDefault()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
@ -253,7 +267,9 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
$el.on "click", ".delete-value", (event) -> $el.on "click", ".delete-value", (event) ->
event.preventDefault() event.preventDefault()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
value = target.scope().value formEl = target.parents("form")
value = formEl.scope().value
choices = {} choices = {}
_.each $scope.values, (option) -> _.each $scope.values, (option) ->
if value.id != option.id if value.id != option.id
@ -337,3 +353,276 @@ ColorSelectionDirective = () ->
} }
module.directive("tgColorSelection", ColorSelectionDirective) module.directive("tgColorSelection", ColorSelectionDirective)
#############################################################################
## Custom Attributes Controller
#############################################################################
class ProjectCustomAttributesController extends mixOf(taiga.Controller, taiga.PageMixin)
@.$inject = [
"$scope",
"$rootScope",
"$tgRepo",
"$tgResources",
"$routeParams",
"$q",
"$tgLocation",
"$tgNavUrls",
"$appTitle",
]
constructor: (@scope, @rootscope, @repo, @rs, @params, @q, @location, @navUrls, @appTitle) ->
@scope.project = {}
@rootscope.$on "project:loaded", =>
@.loadCustomAttributes()
@appTitle.set("Project Custom Attributes - " + @scope.sectionName + " - " + @scope.project.name)
#########################
# Custom Attribute
#########################
loadCustomAttributes: =>
return @rs.customAttributes[@scope.type].list(@scope.projectId).then (customAttributes) =>
@scope.customAttributes = customAttributes
@scope.maxOrder = _.max(customAttributes, "order").order
return customAttributes
createCustomAttribute: (attrValues) =>
return @repo.create("custom-attributes/#{@scope.type}", attrValues)
saveCustomAttribute: (attrModel) =>
return @repo.save(attrModel)
deleteCustomAttribute: (attrModel) =>
return @repo.remove(attrModel)
moveCustomAttributes: (attrModel, newIndex) =>
customAttributes = @scope.customAttributes
r = customAttributes.indexOf(attrModel)
customAttributes.splice(r, 1)
customAttributes.splice(newIndex, 0, attrModel)
_.each customAttributes, (val, idx) ->
val.order = idx
@repo.saveAll(customAttributes)
module.controller("ProjectCustomAttributesController", ProjectCustomAttributesController)
#############################################################################
## Custom Attributes Directive
#############################################################################
ProjectCustomAttributesDirective = ($log, $confirm, animationFrame) ->
link = ($scope, $el, $attrs) ->
$ctrl = $el.controller()
$scope.$on "$destroy", ->
$el.off()
##################################
# Drag & Drop
##################################
sortableEl = $el.find(".js-sortable")
sortableEl.sortable({
handle: ".js-view-custom-field",
dropOnEmpty: true
revert: 400
axis: "y"
})
sortableEl.on "sortstop", (event, ui) ->
itemEl = ui.item
itemAttr = itemEl.scope().attr
itemIndex = itemEl.index()
$ctrl.moveCustomAttributes(itemAttr, itemIndex)
##################################
# New custom attribute
##################################
showCreateForm = ->
$el.find(".js-new-custom-field").removeClass("hidden")
$el.find(".js-new-custom-field input:visible").first().focus()
hideCreateForm = ->
$el.find(".js-new-custom-field").addClass("hidden")
showAddButton = ->
$el.find(".js-add-custom-field-button").removeClass("hidden")
hideAddButton = ->
$el.find(".js-add-custom-field-button").addClass("hidden")
showCancelButton = ->
$el.find(".js-cancel-new-custom-field-button").removeClass("hidden")
hideCancelButton = ->
$el.find(".js-cancel-new-custom-field-button").addClass("hidden")
resetNewAttr = ->
$scope.newAttr = {}
create = (formEl) ->
form = formEl.checksley()
return if not form.validate()
onSucces = =>
$ctrl.loadCustomAttributes()
hideCreateForm()
resetNewAttr()
$confirm.notify("success")
onError = (data) =>
form.setErrors(data)
attr = $scope.newAttr
attr.project = $scope.projectId
attr.order = if $scope.maxOrder then $scope.maxOrder + 1 else 1
$ctrl.createCustomAttribute(attr).then(onSucces, onError)
cancelCreate = ->
hideCreateForm()
resetNewAttr()
$scope.$watch "customAttributes", (customAttributes) ->
return if not customAttributes
if customAttributes.length == 0
hideCancelButton()
hideAddButton()
showCreateForm()
else
hideCreateForm()
showAddButton()
showCancelButton()
$el.on "click", ".js-add-custom-field-button", (event) ->
event.preventDefault()
showCreateForm()
$el.on "click", ".js-create-custom-field-button", debounce 2000, (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
formEl = target.closest("form")
create(formEl)
$el.on "click", ".js-cancel-new-custom-field-button", (event) ->
event.preventDefault()
cancelCreate()
$el.on "keyup", ".js-new-custom-field input", (event) ->
if event.keyCode == 13 # Enter
target = angular.element(event.currentTarget)
formEl = target.closest("form")
create(formEl)
else if event.keyCode == 27 # Esc
cancelCreate()
##################################
# Edit custom attribute
##################################
showEditForm = (formEl) ->
formEl.find(".js-view-custom-field").addClass("hidden")
formEl.find(".js-edit-custom-field").removeClass("hidden")
formEl.find(".js-edit-custom-field input:visible").first().focus().select()
hideEditForm = (formEl) ->
formEl.find(".js-edit-custom-field").addClass("hidden")
formEl.find(".js-view-custom-field").removeClass("hidden")
revertChangesInCustomAttribute = (formEl) ->
$scope.$apply ->
formEl.scope().attr.revert()
update = (formEl) ->
form = formEl.checksley()
return if not form.validate()
onSucces = =>
$ctrl.loadCustomAttributes()
hideEditForm(formEl)
$confirm.notify("success")
onError = (data) =>
form.setErrors(data)
attr = formEl.scope().attr
$ctrl.saveCustomAttribute(attr).then(onSucces, onError)
cancelUpdate = (formEl) ->
hideEditForm(formEl)
revertChangesInCustomAttribute(formEl)
$el.on "click", ".js-edit-custom-field-button", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
formEl = target.closest("form")
showEditForm(formEl)
$el.on "click", ".js-update-custom-field-button", debounce 2000, (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
formEl = target.closest("form")
update(formEl)
$el.on "click", ".js-cancel-edit-custom-field-button", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
formEl = target.closest("form")
cancelUpdate(formEl)
$el.on "keyup", ".js-edit-custom-field input", (event) ->
if event.keyCode == 13 # Enter
target = angular.element(event.currentTarget)
formEl = target.closest("form")
update(formEl)
else if event.keyCode == 27 # Esc
target = angular.element(event.currentTarget)
formEl = target.closest("form")
cancelUpdate(formEl)
##################################
# Delete custom attribute
##################################
deleteCustomAttribute = (formEl) ->
attr = formEl.scope().attr
title = "Delete custom attribute" # i18n
subtitle = "Remeber that all values in this custom field will be deleted.</br> Are you sure you want to continue?"
message = attr.name
$confirm.ask(title, subtitle, message).then (finish) ->
onSucces = ->
$ctrl.loadCustomAttributes().finally ->
finish()
onError = ->
finish(false)
$confirm.notify("error", null, "We have not been able to delete '#{message}'.")
$ctrl.deleteCustomAttribute(attr).then(onSucces, onError)
$el.on "click", ".js-delete-custom-field-button", debounce 2000, (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
formEl = target.closest("form")
deleteCustomAttribute(formEl)
return {link: link}
module.directive("tgProjectCustomAttributes", ["$log", "$tgConfirm", "animationFrame", ProjectCustomAttributesDirective])

View File

@ -63,17 +63,40 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil
loadProject: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
if not project.i_am_owner
@location.path(@navUrls.resolve("permission-denied"))
@scope.project = project @scope.project = project
@scope.$emit('project:loaded', project) @scope.$emit('project:loaded', project)
@scope.anyComputableRole = _.some(_.map(project.roles, (point) -> point.computable)) @scope.anyComputableRole = _.some(_.map(project.roles, (point) -> point.computable))
return project return project
loadExternalUserRole: (roles) ->
roles = roles.map (role) ->
role.external_user = false
return role
public_permission = {
"name": "External User",
"permissions": @scope.project.public_permissions,
"external_user": true
}
roles.push(public_permission)
return roles
loadRoles: -> loadRoles: ->
return @rs.roles.list(@scope.projectId).then (data) => return @rs.roles.list(@scope.projectId)
@scope.roles = data .then @loadExternalUserRole
.then (roles) =>
@scope.roles = roles
@scope.role = @scope.roles[0] @scope.role = @scope.roles[0]
return data
return roles
loadInitialData: -> loadInitialData: ->
promise = @repo.resolve({pslug: @params.pslug}).then (data) => promise = @repo.resolve({pslug: @params.pslug}).then (data) =>
@ -256,7 +279,7 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm) ->
<div class="category-item" data-id="<%- permission.key %>"> <div class="category-item" data-id="<%- permission.key %>">
<span><%- permission.description %></span> <span><%- permission.description %></span>
<div class="check"> <div class="check">
<input type="checkbox" <% if(permission.active) { %>checked="checked"<% } %>/> <input type="checkbox" <% if(!permission.editable) { %>disabled="disabled"<% } %> <% if(permission.active) { %>checked="checked"<% } %>/>
<div></div> <div></div>
<span class="check-text check-yes">Yes</span> <span class="check-text check-yes">Yes</span>
<span class="check-text check-no">No</span> <span class="check-text check-no">No</span>
@ -279,10 +302,23 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm) ->
setActivePermissions = (permissions) -> setActivePermissions = (permissions) ->
return _.map(permissions, (x) -> _.extend({}, x, {active: x["key"] in role.permissions})) return _.map(permissions, (x) -> _.extend({}, x, {active: x["key"] in role.permissions}))
isPermissionEditable = (permission, role, project) ->
if role.external_user &&
!project.is_private &&
permission.key.indexOf("view_") == 0
return false
else
return true
setActivePermissionsPerCategory = (category) -> setActivePermissionsPerCategory = (category) ->
return _.map(category, (x) -> return _.map(category, (cat) ->
_.extend({}, x, { cat.permissions = cat.permissions.map (permission) ->
activePermissions: _.filter(x["permissions"], "active").length permission.editable = isPermissionEditable(permission, role, $scope.project)
return permission
_.extend({}, cat, {
activePermissions: _.filter(cat["permissions"], "active").length
}) })
) )
@ -366,10 +402,11 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm) ->
return activePermissions return activePermissions
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
$scope.role.permissions = getActivePermissions() $scope.role.permissions = getActivePermissions()
onSuccess = (role) -> onSuccess = () ->
categories = generateCategoriesFromRole(role) categories = generateCategoriesFromRole($scope.role)
categoryId = target.parents(".category-config").data("id") categoryId = target.parents(".category-config").data("id")
renderResume(target.parents(".category-config"), categories[categoryId]) renderResume(target.parents(".category-config"), categories[categoryId])
$rootscope.$broadcast("projects:reload") $rootscope.$broadcast("projects:reload")
@ -381,6 +418,13 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm) ->
target.prop "checked", !target.prop("checked") target.prop "checked", !target.prop("checked")
$scope.role.permissions = getActivePermissions() $scope.role.permissions = getActivePermissions()
if $scope.role.external_user
$scope.project.public_permissions = $scope.role.permissions
$scope.project.anon_permissions = $scope.role.permissions.filter (permission) ->
return permission.indexOf("view_") == 0
$repo.save($scope.project).then onSuccess, onError
else
$repo.save($scope.role).then onSuccess, onError $repo.save($scope.role).then onSuccess, onError
$scope.$on "$destroy", -> $scope.$on "$destroy", ->

View File

@ -38,10 +38,12 @@ class WebhooksController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.
"$tgRepo", "$tgRepo",
"$tgResources", "$tgResources",
"$routeParams", "$routeParams",
"$tgLocation",
"$tgNavUrls",
"$appTitle" "$appTitle"
] ]
constructor: (@scope, @repo, @rs, @params, @appTitle) -> constructor: (@scope, @repo, @rs, @params, @location, @navUrls, @appTitle) ->
bindMethods(@) bindMethods(@)
@scope.sectionName = "Webhooks" #i18n @scope.sectionName = "Webhooks" #i18n
@ -62,6 +64,9 @@ class WebhooksController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.
loadProject: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
if not project.i_am_owner
@location.path(@navUrls.resolve("permission-denied"))
@scope.project = project @scope.project = project
@scope.$emit('project:loaded', project) @scope.$emit('project:loaded', project)
return project return project
@ -89,7 +94,7 @@ WebhookDirective = ($rs, $repo, $confirm, $loading) ->
for log in webhooklogs for log in webhooklogs
log.validStatus = 200 <= log.status < 300 log.validStatus = 200 <= log.status < 300
log.prettySentHeaders = _.map(_.pairs(log.request_headers), ([header, value]) -> "#{header}: #{value}").join("\n") log.prettySentHeaders = _.map(_.pairs(log.request_headers), ([header, value]) -> "#{header}: #{value}").join("\n")
log.prettySentData = JSON.stringify(log.request_data.data, undefined, 2) log.prettySentData = JSON.stringify(log.request_data)
log.prettyDate = moment(log.created).format("DD MMM YYYY [at] hh:mm:ss") # TODO: i18n log.prettyDate = moment(log.created).format("DD MMM YYYY [at] hh:mm:ss") # TODO: i18n
webhook.logs_counter = webhooklogs.length webhook.logs_counter = webhooklogs.length
@ -123,8 +128,6 @@ WebhookDirective = ($rs, $repo, $confirm, $loading) ->
save = debounce 2000, (target) -> save = debounce 2000, (target) ->
form = target.parents("form").checksley() form = target.parents("form").checksley()
return if not form.validate() return if not form.validate()
value = target.scope().value
promise = $repo.save(webhook) promise = $repo.save(webhook)
promise.then => promise.then =>
showVisualizationMode() showVisualizationMode()
@ -152,7 +155,7 @@ WebhookDirective = ($rs, $repo, $confirm, $loading) ->
$el.on "keyup", ".edition-mode input", (event) -> $el.on "keyup", ".edition-mode input", (event) ->
if event.keyCode == 13 if event.keyCode == 13
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
saveWebhook(target) save(target)
else if event.keyCode == 27 else if event.keyCode == 27
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
cancel(target) cancel(target)
@ -231,8 +234,7 @@ NewWebhookDirective = ($rs, $repo, $confirm, $loading) ->
formDOMNode.addClass("hidden") formDOMNode.addClass("hidden")
addWebhookDOMNode.removeClass("hidden") addWebhookDOMNode.removeClass("hidden")
formDOMNode.on "click", ".add-new", debounce 2000, (event) -> save = debounce 2000, () ->
event.preventDefault()
form = formDOMNode.checksley() form = formDOMNode.checksley()
return if not form.validate() return if not form.validate()
@ -246,6 +248,14 @@ NewWebhookDirective = ($rs, $repo, $confirm, $loading) ->
$confirm.notify("error") $confirm.notify("error")
form.setErrors(data) form.setErrors(data)
formDOMNode.on "click", ".add-new", (event) ->
event.preventDefault()
save()
formDOMNode.on "keyup", "input", (event) ->
if event.keyCode == 13
save()
formDOMNode.on "click", ".cancel-new", (event) -> formDOMNode.on "click", ".cancel-new", (event) ->
$scope.$apply -> $scope.$apply ->
initializeNewValue() initializeNewValue()
@ -445,7 +455,6 @@ GithubWebhooksDirective = ($repo, $confirm, $loading) ->
submitButton = $el.find(".submit-button") submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link} return {link:link}
@ -481,7 +490,6 @@ GitlabWebhooksDirective = ($repo, $confirm, $loading) ->
submitButton = $el.find(".submit-button") submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link} return {link:link}
@ -517,7 +525,6 @@ BitbucketWebhooksDirective = ($repo, $confirm, $loading) ->
submitButton = $el.find(".submit-button") submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link} return {link:link}

View File

@ -197,11 +197,12 @@ LoginDirective = ($auth, $confirm, $location, $config, $routeParams, $navUrls, $
"password": $el.find("form.login-form input[name=password]").val() "password": $el.find("form.login-form input[name=password]").val()
} }
promise = $auth.login(data) loginFormType = $config.get("loginFormType", "normal")
promise = $auth.login(data, loginFormType)
return promise.then(onSuccess, onError) return promise.then(onSuccess, onError)
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link} return {link:link}
@ -242,7 +243,6 @@ RegisterDirective = ($auth, $confirm, $location, $navUrls, $config, $analytics)
promise.then(onSuccessSubmit, onErrorSubmit) promise.then(onSuccessSubmit, onErrorSubmit)
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link} return {link:link}
@ -279,7 +279,6 @@ ForgotPasswordDirective = ($auth, $confirm, $location, $navUrls) ->
promise.then(onSuccessSubmit, onErrorSubmit) promise.then(onSuccessSubmit, onErrorSubmit)
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link} return {link:link}
@ -321,7 +320,6 @@ ChangePasswordFromRecoveryDirective = ($auth, $confirm, $location, $params, $nav
promise.then(onSuccessSubmit, onErrorSubmit) promise.then(onSuccessSubmit, onErrorSubmit)
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link} return {link:link}
@ -471,7 +469,6 @@ CancelAccountDirective = ($repo, $model, $auth, $confirm, $location, $params, $n
promise.then(onSuccessSubmit, onErrorSubmit) promise.then(onSuccessSubmit, onErrorSubmit)
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link} return {link:link}

View File

@ -89,12 +89,10 @@ BacklogFiltersDirective = ($log, $location, $templates) ->
selectedFilters.push(filter) selectedFilters.push(filter)
$scope.$apply -> $scope.$apply ->
$ctrl.selectFilter(type, id) $ctrl.selectFilter(type, id)
$ctrl.filterVisibleUserstories()
else else
selectedFilters = _.reject(selectedFilters, filter) selectedFilters = _.reject(selectedFilters, filter)
$scope.$apply -> $scope.$apply ->
$ctrl.unselectFilter(type, id) $ctrl.unselectFilter(type, id)
$ctrl.filterVisibleUserstories()
renderSelectedFilters(selectedFilters) renderSelectedFilters(selectedFilters)

View File

@ -103,6 +103,9 @@ CreateEditSprint = ($repo, $confirm, $rs, $rootscope, lightboxService, $loading)
$repo.remove($scope.sprint).then(onSuccess, onError) $repo.remove($scope.sprint).then(onSuccess, onError)
$scope.$on "sprintform:create", (event, projectId) -> $scope.$on "sprintform:create", (event, projectId) ->
form = $el.find("form").checksley()
form.reset()
createSprint = true createSprint = true
$scope.sprint.project = projectId $scope.sprint.project = projectId
$scope.sprint.name = null $scope.sprint.name = null
@ -158,7 +161,6 @@ CreateEditSprint = ($repo, $confirm, $rs, $rootscope, lightboxService, $loading)
submitButton = $el.find(".submit-button") submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$el.on "click", ".delete-sprint .icon-delete", (event) -> $el.on "click", ".delete-sprint .icon-delete", (event) ->
event.preventDefault() event.preventDefault()

View File

@ -59,7 +59,6 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
@scope.sectionName = "Backlog" @scope.sectionName = "Backlog"
@showTags = false @showTags = false
@activeFilters = false @activeFilters = false
@excludeClosedSprints = true
@.initializeEventHandlers() @.initializeEventHandlers()
@ -111,7 +110,8 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
@scope.$on("sprint:us:moved", @.loadSprints) @scope.$on("sprint:us:moved", @.loadSprints)
@scope.$on("sprint:us:moved", @.loadProjectStats) @scope.$on("sprint:us:moved", @.loadProjectStats)
@scope.$on("backlog:toggle-closed-sprints-visualization", @.toggleClosedSprintsVisualization) @scope.$on("backlog:load-closed-sprints", @.loadClosedSprints)
@scope.$on("backlog:unload-closed-sprints", @.unloadClosedSprints)
initializeSubscription: -> initializeSubscription: ->
routingKey1 = "changes.project.#{@scope.projectId}.userstories" routingKey1 = "changes.project.#{@scope.projectId}.userstories"
@ -146,11 +146,23 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
return @rs.projects.tagsColors(@scope.projectId).then (tags_colors) => return @rs.projects.tagsColors(@scope.projectId).then (tags_colors) =>
@scope.project.tags_colors = tags_colors @scope.project.tags_colors = tags_colors
loadSprints: -> unloadClosedSprints: ->
params = {} @scope.$apply =>
if @excludeClosedSprints @scope.closedSprints = []
params["closed"] = false @rootscope.$broadcast("closed-sprints:reloaded", [])
loadClosedSprints: ->
params = {closed: true}
return @rs.sprints.list(@scope.projectId, params).then (sprints) =>
# NOTE: Fix order of USs because the filter orderBy does not work propertly in partials files
for sprint in sprints
sprint.user_stories = _.sortBy(sprint.user_stories, "sprint_order")
@scope.closedSprints = sprints
@rootscope.$broadcast("closed-sprints:reloaded", sprints)
return sprints
loadSprints: ->
params = {closed: false}
return @rs.sprints.list(@scope.projectId, params).then (sprints) => return @rs.sprints.list(@scope.projectId, params).then (sprints) =>
# NOTE: Fix order of USs because the filter orderBy does not work propertly in partials files # NOTE: Fix order of USs because the filter orderBy does not work propertly in partials files
for sprint in sprints for sprint in sprints
@ -158,9 +170,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
@scope.sprints = sprints @scope.sprints = sprints
@scope.openSprints = _.filter(sprints, (sprint) => not sprint.closed).reverse() @scope.openSprints = _.filter(sprints, (sprint) => not sprint.closed).reverse()
@scope.closedSprints = _.filter(sprints, (sprint) => sprint.closed) @scope.closedSprints = [] if !@scope.closedSprints
if not @excludeClosedSprints
@scope.totalClosedMilestones = @scope.closedSprints.length
@scope.sprintsCounter = sprints.length @scope.sprintsCounter = sprints.length
@scope.sprintsById = groupBy(sprints, (x) -> x.id) @scope.sprintsById = groupBy(sprints, (x) -> x.id)
@ -194,8 +204,9 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
# NOTE: Fix order of USs because the filter orderBy does not work propertly in the partials files # NOTE: Fix order of USs because the filter orderBy does not work propertly in the partials files
@scope.userstories = _.sortBy(userstories, "backlog_order") @scope.userstories = _.sortBy(userstories, "backlog_order")
@.generateFilters() @.setSearchDataFilters()
@.filterVisibleUserstories() @.filterVisibleUserstories()
@.generateFilters()
@rootscope.$broadcast("filters:loaded", @scope.filters) @rootscope.$broadcast("filters:loaded", @scope.filters)
# The broadcast must be executed when the DOM has been fully reloaded. # The broadcast must be executed when the DOM has been fully reloaded.
@ -214,6 +225,9 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
loadProject: -> loadProject: ->
return @rs.projects.getBySlug(@params.pslug).then (project) => return @rs.projects.getBySlug(@params.pslug).then (project) =>
if not project.is_backlog_activated
@location.path(@navUrls.resolve("permission-denied"))
@scope.projectId = project.id @scope.projectId = project.id
@scope.project = project @scope.project = project
@scope.totalClosedMilestones = project.total_closed_milestones @scope.totalClosedMilestones = project.total_closed_milestones
@ -232,40 +246,20 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
return promise.then(=> @.loadBacklog()) return promise.then(=> @.loadBacklog())
toggleClosedSprintsVisualization: ->
@excludeClosedSprints = not @excludeClosedSprints
@.loadSprints()
filterVisibleUserstories: -> filterVisibleUserstories: ->
@scope.visibleUserstories = [] @scope.visibleUserstories = []
# Filter by tags # Filter by tags
selectedTags = _.filter(@scope.filters.tags, "selected")
selectedTags = _.map(selectedTags, "name")
if selectedTags.length == 0
@scope.visibleUserstories = _.clone(@scope.userstories, false)
else
@scope.visibleUserstories = _.reject @scope.userstories, (us) => @scope.visibleUserstories = _.reject @scope.userstories, (us) =>
if _.intersection(selectedTags, us.tags).length == 0 return _.some us.tags, (tag) =>
return true return @isFilterSelected("tag", tag)
return false
# Filter by status # Filter by status
selectedStatuses = _.filter(@scope.filters.statuses, "selected") @scope.visibleUserstories = _.filter @scope.visibleUserstories, (us) =>
selectedStatuses = _.map(selectedStatuses, "id") if @searchdata["statuses"] && Object.keys(@searchdata["statuses"]).length
return @isFilterSelected("statuses", taiga.toString(us.status))
if selectedStatuses.length > 0 return true
@scope.visibleUserstories = _.reject @scope.visibleUserstories, (us) =>
res = _.find(selectedStatuses, (x) -> x == taiga.toString(us.status))
return not res
@rs.userstories.storeQueryParams(@scope.projectId, {
"status": selectedStatuses,
"tags": selectedTags,
"project": @scope.projectId
"milestone": null
})
prepareBulkUpdateData: (uses, field="backlog_order") -> prepareBulkUpdateData: (uses, field="backlog_order") ->
return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]}) return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]})
@ -422,31 +416,33 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
return promise return promise
getUrlFilters: -> isFilterSelected: (type, id) ->
return _.pick(@location.search(), "statuses", "tags", "q") if @searchdata[type]? and @searchdata[type][id]
return true
return false
generateFilters: -> setSearchDataFilters: () ->
urlfilters = @.getUrlFilters() urlfilters = @.getUrlFilters()
if urlfilters.q if urlfilters.q
@scope.filtersQ = @scope.filtersQ or urlfilters.q @scope.filtersQ = @scope.filtersQ or urlfilters.q
searchdata = {} @searchdata = {}
for name, value of urlfilters for name, value of urlfilters
if not searchdata[name]? if not @searchdata[name]?
searchdata[name] = {} @searchdata[name] = {}
for val in taiga.toString(value).split(",") for val in taiga.toString(value).split(",")
searchdata[name][val] = true @searchdata[name][val] = true
isSelected = (type, id) -> getUrlFilters: ->
if searchdata[type]? and searchdata[type][id] return _.pick(@location.search(), "statuses", "tags", "q")
return true
return false
generateFilters: ->
@scope.filters = {} @scope.filters = {}
plainTags = _.flatten(_.filter(_.map(@scope.userstories, "tags"))) #tags
plainTags = _.flatten(_.filter(_.map(@scope.visibleUserstories, "tags")))
plainTags.sort() plainTags.sort()
@scope.filters.tags = _.map _.countBy(plainTags), (v, k) => @scope.filters.tags = _.map _.countBy(plainTags), (v, k) =>
@ -457,10 +453,14 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
color: @scope.project.tags_colors[k], color: @scope.project.tags_colors[k],
count: v count: v
} }
obj.selected = true if isSelected("tags", obj.id) obj.selected = true if @isFilterSelected("tags", obj.id)
return obj return obj
plainStatuses = _.map(@scope.userstories, "status") selectedTags = _.filter(@scope.filters.tags, "selected")
selectedTags = _.map(selectedTags, "name")
#status
plainStatuses = _.map(@scope.visibleUserstories, "status")
plainStatuses = _.filter plainStatuses, (status) => plainStatuses = _.filter plainStatuses, (status) =>
if status if status
@ -474,11 +474,20 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
color: @scope.usStatusById[k].color, color: @scope.usStatusById[k].color,
count:v count:v
} }
obj.selected = true if isSelected("statuses", obj.id) obj.selected = true if @isFilterSelected("statuses", obj.id)
return obj return obj
return @scope.filters selectedStatuses = _.filter(@scope.filters.statuses, "selected")
selectedStatuses = _.map(selectedStatuses, "id")
#store query params
@rs.userstories.storeQueryParams(@scope.projectId, {
"status": selectedStatuses,
"tags": selectedTags,
"project": @scope.projectId
"milestone": null
})
## Template actions ## Template actions
@ -514,7 +523,6 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
module.controller("BacklogController", BacklogController) module.controller("BacklogController", BacklogController)
############################################################################# #############################################################################
## Backlog Directive ## Backlog Directive
############################################################################# #############################################################################
@ -531,29 +539,27 @@ BacklogDirective = ($repo, $rootscope) ->
if $scope.stats? if $scope.stats?
removeDoomlineDom() removeDoomlineDom()
elements = getUsItems()
stats = $scope.stats stats = $scope.stats
total_points = stats.total_points total_points = stats.total_points
current_sum = stats.assigned_points current_sum = stats.assigned_points
for element in elements return if not $scope.visibleUserstories
scope = element.scope()
if not scope.us? for us, i in $scope.visibleUserstories
continue current_sum += us.total_points
current_sum += scope.us.total_points
if current_sum > total_points if current_sum > total_points
addDoomLineDom(element) domElement = $el.find('.backlog-table-body .us-item-row')[i]
addDoomLineDom(domElement)
break break
removeDoomlineDom = -> removeDoomlineDom = ->
$el.find(".doom-line").remove() $el.find(".doom-line").remove()
addDoomLineDom = (element) -> addDoomLineDom = (element) ->
element?.before(doomLineTemplate({})) $(element).before(doomLineTemplate({}))
getUsItems = -> getUsItems = ->
rowElements = $el.find('.backlog-table-body .us-item-row') rowElements = $el.find('.backlog-table-body .us-item-row')
@ -604,7 +610,8 @@ BacklogDirective = ($repo, $rootscope) ->
ussDom = $el.find(".backlog-table-body .user-stories input:checkbox:checked") ussDom = $el.find(".backlog-table-body .user-stories input:checkbox:checked")
ussToMove = _.map ussDom, (item) -> ussToMove = _.map ussDom, (item) ->
itemScope = angular.element(item).scope() item = $(item).closest('.tg-scope')
itemScope = item.scope()
itemScope.us.milestone = $scope.sprints[0].id itemScope.us.milestone = $scope.sprints[0].id
return itemScope.us return itemScope.us
@ -727,7 +734,6 @@ UsRolePointsSelectorDirective = ($rootscope, $template) ->
$el.on "click", ".role", (event) -> $el.on "click", ".role", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
rolScope = target.scope() rolScope = target.scope()
$rootscope.$broadcast("uspoints:select", target.data("role-id"), target.text()) $rootscope.$broadcast("uspoints:select", target.data("role-id"), target.text())
@ -740,170 +746,107 @@ UsRolePointsSelectorDirective = ($rootscope, $template) ->
module.directive("tgUsRolePointsSelector", ["$rootScope", "$tgTemplate", UsRolePointsSelectorDirective]) module.directive("tgUsRolePointsSelector", ["$rootScope", "$tgTemplate", UsRolePointsSelectorDirective])
UsPointsDirective = ($repo, $tgTemplate) -> UsPointsDirective = ($tgEstimationsService, $repo, $tgTemplate) ->
rolesTemplate = $tgTemplate.get("backlog/us-points-roles-popover.html", true) rolesTemplate = $tgTemplate.get("common/estimation/us-points-roles-popover.html", true)
pointsTemplate = $tgTemplate.get("backlog/us-points-popover.html", true)
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
$ctrl = $el.controller() $ctrl = $el.controller()
us = $scope.$eval($attrs.tgBacklogUsPoints)
updatingSelectedRoleId = null updatingSelectedRoleId = null
selectedRoleId = null selectedRoleId = null
numberOfRoles = _.size(us.points) filteringRoleId = null
estimationProcess = null
# Preselect the role if we have only one $scope.$on "uspoints:select", (ctx, roleId, roleName) ->
if numberOfRoles == 1 us = $scope.$eval($attrs.tgBacklogUsPoints)
selectedRoleId = _.keys(us.points)[0] selectedRoleId = roleId
estimationProcess.render()
roles = [] $scope.$on "uspoints:clear-selection", (ctx) ->
updatePointsRoles = -> us = $scope.$eval($attrs.tgBacklogUsPoints)
roles = _.map computableRoles, (role) -> selectedRoleId = null
pointId = us.points[role.id] estimationProcess.render()
pointObj = $scope.pointsById[pointId]
role = _.clone(role, true) $scope.$watch $attrs.tgBacklogUsPoints, (us) ->
role.points = if pointObj.value? then pointObj.value else "?" if us
return role estimationProcess = $tgEstimationsService.create($el, us, $scope.project)
computableRoles = _.filter($scope.project.roles, "computable")
updatePointsRoles()
# Update roles
roles = estimationProcess.calculateRoles()
if roles.length == 0 if roles.length == 0
$el.find(".icon-arrow-bottom").remove() $el.find(".icon-arrow-bottom").remove()
$el.find("a.us-points").addClass("not-clickable") $el.find("a.us-points").addClass("not-clickable")
renderPointsSelector = (us, roleId) -> else if roles.length == 1
# Prepare data for rendering # Preselect the role if we have only one
points = _.map $scope.project.points, (point) -> selectedRoleId = _.keys(us.points)[0]
point = _.clone(point, true)
point.selected = if us.points[roleId] == point.id then false else true
return point
html = pointsTemplate({"points": points}) if estimationProcess.isEditable
bindClickElements()
# Remove any prevous state estimationProcess.onSelectedPointForRole = (roleId, pointId) ->
$el.find(".popover").popover().close() @save(roleId, pointId).then ->
$el.find(".pop-points-open").remove() $ctrl.loadProjectStats()
# Render into DOM and show the new created element estimationProcess.render = () ->
$el.append(html) totalPoints = @calculateTotalPoints()
if not selectedRoleId? or roles.length == 1
text = totalPoints
title = totalPoints
else
pointId = @us.points[selectedRoleId]
pointObj = @pointsById[pointId]
text = "#{pointObj.name} / <span>#{totalPoints}</span>"
title = "#{pointObj.name} / #{totalPoints}"
# If not showing role selection let's move to the left ctx = {
if not $el.find(".pop-role:visible").css("left")? totalPoints: totalPoints
$el.find(".pop-points-open").css("left", "110px") roles: @calculateRoles()
editable: @isEditable
text: text
title: title
}
mainTemplate = "common/estimation/us-estimation-total.html"
template = $tgTemplate.get(mainTemplate, true)
html = template(ctx)
@$el.html(html)
$el.find(".pop-points-open").popover().open() estimationProcess.render()
renderRolesSelector = (us) ->
updatePointsRoles()
renderRolesSelector = () ->
roles = estimationProcess.calculateRoles()
html = rolesTemplate({"roles": roles}) html = rolesTemplate({"roles": roles})
# Render into DOM and show the new created element # Render into DOM and show the new created element
$el.append(html) $el.append(html)
$el.find(".pop-role").popover().open(() -> $(this).remove()) $el.find(".pop-role").popover().open(() -> $(this).remove())
renderPoints = (us, roleId) -> bindClickElements = () ->
dom = $el.find("a > span.points-value")
if roleId == null or numberOfRoles == 1
totalPoints = if us.total_points? then us.total_points else "?"
dom.text(totalPoints)
dom.parent().prop("title", totalPoints)
else
pointId = us.points[roleId]
pointObj = $scope.pointsById[pointId]
dom.html("#{pointObj.name} / <span>#{us.total_points}</span>")
dom.parent().prop("title", "#{pointObj.name} / #{us.total_points}")
calculateTotalPoints = ->
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)
$scope.$watch $attrs.tgBacklogUsPoints, (us) ->
renderPoints(us, selectedRoleId) if us
$scope.$on "uspoints:select", (ctx, roleId, roleName) ->
us = $scope.$eval($attrs.tgBacklogUsPoints)
renderPoints(us, roleId)
selectedRoleId = roleId
$scope.$on "uspoints:clear-selection", (ctx) ->
us = $scope.$eval($attrs.tgBacklogUsPoints)
renderPoints(us, null)
selectedRoleId = null
if roles.length > 0
$el.on "click", "a.us-points span", (event) -> $el.on "click", "a.us-points span", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
us = $scope.$eval($attrs.tgBacklogUsPoints) us = $scope.$eval($attrs.tgBacklogUsPoints)
updatingSelectedRoleId = selectedRoleId updatingSelectedRoleId = selectedRoleId
if selectedRoleId? if selectedRoleId?
renderPointsSelector(us, selectedRoleId) estimationProcess.renderPointsSelector(selectedRoleId)
else else
renderRolesSelector(us) renderRolesSelector()
$el.on "click", ".role", (event) -> $el.on "click", ".role", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
us = $scope.$eval($attrs.tgBacklogUsPoints) us = $scope.$eval($attrs.tgBacklogUsPoints)
updatingSelectedRoleId = target.data("role-id") updatingSelectedRoleId = target.data("role-id")
popRolesDom = $el.find(".pop-role") popRolesDom = $el.find(".pop-role")
popRolesDom.find("a").removeClass("active") popRolesDom.find("a").removeClass("active")
popRolesDom.find("a[data-role-id='#{updatingSelectedRoleId}']").addClass("active") popRolesDom.find("a[data-role-id='#{updatingSelectedRoleId}']").addClass("active")
estimationProcess.renderPointsSelector(updatingSelectedRoleId)
renderPointsSelector(us, updatingSelectedRoleId)
$el.on "click", ".point", (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
$el.find(".pop-points-open").hide()
$el.find(".pop-role").hide()
us = $scope.$eval($attrs.tgBacklogUsPoints)
points = _.clone(us.points, true)
points[updatingSelectedRoleId] = target.data("point-id")
$scope.$apply ->
us.points = points
us.total_points = calculateTotalPoints(us)
renderPoints(us, selectedRoleId)
$repo.save(us).then ->
# Little Hack for refresh.
$repo.refresh(us).then ->
$ctrl.loadProjectStats()
bindOnce $scope, "project", (project) ->
# If the user has not enough permissions the click events are unbinded
if project.my_permissions.indexOf("modify_us") == -1
$el.unbind("click")
$el.find("a").addClass("not-clickable")
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()
return {link: link} return {link: link}
module.directive("tgBacklogUsPoints", ["$tgRepo", "$tgTemplate", UsPointsDirective]) module.directive("tgBacklogUsPoints", ["$tgEstimationsService", "$tgRepo", "$tgTemplate", UsPointsDirective])
############################################################################# #############################################################################
## Burndown graph directive ## Burndown graph directive

View File

@ -58,6 +58,7 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm) ->
$el.sortable({ $el.sortable({
items: ".us-item-row", items: ".us-item-row",
cancel: ".popover"
connectWith: ".sprint" connectWith: ".sprint"
containment: ".wrapper" containment: ".wrapper"
dropOnEmpty: true dropOnEmpty: true

View File

@ -61,6 +61,8 @@ BacklogSprintDirective = ($repo, $rootscope) ->
# Event Handlers # Event Handlers
$el.on "click", ".sprint-name > .icon-arrow-up", (event) -> $el.on "click", ".sprint-name > .icon-arrow-up", (event) ->
event.preventDefault()
toggleSprint($el) toggleSprint($el)
$el.find(".sprint-table").slideToggle(slideOptions) $el.find(".sprint-table").slideToggle(slideOptions)
@ -135,26 +137,38 @@ module.directive("tgBacklogSprintHeader", ["$tgNavUrls", "$tgTemplate", BacklogS
############################################################################# #############################################################################
ToggleExcludeClosedSprintsVisualization = ($rootscope, $loading) -> ToggleExcludeClosedSprintsVisualization = ($rootscope, $loading) ->
excludeClosedSprints = false excludeClosedSprints = true
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
# insert loading wrapper
loadingElm = $("<div>")
$el.after(loadingElm)
# Event Handlers # Event Handlers
$el.on "click", "", (event) -> $el.on "click", (event) ->
$loading.start($el.parent().siblings('.loading-spinner')) event.preventDefault()
$rootscope.$broadcast("backlog:toggle-closed-sprints-visualization") excludeClosedSprints = not excludeClosedSprints
$loading.start(loadingElm)
if excludeClosedSprints
$rootscope.$broadcast("backlog:unload-closed-sprints")
else
$rootscope.$broadcast("backlog:load-closed-sprints")
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()
$scope.$on "sprints:loaded", (ctx, sprints) => $scope.$on "closed-sprints:reloaded", (ctx, sprints) =>
closedSprints = _.filter(sprints, (sprint) -> sprint.closed) $loading.finish(loadingElm)
$loading.finish($el.parent().siblings('.loading-spinner'))
#TODO: i18n #TODO: i18n
if closedSprints.length > 0 if sprints.length > 0
$el.text("Hide closed sprints") text = "Hide closed sprints"
else else
$el.text("Show closed sprints") text = "Show closed sprints"
$el.find(".text").text(text)
return {link: link} return {link: link}

View File

@ -84,13 +84,15 @@ urls = {
"project-admin-project-profile-default-values": "/project/:project/admin/project-profile/default-values" "project-admin-project-profile-default-values": "/project/:project/admin/project-profile/default-values"
"project-admin-project-profile-modules": "/project/:project/admin/project-profile/modules" "project-admin-project-profile-modules": "/project/:project/admin/project-profile/modules"
"project-admin-project-profile-export": "/project/:project/admin/project-profile/export" "project-admin-project-profile-export": "/project/:project/admin/project-profile/export"
"project-admin-project-values-us-status": "/project/:project/admin/project-values/us-status" "project-admin-project-profile-reports": "/project/:project/admin/project-profile/reports"
"project-admin-project-values-us-points": "/project/:project/admin/project-values/us-points"
"project-admin-project-values-task-status": "/project/:project/admin/project-values/task-status" "project-admin-project-values-status": "/project/:project/admin/project-values/status"
"project-admin-project-values-issue-status": "/project/:project/admin/project-values/issue-status" "project-admin-project-values-points": "/project/:project/admin/project-values/points"
"project-admin-project-values-issue-types": "/project/:project/admin/project-values/issue-types" "project-admin-project-values-priorities": "/project/:project/admin/project-values/priorities"
"project-admin-project-values-issue-priorities": "/project/:project/admin/project-values/issue-priorities" "project-admin-project-values-severities": "/project/:project/admin/project-values/severities"
"project-admin-project-values-issue-severities": "/project/:project/admin/project-values/issue-severities" "project-admin-project-values-types": "/project/:project/admin/project-values/types"
"project-admin-project-values-custom-fields": "/project/:project/admin/project-values/custom-fields"
"project-admin-memberships": "/project/:project/admin/memberships" "project-admin-memberships": "/project/:project/admin/memberships"
"project-admin-roles": "/project/:project/admin/roles" "project-admin-roles": "/project/:project/admin/roles"
"project-admin-third-parties-webhooks": "/project/:project/admin/third-parties/webhooks" "project-admin-third-parties-webhooks": "/project/:project/admin/third-parties/webhooks"

View File

@ -28,6 +28,10 @@ locationFactory = ($location, $route, $rootscope) ->
un() un()
return $location return $location
$location.isInCurrentRouteParams = (name, value) ->
return $route.current.params[name] == value
return $location return $location

View File

@ -49,6 +49,14 @@ class UrlsService extends taiga.Service
_.str.ltrim(url, "/") _.str.ltrim(url, "/")
]) ])
resolveAbsolute: ->
url = @.resolve.apply(@, arguments)
if (/^https?:\/\//i).test(url)
return url
if (/^\//).test(url)
return "#{window.location.protocol}//#{window.location.host}#{url}"
return "#{window.location.protocol}//#{window.location.host}/#{url}"
module = angular.module("taigaBase") module = angular.module("taigaBase")
module.service('$tgUrls', UrlsService) module.service('$tgUrls', UrlsService)

View File

@ -60,6 +60,42 @@ CheckPermissionDirective = ->
module.directive("tgCheckPermission", CheckPermissionDirective) module.directive("tgCheckPermission", CheckPermissionDirective)
#############################################################################
## Add class based on permissions
#############################################################################
ClassPermissionDirective = ->
name = "tgClassPermission"
link = ($scope, $el, $attrs) ->
checkPermissions = (project, className, permission) ->
negation = permission[0] == "!"
permission = permission.slice(1) if negation
if negation && project.my_permissions.indexOf(permission) == -1
$el.addClass(className)
else if !negation && project.my_permissions.indexOf(permission) != -1
$el.addClass(className)
else
$el.removeClass(className)
tgClassPermissionWatchAction = (project) ->
if project
unbindWatcher()
classes = $scope.$eval($attrs[name])
for className, permission of classes
checkPermissions(project, className, permission)
unbindWatcher = $scope.$watch "project", tgClassPermissionWatchAction
return {link:link}
module.directive("tgClassPermission", ClassPermissionDirective)
############################################################################# #############################################################################
## Animation frame service, apply css changes in the next render frame ## Animation frame service, apply css changes in the next render frame
############################################################################# #############################################################################

View File

@ -0,0 +1,15 @@
module = angular.module("taigaCommon")
BindScope = (config) ->
if !config.debugInfo
jQuery.fn.scope = () -> this.data('scope')
link = ($scope, $el) ->
if !config.debugInfo
$el
.data('scope', $scope)
.addClass('tg-scope')
return {link: link}
module.directive("tgBindScope", ["$tgConfig", BindScope])

View File

@ -536,7 +536,9 @@ EditableDescriptionDirective = ($rootscope, $repo, $confirm, $compile, $loading,
$el.find('.view-description').hide() $el.find('.view-description').hide()
$el.find('textarea').focus() $el.find('textarea').focus()
$el.on "click", ".save", -> $el.on "click", ".save", (e) ->
e.preventDefault()
description = $scope.item.description description = $scope.item.description
save(description) save(description)

View File

@ -0,0 +1,198 @@
###
# 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/custom-field-values.coffee
###
taiga = @.taiga
bindMethods = @.taiga.bindMethods
bindOnce = @.taiga.bindOnce
debounce = @.taiga.debounce
generateHash = taiga.generateHash
module = angular.module("taigaCommon")
class CustomAttributesValuesController extends taiga.Controller
@.$inject = ["$scope", "$rootScope", "$tgRepo", "$tgResources", "$tgConfirm", "$q"]
constructor: (@scope, @rootscope, @repo, @rs, @confirm, @q) ->
bindMethods(@)
@.type = null
@.objectId = null
@.projectId = null
@.customAttributes = []
@.customAttributesValues = null
initialize: (type, objectId) ->
@.project = @scope.project
@.type = type
@.objectId = objectId
@.projectId = @scope.projectId
loadCustomAttributesValues: ->
return @.customAttributesValues if not @.objectId
return @rs.customAttributesValues[@.type].get(@.objectId).then (customAttributesValues) =>
@.customAttributes = @.project["#{@.type}_custom_attributes"]
@.customAttributesValues = customAttributesValues
return customAttributesValues
getAttributeValue: (attribute) ->
attributeValue = _.clone(attribute, false)
attributeValue.value = @.customAttributesValues.attributes_values[attribute.id]
return attributeValue
updateAttributeValue: (attributeValue) ->
onSuccess = =>
@rootscope.$broadcast("custom-attributes-values:edit")
onError = (response) =>
@confirm.notify("error")
return @q.reject()
# We need to update the full array so angular understand the model is modified
attributesValues = _.clone(@.customAttributesValues.attributes_values, true)
attributesValues[attributeValue.id] = attributeValue.value
@.customAttributesValues.attributes_values = attributesValues
@.customAttributesValues.id = @.objectId
return @repo.save(@.customAttributesValues).then(onSuccess, onError)
CustomAttributesValuesDirective = ($templates, $storage) ->
template = $templates.get("custom-attributes/custom-attributes-values.html", true)
collapsedHash = (type) ->
return generateHash(["custom-attributes-collapsed", type])
link = ($scope, $el, $attrs, $ctrls) ->
$ctrl = $ctrls[0]
$model = $ctrls[1]
bindOnce $scope, $attrs.ngModel, (value) ->
$ctrl.initialize($attrs.type, value.id)
$ctrl.loadCustomAttributesValues()
$el.on "click", ".custom-fields-header a", ->
hash = collapsedHash($attrs.type)
collapsed = not($storage.get(hash) or false)
$storage.set(hash, collapsed)
if collapsed
$el.find(".custom-fields-header a").removeClass("open")
$el.find(".custom-fields-body").removeClass("open")
else
$el.find(".custom-fields-header a").addClass("open")
$el.find(".custom-fields-body").addClass("open")
$scope.$on "$destroy", ->
$el.off()
templateFn = ($el, $attrs) ->
collapsed = $storage.get(collapsedHash($attrs.type)) or false
return template({
requiredEditionPerm: $attrs.requiredEditionPerm
collapsed: collapsed
})
return {
require: ["tgCustomAttributesValues", "ngModel"]
controller: CustomAttributesValuesController
controllerAs: "ctrl"
restrict: "AE"
scope: true
link: link
template: templateFn
}
module.directive("tgCustomAttributesValues", ["$tgTemplate", "$tgStorage", CustomAttributesValuesDirective])
CustomAttributeValueDirective = ($template, $selectedText) ->
template = $template.get("custom-attributes/custom-attribute-value.html", true)
templateEdit = $template.get("custom-attributes/custom-attribute-value-edit.html", true)
link = ($scope, $el, $attrs, $ctrl) ->
render = (attributeValue, edit=false) ->
value = attributeValue.value
editable = isEditable()
ctx = {
id: attributeValue.id
name: attributeValue.name
description: attributeValue.description
value: value
isEditable: editable
}
if editable and (edit or not value)
html = templateEdit(ctx)
else
html = template(ctx)
$el.html(html)
isEditable = ->
permissions = $scope.project.my_permissions
requiredEditionPerm = $attrs.requiredEditionPerm
return permissions.indexOf(requiredEditionPerm) > -1
saveAttributeValue = ->
attributeValue.value = $el.find("input").val()
$scope.$apply ->
$ctrl.updateAttributeValue(attributeValue).then ->
render(attributeValue, false)
$el.on "keyup", "input[name=description]", (event) ->
if event.keyCode == 13
submit(event)
else if event.keyCode == 27
render(attributeValue, false)
## Actions (on view mode)
$el.on "click", ".custom-field-value.read-mode", ->
return if not isEditable()
return if $selectedText.get().length
render(attributeValue, true)
$el.find("input[name='description']").focus().select()
$el.on "click", "a.icon-edit", (event) ->
event.preventDefault()
render(attributeValue, true)
$el.find("input[name='description']").focus().select()
## Actions (on edit mode)
submit = debounce 2000, (event) =>
event.preventDefault()
saveAttributeValue()
$el.on "submit", "form", submit
$el.on "click", "a.icon-floppy", submit
$scope.$on "$destroy", ->
$el.off()
# Bootstrap
attributeValue = $scope.$eval($attrs.tgCustomAttributeValue)
render(attributeValue)
return {
link: link
require: "^tgCustomAttributesValues"
restrict: "AE"
}
module.directive("tgCustomAttributeValue", ["$tgTemplate", "$selectedText", CustomAttributeValueDirective])

View File

@ -20,6 +20,7 @@
### ###
taiga = @.taiga taiga = @.taiga
groupBy = @.taiga.groupBy
module = angular.module("taigaCommon") module = angular.module("taigaCommon")
@ -27,7 +28,53 @@ module = angular.module("taigaCommon")
## User story estimation directive (for Lightboxes) ## User story estimation directive (for Lightboxes)
############################################################################# #############################################################################
LbUsEstimationDirective = ($rootScope, $repo, $confirm, $template) -> LbUsEstimationDirective = ($tgEstimationsService, $rootScope, $repo, $confirm, $template) ->
# Display the points of a US and you can edit it.
#
# Example:
# tg-lb-us-estimation-progress-bar(ng-model="us")
#
# Requirements:
# - Us object (ng-model)
# - scope.project object
link = ($scope, $el, $attrs, $model) ->
$scope.$watch $attrs.ngModel, (us) ->
if us
estimationProcess = $tgEstimationsService.create($el, us, $scope.project)
estimationProcess.onSelectedPointForRole = (roleId, pointId) ->
$scope.$apply ->
$model.$setViewValue(us)
estimationProcess.render = () ->
ctx = {
totalPoints: @calculateTotalPoints()
roles: @calculateRoles()
editable: @isEditable
}
mainTemplate = "common/estimation/us-estimation-points-per-role.html"
template = $template.get(mainTemplate, true)
html = template(ctx)
@$el.html(html)
estimationProcess.render()
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgLbUsEstimation", ["$tgEstimationsService", "$rootScope", "$tgRepo", "$tgConfirm", "$tgTemplate", LbUsEstimationDirective])
#############################################################################
## User story estimation directive
#############################################################################
UsEstimationDirective = ($tgEstimationsService, $rootScope, $repo, $confirm, $qqueue, $template) ->
# Display the points of a US and you can edit it. # Display the points of a US and you can edit it.
# #
# Example: # Example:
@ -37,90 +84,26 @@ LbUsEstimationDirective = ($rootScope, $repo, $confirm, $template) ->
# - Us object (ng-model) # - Us object (ng-model)
# - scope.project object # - scope.project object
mainTemplate = $template.get("common/estimation/lb-us-estimation-points-per-role.html", true)
pointsTemplate = $template.get("common/estimation/lb-us-estimation-points.html", true)
link = ($scope, $el, $attrs, $model) -> link = ($scope, $el, $attrs, $model) ->
render = (points) -> $scope.$watch $attrs.ngModel, (us) ->
totalPoints = calculateTotalPoints(points) or 0 if us
computableRoles = _.filter($scope.project.roles, "computable") estimationProcess = $tgEstimationsService.create($el, us, $scope.project)
estimationProcess.onSelectedPointForRole = (roleId, pointId) ->
roles = _.map computableRoles, (role) -> @save(roleId, pointId).then ->
pointId = points[role.id] $rootScope.$broadcast("history:reload")
pointObj = $scope.pointsById[pointId]
role = _.clone(role, true)
role.points = if pointObj? and pointObj.name? then pointObj.name else "?"
return role
estimationProcess.render = () ->
ctx = { ctx = {
totalPoints: totalPoints totalPoints: @calculateTotalPoints()
roles: roles roles: @calculateRoles()
editable: @isEditable
} }
html = mainTemplate(ctx) mainTemplate = "common/estimation/us-estimation-points-per-role.html"
$el.html(html) template = $template.get(mainTemplate, true)
html = template(ctx)
@$el.html(html)
renderPoints = (target, usPoints, roleId) -> estimationProcess.render()
points = _.map $scope.project.points, (point) ->
point = _.clone(point, true)
point.selected = if usPoints[roleId] == point.id then false else true
return point
html = pointsTemplate({"points": points, roleId: roleId})
# Remove any prevous state
$el.find(".popover").popover().close()
$el.find(".pop-points-open").remove()
# If not showing role selection let's move to the left
if not $el.find(".pop-role:visible").css("left")?
$el.find(".pop-points-open").css("left", "110px")
$el.find(".pop-points-open").remove()
# Render into DOM and show the new created element
$el.find(target).append(html)
$el.find(".pop-points-open").popover().open(-> $(this).removeClass("active"))
$el.find(".pop-points-open").show()
calculateTotalPoints = (points) ->
values = _.map(points, (v, k) -> $scope.pointsById[v]?.value or 0)
if values.length == 0
return "0"
return _.reduce(values, (acc, num) -> acc + num)
$el.on "click", ".total.clickable", (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
roleId = target.data("role-id")
points = $model.$modelValue
renderPoints(target, points, roleId)
target.siblings().removeClass('active')
target.addClass('active')
$el.on "click", ".point", (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
roleId = target.data("role-id")
pointId = target.data("point-id")
$el.find(".popover").popover().close()
points = _.clone($model.$modelValue, true)
points[roleId] = pointId
$scope.$apply ->
$model.$setViewValue(points)
$scope.$watch $attrs.ngModel, (points) ->
render(points) if points
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()
@ -131,81 +114,46 @@ LbUsEstimationDirective = ($rootScope, $repo, $confirm, $template) ->
require: "ngModel" require: "ngModel"
} }
module.directive("tgLbUsEstimation", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgTemplate", LbUsEstimationDirective]) module.directive("tgUsEstimation", ["$tgEstimationsService", "$rootScope", "$tgRepo", "$tgConfirm", "$tgQqueue", "$tgTemplate",
UsEstimationDirective])
############################################################################# #############################################################################
## User story estimation directive ## Estimations service
############################################################################# #############################################################################
UsEstimationDirective = ($rootScope, $repo, $confirm, $qqueue, $template) -> EstimationsService = ($template, $qqueue, $repo, $confirm, $q) ->
# 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
mainTemplate = $template.get("common/estimation/us-estimation-points-per-role.html", true)
pointsTemplate = $template.get("common/estimation/us-estimation-points.html", true) pointsTemplate = $template.get("common/estimation/us-estimation-points.html", true)
link = ($scope, $el, $attrs, $model) -> class EstimationProcess
isEditable = -> constructor: (@$el, @us, @project) ->
return $scope.project.my_permissions.indexOf("modify_us") != -1 @isEditable = @project.my_permissions.indexOf("modify_us") != -1
@roles = @project.roles
@points = @project.points
@pointsById = groupBy(@points, (x) -> x.id)
@onSelectedPointForRole = (roleId, pointId) ->
@render = () ->
render = (us) -> save: (roleId, pointId) ->
totalPoints = if us.total_points? then us.total_points else "?" deferred = $q.defer()
computableRoles = _.filter($scope.project.roles, "computable") $qqueue.add () =>
onSuccess = =>
deferred.resolve()
$confirm.notify("success")
roles = _.map computableRoles, (role) -> onError = =>
pointId = us.points[role.id] $confirm.notify("error")
pointObj = $scope.pointsById[pointId] @us.revert()
@render()
deferred.reject()
role = _.clone(role, true) $repo.save(@us).then(onSuccess, onError)
role.points = if pointObj? and pointObj.name? then pointObj.name else "?"
return role
ctx = { return deferred.promise
totalPoints: totalPoints
roles: roles
editable: isEditable()
}
html = mainTemplate(ctx)
$el.html(html)
renderPoints = (target, us, roleId) -> calculateTotalPoints: () ->
points = _.map $scope.project.points, (point) -> values = _.map(@us.points, (v, k) => @pointsById[v]?.value)
point = _.clone(point, true)
point.selected = if us.points[roleId] == point.id then false else true
return point
html = pointsTemplate({"points": points, roleId: roleId})
# Remove any prevous state
$el.find(".popover").popover().close()
$el.find(".pop-points-open").remove()
# If not showing role selection let's move to the left
if not $el.find(".pop-role:visible").css("left")?
$el.find(".pop-points-open").css("left", "110px")
$el.find(".pop-points-open").remove()
# Render into DOM and show the new created element
$el.find(target).append(html)
$el.find(".pop-points-open").popover().open ->
$(this)
.removeClass("active")
.closest("li").removeClass("active")
$el.find(".pop-points-open").show()
calculateTotalPoints = (us) ->
values = _.map(us.points, (v, k) -> $scope.pointsById[v]?.value)
if values.length == 0 if values.length == 0
return "0" return "0"
@ -215,64 +163,82 @@ UsEstimationDirective = ($rootScope, $repo, $confirm, $qqueue, $template) ->
return _.reduce(notNullValues, (acc, num) -> acc + num) return _.reduce(notNullValues, (acc, num) -> acc + num)
save = $qqueue.bindAdd (roleId, pointId) => calculateRoles: () ->
$el.find(".popover").popover().close() computableRoles = _.filter(@project.roles, "computable")
roles = _.map computableRoles, (role) =>
pointId = @us.points[role.id]
pointObj = @pointsById[pointId]
role = _.clone(role, true)
role.points = if pointObj? and pointObj.name? then pointObj.name else "?"
return role
# Hell starts here return roles
us = angular.copy($model.$modelValue)
points = _.clone($model.$modelValue.points, true)
points[roleId] = pointId
us.setAttr('points', points)
us.points = points
us.total_points = calculateTotalPoints(us)
$model.$setViewValue(us)
# Hell ends here
onSuccess = -> bindClickEvents: =>
$confirm.notify("success") @$el.on "click", ".total.clickable", (event) =>
$rootScope.$broadcast("history:reload")
onError = ->
$confirm.notify("error")
us.revert()
$model.$setViewValue(us)
$repo.save($model.$modelValue).then(onSuccess, onError)
$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")
@renderPointsSelector(roleId, target)
us = $model.$modelValue
renderPoints(target, us, roleId)
target.siblings().removeClass('active') target.siblings().removeClass('active')
target.addClass('active') target.addClass('active')
$el.on "click", ".point", (event) -> @$el.on "click", ".point", (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")
pointId = target.data("point-id") pointId = target.data("point-id")
@$el.find(".popover").popover().close()
points = _.clone(@us.points, true)
points[roleId] = pointId
@us.points = points
@render()
@onSelectedPointForRole(roleId, pointId)
save(roleId, pointId) renderPointsSelector: (roleId, target) ->
points = _.map @points, (point) =>
point = _.clone(point, true)
point.selected = if @us.points[roleId] == point.id then false else true
return point
$scope.$watch $attrs.ngModel, (us) -> maxPointLength = 5
render(us) if us horizontalList = _.some points, (point) => point.name.length > maxPointLength
$scope.$on "$destroy", -> html = pointsTemplate({"points": points, roleId: roleId, horizontal: horizontalList})
$el.off() # Remove any previous state
@$el.find(".popover").popover().close()
@$el.find(".pop-points-open").remove()
# Render into DOM and show the new created element
if target?
@$el.find(target).append(html)
else
@$el.append(html)
@$el.find(".pop-points-open").popover().open ->
$(this)
.removeClass("active")
.closest("li").removeClass("active")
@$el.find(".pop-points-open").show()
create = ($el, us, project) ->
estimationProcess = $el.data("estimationProcess")
if !estimationProcess
estimationProcess = new EstimationProcess($el, us, project)
$el.data("estimationProcess", estimationProcess)
if estimationProcess.isEditable
estimationProcess.bindClickEvents()
else
$el.unbind("click")
return estimationProcess
return { return {
link: link create: create
restrict: "EA"
require: "ngModel"
} }
module.directive("tgUsEstimation", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgQqueue", "$tgTemplate", UsEstimationDirective]) module.factory("$tgEstimationsService", ["$tgTemplate", "$tgQqueue", "$tgRepo", "$tgConfirm", "$q", EstimationsService])

View File

@ -51,6 +51,13 @@ class HistoryController extends taiga.Controller
delete historyResult.values_diff.description_html delete historyResult.values_diff.description_html
delete historyResult.values_diff.description_diff delete historyResult.values_diff.description_diff
# If block note was modified take only the blocked_note_html field
if historyResult.values_diff.blocked_note_diff?
historyResult.values_diff.blocked_note = historyResult.values_diff.blocked_note_diff
delete historyResult.values_diff.blocked_note_html
delete historyResult.values_diff.blocked_note_diff
@scope.history = history @scope.history = history
@scope.comments = _.filter(history, (item) -> item.comment != "") @scope.comments = _.filter(history, (item) -> item.comment != "")
@ -66,6 +73,7 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm) ->
templateChangePoints = $template.get("common/history/history-change-points.html", true) templateChangePoints = $template.get("common/history/history-change-points.html", true)
templateChangeGeneric = $template.get("common/history/history-change-generic.html", true) templateChangeGeneric = $template.get("common/history/history-change-generic.html", true)
templateChangeAttachment = $template.get("common/history/history-change-attachment.html", true) templateChangeAttachment = $template.get("common/history/history-change-attachment.html", true)
templateChangeList = $template.get("common/history/history-change-list.html", true)
templateDeletedComment = $template.get("common/history/history-deleted-comment.html", true) templateDeletedComment = $template.get("common/history/history-deleted-comment.html", true)
templateActivity = $template.get("common/history/history-activity.html", true) templateActivity = $template.get("common/history/history-activity.html", true)
templateBaseEntries = $template.get("common/history/history-base-entries.html", true) templateBaseEntries = $template.get("common/history/history-base-entries.html", true)
@ -103,6 +111,9 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm) ->
# Attachment # Attachment
is_deprecated: "is deprecated" is_deprecated: "is deprecated"
blocked_note: "blocked note"
is_blocked: "is blocked"
} # TODO i18n } # TODO i18n
return humanizedFieldNames[field] or field return humanizedFieldNames[field] or field
@ -121,18 +132,18 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm) ->
formatChange = (change) -> formatChange = (change) ->
if _.isArray(change) if _.isArray(change)
if change.length == 0 if change.length == 0
return "nil" return "empty"
return change.join(", ") return change.join(", ")
if change == "" if change == ""
return "nil" return "empty"
if not change? or change == false
return "no"
if change == true if change == true
return "yes" return "yes"
if change == false
return "no"
return change return change
# Render into string (operations without mutability) # Render into string (operations without mutability)
@ -162,14 +173,50 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm) ->
return _.flatten(attachments).join("\n") return _.flatten(attachments).join("\n")
renderCustomAttributesEntry = (value) ->
customAttributes = _.map value, (changes, type) ->
if type == "new"
return _.map changes, (change) ->
return templateChangeGeneric({
name: change.name,
from: formatChange(""),
to: formatChange(change.value)
})
else if type == "deleted"
return _.map changes, (change) ->
# TODO: i18n
return templateChangeDiff({
name: "deleted custom attribute",
diff: change.name
})
else
return _.map changes, (change) ->
customAttrsChanges = _.map change.changes, (values) ->
return templateChangeGeneric({
name: change.name
from: formatChange(values[0])
to: formatChange(values[1])
})
return _.flatten(customAttrsChanges).join("\n")
return _.flatten(customAttributes).join("\n")
renderChangeEntry = (field, value) -> renderChangeEntry = (field, value) ->
if field == "description" if field == "description"
# TODO: i18n return templateChangeDiff({name: getHumanizedFieldName("description"), diff: value[1]})
return templateChangeDiff({name: "description", diff: value[1]}) else if field == "blocked_note"
return templateChangeDiff({name: getHumanizedFieldName("blocked_note"), diff: value[1]})
else if field == "points" else if field == "points"
return templateChangePoints({points: value}) return templateChangePoints({points: value})
else if field == "attachments" else if field == "attachments"
return renderAttachmentEntry(value) return renderAttachmentEntry(value)
else if field == "custom_attributes"
return renderCustomAttributesEntry(value)
else if field in ["tags", "watchers"]
name = getHumanizedFieldName(field)
removed = _.difference(value[0], value[1])
added = _.difference(value[1], value[0])
return templateChangeList({name:name, removed:removed, added: added})
else if field == "assigned_to" else if field == "assigned_to"
name = getHumanizedFieldName(field) name = getHumanizedFieldName(field)
from = formatChange(value[0] or "Unassigned") from = formatChange(value[0] or "Unassigned")
@ -211,7 +258,7 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm) ->
deleteCommentDate: moment(comment.delete_comment_date).format("DD MMM YYYY HH:mm") if comment.delete_comment_date deleteCommentDate: moment(comment.delete_comment_date).format("DD MMM YYYY HH:mm") if comment.delete_comment_date
deleteCommentUser: comment.delete_comment_user.name if comment.delete_comment_user?.name deleteCommentUser: comment.delete_comment_user.name if comment.delete_comment_user?.name
activityId: comment.id activityId: comment.id
canDeleteComment: comment.user.pk == $scope.user.id or $scope.project.my_permissions.indexOf("modify_project") > -1 canDeleteComment: comment.user.pk == $scope.user?.id or $scope.project.my_permissions.indexOf("modify_project") > -1
}) })
renderChange = (change) -> renderChange = (change) ->

View File

@ -45,6 +45,8 @@ class LightboxService extends taiga.Service
@animationFrame.add => @animationFrame.add =>
$el.addClass("open") $el.addClass("open")
@animationFrame.add =>
lightboxContent.show() lightboxContent.show()
defered.resolve() defered.resolve()
@ -334,7 +336,6 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
submitButton = $el.find(".submit-button") submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$el.on "click", ".close", (event) -> $el.on "click", ".close", (event) ->
event.preventDefault() event.preventDefault()
@ -403,7 +404,6 @@ CreateBulkUserstoriesDirective = ($repo, $rs, $rootscope, lightboxService, $load
submitButton = $el.find(".submit-button") submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()
@ -604,41 +604,3 @@ WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationS
} }
module.directive("tgLbWatchers", ["$tgRepo", "lightboxService", "lightboxKeyboardNavigationService", "$tgTemplate", WatchersLightboxDirective]) module.directive("tgLbWatchers", ["$tgRepo", "lightboxService", "lightboxKeyboardNavigationService", "$tgTemplate", WatchersLightboxDirective])
#############################################################################
## Notion Lightbox Directive
#############################################################################
# Lightbox
NotionLightboxDirective = (lightboxService) ->
link = ($scope, $el, $attrs, $model) ->
$scope.$on "notion:open", (event, lightboxId) ->
if $el.attr("id") == lightboxId
lightboxService.open($el)
$el.on "click", ".button-green", (event) ->
lightboxService.close($el)
$scope.$on "$destroy", ->
$el.off()
return {link:link}
module.directive("tgLbNotion", ["lightboxService", NotionLightboxDirective])
# Button
NotionButtonDirective = ($log, $rootScope) ->
link = ($scope, $el, $attrs, $model) ->
if not $attrs.tgLbNotionButton?
return $log.error "NotionButtonDirective: the directive need the id of the notion lightbox"
$el.on "click", ->
$rootScope.$broadcast("notion:open", $attrs.tgLbNotionButton)
$scope.$on "$destroy", ->
$el.off()
return {link:link}
module.directive("tgLbNotionButton", ["$log", "$rootScope", NotionButtonDirective])

View File

@ -24,6 +24,33 @@ bindOnce = @.taiga.bindOnce
module = angular.module("taigaCommon") module = angular.module("taigaCommon")
# How to test lists (-, *, 1.)
# test it with text after & before the list
# + is the cursor position
# CASE 1
# - aa+
# --> enter
# - aa
# - +
# CASE 1
# - +
# --> enter
# +
# CASE 3
# - bb+cc
# --> enter
# - bb
# - cc
# CASE 3
# +- aa
# --> enter
# - aa
############################################################################# #############################################################################
## WYSIWYG markitup editor directive ## WYSIWYG markitup editor directive
@ -64,35 +91,52 @@ tgMarkitupDirective = ($rootscope, $rs, $tr, $selectedText, $template) ->
markdown.off(".preview") markdown.off(".preview")
closePreviewMode() closePreviewMode()
markdownCaretPositon = false setCaretPosition = (textarea, caretPosition) ->
if textarea.createTextRange
setCaretPosition = (elm, caretPos) -> range = textarea.createTextRange()
if elm.createTextRange range.move("character", caretPosition)
range = elm.createTextRange()
range.move("character", caretPos)
range.select() range.select()
else if elm.selectionStart else if textarea.selectionStart
elm.focus() textarea.focus()
elm.setSelectionRange(caretPos, caretPos) textarea.setSelectionRange(caretPosition, caretPosition)
removeEmptyLine = (textarea, line, currentCaretPosition) -> # Calculate the scroll position
totalLines = textarea.value.split("\n").length
line = textarea.value[0..(caretPosition - 1)].split("\n").length
scrollRelation = line / totalLines
$el.scrollTop((scrollRelation * $el[0].scrollHeight) - ($el.height() / 2))
addLine = (textarea, nline, replace) ->
lines = textarea.value.split("\n") lines = textarea.value.split("\n")
removedLineLength = lines[line].length
lines[line] = "" if replace
lines[nline] = replace + lines[nline]
else
lines[nline] = ""
cursorPosition = 0
for line, key in lines
cursorPosition += line.length + 1 || 1
break if key == nline
textarea.value = lines.join("\n") textarea.value = lines.join("\n")
#return the new position #return the new position
return currentCaretPosition - removedLineLength + 1 if replace
return cursorPosition - lines[nline].length + replace.length - 1
else
return cursorPosition
markdownSettings = markdownSettings =
nameSpace: "markdown" nameSpace: "markdown"
onShiftEnter: {keepDefault:false, openWith:"\n\n"} onShiftEnter: {keepDefault:false, openWith:"\n\n"}
onEnter: onEnter:
keepDefault: false keepDefault: false,
replaceWith: (data) => replaceWith: () -> "\n"
afterInsert: (data) ->
lines = data.textarea.value.split("\n") lines = data.textarea.value.split("\n")
cursorLine = data.textarea.value[0..(data.caretPosition - 1)].split("\n").length cursorLine = data.textarea.value[0..(data.caretPosition - 1)].split("\n").length
newLineContent = data.textarea.value[data.caretPosition..].split("\n")[0] newLineContent = data.textarea.value[data.caretPosition..].split("\n")[0]
@ -105,12 +149,9 @@ tgMarkitupDirective = ($rootscope, $rs, $tr, $selectedText, $template) ->
emptyListItem = lastLine.match /^(\s*)\-\s$/ emptyListItem = lastLine.match /^(\s*)\-\s$/
if emptyListItem if emptyListItem
markdownCaretPositon = removeEmptyLine(data.textarea, lines.length - 1, data.caretPosition) markdownCaretPositon = addLine(data.textarea, cursorLine - 1)
else else
breakLineAtBeginning = newLineContent.match /^(\s*)\-\s/ markdownCaretPositon = addLine(data.textarea, cursorLine, "#{match[1]}")
if !breakLineAtBeginning
return "\n#{match[1]}" if match
# unordered list * # unordered list *
match = lastLine.match /^(\s*\* ).*/ match = lastLine.match /^(\s*\* ).*/
@ -119,12 +160,9 @@ tgMarkitupDirective = ($rootscope, $rs, $tr, $selectedText, $template) ->
emptyListItem = lastLine.match /^(\s*\* )$/ emptyListItem = lastLine.match /^(\s*\* )$/
if emptyListItem if emptyListItem
markdownCaretPositon = removeEmptyLine(data.textarea, lines.length - 1, data.caretPosition) markdownCaretPositon = addLine(data.textarea, cursorLine - 1)
else else
breakLineAtBeginning = newLineContent.match /^(\s*)\*\s/ markdownCaretPositon = addLine(data.textarea, cursorLine, "#{match[1]}")
if !breakLineAtBeginning
return "\n#{match[1]}" if match
# ordered list # ordered list
match = lastLine.match /^(\s*)(\d+)\.\s/ match = lastLine.match /^(\s*)(\d+)\.\s/
@ -133,29 +171,12 @@ tgMarkitupDirective = ($rootscope, $rs, $tr, $selectedText, $template) ->
emptyListItem = lastLine.match /^(\s*)(\d+)\.\s$/ emptyListItem = lastLine.match /^(\s*)(\d+)\.\s$/
if emptyListItem if emptyListItem
markdownCaretPositon = removeEmptyLine(data.textarea, lines.length - 1, data.caretPosition) markdownCaretPositon = addLine(data.textarea, cursorLine - 1)
else else
breakLineAtBeginning = newLineContent.match /^(\s*)(\d+)\.\s/ markdownCaretPositon = addLine(data.textarea, cursorLine, "#{match[1] + (parseInt(match[2], 10) + 1)}. ")
if !breakLineAtBeginning
return "\n#{match[1] + (parseInt(match[2], 10) + 1)}. "
return "\n" setCaretPosition(data.textarea, markdownCaretPositon) if markdownCaretPositon
afterInsert: (data) ->
# Calculate the scroll position
if markdownCaretPositon
setCaretPosition(data.textarea, markdownCaretPositon)
caretPosition = markdownCaretPositon
markdownCaretPositon = false
else
caretPosition = data.caretPosition
totalLines = data.textarea.value.split("\n").length
line = data.textarea.value[0..(caretPosition - 1)].split("\n").length
scrollRelation = line / totalLines
$el.scrollTop((scrollRelation * $el[0].scrollHeight) - ($el.height() / 2))
markupSet: [ markupSet: [
{ {
@ -214,14 +235,18 @@ tgMarkitupDirective = ($rootscope, $rs, $tr, $selectedText, $template) ->
{ {
name: $tr.t("markdown-editor.picture") name: $tr.t("markdown-editor.picture")
key: "P" key: "P"
replaceWith: '![[![Alternative text]!]]([![Url:!:http://]!] "[![Title]!]")' replaceWith: '![[![Alternative text]!]](<<<[![Url:!:http://]!]>>> "[![Title]!]")'
beforeInsert:(markItUp) -> prepareUrlFormatting(markItUp)
afterInsert:(markItUp) -> urlFormatting(markItUp)
}, },
{ {
name: $tr.t("markdown-editor.link") name: $tr.t("markdown-editor.link")
key: "L" key: "L"
openWith: "[" openWith: "["
closeWith: ']([![Url:!:http://]!] "[![Title]!]")' closeWith: '](<<<[![Url:!:http://]!]>>> "[![Title]!]")'
placeHolder: $tr.t("markdown-editor.link-placeholder") placeHolder: $tr.t("markdown-editor.link-placeholder")
beforeInsert:(markItUp) -> prepareUrlFormatting(markItUp)
afterInsert:(markItUp) -> urlFormatting(markItUp)
}, },
{ {
separator: "---------------" separator: "---------------"
@ -256,6 +281,45 @@ tgMarkitupDirective = ($rootscope, $rs, $tr, $selectedText, $template) ->
target = angular.element(event.textarea) target = angular.element(event.textarea)
$model.$setViewValue(target.val()) $model.$setViewValue(target.val())
prepareUrlFormatting = (markItUp) ->
console.log(markItUp)
regex = /(<<<|>>>)/gi
result = 0
indices = []
(indices.push(result.index)) while ( (result = regex.exec(markItUp.textarea.value)) )
markItUp.donotparse = indices
console.log(indices)
urlFormatting = (markItUp) ->
console.log(markItUp.donotparse)
regex = /<<</gi
result = 0
startIndex = 0
loop
result = regex.exec(markItUp.textarea.value)
break if !result
if result.index not in markItUp.donotparse
startIndex = result.index
break
regex = />>>/gi
endIndex = 0
loop
result = regex.exec(markItUp.textarea.value)
break if !result
if result.index not in markItUp.donotparse
endIndex = result.index
break
value = markItUp.textarea.value
url = value.substring(startIndex, endIndex).replace('<<<', '').replace('>>>', '')
url = url.replace('(', '%28').replace(')', '%29')
url = url.replace('[', '%5B').replace(']', '%5D')
value = value.substring(0, startIndex) + url + value.substring(endIndex+3, value.length)
markItUp.textarea.value = value
markItUp.donotparse = undefined
markdownTitle = (markItUp, char) -> markdownTitle = (markItUp, char) ->
heading = "" heading = ""
n = $.trim(markItUp.selection or markItUp.placeHolder).length n = $.trim(markItUp.selection or markItUp.placeHolder).length

View File

@ -62,7 +62,7 @@ taiga.PageMixin = PageMixin
############################################################################# #############################################################################
## Filters Mixin ## Filters Mixin
############################################################################# #############################################################################
# This mixin requires @location ($tgLocation) and @scope # This mixin requires @location ($tgLocation), and @scope
class FiltersMixin class FiltersMixin
selectFilter: (name, value, load=false) -> selectFilter: (name, value, load=false) ->
@ -73,10 +73,12 @@ class FiltersMixin
existing = _.compact(existing) existing = _.compact(existing)
value = joinStr(",", _.uniq(existing)) value = joinStr(",", _.uniq(existing))
if !@location.isInCurrentRouteParams(name, value)
location = if load then @location else @location.noreload(@scope) location = if load then @location else @location.noreload(@scope)
location.search(name, value) location.search(name, value)
replaceFilter: (name, value, load=false) -> replaceFilter: (name, value, load=false) ->
if !@location.isInCurrentRouteParams(name, value)
location = if load then @location else @location.noreload(@scope) location = if load then @location else @location.noreload(@scope)
location.search(name, value) location.search(name, value)

View File

@ -55,7 +55,6 @@ FeedbackDirective = ($lightboxService, $repo, $confirm, $loading)->
submitButton = $el.find(".submit-button") submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$scope.$on "feedback:show", -> $scope.$on "feedback:show", ->
$scope.$apply -> $scope.$apply ->

View File

@ -1,111 +0,0 @@
###
# 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/integrations/github.coffee
###
taiga = @.taiga
module = angular.module("taigaIntegrations")
AUTH_URL = "https://github.com/login/oauth/authorize"
#############################################################################
## User story team requirements button directive
#############################################################################
GithubLoginButtonDirective = ($window, $params, $location, $config, $events, $confirm, $auth, $navUrls, $loader) ->
# Login or registar a user with his/her github account.
#
# Example:
# tg-github-login-button()
#
# Requirements:
# - ...
template = """
<a class="button button-github" href="" title="Enter with your github account">
<span class="icon icon-github"></span>
<span>Login with Github</span>
</a>
""" #TODO: i18n
link = ($scope, $el, $attrs) ->
clientId = $config.get("gitHubClientId", null)
return if not clientId
renderGitHubButton = ->
$el.html(template) if clientId
loginOnSuccess = (response) ->
if $params.next and $params.next != $navUrls.resolve("login")
nextUrl = $params.next
else
nextUrl = $navUrls.resolve("home")
$events.setupConnection()
$location.search("next", null)
$location.search("token", null)
$location.search("state", null)
$location.search("code", null)
$location.path(nextUrl)
loginOnError = (response) ->
$location.search("state", null)
$location.search("code", null)
$loader.pageLoaded()
if response.data.error_message
$confirm.notify("light-error", response.data.error_message )
else
$confirm.notify("light-error", "Our Oompa Loompas have not been able to get you
credentials from GitHub.") #TODO: i18n
loginWithGitHubAccount = ->
type = $params.state
code = $params.code
token = $params.token
return if not (type == "github" and code)
$loader.start()
data = {code: code, token: token}
$auth.login(data, type).then(loginOnSuccess, loginOnError)
renderGitHubButton()
loginWithGitHubAccount()
$el.on "click", ".button-github", (event) ->
redirectToUri = $location.absUrl()
url = "#{AUTH_URL}?client_id=#{clientId}&redirect_uri=#{redirectToUri}&state=github&scope=user:email"
$window.location.href = url
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
template: ""
}
module.directive("tgGithubLoginButton", ["$window", '$routeParams', "$tgLocation", "$tgConfig", "$tgEvents",
"$tgConfirm", "$tgAuth", "$tgNavUrls", "tgLoader",
GithubLoginButtonDirective])

View File

@ -85,6 +85,9 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@rootscope.$broadcast("history:reload") @rootscope.$broadcast("history:reload")
@.loadIssue() @.loadIssue()
@scope.$on "custom-attributes-values:edit", =>
@rootscope.$broadcast("history:reload")
initializeOnDeleteGoToUrl: -> initializeOnDeleteGoToUrl: ->
ctx = {project: @scope.project.slug} ctx = {project: @scope.project.slug}
if @scope.project.is_issues_activated if @scope.project.is_issues_activated

View File

@ -75,7 +75,6 @@ CreateIssueDirective = ($repo, $confirm, $rootscope, lightboxService, $loading)
submitButton = $el.find(".submit-button") submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link} return {link:link}
@ -123,7 +122,6 @@ CreateBulkIssuesDirective = ($repo, $rs, $confirm, $rootscope, $loading, lightbo
submitButton = $el.find(".submit-button") submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()

View File

@ -94,6 +94,9 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
loadProject: -> loadProject: ->
return @rs.projects.getBySlug(@params.pslug).then (project) => return @rs.projects.getBySlug(@params.pslug).then (project) =>
if not project.is_issues_activated
@location.path(@navUrls.resolve("permission-denied"))
@scope.projectId = project.id @scope.projectId = project.id
@scope.project = project @scope.project = project
@scope.$emit('project:loaded', project) @scope.$emit('project:loaded', project)
@ -530,6 +533,9 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template) -
selectQFilter = debounceLeading 100, (value) -> selectQFilter = debounceLeading 100, (value) ->
return if value is undefined return if value is undefined
$ctrl.replaceFilter("page", null)
if value.length == 0 if value.length == 0
$ctrl.replaceFilter("q", null) $ctrl.replaceFilter("q", null)
$ctrl.storeFilters() $ctrl.storeFilters()

View File

@ -138,13 +138,20 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
@scope.userstories = userstories @scope.userstories = userstories
usByStatus = _.groupBy(userstories, "status") usByStatus = _.groupBy(userstories, "status")
us_archived = []
for status in @scope.usStatusList for status in @scope.usStatusList
if not usByStatus[status.id]? if not usByStatus[status.id]?
usByStatus[status.id] = [] usByStatus[status.id] = []
if @scope.usByStatus?
for us in @scope.usByStatus[status.id]
if us.status != status.id
us_archived.push(us)
# Must preserve the archived columns if loaded # Must preserve the archived columns if loaded
if status.is_archived and @scope.usByStatus? if status.is_archived and @scope.usByStatus? and @scope.usByStatus[status.id].length != 0
usByStatus[status.id] = @scope.usByStatus[status.id] for us in @scope.usByStatus[status.id].concat(us_archived)
if us.status == status.id
usByStatus[status.id].push(us)
usByStatus[status.id] = _.sortBy(usByStatus[status.id], "kanban_order") usByStatus[status.id] = _.sortBy(usByStatus[status.id], "kanban_order")
@ -176,6 +183,9 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
loadProject: -> loadProject: ->
return @rs.projects.getBySlug(@params.pslug).then (project) => return @rs.projects.getBySlug(@params.pslug).then (project) =>
if not project.is_kanban_activated
@location.path(@navUrls.resolve("permission-denied"))
@scope.projectId = project.id @scope.projectId = project.id
@scope.project = project @scope.project = project
@scope.projectId = project.id @scope.projectId = project.id
@ -293,36 +303,6 @@ KanbanDirective = ($repo, $rootscope) ->
module.directive("tgKanban", ["$tgRepo", "$rootScope", KanbanDirective]) module.directive("tgKanban", ["$tgRepo", "$rootScope", KanbanDirective])
#############################################################################
## Kanban Column Height Fixer Directive
#############################################################################
KanbanColumnHeightFixerDirective = ->
mainPadding = 32 # px
scrollPadding = 0 # px
renderSize = ($el) ->
elementOffset = $el.parent().parent().offset().top
windowHeight = angular.element(window).height()
columnHeight = windowHeight - elementOffset - mainPadding - scrollPadding
$el.css("height", "#{columnHeight}px")
link = ($scope, $el, $attrs) ->
timeout(500, -> renderSize($el))
$scope.$on "resize", ->
renderSize($el)
$scope.$on "$destroy", ->
$el.off()
return {link:link}
module.directive("tgKanbanColumnHeightFixer", KanbanColumnHeightFixerDirective)
############################################################################# #############################################################################
## Kanban Archived Status Column Header Control ## Kanban Archived Status Column Header Control
############################################################################# #############################################################################
@ -527,6 +507,9 @@ KanbanUserDirective = ($log) ->
clickable = false clickable = false
link = ($scope, $el, $attrs, $model) -> link = ($scope, $el, $attrs, $model) ->
username_label = $el.parent().find("a.task-assigned")
username_label.addClass("not-clickable")
if not $attrs.tgKanbanUserAvatar if not $attrs.tgKanbanUserAvatar
return $log.error "KanbanUserDirective: no attr is defined" return $log.error "KanbanUserDirective: no attr is defined"
@ -546,9 +529,12 @@ KanbanUserDirective = ($log) ->
html = template(ctx) html = template(ctx)
$el.html(html) $el.html(html)
username_label = $el.parent().find("a.task-assigned") username_label.text(ctx.name)
username_label.html(ctx.name)
username_label.on "click", (event) -> bindOnce $scope, "project", (project) ->
if project.my_permissions.indexOf("modify_us") > -1
clickable = true
$el.on "click", (event) =>
if $el.find("a").hasClass("noclick") if $el.find("a").hasClass("noclick")
return return
@ -556,10 +542,8 @@ KanbanUserDirective = ($log) ->
$ctrl = $el.controller() $ctrl = $el.controller()
$ctrl.changeUsAssignedTo(us) $ctrl.changeUsAssignedTo(us)
bindOnce $scope, "project", (project) -> username_label.removeClass("not-clickable")
if project.my_permissions.indexOf("modify_us") > -1 username_label.on "click", (event) ->
clickable = true
$el.on "click", (event) =>
if $el.find("a").hasClass("noclick") if $el.find("a").hasClass("noclick")
return return

View File

@ -49,7 +49,7 @@ class ProjectsNavigationController extends taiga.Controller
@.loadInitialData() @.loadInitialData()
loadInitialData: -> loadInitialData: ->
return @rs.projects.list().then (projects) => return @rs.projects.listByMember(@rootscope.user?.id).then (projects) =>
for project in projects for project in projects
project.url = @projectUrl.get(project) project.url = @projectUrl.get(project)
@scope.projects = projects @scope.projects = projects

View File

@ -118,7 +118,6 @@ CreateProject = ($rootscope, $repo, $confirm, $location, $navurls, $rs, $project
submitButton = $el.find(".submit-button") submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$el.on "click", ".close", (event) -> $el.on "click", ".close", (event) ->
event.preventDefault() event.preventDefault()

View File

@ -50,7 +50,7 @@ class ProjectsController extends taiga.Controller
promise = @.loadInitialData() promise = @.loadInitialData()
promise.then () => promise.then () =>
@scope.$emit("projects:loaded") @scope.$emit("projects:loaded", @.projects)
promise.then null, @.onInitialDataError.bind(@) promise.then null, @.onInitialDataError.bind(@)
@ -58,7 +58,7 @@ class ProjectsController extends taiga.Controller
promise.finally tgLoader.pageLoaded promise.finally tgLoader.pageLoaded
loadInitialData: -> loadInitialData: ->
return @rs.projects.list().then (projects) => return @rs.projects.listByMember(@rootscope.user?.id).then (projects) =>
@.projects = {'recents': projects.slice(0, 8), 'all': projects} @.projects = {'recents': projects.slice(0, 8), 'all': projects}
for project in projects for project in projects
project.url = @projectUrl.get(project) project.url = @projectUrl.get(project)
@ -257,8 +257,8 @@ ProjectsListDirective = ($compile, $template) ->
$el.html($compile(template({projects: projects}))($scope)) $el.html($compile(template({projects: projects}))($scope))
$scope.$emit("regenerate:project-pagination") $scope.$emit("regenerate:project-pagination")
$scope.$watch "projects", (projects) -> $scope.$on "projects:loaded", (ctx, projects) ->
render(projects) if projects? render(projects.all) if projects.all?
return { return {
link: link link: link

View File

@ -102,12 +102,27 @@ urls = {
"attachments/task": "/tasks/attachments" "attachments/task": "/tasks/attachments"
"attachments/wiki_page": "/wiki/attachments" "attachments/wiki_page": "/wiki/attachments"
# Custom Attributess
"custom-attributes/userstory": "/userstory-custom-attributes"
"custom-attributes/issue": "/issue-custom-attributes"
"custom-attributes/task": "/task-custom-attributes"
# Custom field values
"custom-attributes-values/userstory": "/userstories/custom-attributes-values"
"custom-attributes-values/issue": "/issues/custom-attributes-values"
"custom-attributes-values/task": "/tasks/custom-attributes-values"
# Feedback # Feedback
"feedback": "/feedback" "feedback": "/feedback"
# Export/Import # Export/Import
"exporter": "/exporter" "exporter": "/exporter"
"importer": "/importer/load_dump" "importer": "/importer/load_dump"
# CSV
"userstories-csv": "/userstories/csv?uuid=%s"
"tasks-csv": "/tasks/csv?uuid=%s"
"issues-csv": "/issues/csv?uuid=%s"
} }
# Initialize api urls service # Initialize api urls service
@ -133,6 +148,8 @@ module.run([
"$log", "$log",
"$tgResources", "$tgResources",
"$tgProjectsResourcesProvider", "$tgProjectsResourcesProvider",
"$tgCustomAttributesResourcesProvider",
"$tgCustomAttributesValuesResourcesProvider",
"$tgMembershipsResourcesProvider", "$tgMembershipsResourcesProvider",
"$tgNotifyPoliciesResourcesProvider", "$tgNotifyPoliciesResourcesProvider",
"$tgInvitationsResourcesProvider", "$tgInvitationsResourcesProvider",

View File

@ -0,0 +1,44 @@
###
# 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/resources/custom-field-values.coffee
###
taiga = @.taiga
resourceProvider = ($repo) ->
_get = (objectId, resource) ->
return $repo.queryOne(resource, objectId)
service = {
userstory: {
get: (objectId) -> _get(objectId, "custom-attributes-values/userstory")
}
task: {
get: (objectId) -> _get(objectId, "custom-attributes-values/task")
}
issue: {
get: (objectId) -> _get(objectId, "custom-attributes-values/issue")
}
}
return (instance) ->
instance.customAttributesValues = service
module = angular.module("taigaResources")
module.factory("$tgCustomAttributesValuesResourcesProvider", ["$tgRepo", resourceProvider])

View File

@ -0,0 +1,48 @@
###
# 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/resources/projects.coffee
###
taiga = @.taiga
sizeFormat = @.taiga.sizeFormat
resourceProvider = ($repo) ->
_list = (projectId, resource) ->
return $repo.queryMany(resource, {project: projectId})
service = {
userstory:{
list: (projectId) -> _list(projectId, "custom-attributes/userstory")
}
task:{
list: (projectId) -> _list(projectId, "custom-attributes/task")
}
issue: {
list: (projectId) -> _list(projectId, "custom-attributes/issue")
}
}
return (instance) ->
instance.customAttributes = service
module = angular.module("taigaResources")
module.factory("$tgCustomAttributesResourcesProvider", ["$tgRepo", resourceProvider])

View File

@ -36,6 +36,10 @@ resourceProvider = ($config, $repo, $http, $urls, $auth, $q, $rootScope) ->
service.list = -> service.list = ->
return $repo.queryMany("projects") return $repo.queryMany("projects")
service.listByMember = (memberId) ->
params = {"member": memberId}
return $repo.queryMany("projects", params)
service.templates = -> service.templates = ->
return $repo.queryMany("project-templates") return $repo.queryMany("project-templates")
@ -50,6 +54,18 @@ resourceProvider = ($config, $repo, $http, $urls, $auth, $q, $rootScope) ->
service.stats = (projectId) -> service.stats = (projectId) ->
return $repo.queryOneRaw("projects", "#{projectId}/stats") return $repo.queryOneRaw("projects", "#{projectId}/stats")
service.regenerate_userstories_csv_uuid = (projectId) ->
url = "#{$urls.resolve("projects")}/#{projectId}/regenerate_userstories_csv_uuid"
return $http.post(url)
service.regenerate_issues_csv_uuid = (projectId) ->
url = "#{$urls.resolve("projects")}/#{projectId}/regenerate_issues_csv_uuid"
return $http.post(url)
service.regenerate_tasks_csv_uuid = (projectId) ->
url = "#{$urls.resolve("projects")}/#{projectId}/regenerate_tasks_csv_uuid"
return $http.post(url)
service.leave = (projectId) -> service.leave = (projectId) ->
url = "#{$urls.resolve("projects")}/#{projectId}/leave" url = "#{$urls.resolve("projects")}/#{projectId}/leave"
return $http.post(url) return $http.post(url)

View File

@ -134,7 +134,6 @@ SearchBoxDirective = ($lightboxService, $navurls, $location, $route)->
$el.find("#search-text").val("") $el.find("#search-text").val("")
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link} return {link:link}

View File

@ -40,7 +40,7 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
$scope.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").html("Create") #TODO: i18n
$el.find(".title").html("New task ") #TODO: i18n $el.find(".title").html("New task ") #TODO: i18n
$el.find(".tag-input").val("") $el.find(".tag-input").val("")
@ -51,7 +51,7 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
$scope.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").html("Save") #TODO: i18n
$el.find(".title").html("Edit task ") #TODO: i18n $el.find(".title").html("Edit task ") #TODO: i18n
$el.find(".tag-input").val("") $el.find(".tag-input").val("")
@ -83,7 +83,6 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
$rootscope.$broadcast(broadcastEvent, data) $rootscope.$broadcast(broadcastEvent, data)
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()
@ -127,7 +126,6 @@ CreateBulkTasksDirective = ($repo, $rs, $rootscope, $loading, lightboxService) -
submitButton = $el.find(".submit-button") submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()

View File

@ -104,6 +104,9 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
loadProject: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
if not project.is_backlog_activated
@location.path(@navUrls.resolve("permission-denied"))
@scope.project = project @scope.project = project
# Not used at this momment # Not used at this momment
@scope.pointsList = _.sortBy(project.points, "order") @scope.pointsList = _.sortBy(project.points, "order")
@ -116,6 +119,8 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.$emit('project:loaded', project) @scope.$emit('project:loaded', project)
@.fillUsersAndRoles(project.users, project.roles)
return project return project
loadSprintStats: -> loadSprintStats: ->
@ -185,7 +190,6 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
return data return data
return promise.then(=> @.loadProject()) return promise.then(=> @.loadProject())
.then(=> @.loadUsersAndRoles())
.then(=> @.loadTaskboard()) .then(=> @.loadTaskboard())
refreshTasksOrder: (tasks) -> refreshTasksOrder: (tasks) ->
@ -297,30 +301,6 @@ TaskboardTaskDirective = ($rootscope) ->
module.directive("tgTaskboardTask", ["$rootScope", TaskboardTaskDirective]) module.directive("tgTaskboardTask", ["$rootScope", TaskboardTaskDirective])
#############################################################################
## Taskboard Table Height Fixer Directive
#############################################################################
TaskboardTableHeightFixerDirective = ->
mainPadding = 32 # px
renderSize = ($el) ->
elementOffset = $el.offset().top
windowHeight = angular.element(window).height()
columnHeight = windowHeight - elementOffset - mainPadding
$el.css("height", "#{columnHeight}px")
link = ($scope, $el, $attrs) ->
timeout(500, -> renderSize($el))
$scope.$on "resize", ->
renderSize($el)
return {link:link}
module.directive("tgTaskboardTableHeightFixer", TaskboardTableHeightFixerDirective)
############################################################################# #############################################################################
## Taskboard Squish Column Directive ## Taskboard Squish Column Directive
############################################################################# #############################################################################
@ -421,12 +401,7 @@ TaskboardUserDirective = ($log) ->
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
username_label = $el.parent().find("a.task-assigned") username_label = $el.parent().find("a.task-assigned")
username_label.on "click", (event) -> username_label.addClass("not-clickable")
if $el.find('a').hasClass('noclick')
return
$ctrl = $el.controller()
$ctrl.editTaskAssignedTo($scope.task)
$scope.$watch 'task.assigned_to', (assigned_to) -> $scope.$watch 'task.assigned_to', (assigned_to) ->
user = $scope.usersById[assigned_to] user = $scope.usersById[assigned_to]
@ -449,6 +424,15 @@ TaskboardUserDirective = ($log) ->
$ctrl = $el.controller() $ctrl = $el.controller()
$ctrl.editTaskAssignedTo($scope.task) $ctrl.editTaskAssignedTo($scope.task)
username_label.removeClass("not-clickable")
username_label.on "click", (event) ->
if $el.find('a').hasClass('noclick')
return
$ctrl = $el.controller()
$ctrl.editTaskAssignedTo($scope.task)
return { return {
link: link, link: link,
templateUrl: "taskboard/taskboard-user.html", templateUrl: "taskboard/taskboard-user.html",

View File

@ -36,6 +36,11 @@ module = angular.module("taigaBacklog")
TaskboardSortableDirective = ($repo, $rs, $rootscope) -> TaskboardSortableDirective = ($repo, $rs, $rootscope) ->
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
bindOnce $scope, "project", (project) ->
# If the user has not enough permissions we don't enable the sortable
if not (project.my_permissions.indexOf("modify_us") > -1)
return
oldParentScope = null oldParentScope = null
newParentScope = null newParentScope = null
itemEl = null itemEl = null

View File

@ -71,6 +71,8 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@rootscope.$broadcast("history:reload") @rootscope.$broadcast("history:reload")
@scope.$on "attachment:delete", => @scope.$on "attachment:delete", =>
@rootscope.$broadcast("history:reload") @rootscope.$broadcast("history:reload")
@scope.$on "custom-attributes-values:edit", =>
@rootscope.$broadcast("history:reload")
initializeOnDeleteGoToUrl: -> initializeOnDeleteGoToUrl: ->
ctx = {project: @scope.project.slug} ctx = {project: @scope.project.slug}

View File

@ -69,18 +69,18 @@ class TeamController extends mixOf(taiga.Controller, taiga.PageMixin)
loadMembers: -> loadMembers: ->
return @rs.memberships.list(@scope.projectId, {}, false).then (data) => return @rs.memberships.list(@scope.projectId, {}, false).then (data) =>
currentUser = @auth.getUser() currentUser = @auth.getUser()
if not currentUser.photo? if currentUser? and not currentUser.photo?
currentUser.photo = "/images/unnamed.png" currentUser.photo = "/images/unnamed.png"
@scope.currentUser = _.find data, (membership) => @scope.currentUser = _.find data, (membership) =>
return membership.user == currentUser.id return currentUser? and membership.user == currentUser.id
@scope.totals = {} @scope.totals = {}
_.forEach data, (membership) => _.forEach data, (membership) =>
@scope.totals[membership.user] = 0 @scope.totals[membership.user] = 0
@scope.memberships = _.filter data, (membership) => @scope.memberships = _.filter data, (membership) =>
if membership.user && membership.user != currentUser.id && membership.is_user_active if membership.user && (not currentUser? or membership.user != currentUser.id) && membership.is_user_active
return membership return membership
for membership in @scope.memberships for membership in @scope.memberships

View File

@ -97,7 +97,6 @@ UserChangePasswordDirective = ($rs, $confirm, $loading) ->
submitButton = $el.find(".submit-button") submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()

View File

@ -91,7 +91,8 @@ UserProfileDirective = ($confirm, $auth, $repo) ->
changeEmail = $scope.user.isAttributeModified("email") changeEmail = $scope.user.isAttributeModified("email")
onSuccess = (data) => onSuccess = (data) =>
$auth.setUser($scope.user) $auth.setUser(data)
if changeEmail if changeEmail
$confirm.success("<strong>Check your inbox!</strong><br /> $confirm.success("<strong>Check your inbox!</strong><br />
We have sent a mail to your account<br /> We have sent a mail to your account<br />
@ -107,8 +108,6 @@ UserProfileDirective = ($confirm, $auth, $repo) ->
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()
@ -131,26 +130,26 @@ UserAvatarDirective = ($auth, $model, $rs, $confirm) ->
$auth.setUser(user) $auth.setUser(user)
$scope.user = user $scope.user = user
$el.find('.overlay').hide() $el.find('.overlay').addClass('hidden')
$confirm.notify('success') $confirm.notify('success')
onError = (response) -> onError = (response) ->
showSizeInfo() if response.status == 413 showSizeInfo() if response.status == 413
$el.find('.overlay').hide() $el.find('.overlay').addClass('hidden')
$confirm.notify('error', response.data._error_message) $confirm.notify('error', response.data._error_message)
# Change photo # Change photo
$el.on "click", ".button.change", -> $el.on "click", ".js-change-avatar", ->
$el.find("#avatar-field").click() $el.find("#avatar-field").click()
$el.on "change", "#avatar-field", (event) -> $el.on "change", "#avatar-field", (event) ->
if $scope.avatarAttachment if $scope.avatarAttachment
$el.find('.overlay').css('display', 'flex') $el.find('.overlay').removeClass('hidden')
$rs.userSettings.changeAvatar($scope.avatarAttachment).then(onSuccess, onError) $rs.userSettings.changeAvatar($scope.avatarAttachment).then(onSuccess, onError)
# Use gravatar photo # Use gravatar photo
$el.on "click", "a.use-gravatar", (event) -> $el.on "click", "a.use-gravatar", (event) ->
$el.find('.overlay').show() $el.find('.overlay').removeClass('hidden')
$rs.userSettings.removeAvatar().then(onSuccess, onError) $rs.userSettings.removeAvatar().then(onSuccess, onError)
$scope.$on "$destroy", -> $scope.$on "$destroy", ->

View File

@ -80,6 +80,9 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.$on "attachment:delete", => @scope.$on "attachment:delete", =>
@rootscope.$broadcast("history:reload") @rootscope.$broadcast("history:reload")
@scope.$on "custom-attributes-values:edit", =>
@rootscope.$broadcast("history:reload")
initializeOnDeleteGoToUrl: -> initializeOnDeleteGoToUrl: ->
ctx = {project: @scope.project.slug} ctx = {project: @scope.project.slug}
@scope.onDeleteGoToUrl = @navUrls.resolve("project", ctx) @scope.onDeleteGoToUrl = @navUrls.resolve("project", ctx)

View File

@ -70,6 +70,9 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
loadProject: -> loadProject: ->
return @rs.projects.getBySlug(@params.pslug).then (project) => return @rs.projects.getBySlug(@params.pslug).then (project) =>
if not project.is_wiki_activated
@location.path(@navUrls.resolve("permission-denied"))
@scope.projectId = project.id @scope.projectId = project.id
@scope.project = project @scope.project = project
@scope.$emit('project:loaded', project) @scope.$emit('project:loaded', project)
@ -159,9 +162,6 @@ WikiSummaryDirective = ($log, $template) ->
return if not wikiPage return if not wikiPage
render(wikiPage) render(wikiPage)
$scope.$on "wiki:edit", (event, wikiPage) ->
render(wikiPage)
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()
@ -215,8 +215,7 @@ EditableWikiContentDirective = ($window, $document, $repo, $confirm, $loading, $
if not wiki.id? if not wiki.id?
$analytics.trackEvent("wikipage", "create", "create wiki page", 1) $analytics.trackEvent("wikipage", "create", "create wiki page", 1)
$model.$modelValue = wikiPage $model.$setViewValue wikiPage
$scope.$broadcast("wiki:edit", wikiPage)
$confirm.notify("success") $confirm.notify("success")
switchToReadMode() switchToReadMode()
@ -235,23 +234,16 @@ EditableWikiContentDirective = ($window, $document, $repo, $confirm, $loading, $
$loading.finish($el.find('.save-container')) $loading.finish($el.find('.save-container'))
$el.on "mousedown", ".view-wiki-content", (event) -> $el.on "mousedown", ".view-wiki-content", (event) ->
# Prepare the scroll movement detection
target = angular.element(event.target)
if target.is('pre')
target.data("scroll-pos", target[0].scrollLeft)
$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) target = angular.element(event.target)
return if not isEditable() return if not isEditable()
return if target.is('a') return if event.button == 2
$el.on "mouseup", ".view-wiki-content", (event) ->
target = angular.element(event.target)
return if getSelectedText() return if getSelectedText()
if target.is('pre') return if not isEditable()
prevPos = target.data("scroll-pos") return if target.is('a')
target.data("scroll-pos", null) return if target.is('pre')
if prevPos != target[0].scrollLeft
return
switchToEditMode() switchToEditMode()

View File

@ -9,7 +9,7 @@ div.wrapper.memberships(ng-controller="MembershipsController as ctrl",
include ../includes/components/mainTitle include ../includes/components/mainTitle
.action-buttons .action-buttons
a.button.button-green(title="Add new member" href="" ng-click="ctrl.addNewMembers()") a.button-green(title="Add new member" href="" ng-click="ctrl.addNewMembers()")
span.text + New member span.text + New member
include ../includes/modules/admin/admin-membership-table include ../includes/modules/admin/admin-membership-table

View File

@ -12,10 +12,15 @@ div.wrapper(ng-controller="ProjectProfileController as ctrl",
p.admin-subtitle Export your project to save a backup or to create a new one based on this. p.admin-subtitle Export your project to save a backup or to create a new one based on this.
div.admin-project-export-buttons div.admin-project-export-buttons
a.button.button-green.button-export(href="", title="Export your project") Export a.button-green.button-export(href="", title="Export your project")
span Export
div.admin-project-export-result.hidden div.admin-project-export-result.hidden
div.spin.hidden div.spin.hidden
img(src="/svg/spinner-circle.svg", alt="loading...") img(src="/svg/spinner-circle.svg", alt="loading...")
h3.result-title h3.result-title
p.result-message p.result-message
a.help-button(href="https://taiga.io/support/import-export-projects/", target="_blank")
span.icon.icon-help
span Do you need help? Check out our support page!

View File

@ -87,5 +87,4 @@ div.wrapper(tg-project-modules, ng-controller="ProjectProfileController as ctrl"
option(value="") Select a videoconference system option(value="") Select a videoconference system
input(type="text", ng-model="project.videoconferences_salt", input(type="text", ng-model="project.videoconferences_salt",
placeholder="If you want you can append a salt code to the name of the chat room") placeholder="If you want you can append a salt code to the name of the chat room")
button(type="submit", class="hidden") button.button-green.submit-button(type="submit", title="Save") Save
a.button.button-green.submit-button(href="", title="Save") Save

View File

@ -37,19 +37,18 @@ div.wrapper(tg-project-profile, ng-controller="ProjectProfileController as ctrl"
textarea(name="description", placeholder="Description", id="project-description", textarea(name="description", placeholder="Description", id="project-description",
ng-model="project.description", data-required="true") ng-model="project.description", data-required="true")
tg-privacy-settings-inputs div
div.privacy-settings div.privacy-settings
div div
input.hidden(type="radio", disabled="disabled") input.privacy-project(type="radio", name="private-project", ng-model="project.is_private", ng-value="false")
label.button(for="public-project") Public Project label.trans-button(for="public-project")
span Public Project
div div
input.hidden(type="radio", checked="checked", disabled="disabled") input.privacy-project(type="radio", name="private-project", ng-model="project.is_private", ng-value="true")
label.button(for="private-project") Private Project label.trans-button(for="private-project")
span Private Project
p All projects are private during Taiga's beta period. button.button-green.submit-button(type="submit", title="Save") Save
button(type="submit", class="hidden")
a.button.button-green.submit-button(href="", title="Save") 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(tg-lb-delete-project) div.lightbox.lightbox-delete-project(tg-lb-delete-project)

View File

@ -0,0 +1,32 @@
div.wrapper(ng-controller="ProjectProfileController as ctrl",
ng-init="section='admin'; sectionName='Reports'")
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-profile")
include ../includes/modules/admin-menu
sidebar.menu-tertiary.sidebar(tg-admin-navigation="reports")
include ../includes/modules/admin-submenu-project-profile
section.main.admin-common(tg-project-export)
header
include ../includes/components/mainTitle
p.admin-subtitle Export your project data in CSV format and make your own reports.
p Download a CSV file or copy the generated URL and open it in your favourite text editor or spreadsheet to make your own project data reports. You will be able to visualize and analize all your data easily.
- var csvType = "US";
- var controller = "CsvExporterUserstoriesController";
div.admin-attributes-section
include ../includes/modules/admin/project-csv
- var csvType = "Task";
- var controller = "CsvExporterTasksController";
div.admin-attributes-section
include ../includes/modules/admin/project-csv
- var csvType = "Issues";
- var controller = "CsvExporterIssuesController";
div.admin-attributes-section
include ../includes/modules/admin/project-csv
a.help-button(href="https://taiga.io/support/csv-reports/", target="_blank")
span.icon.icon-help
span How to use this on my own spreadsheet?

View File

@ -0,0 +1,31 @@
div.wrapper(ng-controller="ProjectValuesSectionController")
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-values")
include ../includes/modules/admin-menu
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-custom-fields")
include ../includes/modules/admin-submenu-project-values
section.main.admin-common.admin-attributes
include ../includes/components/mainTitle
p.admin-subtitle Specify the custom fields for your user stories, tasks and issues
div.admin-attributes-section(tg-project-custom-attributes,
ng-controller="ProjectCustomAttributesController as ctrl",
ng-init="type='userstory';")
- var customFieldSectionTitle = "User stories custom fields"
- var customFieldButtonTitle = "Add a custom field in user stories"
include ../includes/modules/admin/admin-custom-attributes
div.admin-attributes-section(tg-project-custom-attributes,
ng-controller="ProjectCustomAttributesController as ctrl",
ng-init="type='task';")
- var customFieldSectionTitle = "Tasks custom fields"
- var customFieldButtonTitle = "Add a custom field in tasks"
include ../includes/modules/admin/admin-custom-attributes
div.admin-attributes-section(tg-project-custom-attributes,
ng-controller="ProjectCustomAttributesController as ctrl",
ng-init="type='issue';")
- var customFieldSectionTitle = "Issues custom fields"
- var customFieldButtonTitle = "Add a custom field in issues"
include ../includes/modules/admin/admin-custom-attributes

View File

@ -1,18 +0,0 @@
div.wrapper(tg-project-values, ng-controller="ProjectValuesController as ctrl",
ng-init="section='admin'; resource='issues'; type='priorities'; sectionName='Issue Priorities'",
type="priorities")
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-values")
include ../includes/modules/admin-menu
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-priorities")
include ../includes/modules/admin-submenu-project-values
section.main.admin-common
include ../includes/components/mainTitle
p.admin-subtitle Specify the priority levels users can assign to issues
div.project-values-options
a.button.button-green.show-add-new(href="", title="Add New")
span Add new priority
include ../includes/modules/admin/project-types

View File

@ -1,18 +0,0 @@
div.wrapper(tg-project-values, ng-controller="ProjectValuesController as ctrl",
ng-init="section='admin'; resource='issues'; type='severities'; sectionName='Issue severities'",
type="severities")
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-values")
include ../includes/modules/admin-menu
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-severities")
include ../includes/modules/admin-submenu-project-values
section.main.admin-common
include ../includes/components/mainTitle
p.admin-subtitle Specify the severity level users can select to classify issues
div.project-values-options
a.button.button-green.show-add-new(href="", title="Add New")
span Add new severity
include ../includes/modules/admin/project-types

View File

@ -1,18 +0,0 @@
div.wrapper(tg-project-values, ng-controller="ProjectValuesController as ctrl",
ng-init="section='admin'; resource='issues'; type='issue-statuses'; sectionName='Issue Statuses'",
type="issue-statuses")
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-values")
include ../includes/modules/admin-menu
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-issue-status")
include ../includes/modules/admin-submenu-project-values
section.main.admin-common
include ../includes/components/mainTitle
p.admin-subtitle Specify the column headers that you will use to classify Issues
div.project-values-options
a.button.button-green.show-add-new(href="", title="Add New")
span Add new status
include ../includes/modules/admin/project-status

View File

@ -1,18 +0,0 @@
div.wrapper(tg-project-values, ng-controller="ProjectValuesController as ctrl",
ng-init="section='admin'; resource='issues'; type='issue-types'; sectionName='Issue Types'",
type="issue-types")
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-values")
include ../includes/modules/admin-menu
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-issue-types")
include ../includes/modules/admin-submenu-project-values
section.main.admin-common
include ../includes/components/mainTitle
p.admin-subtitle Specify the categories users can select to classify issues
div.project-values-options
a.button.button-green.show-add-new(href="", title="Add New")
span Add new type
include ../includes/modules/admin/project-types

View File

@ -0,0 +1,15 @@
div.wrapper(ng-controller="ProjectValuesSectionController")
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-values")
include ../includes/modules/admin-menu
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-points")
include ../includes/modules/admin-submenu-project-values
section.main.admin-common.admin-attributes
include ../includes/components/mainTitle
p.admin-subtitle Specify the points your user stories could be estimated to
div.admin-attributes-section(tg-project-values, ng-controller="ProjectValuesController as ctrl",
ng-init="section='admin'; resource='userstories'; type='points'; sectionName='Us points'",
type="points")
include ../includes/modules/admin/project-points

View File

@ -0,0 +1,15 @@
div.wrapper(ng-controller="ProjectValuesSectionController")
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-values")
include ../includes/modules/admin-menu
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-priorities")
include ../includes/modules/admin-submenu-project-values
section.main.admin-common.admin-attributes
include ../includes/components/mainTitle
p.admin-subtitle Specify the priorities your issues will have
div.admin-attributes-section(tg-project-values, ng-controller="ProjectValuesController as ctrl",
ng-init="section='admin'; resource='issues'; type='priorities'; sectionName='Issue priorities'; objName='priority'",
type="priorities")
include ../includes/modules/admin/project-types

View File

@ -0,0 +1,15 @@
div.wrapper(ng-controller="ProjectValuesSectionController")
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-values")
include ../includes/modules/admin-menu
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-severities")
include ../includes/modules/admin-submenu-project-values
section.main.admin-common.admin-attributes
include ../includes/components/mainTitle
p.admin-subtitle Specify the severities your issues will have
div.admin-attributes-section(tg-project-values, ng-controller="ProjectValuesController as ctrl",
ng-init="section='admin'; resource='issues'; type='severities'; sectionName='Issue severities'; objName='severity'",
type="severities")
include ../includes/modules/admin/project-types

View File

@ -0,0 +1,25 @@
div.wrapper(ng-controller="ProjectValuesSectionController")
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-values")
include ../includes/modules/admin-menu
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-status")
include ../includes/modules/admin-submenu-project-values
section.main.admin-common.admin-attributes
include ../includes/components/mainTitle
p.admin-subtitle Specify the statuses your user stories, tasks and issues will go through
div.admin-attributes-section(tg-project-values, ng-controller="ProjectValuesController as ctrl",
ng-init="section='admin'; resource='userstories'; type='userstory-statuses'; sectionName='Us Statuses'",
type="userstory-statuses")
include ../includes/modules/admin/project-us-status
div.admin-attributes-section(tg-project-values, ng-controller="ProjectValuesController as ctrl",
ng-init="section='admin'; resource='tasks'; type='task-statuses'; sectionName='Task Statuses'",
type="task-statuses")
include ../includes/modules/admin/project-status
div.admin-attributes-section(tg-project-values, ng-controller="ProjectValuesController as ctrl",
ng-init="section='admin'; resource='issues'; type='issue-statuses'; sectionName='Issue Statuses'",
type="issue-statuses")
include ../includes/modules/admin/project-status

View File

@ -1,18 +0,0 @@
div.wrapper(tg-project-values, ng-controller="ProjectValuesController as ctrl",
ng-init="section='admin'; resource='tasks'; type='task-statuses'; sectionName='Task Statuses'",
type="task-statuses")
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-values")
include ../includes/modules/admin-menu
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-task-status")
include ../includes/modules/admin-submenu-project-values
section.main.admin-common
include ../includes/components/mainTitle
p.admin-subtitle Specify the column headers that you will use to classify Tasks related to each User Stories
div.project-values-options
a.button.button-green.show-add-new(href="", title="Add New")
span Add new status
include ../includes/modules/admin/project-status

View File

@ -0,0 +1,15 @@
div.wrapper(ng-controller="ProjectValuesSectionController")
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-values")
include ../includes/modules/admin-menu
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-types")
include ../includes/modules/admin-submenu-project-values
section.main.admin-common.admin-attributes
include ../includes/components/mainTitle
p.admin-subtitle Specify the types your user stories could be estimated to
div.admin-attributes-section(tg-project-values, ng-controller="ProjectValuesController as ctrl",
ng-init="section='admin'; resource='issues'; type='issue-types'; sectionName='Issue types'; objName='type'",
type="issue-types")
include ../includes/modules/admin/project-types

View File

@ -1,24 +0,0 @@
div.wrapper(tg-project-values, ng-controller="ProjectValuesController as ctrl",
ng-init="section='admin'; resource='userstories'; type='points'; sectionName='Us points'",
type="points")
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-values")
include ../includes/modules/admin-menu
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-us-points")
include ../includes/modules/admin-submenu-project-values
section.main.admin-common
include ../includes/components/mainTitle
p.admin-subtitle Specify the numerical system you will use to indicate the level of difficulty for each User Story
- var helpLightboxId = "notion-admin-project-values-us-points"
include ../includes/components/help-notion-button
div.project-values-options
a.button.button-green.show-add-new(href="", title="Add New")
span Add new point
include ../includes/modules/admin/project-points
div.lightbox.lightbox-generic-notion.notion-admin-project-values-us-points(id="notion-admin-project-values-us-points", tg-lb-notion)
include ../includes/modules/help-notions/lightbox-notion-admin-project-values-us-points

View File

@ -1,18 +0,0 @@
div.wrapper(tg-project-values, ng-controller="ProjectValuesController as ctrl",
ng-init="section='admin'; resource='userstories'; type='userstory-statuses'; sectionName='Us Statuses'",
type="userstory-statuses")
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-values")
include ../includes/modules/admin-menu
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-us-status")
include ../includes/modules/admin-submenu-project-values
section.main.admin-common
include ../includes/components/mainTitle
p.admin-subtitle Specify the column headers that you will use to classify User Stories
div.project-values-options
a.button.button-green.show-add-new(href="", title="Add New")
span Add new status
include ../includes/modules/admin/project-us-status

View File

@ -8,10 +8,12 @@ div.wrapper.roles(ng-controller="RolesController as ctrl",
section.main.admin-roles.admin-common section.main.admin-roles.admin-common
.header-with-actions .header-with-actions
include ../includes/components/mainTitle include ../includes/components/mainTitle
.action-buttons .action-buttons(ng-if="!role.external_user")
a.button.button-red.delete-role(href="", title="Delete", ng-click="ctrl.delete()") Delete a.button-red.delete-role(href="", title="Delete", ng-click="ctrl.delete()")
span Delete
div(ng-if="!role.external_user")
div(tg-edit-role) div(tg-edit-role)
.edit-role .edit-role
input(type="text", value="{{ role.name }}") input(type="text", value="{{ role.name }}")
@ -31,4 +33,8 @@ div.wrapper.roles(ng-controller="RolesController as ctrl",
span.check-text.check-yes Yes span.check-text.check-yes Yes
span.check-text.check-no No span.check-text.check-no No
div(ng-if="role.external_user")
p.total
span.role-name {{ role.name }}
div(tg-role-permissions, ng-model="role") div(tg-role-permissions, ng-model="role")

View File

@ -27,9 +27,7 @@ div.wrapper.roles(tg-bitbucket-webhooks, ng-controller="BitbucketController as c
label(for="valid-origin-ips") Valid origin ips (separated by ,) label(for="valid-origin-ips") Valid origin ips (separated by ,)
input(type="text", name="valid-origin-ips", tg-valid-origin-ips, ng-model="bitbucket.valid_origin_ips", placeholder="Bitbucket requests are not signed so the best way of verifying the origin is by IP. If the field is empty there will be no IP validation.", id="valid-origin-ips") input(type="text", name="valid-origin-ips", tg-valid-origin-ips, ng-model="bitbucket.valid_origin_ips", placeholder="Bitbucket requests are not signed so the best way of verifying the origin is by IP. If the field is empty there will be no IP validation.", id="valid-origin-ips")
button(type="submit", class="hidden") button.button-green.submit-button(type="submit", title="Save") Save
a.button.button-green.submit-button(href="", title="Save") Save
a.help-button(href="https://taiga.io/support/bitbucket-integration/", target="_blank") a.help-button(href="https://taiga.io/support/bitbucket-integration/", target="_blank")
span.icon.icon-help span.icon.icon-help

View File

@ -23,9 +23,7 @@ div.wrapper.roles(tg-github-webhooks, ng-controller="GithubController as ctrl",
.icon.icon-copy .icon.icon-copy
.help-copy Copy to clipboard: Ctrl+C .help-copy Copy to clipboard: Ctrl+C
button(type="submit", class="hidden") button.button-green.submit-button(type="submit", title="Save") Save
a.button.button-green.submit-button(href="", title="Save") Save
a.help-button(href="https://taiga.io/support/github-integration/", target="_blank") a.help-button(href="https://taiga.io/support/github-integration/", target="_blank")
span.icon.icon-help span.icon.icon-help

View File

@ -31,8 +31,7 @@ block content
label(for="valid-origin-ips") Valid origin ips (separated by ,) label(for="valid-origin-ips") Valid origin ips (separated by ,)
input(type="text", name="valid-origin-ips", tg-valid-origin-ips, ng-model="gitlab.valid_origin_ips", placeholder="Gitlab requests are not signed so the best way of verifying the origin is by IP. If the field is empty there will be no IP validation.", id="valid-origin-ips") input(type="text", name="valid-origin-ips", tg-valid-origin-ips, ng-model="gitlab.valid_origin_ips", placeholder="Gitlab requests are not signed so the best way of verifying the origin is by IP. If the field is empty there will be no IP validation.", id="valid-origin-ips")
button(type="submit", class="hidden") button.button-green.submit-button(type="submit", title="Save") Save
a.button.button-green.submit-button(href="", title="Save") Save
a.help-button(href="https://taiga.io/support/gitlab-integration/", target="_blank") a.help-button(href="https://taiga.io/support/gitlab-integration/", target="_blank")
span.icon.icon-help span.icon.icon-help

View File

@ -14,7 +14,7 @@ block content
p.admin-subtitle Webhooks notify external services about events in Taiga, like comments, user stories.... p.admin-subtitle Webhooks notify external services about events in Taiga, like comments, user stories....
div.webhooks-options div.webhooks-options
a.button.button-green.hidden.add-webhook(href="",title="Add a New Webhook") Add Webhook a.button-green.hidden.add-webhook(href="",title="Add a New Webhook") Add Webhook
section.webhooks-table.basic-table section.webhooks-table.basic-table
div.table-header div.table-header

View File

@ -11,7 +11,7 @@ section.attachments
input(id="add-attach", type="file", multiple="multiple") input(id="add-attach", type="file", multiple="multiple")
.attachment-body.sortable .attachment-body.sortable
.single-attachment(ng-repeat="attach in ctrl.attachments|filter:ctrl.filterAttachments track by attach.id" tg-attachment="attach") .single-attachment(ng-repeat="attach in ctrl.attachments|filter:ctrl.filterAttachments track by attach.id" tg-attachment="attach", tg-bind-scope)
.single-attachment(ng-repeat="file in ctrl.uploadingAttachments") .single-attachment(ng-repeat="file in ctrl.uploadingAttachments")
.attachment-name .attachment-name

View File

@ -9,6 +9,7 @@ div.wrapper(tg-backlog, ng-controller="BacklogController as ctrl",
div.burndown(tg-gm-backlog-graph) div.burndown(tg-gm-backlog-graph)
include ../includes/modules/burndown include ../includes/modules/burndown
div.backlog-menu div.backlog-menu
div.backlog-table-options
a.trans-button.move-to-current-sprint(href="", title="Move to Current Sprint", a.trans-button.move-to-current-sprint(href="", title="Move to Current Sprint",
id="move-to-current-sprint") id="move-to-current-sprint")
span.icon.icon-move span.icon.icon-move

View File

@ -1,11 +0,0 @@
ul.popover.pop-points-open
<% _.each(points, function(point) { %>
li
<% if (point.selected) { %>
a.point(href="", title!="<%- point.name %>", data-point-id!="<%- point.id %>")
| <%- point.name %>
<% } else { %>
a.point.active(href="", title!="<%- point.name %>", data-point-id!="<%- point.id %>")
| <%- point.name %>
<% } %>
<% }); %>

View File

@ -1,2 +1,4 @@
a(href="#", class="button button-gray item-block") Block a(href="#", class="button button-gray item-block")
a(href="#", class="button button-red item-unblock") Unblock span Block
a(href="#", class="button button-red item-unblock")
span Unblock

View File

@ -1 +1,2 @@
a(href="", class="button button-red") Delete a(href="", class="button button-red")
span Delete

View File

@ -11,7 +11,6 @@
<% if(watcher) { %> <% if(watcher) { %>
.watcher-single .watcher-single
.watcher-avatar .watcher-avatar
span.avatar(title!="<%- watcher.full_name_display %>")
img(src!="<%- watcher.photo %>" alt!="<%- watcher.full_name_display %>") img(src!="<%- watcher.photo %>" alt!="<%- watcher.full_name_display %>")
.watcher-name .watcher-name
span <%- watcher.full_name_display %> span <%- watcher.full_name_display %>

View File

@ -1,9 +0,0 @@
ul.points-per-role
li.total
span.points <%- totalPoints %>
span.role total
<% _.each(roles, function(role) { %>
li.total.clickable(data-role-id!="<%- role.id %>")
span.points <%- role.points %>
span.role <%- role.name %>
<% }); %>

View File

@ -1,13 +0,0 @@
ul.popover.pop-points-open
<% _.each(points, function(point) { %>
li
<% if (point.selected) { %>
a(href="", class="point", title!="<%- point.name %>",
data-point-id!="<%- point.id %>", data-role-id!="<%- roleId %>")
| <%- point.name %>
<% } else { %>
a(href="", class="point active", title!="<%- point.name %>",
data-point-id!="<%- point.id %>" data-role-id!="<%- roleId %>")
| <%- point.name %>
<% } %>
<% }); %>

View File

@ -3,7 +3,7 @@ ul.points-per-role
span.points <%- totalPoints %> span.points <%- totalPoints %>
span.role total span.role total
<% _.each(roles, function(role) { %> <% _.each(roles, function(role) { %>
li(class!="total <% if(editable){ %>clickable<% } %>", data-role-id!="<%- role.id %>") li(class!="total <% if(editable){ %>clickable<% } %>", data-role-id!="<%- role.id %>", title!="<%- role.name %>")
span.points <%- role.points %> span.points <%- role.points %>
span.role <%- role.name %> span.role <%- role.name %>
<% }); %> <% }); %>

View File

@ -1,4 +1,4 @@
ul.popover.pop-points-open ul.popover.pop-points-open(class!="<% if (horizontal) { %>horizontal<% }; %>")
<% _.each(points, function(point) { %> <% _.each(points, function(point) { %>
li li
<% if (point.selected) { %> <% if (point.selected) { %>

View File

@ -0,0 +1,5 @@
a.us-points(href="", title!="<%= title %>", class!="<% if (!editable) { %>not-clickable<% } %>")
span.points-value <%= text %>
<% if (editable) { %>
span.icon.icon-arrow-bottom(tg-check-permission="modify_us")
<% } %>

View File

@ -11,13 +11,13 @@ section.history
section.history-comments section.history-comments
.comments-list .comments-list
div(tg-check-permission!="modify_<%- type %>", tg-toggle-comment, class="add-comment") div(tg-check-permission!="modify_<%- type %>", tg-toggle-comment, class="add-comment")
textarea(placeholder="Type a new comment here", textarea(placeholder="Type a new comment here", ng-model!="<%- ngmodel %>.comment", tg-markitup="tg-markitup")
ng-model!="<%- ngmodel %>.comment", tg-markitup="tg-markitup")
<% if (mode !== "edit") { %> <% if (mode !== "edit") { %>
a(class="help-markdown", href="https://taiga.io/support/taiga-markdown-syntax/", target="_blank", title="Mardown syntax help") a(class="help-markdown", href="https://taiga.io/support/taiga-markdown-syntax/", target="_blank", title="Mardown syntax help")
span.icon.icon-help span.icon.icon-help
span Markdown syntax help span Markdown syntax help
a(href="", title="Comment", class="button button-green save-comment") Comment a(href="", title="Comment", class="button button-green save-comment")
span Comment
<% } %> <% } %>
section.history-activity.hidden section.history-activity.hidden
.changes-list .changes-list

View File

@ -0,0 +1,17 @@
.change-entry
.activity-changed
span <%- name %>
.activity-fromto
<% if (removed.length > 0) { %>
p
strong removed
br
span <%- removed %>
<% } %>
<% if (added.length > 0) { %>
p
strong added
br
span <%- added %>
<% } %>

View File

@ -5,5 +5,5 @@ div.form
fieldset fieldset
textarea.reason(placeholder="Please explain the reason") textarea.reason(placeholder="Please explain the reason")
a.button.button-green(href="") a.button-green(href="")
span Save span Save

View File

@ -0,0 +1,14 @@
form.custom-field-single.editable
div.custom-field-data
label.custom-field-name(for="custom-field-description")
<%- name %>
<% if (description){ %>
span.custom-field-description
<%- description %>
<% } %>
div.custom-field-value
input#custom-field-description(name="description", type="text", value!="<%- value %>")
div.custom-field-options
a.icon.icon-floppy(href="", title="Save Custom Field")

View File

@ -0,0 +1,17 @@
div.custom-field-single
div.custom-field-data
span.custom-field-name
<%- name %>
<% if (description){ %>
span.custom-field-description
<%- description %>
<% } %>
div.custom-field-value.read-mode
span
<%- value %>
<% if (isEditable) { %>
div.custom-field-options
a.icon.icon-edit(href="", title="Edit Custom Field")
<% } %>

View File

@ -0,0 +1,7 @@
section.duty-custom-fields(ng-show="ctrl.customAttributes.length")
div.custom-fields-header
span Custom Fields
// Remove .open class on click on this button in both .icon and .custom-fields-body to close
a.icon.icon-arrow-bottom(class!="<% if (!collapsed) { %>open<% } %>")
div.custom-fields-body(class!="<% if (!collapsed) { %>open<% } %>")
div(ng-repeat="att in ctrl.customAttributes", tg-custom-attribute-value="ctrl.getAttributeValue(att)", required-edition-perm!="<%- requiredEditionPerm %>")

View File

@ -1,9 +1,9 @@
div.new-us div.new-us
a.button.button-green(href="", title="Add a new User Story", a.button-green(href="", title="Add a new User Story",
ng-click="ctrl.addNewUs('standard')", ng-click="ctrl.addNewUs('standard')",
tg-check-permission="add_us") tg-check-permission="add_us")
span.text + Add a new User Story span.text + Add a new User Story
a.button.button-bulk(href="", title="Add some new User Stories in bulk", a.button-bulk(href="", title="Add some new User Stories in bulk",
ng-click="ctrl.addNewUs('bulk')", ng-click="ctrl.addNewUs('bulk')",
tg-check-permission="add_us") tg-check-permission="add_us")
span.icon.icon-bulk span.icon.icon-bulk

View File

@ -1,4 +1,4 @@
div.row.us-item-row(ng-repeat="us in visibleUserstories track by us.id", tg-draggable, ng-class="{blocked: us.is_blocked}") div.row.us-item-row(ng-repeat="us in visibleUserstories track by us.id", tg-bind-scope, ng-class="{blocked: us.is_blocked}", tg-class-permission="{'readonly': '!modify_us'}")
div.user-stories div.user-stories
div.tags-block(tg-colorize-tags="us.tags", tg-colorize-tags-type="backlog") div.tags-block(tg-colorize-tags="us.tags", tg-colorize-tags-type="backlog")
div.user-story-name div.user-story-name
@ -20,7 +20,5 @@ div.row.us-item-row(ng-repeat="us in visibleUserstories track by us.id", tg-drag
div.points(tg-backlog-us-points="us") div.points(tg-backlog-us-points="us")
a.us-points(href="", title="Points") a.us-points(href="", title="Points")
span.points-value 0
span.icon.icon-arrow-bottom(tg-check-permission="modify_us")
a.icon.icon-drag-v(tg-check-permission="modify_us", href="", title="Drag") a.icon.icon-drag-v(tg-check-permission="modify_us", href="", title="Drag")

View File

@ -1,12 +0,0 @@
//- NOTE: - Add a lightbox-notion at the end of parent template. Ex:
//-
//- div.hide.lightbox.lightbox-generic-notion(id="notion-admin-project-values-us-points",
//- tg-lb-notion)
//- include views/modules/help-notions/lightbox-notion-admin-project-values-us-points
//-
//- - Defined variable 'helpLightboxId' in parent template. Ex:
//-
//- - var helpLightboxId = "admin-project-values-us-points"
//- include views/components/help
a.icon.icon-idea.help(href="", title="You need some help?", tg-lb-notion-button=helpLightboxId)

View File

@ -1,23 +0,0 @@
div.kanban-tagline(tg-colorize-tags="us.tags", tg-colorize-tags-type="kanban", ng-hide="us.isArchived")
div.kanban-task-inner(ng-class="{'task-archived': us.isArchived}")
div.avatar-wrapper(tg-kanban-user-avatar="us.assigned_to", ng-model="us", ng-hide="us.isArchived")
div.task-text(ng-hide="us.isArchived")
a.task-assigned(href="", title="Assign User Story")
span.task-num(tg-bo-ref="us.ref")
a.task-name(href="", title="See user story detail", ng-bind="us.subject",
tg-nav="project-userstories-detail:project=project.slug,ref=us.ref")
p.task-points(href="", title="Total Us points")
span(ng-if="us.total_points !== null", ng-bind="us.total_points")
span(ng-if="us.total_points !== null") points
span(ng-if="us.total_points === null") Not estimated
div.task-archived-text(ng-show="us.isArchived")
p You have archived
p
span.task-num(tg-bo-ref="us.ref")
span.task-name(ng-bind="us.subject")
p Drag & drop again to undo
a.icon.icon-edit(tg-check-permission="modify_us", href="", title="Edit", ng-hide="us.isArchived")
a.icon.icon-drag-h(tg-check-permission="modify_us", href="", title="Drag&Drop", ng-hide="us.isArchived")

View File

@ -1,37 +1,35 @@
div.summary.large-summary div.summary.large-summary
div.large-summary-wrapper
div div
div.summary-progress-bar div.summary-progress-bar
div.current-progress div.current-progress
div.data div.data
span.number 30% span.number 30%
span.description completed span.description completed
ul div.summary-stats
li
span.number 12 span.number 12
span.description project<br />points span.description project<br />points
li div.summary-stats
span.number 23 span.number 23
span.description defined<br />points span.description defined<br />points
li div.summary-stats
span.number 12 span.number 12
span.description assigned<br />points span.description assigned<br />points
li div.summary-stats.summary-stats-divider
span.number 23 span.number 23
span.description closed<br />points span.description closed<br />points
ul div.summary-stats
li
span.icon.icon-bulk span.icon.icon-bulk
span.number 73 span.number 73
span.description created<br />tasks span.description created<br />tasks
li div.summary-stats
span.number 72 span.number 72
span.description closed<br />tasks span.description closed<br />tasks
li div.summary-stats
span.number 18 span.number 18
span.description remaining<br />tasks span.description remaining<br />tasks
ul div.summary-stats
li
span.icon.icon-iocaine span.icon.icon-iocaine
span.number 10 span.number 10
span.description iocanie<br />doses span.description iocanie<br />doses

View File

@ -1,28 +1,26 @@
div.summary.large-summary div.summary.large-summary
div div.large-summary-wrapper
div.summary-progress-wrapper
div.summary-progress-bar(tg-progress-bar="stats.completedPercentage") div.summary-progress-bar(tg-progress-bar="stats.completedPercentage")
div.data div.data
span.number(ng-bind="stats.completedPercentage + '%'") span.number(ng-bind="stats.completedPercentage + '%'")
ul div.summary-stats
li
span.number(ng-bind="stats.totalPointsSum|default:'--'") span.number(ng-bind="stats.totalPointsSum|default:'--'")
span.description total<br />points span.description total<br />points
li div.summary-stats
span.number(ng-bind="stats.completedPointsSum|default:'--'") span.number(ng-bind="stats.completedPointsSum|default:'--'")
span.description completed<br />points span.description completed<br />points
ul div.summary-stats
li
span.icon.icon-bulk span.icon.icon-bulk
span.number(ng-bind="stats.openTasks|default:'--'") span.number(ng-bind="stats.openTasks|default:'--'")
span.description open<br />tasks span.description open<br />tasks
li div.summary-stats
span.number(ng-bind="stats.completed_tasks|default:'--'") span.number(ng-bind="stats.completed_tasks|default:'--'")
span.description closed<br />tasks span.description closed<br />tasks
ul div.summary-stats(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!")
li(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!")
span.icon.icon-iocaine span.icon.icon-iocaine
span.number(ng-bind="stats.iocaine_doses|default:'--'") span.number(ng-bind="stats.iocaine_doses|default:'--'")
span.description iocaine<br />doses span.description iocaine<br />doses

View File

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

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