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>
- Anler Hernández <hello@anler.me>
- 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.
And here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS --
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>
- 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 #
## 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)
### Features

View File

@ -1,6 +1,7 @@
# Taiga Front #
![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 ##

View File

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

View File

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

View File

@ -33,7 +33,9 @@ MAX_MEMBERSHIP_FIELDSETS = 4
CreateMembersDirective = ($rs, $rootScope, $confirm, $loading ,lightboxService) ->
extraTextTemplate = """
<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>
"""
@ -150,7 +152,6 @@ CreateMembersDirective = ($rs, $rootScope, $confirm, $loading ,lightboxService)
submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link: link}

View File

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

View File

@ -65,6 +65,9 @@ class ProjectProfileController extends mixOf(taiga.Controller, taiga.PageMixin)
loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) =>
if not project.i_am_owner
@location.path(@navUrls.resolve("permission-denied"))
@scope.project = project
@scope.pointsList = _.sortBy(project.points, "order")
@scope.usStatusList = _.sortBy(project.us_statuses, "order")
@ -120,7 +123,6 @@ ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location) ->
submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link}
@ -154,7 +156,6 @@ ProjectDefaultValuesDirective = ($repo, $confirm, $loading) ->
submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$scope.$on "$destroy", ->
$el.off()
@ -233,7 +234,7 @@ ProjectExportDirective = ($window, $rs, $confirm) ->
resultTitleEl = $el.find(".result-title")
setLoadingTitle = -> 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 ")
setLoadingMessage = -> resultMessageEl.html("Please don't close this page.") # TODO: i18n
@ -296,3 +297,67 @@ ProjectExportDirective = ($window, $rs, $confirm) ->
return {link:link}
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")
#############################################################################
## Project values Controller
## Project values section Controller
#############################################################################
class ProjectValuesController extends mixOf(taiga.Controller, taiga.PageMixin)
class ProjectValuesSectionController extends mixOf(taiga.Controller, taiga.PageMixin)
@.$inject = [
"$scope",
"$rootScope",
@ -59,29 +59,47 @@ class ProjectValuesController extends mixOf(taiga.Controller, taiga.PageMixin)
promise.then null, @.onInitialDataError.bind(@)
@scope.$on("admin:project-values:move", @.moveValue)
loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) =>
if not project.i_am_owner
@location.path(@navUrls.resolve("permission-denied"))
@scope.project = project
@scope.$emit('project:loaded', 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: ->
promise = @repo.resolve({pslug: @params.pslug}).then (data) =>
@scope.projectId = data.project
return data
return promise.then( => @q.all([
@.loadProject(),
@.loadValues(),
]))
return promise.then => @.loadProject()
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) =>
values = @scope.values
@ -147,21 +165,14 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
$(document.body).scrollTop(table.offset().top + table.height())
if focus
$(".new-value input").focus()
$el.find(".new-value input:visible").first().focus()
submit = debounce 2000, =>
promise = $repo.save($scope.project)
promise.then ->
$confirm.notify("success")
promise.then null, (data) ->
$confirm.notify("error", data._error_message)
saveValue = debounce 2000, (target) ->
form = target.parents("form").checksley()
saveValue = (target) ->
formEl = target.parents("form")
form = formEl.checksley()
return if not form.validate()
value = target.scope().value
value = formEl.scope().value
promise = $repo.save(value)
promise.then =>
row = target.parents(".row.table-main")
@ -169,25 +180,37 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
row.siblings(".visualization").removeClass('hidden')
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)
cancel = (target) ->
row = target.parents(".row.table-main")
value = target.scope().value
formEl = target.parents("form")
value = formEl.scope().value
$scope.$apply ->
row.addClass("hidden")
value.revert()
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) ->
event.preventDefault()
$el.find(".new-value").removeClass('hidden')
@ -196,29 +219,12 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
$el.on "click", ".add-new", debounce 2000, (event) ->
event.preventDefault()
form = $el.find(".new-value").parents("form").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 =>
$ctrl.loadValues().then ->
animationFrame.add () ->
goToBottomList()
$el.find(".new-value").addClass("hidden")
initializeNewValue()
promise.then null, (data) ->
$confirm.notify("error")
form.setErrors(data)
target = $el.find(".new-value")
saveNewValue(target)
$el.on "click", ".delete-new", (event) ->
event.preventDefault()
$el.find(".new-value").hide()
$el.find(".new-value").addClass("hidden")
initializeNewValue()
$el.on "click", ".edit-value", (event) ->
@ -240,6 +246,14 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
target = angular.element(event.currentTarget)
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) ->
event.preventDefault()
target = angular.element(event.currentTarget)
@ -253,7 +267,9 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
$el.on "click", ".delete-value", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
value = target.scope().value
formEl = target.parents("form")
value = formEl.scope().value
choices = {}
_.each $scope.values, (option) ->
if value.id != option.id
@ -337,3 +353,276 @@ 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: ->
return @rs.projects.get(@scope.projectId).then (project) =>
if not project.i_am_owner
@location.path(@navUrls.resolve("permission-denied"))
@scope.project = project
@scope.$emit('project:loaded', project)
@scope.anyComputableRole = _.some(_.map(project.roles, (point) -> point.computable))
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: ->
return @rs.roles.list(@scope.projectId).then (data) =>
@scope.roles = data
@scope.role = @scope.roles[0]
return data
return @rs.roles.list(@scope.projectId)
.then @loadExternalUserRole
.then (roles) =>
@scope.roles = roles
@scope.role = @scope.roles[0]
return roles
loadInitialData: ->
promise = @repo.resolve({pslug: @params.pslug}).then (data) =>
@ -256,7 +279,7 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm) ->
<div class="category-item" data-id="<%- permission.key %>">
<span><%- permission.description %></span>
<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>
<span class="check-text check-yes">Yes</span>
<span class="check-text check-no">No</span>
@ -279,10 +302,23 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm) ->
setActivePermissions = (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) ->
return _.map(category, (x) ->
_.extend({}, x, {
activePermissions: _.filter(x["permissions"], "active").length
return _.map(category, (cat) ->
cat.permissions = cat.permissions.map (permission) ->
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
target = angular.element(event.currentTarget)
$scope.role.permissions = getActivePermissions()
onSuccess = (role) ->
categories = generateCategoriesFromRole(role)
onSuccess = () ->
categories = generateCategoriesFromRole($scope.role)
categoryId = target.parents(".category-config").data("id")
renderResume(target.parents(".category-config"), categories[categoryId])
$rootscope.$broadcast("projects:reload")
@ -381,7 +418,14 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm) ->
target.prop "checked", !target.prop("checked")
$scope.role.permissions = getActivePermissions()
$repo.save($scope.role).then onSuccess, onError
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
$scope.$on "$destroy", ->
$el.off()

View File

@ -38,10 +38,12 @@ class WebhooksController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.
"$tgRepo",
"$tgResources",
"$routeParams",
"$tgLocation",
"$tgNavUrls",
"$appTitle"
]
constructor: (@scope, @repo, @rs, @params, @appTitle) ->
constructor: (@scope, @repo, @rs, @params, @location, @navUrls, @appTitle) ->
bindMethods(@)
@scope.sectionName = "Webhooks" #i18n
@ -62,6 +64,9 @@ class WebhooksController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.
loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) =>
if not project.i_am_owner
@location.path(@navUrls.resolve("permission-denied"))
@scope.project = project
@scope.$emit('project:loaded', project)
return project
@ -89,7 +94,7 @@ WebhookDirective = ($rs, $repo, $confirm, $loading) ->
for log in webhooklogs
log.validStatus = 200 <= log.status < 300
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
webhook.logs_counter = webhooklogs.length
@ -123,8 +128,6 @@ WebhookDirective = ($rs, $repo, $confirm, $loading) ->
save = debounce 2000, (target) ->
form = target.parents("form").checksley()
return if not form.validate()
value = target.scope().value
promise = $repo.save(webhook)
promise.then =>
showVisualizationMode()
@ -152,7 +155,7 @@ WebhookDirective = ($rs, $repo, $confirm, $loading) ->
$el.on "keyup", ".edition-mode input", (event) ->
if event.keyCode == 13
target = angular.element(event.currentTarget)
saveWebhook(target)
save(target)
else if event.keyCode == 27
target = angular.element(event.currentTarget)
cancel(target)
@ -231,8 +234,7 @@ NewWebhookDirective = ($rs, $repo, $confirm, $loading) ->
formDOMNode.addClass("hidden")
addWebhookDOMNode.removeClass("hidden")
formDOMNode.on "click", ".add-new", debounce 2000, (event) ->
event.preventDefault()
save = debounce 2000, () ->
form = formDOMNode.checksley()
return if not form.validate()
@ -246,6 +248,14 @@ NewWebhookDirective = ($rs, $repo, $confirm, $loading) ->
$confirm.notify("error")
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) ->
$scope.$apply ->
initializeNewValue()
@ -445,7 +455,6 @@ GithubWebhooksDirective = ($repo, $confirm, $loading) ->
submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link}
@ -481,7 +490,6 @@ GitlabWebhooksDirective = ($repo, $confirm, $loading) ->
submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link}
@ -517,7 +525,6 @@ BitbucketWebhooksDirective = ($repo, $confirm, $loading) ->
submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
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()
}
promise = $auth.login(data)
loginFormType = $config.get("loginFormType", "normal")
promise = $auth.login(data, loginFormType)
return promise.then(onSuccess, onError)
$el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link}
@ -242,7 +243,6 @@ RegisterDirective = ($auth, $confirm, $location, $navUrls, $config, $analytics)
promise.then(onSuccessSubmit, onErrorSubmit)
$el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link}
@ -279,7 +279,6 @@ ForgotPasswordDirective = ($auth, $confirm, $location, $navUrls) ->
promise.then(onSuccessSubmit, onErrorSubmit)
$el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link}
@ -321,7 +320,6 @@ ChangePasswordFromRecoveryDirective = ($auth, $confirm, $location, $params, $nav
promise.then(onSuccessSubmit, onErrorSubmit)
$el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link}
@ -471,7 +469,6 @@ CancelAccountDirective = ($repo, $model, $auth, $confirm, $location, $params, $n
promise.then(onSuccessSubmit, onErrorSubmit)
$el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
return {link:link}

View File

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

View File

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

View File

@ -59,7 +59,6 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
@scope.sectionName = "Backlog"
@showTags = false
@activeFilters = false
@excludeClosedSprints = true
@.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", @.loadProjectStats)
@scope.$on("backlog:toggle-closed-sprints-visualization", @.toggleClosedSprintsVisualization)
@scope.$on("backlog:load-closed-sprints", @.loadClosedSprints)
@scope.$on("backlog:unload-closed-sprints", @.unloadClosedSprints)
initializeSubscription: ->
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) =>
@scope.project.tags_colors = tags_colors
loadSprints: ->
params = {}
if @excludeClosedSprints
params["closed"] = false
unloadClosedSprints: ->
@scope.$apply =>
@scope.closedSprints = []
@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) =>
# NOTE: Fix order of USs because the filter orderBy does not work propertly in partials files
for sprint in sprints
@ -158,10 +170,8 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
@scope.sprints = sprints
@scope.openSprints = _.filter(sprints, (sprint) => not sprint.closed).reverse()
@scope.closedSprints = _.filter(sprints, (sprint) => sprint.closed)
if not @excludeClosedSprints
@scope.totalClosedMilestones = @scope.closedSprints.length
@scope.closedSprints = [] if !@scope.closedSprints
@scope.sprintsCounter = sprints.length
@scope.sprintsById = groupBy(sprints, (x) -> x.id)
@rootscope.$broadcast("sprints:loaded", sprints)
@ -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
@scope.userstories = _.sortBy(userstories, "backlog_order")
@.generateFilters()
@.setSearchDataFilters()
@.filterVisibleUserstories()
@.generateFilters()
@rootscope.$broadcast("filters:loaded", @scope.filters)
# 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: ->
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.project = project
@scope.totalClosedMilestones = project.total_closed_milestones
@ -232,40 +246,20 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
return promise.then(=> @.loadBacklog())
toggleClosedSprintsVisualization: ->
@excludeClosedSprints = not @excludeClosedSprints
@.loadSprints()
filterVisibleUserstories: ->
@scope.visibleUserstories = []
# 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) =>
if _.intersection(selectedTags, us.tags).length == 0
return true
return false
@scope.visibleUserstories = _.reject @scope.userstories, (us) =>
return _.some us.tags, (tag) =>
return @isFilterSelected("tag", tag)
# Filter by status
selectedStatuses = _.filter(@scope.filters.statuses, "selected")
selectedStatuses = _.map(selectedStatuses, "id")
@scope.visibleUserstories = _.filter @scope.visibleUserstories, (us) =>
if @searchdata["statuses"] && Object.keys(@searchdata["statuses"]).length
return @isFilterSelected("statuses", taiga.toString(us.status))
if selectedStatuses.length > 0
@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
})
return true
prepareBulkUpdateData: (uses, field="backlog_order") ->
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
getUrlFilters: ->
return _.pick(@location.search(), "statuses", "tags", "q")
isFilterSelected: (type, id) ->
if @searchdata[type]? and @searchdata[type][id]
return true
return false
generateFilters: ->
setSearchDataFilters: () ->
urlfilters = @.getUrlFilters()
if urlfilters.q
@scope.filtersQ = @scope.filtersQ or urlfilters.q
searchdata = {}
@searchdata = {}
for name, value of urlfilters
if not searchdata[name]?
searchdata[name] = {}
if not @searchdata[name]?
@searchdata[name] = {}
for val in taiga.toString(value).split(",")
searchdata[name][val] = true
@searchdata[name][val] = true
isSelected = (type, id) ->
if searchdata[type]? and searchdata[type][id]
return true
return false
getUrlFilters: ->
return _.pick(@location.search(), "statuses", "tags", "q")
generateFilters: ->
@scope.filters = {}
plainTags = _.flatten(_.filter(_.map(@scope.userstories, "tags")))
#tags
plainTags = _.flatten(_.filter(_.map(@scope.visibleUserstories, "tags")))
plainTags.sort()
@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],
count: v
}
obj.selected = true if isSelected("tags", obj.id)
obj.selected = true if @isFilterSelected("tags", obj.id)
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) =>
if status
@ -474,11 +474,20 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
color: @scope.usStatusById[k].color,
count:v
}
obj.selected = true if isSelected("statuses", obj.id)
obj.selected = true if @isFilterSelected("statuses", obj.id)
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
@ -514,7 +523,6 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
module.controller("BacklogController", BacklogController)
#############################################################################
## Backlog Directive
#############################################################################
@ -531,29 +539,27 @@ BacklogDirective = ($repo, $rootscope) ->
if $scope.stats?
removeDoomlineDom()
elements = getUsItems()
stats = $scope.stats
total_points = stats.total_points
current_sum = stats.assigned_points
for element in elements
scope = element.scope()
return if not $scope.visibleUserstories
if not scope.us?
continue
current_sum += scope.us.total_points
for us, i in $scope.visibleUserstories
current_sum += us.total_points
if current_sum > total_points
addDoomLineDom(element)
domElement = $el.find('.backlog-table-body .us-item-row')[i]
addDoomLineDom(domElement)
break
removeDoomlineDom = ->
$el.find(".doom-line").remove()
addDoomLineDom = (element) ->
element?.before(doomLineTemplate({}))
$(element).before(doomLineTemplate({}))
getUsItems = ->
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")
ussToMove = _.map ussDom, (item) ->
itemScope = angular.element(item).scope()
item = $(item).closest('.tg-scope')
itemScope = item.scope()
itemScope.us.milestone = $scope.sprints[0].id
return itemScope.us
@ -727,7 +734,6 @@ UsRolePointsSelectorDirective = ($rootscope, $template) ->
$el.on "click", ".role", (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
rolScope = target.scope()
$rootscope.$broadcast("uspoints:select", target.data("role-id"), target.text())
@ -740,170 +746,107 @@ UsRolePointsSelectorDirective = ($rootscope, $template) ->
module.directive("tgUsRolePointsSelector", ["$rootScope", "$tgTemplate", UsRolePointsSelectorDirective])
UsPointsDirective = ($repo, $tgTemplate) ->
rolesTemplate = $tgTemplate.get("backlog/us-points-roles-popover.html", true)
pointsTemplate = $tgTemplate.get("backlog/us-points-popover.html", true)
UsPointsDirective = ($tgEstimationsService, $repo, $tgTemplate) ->
rolesTemplate = $tgTemplate.get("common/estimation/us-points-roles-popover.html", true)
link = ($scope, $el, $attrs) ->
$ctrl = $el.controller()
us = $scope.$eval($attrs.tgBacklogUsPoints)
updatingSelectedRoleId = null
selectedRoleId = null
numberOfRoles = _.size(us.points)
filteringRoleId = null
estimationProcess = null
# Preselect the role if we have only one
if numberOfRoles == 1
selectedRoleId = _.keys(us.points)[0]
$scope.$on "uspoints:select", (ctx, roleId, roleName) ->
us = $scope.$eval($attrs.tgBacklogUsPoints)
selectedRoleId = roleId
estimationProcess.render()
roles = []
updatePointsRoles = ->
roles = _.map computableRoles, (role) ->
pointId = us.points[role.id]
pointObj = $scope.pointsById[pointId]
$scope.$on "uspoints:clear-selection", (ctx) ->
us = $scope.$eval($attrs.tgBacklogUsPoints)
selectedRoleId = null
estimationProcess.render()
role = _.clone(role, true)
role.points = if pointObj.value? then pointObj.value else "?"
return role
$scope.$watch $attrs.tgBacklogUsPoints, (us) ->
if us
estimationProcess = $tgEstimationsService.create($el, us, $scope.project)
computableRoles = _.filter($scope.project.roles, "computable")
updatePointsRoles()
# Update roles
roles = estimationProcess.calculateRoles()
if roles.length == 0
$el.find(".icon-arrow-bottom").remove()
$el.find("a.us-points").addClass("not-clickable")
if roles.length == 0
$el.find(".icon-arrow-bottom").remove()
$el.find("a.us-points").addClass("not-clickable")
else if roles.length == 1
# Preselect the role if we have only one
selectedRoleId = _.keys(us.points)[0]
renderPointsSelector = (us, roleId) ->
# Prepare data for rendering
points = _.map $scope.project.points, (point) ->
point = _.clone(point, true)
point.selected = if us.points[roleId] == point.id then false else true
return point
if estimationProcess.isEditable
bindClickElements()
html = pointsTemplate({"points": points})
estimationProcess.onSelectedPointForRole = (roleId, pointId) ->
@save(roleId, pointId).then ->
$ctrl.loadProjectStats()
# Remove any prevous state
$el.find(".popover").popover().close()
$el.find(".pop-points-open").remove()
estimationProcess.render = () ->
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}"
# Render into DOM and show the new created element
$el.append(html)
ctx = {
totalPoints: totalPoints
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)
# 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").popover().open()
renderRolesSelector = (us) ->
updatePointsRoles()
estimationProcess.render()
renderRolesSelector = () ->
roles = estimationProcess.calculateRoles()
html = rolesTemplate({"roles": roles})
# Render into DOM and show the new created element
$el.append(html)
$el.find(".pop-role").popover().open(() -> $(this).remove())
renderPoints = (us, roleId) ->
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
bindClickElements = () ->
$el.on "click", "a.us-points span", (event) ->
event.preventDefault()
event.stopPropagation()
us = $scope.$eval($attrs.tgBacklogUsPoints)
updatingSelectedRoleId = selectedRoleId
if selectedRoleId?
renderPointsSelector(us, selectedRoleId)
estimationProcess.renderPointsSelector(selectedRoleId)
else
renderRolesSelector(us)
renderRolesSelector()
$el.on "click", ".role", (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
us = $scope.$eval($attrs.tgBacklogUsPoints)
updatingSelectedRoleId = target.data("role-id")
popRolesDom = $el.find(".pop-role")
popRolesDom.find("a").removeClass("active")
popRolesDom.find("a[data-role-id='#{updatingSelectedRoleId}']").addClass("active")
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")
estimationProcess.renderPointsSelector(updatingSelectedRoleId)
$scope.$on "$destroy", ->
$el.off()
return {link: link}
module.directive("tgBacklogUsPoints", ["$tgRepo", "$tgTemplate", UsPointsDirective])
module.directive("tgBacklogUsPoints", ["$tgEstimationsService", "$tgRepo", "$tgTemplate", UsPointsDirective])
#############################################################################
## Burndown graph directive

View File

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

View File

@ -61,6 +61,8 @@ BacklogSprintDirective = ($repo, $rootscope) ->
# Event Handlers
$el.on "click", ".sprint-name > .icon-arrow-up", (event) ->
event.preventDefault()
toggleSprint($el)
$el.find(".sprint-table").slideToggle(slideOptions)
@ -135,26 +137,38 @@ module.directive("tgBacklogSprintHeader", ["$tgNavUrls", "$tgTemplate", BacklogS
#############################################################################
ToggleExcludeClosedSprintsVisualization = ($rootscope, $loading) ->
excludeClosedSprints = false
excludeClosedSprints = true
link = ($scope, $el, $attrs) ->
# insert loading wrapper
loadingElm = $("<div>")
$el.after(loadingElm)
# Event Handlers
$el.on "click", "", (event) ->
$loading.start($el.parent().siblings('.loading-spinner'))
$rootscope.$broadcast("backlog:toggle-closed-sprints-visualization")
$el.on "click", (event) ->
event.preventDefault()
excludeClosedSprints = not excludeClosedSprints
$loading.start(loadingElm)
if excludeClosedSprints
$rootscope.$broadcast("backlog:unload-closed-sprints")
else
$rootscope.$broadcast("backlog:load-closed-sprints")
$scope.$on "$destroy", ->
$el.off()
$scope.$on "sprints:loaded", (ctx, sprints) =>
closedSprints = _.filter(sprints, (sprint) -> sprint.closed)
$loading.finish($el.parent().siblings('.loading-spinner'))
$scope.$on "closed-sprints:reloaded", (ctx, sprints) =>
$loading.finish(loadingElm)
#TODO: i18n
if closedSprints.length > 0
$el.text("Hide closed sprints")
if sprints.length > 0
text = "Hide closed sprints"
else
$el.text("Show closed sprints")
text = "Show closed sprints"
$el.find(".text").text(text)
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-modules": "/project/:project/admin/project-profile/modules"
"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-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-issue-status": "/project/:project/admin/project-values/issue-status"
"project-admin-project-values-issue-types": "/project/:project/admin/project-values/issue-types"
"project-admin-project-values-issue-priorities": "/project/:project/admin/project-values/issue-priorities"
"project-admin-project-values-issue-severities": "/project/:project/admin/project-values/issue-severities"
"project-admin-project-profile-reports": "/project/:project/admin/project-profile/reports"
"project-admin-project-values-status": "/project/:project/admin/project-values/status"
"project-admin-project-values-points": "/project/:project/admin/project-values/points"
"project-admin-project-values-priorities": "/project/:project/admin/project-values/priorities"
"project-admin-project-values-severities": "/project/:project/admin/project-values/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-roles": "/project/:project/admin/roles"
"project-admin-third-parties-webhooks": "/project/:project/admin/third-parties/webhooks"

View File

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

View File

@ -49,6 +49,14 @@ class UrlsService extends taiga.Service
_.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.service('$tgUrls', UrlsService)

View File

@ -60,6 +60,42 @@ 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
#############################################################################

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('textarea').focus()
$el.on "click", ".save", ->
$el.on "click", ".save", (e) ->
e.preventDefault()
description = $scope.item.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
groupBy = @.taiga.groupBy
module = angular.module("taigaCommon")
@ -27,7 +28,53 @@ module = angular.module("taigaCommon")
## 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.
#
# Example:
@ -37,90 +84,26 @@ LbUsEstimationDirective = ($rootScope, $repo, $confirm, $template) ->
# - Us object (ng-model)
# - 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) ->
render = (points) ->
totalPoints = calculateTotalPoints(points) or 0
computableRoles = _.filter($scope.project.roles, "computable")
$scope.$watch $attrs.ngModel, (us) ->
if us
estimationProcess = $tgEstimationsService.create($el, us, $scope.project)
estimationProcess.onSelectedPointForRole = (roleId, pointId) ->
@save(roleId, pointId).then ->
$rootScope.$broadcast("history:reload")
roles = _.map computableRoles, (role) ->
pointId = points[role.id]
pointObj = $scope.pointsById[pointId]
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)
role = _.clone(role, true)
role.points = if pointObj? and pointObj.name? then pointObj.name else "?"
return role
ctx = {
totalPoints: totalPoints
roles: roles
}
html = mainTemplate(ctx)
$el.html(html)
renderPoints = (target, usPoints, roleId) ->
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
estimationProcess.render()
$scope.$on "$destroy", ->
$el.off()
@ -131,81 +114,46 @@ LbUsEstimationDirective = ($rootScope, $repo, $confirm, $template) ->
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) ->
# 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)
EstimationsService = ($template, $qqueue, $repo, $confirm, $q) ->
pointsTemplate = $template.get("common/estimation/us-estimation-points.html", true)
link = ($scope, $el, $attrs, $model) ->
isEditable = ->
return $scope.project.my_permissions.indexOf("modify_us") != -1
class EstimationProcess
constructor: (@$el, @us, @project) ->
@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) ->
totalPoints = if us.total_points? then us.total_points else "?"
computableRoles = _.filter($scope.project.roles, "computable")
save: (roleId, pointId) ->
deferred = $q.defer()
$qqueue.add () =>
onSuccess = =>
deferred.resolve()
$confirm.notify("success")
roles = _.map computableRoles, (role) ->
pointId = us.points[role.id]
pointObj = $scope.pointsById[pointId]
onError = =>
$confirm.notify("error")
@us.revert()
@render()
deferred.reject()
role = _.clone(role, true)
role.points = if pointObj? and pointObj.name? then pointObj.name else "?"
return role
$repo.save(@us).then(onSuccess, onError)
ctx = {
totalPoints: totalPoints
roles: roles
editable: isEditable()
}
html = mainTemplate(ctx)
$el.html(html)
return deferred.promise
renderPoints = (target, us, roleId) ->
points = _.map $scope.project.points, (point) ->
point = _.clone(point, true)
point.selected = if us.points[roleId] == point.id then false else true
return point
calculateTotalPoints: () ->
values = _.map(@us.points, (v, k) => @pointsById[v]?.value)
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
return "0"
@ -215,64 +163,82 @@ UsEstimationDirective = ($rootScope, $repo, $confirm, $qqueue, $template) ->
return _.reduce(notNullValues, (acc, num) -> acc + num)
save = $qqueue.bindAdd (roleId, pointId) =>
$el.find(".popover").popover().close()
calculateRoles: () ->
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
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
return roles
onSuccess = ->
$confirm.notify("success")
$rootScope.$broadcast("history:reload")
onError = ->
$confirm.notify("error")
us.revert()
$model.$setViewValue(us)
bindClickEvents: =>
@$el.on "click", ".total.clickable", (event) =>
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
roleId = target.data("role-id")
@renderPointsSelector(roleId, target)
target.siblings().removeClass('active')
target.addClass('active')
$repo.save($model.$modelValue).then(onSuccess, onError)
@$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(@us.points, true)
points[roleId] = pointId
@us.points = points
@render()
@onSelectedPointForRole(roleId, pointId)
$el.on "click", ".total.clickable", (event) ->
event.preventDefault()
event.stopPropagation()
return if not isEditable()
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
target = angular.element(event.currentTarget)
roleId = target.data("role-id")
maxPointLength = 5
horizontalList = _.some points, (point) => point.name.length > maxPointLength
us = $model.$modelValue
renderPoints(target, us, roleId)
html = pointsTemplate({"points": points, roleId: roleId, horizontal: horizontalList})
# 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)
target.siblings().removeClass('active')
target.addClass('active')
@$el.find(".pop-points-open").popover().open ->
$(this)
.removeClass("active")
.closest("li").removeClass("active")
$el.on "click", ".point", (event) ->
event.preventDefault()
event.stopPropagation()
return if not isEditable()
@$el.find(".pop-points-open").show()
target = angular.element(event.currentTarget)
roleId = target.data("role-id")
pointId = target.data("point-id")
create = ($el, us, project) ->
estimationProcess = $el.data("estimationProcess")
save(roleId, pointId)
if !estimationProcess
estimationProcess = new EstimationProcess($el, us, project)
$el.data("estimationProcess", estimationProcess)
$scope.$watch $attrs.ngModel, (us) ->
render(us) if us
if estimationProcess.isEditable
estimationProcess.bindClickEvents()
else
$el.unbind("click")
$scope.$on "$destroy", ->
$el.off()
return estimationProcess
return {
link: link
restrict: "EA"
require: "ngModel"
create: create
}
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_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.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)
templateChangeGeneric = $template.get("common/history/history-change-generic.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)
templateActivity = $template.get("common/history/history-activity.html", true)
templateBaseEntries = $template.get("common/history/history-base-entries.html", true)
@ -103,6 +111,9 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm) ->
# Attachment
is_deprecated: "is deprecated"
blocked_note: "blocked note"
is_blocked: "is blocked"
} # TODO i18n
return humanizedFieldNames[field] or field
@ -121,18 +132,18 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm) ->
formatChange = (change) ->
if _.isArray(change)
if change.length == 0
return "nil"
return "empty"
return change.join(", ")
if change == ""
return "nil"
return "empty"
if not change? or change == false
return "no"
if change == true
return "yes"
if change == false
return "no"
return change
# Render into string (operations without mutability)
@ -162,14 +173,50 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm) ->
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) ->
if field == "description"
# TODO: i18n
return templateChangeDiff({name: "description", diff: value[1]})
return templateChangeDiff({name: getHumanizedFieldName("description"), diff: value[1]})
else if field == "blocked_note"
return templateChangeDiff({name: getHumanizedFieldName("blocked_note"), diff: value[1]})
else if field == "points"
return templateChangePoints({points: value})
else if field == "attachments"
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"
name = getHumanizedFieldName(field)
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
deleteCommentUser: comment.delete_comment_user.name if comment.delete_comment_user?.name
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) ->

View File

@ -45,6 +45,8 @@ class LightboxService extends taiga.Service
@animationFrame.add =>
$el.addClass("open")
@animationFrame.add =>
lightboxContent.show()
defered.resolve()
@ -334,7 +336,6 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$el.on "click", ".close", (event) ->
event.preventDefault()
@ -403,7 +404,6 @@ CreateBulkUserstoriesDirective = ($repo, $rs, $rootscope, lightboxService, $load
submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$scope.$on "$destroy", ->
$el.off()
@ -604,41 +604,3 @@ WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationS
}
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")
# 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
@ -64,35 +91,52 @@ tgMarkitupDirective = ($rootscope, $rs, $tr, $selectedText, $template) ->
markdown.off(".preview")
closePreviewMode()
markdownCaretPositon = false
setCaretPosition = (elm, caretPos) ->
if elm.createTextRange
range = elm.createTextRange()
range.move("character", caretPos)
setCaretPosition = (textarea, caretPosition) ->
if textarea.createTextRange
range = textarea.createTextRange()
range.move("character", caretPosition)
range.select()
else if elm.selectionStart
elm.focus()
elm.setSelectionRange(caretPos, caretPos)
else if textarea.selectionStart
textarea.focus()
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")
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")
#return the new position
return currentCaretPosition - removedLineLength + 1
if replace
return cursorPosition - lines[nline].length + replace.length - 1
else
return cursorPosition
markdownSettings =
nameSpace: "markdown"
onShiftEnter: {keepDefault:false, openWith:"\n\n"}
onEnter:
keepDefault: false
replaceWith: (data) =>
keepDefault: false,
replaceWith: () -> "\n"
afterInsert: (data) ->
lines = data.textarea.value.split("\n")
cursorLine = data.textarea.value[0..(data.caretPosition - 1)].split("\n").length
newLineContent = data.textarea.value[data.caretPosition..].split("\n")[0]
@ -105,12 +149,9 @@ tgMarkitupDirective = ($rootscope, $rs, $tr, $selectedText, $template) ->
emptyListItem = lastLine.match /^(\s*)\-\s$/
if emptyListItem
markdownCaretPositon = removeEmptyLine(data.textarea, lines.length - 1, data.caretPosition)
markdownCaretPositon = addLine(data.textarea, cursorLine - 1)
else
breakLineAtBeginning = newLineContent.match /^(\s*)\-\s/
if !breakLineAtBeginning
return "\n#{match[1]}" if match
markdownCaretPositon = addLine(data.textarea, cursorLine, "#{match[1]}")
# unordered list *
match = lastLine.match /^(\s*\* ).*/
@ -119,12 +160,9 @@ tgMarkitupDirective = ($rootscope, $rs, $tr, $selectedText, $template) ->
emptyListItem = lastLine.match /^(\s*\* )$/
if emptyListItem
markdownCaretPositon = removeEmptyLine(data.textarea, lines.length - 1, data.caretPosition)
markdownCaretPositon = addLine(data.textarea, cursorLine - 1)
else
breakLineAtBeginning = newLineContent.match /^(\s*)\*\s/
if !breakLineAtBeginning
return "\n#{match[1]}" if match
markdownCaretPositon = addLine(data.textarea, cursorLine, "#{match[1]}")
# ordered list
match = lastLine.match /^(\s*)(\d+)\.\s/
@ -133,29 +171,12 @@ tgMarkitupDirective = ($rootscope, $rs, $tr, $selectedText, $template) ->
emptyListItem = lastLine.match /^(\s*)(\d+)\.\s$/
if emptyListItem
markdownCaretPositon = removeEmptyLine(data.textarea, lines.length - 1, data.caretPosition)
markdownCaretPositon = addLine(data.textarea, cursorLine - 1)
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"
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))
setCaretPosition(data.textarea, markdownCaretPositon) if markdownCaretPositon
markupSet: [
{
@ -214,14 +235,18 @@ tgMarkitupDirective = ($rootscope, $rs, $tr, $selectedText, $template) ->
{
name: $tr.t("markdown-editor.picture")
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")
key: "L"
openWith: "["
closeWith: ']([![Url:!:http://]!] "[![Title]!]")'
closeWith: '](<<<[![Url:!:http://]!]>>> "[![Title]!]")'
placeHolder: $tr.t("markdown-editor.link-placeholder")
beforeInsert:(markItUp) -> prepareUrlFormatting(markItUp)
afterInsert:(markItUp) -> urlFormatting(markItUp)
},
{
separator: "---------------"
@ -256,6 +281,45 @@ tgMarkitupDirective = ($rootscope, $rs, $tr, $selectedText, $template) ->
target = angular.element(event.textarea)
$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) ->
heading = ""
n = $.trim(markItUp.selection or markItUp.placeHolder).length

View File

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

View File

@ -55,7 +55,6 @@ FeedbackDirective = ($lightboxService, $repo, $confirm, $loading)->
submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$scope.$on "feedback:show", ->
$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")
@.loadIssue()
@scope.$on "custom-attributes-values:edit", =>
@rootscope.$broadcast("history:reload")
initializeOnDeleteGoToUrl: ->
ctx = {project: @scope.project.slug}
if @scope.project.is_issues_activated

View File

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

View File

@ -94,6 +94,9 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
loadProject: ->
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.project = project
@scope.$emit('project:loaded', project)
@ -530,6 +533,9 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template) -
selectQFilter = debounceLeading 100, (value) ->
return if value is undefined
$ctrl.replaceFilter("page", null)
if value.length == 0
$ctrl.replaceFilter("q", null)
$ctrl.storeFilters()

View File

@ -138,13 +138,20 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
@scope.userstories = userstories
usByStatus = _.groupBy(userstories, "status")
us_archived = []
for status in @scope.usStatusList
if not 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
if status.is_archived and @scope.usByStatus?
usByStatus[status.id] = @scope.usByStatus[status.id]
if status.is_archived and @scope.usByStatus? and @scope.usByStatus[status.id].length != 0
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")
@ -176,6 +183,9 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
loadProject: ->
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.project = project
@scope.projectId = project.id
@ -293,36 +303,6 @@ KanbanDirective = ($repo, $rootscope) ->
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
#############################################################################
@ -527,6 +507,9 @@ KanbanUserDirective = ($log) ->
clickable = false
link = ($scope, $el, $attrs, $model) ->
username_label = $el.parent().find("a.task-assigned")
username_label.addClass("not-clickable")
if not $attrs.tgKanbanUserAvatar
return $log.error "KanbanUserDirective: no attr is defined"
@ -546,15 +529,7 @@ KanbanUserDirective = ($log) ->
html = template(ctx)
$el.html(html)
username_label = $el.parent().find("a.task-assigned")
username_label.html(ctx.name)
username_label.on "click", (event) ->
if $el.find("a").hasClass("noclick")
return
us = $model.$modelValue
$ctrl = $el.controller()
$ctrl.changeUsAssignedTo(us)
username_label.text(ctx.name)
bindOnce $scope, "project", (project) ->
if project.my_permissions.indexOf("modify_us") > -1
@ -567,6 +542,15 @@ KanbanUserDirective = ($log) ->
$ctrl = $el.controller()
$ctrl.changeUsAssignedTo(us)
username_label.removeClass("not-clickable")
username_label.on "click", (event) ->
if $el.find("a").hasClass("noclick")
return
us = $model.$modelValue
$ctrl = $el.controller()
$ctrl.changeUsAssignedTo(us)
$scope.$on "$destroy", ->
$el.off()

View File

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

View File

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

View File

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

View File

@ -102,12 +102,27 @@ urls = {
"attachments/task": "/tasks/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"
# Export/Import
"exporter": "/exporter"
"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
@ -133,6 +148,8 @@ module.run([
"$log",
"$tgResources",
"$tgProjectsResourcesProvider",
"$tgCustomAttributesResourcesProvider",
"$tgCustomAttributesValuesResourcesProvider",
"$tgMembershipsResourcesProvider",
"$tgNotifyPoliciesResourcesProvider",
"$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 = ->
return $repo.queryMany("projects")
service.listByMember = (memberId) ->
params = {"member": memberId}
return $repo.queryMany("projects", params)
service.templates = ->
return $repo.queryMany("project-templates")
@ -50,6 +54,18 @@ resourceProvider = ($config, $repo, $http, $urls, $auth, $q, $rootScope) ->
service.stats = (projectId) ->
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) ->
url = "#{$urls.resolve("projects")}/#{projectId}/leave"
return $http.post(url)

View File

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

View File

@ -40,7 +40,7 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
$scope.isNew = true
# 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(".tag-input").val("")
@ -51,7 +51,7 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
$scope.isNew = false
# 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(".tag-input").val("")
@ -83,7 +83,6 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
$rootscope.$broadcast(broadcastEvent, data)
$el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$scope.$on "$destroy", ->
$el.off()
@ -127,7 +126,6 @@ CreateBulkTasksDirective = ($repo, $rs, $rootscope, $loading, lightboxService) -
submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit
$el.on "click", ".submit-button", submit
$scope.$on "$destroy", ->
$el.off()

View File

@ -104,6 +104,9 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) =>
if not project.is_backlog_activated
@location.path(@navUrls.resolve("permission-denied"))
@scope.project = project
# Not used at this momment
@scope.pointsList = _.sortBy(project.points, "order")
@ -116,6 +119,8 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.$emit('project:loaded', project)
@.fillUsersAndRoles(project.users, project.roles)
return project
loadSprintStats: ->
@ -185,7 +190,6 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
return data
return promise.then(=> @.loadProject())
.then(=> @.loadUsersAndRoles())
.then(=> @.loadTaskboard())
refreshTasksOrder: (tasks) ->
@ -297,30 +301,6 @@ TaskboardTaskDirective = ($rootscope) ->
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
#############################################################################
@ -421,12 +401,7 @@ TaskboardUserDirective = ($log) ->
link = ($scope, $el, $attrs) ->
username_label = $el.parent().find("a.task-assigned")
username_label.on "click", (event) ->
if $el.find('a').hasClass('noclick')
return
$ctrl = $el.controller()
$ctrl.editTaskAssignedTo($scope.task)
username_label.addClass("not-clickable")
$scope.$watch 'task.assigned_to', (assigned_to) ->
user = $scope.usersById[assigned_to]
@ -449,6 +424,15 @@ TaskboardUserDirective = ($log) ->
$ctrl = $el.controller()
$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 {
link: link,
templateUrl: "taskboard/taskboard-user.html",

View File

@ -36,47 +36,52 @@ module = angular.module("taigaBacklog")
TaskboardSortableDirective = ($repo, $rs, $rootscope) ->
link = ($scope, $el, $attrs) ->
oldParentScope = null
newParentScope = null
itemEl = null
tdom = $el
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
deleteElement = (itemEl) ->
# Completelly remove item and its scope from dom
itemEl.scope().$destroy()
itemEl.off()
itemEl.remove()
oldParentScope = null
newParentScope = null
itemEl = null
tdom = $el
tdom.sortable({
handle: ".taskboard-task-inner",
dropOnEmpty: true
connectWith: ".taskboard-tasks-box"
revert: 400
})
deleteElement = (itemEl) ->
# Completelly remove item and its scope from dom
itemEl.scope().$destroy()
itemEl.off()
itemEl.remove()
tdom.on "sortstop", (event, ui) ->
parentEl = ui.item.parent()
itemEl = ui.item
itemTask = itemEl.scope().task
itemIndex = itemEl.index()
newParentScope = parentEl.scope()
tdom.sortable({
handle: ".taskboard-task-inner",
dropOnEmpty: true
connectWith: ".taskboard-tasks-box"
revert: 400
})
oldUsId = if oldParentScope.us then oldParentScope.us.id else null
oldStatusId = oldParentScope.st.id
newUsId = if newParentScope.us then newParentScope.us.id else null
newStatusId = newParentScope.st.id
tdom.on "sortstop", (event, ui) ->
parentEl = ui.item.parent()
itemEl = ui.item
itemTask = itemEl.scope().task
itemIndex = itemEl.index()
newParentScope = parentEl.scope()
if newStatusId != oldStatusId or newUsId != oldUsId
deleteElement(itemEl)
oldUsId = if oldParentScope.us then oldParentScope.us.id else null
oldStatusId = oldParentScope.st.id
newUsId = if newParentScope.us then newParentScope.us.id else null
newStatusId = newParentScope.st.id
$scope.$apply ->
$rootscope.$broadcast("taskboard:task:move", itemTask, newUsId, newStatusId, itemIndex)
if newStatusId != oldStatusId or newUsId != oldUsId
deleteElement(itemEl)
ui.item.find('a').removeClass('noclick')
$scope.$apply ->
$rootscope.$broadcast("taskboard:task:move", itemTask, newUsId, newStatusId, itemIndex)
tdom.on "sortstart", (event, ui) ->
oldParentScope = ui.item.parent().scope()
ui.item.find('a').addClass('noclick')
ui.item.find('a').removeClass('noclick')
tdom.on "sortstart", (event, ui) ->
oldParentScope = ui.item.parent().scope()
ui.item.find('a').addClass('noclick')
$scope.$on "$destroy", ->
$el.off()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -70,6 +70,9 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
loadProject: ->
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.project = project
@scope.$emit('project:loaded', project)
@ -159,9 +162,6 @@ WikiSummaryDirective = ($log, $template) ->
return if not wikiPage
render(wikiPage)
$scope.$on "wiki:edit", (event, wikiPage) ->
render(wikiPage)
$scope.$on "$destroy", ->
$el.off()
@ -215,8 +215,7 @@ EditableWikiContentDirective = ($window, $document, $repo, $confirm, $loading, $
if not wiki.id?
$analytics.trackEvent("wikipage", "create", "create wiki page", 1)
$model.$modelValue = wikiPage
$scope.$broadcast("wiki:edit", wikiPage)
$model.$setViewValue wikiPage
$confirm.notify("success")
switchToReadMode()
@ -235,23 +234,16 @@ EditableWikiContentDirective = ($window, $document, $repo, $confirm, $loading, $
$loading.finish($el.find('.save-container'))
$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)
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()
if target.is('pre')
prevPos = target.data("scroll-pos")
target.data("scroll-pos", null)
if prevPos != target[0].scrollLeft
return
return if not isEditable()
return if target.is('a')
return if target.is('pre')
switchToEditMode()

View File

@ -9,7 +9,7 @@ div.wrapper.memberships(ng-controller="MembershipsController as ctrl",
include ../includes/components/mainTitle
.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
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.
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.spin.hidden
img(src="/svg/spinner-circle.svg", alt="loading...")
h3.result-title
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
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")
button(type="submit", class="hidden")
a.button.button-green.submit-button(href="", title="Save") Save
button.button-green.submit-button(type="submit", 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",
ng-model="project.description", data-required="true")
tg-privacy-settings-inputs
div
div.privacy-settings
div
input.hidden(type="radio", disabled="disabled")
label.button(for="public-project") Public Project
input.privacy-project(type="radio", name="private-project", ng-model="project.is_private", ng-value="false")
label.trans-button(for="public-project")
span Public Project
div
input.hidden(type="radio", checked="checked", disabled="disabled")
label.button(for="private-project") Private Project
input.privacy-project(type="radio", name="private-project", ng-model="project.is_private", ng-value="true")
label.trans-button(for="private-project")
span Private Project
p All projects are private during Taiga's beta period.
button(type="submit", class="hidden")
a.button.button-green.submit-button(href="", title="Save") Save
button.button-green.submit-button(type="submit", title="Save") Save
a.delete-project(href="", title="Delete this project", ng-click="ctrl.openDeleteLightbox()") Delete this 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,27 +8,33 @@ div.wrapper.roles(ng-controller="RolesController as ctrl",
section.main.admin-roles.admin-common
.header-with-actions
include ../includes/components/mainTitle
.action-buttons
a.button.button-red.delete-role(href="", title="Delete", ng-click="ctrl.delete()") Delete
.action-buttons(ng-if="!role.external_user")
a.button-red.delete-role(href="", title="Delete", ng-click="ctrl.delete()")
span Delete
div(tg-edit-role)
.edit-role
input(type="text", value="{{ role.name }}")
a.save.icon.icon-floppy(href="", title="Save")
div(ng-if="!role.external_user")
div(tg-edit-role)
.edit-role
input(type="text", value="{{ role.name }}")
a.save.icon.icon-floppy(href="", title="Save")
p.total
span.role-name(title="{{ role.members_count }} members with this role") {{ role.name }}
a.edit-value.icon.icon-edit
div.any-computable-role(ng-hide="anyComputableRole") Be careful, no role in your project will be able to estimate the point value for user stories
div.general-category
| When enabled, members assigned to this role will be able to estimate the point value for user stories
div.check
input(type="checkbox", ng-model="role.computable", ng-change="ctrl.setComputable()")
div
span.check-text.check-yes Yes
span.check-text.check-no No
div(ng-if="role.external_user")
p.total
span.role-name(title="{{ role.members_count }} members with this role") {{ role.name }}
a.edit-value.icon.icon-edit
div.any-computable-role(ng-hide="anyComputableRole") Be careful, no role in your project will be able to estimate the point value for user stories
div.general-category
| When enabled, members assigned to this role will be able to estimate the point value for user stories
div.check
input(type="checkbox", ng-model="role.computable", ng-change="ctrl.setComputable()")
div
span.check-text.check-yes Yes
span.check-text.check-no No
span.role-name {{ role.name }}
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 ,)
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")
a.button.button-green.submit-button(href="", title="Save") Save
button.button-green.submit-button(type="submit", title="Save") Save
a.help-button(href="https://taiga.io/support/bitbucket-integration/", target="_blank")
span.icon.icon-help

View File

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

View File

@ -31,8 +31,7 @@ block content
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")
button(type="submit", class="hidden")
a.button.button-green.submit-button(href="", title="Save") Save
button.button-green.submit-button(type="submit", title="Save") Save
a.help-button(href="https://taiga.io/support/gitlab-integration/", target="_blank")
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....
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
div.table-header

View File

@ -11,7 +11,7 @@ section.attachments
input(id="add-attach", type="file", multiple="multiple")
.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")
.attachment-name

View File

@ -9,16 +9,17 @@ div.wrapper(tg-backlog, ng-controller="BacklogController as ctrl",
div.burndown(tg-gm-backlog-graph)
include ../includes/modules/burndown
div.backlog-menu
a.trans-button.move-to-current-sprint(href="", title="Move to Current Sprint",
id="move-to-current-sprint")
span.icon.icon-move
span.text Move to current Sprint
a.trans-button(href="", title="Show Filters", id="show-filters-button")
span.icon.icon-filter
span.text Show Filters
a.trans-button(href="", title="Show Tags", id="show-tags")
span.icon.icon-tag
span.text Show Tags
div.backlog-table-options
a.trans-button.move-to-current-sprint(href="", title="Move to Current Sprint",
id="move-to-current-sprint")
span.icon.icon-move
span.text Move to current Sprint
a.trans-button(href="", title="Show Filters", id="show-filters-button")
span.icon.icon-filter
span.text Show Filters
a.trans-button(href="", title="Show Tags", id="show-tags")
span.icon.icon-tag
span.text Show Tags
include ../includes/components/addnewus
section.backlog-table(ng-class="{'hidden': !visibleUserstories.length}")
include ../includes/modules/backlog-table

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-red item-unblock") Unblock
a(href="#", class="button button-gray item-block")
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,8 +11,7 @@
<% if(watcher) { %>
.watcher-single
.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
span <%- watcher.full_name_display %>
@ -20,4 +19,4 @@
a.icon.icon-delete(data-watcher-id!="<%- watcher.id %>" href="" title="delete-watcher")
<% }; %>
<% } %>
<% }); %>
<% }); %>

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.role total
<% _.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.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) { %>
li
<% 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
.comments-list
div(tg-check-permission!="modify_<%- type %>", tg-toggle-comment, class="add-comment")
textarea(placeholder="Type a new comment here",
ng-model!="<%- ngmodel %>.comment", tg-markitup="tg-markitup")
textarea(placeholder="Type a new comment here", ng-model!="<%- ngmodel %>.comment", tg-markitup="tg-markitup")
<% if (mode !== "edit") { %>
a(class="help-markdown", href="https://taiga.io/support/taiga-markdown-syntax/", target="_blank", title="Mardown syntax help")
span.icon.icon-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
.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
textarea.reason(placeholder="Please explain the reason")
a.button.button-green(href="")
a.button-green(href="")
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
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')",
tg-check-permission="add_us")
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')",
tg-check-permission="add_us")
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.tags-block(tg-colorize-tags="us.tags", tg-colorize-tags-type="backlog")
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")
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")

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
div.summary-progress-bar
div.current-progress
div.data
span.number 30%
span.description completed
ul
li
div.large-summary-wrapper
div
div.summary-progress-bar
div.current-progress
div.data
span.number 30%
span.description completed
div.summary-stats
span.number 12
span.description project<br />points
li
div.summary-stats
span.number 23
span.description defined<br />points
li
div.summary-stats
span.number 12
span.description assigned<br />points
li
div.summary-stats.summary-stats-divider
span.number 23
span.description closed<br />points
ul
li
div.summary-stats
span.icon.icon-bulk
span.number 73
span.description created<br />tasks
li
div.summary-stats
span.number 72
span.description closed<br />tasks
li
div.summary-stats
span.number 18
span.description remaining<br />tasks
ul
li
div.summary-stats
span.icon.icon-iocaine
span.number 10
span.description iocanie<br />doses

View File

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

View File

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

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