commit
3be01527da
14
AUTHORS.rst
14
AUTHORS.rst
|
@ -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
|
||||
|
|
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Taiga Front #
|
||||
|
||||

|
||||
[](https://taiga.io "Managed with Taiga")
|
||||
|
||||
## Get the compiled version ##
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ window.taigaConfig = {
|
|||
"privacyPolicyUrl": null,
|
||||
"termsOfServiceUrl": null,
|
||||
"maxUploadFileSize": null,
|
||||
"gitHubClientId": null,
|
||||
"contribPlugins": []
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
])
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -58,6 +58,7 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm) ->
|
|||
|
||||
$el.sortable({
|
||||
items: ".us-item-row",
|
||||
cancel: ".popover"
|
||||
connectWith: ".sprint"
|
||||
containment: ".wrapper"
|
||||
dropOnEmpty: true
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -28,6 +28,10 @@ locationFactory = ($location, $route, $rootscope) ->
|
|||
un()
|
||||
|
||||
return $location
|
||||
|
||||
$location.isInCurrentRouteParams = (name, value) ->
|
||||
return $route.current.params[name] == value
|
||||
|
||||
return $location
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
#############################################################################
|
||||
|
|
|
@ -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])
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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])
|
|
@ -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])
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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])
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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])
|
|
@ -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])
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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", ->
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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?
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 %>
|
||||
<% } %>
|
||||
<% }); %>
|
|
@ -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
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
a(href="", class="button button-red") Delete
|
||||
a(href="", class="button button-red")
|
||||
span Delete
|
||||
|
|
|
@ -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")
|
||||
<% }; %>
|
||||
<% } %>
|
||||
<% }); %>
|
||||
<% }); %>
|
||||
|
|
|
@ -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 %>
|
||||
<% }); %>
|
|
@ -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 %>
|
||||
<% } %>
|
||||
<% }); %>
|
|
@ -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 %>
|
||||
<% }); %>
|
||||
|
|
|
@ -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) { %>
|
||||
|
|
|
@ -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")
|
||||
<% } %>
|
|
@ -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
|
||||
|
|
|
@ -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 %>
|
||||
<% } %>
|
|
@ -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
|
||||
|
|
|
@ -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")
|
|
@ -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")
|
||||
<% } %>
|
|
@ -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 %>")
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
|
@ -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")
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue