cards & filters ui refactor
parent
e6ef8ffa34
commit
59bf55fc30
|
@ -23,6 +23,10 @@
|
||||||
- Add Wiki history
|
- Add Wiki history
|
||||||
- Third party integrations:
|
- Third party integrations:
|
||||||
- Included gogs as builtin integration.
|
- Included gogs as builtin integration.
|
||||||
|
- Filters refactor
|
||||||
|
- Cards ui refactor with zoom
|
||||||
|
- Kanban filters
|
||||||
|
- Taskboard filters
|
||||||
|
|
||||||
### Misc
|
### Misc
|
||||||
- Lots of small and not so small bugfixes.
|
- Lots of small and not so small bugfixes.
|
||||||
|
|
|
@ -1,185 +0,0 @@
|
||||||
###
|
|
||||||
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
|
||||||
# Copyright (C) 2014-2016 Jesús Espino Garcia <jespinog@gmail.com>
|
|
||||||
# Copyright (C) 2014-2016 David Barragán Merino <bameda@dbarragan.com>
|
|
||||||
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
|
||||||
# Copyright (C) 2014-2016 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
|
|
||||||
# Copyright (C) 2014-2016 Xavi Julian <xavier.julian@kaleidos.net>
|
|
||||||
#
|
|
||||||
# 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/backlog/main.coffee
|
|
||||||
###
|
|
||||||
|
|
||||||
taiga = @.taiga
|
|
||||||
|
|
||||||
mixOf = @.taiga.mixOf
|
|
||||||
toggleText = @.taiga.toggleText
|
|
||||||
scopeDefer = @.taiga.scopeDefer
|
|
||||||
bindOnce = @.taiga.bindOnce
|
|
||||||
groupBy = @.taiga.groupBy
|
|
||||||
debounceLeading = @.taiga.debounceLeading
|
|
||||||
|
|
||||||
|
|
||||||
module = angular.module("taigaBacklog")
|
|
||||||
|
|
||||||
#############################################################################
|
|
||||||
## Issues Filters Directive
|
|
||||||
#############################################################################
|
|
||||||
|
|
||||||
BacklogFiltersDirective = ($q, $log, $location, $template, $compile) ->
|
|
||||||
template = $template.get("backlog/filters.html", true)
|
|
||||||
templateSelected = $template.get("backlog/filter-selected.html", true)
|
|
||||||
|
|
||||||
link = ($scope, $el, $attrs) ->
|
|
||||||
currentFiltersType = ''
|
|
||||||
|
|
||||||
$ctrl = $el.closest(".wrapper").controller()
|
|
||||||
selectedFilters = []
|
|
||||||
|
|
||||||
showFilters = (title, type) ->
|
|
||||||
$el.find(".filters-cats").hide()
|
|
||||||
$el.find(".filter-list").removeClass("hidden")
|
|
||||||
$el.find("h2.breadcrumb").removeClass("hidden")
|
|
||||||
$el.find("h2 a.subfilter span.title").html(title)
|
|
||||||
$el.find("h2 a.subfilter span.title").prop("data-type", type)
|
|
||||||
|
|
||||||
currentFiltersType = getFiltersType()
|
|
||||||
|
|
||||||
showCategories = ->
|
|
||||||
$el.find(".filters-cats").show()
|
|
||||||
$el.find(".filter-list").addClass("hidden")
|
|
||||||
$el.find("h2.breadcrumb").addClass("hidden")
|
|
||||||
|
|
||||||
initializeSelectedFilters = () ->
|
|
||||||
showCategories()
|
|
||||||
selectedFilters = []
|
|
||||||
|
|
||||||
for name, values of $scope.filters
|
|
||||||
for val in values
|
|
||||||
selectedFilters.push(val) if val.selected
|
|
||||||
|
|
||||||
renderSelectedFilters()
|
|
||||||
|
|
||||||
renderSelectedFilters = ->
|
|
||||||
_.map selectedFilters, (f) =>
|
|
||||||
if f.color
|
|
||||||
f.style = "border-left: 3px solid #{f.color}"
|
|
||||||
|
|
||||||
html = templateSelected({filters: selectedFilters})
|
|
||||||
html = $compile(html)($scope)
|
|
||||||
|
|
||||||
$el.find(".filters-applied").html(html)
|
|
||||||
|
|
||||||
renderFilters = (filters) ->
|
|
||||||
_.map filters, (f) =>
|
|
||||||
if f.color
|
|
||||||
f.style = "border-left: 3px solid #{f.color}"
|
|
||||||
|
|
||||||
html = template({filters:filters})
|
|
||||||
html = $compile(html)($scope)
|
|
||||||
$el.find(".filter-list").html(html)
|
|
||||||
|
|
||||||
getFiltersType = () ->
|
|
||||||
return $el.find("h2 a.subfilter span.title").prop('data-type')
|
|
||||||
|
|
||||||
reloadUserstories = () ->
|
|
||||||
currentFiltersType = getFiltersType()
|
|
||||||
|
|
||||||
$q.all([$ctrl.loadUserstories(true), $ctrl.generateFilters()]).then () ->
|
|
||||||
currentFilters = $scope.filters[currentFiltersType]
|
|
||||||
renderFilters(_.reject(currentFilters, "selected"))
|
|
||||||
|
|
||||||
toggleFilterSelection = (type, id) ->
|
|
||||||
currentFiltersType = getFiltersType()
|
|
||||||
|
|
||||||
filters = $scope.filters[type]
|
|
||||||
filter = _.find(filters, {id: id})
|
|
||||||
filter.selected = (not filter.selected)
|
|
||||||
|
|
||||||
if filter.selected
|
|
||||||
selectedFilters.push(filter)
|
|
||||||
$scope.$apply ->
|
|
||||||
$ctrl.selectFilter(type, id)
|
|
||||||
else
|
|
||||||
selectedFilters = _.reject selectedFilters, (selected) ->
|
|
||||||
return filter.type == selected.type && filter.id == selected.id
|
|
||||||
|
|
||||||
$ctrl.unselectFilter(type, id)
|
|
||||||
|
|
||||||
renderSelectedFilters(selectedFilters)
|
|
||||||
|
|
||||||
if type == currentFiltersType
|
|
||||||
renderFilters(_.reject(filters, "selected"))
|
|
||||||
|
|
||||||
reloadUserstories()
|
|
||||||
|
|
||||||
selectQFilter = debounceLeading 100, (value) ->
|
|
||||||
return if value is undefined
|
|
||||||
|
|
||||||
if value.length == 0
|
|
||||||
$ctrl.replaceFilter("q", null)
|
|
||||||
else
|
|
||||||
$ctrl.replaceFilter("q", value)
|
|
||||||
|
|
||||||
reloadUserstories()
|
|
||||||
|
|
||||||
$scope.$watch("filtersQ", selectQFilter)
|
|
||||||
|
|
||||||
## Angular Watchers
|
|
||||||
$scope.$on "backlog:loaded", (ctx) ->
|
|
||||||
initializeSelectedFilters()
|
|
||||||
|
|
||||||
$scope.$on "filters:update", (ctx) ->
|
|
||||||
$ctrl.generateFilters().then () ->
|
|
||||||
filters = $scope.filters[currentFiltersType]
|
|
||||||
|
|
||||||
if currentFiltersType
|
|
||||||
renderFilters(_.reject(filters, "selected"))
|
|
||||||
|
|
||||||
## Dom Event Handlers
|
|
||||||
$el.on "click", ".filters-cats > ul > li > a", (event) ->
|
|
||||||
event.preventDefault()
|
|
||||||
target = angular.element(event.currentTarget)
|
|
||||||
tags = $scope.filters[target.data("type")]
|
|
||||||
|
|
||||||
renderFilters(_.reject(tags, "selected"))
|
|
||||||
showFilters(target.attr("title"), target.data('type'))
|
|
||||||
|
|
||||||
$el.on "click", ".filters-inner > .filters-step-cat > .breadcrumb > .back", (event) ->
|
|
||||||
event.preventDefault()
|
|
||||||
showCategories()
|
|
||||||
|
|
||||||
$el.on "click", ".remove-filter", (event) ->
|
|
||||||
event.preventDefault()
|
|
||||||
target = angular.element(event.currentTarget).parent()
|
|
||||||
id = target.data("id")
|
|
||||||
type = target.data("type")
|
|
||||||
toggleFilterSelection(type, id)
|
|
||||||
|
|
||||||
$el.on "click", ".filter-list .single-filter", (event) ->
|
|
||||||
event.preventDefault()
|
|
||||||
target = angular.element(event.currentTarget)
|
|
||||||
if target.hasClass("active")
|
|
||||||
target.removeClass("active")
|
|
||||||
else
|
|
||||||
target.addClass("active")
|
|
||||||
|
|
||||||
id = target.data("id")
|
|
||||||
type = target.data("type")
|
|
||||||
toggleFilterSelection(type, id)
|
|
||||||
|
|
||||||
return {link:link}
|
|
||||||
|
|
||||||
module.directive("tgBacklogFilters", ["$q", "$log", "$tgLocation", "$tgTemplate", "$compile", BacklogFiltersDirective])
|
|
|
@ -39,7 +39,7 @@ module = angular.module("taigaBacklog")
|
||||||
## Backlog Controller
|
## Backlog Controller
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
|
||||||
class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin)
|
class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin, taiga.UsFiltersMixin)
|
||||||
@.$inject = [
|
@.$inject = [
|
||||||
"$scope",
|
"$scope",
|
||||||
"$rootScope",
|
"$rootScope",
|
||||||
|
@ -57,18 +57,30 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
||||||
"$tgLoading",
|
"$tgLoading",
|
||||||
"tgResources",
|
"tgResources",
|
||||||
"$tgQueueModelTransformation",
|
"$tgQueueModelTransformation",
|
||||||
"tgErrorHandlingService"
|
"tgErrorHandlingService",
|
||||||
|
"$tgStorage",
|
||||||
|
"tgFilterRemoteStorageService"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
storeCustomFiltersName: 'backlog-custom-filters'
|
||||||
|
storeFiltersName: 'backlog-filters'
|
||||||
|
backlogOrder: {}
|
||||||
|
milestonesOrder: {}
|
||||||
|
|
||||||
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @appMetaService, @navUrls,
|
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @appMetaService, @navUrls,
|
||||||
@events, @analytics, @translate, @loading, @rs2, @modelTransform, @errorHandlingService) ->
|
@events, @analytics, @translate, @loading, @rs2, @modelTransform, @errorHandlingService, @storage, @filterRemoteStorageService) ->
|
||||||
bindMethods(@)
|
bindMethods(@)
|
||||||
|
|
||||||
|
@.backlogOrder = {}
|
||||||
|
@.milestonesOrder = {}
|
||||||
|
|
||||||
@.page = 1
|
@.page = 1
|
||||||
@.disablePagination = false
|
@.disablePagination = false
|
||||||
@.firstLoadComplete = false
|
@.firstLoadComplete = false
|
||||||
@scope.userstories = []
|
@scope.userstories = []
|
||||||
|
|
||||||
|
return if @.applyStoredFilters(@params.pslug, "backlog-filters")
|
||||||
|
|
||||||
@scope.sectionName = @translate.instant("BACKLOG.SECTION_NAME")
|
@scope.sectionName = @translate.instant("BACKLOG.SECTION_NAME")
|
||||||
@showTags = false
|
@showTags = false
|
||||||
@activeFilters = false
|
@activeFilters = false
|
||||||
|
@ -97,6 +109,9 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
||||||
# On Error
|
# On Error
|
||||||
promise.then null, @.onInitialDataError.bind(@)
|
promise.then null, @.onInitialDataError.bind(@)
|
||||||
|
|
||||||
|
filtersReloadContent: () ->
|
||||||
|
@.loadUserstories(true)
|
||||||
|
|
||||||
initializeEventHandlers: ->
|
initializeEventHandlers: ->
|
||||||
@scope.$on "usform:bulk:success", =>
|
@scope.$on "usform:bulk:success", =>
|
||||||
@.loadUserstories(true)
|
@.loadUserstories(true)
|
||||||
|
@ -175,6 +190,12 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
||||||
@scope.showGraphPlaceholder = !(stats.total_points? && stats.total_milestones?)
|
@scope.showGraphPlaceholder = !(stats.total_points? && stats.total_milestones?)
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
|
setMilestonesOrder: (sprints) ->
|
||||||
|
for sprint in sprints
|
||||||
|
@.milestonesOrder[sprint.id] = {}
|
||||||
|
for it in sprint.user_stories
|
||||||
|
@.milestonesOrder[sprint.id][it.id] = it.sprint_order
|
||||||
|
|
||||||
unloadClosedSprints: ->
|
unloadClosedSprints: ->
|
||||||
@scope.$apply =>
|
@scope.$apply =>
|
||||||
@scope.closedSprints = []
|
@scope.closedSprints = []
|
||||||
|
@ -185,6 +206,8 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
||||||
return @rs.sprints.list(@scope.projectId, params).then (result) =>
|
return @rs.sprints.list(@scope.projectId, params).then (result) =>
|
||||||
sprints = result.milestones
|
sprints = result.milestones
|
||||||
|
|
||||||
|
@.setMilestonesOrder(sprints)
|
||||||
|
|
||||||
@scope.totalClosedMilestones = result.closed
|
@scope.totalClosedMilestones = result.closed
|
||||||
|
|
||||||
# NOTE: Fix order of USs because the filter orderBy does not work propertly in partials files
|
# NOTE: Fix order of USs because the filter orderBy does not work propertly in partials files
|
||||||
|
@ -200,6 +223,8 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
||||||
return @rs.sprints.list(@scope.projectId, params).then (result) =>
|
return @rs.sprints.list(@scope.projectId, params).then (result) =>
|
||||||
sprints = result.milestones
|
sprints = result.milestones
|
||||||
|
|
||||||
|
@.setMilestonesOrder(sprints)
|
||||||
|
|
||||||
@scope.totalMilestones = sprints
|
@scope.totalMilestones = sprints
|
||||||
@scope.totalClosedMilestones = result.closed
|
@scope.totalClosedMilestones = result.closed
|
||||||
@scope.totalOpenMilestones = result.open
|
@scope.totalOpenMilestones = result.open
|
||||||
|
@ -221,47 +246,6 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
||||||
|
|
||||||
return sprints
|
return sprints
|
||||||
|
|
||||||
restoreFilters: ->
|
|
||||||
selectedTags = @scope.oldSelectedTags
|
|
||||||
selectedStatuses = @scope.oldSelectedStatuses
|
|
||||||
|
|
||||||
return if !selectedStatuses and !selectedStatuses
|
|
||||||
|
|
||||||
@scope.filtersQ = @scope.filtersQOld
|
|
||||||
|
|
||||||
@.replaceFilter("q", @scope.filtersQ)
|
|
||||||
|
|
||||||
_.each [selectedTags, selectedStatuses], (filterGrp) =>
|
|
||||||
_.each filterGrp, (item) =>
|
|
||||||
filters = @scope.filters[item.type]
|
|
||||||
filter = _.find(filters, {id: item.id})
|
|
||||||
filter.selected = true
|
|
||||||
|
|
||||||
@.selectFilter(item.type, item.id)
|
|
||||||
|
|
||||||
@.loadUserstories()
|
|
||||||
|
|
||||||
resetFilters: ->
|
|
||||||
selectedTags = _.filter(@scope.filters.tags, "selected")
|
|
||||||
selectedStatuses = _.filter(@scope.filters.status, "selected")
|
|
||||||
|
|
||||||
@scope.oldSelectedTags = selectedTags
|
|
||||||
@scope.oldSelectedStatuses = selectedStatuses
|
|
||||||
|
|
||||||
@scope.filtersQOld = @scope.filtersQ
|
|
||||||
@scope.filtersQ = undefined
|
|
||||||
@.replaceFilter("q", @scope.filtersQ)
|
|
||||||
|
|
||||||
_.each [selectedTags, selectedStatuses], (filterGrp) =>
|
|
||||||
_.each filterGrp, (item) =>
|
|
||||||
filters = @scope.filters[item.type]
|
|
||||||
filter = _.find(filters, {id: item.id})
|
|
||||||
filter.selected = false
|
|
||||||
|
|
||||||
@.unselectFilter(item.type, item.id)
|
|
||||||
|
|
||||||
@.loadUserstories()
|
|
||||||
|
|
||||||
loadAllPaginatedUserstories: () ->
|
loadAllPaginatedUserstories: () ->
|
||||||
page = @.page
|
page = @.page
|
||||||
|
|
||||||
|
@ -273,15 +257,15 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
||||||
|
|
||||||
@.loadingUserstories = true
|
@.loadingUserstories = true
|
||||||
@.disablePagination = true
|
@.disablePagination = true
|
||||||
@scope.httpParams = @.getUrlFilters()
|
params = _.clone(@location.search())
|
||||||
@rs.userstories.storeQueryParams(@scope.projectId, @scope.httpParams)
|
@rs.userstories.storeQueryParams(@scope.projectId, params)
|
||||||
|
|
||||||
if resetPagination
|
if resetPagination
|
||||||
@.page = 1
|
@.page = 1
|
||||||
|
|
||||||
@scope.httpParams.page = @.page
|
params.page = @.page
|
||||||
|
|
||||||
promise = @rs.userstories.listUnassigned(@scope.projectId, @scope.httpParams, pageSize)
|
promise = @rs.userstories.listUnassigned(@scope.projectId, params, pageSize)
|
||||||
|
|
||||||
return promise.then (result) =>
|
return promise.then (result) =>
|
||||||
userstories = result[0]
|
userstories = result[0]
|
||||||
|
@ -293,7 +277,8 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
||||||
# NOTE: Fix order of USs because the filter orderBy does not work propertly in the partials files
|
# NOTE: Fix order of USs because the filter orderBy does not work propertly in the partials files
|
||||||
@scope.userstories = @scope.userstories.concat(_.sortBy(userstories, "backlog_order"))
|
@scope.userstories = @scope.userstories.concat(_.sortBy(userstories, "backlog_order"))
|
||||||
|
|
||||||
@.setSearchDataFilters()
|
for it in @scope.userstories
|
||||||
|
@.backlogOrder[it.id] = it.backlog_order
|
||||||
|
|
||||||
@.loadingUserstories = false
|
@.loadingUserstories = false
|
||||||
|
|
||||||
|
@ -354,242 +339,142 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
# --move us api behavior--
|
||||||
|
# if your are moving multiples USs you must use the bulk api
|
||||||
|
# if there is only one US you must use patch (repo.save)
|
||||||
|
# the new US position is the position of the previous US + 1
|
||||||
|
# if the previous US has a position value that it is equal to other USs, you must send all the USs with that position value only if they are before of the target position
|
||||||
|
# with this USs if it's a patch you must add them to the header, if is a bulk you must send them with the other USs
|
||||||
moveUs: (ctx, usList, newUsIndex, newSprintId) ->
|
moveUs: (ctx, usList, newUsIndex, newSprintId) ->
|
||||||
oldSprintId = usList[0].milestone
|
oldSprintId = usList[0].milestone
|
||||||
project = usList[0].project
|
project = usList[0].project
|
||||||
|
|
||||||
movedFromClosedSprint = false
|
if oldSprintId
|
||||||
movedToClosedSprint = false
|
sprint = @scope.sprintsById[oldSprintId] || @scope.closedSprintsById[oldSprintId]
|
||||||
|
|
||||||
sprint = @scope.sprintsById[oldSprintId]
|
if newSprintId
|
||||||
|
newSprint = @scope.sprintsById[newSprintId] || @scope.closedSprintsById[newSprintId]
|
||||||
|
|
||||||
# Move from closed sprint
|
currentSprintId = if newSprintId != oldSprintId then newSprintId else oldSprintId
|
||||||
if !sprint && @scope.closedSprintsById
|
|
||||||
sprint = @scope.closedSprintsById[oldSprintId]
|
|
||||||
movedFromClosedSprint = true if sprint
|
|
||||||
|
|
||||||
newSprint = @scope.sprintsById[newSprintId]
|
orderList = null
|
||||||
|
orderField = ""
|
||||||
|
|
||||||
# Move to closed sprint
|
if newSprintId != oldSprintId
|
||||||
if !newSprint && newSprintId
|
if newSprintId == null # From sprint to backlog
|
||||||
newSprint = @scope.closedSprintsById[newSprintId]
|
for us, key in usList # delete from sprint userstories
|
||||||
movedToClosedSprint = true if newSprint
|
_.remove sprint.user_stories, (it) -> it.id == us.id
|
||||||
|
|
||||||
# In the same sprint or in the backlog
|
orderField = "backlog_order"
|
||||||
if newSprintId == oldSprintId
|
orderList = @.backlogOrder
|
||||||
items = null
|
|
||||||
userstories = null
|
|
||||||
|
|
||||||
if newSprintId == null
|
beforeDestination = _.slice(@scope.userstories, 0, newUsIndex)
|
||||||
userstories = @scope.userstories
|
afterDestination = _.slice(@scope.userstories, newUsIndex)
|
||||||
else
|
|
||||||
userstories = newSprint.user_stories
|
|
||||||
|
|
||||||
@scope.$apply ->
|
@scope.userstories = @scope.userstories.concat(usList)
|
||||||
for us, key in usList
|
else # From backlog to sprint
|
||||||
r = userstories.indexOf(us)
|
for us in usList # delete from sprint userstories
|
||||||
userstories.splice(r, 1)
|
_.remove @scope.userstories, (it) -> it.id == us.id
|
||||||
|
|
||||||
args = [newUsIndex, 0].concat(usList)
|
orderField = "sprint_order"
|
||||||
Array.prototype.splice.apply(userstories, args)
|
orderList = @.milestonesOrder[newSprint.id]
|
||||||
|
|
||||||
# If in backlog
|
beforeDestination = _.slice(newSprint.user_stories, 0, newUsIndex)
|
||||||
if newSprintId == null
|
afterDestination = _.slice(newSprint.user_stories, newUsIndex)
|
||||||
# Rehash userstories order field
|
|
||||||
|
|
||||||
items = @.resortUserStories(userstories, "backlog_order")
|
newSprint.user_stories = newSprint.user_stories.concat(usList)
|
||||||
data = @.prepareBulkUpdateData(items, "backlog_order")
|
|
||||||
|
|
||||||
# Persist in bulk all affected
|
|
||||||
# userstories with order change
|
|
||||||
@rs.userstories.bulkUpdateBacklogOrder(project, data).then =>
|
|
||||||
@rootscope.$broadcast("sprint:us:moved")
|
|
||||||
|
|
||||||
# For sprint
|
|
||||||
else
|
|
||||||
# Rehash userstories order field
|
|
||||||
items = @.resortUserStories(userstories, "sprint_order")
|
|
||||||
data = @.prepareBulkUpdateData(items, "sprint_order")
|
|
||||||
|
|
||||||
# Persist in bulk all affected
|
|
||||||
# userstories with order change
|
|
||||||
@rs.userstories.bulkUpdateSprintOrder(project, data).then =>
|
|
||||||
@rootscope.$broadcast("sprint:us:moved")
|
|
||||||
|
|
||||||
return promise
|
|
||||||
|
|
||||||
# From sprint to backlog
|
|
||||||
if newSprintId == null
|
|
||||||
us.milestone = null for us in usList
|
|
||||||
|
|
||||||
@scope.$apply =>
|
|
||||||
# Add new us to backlog userstories list
|
|
||||||
# @scope.userstories.splice(newUsIndex, 0, us)
|
|
||||||
args = [newUsIndex, 0].concat(usList)
|
|
||||||
Array.prototype.splice.apply(@scope.userstories, args)
|
|
||||||
|
|
||||||
for us, key in usList
|
|
||||||
r = sprint.user_stories.indexOf(us)
|
|
||||||
sprint.user_stories.splice(r, 1)
|
|
||||||
|
|
||||||
# Persist the milestone change of userstory
|
|
||||||
promise = @repo.save(us)
|
|
||||||
|
|
||||||
# Rehash userstories order field
|
|
||||||
# and persist in bulk all changes.
|
|
||||||
promise = promise.then =>
|
|
||||||
items = @.resortUserStories(@scope.userstories, "backlog_order")
|
|
||||||
data = @.prepareBulkUpdateData(items, "backlog_order")
|
|
||||||
return @rs.userstories.bulkUpdateBacklogOrder(us.project, data).then =>
|
|
||||||
@rootscope.$broadcast("sprint:us:moved")
|
|
||||||
|
|
||||||
if movedFromClosedSprint
|
|
||||||
@rootscope.$broadcast("backlog:load-closed-sprints")
|
|
||||||
|
|
||||||
promise.then null, ->
|
|
||||||
console.log "FAIL" # TODO
|
|
||||||
|
|
||||||
return promise
|
|
||||||
|
|
||||||
# From backlog to sprint
|
|
||||||
if oldSprintId == null
|
|
||||||
us.milestone = newSprintId for us in usList
|
|
||||||
args = [newUsIndex, 0].concat(usList)
|
|
||||||
|
|
||||||
# Add moving us to sprint user stories list
|
|
||||||
Array.prototype.splice.apply(newSprint.user_stories, args)
|
|
||||||
|
|
||||||
# Remove moving us from backlog userstories lists.
|
|
||||||
for us, key in usList
|
|
||||||
r = @scope.userstories.indexOf(us)
|
|
||||||
@scope.userstories.splice(r, 1)
|
|
||||||
|
|
||||||
# From sprint to sprint
|
|
||||||
else
|
else
|
||||||
us.milestone = newSprintId for us in usList
|
if oldSprintId == null # backlog
|
||||||
|
orderField = "backlog_order"
|
||||||
|
orderList = @.backlogOrder
|
||||||
|
|
||||||
@scope.$apply =>
|
list = _.filter @scope.userstories, (listIt) -> # Remove moved US from list
|
||||||
args = [newUsIndex, 0].concat(usList)
|
return !_.find usList, (moveIt) -> return listIt.id == moveIt.id
|
||||||
|
|
||||||
# Add new us to backlog userstories list
|
beforeDestination = _.slice(list, 0, newUsIndex)
|
||||||
Array.prototype.splice.apply(newSprint.user_stories, args)
|
afterDestination = _.slice(list, newUsIndex)
|
||||||
|
else # sprint
|
||||||
|
orderField = "sprint_order"
|
||||||
|
orderList = @.milestonesOrder[sprint.id]
|
||||||
|
|
||||||
# Remove the us from the sprint list.
|
list = _.filter newSprint.user_stories, (listIt) -> # Remove moved US from list
|
||||||
for us in usList
|
return !_.find usList, (moveIt) -> return listIt.id == moveIt.id
|
||||||
r = sprint.user_stories.indexOf(us)
|
|
||||||
sprint.user_stories.splice(r, 1)
|
|
||||||
|
|
||||||
#Persist the milestone change of userstory
|
beforeDestination = _.slice(list, 0, newUsIndex)
|
||||||
promises = _.map usList, (us) => @repo.save(us)
|
afterDestination = _.slice(list, newUsIndex)
|
||||||
|
|
||||||
#Rehash userstories order field
|
# previous us
|
||||||
#and persist in bulk all changes.
|
previous = beforeDestination[beforeDestination.length - 1]
|
||||||
promise = @q.all(promises).then =>
|
|
||||||
items = @.resortUserStories(newSprint.user_stories, "sprint_order")
|
|
||||||
data = @.prepareBulkUpdateData(items, "sprint_order")
|
|
||||||
|
|
||||||
@rs.userstories.bulkUpdateSprintOrder(project, data).then (result) =>
|
# this will store the previous us with the same position
|
||||||
@rootscope.$broadcast("sprint:us:moved")
|
setPreviousOrders = []
|
||||||
|
|
||||||
@rs.userstories.bulkUpdateBacklogOrder(project, data).then =>
|
if !previous
|
||||||
@rootscope.$broadcast("sprint:us:moved")
|
startIndex = 0
|
||||||
|
else if previous
|
||||||
|
startIndex = orderList[previous.id] + 1
|
||||||
|
|
||||||
if movedToClosedSprint || movedFromClosedSprint
|
previousWithTheSameOrder = _.filter beforeDestination, (it) -> it[orderField] == orderList[previous.id]
|
||||||
@scope.$broadcast("backlog:load-closed-sprints")
|
|
||||||
|
|
||||||
promise.then null, ->
|
# we must send the USs previous to the dropped USs to tell the backend which USs are before the dropped USs,
|
||||||
console.log "FAIL" # TODO
|
# if they have the same value to order, the backend doens't know after which one do you want to drop the USs
|
||||||
|
if previousWithTheSameOrder.length > 1
|
||||||
|
setPreviousOrders = _.map previousWithTheSameOrder, (it) -> {us_id: it.id, order: orderList[it.id]}
|
||||||
|
|
||||||
|
modifiedUs = []
|
||||||
|
|
||||||
|
for us, key in usList # update sprint and new position
|
||||||
|
us.milestone = currentSprintId
|
||||||
|
us[orderField] = startIndex + key
|
||||||
|
orderList[us.id] = us[orderField]
|
||||||
|
|
||||||
|
modifiedUs.push({us_id: us.id, order: us[orderField]})
|
||||||
|
|
||||||
|
startIndex = orderList[usList[usList.length - 1].id]
|
||||||
|
|
||||||
|
for it, key in afterDestination # increase position of the us after the dragged us's
|
||||||
|
orderList[it.id] = startIndex + key + 1
|
||||||
|
|
||||||
|
# refresh order
|
||||||
|
@scope.userstories = _.sortBy @scope.userstories, (it) => @.backlogOrder[it.id]
|
||||||
|
|
||||||
|
for sprint in @scope.sprints
|
||||||
|
sprint.user_stories = _.sortBy sprint.user_stories, (it) => @.milestonesOrder[sprint.id][it.id]
|
||||||
|
|
||||||
|
for sprint in @scope.closedSprints
|
||||||
|
sprint.user_stories = _.sortBy sprint.user_stories, (it) => @.milestonesOrder[sprint.id][it.id]
|
||||||
|
|
||||||
|
#saving
|
||||||
|
if usList.length > 1 && (newSprintId != oldSprintId) # drag multiple to sprint
|
||||||
|
data = modifiedUs.concat(setPreviousOrders)
|
||||||
|
promise = @rs.userstories.bulkUpdateMilestone(project, newSprintId, data)
|
||||||
|
else if usList.length > 1 # drag multiple in backlog
|
||||||
|
data = modifiedUs.concat(setPreviousOrders)
|
||||||
|
promise = @rs.userstories.bulkUpdateBacklogOrder(project, data)
|
||||||
|
else # drag single
|
||||||
|
setOrders = {}
|
||||||
|
for it in setPreviousOrders
|
||||||
|
setOrders[it.us_id] = it.order
|
||||||
|
|
||||||
|
options = {
|
||||||
|
headers: {
|
||||||
|
"set-orders": JSON.stringify(setOrders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
promise = @repo.save(usList[0], true, {}, options, true)
|
||||||
|
|
||||||
|
promise.then () =>
|
||||||
|
@rootscope.$broadcast("sprint:us:moved")
|
||||||
|
|
||||||
|
if @scope.closedSprintsById && @scope.closedSprintsById[oldSprintId]
|
||||||
|
@rootscope.$broadcast("backlog:load-closed-sprints")
|
||||||
|
|
||||||
return promise
|
return promise
|
||||||
|
|
||||||
isFilterSelected: (type, id) ->
|
|
||||||
if @searchdata[type]? and @searchdata[type][id]
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
setSearchDataFilters: () ->
|
|
||||||
urlfilters = @.getUrlFilters()
|
|
||||||
|
|
||||||
if urlfilters.q
|
|
||||||
@scope.filtersQ = @scope.filtersQ or urlfilters.q
|
|
||||||
|
|
||||||
@searchdata = {}
|
|
||||||
for name, value of urlfilters
|
|
||||||
if not @searchdata[name]?
|
|
||||||
@searchdata[name] = {}
|
|
||||||
|
|
||||||
for val in taiga.toString(value).split(",")
|
|
||||||
@searchdata[name][val] = true
|
|
||||||
|
|
||||||
getUrlFilters: ->
|
|
||||||
return _.pick(@location.search(), "status", "tags", "q")
|
|
||||||
|
|
||||||
generateFilters: ->
|
|
||||||
urlfilters = @.getUrlFilters()
|
|
||||||
@scope.filters = {}
|
|
||||||
|
|
||||||
loadFilters = {}
|
|
||||||
loadFilters.project = @scope.projectId
|
|
||||||
loadFilters.tags = urlfilters.tags
|
|
||||||
loadFilters.status = urlfilters.status
|
|
||||||
loadFilters.q = urlfilters.q
|
|
||||||
loadFilters.milestone = 'null'
|
|
||||||
|
|
||||||
return @rs.userstories.filtersData(loadFilters).then (data) =>
|
|
||||||
choicesFiltersFormat = (choices, type, byIdObject) =>
|
|
||||||
_.map choices, (t) ->
|
|
||||||
t.type = type
|
|
||||||
return t
|
|
||||||
|
|
||||||
tagsFilterFormat = (tags) =>
|
|
||||||
return _.map tags, (t) ->
|
|
||||||
t.id = t.name
|
|
||||||
t.type = 'tags'
|
|
||||||
return t
|
|
||||||
|
|
||||||
# Build filters data structure
|
|
||||||
@scope.filters.status = choicesFiltersFormat(data.statuses, "status", @scope.usStatusById)
|
|
||||||
@scope.filters.tags = tagsFilterFormat(data.tags)
|
|
||||||
|
|
||||||
selectedTags = _.filter(@scope.filters.tags, "selected")
|
|
||||||
selectedTags = _.map(selectedTags, "id")
|
|
||||||
|
|
||||||
selectedStatuses = _.filter(@scope.filters.status, "selected")
|
|
||||||
selectedStatuses = _.map(selectedStatuses, "id")
|
|
||||||
|
|
||||||
@.markSelectedFilters(@scope.filters, urlfilters)
|
|
||||||
|
|
||||||
#store query params
|
|
||||||
@rs.userstories.storeQueryParams(@scope.projectId, {
|
|
||||||
"status": selectedStatuses,
|
|
||||||
"tags": selectedTags,
|
|
||||||
"project": @scope.projectId
|
|
||||||
"milestone": null
|
|
||||||
})
|
|
||||||
|
|
||||||
markSelectedFilters: (filters, urlfilters) ->
|
|
||||||
# Build selected filters (from url) fast lookup data structure
|
|
||||||
searchdata = {}
|
|
||||||
for name, value of _.omit(urlfilters, "page", "orderBy")
|
|
||||||
if not searchdata[name]?
|
|
||||||
searchdata[name] = {}
|
|
||||||
|
|
||||||
for val in "#{value}".split(",")
|
|
||||||
searchdata[name][val] = true
|
|
||||||
|
|
||||||
isSelected = (type, id) ->
|
|
||||||
if searchdata[type]? and searchdata[type][id]
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
for key, value of filters
|
|
||||||
for obj in value
|
|
||||||
obj.selected = if isSelected(obj.type, obj.id) then true else undefined
|
|
||||||
|
|
||||||
## Template actions
|
## Template actions
|
||||||
|
|
||||||
updateUserStoryStatus: () ->
|
updateUserStoryStatus: () ->
|
||||||
@.setSearchDataFilters()
|
|
||||||
@.generateFilters().then () =>
|
@.generateFilters().then () =>
|
||||||
@rootscope.$broadcast("filters:update")
|
@rootscope.$broadcast("filters:update")
|
||||||
@.loadProjectStats()
|
@.loadProjectStats()
|
||||||
|
@ -807,8 +692,15 @@ BacklogDirective = ($repo, $rootscope, $translate) ->
|
||||||
text = $translate.instant("BACKLOG.TAGS.SHOW")
|
text = $translate.instant("BACKLOG.TAGS.SHOW")
|
||||||
elm.text(text)
|
elm.text(text)
|
||||||
|
|
||||||
|
openFilterInit = ($scope, $el, $ctrl) ->
|
||||||
|
sidebar = $el.find("sidebar.backlog-filter")
|
||||||
|
|
||||||
|
sidebar.addClass("active")
|
||||||
|
|
||||||
|
$ctrl.activeFilters = true
|
||||||
|
|
||||||
showHideFilter = ($scope, $el, $ctrl) ->
|
showHideFilter = ($scope, $el, $ctrl) ->
|
||||||
sidebar = $el.find("sidebar.filters-bar")
|
sidebar = $el.find("sidebar.backlog-filter")
|
||||||
sidebar.one "transitionend", () ->
|
sidebar.one "transitionend", () ->
|
||||||
timeout 150, ->
|
timeout 150, ->
|
||||||
$rootscope.$broadcast("resize")
|
$rootscope.$broadcast("resize")
|
||||||
|
@ -824,11 +716,6 @@ BacklogDirective = ($repo, $rootscope, $translate) ->
|
||||||
|
|
||||||
toggleText(target, [hideText, showText])
|
toggleText(target, [hideText, showText])
|
||||||
|
|
||||||
if !sidebar.hasClass("active")
|
|
||||||
$ctrl.resetFilters()
|
|
||||||
else
|
|
||||||
$ctrl.restoreFilters()
|
|
||||||
|
|
||||||
$ctrl.toggleActiveFilters()
|
$ctrl.toggleActiveFilters()
|
||||||
|
|
||||||
## Filters Link
|
## Filters Link
|
||||||
|
@ -847,11 +734,13 @@ BacklogDirective = ($repo, $rootscope, $translate) ->
|
||||||
linkFilters($scope, $el, $attrs, $ctrl)
|
linkFilters($scope, $el, $attrs, $ctrl)
|
||||||
linkDoomLine($scope, $el, $attrs, $ctrl)
|
linkDoomLine($scope, $el, $attrs, $ctrl)
|
||||||
|
|
||||||
filters = $ctrl.getUrlFilters()
|
filters = $ctrl.location.search()
|
||||||
if filters.status ||
|
if filters.status ||
|
||||||
filters.tags ||
|
filters.tags ||
|
||||||
filters.q
|
filters.q ||
|
||||||
showHideFilter($scope, $el, $ctrl)
|
filters.assigned_to ||
|
||||||
|
filters.owner
|
||||||
|
openFilterInit($scope, $el, $ctrl)
|
||||||
|
|
||||||
$scope.$on "showTags", () ->
|
$scope.$on "showTags", () ->
|
||||||
showHideTags($ctrl)
|
showHideTags($ctrl)
|
||||||
|
|
|
@ -42,7 +42,7 @@ deleteElement = (el) ->
|
||||||
$(el).off()
|
$(el).off()
|
||||||
$(el).remove()
|
$(el).remove()
|
||||||
|
|
||||||
BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) ->
|
BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm) ->
|
||||||
link = ($scope, $el, $attrs) ->
|
link = ($scope, $el, $attrs) ->
|
||||||
bindOnce $scope, "project", (project) ->
|
bindOnce $scope, "project", (project) ->
|
||||||
# If the user has not enough permissions we don't enable the sortable
|
# If the user has not enough permissions we don't enable the sortable
|
||||||
|
@ -51,10 +51,6 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) ->
|
||||||
|
|
||||||
initIsBacklog = false
|
initIsBacklog = false
|
||||||
|
|
||||||
filterError = ->
|
|
||||||
text = $translate.instant("BACKLOG.SORTABLE_FILTER_ERROR")
|
|
||||||
$tgConfirm.notify("error", text)
|
|
||||||
|
|
||||||
drake = dragula([$el[0], $('.empty-backlog')[0]], {
|
drake = dragula([$el[0], $('.empty-backlog')[0]], {
|
||||||
copySortSource: false,
|
copySortSource: false,
|
||||||
copy: false,
|
copy: false,
|
||||||
|
@ -63,18 +59,11 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) ->
|
||||||
if !$(item).hasClass('row')
|
if !$(item).hasClass('row')
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# it doesn't move is the filter is open
|
|
||||||
parent = $(item).parent()
|
|
||||||
initIsBacklog = parent.hasClass('backlog-table-body')
|
|
||||||
|
|
||||||
if initIsBacklog && $el.hasClass("active-filters")
|
|
||||||
filterError()
|
|
||||||
return false
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
drake.on 'drag', (item, container) ->
|
drake.on 'drag', (item, container) ->
|
||||||
|
# it doesn't move is the filter is open
|
||||||
parent = $(item).parent()
|
parent = $(item).parent()
|
||||||
initIsBacklog = parent.hasClass('backlog-table-body')
|
initIsBacklog = parent.hasClass('backlog-table-body')
|
||||||
|
|
||||||
|
@ -88,6 +77,8 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) ->
|
||||||
$(item).addClass('backlog-us-mirror')
|
$(item).addClass('backlog-us-mirror')
|
||||||
|
|
||||||
drake.on 'dragend', (item) ->
|
drake.on 'dragend', (item) ->
|
||||||
|
parent = $(item).parent()
|
||||||
|
|
||||||
$('.doom-line').remove()
|
$('.doom-line').remove()
|
||||||
|
|
||||||
parent = $(item).parent()
|
parent = $(item).parent()
|
||||||
|
@ -102,8 +93,6 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) ->
|
||||||
|
|
||||||
$(document.body).removeClass("drag-active")
|
$(document.body).removeClass("drag-active")
|
||||||
|
|
||||||
items = $(item).parent().find('.row')
|
|
||||||
|
|
||||||
sprint = null
|
sprint = null
|
||||||
|
|
||||||
firstElement = if dragMultipleItems.length then dragMultipleItems[0] else item
|
firstElement = if dragMultipleItems.length then dragMultipleItems[0] else item
|
||||||
|
@ -131,11 +120,7 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) ->
|
||||||
usList = _.map dragMultipleItems, (item) ->
|
usList = _.map dragMultipleItems, (item) ->
|
||||||
return item = $(item).scope().us
|
return item = $(item).scope().us
|
||||||
else
|
else
|
||||||
usList = _.map items, (item) ->
|
usList = [$(item).scope().us]
|
||||||
item = $(item)
|
|
||||||
itemUs = item.scope().us
|
|
||||||
|
|
||||||
return itemUs
|
|
||||||
|
|
||||||
$scope.$emit("sprint:us:move", usList, index, sprint)
|
$scope.$emit("sprint:us:move", usList, index, sprint)
|
||||||
|
|
||||||
|
@ -158,6 +143,5 @@ module.directive("tgBacklogSortable", [
|
||||||
"$tgResources",
|
"$tgResources",
|
||||||
"$rootScope",
|
"$rootScope",
|
||||||
"$tgConfirm",
|
"$tgConfirm",
|
||||||
"$translate",
|
|
||||||
BacklogSortableDirective
|
BacklogSortableDirective
|
||||||
])
|
])
|
||||||
|
|
|
@ -41,7 +41,7 @@ class RepositoryService extends taiga.Service
|
||||||
defered = @q.defer()
|
defered = @q.defer()
|
||||||
url = @urls.resolve(name)
|
url = @urls.resolve(name)
|
||||||
|
|
||||||
promise = @http.post(url, JSON.stringify(data))
|
promise = @http.post(url, JSON.stringify(data), extraParams)
|
||||||
promise.success (_data, _status) =>
|
promise.success (_data, _status) =>
|
||||||
defered.resolve(@model.make_model(name, _data, null, dataTypes))
|
defered.resolve(@model.make_model(name, _data, null, dataTypes))
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ class RepositoryService extends taiga.Service
|
||||||
promises = _.map(models, (x) => @.save(x, true))
|
promises = _.map(models, (x) => @.save(x, true))
|
||||||
return @q.all(promises)
|
return @q.all(promises)
|
||||||
|
|
||||||
save: (model, patch=true) ->
|
save: (model, patch=true, params = {}, options, returnHeaders = false) ->
|
||||||
defered = @q.defer()
|
defered = @q.defer()
|
||||||
|
|
||||||
if not model.isModified() and patch
|
if not model.isModified() and patch
|
||||||
|
@ -75,20 +75,25 @@ class RepositoryService extends taiga.Service
|
||||||
return defered.promise
|
return defered.promise
|
||||||
|
|
||||||
url = @.resolveUrlForModel(model)
|
url = @.resolveUrlForModel(model)
|
||||||
|
|
||||||
data = JSON.stringify(model.getAttrs(patch))
|
data = JSON.stringify(model.getAttrs(patch))
|
||||||
|
|
||||||
if patch
|
if patch
|
||||||
promise = @http.patch(url, data)
|
promise = @http.patch(url, data, params, options)
|
||||||
else
|
else
|
||||||
promise = @http.put(url, data)
|
promise = @http.put(url, data, params, options)
|
||||||
|
|
||||||
promise.success (data, status) =>
|
promise.success (data, status, headers, response) =>
|
||||||
model._isModified = false
|
model._isModified = false
|
||||||
model._attrs = _.extend(model.getAttrs(), data)
|
model._attrs = _.extend(model.getAttrs(), data)
|
||||||
model._modifiedAttrs = {}
|
model._modifiedAttrs = {}
|
||||||
|
|
||||||
model.applyCasts()
|
model.applyCasts()
|
||||||
defered.resolve(model)
|
|
||||||
|
if returnHeaders
|
||||||
|
defered.resolve([model, headers()])
|
||||||
|
else
|
||||||
|
defered.resolve(model)
|
||||||
|
|
||||||
promise.error (data, status) ->
|
promise.error (data, status) ->
|
||||||
defered.reject(data)
|
defered.reject(data)
|
||||||
|
|
|
@ -378,22 +378,28 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
|
||||||
.target(submitButton)
|
.target(submitButton)
|
||||||
.start()
|
.start()
|
||||||
|
|
||||||
|
params = {
|
||||||
|
include_attachments: true,
|
||||||
|
include_tasks: true
|
||||||
|
}
|
||||||
|
|
||||||
if $scope.isNew
|
if $scope.isNew
|
||||||
promise = $repo.create("userstories", $scope.us)
|
promise = $repo.create("userstories", $scope.us)
|
||||||
broadcastEvent = "usform:new:success"
|
broadcastEvent = "usform:new:success"
|
||||||
else
|
else
|
||||||
promise = $repo.save($scope.us)
|
promise = $repo.save($scope.us, true)
|
||||||
broadcastEvent = "usform:edit:success"
|
broadcastEvent = "usform:edit:success"
|
||||||
|
|
||||||
promise.then (data) ->
|
promise.then (data) ->
|
||||||
deleteAttachments(data).then () => createAttachments(data)
|
deleteAttachments(data)
|
||||||
|
.then () => createAttachments(data)
|
||||||
|
.then () =>
|
||||||
|
currentLoading.finish()
|
||||||
|
lightboxService.close($el)
|
||||||
|
|
||||||
return data
|
$rs.userstories.getByRef(data.project, data.ref, params).then (us) ->
|
||||||
|
$rootScope.$broadcast(broadcastEvent, us)
|
||||||
|
|
||||||
promise.then (data) ->
|
|
||||||
currentLoading.finish()
|
|
||||||
lightboxService.close($el)
|
|
||||||
$rootScope.$broadcast(broadcastEvent, data)
|
|
||||||
|
|
||||||
promise.then null, (data) ->
|
promise.then null, (data) ->
|
||||||
currentLoading.finish()
|
currentLoading.finish()
|
||||||
|
@ -433,7 +439,7 @@ module.directive("tgLbCreateEditUserstory", [
|
||||||
"$translate",
|
"$translate",
|
||||||
"$tgConfirm",
|
"$tgConfirm",
|
||||||
"$q",
|
"$q",
|
||||||
"tgAttachmentsService",
|
"tgAttachmentsService"
|
||||||
CreateEditUserstoryDirective
|
CreateEditUserstoryDirective
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -442,7 +448,7 @@ module.directive("tgLbCreateEditUserstory", [
|
||||||
## Creare Bulk Userstories Lightbox Directive
|
## Creare Bulk Userstories Lightbox Directive
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
|
||||||
CreateBulkUserstoriesDirective = ($repo, $rs, $rootscope, lightboxService, $loading) ->
|
CreateBulkUserstoriesDirective = ($repo, $rs, $rootscope, lightboxService, $loading, $model) ->
|
||||||
link = ($scope, $el, attrs) ->
|
link = ($scope, $el, attrs) ->
|
||||||
form = null
|
form = null
|
||||||
|
|
||||||
|
@ -469,6 +475,7 @@ CreateBulkUserstoriesDirective = ($repo, $rs, $rootscope, lightboxService, $load
|
||||||
|
|
||||||
promise = $rs.userstories.bulkCreate($scope.new.projectId, $scope.new.statusId, $scope.new.bulk)
|
promise = $rs.userstories.bulkCreate($scope.new.projectId, $scope.new.statusId, $scope.new.bulk)
|
||||||
promise.then (result) ->
|
promise.then (result) ->
|
||||||
|
result = _.map(result.data, (x) => $model.make_model('userstories', x))
|
||||||
currentLoading.finish()
|
currentLoading.finish()
|
||||||
$rootscope.$broadcast("usform:bulk:success", result)
|
$rootscope.$broadcast("usform:bulk:success", result)
|
||||||
lightboxService.close($el)
|
lightboxService.close($el)
|
||||||
|
@ -494,6 +501,7 @@ module.directive("tgLbCreateBulkUserstories", [
|
||||||
"$rootScope",
|
"$rootScope",
|
||||||
"lightboxService",
|
"lightboxService",
|
||||||
"$tgLoading",
|
"$tgLoading",
|
||||||
|
"$tgModel",
|
||||||
CreateBulkUserstoriesDirective
|
CreateBulkUserstoriesDirective
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -535,7 +543,7 @@ AssignedToLightboxDirective = (lightboxService, lightboxKeyboardNavigationServic
|
||||||
visibleUsers = _.map visibleUsers, (user) ->
|
visibleUsers = _.map visibleUsers, (user) ->
|
||||||
user.avatar = avatarService.getAvatar(user)
|
user.avatar = avatarService.getAvatar(user)
|
||||||
|
|
||||||
selected.avatar = avatarService.getAvatar(selected)
|
selected.avatar = avatarService.getAvatar(selected) if selected
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
selected: selected
|
selected: selected
|
||||||
|
|
|
@ -110,4 +110,179 @@ class FiltersMixin
|
||||||
location = if load then @location else @location.noreload(@scope)
|
location = if load then @location else @location.noreload(@scope)
|
||||||
location.search(name, value)
|
location.search(name, value)
|
||||||
|
|
||||||
|
applyStoredFilters: (projectSlug, key) ->
|
||||||
|
if _.isEmpty(@location.search())
|
||||||
|
filters = @.getFilters(projectSlug, key)
|
||||||
|
if Object.keys(filters).length
|
||||||
|
@location.search(filters)
|
||||||
|
@location.replace()
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
storeFilters: (projectSlug, params, filtersHashSuffix) ->
|
||||||
|
ns = "#{projectSlug}:#{filtersHashSuffix}"
|
||||||
|
hash = taiga.generateHash([projectSlug, ns])
|
||||||
|
@storage.set(hash, params)
|
||||||
|
|
||||||
|
getFilters: (projectSlug, filtersHashSuffix) ->
|
||||||
|
ns = "#{projectSlug}:#{filtersHashSuffix}"
|
||||||
|
hash = taiga.generateHash([projectSlug, ns])
|
||||||
|
|
||||||
|
return @storage.get(hash) or {}
|
||||||
|
|
||||||
|
formatSelectedFilters: (type, list, urlIds) ->
|
||||||
|
selectedIds = urlIds.split(',')
|
||||||
|
selectedFilters = _.filter list, (it) ->
|
||||||
|
selectedIds.indexOf(_.toString(it.id)) != -1
|
||||||
|
|
||||||
|
return _.map selectedFilters, (it) ->
|
||||||
|
return {
|
||||||
|
id: it.id
|
||||||
|
key: type + ":" + it.id
|
||||||
|
dataType: type,
|
||||||
|
name: it.name
|
||||||
|
color: it.color
|
||||||
|
}
|
||||||
|
|
||||||
taiga.FiltersMixin = FiltersMixin
|
taiga.FiltersMixin = FiltersMixin
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
## Us Filters Mixin
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
class UsFiltersMixin
|
||||||
|
changeQ: (q) ->
|
||||||
|
@.replaceFilter("q", q)
|
||||||
|
@.filtersReloadContent()
|
||||||
|
@.generateFilters()
|
||||||
|
|
||||||
|
removeFilter: (filter) ->
|
||||||
|
@.unselectFilter(filter.dataType, filter.id)
|
||||||
|
@.filtersReloadContent()
|
||||||
|
@.generateFilters()
|
||||||
|
|
||||||
|
addFilter: (newFilter) ->
|
||||||
|
@.selectFilter(newFilter.category.dataType, newFilter.filter.id)
|
||||||
|
@.filtersReloadContent()
|
||||||
|
@.generateFilters()
|
||||||
|
|
||||||
|
selectCustomFilter: (customFilter) ->
|
||||||
|
@.replaceAllFilters(customFilter.filter)
|
||||||
|
@.filtersReloadContent()
|
||||||
|
@.generateFilters()
|
||||||
|
|
||||||
|
saveCustomFilter: (name) ->
|
||||||
|
filters = {}
|
||||||
|
urlfilters = @location.search()
|
||||||
|
filters.tags = urlfilters.tags
|
||||||
|
filters.status = urlfilters.status
|
||||||
|
filters.assigned_to = urlfilters.assigned_to
|
||||||
|
filters.owner = urlfilters.owner
|
||||||
|
|
||||||
|
@filterRemoteStorageService.getFilters(@scope.projectId, @.storeCustomFiltersName).then (userFilters) =>
|
||||||
|
userFilters[name] = filters
|
||||||
|
|
||||||
|
@filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, @.storeCustomFiltersName).then(@.generateFilters)
|
||||||
|
|
||||||
|
removeCustomFilter: (customFilter) ->
|
||||||
|
@filterRemoteStorageService.getFilters(@scope.projectId, @.storeCustomFiltersName).then (userFilters) =>
|
||||||
|
delete userFilters[customFilter.id]
|
||||||
|
|
||||||
|
@filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, @.storeCustomFiltersName).then(@.generateFilters)
|
||||||
|
@.generateFilters()
|
||||||
|
|
||||||
|
generateFilters: ->
|
||||||
|
@.storeFilters(@params.pslug, @location.search(), @.storeFiltersName)
|
||||||
|
|
||||||
|
urlfilters = @location.search()
|
||||||
|
|
||||||
|
loadFilters = {}
|
||||||
|
loadFilters.project = @scope.projectId
|
||||||
|
loadFilters.tags = urlfilters.tags
|
||||||
|
loadFilters.status = urlfilters.status
|
||||||
|
loadFilters.assigned_to = urlfilters.assigned_to
|
||||||
|
loadFilters.owner = urlfilters.owner
|
||||||
|
loadFilters.q = urlfilters.q
|
||||||
|
|
||||||
|
return @q.all([
|
||||||
|
@rs.userstories.filtersData(loadFilters),
|
||||||
|
@filterRemoteStorageService.getFilters(@scope.projectId, @.storeCustomFiltersName)
|
||||||
|
]).then (result) =>
|
||||||
|
data = result[0]
|
||||||
|
customFiltersRaw = result[1]
|
||||||
|
|
||||||
|
statuses = _.map data.statuses, (it) ->
|
||||||
|
it.id = it.id.toString()
|
||||||
|
|
||||||
|
return it
|
||||||
|
tags = _.map data.tags, (it) ->
|
||||||
|
it.id = it.name
|
||||||
|
|
||||||
|
return it
|
||||||
|
assignedTo = _.map data.assigned_to, (it) ->
|
||||||
|
if it.id
|
||||||
|
it.id = it.id.toString()
|
||||||
|
else
|
||||||
|
it.id = "null"
|
||||||
|
|
||||||
|
it.name = it.full_name || "Unassigned"
|
||||||
|
|
||||||
|
return it
|
||||||
|
owner = _.map data.owners, (it) ->
|
||||||
|
it.id = it.id.toString()
|
||||||
|
it.name = it.full_name
|
||||||
|
|
||||||
|
return it
|
||||||
|
|
||||||
|
@.selectedFilters = []
|
||||||
|
|
||||||
|
if loadFilters.status
|
||||||
|
selected = @.formatSelectedFilters("status", statuses, loadFilters.status)
|
||||||
|
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||||
|
|
||||||
|
if loadFilters.tags
|
||||||
|
selected = @.formatSelectedFilters("tags", tags, loadFilters.tags)
|
||||||
|
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||||
|
|
||||||
|
if loadFilters.assigned_to
|
||||||
|
selected = @.formatSelectedFilters("assigned_to", assignedTo, loadFilters.assigned_to)
|
||||||
|
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||||
|
|
||||||
|
if loadFilters.owner
|
||||||
|
selected = @.formatSelectedFilters("owner", owner, loadFilters.owner)
|
||||||
|
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||||
|
|
||||||
|
@.filterQ = loadFilters.q
|
||||||
|
|
||||||
|
@.filters = [
|
||||||
|
{
|
||||||
|
title: @translate.instant("COMMON.FILTERS.CATEGORIES.STATUS"),
|
||||||
|
dataType: "status",
|
||||||
|
content: statuses
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: @translate.instant("COMMON.FILTERS.CATEGORIES.TAGS"),
|
||||||
|
dataType: "tags",
|
||||||
|
content: tags,
|
||||||
|
hideEmpty: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: @translate.instant("COMMON.FILTERS.CATEGORIES.ASSIGNED_TO"),
|
||||||
|
dataType: "assigned_to",
|
||||||
|
content: assignedTo
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"),
|
||||||
|
dataType: "owner",
|
||||||
|
content: owner
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@.customFilters = []
|
||||||
|
_.forOwn customFiltersRaw, (value, key) =>
|
||||||
|
@.customFilters.push({id: key, name: key, filter: value})
|
||||||
|
|
||||||
|
|
||||||
|
taiga.UsFiltersMixin = UsFiltersMixin
|
||||||
|
|
|
@ -32,6 +32,7 @@ groupBy = @.taiga.groupBy
|
||||||
bindOnce = @.taiga.bindOnce
|
bindOnce = @.taiga.bindOnce
|
||||||
debounceLeading = @.taiga.debounceLeading
|
debounceLeading = @.taiga.debounceLeading
|
||||||
startswith = @.taiga.startswith
|
startswith = @.taiga.startswith
|
||||||
|
bindMethods = @.taiga.bindMethods
|
||||||
|
|
||||||
module = angular.module("taigaIssues")
|
module = angular.module("taigaIssues")
|
||||||
|
|
||||||
|
@ -55,21 +56,23 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
||||||
"$tgEvents",
|
"$tgEvents",
|
||||||
"$tgAnalytics",
|
"$tgAnalytics",
|
||||||
"$translate",
|
"$translate",
|
||||||
"tgErrorHandlingService"
|
"tgErrorHandlingService",
|
||||||
|
"$tgStorage",
|
||||||
|
"tgFilterRemoteStorageService"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
filtersHashSuffix: "issues-filters"
|
||||||
|
myFiltersHashSuffix: "issues-my-filters"
|
||||||
|
|
||||||
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @urls, @params, @q, @location, @appMetaService,
|
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @urls, @params, @q, @location, @appMetaService,
|
||||||
@navUrls, @events, @analytics, @translate, @errorHandlingService) ->
|
@navUrls, @events, @analytics, @translate, @errorHandlingService, @storage, @filterRemoteStorageService) ->
|
||||||
|
bindMethods(@)
|
||||||
|
|
||||||
@scope.sectionName = "Issues"
|
@scope.sectionName = "Issues"
|
||||||
@scope.filters = {}
|
@scope.filters = {}
|
||||||
@.voting = false
|
@.voting = false
|
||||||
|
|
||||||
if _.isEmpty(@location.search())
|
return if @.applyStoredFilters(@params.pslug, @.filtersHashSuffix)
|
||||||
filters = @rs.issues.getFilters(@params.pslug)
|
|
||||||
filters.page = 1
|
|
||||||
@location.search(filters)
|
|
||||||
@location.replace()
|
|
||||||
return
|
|
||||||
|
|
||||||
promise = @.loadInitialData()
|
promise = @.loadInitialData()
|
||||||
|
|
||||||
|
@ -89,13 +92,196 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
||||||
@analytics.trackEvent("issue", "create", "create issue on issues list", 1)
|
@analytics.trackEvent("issue", "create", "create issue on issues list", 1)
|
||||||
@.loadIssues()
|
@.loadIssues()
|
||||||
|
|
||||||
|
changeQ: (q) ->
|
||||||
|
@.unselectFilter("page")
|
||||||
|
@.replaceFilter("q", q)
|
||||||
|
@.loadIssues()
|
||||||
|
@.generateFilters()
|
||||||
|
|
||||||
|
removeFilter: (filter) ->
|
||||||
|
@.unselectFilter("page")
|
||||||
|
@.unselectFilter(filter.dataType, filter.id)
|
||||||
|
@.loadIssues()
|
||||||
|
@.generateFilters()
|
||||||
|
|
||||||
|
addFilter: (newFilter) ->
|
||||||
|
@.unselectFilter("page")
|
||||||
|
@.selectFilter(newFilter.category.dataType, newFilter.filter.id)
|
||||||
|
@.loadIssues()
|
||||||
|
@.generateFilters()
|
||||||
|
|
||||||
|
selectCustomFilter: (customFilter) ->
|
||||||
|
orderBy = @location.search().order_by
|
||||||
|
|
||||||
|
if orderBy
|
||||||
|
customFilter.filter.order_by = orderBy
|
||||||
|
|
||||||
|
@.unselectFilter("page")
|
||||||
|
@.replaceAllFilters(customFilter.filter)
|
||||||
|
@.loadIssues()
|
||||||
|
@.generateFilters()
|
||||||
|
|
||||||
|
removeCustomFilter: (customFilter) ->
|
||||||
|
console.log "oooo"
|
||||||
|
@filterRemoteStorageService.getFilters(@scope.projectId, @.myFiltersHashSuffix).then (userFilters) =>
|
||||||
|
console.log userFilters[customFilter.id]
|
||||||
|
delete userFilters[customFilter.id]
|
||||||
|
|
||||||
|
@filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, @.myFiltersHashSuffix).then(@.generateFilters)
|
||||||
|
|
||||||
|
saveCustomFilter: (name) ->
|
||||||
|
filters = {}
|
||||||
|
urlfilters = @location.search()
|
||||||
|
filters.tags = urlfilters.tags
|
||||||
|
filters.status = urlfilters.status
|
||||||
|
filters.type = urlfilters.type
|
||||||
|
filters.severity = urlfilters.severity
|
||||||
|
filters.priority = urlfilters.priority
|
||||||
|
filters.assigned_to = urlfilters.assigned_to
|
||||||
|
filters.owner = urlfilters.owner
|
||||||
|
|
||||||
|
@filterRemoteStorageService.getFilters(@scope.projectId, @.myFiltersHashSuffix).then (userFilters) =>
|
||||||
|
userFilters[name] = filters
|
||||||
|
|
||||||
|
@filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, @.myFiltersHashSuffix).then(@.generateFilters)
|
||||||
|
|
||||||
|
generateFilters: ->
|
||||||
|
@.storeFilters(@params.pslug, @location.search(), @.filtersHashSuffix)
|
||||||
|
|
||||||
|
urlfilters = @location.search()
|
||||||
|
|
||||||
|
loadFilters = {}
|
||||||
|
loadFilters.project = @scope.projectId
|
||||||
|
loadFilters.tags = urlfilters.tags
|
||||||
|
loadFilters.status = urlfilters.status
|
||||||
|
loadFilters.type = urlfilters.type
|
||||||
|
loadFilters.severity = urlfilters.severity
|
||||||
|
loadFilters.priority = urlfilters.priority
|
||||||
|
loadFilters.assigned_to = urlfilters.assigned_to
|
||||||
|
loadFilters.owner = urlfilters.owner
|
||||||
|
loadFilters.q = urlfilters.q
|
||||||
|
|
||||||
|
return @q.all([
|
||||||
|
@rs.issues.filtersData(loadFilters),
|
||||||
|
@filterRemoteStorageService.getFilters(@scope.projectId, @.myFiltersHashSuffix)
|
||||||
|
]).then (result) =>
|
||||||
|
data = result[0]
|
||||||
|
customFiltersRaw = result[1]
|
||||||
|
|
||||||
|
statuses = _.map data.statuses, (it) ->
|
||||||
|
it.id = it.id.toString()
|
||||||
|
|
||||||
|
return it
|
||||||
|
type = _.map data.types, (it) ->
|
||||||
|
it.id = it.id.toString()
|
||||||
|
|
||||||
|
return it
|
||||||
|
severity = _.map data.severities, (it) ->
|
||||||
|
it.id = it.id.toString()
|
||||||
|
|
||||||
|
return it
|
||||||
|
priority = _.map data.priorities, (it) ->
|
||||||
|
it.id = it.id.toString()
|
||||||
|
|
||||||
|
return it
|
||||||
|
tags = _.map data.tags, (it) ->
|
||||||
|
it.id = it.name
|
||||||
|
|
||||||
|
return it
|
||||||
|
assignedTo = _.map data.assigned_to, (it) ->
|
||||||
|
if it.id
|
||||||
|
it.id = it.id.toString()
|
||||||
|
else
|
||||||
|
it.id = "null"
|
||||||
|
|
||||||
|
it.name = it.full_name || "Unassigned"
|
||||||
|
|
||||||
|
return it
|
||||||
|
owner = _.map data.owners, (it) ->
|
||||||
|
it.id = it.id.toString()
|
||||||
|
it.name = it.full_name
|
||||||
|
|
||||||
|
return it
|
||||||
|
|
||||||
|
@.selectedFilters = []
|
||||||
|
|
||||||
|
if loadFilters.status
|
||||||
|
selected = @.formatSelectedFilters("status", statuses, loadFilters.status)
|
||||||
|
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||||
|
|
||||||
|
if loadFilters.tags
|
||||||
|
selected = @.formatSelectedFilters("tags", tags, loadFilters.tags)
|
||||||
|
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||||
|
|
||||||
|
if loadFilters.assigned_to
|
||||||
|
selected = @.formatSelectedFilters("assigned_to", assignedTo, loadFilters.assigned_to)
|
||||||
|
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||||
|
|
||||||
|
if loadFilters.owner
|
||||||
|
selected = @.formatSelectedFilters("owner", owner, loadFilters.owner)
|
||||||
|
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||||
|
|
||||||
|
if loadFilters.type
|
||||||
|
selected = @.formatSelectedFilters("type", type, loadFilters.type)
|
||||||
|
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||||
|
|
||||||
|
if loadFilters.severity
|
||||||
|
selected = @.formatSelectedFilters("severity", severity, loadFilters.severity)
|
||||||
|
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||||
|
|
||||||
|
if loadFilters.priority
|
||||||
|
selected = @.formatSelectedFilters("priority", priority, loadFilters.priority)
|
||||||
|
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||||
|
|
||||||
|
@.filterQ = loadFilters.q
|
||||||
|
|
||||||
|
@.filters = [
|
||||||
|
{
|
||||||
|
title: @translate.instant("COMMON.FILTERS.CATEGORIES.TYPE"),
|
||||||
|
dataType: "type",
|
||||||
|
content: type
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: @translate.instant("COMMON.FILTERS.CATEGORIES.SEVERITY"),
|
||||||
|
dataType: "severity",
|
||||||
|
content: severity
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: @translate.instant("COMMON.FILTERS.CATEGORIES.PRIORITIES"),
|
||||||
|
dataType: "priority",
|
||||||
|
content: priority
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: @translate.instant("COMMON.FILTERS.CATEGORIES.STATUS"),
|
||||||
|
dataType: "status",
|
||||||
|
content: statuses
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: @translate.instant("COMMON.FILTERS.CATEGORIES.TAGS"),
|
||||||
|
dataType: "tags",
|
||||||
|
content: tags
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: @translate.instant("COMMON.FILTERS.CATEGORIES.ASSIGNED_TO"),
|
||||||
|
dataType: "assigned_to",
|
||||||
|
content: assignedTo
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"),
|
||||||
|
dataType: "owner",
|
||||||
|
content: owner
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@.customFilters = []
|
||||||
|
_.forOwn customFiltersRaw, (value, key) =>
|
||||||
|
@.customFilters.push({id: key, name: key, filter: value})
|
||||||
|
|
||||||
initializeSubscription: ->
|
initializeSubscription: ->
|
||||||
routingKey = "changes.project.#{@scope.projectId}.issues"
|
routingKey = "changes.project.#{@scope.projectId}.issues"
|
||||||
@events.subscribe @scope, routingKey, (message) =>
|
@events.subscribe @scope, routingKey, (message) =>
|
||||||
@.loadIssues()
|
@.loadIssues()
|
||||||
|
|
||||||
storeFilters: ->
|
|
||||||
@rs.issues.storeFilters(@params.pslug, @location.search())
|
|
||||||
|
|
||||||
loadProject: ->
|
loadProject: ->
|
||||||
return @rs.projects.getBySlug(@params.pslug).then (project) =>
|
return @rs.projects.getBySlug(@params.pslug).then (project) =>
|
||||||
|
@ -117,160 +303,15 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
||||||
|
|
||||||
return project
|
return project
|
||||||
|
|
||||||
getUrlFilters: ->
|
|
||||||
filters = _.pick(@location.search(), "page", "tags", "status", "types",
|
|
||||||
"q", "severities", "priorities",
|
|
||||||
"assignedTo", "createdBy", "orderBy")
|
|
||||||
|
|
||||||
filters.page = 1 if not filters.page
|
|
||||||
return filters
|
|
||||||
|
|
||||||
getUrlFilter: (name) ->
|
|
||||||
filters = _.pick(@location.search(), name)
|
|
||||||
return filters[name]
|
|
||||||
|
|
||||||
loadMyFilters: ->
|
|
||||||
return @rs.issues.getMyFilters(@scope.projectId).then (filters) =>
|
|
||||||
return _.map filters, (value, key) =>
|
|
||||||
return {id: key, name: key, type: "myFilters", selected: false}
|
|
||||||
|
|
||||||
removeNotExistingFiltersFromUrl: ->
|
|
||||||
currentSearch = @location.search()
|
|
||||||
urlfilters = @.getUrlFilters()
|
|
||||||
|
|
||||||
for filterName, filterValue of urlfilters
|
|
||||||
if filterName == "page" or filterName == "orderBy" or filterName == "q"
|
|
||||||
continue
|
|
||||||
|
|
||||||
if filterName == "tags"
|
|
||||||
splittedValues = _.map("#{filterValue}".split(","))
|
|
||||||
else
|
|
||||||
splittedValues = _.map("#{filterValue}".split(","), (x) -> if x == "null" then null else parseInt(x))
|
|
||||||
|
|
||||||
existingValues = _.intersection(splittedValues, _.map(@scope.filters[filterName], "id"))
|
|
||||||
if splittedValues.length != existingValues.length
|
|
||||||
@location.search(filterName, existingValues.join())
|
|
||||||
|
|
||||||
if currentSearch != @location.search()
|
|
||||||
@location.replace()
|
|
||||||
|
|
||||||
markSelectedFilters: (filters, urlfilters) ->
|
|
||||||
# Build selected filters (from url) fast lookup data structure
|
|
||||||
searchdata = {}
|
|
||||||
for name, value of _.omit(urlfilters, "page", "orderBy")
|
|
||||||
if not searchdata[name]?
|
|
||||||
searchdata[name] = {}
|
|
||||||
|
|
||||||
for val in "#{value}".split(",")
|
|
||||||
searchdata[name][val] = true
|
|
||||||
|
|
||||||
isSelected = (type, id) ->
|
|
||||||
if searchdata[type]? and searchdata[type][id]
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
for key, value of filters
|
|
||||||
for obj in value
|
|
||||||
obj.selected = if isSelected(obj.type, obj.id) then true else undefined
|
|
||||||
|
|
||||||
loadFilters: () ->
|
|
||||||
urlfilters = @.getUrlFilters()
|
|
||||||
|
|
||||||
if urlfilters.q
|
|
||||||
@scope.filtersQ = urlfilters.q
|
|
||||||
|
|
||||||
# Load My Filters
|
|
||||||
promise = @.loadMyFilters().then (myFilters) =>
|
|
||||||
@scope.filters.myFilters = myFilters
|
|
||||||
return myFilters
|
|
||||||
|
|
||||||
loadFilters = {}
|
|
||||||
loadFilters.project = @scope.projectId
|
|
||||||
loadFilters.tags = urlfilters.tags
|
|
||||||
loadFilters.status = urlfilters.status
|
|
||||||
loadFilters.q = urlfilters.q
|
|
||||||
loadFilters.types = urlfilters.types
|
|
||||||
loadFilters.severities = urlfilters.severities
|
|
||||||
loadFilters.priorities = urlfilters.priorities
|
|
||||||
loadFilters.assigned_to = urlfilters.assignedTo
|
|
||||||
loadFilters.owner = urlfilters.createdBy
|
|
||||||
|
|
||||||
# Load default filters data
|
|
||||||
promise = promise.then =>
|
|
||||||
return @rs.issues.filtersData(loadFilters)
|
|
||||||
|
|
||||||
# Format filters and set them on scope
|
|
||||||
return promise.then (data) =>
|
|
||||||
usersFiltersFormat = (users, type, unknownOption) =>
|
|
||||||
reformatedUsers = _.map users, (t) =>
|
|
||||||
t.type = type
|
|
||||||
t.name = if t.full_name then t.full_name else unknownOption
|
|
||||||
|
|
||||||
return t
|
|
||||||
|
|
||||||
unknownItem = _.remove(reformatedUsers, (u) -> not u.id)
|
|
||||||
reformatedUsers = _.sortBy(reformatedUsers, (u) -> u.name.toUpperCase())
|
|
||||||
if unknownItem.length > 0
|
|
||||||
reformatedUsers.unshift(unknownItem[0])
|
|
||||||
return reformatedUsers
|
|
||||||
|
|
||||||
choicesFiltersFormat = (choices, type, byIdObject) =>
|
|
||||||
_.map choices, (t) ->
|
|
||||||
t.type = type
|
|
||||||
return t
|
|
||||||
|
|
||||||
tagsFilterFormat = (tags) =>
|
|
||||||
return _.map tags, (t) ->
|
|
||||||
t.id = t.name
|
|
||||||
t.type = 'tags'
|
|
||||||
return t
|
|
||||||
|
|
||||||
# Build filters data structure
|
|
||||||
@scope.filters.status = choicesFiltersFormat(data.statuses, "status", @scope.issueStatusById)
|
|
||||||
@scope.filters.severities = choicesFiltersFormat(data.severities, "severities", @scope.severityById)
|
|
||||||
@scope.filters.priorities = choicesFiltersFormat(data.priorities, "priorities", @scope.priorityById)
|
|
||||||
@scope.filters.assignedTo = usersFiltersFormat(data.assigned_to, "assignedTo", "Unassigned")
|
|
||||||
@scope.filters.createdBy = usersFiltersFormat(data.owners, "createdBy", "Unknown")
|
|
||||||
@scope.filters.types = choicesFiltersFormat(data.types, "types", @scope.issueTypeById)
|
|
||||||
@scope.filters.tags = tagsFilterFormat(data.tags)
|
|
||||||
|
|
||||||
@.removeNotExistingFiltersFromUrl()
|
|
||||||
@.markSelectedFilters(@scope.filters, urlfilters)
|
|
||||||
|
|
||||||
@rootscope.$broadcast("filters:loaded", @scope.filters)
|
|
||||||
|
|
||||||
# We need to guarantee that the last petition done here is the finally used
|
# We need to guarantee that the last petition done here is the finally used
|
||||||
# When searching by text loadIssues can be called fastly with different parameters and
|
# When searching by text loadIssues can be called fastly with different parameters and
|
||||||
# can be resolved in a different order than generated
|
# can be resolved in a different order than generated
|
||||||
# We count the requests made and only if the callback is for the last one data is updated
|
# We count the requests made and only if the callback is for the last one data is updated
|
||||||
loadIssuesRequests: 0
|
loadIssuesRequests: 0
|
||||||
loadIssues: =>
|
loadIssues: =>
|
||||||
@scope.urlFilters = @.getUrlFilters()
|
params = @location.search()
|
||||||
|
|
||||||
# Convert stored filters to http parameters
|
promise = @rs.issues.list(@scope.projectId, params)
|
||||||
# ready filters (the name difference exists
|
|
||||||
# because of some automatic lookups and is
|
|
||||||
# the simplest way todo it without adding
|
|
||||||
# additional complexity to code.
|
|
||||||
@scope.httpParams = {}
|
|
||||||
for name, values of @scope.urlFilters
|
|
||||||
if name == "severities"
|
|
||||||
name = "severity"
|
|
||||||
else if name == "orderBy"
|
|
||||||
name = "order_by"
|
|
||||||
else if name == "priorities"
|
|
||||||
name = "priority"
|
|
||||||
else if name == "assignedTo"
|
|
||||||
name = "assigned_to"
|
|
||||||
else if name == "createdBy"
|
|
||||||
name = "owner"
|
|
||||||
else if name == "status"
|
|
||||||
name = "status"
|
|
||||||
else if name == "types"
|
|
||||||
name = "type"
|
|
||||||
@scope.httpParams[name] = values
|
|
||||||
|
|
||||||
promise = @rs.issues.list(@scope.projectId, @scope.httpParams)
|
|
||||||
@.loadIssuesRequests += 1
|
@.loadIssuesRequests += 1
|
||||||
promise.index = @.loadIssuesRequests
|
promise.index = @.loadIssuesRequests
|
||||||
promise.then (data) =>
|
promise.then (data) =>
|
||||||
|
@ -289,26 +330,10 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
||||||
return promise.then (project) =>
|
return promise.then (project) =>
|
||||||
@.fillUsersAndRoles(project.members, project.roles)
|
@.fillUsersAndRoles(project.members, project.roles)
|
||||||
@.initializeSubscription()
|
@.initializeSubscription()
|
||||||
@.loadFilters()
|
@.generateFilters()
|
||||||
|
|
||||||
return @.loadIssues()
|
return @.loadIssues()
|
||||||
|
|
||||||
saveCurrentFiltersTo: (newFilter) ->
|
|
||||||
deferred = @q.defer()
|
|
||||||
@rs.issues.getMyFilters(@scope.projectId).then (filters) =>
|
|
||||||
filters[newFilter] = @location.search()
|
|
||||||
@rs.issues.storeMyFilters(@scope.projectId, filters).then =>
|
|
||||||
deferred.resolve()
|
|
||||||
return deferred.promise
|
|
||||||
|
|
||||||
deleteMyFilter: (filter) ->
|
|
||||||
deferred = @q.defer()
|
|
||||||
@rs.issues.getMyFilters(@scope.projectId).then (filters) =>
|
|
||||||
delete filters[filter]
|
|
||||||
@rs.issues.storeMyFilters(@scope.projectId, filters).then =>
|
|
||||||
deferred.resolve()
|
|
||||||
return deferred.promise
|
|
||||||
|
|
||||||
# Functions used from templates
|
# Functions used from templates
|
||||||
addNewIssue: ->
|
addNewIssue: ->
|
||||||
@rootscope.$broadcast("issueform:new", @scope.project)
|
@rootscope.$broadcast("issueform:new", @scope.project)
|
||||||
|
@ -338,6 +363,12 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
||||||
|
|
||||||
return @rs.issues.downvote(issueId).then(onSuccess, onError)
|
return @rs.issues.downvote(issueId).then(onSuccess, onError)
|
||||||
|
|
||||||
|
getOrderBy: ->
|
||||||
|
if _.isString(@location.search().order_by)
|
||||||
|
return @location.search().order_by
|
||||||
|
else
|
||||||
|
return "created_date"
|
||||||
|
|
||||||
module.controller("IssuesController", IssuesController)
|
module.controller("IssuesController", IssuesController)
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
@ -431,28 +462,40 @@ IssuesDirective = ($log, $location, $template, $compile) ->
|
||||||
## Issues Filters
|
## Issues Filters
|
||||||
linkOrdering = ($scope, $el, $attrs, $ctrl) ->
|
linkOrdering = ($scope, $el, $attrs, $ctrl) ->
|
||||||
# Draw the arrow the first time
|
# Draw the arrow the first time
|
||||||
currentOrder = $ctrl.getUrlFilter("orderBy") or "created_date"
|
|
||||||
|
currentOrder = $ctrl.getOrderBy()
|
||||||
|
|
||||||
if currentOrder
|
if currentOrder
|
||||||
icon = if startswith(currentOrder, "-") then "icon-arrow-up" else "icon-arrow-bottom"
|
icon = if startswith(currentOrder, "-") then "icon-arrow-up" else "icon-arrow-down"
|
||||||
colHeadElement = $el.find(".row.title > div[data-fieldname='#{trim(currentOrder, "-")}']")
|
colHeadElement = $el.find(".row.title > div[data-fieldname='#{trim(currentOrder, "-")}']")
|
||||||
colHeadElement.html("#{colHeadElement.html()}<span class='icon #{icon}'></span>")
|
|
||||||
|
svg = $("<tg-svg>").attr("svg-icon", icon)
|
||||||
|
|
||||||
|
colHeadElement.append(svg)
|
||||||
|
$compile(colHeadElement.contents())($scope);
|
||||||
|
|
||||||
$el.on "click", ".row.title > div", (event) ->
|
$el.on "click", ".row.title > div", (event) ->
|
||||||
target = angular.element(event.currentTarget)
|
target = angular.element(event.currentTarget)
|
||||||
|
|
||||||
currentOrder = $ctrl.getUrlFilter("orderBy")
|
currentOrder = $ctrl.getOrderBy()
|
||||||
newOrder = target.data("fieldname")
|
newOrder = target.data("fieldname")
|
||||||
|
|
||||||
finalOrder = if currentOrder == newOrder then "-#{newOrder}" else newOrder
|
finalOrder = if currentOrder == newOrder then "-#{newOrder}" else newOrder
|
||||||
|
|
||||||
$scope.$apply ->
|
$scope.$apply ->
|
||||||
$ctrl.replaceFilter("orderBy", finalOrder)
|
$ctrl.replaceFilter("order_by", finalOrder)
|
||||||
$ctrl.storeFilters()
|
|
||||||
|
$ctrl.storeFilters($ctrl.params.pslug, $location.search(), $ctrl.filtersHashSuffix)
|
||||||
$ctrl.loadIssues().then ->
|
$ctrl.loadIssues().then ->
|
||||||
# Update the arrow
|
# Update the arrow
|
||||||
$el.find(".row.title > div > span.icon").remove()
|
$el.find(".row.title > div > tg-svg").remove()
|
||||||
icon = if startswith(finalOrder, "-") then "icon-arrow-up" else "icon-arrow-bottom"
|
icon = if startswith(finalOrder, "-") then "icon-arrow-up" else "icon-arrow-down"
|
||||||
target.html("#{target.html()}<span class='icon #{icon}'></span>")
|
|
||||||
|
svg = $("<tg-svg>")
|
||||||
|
.attr("svg-icon", icon)
|
||||||
|
|
||||||
|
target.append(svg)
|
||||||
|
$compile(target.contents())($scope);
|
||||||
|
|
||||||
## Issues Link
|
## Issues Link
|
||||||
link = ($scope, $el, $attrs) ->
|
link = ($scope, $el, $attrs) ->
|
||||||
|
@ -468,253 +511,6 @@ IssuesDirective = ($log, $location, $template, $compile) ->
|
||||||
module.directive("tgIssues", ["$log", "$tgLocation", "$tgTemplate", "$compile", IssuesDirective])
|
module.directive("tgIssues", ["$log", "$tgLocation", "$tgTemplate", "$compile", IssuesDirective])
|
||||||
|
|
||||||
|
|
||||||
#############################################################################
|
|
||||||
## Issues Filters Directive
|
|
||||||
#############################################################################
|
|
||||||
|
|
||||||
IssuesFiltersDirective = ($q, $log, $location, $rs, $confirm, $loading, $template, $translate, $compile, $auth) ->
|
|
||||||
template = $template.get("issue/issues-filters.html", true)
|
|
||||||
templateSelected = $template.get("issue/issues-filters-selected.html", true)
|
|
||||||
|
|
||||||
link = ($scope, $el, $attrs) ->
|
|
||||||
$ctrl = $el.closest(".wrapper").controller()
|
|
||||||
|
|
||||||
selectedFilters = []
|
|
||||||
|
|
||||||
showFilters = (title, type) ->
|
|
||||||
$el.find(".filters-cats").hide()
|
|
||||||
$el.find(".filter-list").removeClass("hidden")
|
|
||||||
$el.find(".breadcrumb").removeClass("hidden")
|
|
||||||
$el.find("h2 .subfilter .title").html(title)
|
|
||||||
$el.find("h2 .subfilter .title").prop("data-type", type)
|
|
||||||
|
|
||||||
showCategories = ->
|
|
||||||
$el.find(".filters-cats").show()
|
|
||||||
$el.find(".filter-list").addClass("hidden")
|
|
||||||
$el.find(".breadcrumb").addClass("hidden")
|
|
||||||
|
|
||||||
initializeSelectedFilters = (filters) ->
|
|
||||||
selectedFilters = []
|
|
||||||
for name, values of filters
|
|
||||||
for val in values
|
|
||||||
selectedFilters.push(val) if val.selected
|
|
||||||
|
|
||||||
renderSelectedFilters(selectedFilters)
|
|
||||||
|
|
||||||
renderSelectedFilters = (selectedFilters) ->
|
|
||||||
_.filter selectedFilters, (f) =>
|
|
||||||
if f.color
|
|
||||||
f.style = "border-left: 3px solid #{f.color}"
|
|
||||||
|
|
||||||
html = templateSelected({filters:selectedFilters})
|
|
||||||
html = $compile(html)($scope)
|
|
||||||
$el.find(".filters-applied").html(html)
|
|
||||||
|
|
||||||
if $auth.isAuthenticated() && selectedFilters.length > 0
|
|
||||||
$el.find(".save-filters").show()
|
|
||||||
else
|
|
||||||
$el.find(".save-filters").hide()
|
|
||||||
|
|
||||||
renderFilters = (filters) ->
|
|
||||||
_.filter filters, (f) =>
|
|
||||||
if f.color
|
|
||||||
f.style = "border-left: 3px solid #{f.color}"
|
|
||||||
|
|
||||||
html = template({filters:filters})
|
|
||||||
html = $compile(html)($scope)
|
|
||||||
$el.find(".filter-list").html(html)
|
|
||||||
|
|
||||||
getFiltersType = () ->
|
|
||||||
return $el.find(".subfilter .title").prop('data-type')
|
|
||||||
|
|
||||||
reloadIssues = () ->
|
|
||||||
currentFiltersType = getFiltersType()
|
|
||||||
|
|
||||||
$q.all([$ctrl.loadIssues(), $ctrl.loadFilters()]).then () ->
|
|
||||||
filters = $scope.filters[currentFiltersType]
|
|
||||||
renderFilters(_.reject(filters, "selected"))
|
|
||||||
|
|
||||||
toggleFilterSelection = (type, id) ->
|
|
||||||
if type == "myFilters"
|
|
||||||
$rs.issues.getMyFilters($scope.projectId).then (data) ->
|
|
||||||
myFilters = data
|
|
||||||
filters = myFilters[id]
|
|
||||||
filters.page = 1
|
|
||||||
$ctrl.replaceAllFilters(filters)
|
|
||||||
$ctrl.storeFilters()
|
|
||||||
$ctrl.loadIssues()
|
|
||||||
$ctrl.markSelectedFilters($scope.filters, filters)
|
|
||||||
initializeSelectedFilters($scope.filters)
|
|
||||||
return null
|
|
||||||
|
|
||||||
filters = $scope.filters[type]
|
|
||||||
filterId = if type == 'tags' then taiga.toString(id) else id
|
|
||||||
filter = _.find(filters, {id: filterId})
|
|
||||||
filter.selected = (not filter.selected)
|
|
||||||
|
|
||||||
# Convert id to null as string for properly
|
|
||||||
# put null value on url parameters
|
|
||||||
id = "null" if id is null
|
|
||||||
|
|
||||||
if filter.selected
|
|
||||||
selectedFilters.push(filter)
|
|
||||||
$ctrl.selectFilter(type, id)
|
|
||||||
$ctrl.selectFilter("page", 1)
|
|
||||||
$ctrl.storeFilters()
|
|
||||||
else
|
|
||||||
selectedFilters = _.reject selectedFilters, (f) ->
|
|
||||||
return f.id == filter.id && f.type == filter.type
|
|
||||||
|
|
||||||
$ctrl.unselectFilter(type, id)
|
|
||||||
$ctrl.selectFilter("page", 1)
|
|
||||||
$ctrl.storeFilters()
|
|
||||||
|
|
||||||
reloadIssues()
|
|
||||||
|
|
||||||
renderSelectedFilters(selectedFilters)
|
|
||||||
|
|
||||||
currentFiltersType = getFiltersType()
|
|
||||||
|
|
||||||
if type == currentFiltersType
|
|
||||||
renderFilters(_.reject(filters, "selected"))
|
|
||||||
|
|
||||||
# Angular Watchers
|
|
||||||
$scope.$on "filters:loaded", (ctx, filters) ->
|
|
||||||
initializeSelectedFilters(filters)
|
|
||||||
|
|
||||||
$scope.$on "filters:issueupdate", (ctx, filters) ->
|
|
||||||
html = template({filters:filters.status})
|
|
||||||
html = $compile(html)($scope)
|
|
||||||
$el.find(".filter-list").html(html)
|
|
||||||
|
|
||||||
selectQFilter = debounceLeading 100, (value, oldValue) ->
|
|
||||||
return if value is undefined or value == oldValue
|
|
||||||
|
|
||||||
$ctrl.replaceFilter("page", null, true)
|
|
||||||
|
|
||||||
if value.length == 0
|
|
||||||
$ctrl.replaceFilter("q", null)
|
|
||||||
$ctrl.storeFilters()
|
|
||||||
else
|
|
||||||
$ctrl.replaceFilter("q", value)
|
|
||||||
$ctrl.storeFilters()
|
|
||||||
|
|
||||||
reloadIssues()
|
|
||||||
|
|
||||||
unwatchIssues = $scope.$watch "issues", (newValue) ->
|
|
||||||
if !_.isUndefined(newValue)
|
|
||||||
$scope.$watch("filtersQ", selectQFilter)
|
|
||||||
unwatchIssues()
|
|
||||||
|
|
||||||
# Dom Event Handlers
|
|
||||||
$el.on "click", ".filters-cat-single", (event) ->
|
|
||||||
event.preventDefault()
|
|
||||||
target = angular.element(event.currentTarget)
|
|
||||||
tags = $scope.filters[target.data("type")]
|
|
||||||
renderFilters(_.reject(tags, "selected"))
|
|
||||||
showFilters(target.attr("title"), target.data("type"))
|
|
||||||
|
|
||||||
$el.on "click", ".back", (event) ->
|
|
||||||
event.preventDefault()
|
|
||||||
showCategories($el)
|
|
||||||
|
|
||||||
$el.on "click", ".filters-applied .remove-filter", (event) ->
|
|
||||||
event.preventDefault()
|
|
||||||
target = angular.element(event.currentTarget).parent()
|
|
||||||
|
|
||||||
id = target.data("id") or null
|
|
||||||
type = target.data("type")
|
|
||||||
toggleFilterSelection(type, id)
|
|
||||||
|
|
||||||
$el.on "click", ".filter-list .single-filter", (event) ->
|
|
||||||
event.preventDefault()
|
|
||||||
target = angular.element(event.currentTarget)
|
|
||||||
target.toggleClass("active")
|
|
||||||
|
|
||||||
id = target.data("id") or null
|
|
||||||
type = target.data("type")
|
|
||||||
|
|
||||||
# A saved filter can't be active
|
|
||||||
if type == "myFilters"
|
|
||||||
target.removeClass("active")
|
|
||||||
|
|
||||||
toggleFilterSelection(type, id)
|
|
||||||
|
|
||||||
$el.on "click", ".filter-list .remove-filter", (event) ->
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
target = angular.element(event.currentTarget)
|
|
||||||
customFilterName = target.parent().data('id')
|
|
||||||
title = $translate.instant("ISSUES.FILTERS.CONFIRM_DELETE.TITLE")
|
|
||||||
message = $translate.instant("ISSUES.FILTERS.CONFIRM_DELETE.MESSAGE", {customFilterName: customFilterName})
|
|
||||||
|
|
||||||
$confirm.askOnDelete(title, message).then (askResponse) ->
|
|
||||||
promise = $ctrl.deleteMyFilter(customFilterName)
|
|
||||||
promise.then ->
|
|
||||||
promise = $ctrl.loadMyFilters()
|
|
||||||
promise.then (filters) ->
|
|
||||||
askResponse.finish()
|
|
||||||
$scope.filters.myFilters = filters
|
|
||||||
renderFilters($scope.filters.myFilters)
|
|
||||||
promise.then null, ->
|
|
||||||
askResponse.finish()
|
|
||||||
promise.then null, ->
|
|
||||||
askResponse.finish(false)
|
|
||||||
$confirm.notify("error")
|
|
||||||
|
|
||||||
|
|
||||||
$el.on "click", ".save-filters", (event) ->
|
|
||||||
event.preventDefault()
|
|
||||||
renderFilters($scope.filters["myFilters"])
|
|
||||||
showFilters("My filters", "myFilters")
|
|
||||||
$el.find('.save-filters').hide()
|
|
||||||
$el.find('.my-filter-name').removeClass("hidden")
|
|
||||||
$el.find('.my-filter-name').focus()
|
|
||||||
$scope.$apply()
|
|
||||||
|
|
||||||
$el.on "keyup", ".my-filter-name", (event) ->
|
|
||||||
event.preventDefault()
|
|
||||||
if event.keyCode == 13
|
|
||||||
target = angular.element(event.currentTarget)
|
|
||||||
newFilter = target.val()
|
|
||||||
currentLoading = $loading()
|
|
||||||
.target($el.find(".new"))
|
|
||||||
.start()
|
|
||||||
promise = $ctrl.saveCurrentFiltersTo(newFilter)
|
|
||||||
promise.then ->
|
|
||||||
loadPromise = $ctrl.loadMyFilters()
|
|
||||||
loadPromise.then (filters) ->
|
|
||||||
currentLoading.finish()
|
|
||||||
$scope.filters.myFilters = filters
|
|
||||||
|
|
||||||
currentfilterstype = $el.find("h2 .subfilter .title").prop('data-type')
|
|
||||||
if currentfilterstype == "myFilters"
|
|
||||||
renderFilters($scope.filters.myFilters)
|
|
||||||
|
|
||||||
$el.find('.my-filter-name').addClass("hidden")
|
|
||||||
$el.find('.save-filters').show()
|
|
||||||
|
|
||||||
loadPromise.then null, ->
|
|
||||||
currentLoading.finish()
|
|
||||||
$confirm.notify("error", "Error loading custom filters")
|
|
||||||
|
|
||||||
promise.then null, ->
|
|
||||||
currentLoading.finish()
|
|
||||||
$el.find(".my-filter-name").val(newFilter).focus().select()
|
|
||||||
$confirm.notify("error", "Filter not saved")
|
|
||||||
|
|
||||||
else if event.keyCode == 27
|
|
||||||
$el.find('.my-filter-name').val('')
|
|
||||||
$el.find('.my-filter-name').addClass("hidden")
|
|
||||||
$el.find('.save-filters').show()
|
|
||||||
|
|
||||||
return {link:link}
|
|
||||||
|
|
||||||
module.directive("tgIssuesFilters", ["$q", "$log", "$tgLocation", "$tgResources", "$tgConfirm", "$tgLoading",
|
|
||||||
"$tgTemplate", "$translate", "$compile", "$tgAuth", IssuesFiltersDirective])
|
|
||||||
|
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
## Issue status Directive (popover for change status)
|
## Issue status Directive (popover for change status)
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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: kanban-userstories.service.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
groupBy = @.taiga.groupBy
|
||||||
|
|
||||||
|
class KanbanUserstoriesService extends taiga.Service
|
||||||
|
@.$inject = []
|
||||||
|
|
||||||
|
constructor: () ->
|
||||||
|
@.reset()
|
||||||
|
|
||||||
|
reset: () ->
|
||||||
|
@.userstoriesRaw = []
|
||||||
|
@.archivedStatus = []
|
||||||
|
@.statusHide = []
|
||||||
|
@.foldStatusChanged = {}
|
||||||
|
@.usByStatus = Immutable.Map()
|
||||||
|
|
||||||
|
init: (project, usersById) ->
|
||||||
|
@.project = project
|
||||||
|
@.usersById = usersById
|
||||||
|
|
||||||
|
resetFolds: () ->
|
||||||
|
@.foldStatusChanged = {}
|
||||||
|
@.refresh()
|
||||||
|
|
||||||
|
toggleFold: (usId) ->
|
||||||
|
@.foldStatusChanged[usId] = !@.foldStatusChanged[usId]
|
||||||
|
@.refresh()
|
||||||
|
|
||||||
|
set: (userstories) ->
|
||||||
|
@.userstoriesRaw = userstories
|
||||||
|
@.refreshRawOrder()
|
||||||
|
@.refresh()
|
||||||
|
|
||||||
|
add: (us) ->
|
||||||
|
@.userstoriesRaw = @.userstoriesRaw.concat(us)
|
||||||
|
@.refreshRawOrder()
|
||||||
|
@.refresh()
|
||||||
|
|
||||||
|
addArchivedStatus: (statusId) ->
|
||||||
|
@.archivedStatus.push(statusId)
|
||||||
|
|
||||||
|
isUsInArchivedHiddenStatus: (usId) ->
|
||||||
|
us = @.getUsModel(usId)
|
||||||
|
|
||||||
|
return @.archivedStatus.indexOf(us.status) != -1 &&
|
||||||
|
@.statusHide.indexOf(us.status) != -1
|
||||||
|
|
||||||
|
hideStatus: (statusId) ->
|
||||||
|
@.deleteStatus(statusId)
|
||||||
|
@.statusHide.push(statusId)
|
||||||
|
|
||||||
|
showStatus: (statusId) ->
|
||||||
|
_.remove @.statusHide, (it) -> return it == statusId
|
||||||
|
|
||||||
|
getStatus: (statusId) ->
|
||||||
|
return _.filter @.userstoriesRaw, (us) -> return us.status == statusId
|
||||||
|
|
||||||
|
deleteStatus: (statusId) ->
|
||||||
|
toDelete = _.filter @.userstoriesRaw, (us) -> return us.status == statusId
|
||||||
|
toDelete = _.map (it) -> return it.id
|
||||||
|
|
||||||
|
@.archived = _.difference(@.archived, toDelete)
|
||||||
|
|
||||||
|
@.userstoriesRaw = _.filter @.userstoriesRaw, (us) -> return us.status != statusId
|
||||||
|
|
||||||
|
@.refresh()
|
||||||
|
|
||||||
|
refreshRawOrder: () ->
|
||||||
|
@.order = {}
|
||||||
|
|
||||||
|
@.order[it.id] = it.kanban_order for it in @.userstoriesRaw
|
||||||
|
|
||||||
|
assignOrders: (order) ->
|
||||||
|
order = _.invert(order)
|
||||||
|
@.order = _.assign(@.order, order)
|
||||||
|
|
||||||
|
@.refresh()
|
||||||
|
|
||||||
|
move: (id, statusId, index) ->
|
||||||
|
us = @.getUsModel(id)
|
||||||
|
|
||||||
|
usByStatus = _.filter @.userstoriesRaw, (it) =>
|
||||||
|
return it.status == statusId
|
||||||
|
|
||||||
|
usByStatus = _.sortBy usByStatus, (it) => @.order[it.id]
|
||||||
|
|
||||||
|
usByStatusWithoutMoved = _.filter usByStatus, (it) => it.id != id
|
||||||
|
beforeDestination = _.slice(usByStatusWithoutMoved, 0, index)
|
||||||
|
afterDestination = _.slice(usByStatusWithoutMoved, index)
|
||||||
|
|
||||||
|
setOrders = {}
|
||||||
|
|
||||||
|
previous = beforeDestination[beforeDestination.length - 1]
|
||||||
|
|
||||||
|
previousWithTheSameOrder = _.filter beforeDestination, (it) =>
|
||||||
|
@.order[it.id] == @.order[previous.id]
|
||||||
|
|
||||||
|
if previousWithTheSameOrder.length > 1
|
||||||
|
for it in previousWithTheSameOrder
|
||||||
|
setOrders[it.id] = @.order[it.id]
|
||||||
|
|
||||||
|
if !previous
|
||||||
|
@.order[us.id] = 0
|
||||||
|
else if previous
|
||||||
|
@.order[us.id] = @.order[previous.id] + 1
|
||||||
|
|
||||||
|
for it, key in afterDestination
|
||||||
|
@.order[it.id] = @.order[us.id] + key + 1
|
||||||
|
|
||||||
|
us.status = statusId
|
||||||
|
us.kanban_order = @.order[us.id]
|
||||||
|
|
||||||
|
@.refresh()
|
||||||
|
|
||||||
|
return {"us_id": us.id, "order": @.order[us.id], "set_orders": setOrders}
|
||||||
|
|
||||||
|
replace: (us) ->
|
||||||
|
@.usByStatus = @.usByStatus.map (status) ->
|
||||||
|
findedIndex = status.findIndex (usItem) ->
|
||||||
|
return usItem.get('id') == us.get('id')
|
||||||
|
|
||||||
|
if findedIndex != -1
|
||||||
|
status = status.set(findedIndex, us)
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
replaceModel: (us) ->
|
||||||
|
@.userstoriesRaw = _.map @.userstoriesRaw, (usItem) ->
|
||||||
|
if us.id == usItem.id
|
||||||
|
return us
|
||||||
|
else
|
||||||
|
return usItem
|
||||||
|
|
||||||
|
@.refresh()
|
||||||
|
|
||||||
|
getUs: (id) ->
|
||||||
|
findedUs = null
|
||||||
|
|
||||||
|
@.usByStatus.forEach (status) ->
|
||||||
|
findedUs = status.find (us) -> return us.get('id') == id
|
||||||
|
|
||||||
|
return false if findedUs
|
||||||
|
|
||||||
|
return findedUs
|
||||||
|
|
||||||
|
getUsModel: (id) ->
|
||||||
|
return _.find @.userstoriesRaw, (us) -> return us.id == id
|
||||||
|
|
||||||
|
refresh: ->
|
||||||
|
@.userstoriesRaw = _.sortBy @.userstoriesRaw, (it) => @.order[it.id]
|
||||||
|
|
||||||
|
userstories = @.userstoriesRaw
|
||||||
|
userstories = _.map userstories, (usModel) =>
|
||||||
|
us = {}
|
||||||
|
us.foldStatusChanged = @.foldStatusChanged[usModel.id]
|
||||||
|
us.model = usModel.getAttrs()
|
||||||
|
us.images = _.filter usModel.attachments, (it) -> return !!it.thumbnail_card_url
|
||||||
|
us.id = usModel.id
|
||||||
|
us.assigned_to = @.usersById[usModel.assigned_to]
|
||||||
|
us.colorized_tags = _.map us.model.tags, (tag) =>
|
||||||
|
color = @.project.tags_colors[tag]
|
||||||
|
return {name: tag, color: color}
|
||||||
|
|
||||||
|
return us
|
||||||
|
|
||||||
|
usByStatus = _.groupBy userstories, (us) ->
|
||||||
|
return us.model.status
|
||||||
|
|
||||||
|
@.usByStatus = Immutable.fromJS(usByStatus)
|
||||||
|
|
||||||
|
angular.module("taigaKanban").service("tgKanbanUserstories", KanbanUserstoriesService)
|
|
@ -34,26 +34,18 @@ bindMethods = @.taiga.bindMethods
|
||||||
|
|
||||||
module = angular.module("taigaKanban")
|
module = angular.module("taigaKanban")
|
||||||
|
|
||||||
# Vars
|
|
||||||
|
|
||||||
defaultViewMode = "maximized"
|
|
||||||
viewModes = [
|
|
||||||
"maximized",
|
|
||||||
"minimized"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
## Kanban Controller
|
## Kanban Controller
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
|
||||||
class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin)
|
class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin, taiga.UsFiltersMixin)
|
||||||
@.$inject = [
|
@.$inject = [
|
||||||
"$scope",
|
"$scope",
|
||||||
"$rootScope",
|
"$rootScope",
|
||||||
"$tgRepo",
|
"$tgRepo",
|
||||||
"$tgConfirm",
|
"$tgConfirm",
|
||||||
"$tgResources",
|
"$tgResources",
|
||||||
|
"tgResources",
|
||||||
"$routeParams",
|
"$routeParams",
|
||||||
"$q",
|
"$q",
|
||||||
"$tgLocation",
|
"$tgLocation",
|
||||||
|
@ -62,16 +54,26 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
||||||
"$tgEvents",
|
"$tgEvents",
|
||||||
"$tgAnalytics",
|
"$tgAnalytics",
|
||||||
"$translate",
|
"$translate",
|
||||||
"tgErrorHandlingService"
|
"tgErrorHandlingService",
|
||||||
|
"$tgModel",
|
||||||
|
"tgKanbanUserstories",
|
||||||
|
"$tgStorage",
|
||||||
|
"tgFilterRemoteStorageService"
|
||||||
]
|
]
|
||||||
|
|
||||||
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location,
|
storeCustomFiltersName: 'kanban-custom-filters'
|
||||||
@appMetaService, @navUrls, @events, @analytics, @translate, @errorHandlingService) ->
|
storeFiltersName: 'kanban-filters'
|
||||||
|
|
||||||
|
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @rs2, @params, @q, @location,
|
||||||
|
@appMetaService, @navUrls, @events, @analytics, @translate, @errorHandlingService,
|
||||||
|
@model, @kanbanUserstoriesService, @storage, @filterRemoteStorageService) ->
|
||||||
bindMethods(@)
|
bindMethods(@)
|
||||||
|
@kanbanUserstoriesService.reset()
|
||||||
|
@.openFilter = false
|
||||||
|
|
||||||
|
return if @.applyStoredFilters(@params.pslug, "kanban-filters")
|
||||||
|
|
||||||
@scope.sectionName = @translate.instant("KANBAN.SECTION_NAME")
|
@scope.sectionName = @translate.instant("KANBAN.SECTION_NAME")
|
||||||
@scope.statusViewModes = {}
|
|
||||||
@.initializeEventHandlers()
|
@.initializeEventHandlers()
|
||||||
|
|
||||||
promise = @.loadInitialData()
|
promise = @.loadInitialData()
|
||||||
|
@ -88,80 +90,106 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
||||||
# On Error
|
# On Error
|
||||||
promise.then null, @.onInitialDataError.bind(@)
|
promise.then null, @.onInitialDataError.bind(@)
|
||||||
|
|
||||||
|
taiga.defineImmutableProperty @.scope, "usByStatus", () =>
|
||||||
|
return @kanbanUserstoriesService.usByStatus
|
||||||
|
|
||||||
|
setZoom: (zoomLevel, zoom) ->
|
||||||
|
if @.zoomLevel != zoomLevel
|
||||||
|
@kanbanUserstoriesService.resetFolds()
|
||||||
|
|
||||||
|
@.zoomLevel = zoomLevel
|
||||||
|
@.zoom = zoom
|
||||||
|
|
||||||
|
filtersReloadContent: () ->
|
||||||
|
@.loadUserstories().then () =>
|
||||||
|
openArchived = _.difference(@kanbanUserstoriesService.archivedStatus, @kanbanUserstoriesService.statusHide)
|
||||||
|
if openArchived.length
|
||||||
|
for statusId in openArchived
|
||||||
|
@.loadUserStoriesForStatus({}, statusId)
|
||||||
|
|
||||||
initializeEventHandlers: ->
|
initializeEventHandlers: ->
|
||||||
@scope.$on "usform:new:success", =>
|
@scope.$on "usform:new:success", (event, us) =>
|
||||||
@.loadUserstories()
|
@.refreshTagsColors().then () =>
|
||||||
@.refreshTagsColors()
|
@kanbanUserstoriesService.add(us)
|
||||||
|
|
||||||
@analytics.trackEvent("userstory", "create", "create userstory on kanban", 1)
|
@analytics.trackEvent("userstory", "create", "create userstory on kanban", 1)
|
||||||
|
|
||||||
@scope.$on "usform:bulk:success", =>
|
@scope.$on "usform:bulk:success", (event, uss) =>
|
||||||
@.loadUserstories()
|
@.refreshTagsColors().then () =>
|
||||||
|
@kanbanUserstoriesService.add(uss)
|
||||||
|
|
||||||
@analytics.trackEvent("userstory", "create", "bulk create userstory on kanban", 1)
|
@analytics.trackEvent("userstory", "create", "bulk create userstory on kanban", 1)
|
||||||
|
|
||||||
@scope.$on "usform:edit:success", =>
|
@scope.$on "usform:edit:success", (event, us) =>
|
||||||
@.loadUserstories()
|
@.refreshTagsColors().then () =>
|
||||||
@.refreshTagsColors()
|
@kanbanUserstoriesService.replaceModel(us)
|
||||||
|
|
||||||
@scope.$on("assigned-to:added", @.onAssignedToChanged)
|
@scope.$on("assigned-to:added", @.onAssignedToChanged)
|
||||||
@scope.$on("kanban:us:move", @.moveUs)
|
@scope.$on("kanban:us:move", @.moveUs)
|
||||||
@scope.$on("kanban:show-userstories-for-status", @.loadUserStoriesForStatus)
|
@scope.$on("kanban:show-userstories-for-status", @.loadUserStoriesForStatus)
|
||||||
@scope.$on("kanban:hide-userstories-for-status", @.hideUserStoriesForStatus)
|
@scope.$on("kanban:hide-userstories-for-status", @.hideUserStoriesForStatus)
|
||||||
|
|
||||||
# Template actions
|
|
||||||
|
|
||||||
addNewUs: (type, statusId) ->
|
addNewUs: (type, statusId) ->
|
||||||
switch type
|
switch type
|
||||||
when "standard" then @rootscope.$broadcast("usform:new", @scope.projectId, statusId, @scope.usStatusList)
|
when "standard" then @rootscope.$broadcast("usform:new", @scope.projectId, statusId, @scope.usStatusList)
|
||||||
when "bulk" then @rootscope.$broadcast("usform:bulk", @scope.projectId, statusId)
|
when "bulk" then @rootscope.$broadcast("usform:bulk", @scope.projectId, statusId)
|
||||||
|
|
||||||
changeUsAssignedTo: (us) ->
|
editUs: (id) ->
|
||||||
|
us = @kanbanUserstoriesService.getUs(id)
|
||||||
|
us = us.set('loading', true)
|
||||||
|
@kanbanUserstoriesService.replace(us)
|
||||||
|
|
||||||
|
@rs.userstories.getByRef(us.getIn(['model', 'project']), us.getIn(['model', 'ref']))
|
||||||
|
.then (editingUserStory) =>
|
||||||
|
@rs2.attachments.list("us", us.get('id'), us.getIn(['model', 'project'])).then (attachments) =>
|
||||||
|
@rootscope.$broadcast("usform:edit", editingUserStory, attachments.toJS())
|
||||||
|
|
||||||
|
us = us.set('loading', false)
|
||||||
|
@kanbanUserstoriesService.replace(us)
|
||||||
|
|
||||||
|
showPlaceHolder: (statusId) ->
|
||||||
|
if @scope.usStatusList[0].id == statusId &&
|
||||||
|
!@kanbanUserstoriesService.userstoriesRaw.length
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
toggleFold: (id) ->
|
||||||
|
@kanbanUserstoriesService.toggleFold(id)
|
||||||
|
|
||||||
|
isUsInArchivedHiddenStatus: (usId) ->
|
||||||
|
return @kanbanUserstoriesService.isUsInArchivedHiddenStatus(usId)
|
||||||
|
|
||||||
|
changeUsAssignedTo: (id) ->
|
||||||
|
us = @kanbanUserstoriesService.getUsModel(id)
|
||||||
|
|
||||||
@rootscope.$broadcast("assigned-to:add", us)
|
@rootscope.$broadcast("assigned-to:add", us)
|
||||||
|
|
||||||
# Scope Events Handlers
|
onAssignedToChanged: (ctx, userid, usModel) ->
|
||||||
|
usModel.assigned_to = userid
|
||||||
|
|
||||||
onAssignedToChanged: (ctx, userid, us) ->
|
@kanbanUserstoriesService.replaceModel(usModel)
|
||||||
us.assigned_to = userid
|
|
||||||
|
|
||||||
promise = @repo.save(us)
|
promise = @repo.save(usModel)
|
||||||
promise.then null, ->
|
promise.then null, ->
|
||||||
console.log "FAIL" # TODO
|
console.log "FAIL" # TODO
|
||||||
|
|
||||||
# Load data methods
|
|
||||||
refreshTagsColors: ->
|
refreshTagsColors: ->
|
||||||
return @rs.projects.tagsColors(@scope.projectId).then (tags_colors) =>
|
return @rs.projects.tagsColors(@scope.projectId).then (tags_colors) =>
|
||||||
@scope.project.tags_colors = tags_colors
|
@scope.project.tags_colors = tags_colors
|
||||||
|
|
||||||
loadUserstories: ->
|
loadUserstories: ->
|
||||||
params = {
|
params = {
|
||||||
status__is_archived: false
|
status__is_archived: false,
|
||||||
|
include_attachments: true,
|
||||||
|
include_tasks: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
params = _.merge params, @location.search()
|
||||||
|
|
||||||
promise = @rs.userstories.listAll(@scope.projectId, params).then (userstories) =>
|
promise = @rs.userstories.listAll(@scope.projectId, params).then (userstories) =>
|
||||||
@scope.userstories = userstories
|
@kanbanUserstoriesService.init(@scope.project, @scope.usersById)
|
||||||
|
@kanbanUserstoriesService.set(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? 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")
|
|
||||||
|
|
||||||
if userstories.length == 0
|
|
||||||
status = @scope.usStatusList[0]
|
|
||||||
usByStatus[status.id].push({isPlaceholder: true})
|
|
||||||
|
|
||||||
@scope.usByStatus = usByStatus
|
|
||||||
|
|
||||||
# The broadcast must be executed when the DOM has been fully reloaded.
|
# The broadcast must be executed when the DOM has been fully reloaded.
|
||||||
# We can't assure when this exactly happens so we need a defer
|
# We can't assure when this exactly happens so we need a defer
|
||||||
|
@ -175,14 +203,28 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
||||||
return promise
|
return promise
|
||||||
|
|
||||||
loadUserStoriesForStatus: (ctx, statusId) ->
|
loadUserStoriesForStatus: (ctx, statusId) ->
|
||||||
params = { status: statusId }
|
filteredStatus = @location.search().status
|
||||||
|
|
||||||
|
# if there are filters applied the action doesn't end if the statusId is not in the url
|
||||||
|
if filteredStatus
|
||||||
|
filteredStatus = filteredStatus.split(",").map (it) -> parseInt(it, 10)
|
||||||
|
|
||||||
|
return if filteredStatus.indexOf(statusId) == -1
|
||||||
|
|
||||||
|
params = {
|
||||||
|
status: statusId
|
||||||
|
include_attachments: true,
|
||||||
|
include_tasks: true
|
||||||
|
}
|
||||||
|
|
||||||
|
params = _.merge params, @location.search()
|
||||||
|
|
||||||
return @rs.userstories.listAll(@scope.projectId, params).then (userstories) =>
|
return @rs.userstories.listAll(@scope.projectId, params).then (userstories) =>
|
||||||
@scope.usByStatus[statusId] = _.sortBy(userstories, "kanban_order")
|
|
||||||
@scope.$broadcast("kanban:shown-userstories-for-status", statusId, userstories)
|
@scope.$broadcast("kanban:shown-userstories-for-status", statusId, userstories)
|
||||||
|
|
||||||
return userstories
|
return userstories
|
||||||
|
|
||||||
hideUserStoriesForStatus: (ctx, statusId) ->
|
hideUserStoriesForStatus: (ctx, statusId) ->
|
||||||
@scope.usByStatus[statusId] = []
|
|
||||||
@scope.$broadcast("kanban:hidden-userstories-for-status", statusId)
|
@scope.$broadcast("kanban:hidden-userstories-for-status", statusId)
|
||||||
|
|
||||||
loadKanban: ->
|
loadKanban: ->
|
||||||
|
@ -204,8 +246,6 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
||||||
@scope.usStatusById = groupBy(project.us_statuses, (x) -> x.id)
|
@scope.usStatusById = groupBy(project.us_statuses, (x) -> x.id)
|
||||||
@scope.usStatusList = _.sortBy(project.us_statuses, "order")
|
@scope.usStatusList = _.sortBy(project.us_statuses, "order")
|
||||||
|
|
||||||
@.generateStatusViewModes()
|
|
||||||
|
|
||||||
@scope.$emit("project:loaded", project)
|
@scope.$emit("project:loaded", project)
|
||||||
return project
|
return project
|
||||||
|
|
||||||
|
@ -220,82 +260,40 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
||||||
@.fillUsersAndRoles(project.members, project.roles)
|
@.fillUsersAndRoles(project.members, project.roles)
|
||||||
@.initializeSubscription()
|
@.initializeSubscription()
|
||||||
@.loadKanban()
|
@.loadKanban()
|
||||||
|
@.generateFilters()
|
||||||
|
|
||||||
## View Mode methods
|
|
||||||
|
|
||||||
generateStatusViewModes: ->
|
|
||||||
storedStatusViewModes = @rs.kanban.getStatusViewModes(@scope.projectId)
|
|
||||||
|
|
||||||
@scope.statusViewModes = {}
|
|
||||||
for status in @scope.usStatusList
|
|
||||||
mode = storedStatusViewModes[status.id] || defaultViewMode
|
|
||||||
|
|
||||||
@scope.statusViewModes[status.id] = mode
|
|
||||||
|
|
||||||
@.storeStatusViewModes()
|
|
||||||
|
|
||||||
storeStatusViewModes: ->
|
|
||||||
@rs.kanban.storeStatusViewModes(@scope.projectId, @scope.statusViewModes)
|
|
||||||
|
|
||||||
updateStatusViewMode: (statusId, newViewMode) ->
|
|
||||||
@scope.statusViewModes[statusId] = newViewMode
|
|
||||||
@.storeStatusViewModes()
|
|
||||||
|
|
||||||
isMaximized: (statusId) ->
|
|
||||||
mode = @scope.statusViewModes[statusId] or defaultViewMode
|
|
||||||
return mode == 'maximized'
|
|
||||||
|
|
||||||
isMinimized: (statusId) ->
|
|
||||||
mode = @scope.statusViewModes[statusId] or defaultViewMode
|
|
||||||
return mode == 'minimized'
|
|
||||||
|
|
||||||
# Utils methods
|
# Utils methods
|
||||||
|
|
||||||
prepareBulkUpdateData: (uses, field="kanban_order") ->
|
prepareBulkUpdateData: (uses, field="kanban_order") ->
|
||||||
return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]})
|
return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]})
|
||||||
|
|
||||||
resortUserStories: (uses) ->
|
|
||||||
items = []
|
|
||||||
for item, index in uses
|
|
||||||
item.kanban_order = index
|
|
||||||
if item.isModified()
|
|
||||||
items.push(item)
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
moveUs: (ctx, us, oldStatusId, newStatusId, index) ->
|
moveUs: (ctx, us, oldStatusId, newStatusId, index) ->
|
||||||
if oldStatusId != newStatusId
|
us = @kanbanUserstoriesService.getUsModel(us.get('id'))
|
||||||
# Remove us from old status column
|
|
||||||
r = @scope.usByStatus[oldStatusId].indexOf(us)
|
|
||||||
@scope.usByStatus[oldStatusId].splice(r, 1)
|
|
||||||
|
|
||||||
# Add us to new status column.
|
moveUpdateData = @kanbanUserstoriesService.move(us.id, newStatusId, index)
|
||||||
@scope.usByStatus[newStatusId].splice(index, 0, us)
|
|
||||||
us.status = newStatusId
|
|
||||||
else
|
|
||||||
r = @scope.usByStatus[newStatusId].indexOf(us)
|
|
||||||
@scope.usByStatus[newStatusId].splice(r, 1)
|
|
||||||
@scope.usByStatus[newStatusId].splice(index, 0, us)
|
|
||||||
|
|
||||||
itemsToSave = @.resortUserStories(@scope.usByStatus[newStatusId])
|
params = {
|
||||||
@scope.usByStatus[newStatusId] = _.sortBy(@scope.usByStatus[newStatusId], "kanban_order")
|
include_attachments: true,
|
||||||
|
include_tasks: true
|
||||||
|
}
|
||||||
|
|
||||||
# Persist the userstory
|
options = {
|
||||||
promise = @repo.save(us)
|
headers: {
|
||||||
|
"set-orders": JSON.stringify(moveUpdateData.set_orders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Rehash userstories order field
|
promise = @repo.save(us, true, params, options, true)
|
||||||
# and persist in bulk all changes.
|
|
||||||
promise = promise.then =>
|
|
||||||
itemsToSave = _.reject(itemsToSave, {"id": us.id})
|
|
||||||
data = @.prepareBulkUpdateData(itemsToSave)
|
|
||||||
|
|
||||||
return @rs.userstories.bulkUpdateKanbanOrder(us.project, data).then =>
|
promise = promise.then (result) =>
|
||||||
return itemsToSave
|
headers = result[1]
|
||||||
|
|
||||||
|
if headers && headers['taiga-info-order-updated']
|
||||||
|
order = JSON.parse(headers['taiga-info-order-updated'])
|
||||||
|
@kanbanUserstoriesService.assignOrders(order)
|
||||||
|
|
||||||
return promise
|
return promise
|
||||||
|
|
||||||
|
|
||||||
module.controller("KanbanController", KanbanController)
|
module.controller("KanbanController", KanbanController)
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
@ -322,7 +320,7 @@ module.directive("tgKanban", ["$tgRepo", "$rootScope", KanbanDirective])
|
||||||
## Kanban Archived Status Column Header Control
|
## Kanban Archived Status Column Header Control
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
|
||||||
KanbanArchivedStatusHeaderDirective = ($rootscope, $translate) ->
|
KanbanArchivedStatusHeaderDirective = ($rootscope, $translate, kanbanUserstoriesService) ->
|
||||||
showArchivedText = $translate.instant("KANBAN.ACTION_SHOW_ARCHIVED")
|
showArchivedText = $translate.instant("KANBAN.ACTION_SHOW_ARCHIVED")
|
||||||
hideArchivedText = $translate.instant("KANBAN.ACTION_HIDE_ARCHIVED")
|
hideArchivedText = $translate.instant("KANBAN.ACTION_HIDE_ARCHIVED")
|
||||||
|
|
||||||
|
@ -330,6 +328,9 @@ KanbanArchivedStatusHeaderDirective = ($rootscope, $translate) ->
|
||||||
status = $scope.$eval($attrs.tgKanbanArchivedStatusHeader)
|
status = $scope.$eval($attrs.tgKanbanArchivedStatusHeader)
|
||||||
hidden = true
|
hidden = true
|
||||||
|
|
||||||
|
kanbanUserstoriesService.addArchivedStatus(status.id)
|
||||||
|
kanbanUserstoriesService.hideStatus(status.id)
|
||||||
|
|
||||||
$scope.class = "icon-watch"
|
$scope.class = "icon-watch"
|
||||||
$scope.title = showArchivedText
|
$scope.title = showArchivedText
|
||||||
|
|
||||||
|
@ -342,24 +343,27 @@ KanbanArchivedStatusHeaderDirective = ($rootscope, $translate) ->
|
||||||
$scope.title = showArchivedText
|
$scope.title = showArchivedText
|
||||||
$rootscope.$broadcast("kanban:hide-userstories-for-status", status.id)
|
$rootscope.$broadcast("kanban:hide-userstories-for-status", status.id)
|
||||||
|
|
||||||
|
kanbanUserstoriesService.hideStatus(status.id)
|
||||||
else
|
else
|
||||||
$scope.class = "icon-unwatch"
|
$scope.class = "icon-unwatch"
|
||||||
$scope.title = hideArchivedText
|
$scope.title = hideArchivedText
|
||||||
$rootscope.$broadcast("kanban:show-userstories-for-status", status.id)
|
$rootscope.$broadcast("kanban:show-userstories-for-status", status.id)
|
||||||
|
|
||||||
|
kanbanUserstoriesService.showStatus(status.id)
|
||||||
|
|
||||||
$scope.$on "$destroy", ->
|
$scope.$on "$destroy", ->
|
||||||
$el.off()
|
$el.off()
|
||||||
|
|
||||||
return {link:link}
|
return {link:link}
|
||||||
|
|
||||||
module.directive("tgKanbanArchivedStatusHeader", [ "$rootScope", "$translate", KanbanArchivedStatusHeaderDirective])
|
module.directive("tgKanbanArchivedStatusHeader", [ "$rootScope", "$translate", "tgKanbanUserstories", KanbanArchivedStatusHeaderDirective])
|
||||||
|
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
## Kanban Archived Status Column Intro Directive
|
## Kanban Archived Status Column Intro Directive
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
|
||||||
KanbanArchivedStatusIntroDirective = ($translate) ->
|
KanbanArchivedStatusIntroDirective = ($translate, kanbanUserstoriesService) ->
|
||||||
userStories = []
|
userStories = []
|
||||||
|
|
||||||
link = ($scope, $el, $attrs) ->
|
link = ($scope, $el, $attrs) ->
|
||||||
|
@ -367,105 +371,40 @@ KanbanArchivedStatusIntroDirective = ($translate) ->
|
||||||
status = $scope.$eval($attrs.tgKanbanArchivedStatusIntro)
|
status = $scope.$eval($attrs.tgKanbanArchivedStatusIntro)
|
||||||
$el.text(hiddenUserStoriexText)
|
$el.text(hiddenUserStoriexText)
|
||||||
|
|
||||||
updateIntroText = ->
|
updateIntroText = (hasArchived) ->
|
||||||
if userStories.length > 0
|
if hasArchived
|
||||||
$el.text("")
|
$el.text("")
|
||||||
else
|
else
|
||||||
$el.text(hiddenUserStoriexText)
|
$el.text(hiddenUserStoriexText)
|
||||||
|
|
||||||
$scope.$on "kanban:us:move", (ctx, itemUs, oldStatusId, newStatusId, itemIndex) ->
|
$scope.$on "kanban:us:move", (ctx, itemUs, oldStatusId, newStatusId, itemIndex) ->
|
||||||
# The destination columnd is this one
|
hasArchived = !!kanbanUserstoriesService.getStatus(newStatusId).length
|
||||||
if status.id == newStatusId
|
updateIntroText(hasArchived)
|
||||||
# Reorder
|
|
||||||
if status.id == oldStatusId
|
|
||||||
r = userStories.indexOf(itemUs)
|
|
||||||
userStories.splice(r, 1)
|
|
||||||
userStories.splice(itemIndex, 0, itemUs)
|
|
||||||
|
|
||||||
# Archiving user story
|
|
||||||
else
|
|
||||||
itemUs.isArchived = true
|
|
||||||
userStories.splice(itemIndex, 0, itemUs)
|
|
||||||
|
|
||||||
# Unarchiving user story
|
|
||||||
else if status.id == oldStatusId
|
|
||||||
itemUs.isArchived = false
|
|
||||||
r = userStories.indexOf(itemUs)
|
|
||||||
userStories.splice(r, 1)
|
|
||||||
|
|
||||||
updateIntroText()
|
|
||||||
|
|
||||||
$scope.$on "kanban:shown-userstories-for-status", (ctx, statusId, userStoriesLoaded) ->
|
$scope.$on "kanban:shown-userstories-for-status", (ctx, statusId, userStoriesLoaded) ->
|
||||||
if statusId == status.id
|
if statusId == status.id
|
||||||
userStories = _.filter(userStoriesLoaded, (us) -> us.status == status.id)
|
kanbanUserstoriesService.deleteStatus(statusId)
|
||||||
updateIntroText()
|
kanbanUserstoriesService.add(userStoriesLoaded)
|
||||||
|
|
||||||
|
hasArchived = !!kanbanUserstoriesService.getStatus(statusId).length
|
||||||
|
updateIntroText(hasArchived)
|
||||||
|
|
||||||
$scope.$on "kanban:hidden-userstories-for-status", (ctx, statusId) ->
|
$scope.$on "kanban:hidden-userstories-for-status", (ctx, statusId) ->
|
||||||
if statusId == status.id
|
if statusId == status.id
|
||||||
userStories = []
|
updateIntroText(false)
|
||||||
updateIntroText()
|
|
||||||
|
|
||||||
$scope.$on "$destroy", ->
|
$scope.$on "$destroy", ->
|
||||||
$el.off()
|
$el.off()
|
||||||
|
|
||||||
return {link:link}
|
return {link:link}
|
||||||
|
|
||||||
module.directive("tgKanbanArchivedStatusIntro", ["$translate", KanbanArchivedStatusIntroDirective])
|
module.directive("tgKanbanArchivedStatusIntro", ["$translate", "tgKanbanUserstories", KanbanArchivedStatusIntroDirective])
|
||||||
|
|
||||||
|
|
||||||
#############################################################################
|
|
||||||
## Kanban User Story Directive
|
|
||||||
#############################################################################
|
|
||||||
|
|
||||||
KanbanUserstoryDirective = ($rootscope, $loading, $rs, $rs2) ->
|
|
||||||
link = ($scope, $el, $attrs, $model) ->
|
|
||||||
$scope.$watch "us", (us) ->
|
|
||||||
if us.is_blocked and not $el.hasClass("blocked")
|
|
||||||
$el.addClass("blocked")
|
|
||||||
else if not us.is_blocked and $el.hasClass("blocked")
|
|
||||||
$el.removeClass("blocked")
|
|
||||||
|
|
||||||
$el.on 'click', '.edit-us', (event) ->
|
|
||||||
if $el.find(".icon-edit").hasClass("noclick")
|
|
||||||
return
|
|
||||||
|
|
||||||
target = $(event.target)
|
|
||||||
|
|
||||||
currentLoading = $loading()
|
|
||||||
.target(target)
|
|
||||||
.timeout(200)
|
|
||||||
.removeClasses("icon-edit")
|
|
||||||
.start()
|
|
||||||
|
|
||||||
us = $model.$modelValue
|
|
||||||
$rs.userstories.getByRef(us.project, us.ref).then (editingUserStory) =>
|
|
||||||
$rs2.attachments.list("us", us.id, us.project).then (attachments) =>
|
|
||||||
$rootscope.$broadcast("usform:edit", editingUserStory, attachments.toJS())
|
|
||||||
currentLoading.finish()
|
|
||||||
|
|
||||||
$scope.getTemplateUrl = () ->
|
|
||||||
if $scope.us.isPlaceholder
|
|
||||||
return "common/components/kanban-placeholder.html"
|
|
||||||
else
|
|
||||||
return "kanban/kanban-task.html"
|
|
||||||
|
|
||||||
$scope.$on "$destroy", ->
|
|
||||||
$el.off()
|
|
||||||
|
|
||||||
return {
|
|
||||||
template: '<ng-include src="getTemplateUrl()"/>',
|
|
||||||
link: link
|
|
||||||
require: "ngModel"
|
|
||||||
}
|
|
||||||
|
|
||||||
module.directive("tgKanbanUserstory", ["$rootScope", "$tgLoading", "$tgResources", "tgResources", KanbanUserstoryDirective])
|
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
## Kanban Squish Column Directive
|
## Kanban Squish Column Directive
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
|
||||||
KanbanSquishColumnDirective = (rs) ->
|
KanbanSquishColumnDirective = (rs) ->
|
||||||
|
|
||||||
link = ($scope, $el, $attrs) ->
|
link = ($scope, $el, $attrs) ->
|
||||||
$scope.$on "project:loaded", (event, project) ->
|
$scope.$on "project:loaded", (event, project) ->
|
||||||
$scope.folds = rs.kanban.getStatusColumnModes(project.id)
|
$scope.folds = rs.kanban.getStatusColumnModes(project.id)
|
||||||
|
@ -485,6 +424,7 @@ KanbanSquishColumnDirective = (rs) ->
|
||||||
return 310
|
return 310
|
||||||
totalWidth = _.reduce columnWidths, (total, width) ->
|
totalWidth = _.reduce columnWidths, (total, width) ->
|
||||||
return total + width
|
return total + width
|
||||||
|
|
||||||
$el.find('.kanban-table-inner').css("width", totalWidth)
|
$el.find('.kanban-table-inner').css("width", totalWidth)
|
||||||
|
|
||||||
return {link: link}
|
return {link: link}
|
||||||
|
@ -502,7 +442,7 @@ KanbanWipLimitDirective = ->
|
||||||
redrawWipLimit = =>
|
redrawWipLimit = =>
|
||||||
$el.find(".kanban-wip-limit").remove()
|
$el.find(".kanban-wip-limit").remove()
|
||||||
timeout 200, =>
|
timeout 200, =>
|
||||||
element = $el.find(".kanban-task")[status.wip_limit]
|
element = $el.find("tg-card")[status.wip_limit]
|
||||||
if element
|
if element
|
||||||
angular.element(element).before("<div class='kanban-wip-limit'></div>")
|
angular.element(element).before("<div class='kanban-wip-limit'></div>")
|
||||||
|
|
||||||
|
@ -518,83 +458,3 @@ KanbanWipLimitDirective = ->
|
||||||
return {link: link}
|
return {link: link}
|
||||||
|
|
||||||
module.directive("tgKanbanWipLimit", KanbanWipLimitDirective)
|
module.directive("tgKanbanWipLimit", KanbanWipLimitDirective)
|
||||||
|
|
||||||
|
|
||||||
#############################################################################
|
|
||||||
## Kanban User Directive
|
|
||||||
#############################################################################
|
|
||||||
|
|
||||||
KanbanUserDirective = ($log, $compile, $translate, avatarService) ->
|
|
||||||
template = _.template("""
|
|
||||||
<figure class="avatar">
|
|
||||||
<a href="#" title="{{'US.ASSIGN' | translate}}" <% if (!clickable) {%>class="not-clickable"<% } %>>
|
|
||||||
<img style="background-color: <%- bg %>" src="<%- imgurl %>" alt="<%- name %>" class="avatar">
|
|
||||||
</a>
|
|
||||||
</figure>
|
|
||||||
""")
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
wtid = $scope.$watch $attrs.tgKanbanUserAvatar, (v) ->
|
|
||||||
if not $scope.usersById?
|
|
||||||
$log.error "KanbanUserDirective requires userById set in scope."
|
|
||||||
wtid()
|
|
||||||
else
|
|
||||||
user = $scope.usersById[v]
|
|
||||||
render(user)
|
|
||||||
|
|
||||||
render = (user) ->
|
|
||||||
avatar = avatarService.getAvatar(user)
|
|
||||||
|
|
||||||
if user is undefined
|
|
||||||
ctx = {
|
|
||||||
name: $translate.instant("COMMON.ASSIGNED_TO.NOT_ASSIGNED"),
|
|
||||||
imgurl: avatar.url,
|
|
||||||
clickable: clickable,
|
|
||||||
bg: null
|
|
||||||
}
|
|
||||||
else
|
|
||||||
ctx = {
|
|
||||||
name: user.full_name_display,
|
|
||||||
imgurl: avatar.url,
|
|
||||||
bg: avatar.bg,
|
|
||||||
clickable: clickable
|
|
||||||
}
|
|
||||||
|
|
||||||
html = $compile(template(ctx))($scope)
|
|
||||||
$el.html(html)
|
|
||||||
username_label.text(ctx.name)
|
|
||||||
|
|
||||||
bindOnce $scope, "project", (project) ->
|
|
||||||
if project.my_permissions.indexOf("modify_us") > -1
|
|
||||||
clickable = true
|
|
||||||
$el.on "click", (event) =>
|
|
||||||
if $el.find("a").hasClass("noclick")
|
|
||||||
return
|
|
||||||
|
|
||||||
us = $model.$modelValue
|
|
||||||
$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()
|
|
||||||
|
|
||||||
return {link: link, require:"ngModel"}
|
|
||||||
|
|
||||||
module.directive("tgKanbanUserAvatar", ["$log", "$compile", "$translate", "tgAvatarService", KanbanUserDirective])
|
|
||||||
|
|
|
@ -40,8 +40,12 @@ module = angular.module("taigaKanban")
|
||||||
|
|
||||||
KanbanSortableDirective = ($repo, $rs, $rootscope) ->
|
KanbanSortableDirective = ($repo, $rs, $rootscope) ->
|
||||||
link = ($scope, $el, $attrs) ->
|
link = ($scope, $el, $attrs) ->
|
||||||
bindOnce $scope, "project", (project) ->
|
unwatch = $scope.$watch "usByStatus", (usByStatus) ->
|
||||||
if not (project.my_permissions.indexOf("modify_us") > -1)
|
return if !usByStatus || !usByStatus.size
|
||||||
|
|
||||||
|
unwatch()
|
||||||
|
|
||||||
|
if not ($scope.project.my_permissions.indexOf("modify_us") > -1)
|
||||||
return
|
return
|
||||||
|
|
||||||
oldParentScope = null
|
oldParentScope = null
|
||||||
|
@ -63,7 +67,7 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) ->
|
||||||
copy: false,
|
copy: false,
|
||||||
mirrorContainer: tdom[0],
|
mirrorContainer: tdom[0],
|
||||||
moves: (item) ->
|
moves: (item) ->
|
||||||
return $(item).hasClass('kanban-task')
|
return $(item).is('tg-card')
|
||||||
})
|
})
|
||||||
|
|
||||||
drake.on 'drag', (item) ->
|
drake.on 'drag', (item) ->
|
||||||
|
@ -83,7 +87,7 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) ->
|
||||||
deleteElement(itemEl)
|
deleteElement(itemEl)
|
||||||
|
|
||||||
$scope.$apply ->
|
$scope.$apply ->
|
||||||
$rootscope.$broadcast("kanban:us:move", itemUs, itemUs.status, newStatusId, itemIndex)
|
$rootscope.$broadcast("kanban:us:move", itemUs, itemUs.getIn(['model', 'status']), newStatusId, itemIndex)
|
||||||
|
|
||||||
scroll = autoScroll(containers, {
|
scroll = autoScroll(containers, {
|
||||||
margin: 100,
|
margin: 100,
|
||||||
|
|
|
@ -96,7 +96,8 @@ urls = {
|
||||||
"userstories": "/userstories"
|
"userstories": "/userstories"
|
||||||
"bulk-create-us": "/userstories/bulk_create"
|
"bulk-create-us": "/userstories/bulk_create"
|
||||||
"bulk-update-us-backlog-order": "/userstories/bulk_update_backlog_order"
|
"bulk-update-us-backlog-order": "/userstories/bulk_update_backlog_order"
|
||||||
"bulk-update-us-sprint-order": "/userstories/bulk_update_sprint_order"
|
"bulk-update-us-milestone": "/userstories/bulk_update_milestone"
|
||||||
|
"bulk-update-us-miles-order": "/userstories/bulk_update_sprint_order"
|
||||||
"bulk-update-us-kanban-order": "/userstories/bulk_update_kanban_order"
|
"bulk-update-us-kanban-order": "/userstories/bulk_update_kanban_order"
|
||||||
"userstories-filters": "/userstories/filters_data"
|
"userstories-filters": "/userstories/filters_data"
|
||||||
"userstory-upvote": "/userstories/%s/upvote"
|
"userstory-upvote": "/userstories/%s/upvote"
|
||||||
|
@ -112,6 +113,7 @@ urls = {
|
||||||
"task-downvote": "/tasks/%s/downvote"
|
"task-downvote": "/tasks/%s/downvote"
|
||||||
"task-watch": "/tasks/%s/watch"
|
"task-watch": "/tasks/%s/watch"
|
||||||
"task-unwatch": "/tasks/%s/unwatch"
|
"task-unwatch": "/tasks/%s/unwatch"
|
||||||
|
"task-filters": "/tasks/filters_data"
|
||||||
|
|
||||||
# Issues
|
# Issues
|
||||||
"issues": "/issues"
|
"issues": "/issues"
|
||||||
|
|
|
@ -30,8 +30,6 @@ generateHash = taiga.generateHash
|
||||||
resourceProvider = ($repo, $http, $urls, $storage, $q) ->
|
resourceProvider = ($repo, $http, $urls, $storage, $q) ->
|
||||||
service = {}
|
service = {}
|
||||||
hashSuffix = "issues-queryparams"
|
hashSuffix = "issues-queryparams"
|
||||||
filtersHashSuffix = "issues-filters"
|
|
||||||
myFiltersHashSuffix = "issues-my-filters"
|
|
||||||
|
|
||||||
service.get = (projectId, issueId) ->
|
service.get = (projectId, issueId) ->
|
||||||
params = service.getQueryParams(projectId)
|
params = service.getQueryParams(projectId)
|
||||||
|
@ -95,53 +93,6 @@ resourceProvider = ($repo, $http, $urls, $storage, $q) ->
|
||||||
hash = generateHash([projectId, ns])
|
hash = generateHash([projectId, ns])
|
||||||
return $storage.get(hash) or {}
|
return $storage.get(hash) or {}
|
||||||
|
|
||||||
service.storeFilters = (projectSlug, params) ->
|
|
||||||
ns = "#{projectSlug}:#{filtersHashSuffix}"
|
|
||||||
hash = generateHash([projectSlug, ns])
|
|
||||||
$storage.set(hash, params)
|
|
||||||
|
|
||||||
service.getFilters = (projectSlug) ->
|
|
||||||
ns = "#{projectSlug}:#{filtersHashSuffix}"
|
|
||||||
hash = generateHash([projectSlug, ns])
|
|
||||||
return $storage.get(hash) or {}
|
|
||||||
|
|
||||||
service.storeMyFilters = (projectId, myFilters) ->
|
|
||||||
deferred = $q.defer()
|
|
||||||
url = $urls.resolve("user-storage")
|
|
||||||
ns = "#{projectId}:#{myFiltersHashSuffix}"
|
|
||||||
hash = generateHash([projectId, ns])
|
|
||||||
if _.isEmpty(myFilters)
|
|
||||||
promise = $http.delete("#{url}/#{hash}", {key: hash, value:myFilters})
|
|
||||||
promise.then ->
|
|
||||||
deferred.resolve()
|
|
||||||
promise.then null, ->
|
|
||||||
deferred.reject()
|
|
||||||
else
|
|
||||||
promise = $http.put("#{url}/#{hash}", {key: hash, value:myFilters})
|
|
||||||
promise.then (data) ->
|
|
||||||
deferred.resolve()
|
|
||||||
promise.then null, (data) ->
|
|
||||||
innerPromise = $http.post("#{url}", {key: hash, value:myFilters})
|
|
||||||
innerPromise.then ->
|
|
||||||
deferred.resolve()
|
|
||||||
innerPromise.then null, ->
|
|
||||||
deferred.reject()
|
|
||||||
return deferred.promise
|
|
||||||
|
|
||||||
service.getMyFilters = (projectId) ->
|
|
||||||
deferred = $q.defer()
|
|
||||||
url = $urls.resolve("user-storage")
|
|
||||||
ns = "#{projectId}:#{myFiltersHashSuffix}"
|
|
||||||
hash = generateHash([projectId, ns])
|
|
||||||
|
|
||||||
promise = $http.get("#{url}/#{hash}")
|
|
||||||
promise.then (data) ->
|
|
||||||
deferred.resolve(data.data.value)
|
|
||||||
promise.then null, (data) ->
|
|
||||||
deferred.resolve({})
|
|
||||||
|
|
||||||
return deferred.promise
|
|
||||||
|
|
||||||
return (instance) ->
|
return (instance) ->
|
||||||
instance.issues = service
|
instance.issues = service
|
||||||
|
|
||||||
|
|
|
@ -32,16 +32,6 @@ resourceProvider = ($storage) ->
|
||||||
hashSuffixStatusViewModes = "kanban-statusviewmodels"
|
hashSuffixStatusViewModes = "kanban-statusviewmodels"
|
||||||
hashSuffixStatusColumnModes = "kanban-statuscolumnmodels"
|
hashSuffixStatusColumnModes = "kanban-statuscolumnmodels"
|
||||||
|
|
||||||
service.storeStatusViewModes = (projectId, params) ->
|
|
||||||
ns = "#{projectId}:#{hashSuffixStatusViewModes}"
|
|
||||||
hash = generateHash([projectId, ns])
|
|
||||||
$storage.set(hash, params)
|
|
||||||
|
|
||||||
service.getStatusViewModes = (projectId) ->
|
|
||||||
ns = "#{projectId}:#{hashSuffixStatusViewModes}"
|
|
||||||
hash = generateHash([projectId, ns])
|
|
||||||
return $storage.get(hash) or {}
|
|
||||||
|
|
||||||
service.storeStatusColumnModes = (projectId, params) ->
|
service.storeStatusColumnModes = (projectId, params) ->
|
||||||
ns = "#{projectId}:#{hashSuffixStatusColumnModes}"
|
ns = "#{projectId}:#{hashSuffixStatusColumnModes}"
|
||||||
hash = generateHash([projectId, ns])
|
hash = generateHash([projectId, ns])
|
||||||
|
|
|
@ -38,17 +38,23 @@ resourceProvider = ($repo, $http, $urls, $storage) ->
|
||||||
params.project = projectId
|
params.project = projectId
|
||||||
return $repo.queryOne("tasks", taskId, params)
|
return $repo.queryOne("tasks", taskId, params)
|
||||||
|
|
||||||
service.getByRef = (projectId, ref) ->
|
service.getByRef = (projectId, ref, extraParams) ->
|
||||||
params = service.getQueryParams(projectId)
|
params = service.getQueryParams(projectId)
|
||||||
params.project = projectId
|
params.project = projectId
|
||||||
params.ref = ref
|
params.ref = ref
|
||||||
|
|
||||||
|
params = _.extend({}, params, extraParams)
|
||||||
|
|
||||||
return $repo.queryOne("tasks", "by_ref", params)
|
return $repo.queryOne("tasks", "by_ref", params)
|
||||||
|
|
||||||
service.listInAllProjects = (filters) ->
|
service.listInAllProjects = (filters) ->
|
||||||
return $repo.queryMany("tasks", filters)
|
return $repo.queryMany("tasks", filters)
|
||||||
|
|
||||||
service.list = (projectId, sprintId=null, userStoryId=null) ->
|
service.filtersData = (params) ->
|
||||||
params = {project: projectId}
|
return $repo.queryOneRaw("task-filters", null, params)
|
||||||
|
|
||||||
|
service.list = (projectId, sprintId=null, userStoryId=null, params) ->
|
||||||
|
params = _.merge(params, {project: projectId})
|
||||||
params.milestone = sprintId if sprintId
|
params.milestone = sprintId if sprintId
|
||||||
params.user_story = userStoryId if userStoryId
|
params.user_story = userStoryId if userStoryId
|
||||||
service.storeQueryParams(projectId, params)
|
service.storeQueryParams(projectId, params)
|
||||||
|
|
|
@ -26,7 +26,7 @@ taiga = @.taiga
|
||||||
|
|
||||||
generateHash = taiga.generateHash
|
generateHash = taiga.generateHash
|
||||||
|
|
||||||
resourceProvider = ($repo, $http, $urls, $storage) ->
|
resourceProvider = ($repo, $http, $urls, $storage, $q) ->
|
||||||
service = {}
|
service = {}
|
||||||
hashSuffix = "userstories-queryparams"
|
hashSuffix = "userstories-queryparams"
|
||||||
|
|
||||||
|
@ -35,10 +35,12 @@ resourceProvider = ($repo, $http, $urls, $storage) ->
|
||||||
params.project = projectId
|
params.project = projectId
|
||||||
return $repo.queryOne("userstories", usId, params)
|
return $repo.queryOne("userstories", usId, params)
|
||||||
|
|
||||||
service.getByRef = (projectId, ref) ->
|
service.getByRef = (projectId, ref, extraParams = {}) ->
|
||||||
params = service.getQueryParams(projectId)
|
params = service.getQueryParams(projectId)
|
||||||
params.project = projectId
|
params.project = projectId
|
||||||
params.ref = ref
|
params.ref = ref
|
||||||
|
params = _.extend({}, params, extraParams)
|
||||||
|
|
||||||
return $repo.queryOne("userstories", "by_ref", params)
|
return $repo.queryOne("userstories", "by_ref", params)
|
||||||
|
|
||||||
service.listInAllProjects = (filters) ->
|
service.listInAllProjects = (filters) ->
|
||||||
|
@ -96,9 +98,9 @@ resourceProvider = ($repo, $http, $urls, $storage) ->
|
||||||
params = {project_id: projectId, bulk_stories: data}
|
params = {project_id: projectId, bulk_stories: data}
|
||||||
return $http.post(url, params)
|
return $http.post(url, params)
|
||||||
|
|
||||||
service.bulkUpdateSprintOrder = (projectId, data) ->
|
service.bulkUpdateMilestone = (projectId, milestoneId, data) ->
|
||||||
url = $urls.resolve("bulk-update-us-sprint-order")
|
url = $urls.resolve("bulk-update-us-milestone")
|
||||||
params = {project_id: projectId, bulk_stories: data}
|
params = {project_id: projectId, milestone_id: milestoneId, bulk_stories: data}
|
||||||
return $http.post(url, params)
|
return $http.post(url, params)
|
||||||
|
|
||||||
service.bulkUpdateKanbanOrder = (projectId, data) ->
|
service.bulkUpdateKanbanOrder = (projectId, data) ->
|
||||||
|
@ -133,4 +135,4 @@ resourceProvider = ($repo, $http, $urls, $storage) ->
|
||||||
instance.userstories = service
|
instance.userstories = service
|
||||||
|
|
||||||
module = angular.module("taigaResources")
|
module = angular.module("taigaResources")
|
||||||
module.factory("$tgUserstoriesResourcesProvider", ["$tgRepo", "$tgHttp", "$tgUrls", "$tgStorage", resourceProvider])
|
module.factory("$tgUserstoriesResourcesProvider", ["$tgRepo", "$tgHttp", "$tgUrls", "$tgStorage", "$q", resourceProvider])
|
||||||
|
|
|
@ -108,6 +108,11 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
|
||||||
if not form.validate()
|
if not form.validate()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
params = {
|
||||||
|
include_attachments: true,
|
||||||
|
include_tasks: true
|
||||||
|
}
|
||||||
|
|
||||||
if $scope.isNew
|
if $scope.isNew
|
||||||
promise = $repo.create("tasks", $scope.task)
|
promise = $repo.create("tasks", $scope.task)
|
||||||
broadcastEvent = "taskform:new:success"
|
broadcastEvent = "taskform:new:success"
|
||||||
|
@ -116,20 +121,22 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
|
||||||
broadcastEvent = "taskform:edit:success"
|
broadcastEvent = "taskform:edit:success"
|
||||||
|
|
||||||
promise.then (data) ->
|
promise.then (data) ->
|
||||||
createAttachments(data)
|
|
||||||
deleteAttachments(data)
|
deleteAttachments(data)
|
||||||
|
.then () => createAttachments(data)
|
||||||
|
.then () =>
|
||||||
|
currentLoading.finish()
|
||||||
|
lightboxService.close($el)
|
||||||
|
|
||||||
return data
|
$rs.tasks.getByRef(data.project, data.ref, params).then (task) ->
|
||||||
|
$rootscope.$broadcast(broadcastEvent, task)
|
||||||
|
|
||||||
currentLoading = $loading()
|
currentLoading = $loading()
|
||||||
.target(submitButton)
|
.target(submitButton)
|
||||||
.start()
|
.start()
|
||||||
|
|
||||||
# FIXME: error handling?
|
|
||||||
promise.then (data) ->
|
promise.then (data) ->
|
||||||
currentLoading.finish()
|
currentLoading.finish()
|
||||||
lightboxService.close($el)
|
lightboxService.close($el)
|
||||||
$rootscope.$broadcast(broadcastEvent, data)
|
|
||||||
|
|
||||||
$el.on "submit", "form", submit
|
$el.on "submit", "form", submit
|
||||||
|
|
||||||
|
@ -139,7 +146,7 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
|
||||||
return {link: link}
|
return {link: link}
|
||||||
|
|
||||||
|
|
||||||
CreateBulkTasksDirective = ($repo, $rs, $rootscope, $loading, lightboxService) ->
|
CreateBulkTasksDirective = ($repo, $rs, $rootscope, $loading, lightboxService, $model) ->
|
||||||
link = ($scope, $el, attrs) ->
|
link = ($scope, $el, attrs) ->
|
||||||
$scope.form = {data: "", usId: null}
|
$scope.form = {data: "", usId: null}
|
||||||
|
|
||||||
|
@ -161,6 +168,7 @@ CreateBulkTasksDirective = ($repo, $rs, $rootscope, $loading, lightboxService) -
|
||||||
|
|
||||||
promise = $rs.tasks.bulkCreate(projectId, sprintId, usId, data)
|
promise = $rs.tasks.bulkCreate(projectId, sprintId, usId, data)
|
||||||
promise.then (result) ->
|
promise.then (result) ->
|
||||||
|
result = _.map(result, (x) => $model.make_model('userstories', x))
|
||||||
currentLoading.finish()
|
currentLoading.finish()
|
||||||
$rootscope.$broadcast("taskform:bulk:success", result)
|
$rootscope.$broadcast("taskform:bulk:success", result)
|
||||||
lightboxService.close($el)
|
lightboxService.close($el)
|
||||||
|
@ -205,5 +213,6 @@ module.directive("tgLbCreateBulkTasks", [
|
||||||
"$rootScope",
|
"$rootScope",
|
||||||
"$tgLoading",
|
"$tgLoading",
|
||||||
"lightboxService",
|
"lightboxService",
|
||||||
|
"$tgModel",
|
||||||
CreateBulkTasksDirective
|
CreateBulkTasksDirective
|
||||||
])
|
])
|
||||||
|
|
|
@ -38,13 +38,14 @@ module = angular.module("taigaTaskboard")
|
||||||
## Taskboard Controller
|
## Taskboard Controller
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
|
||||||
class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
|
class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin)
|
||||||
@.$inject = [
|
@.$inject = [
|
||||||
"$scope",
|
"$scope",
|
||||||
"$rootScope",
|
"$rootScope",
|
||||||
"$tgRepo",
|
"$tgRepo",
|
||||||
"$tgConfirm",
|
"$tgConfirm",
|
||||||
"$tgResources",
|
"$tgResources",
|
||||||
|
"tgResources"
|
||||||
"$routeParams",
|
"$routeParams",
|
||||||
"$q",
|
"$q",
|
||||||
"tgAppMetaService",
|
"tgAppMetaService",
|
||||||
|
@ -53,12 +54,20 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
|
||||||
"$tgEvents"
|
"$tgEvents"
|
||||||
"$tgAnalytics",
|
"$tgAnalytics",
|
||||||
"$translate",
|
"$translate",
|
||||||
"tgErrorHandlingService"
|
"tgErrorHandlingService",
|
||||||
|
"tgTaskboardTasks",
|
||||||
|
"$tgStorage",
|
||||||
|
"tgFilterRemoteStorageService"
|
||||||
]
|
]
|
||||||
|
|
||||||
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @appMetaService, @location, @navUrls,
|
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @rs2, @params, @q, @appMetaService, @location, @navUrls,
|
||||||
@events, @analytics, @translate, @errorHandlingService) ->
|
@events, @analytics, @translate, @errorHandlingService, @taskboardTasksService, @storage, @filterRemoteStorageService) ->
|
||||||
bindMethods(@)
|
bindMethods(@)
|
||||||
|
@taskboardTasksService.reset()
|
||||||
|
@scope.userstories = []
|
||||||
|
@.openFilter = false
|
||||||
|
|
||||||
|
return if @.applyStoredFilters(@params.pslug, "tasks-filters")
|
||||||
|
|
||||||
@scope.sectionName = @translate.instant("TASKBOARD.SECTION_NAME")
|
@scope.sectionName = @translate.instant("TASKBOARD.SECTION_NAME")
|
||||||
@.initializeEventHandlers()
|
@.initializeEventHandlers()
|
||||||
|
@ -70,6 +79,150 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
|
||||||
# On Error
|
# On Error
|
||||||
promise.then null, @.onInitialDataError.bind(@)
|
promise.then null, @.onInitialDataError.bind(@)
|
||||||
|
|
||||||
|
taiga.defineImmutableProperty @.scope, "usTasks", () =>
|
||||||
|
return @taskboardTasksService.usTasks
|
||||||
|
|
||||||
|
setZoom: (zoomLevel, zoom) ->
|
||||||
|
if @.zoomLevel != zoomLevel
|
||||||
|
@taskboardTasksService.resetFolds()
|
||||||
|
|
||||||
|
@.zoomLevel = zoomLevel
|
||||||
|
@.zoom = zoom
|
||||||
|
|
||||||
|
if @.zoomLevel == '0'
|
||||||
|
@rootscope.$broadcast("sprint:zoom0")
|
||||||
|
|
||||||
|
changeQ: (q) ->
|
||||||
|
@.replaceFilter("q", q)
|
||||||
|
@.loadTasks()
|
||||||
|
@.generateFilters()
|
||||||
|
|
||||||
|
removeFilter: (filter) ->
|
||||||
|
@.unselectFilter(filter.dataType, filter.id)
|
||||||
|
@.loadTasks()
|
||||||
|
@.generateFilters()
|
||||||
|
|
||||||
|
addFilter: (newFilter) ->
|
||||||
|
@.selectFilter(newFilter.category.dataType, newFilter.filter.id)
|
||||||
|
@.loadTasks()
|
||||||
|
@.generateFilters()
|
||||||
|
|
||||||
|
selectCustomFilter: (customFilter) ->
|
||||||
|
@.replaceAllFilters(customFilter.filter)
|
||||||
|
@.loadTasks()
|
||||||
|
@.generateFilters()
|
||||||
|
|
||||||
|
removeCustomFilter: (customFilter) ->
|
||||||
|
@filterRemoteStorageService.getFilters(@scope.projectId, 'tasks-custom-filters').then (userFilters) =>
|
||||||
|
delete userFilters[customFilter.id]
|
||||||
|
|
||||||
|
@filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, 'tasks-custom-filters').then(@.generateFilters)
|
||||||
|
|
||||||
|
saveCustomFilter: (name) ->
|
||||||
|
filters = {}
|
||||||
|
urlfilters = @location.search()
|
||||||
|
filters.tags = urlfilters.tags
|
||||||
|
filters.status = urlfilters.status
|
||||||
|
filters.assigned_to = urlfilters.assigned_to
|
||||||
|
filters.owner = urlfilters.owner
|
||||||
|
|
||||||
|
@filterRemoteStorageService.getFilters(@scope.projectId, 'tasks-custom-filters').then (userFilters) =>
|
||||||
|
userFilters[name] = filters
|
||||||
|
|
||||||
|
@filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, 'tasks-custom-filters').then(@.generateFilters)
|
||||||
|
|
||||||
|
generateFilters: ->
|
||||||
|
@.storeFilters(@params.pslug, @location.search(), "tasks-filters")
|
||||||
|
|
||||||
|
urlfilters = @location.search()
|
||||||
|
|
||||||
|
loadFilters = {}
|
||||||
|
loadFilters.project = @scope.projectId
|
||||||
|
loadFilters.milestone = @scope.sprintId
|
||||||
|
loadFilters.tags = urlfilters.tags
|
||||||
|
loadFilters.status = urlfilters.status
|
||||||
|
loadFilters.assigned_to = urlfilters.assigned_to
|
||||||
|
loadFilters.owner = urlfilters.owner
|
||||||
|
loadFilters.q = urlfilters.q
|
||||||
|
|
||||||
|
return @q.all([
|
||||||
|
@rs.tasks.filtersData(loadFilters),
|
||||||
|
@filterRemoteStorageService.getFilters(@scope.projectId, 'tasks-custom-filters')
|
||||||
|
]).then (result) =>
|
||||||
|
data = result[0]
|
||||||
|
customFiltersRaw = result[1]
|
||||||
|
|
||||||
|
statuses = _.map data.statuses, (it) ->
|
||||||
|
it.id = it.id.toString()
|
||||||
|
|
||||||
|
return it
|
||||||
|
tags = _.map data.tags, (it) ->
|
||||||
|
it.id = it.name
|
||||||
|
|
||||||
|
return it
|
||||||
|
assignedTo = _.map data.assigned_to, (it) ->
|
||||||
|
if it.id
|
||||||
|
it.id = it.id.toString()
|
||||||
|
else
|
||||||
|
it.id = "null"
|
||||||
|
|
||||||
|
it.name = it.full_name || "Unassigned"
|
||||||
|
|
||||||
|
return it
|
||||||
|
owner = _.map data.owners, (it) ->
|
||||||
|
it.id = it.id.toString()
|
||||||
|
it.name = it.full_name
|
||||||
|
|
||||||
|
return it
|
||||||
|
|
||||||
|
@.selectedFilters = []
|
||||||
|
|
||||||
|
if loadFilters.status
|
||||||
|
selected = @.formatSelectedFilters("status", statuses, loadFilters.status)
|
||||||
|
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||||
|
|
||||||
|
if loadFilters.tags
|
||||||
|
selected = @.formatSelectedFilters("tags", tags, loadFilters.tags)
|
||||||
|
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||||
|
|
||||||
|
if loadFilters.assigned_to
|
||||||
|
selected = @.formatSelectedFilters("assigned_to", assignedTo, loadFilters.assigned_to)
|
||||||
|
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||||
|
|
||||||
|
if loadFilters.owner
|
||||||
|
selected = @.formatSelectedFilters("owner", owner, loadFilters.owner)
|
||||||
|
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||||
|
|
||||||
|
@.filterQ = loadFilters.q
|
||||||
|
|
||||||
|
@.filters = [
|
||||||
|
{
|
||||||
|
title: @translate.instant("COMMON.FILTERS.CATEGORIES.STATUS"),
|
||||||
|
dataType: "status",
|
||||||
|
content: statuses
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: @translate.instant("COMMON.FILTERS.CATEGORIES.TAGS"),
|
||||||
|
dataType: "tags",
|
||||||
|
content: tags,
|
||||||
|
hideEmpty: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: @translate.instant("COMMON.FILTERS.CATEGORIES.ASSIGNED_TO"),
|
||||||
|
dataType: "assigned_to",
|
||||||
|
content: assignedTo
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"),
|
||||||
|
dataType: "owner",
|
||||||
|
content: owner
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@.customFilters = []
|
||||||
|
_.forOwn customFiltersRaw, (value, key) =>
|
||||||
|
@.customFilters.push({id: key, name: key, filter: value})
|
||||||
|
|
||||||
_setMeta: ->
|
_setMeta: ->
|
||||||
prettyDate = @translate.instant("BACKLOG.SPRINTS.DATE")
|
prettyDate = @translate.instant("BACKLOG.SPRINTS.DATE")
|
||||||
|
|
||||||
|
@ -92,24 +245,33 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
|
||||||
@appMetaService.setAll(title, description)
|
@appMetaService.setAll(title, description)
|
||||||
|
|
||||||
initializeEventHandlers: ->
|
initializeEventHandlers: ->
|
||||||
# TODO: Reload entire taskboard after create/edit tasks seems
|
@scope.$on "taskform:bulk:success", (event, tasks) =>
|
||||||
# a big overhead. It should be optimized in near future.
|
@.refreshTagsColors().then () =>
|
||||||
@scope.$on "taskform:bulk:success", =>
|
@taskboardTasksService.add(tasks)
|
||||||
@.loadTaskboard()
|
|
||||||
@analytics.trackEvent("task", "create", "bulk create task on taskboard", 1)
|
@analytics.trackEvent("task", "create", "bulk create task on taskboard", 1)
|
||||||
|
|
||||||
@scope.$on "taskform:new:success", =>
|
@scope.$on "taskform:new:success", (event, task) =>
|
||||||
@.loadTaskboard()
|
@.refreshTagsColors().then () =>
|
||||||
|
@taskboardTasksService.add(task)
|
||||||
|
|
||||||
@analytics.trackEvent("task", "create", "create task on taskboard", 1)
|
@analytics.trackEvent("task", "create", "create task on taskboard", 1)
|
||||||
|
|
||||||
@scope.$on("taskform:edit:success", => @.loadTaskboard())
|
@scope.$on "taskform:edit:success", (event, task) =>
|
||||||
@scope.$on("taskboard:task:move", @.taskMove)
|
@.refreshTagsColors().then () =>
|
||||||
|
@taskboardTasksService.replaceModel(task)
|
||||||
|
|
||||||
@scope.$on "assigned-to:added", (ctx, userId, task) =>
|
@scope.$on("taskboard:task:move", @.taskMove)
|
||||||
task.assigned_to = userId
|
@scope.$on("assigned-to:added", @.onAssignedToChanged)
|
||||||
promise = @repo.save(task)
|
|
||||||
promise.then null, ->
|
onAssignedToChanged: (ctx, userid, taskModel) ->
|
||||||
console.log "FAIL" # TODO
|
taskModel.assigned_to = userid
|
||||||
|
|
||||||
|
@taskboardTasksService.replaceModel(taskModel)
|
||||||
|
|
||||||
|
promise = @repo.save(taskModel)
|
||||||
|
promise.then null, ->
|
||||||
|
console.log "FAIL" # TODO
|
||||||
|
|
||||||
initializeSubscription: ->
|
initializeSubscription: ->
|
||||||
routingKey = "changes.project.#{@scope.projectId}.tasks"
|
routingKey = "changes.project.#{@scope.projectId}.tasks"
|
||||||
|
@ -130,7 +292,6 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
|
||||||
@scope.project = project
|
@scope.project = project
|
||||||
# Not used at this momment
|
# Not used at this momment
|
||||||
@scope.pointsList = _.sortBy(project.points, "order")
|
@scope.pointsList = _.sortBy(project.points, "order")
|
||||||
# @scope.roleList = _.sortBy(project.roles, "order")
|
|
||||||
@scope.pointsById = groupBy(project.points, (e) -> e.id)
|
@scope.pointsById = groupBy(project.points, (e) -> e.id)
|
||||||
@scope.roleById = groupBy(project.roles, (e) -> e.id)
|
@scope.roleById = groupBy(project.roles, (e) -> e.id)
|
||||||
@scope.taskStatusList = _.sortBy(project.task_statuses, "order")
|
@scope.taskStatusList = _.sortBy(project.task_statuses, "order")
|
||||||
|
@ -170,34 +331,22 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
|
||||||
return @rs.sprints.get(@scope.projectId, @scope.sprintId).then (sprint) =>
|
return @rs.sprints.get(@scope.projectId, @scope.sprintId).then (sprint) =>
|
||||||
@scope.sprint = sprint
|
@scope.sprint = sprint
|
||||||
@scope.userstories = _.sortBy(sprint.user_stories, "sprint_order")
|
@scope.userstories = _.sortBy(sprint.user_stories, "sprint_order")
|
||||||
|
|
||||||
|
@taskboardTasksService.setUserstories(@scope.userstories)
|
||||||
|
|
||||||
return sprint
|
return sprint
|
||||||
|
|
||||||
loadTasks: ->
|
loadTasks: ->
|
||||||
return @rs.tasks.list(@scope.projectId, @scope.sprintId).then (tasks) =>
|
params = {
|
||||||
@scope.tasks = _.sortBy(tasks, 'taskboard_order')
|
include_attachments: true,
|
||||||
@scope.usTasks = {}
|
include_tasks: true
|
||||||
|
}
|
||||||
|
|
||||||
# Iterate over all userstories and
|
params = _.merge params, @location.search()
|
||||||
# null userstory for unassigned tasks
|
|
||||||
for us in _.union(@scope.userstories, [{id:null}])
|
|
||||||
@scope.usTasks[us.id] = {}
|
|
||||||
for status in @scope.taskStatusList
|
|
||||||
@scope.usTasks[us.id][status.id] = []
|
|
||||||
|
|
||||||
for task in @scope.tasks
|
return @rs.tasks.list(@scope.projectId, @scope.sprintId, null, params).then (tasks) =>
|
||||||
if @scope.usTasks[task.user_story]? and @scope.usTasks[task.user_story][task.status]?
|
@taskboardTasksService.init(@scope.project, @scope.usersById)
|
||||||
@scope.usTasks[task.user_story][task.status].push(task)
|
@taskboardTasksService.set(tasks)
|
||||||
|
|
||||||
if tasks.length == 0
|
|
||||||
|
|
||||||
if @scope.userstories.length > 0
|
|
||||||
usId = @scope.userstories[0].id
|
|
||||||
else
|
|
||||||
usId = null
|
|
||||||
|
|
||||||
@scope.usTasks[usId][@scope.taskStatusList[0].id].push({isPlaceholder: true})
|
|
||||||
|
|
||||||
return tasks
|
|
||||||
|
|
||||||
loadTaskboard: ->
|
loadTaskboard: ->
|
||||||
return @q.all([
|
return @q.all([
|
||||||
|
@ -219,59 +368,69 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
return promise.then(=> @.loadProject())
|
return promise.then(=> @.loadProject())
|
||||||
.then(=> @.loadTaskboard())
|
.then =>
|
||||||
.then(=> @.setRolePoints())
|
@.generateFilters()
|
||||||
|
|
||||||
refreshTasksOrder: (tasks) ->
|
return @.loadTaskboard().then(=> @.setRolePoints())
|
||||||
items = @.resortTasks(tasks)
|
|
||||||
data = @.prepareBulkUpdateData(items)
|
|
||||||
|
|
||||||
return @rs.tasks.bulkUpdateTaskTaskboardOrder(@scope.project.id, data)
|
showPlaceHolder: (statusId, usId) ->
|
||||||
|
if !@taskboardTasksService.tasksRaw.length
|
||||||
|
if @scope.taskStatusList[0].id == statusId &&
|
||||||
|
(!@scope.userstories.length || @scope.userstories[0].id == usId)
|
||||||
|
return true
|
||||||
|
|
||||||
resortTasks: (tasks) ->
|
return false
|
||||||
items = []
|
|
||||||
|
|
||||||
for item, index in tasks
|
editTask: (id) ->
|
||||||
item["taskboard_order"] = index
|
task = @.taskboardTasksService.getTask(id)
|
||||||
if item.isModified()
|
|
||||||
items.push(item)
|
|
||||||
|
|
||||||
return items
|
task = task.set('loading', true)
|
||||||
|
@taskboardTasksService.replace(task)
|
||||||
|
|
||||||
prepareBulkUpdateData: (uses) ->
|
@rs.tasks.getByRef(task.getIn(['model', 'project']), task.getIn(['model', 'ref'])).then (editingTask) =>
|
||||||
return _.map(uses, (x) -> {"task_id": x.id, "order": x["taskboard_order"]})
|
@rs2.attachments.list("task", task.get('id'), task.getIn(['model', 'project'])).then (attachments) =>
|
||||||
|
@rootscope.$broadcast("taskform:edit", editingTask, attachments.toJS())
|
||||||
|
task = task.set('loading', false)
|
||||||
|
@taskboardTasksService.replace(task)
|
||||||
|
|
||||||
taskMove: (ctx, task, usId, statusId, order) ->
|
taskMove: (ctx, task, oldStatusId, usId, statusId, order) ->
|
||||||
# Remove task from old position
|
task = @taskboardTasksService.getTaskModel(task.get('id'))
|
||||||
r = @scope.usTasks[task.user_story][task.status].indexOf(task)
|
|
||||||
@scope.usTasks[task.user_story][task.status].splice(r, 1)
|
|
||||||
|
|
||||||
# Add task to new position
|
moveUpdateData = @taskboardTasksService.move(task.id, usId, statusId, order)
|
||||||
tasks = @scope.usTasks[usId][statusId]
|
|
||||||
tasks.splice(order, 0, task)
|
|
||||||
|
|
||||||
task.user_story = usId
|
params = {
|
||||||
task.status = statusId
|
status__is_archived: false,
|
||||||
task.taskboard_order = order
|
include_attachments: true,
|
||||||
|
include_tasks: true
|
||||||
|
}
|
||||||
|
|
||||||
promise = @repo.save(task)
|
options = {
|
||||||
|
headers: {
|
||||||
|
"set-orders": JSON.stringify(moveUpdateData.set_orders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@rootscope.$broadcast("sprint:task:moved", task)
|
promise = @repo.save(task, true, params, options, true).then (result) =>
|
||||||
|
headers = result[1]
|
||||||
|
|
||||||
|
if headers && headers['taiga-info-order-updated']
|
||||||
|
order = JSON.parse(headers['taiga-info-order-updated'])
|
||||||
|
@taskboardTasksService.assignOrders(order)
|
||||||
|
|
||||||
promise.then =>
|
|
||||||
@.refreshTasksOrder(tasks)
|
|
||||||
@.loadSprintStats()
|
@.loadSprintStats()
|
||||||
|
|
||||||
promise.then null, =>
|
|
||||||
console.log "FAIL TASK SAVE"
|
|
||||||
|
|
||||||
## Template actions
|
## Template actions
|
||||||
addNewTask: (type, us) ->
|
addNewTask: (type, us) ->
|
||||||
switch type
|
switch type
|
||||||
when "standard" then @rootscope.$broadcast("taskform:new", @scope.sprintId, us?.id)
|
when "standard" then @rootscope.$broadcast("taskform:new", @scope.sprintId, us?.id)
|
||||||
when "bulk" then @rootscope.$broadcast("taskform:bulk", @scope.sprintId, us?.id)
|
when "bulk" then @rootscope.$broadcast("taskform:bulk", @scope.sprintId, us?.id)
|
||||||
|
|
||||||
editTaskAssignedTo: (task) ->
|
toggleFold: (id) ->
|
||||||
|
@taskboardTasksService.toggleFold(id)
|
||||||
|
|
||||||
|
changeTaskAssignedTo: (id) ->
|
||||||
|
task = @taskboardTasksService.getTaskModel(id)
|
||||||
|
|
||||||
@rootscope.$broadcast("assigned-to:add", task)
|
@rootscope.$broadcast("assigned-to:add", task)
|
||||||
|
|
||||||
setRolePoints: () ->
|
setRolePoints: () ->
|
||||||
|
@ -331,43 +490,6 @@ TaskboardDirective = ($rootscope) ->
|
||||||
|
|
||||||
module.directive("tgTaskboard", ["$rootScope", TaskboardDirective])
|
module.directive("tgTaskboard", ["$rootScope", TaskboardDirective])
|
||||||
|
|
||||||
|
|
||||||
#############################################################################
|
|
||||||
## Taskboard Task Directive
|
|
||||||
#############################################################################
|
|
||||||
|
|
||||||
TaskboardTaskDirective = ($rootscope, $loading, $rs, $rs2) ->
|
|
||||||
link = ($scope, $el, $attrs, $model) ->
|
|
||||||
$scope.$watch "task", (task) ->
|
|
||||||
if task.is_blocked and not $el.hasClass("blocked")
|
|
||||||
$el.addClass("blocked")
|
|
||||||
else if not task.is_blocked and $el.hasClass("blocked")
|
|
||||||
$el.removeClass("blocked")
|
|
||||||
|
|
||||||
$el.find(".edit-task").on "click", (event) ->
|
|
||||||
if $el.find('.edit-task').hasClass('noclick')
|
|
||||||
return
|
|
||||||
|
|
||||||
$scope.$apply ->
|
|
||||||
target = $(event.target)
|
|
||||||
|
|
||||||
currentLoading = $loading()
|
|
||||||
.target(target)
|
|
||||||
.timeout(200)
|
|
||||||
.start()
|
|
||||||
|
|
||||||
task = $scope.task
|
|
||||||
|
|
||||||
$rs.tasks.getByRef(task.project, task.ref).then (editingTask) =>
|
|
||||||
$rs2.attachments.list("task", editingTask.id, editingTask.project).then (attachments) =>
|
|
||||||
$rootscope.$broadcast("taskform:edit", editingTask, attachments.toJS())
|
|
||||||
currentLoading.finish()
|
|
||||||
|
|
||||||
return {link:link}
|
|
||||||
|
|
||||||
|
|
||||||
module.directive("tgTaskboardTask", ["$rootScope", "$tgLoading", "$tgResources", "tgResources", TaskboardTaskDirective])
|
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
## Taskboard Squish Column Directive
|
## Taskboard Squish Column Directive
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
@ -377,14 +499,18 @@ TaskboardSquishColumnDirective = (rs) ->
|
||||||
maxColumnWidth = 300
|
maxColumnWidth = 300
|
||||||
|
|
||||||
link = ($scope, $el, $attrs) ->
|
link = ($scope, $el, $attrs) ->
|
||||||
|
$scope.$on "sprint:zoom0", () =>
|
||||||
|
recalculateTaskboardWidth()
|
||||||
|
|
||||||
$scope.$on "sprint:task:moved", () =>
|
$scope.$on "sprint:task:moved", () =>
|
||||||
recalculateTaskboardWidth()
|
recalculateTaskboardWidth()
|
||||||
|
|
||||||
bindOnce $scope, "usTasks", (project) ->
|
$scope.$watch "usTasks", () ->
|
||||||
$scope.statusesFolded = rs.tasks.getStatusColumnModes($scope.project.id)
|
if $scope.project
|
||||||
$scope.usFolded = rs.tasks.getUsRowModes($scope.project.id, $scope.sprintId)
|
$scope.statusesFolded = rs.tasks.getStatusColumnModes($scope.project.id)
|
||||||
|
$scope.usFolded = rs.tasks.getUsRowModes($scope.project.id, $scope.sprintId)
|
||||||
|
|
||||||
recalculateTaskboardWidth()
|
recalculateTaskboardWidth()
|
||||||
|
|
||||||
$scope.foldStatus = (status) ->
|
$scope.foldStatus = (status) ->
|
||||||
$scope.statusesFolded[status.id] = !!!$scope.statusesFolded[status.id]
|
$scope.statusesFolded[status.id] = !!!$scope.statusesFolded[status.id]
|
||||||
|
@ -403,7 +529,10 @@ TaskboardSquishColumnDirective = (rs) ->
|
||||||
recalculateTaskboardWidth()
|
recalculateTaskboardWidth()
|
||||||
|
|
||||||
getCeilWidth = (usId, statusId) =>
|
getCeilWidth = (usId, statusId) =>
|
||||||
tasks = $scope.usTasks[usId][statusId].length
|
if usId
|
||||||
|
tasks = $scope.usTasks.getIn([usId.toString(), statusId.toString()]).size
|
||||||
|
else
|
||||||
|
tasks = $scope.usTasks.getIn(['null', statusId.toString()]).size
|
||||||
|
|
||||||
if $scope.statusesFolded[statusId]
|
if $scope.statusesFolded[statusId]
|
||||||
if tasks and $scope.usFolded[usId]
|
if tasks and $scope.usFolded[usId]
|
||||||
|
@ -422,7 +551,10 @@ TaskboardSquishColumnDirective = (rs) ->
|
||||||
if width
|
if width
|
||||||
column.css('max-width', width)
|
column.css('max-width', width)
|
||||||
else
|
else
|
||||||
column.css("max-width", maxColumnWidth)
|
if $scope.ctrl.zoomLevel == '0'
|
||||||
|
column.css("max-width", 148)
|
||||||
|
else
|
||||||
|
column.css("max-width", maxColumnWidth)
|
||||||
|
|
||||||
refreshTaskboardTableWidth = () =>
|
refreshTaskboardTableWidth = () =>
|
||||||
columnWidths = []
|
columnWidths = []
|
||||||
|
@ -458,67 +590,3 @@ TaskboardSquishColumnDirective = (rs) ->
|
||||||
return {link: link}
|
return {link: link}
|
||||||
|
|
||||||
module.directive("tgTaskboardSquishColumn", ["$tgResources", TaskboardSquishColumnDirective])
|
module.directive("tgTaskboardSquishColumn", ["$tgResources", TaskboardSquishColumnDirective])
|
||||||
|
|
||||||
#############################################################################
|
|
||||||
## Taskboard User Directive
|
|
||||||
#############################################################################
|
|
||||||
|
|
||||||
TaskboardUserDirective = ($log, $translate, avatarService) ->
|
|
||||||
clickable = false
|
|
||||||
|
|
||||||
link = ($scope, $el, $attrs) ->
|
|
||||||
username_label = $el.parent().find("a.task-assigned")
|
|
||||||
username_label.addClass("not-clickable")
|
|
||||||
|
|
||||||
$scope.$watch 'task.assigned_to', (assigned_to) ->
|
|
||||||
user = $scope.usersById[assigned_to]
|
|
||||||
|
|
||||||
avatar = avatarService.getAvatar(user)
|
|
||||||
|
|
||||||
if user is undefined
|
|
||||||
_.assign($scope, {
|
|
||||||
name: $translate.instant("COMMON.ASSIGNED_TO.NOT_ASSIGNED"),
|
|
||||||
avatar: avatar,
|
|
||||||
clickable: clickable
|
|
||||||
})
|
|
||||||
else
|
|
||||||
_.assign($scope, {
|
|
||||||
name: user.full_name_display,
|
|
||||||
avatar: avatar,
|
|
||||||
clickable: clickable
|
|
||||||
})
|
|
||||||
|
|
||||||
username_label.text($scope.name)
|
|
||||||
|
|
||||||
|
|
||||||
bindOnce $scope, "project", (project) ->
|
|
||||||
if project.my_permissions.indexOf("modify_task") > -1
|
|
||||||
clickable = true
|
|
||||||
$el.find(".avatar-assigned-to").on "click", (event) =>
|
|
||||||
if $el.find('a').hasClass('noclick')
|
|
||||||
return
|
|
||||||
|
|
||||||
$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",
|
|
||||||
scope: {
|
|
||||||
"usersById": "=users",
|
|
||||||
"project": "=",
|
|
||||||
"task": "=",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
module.directive("tgTaskboardUserAvatar", ["$log", "$translate", "tgAvatarService", TaskboardUserDirective])
|
|
||||||
|
|
|
@ -37,11 +37,14 @@ module = angular.module("taigaBacklog")
|
||||||
## Sortable Directive
|
## Sortable Directive
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
|
||||||
TaskboardSortableDirective = ($repo, $rs, $rootscope) ->
|
TaskboardSortableDirective = ($repo, $rs, $rootscope, $translate) ->
|
||||||
link = ($scope, $el, $attrs) ->
|
link = ($scope, $el, $attrs) ->
|
||||||
bindOnce $scope, "tasks", (xx) ->
|
unwatch = $scope.$watch "usTasks", (usTasks) ->
|
||||||
# If the user has not enough permissions we don't enable the sortable
|
return if !usTasks || !usTasks.size
|
||||||
if not ($scope.project.my_permissions.indexOf("modify_us") > -1)
|
|
||||||
|
unwatch()
|
||||||
|
|
||||||
|
if not ($scope.project.my_permissions.indexOf("modify_task") > -1)
|
||||||
return
|
return
|
||||||
|
|
||||||
oldParentScope = null
|
oldParentScope = null
|
||||||
|
@ -49,6 +52,10 @@ TaskboardSortableDirective = ($repo, $rs, $rootscope) ->
|
||||||
itemEl = null
|
itemEl = null
|
||||||
tdom = $el
|
tdom = $el
|
||||||
|
|
||||||
|
filterError = ->
|
||||||
|
text = $translate.instant("BACKLOG.SORTABLE_FILTER_ERROR")
|
||||||
|
$tgConfirm.notify("error", text)
|
||||||
|
|
||||||
deleteElement = (itemEl) ->
|
deleteElement = (itemEl) ->
|
||||||
# Completelly remove item and its scope from dom
|
# Completelly remove item and its scope from dom
|
||||||
itemEl.scope().$destroy()
|
itemEl.scope().$destroy()
|
||||||
|
@ -63,12 +70,22 @@ TaskboardSortableDirective = ($repo, $rs, $rootscope) ->
|
||||||
copy: false,
|
copy: false,
|
||||||
mirrorContainer: $el[0],
|
mirrorContainer: $el[0],
|
||||||
accepts: (el, target) -> return !$(target).hasClass('taskboard-userstory-box')
|
accepts: (el, target) -> return !$(target).hasClass('taskboard-userstory-box')
|
||||||
moves: (item) -> return $(item).hasClass('taskboard-task')
|
moves: (item) ->
|
||||||
|
return $(item).is('tg-card')
|
||||||
})
|
})
|
||||||
|
|
||||||
drake.on 'drag', (item) ->
|
drake.on 'drag', (item) ->
|
||||||
oldParentScope = $(item).parent().scope()
|
oldParentScope = $(item).parent().scope()
|
||||||
|
|
||||||
|
if $el.hasClass("active-filters")
|
||||||
|
filterError()
|
||||||
|
|
||||||
|
setTimeout (() ->
|
||||||
|
drake.cancel(true)
|
||||||
|
), 0
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
drake.on 'dragend', (item) ->
|
drake.on 'dragend', (item) ->
|
||||||
parentEl = $(item).parent()
|
parentEl = $(item).parent()
|
||||||
itemEl = $(item)
|
itemEl = $(item)
|
||||||
|
@ -85,7 +102,7 @@ TaskboardSortableDirective = ($repo, $rs, $rootscope) ->
|
||||||
deleteElement(itemEl)
|
deleteElement(itemEl)
|
||||||
|
|
||||||
$scope.$apply ->
|
$scope.$apply ->
|
||||||
$rootscope.$broadcast("taskboard:task:move", itemTask, newUsId, newStatusId, itemIndex)
|
$rootscope.$broadcast("taskboard:task:move", itemTask, itemTask.getIn(['model', 'status']), newUsId, newStatusId, itemIndex)
|
||||||
|
|
||||||
|
|
||||||
scroll = autoScroll([$('.taskboard-table-body')[0]], {
|
scroll = autoScroll([$('.taskboard-table-body')[0]], {
|
||||||
|
@ -107,5 +124,6 @@ module.directive("tgTaskboardSortable", [
|
||||||
"$tgRepo",
|
"$tgRepo",
|
||||||
"$tgResources",
|
"$tgResources",
|
||||||
"$rootScope",
|
"$rootScope",
|
||||||
|
"$translate",
|
||||||
TaskboardSortableDirective
|
TaskboardSortableDirective
|
||||||
])
|
])
|
||||||
|
|
|
@ -0,0 +1,173 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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: home.service.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
groupBy = @.taiga.groupBy
|
||||||
|
|
||||||
|
class TaskboardTasksService extends taiga.Service
|
||||||
|
@.$inject = []
|
||||||
|
constructor: () ->
|
||||||
|
@.reset()
|
||||||
|
|
||||||
|
reset: () ->
|
||||||
|
@.tasksRaw = []
|
||||||
|
@.foldStatusChanged = {}
|
||||||
|
@.usTasks = Immutable.Map()
|
||||||
|
|
||||||
|
init: (project, usersById) ->
|
||||||
|
@.project = project
|
||||||
|
@.usersById = usersById
|
||||||
|
|
||||||
|
resetFolds: () ->
|
||||||
|
@.foldStatusChanged = {}
|
||||||
|
@.refresh()
|
||||||
|
|
||||||
|
toggleFold: (taskId) ->
|
||||||
|
@.foldStatusChanged[taskId] = !@.foldStatusChanged[taskId]
|
||||||
|
@.refresh()
|
||||||
|
|
||||||
|
add: (task) ->
|
||||||
|
@.tasksRaw = @.tasksRaw.concat(task)
|
||||||
|
@.refresh()
|
||||||
|
|
||||||
|
set: (tasks) ->
|
||||||
|
@.tasksRaw = tasks
|
||||||
|
@.refreshRawOrder()
|
||||||
|
@.refresh()
|
||||||
|
|
||||||
|
setUserstories: (userstories) ->
|
||||||
|
@.userstories = userstories
|
||||||
|
|
||||||
|
refreshRawOrder: () ->
|
||||||
|
@.order = {}
|
||||||
|
|
||||||
|
@.order[task.id] = task.taskboard_order for task in @.tasksRaw
|
||||||
|
|
||||||
|
assignOrders: (order) ->
|
||||||
|
order = _.invert(order)
|
||||||
|
@.order = _.assign(@.order, order)
|
||||||
|
|
||||||
|
@.refresh()
|
||||||
|
|
||||||
|
getTask: (id) ->
|
||||||
|
findedTask = null
|
||||||
|
|
||||||
|
@.usTasks.forEach (us) ->
|
||||||
|
us.forEach (status) ->
|
||||||
|
findedTask = status.find (task) -> return task.get('id') == id
|
||||||
|
|
||||||
|
return false if findedTask
|
||||||
|
|
||||||
|
return false if findedTask
|
||||||
|
|
||||||
|
return findedTask
|
||||||
|
|
||||||
|
replace: (task) ->
|
||||||
|
@.usTasks = @.usTasks.map (us) ->
|
||||||
|
return us.map (status) ->
|
||||||
|
findedIndex = status.findIndex (usItem) ->
|
||||||
|
return usItem.get('id') == us.get('id')
|
||||||
|
|
||||||
|
if findedIndex != -1
|
||||||
|
status = status.set(findedIndex, task)
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
getTaskModel: (id) ->
|
||||||
|
return _.find @.tasksRaw, (task) -> return task.id == id
|
||||||
|
|
||||||
|
replaceModel: (task) ->
|
||||||
|
@.tasksRaw = _.map @.tasksRaw, (it) ->
|
||||||
|
if task.id == it.id
|
||||||
|
return task
|
||||||
|
else
|
||||||
|
return it
|
||||||
|
|
||||||
|
@.refresh()
|
||||||
|
|
||||||
|
move: (id, usId, statusId, index) ->
|
||||||
|
task = @.getTaskModel(id)
|
||||||
|
|
||||||
|
taskByUsStatus = _.filter @.tasksRaw, (task) =>
|
||||||
|
return task.status == statusId && task.user_story == usId
|
||||||
|
|
||||||
|
taskByUsStatus = _.sortBy taskByUsStatus, (it) => @.order[it.id]
|
||||||
|
|
||||||
|
taksWithoutMoved = _.filter taskByUsStatus, (it) => it.id != id
|
||||||
|
beforeDestination = _.slice(taksWithoutMoved, 0, index)
|
||||||
|
afterDestination = _.slice(taksWithoutMoved, index)
|
||||||
|
|
||||||
|
setOrders = {}
|
||||||
|
|
||||||
|
previous = beforeDestination[beforeDestination.length - 1]
|
||||||
|
|
||||||
|
previousWithTheSameOrder = _.filter beforeDestination, (it) =>
|
||||||
|
@.order[it.id] == @.order[previous.id]
|
||||||
|
|
||||||
|
if previousWithTheSameOrder.length > 1
|
||||||
|
for it in previousWithTheSameOrder
|
||||||
|
setOrders[it.id] = @.order[it.id]
|
||||||
|
|
||||||
|
if !previous
|
||||||
|
@.order[task.id] = 0
|
||||||
|
else if previous
|
||||||
|
@.order[task.id] = @.order[previous.id] + 1
|
||||||
|
|
||||||
|
for it, key in afterDestination
|
||||||
|
@.order[it.id] = @.order[task.id] + key + 1
|
||||||
|
|
||||||
|
task.status = statusId
|
||||||
|
task.user_story = usId
|
||||||
|
task.taskboard_order = @.order[task.id]
|
||||||
|
|
||||||
|
@.refresh()
|
||||||
|
|
||||||
|
return {"task_id": task.id, "order": @.order[task.id], "set_orders": setOrders}
|
||||||
|
|
||||||
|
refresh: ->
|
||||||
|
@.tasksRaw = _.sortBy @.tasksRaw, (it) => @.order[it.id]
|
||||||
|
|
||||||
|
tasks = @.tasksRaw
|
||||||
|
taskStatusList = _.sortBy(@.project.task_statuses, "order")
|
||||||
|
|
||||||
|
usTasks = {}
|
||||||
|
|
||||||
|
# Iterate over all userstories and
|
||||||
|
# null userstory for unassigned tasks
|
||||||
|
for us in _.union(@.userstories, [{id:null}])
|
||||||
|
usTasks[us.id] = {}
|
||||||
|
for status in taskStatusList
|
||||||
|
usTasks[us.id][status.id] = []
|
||||||
|
|
||||||
|
for taskModel in tasks
|
||||||
|
if usTasks[taskModel.user_story]? and usTasks[taskModel.user_story][taskModel.status]?
|
||||||
|
task = {}
|
||||||
|
task.foldStatusChanged = @.foldStatusChanged[taskModel.id]
|
||||||
|
task.model = taskModel.getAttrs()
|
||||||
|
task.images = _.filter taskModel.attachments, (it) -> return !!it.thumbnail_card_url
|
||||||
|
task.id = taskModel.id
|
||||||
|
task.assigned_to = @.usersById[taskModel.assigned_to]
|
||||||
|
task.colorized_tags = _.map task.model.tags, (tag) =>
|
||||||
|
color = @.project.tags_colors[tag]
|
||||||
|
return {name: tag, color: color}
|
||||||
|
|
||||||
|
usTasks[taskModel.user_story][taskModel.status].push(task)
|
||||||
|
|
||||||
|
@.usTasks = Immutable.fromJS(usTasks)
|
||||||
|
|
||||||
|
angular.module("taigaKanban").service("tgTaskboardTasks", TaskboardTasksService)
|
|
@ -38,7 +38,7 @@ bindMethods = (object) =>
|
||||||
methods = []
|
methods = []
|
||||||
|
|
||||||
_.forIn object, (value, key) =>
|
_.forIn object, (value, key) =>
|
||||||
if key not in dependencies
|
if key not in dependencies && _.isFunction(value)
|
||||||
methods.push(key)
|
methods.push(key)
|
||||||
|
|
||||||
_.bindAll(object, methods)
|
_.bindAll(object, methods)
|
||||||
|
|
|
@ -45,6 +45,10 @@
|
||||||
"CAPSLOCK_WARNING": "Be careful! You are using capital letters in an input field that is case sensitive.",
|
"CAPSLOCK_WARNING": "Be careful! You are using capital letters in an input field that is case sensitive.",
|
||||||
"CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?",
|
"CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?",
|
||||||
"CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost",
|
"CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost",
|
||||||
|
"CARD": {
|
||||||
|
"ASSIGN_TO": "Assign To",
|
||||||
|
"EDIT": "Edit card"
|
||||||
|
},
|
||||||
"FORM_ERRORS": {
|
"FORM_ERRORS": {
|
||||||
"DEFAULT_MESSAGE": "This value seems to be invalid.",
|
"DEFAULT_MESSAGE": "This value seems to be invalid.",
|
||||||
"TYPE_EMAIL": "This value should be a valid email.",
|
"TYPE_EMAIL": "This value should be a valid email.",
|
||||||
|
@ -196,9 +200,25 @@
|
||||||
"TITLE": "filters",
|
"TITLE": "filters",
|
||||||
"INPUT_PLACEHOLDER": "Subject or reference",
|
"INPUT_PLACEHOLDER": "Subject or reference",
|
||||||
"TITLE_ACTION_FILTER_BUTTON": "search",
|
"TITLE_ACTION_FILTER_BUTTON": "search",
|
||||||
"BREADCRUMB_TITLE": "back to categories",
|
"TITLE": "Filters",
|
||||||
"BREADCRUMB_FILTERS": "Filters",
|
"INPUT_SEARCH_PLACEHOLDER": "Subject or ref",
|
||||||
"BREADCRUMB_STATUS": "status"
|
"TITLE_ACTION_SEARCH": "Search",
|
||||||
|
"ACTION_SAVE_CUSTOM_FILTER": "save as custom filter",
|
||||||
|
"PLACEHOLDER_FILTER_NAME": "Write the filter name and press enter",
|
||||||
|
"CATEGORIES": {
|
||||||
|
"TYPE": "Type",
|
||||||
|
"STATUS": "Status",
|
||||||
|
"SEVERITY": "Severity",
|
||||||
|
"PRIORITIES": "Priorities",
|
||||||
|
"TAGS": "Tags",
|
||||||
|
"ASSIGNED_TO": "Assigned to",
|
||||||
|
"CREATED_BY": "Created by",
|
||||||
|
"CUSTOM_FILTERS": "Custom filters"
|
||||||
|
},
|
||||||
|
"CONFIRM_DELETE": {
|
||||||
|
"TITLE": "Delete custom filter",
|
||||||
|
"MESSAGE": "the custom filter '{{customFilterName}}'"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"WYSIWYG": {
|
"WYSIWYG": {
|
||||||
"H1_BUTTON": "First Level Heading",
|
"H1_BUTTON": "First Level Heading",
|
||||||
|
@ -1169,9 +1189,7 @@
|
||||||
"TITLE": "Filters",
|
"TITLE": "Filters",
|
||||||
"REMOVE": "Remove Filters",
|
"REMOVE": "Remove Filters",
|
||||||
"HIDE": "Hide Filters",
|
"HIDE": "Hide Filters",
|
||||||
"SHOW": "Show Filters",
|
"SHOW": "Show Filters"
|
||||||
"FILTER_CATEGORY_STATUS": "Status",
|
|
||||||
"FILTER_CATEGORY_TAGS": "Tags"
|
|
||||||
},
|
},
|
||||||
"SPRINTS": {
|
"SPRINTS": {
|
||||||
"TITLE": "SPRINTS",
|
"TITLE": "SPRINTS",
|
||||||
|
@ -1278,7 +1296,6 @@
|
||||||
"SECTION_NAME": "Issue",
|
"SECTION_NAME": "Issue",
|
||||||
"ACTION_NEW_ISSUE": "+ NEW ISSUE",
|
"ACTION_NEW_ISSUE": "+ NEW ISSUE",
|
||||||
"ACTION_PROMOTE_TO_US": "Promote to User Story",
|
"ACTION_PROMOTE_TO_US": "Promote to User Story",
|
||||||
"PLACEHOLDER_FILTER_NAME": "Write the filter name and press enter",
|
|
||||||
"PROMOTED": "This issue has been promoted to US:",
|
"PROMOTED": "This issue has been promoted to US:",
|
||||||
"EXTERNAL_REFERENCE": "This issue has been created from",
|
"EXTERNAL_REFERENCE": "This issue has been created from",
|
||||||
"GO_TO_EXTERNAL_REFERENCE": "Go to origin",
|
"GO_TO_EXTERNAL_REFERENCE": "Go to origin",
|
||||||
|
@ -1296,28 +1313,6 @@
|
||||||
"TITLE": "Promote this issue to a new user story",
|
"TITLE": "Promote this issue to a new user story",
|
||||||
"MESSAGE": "Are you sure you want to create a new US from this Issue?"
|
"MESSAGE": "Are you sure you want to create a new US from this Issue?"
|
||||||
},
|
},
|
||||||
"FILTERS": {
|
|
||||||
"TITLE": "Filters",
|
|
||||||
"INPUT_SEARCH_PLACEHOLDER": "Subject or ref",
|
|
||||||
"TITLE_ACTION_SEARCH": "Search",
|
|
||||||
"ACTION_SAVE_CUSTOM_FILTER": "save as custom filter",
|
|
||||||
"BREADCRUMB": "Filters",
|
|
||||||
"TITLE_BREADCRUMB": "Filters",
|
|
||||||
"CATEGORIES": {
|
|
||||||
"TYPE": "Type",
|
|
||||||
"STATUS": "Status",
|
|
||||||
"SEVERITY": "Severity",
|
|
||||||
"PRIORITIES": "Priorities",
|
|
||||||
"TAGS": "Tags",
|
|
||||||
"ASSIGNED_TO": "Assigned to",
|
|
||||||
"CREATED_BY": "Created by",
|
|
||||||
"CUSTOM_FILTERS": "Custom filters"
|
|
||||||
},
|
|
||||||
"CONFIRM_DELETE": {
|
|
||||||
"TITLE": "Delete custom filter",
|
|
||||||
"MESSAGE": "the custom filter '{{customFilterName}}'"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"TABLE": {
|
"TABLE": {
|
||||||
"COLUMNS": {
|
"COLUMNS": {
|
||||||
"TYPE": "Type",
|
"TYPE": "Type",
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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: board-zoom.directive.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
BoardZoomDirective = () ->
|
||||||
|
return {
|
||||||
|
scope: {
|
||||||
|
levels: "=",
|
||||||
|
value: "="
|
||||||
|
},
|
||||||
|
templateUrl: 'components/board-zoom/board-zoom.html'
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('taigaComponents').directive("tgBoardZoom", [BoardZoomDirective])
|
|
@ -0,0 +1,9 @@
|
||||||
|
input.range-slider(
|
||||||
|
type="range",
|
||||||
|
min="0",
|
||||||
|
max="{{levels - 1}}",
|
||||||
|
step="1"
|
||||||
|
ng-model="value"
|
||||||
|
ng-model-options="{ debounce: 200 }"
|
||||||
|
tg-bind-scope
|
||||||
|
)
|
|
@ -0,0 +1,108 @@
|
||||||
|
$track-color: $whitish;
|
||||||
|
$thumb-color: $grayer;
|
||||||
|
$thumb-shadow: rgba($thumb-color, .3);
|
||||||
|
|
||||||
|
$thumb-radius: 50%;
|
||||||
|
$thumb-height: 14px;
|
||||||
|
$thumb-width: 14px;
|
||||||
|
$thumb-border-width: 0;
|
||||||
|
$thumb-border-color: transparent;
|
||||||
|
|
||||||
|
$track-width: 200px;
|
||||||
|
$track-height: 3px;
|
||||||
|
$track-border-width: 0;
|
||||||
|
$track-border-color: transparent;
|
||||||
|
|
||||||
|
$track-radius: 1px;
|
||||||
|
$contrast: 2;
|
||||||
|
|
||||||
|
@mixin track() {
|
||||||
|
width: $track-width;
|
||||||
|
height: $track-height;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin thumb() {
|
||||||
|
border: $thumb-border-width solid $thumb-border-color;
|
||||||
|
height: $thumb-height;
|
||||||
|
width: $thumb-width;
|
||||||
|
border-radius: $thumb-radius;
|
||||||
|
background: $thumb-color;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 0 0 2px $thumb-shadow;
|
||||||
|
transition: box-shadow .2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-slider {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: $thumb-height / 2 0;
|
||||||
|
width: $track-width;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
&::-webkit-slider-runnable-track {
|
||||||
|
background: lighten($track-color, $contrast);
|
||||||
|
}
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
box-shadow: 0 0 0 4px $thumb-shadow;
|
||||||
|
}
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
box-shadow: 0 0 0 4px $thumb-shadow;
|
||||||
|
}
|
||||||
|
&::-ms-fill-lower {
|
||||||
|
background: $track-color;
|
||||||
|
}
|
||||||
|
&::-ms-fill-upper {
|
||||||
|
background: lighten($track-color, $contrast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-slider-runnable-track {
|
||||||
|
@include track();
|
||||||
|
background: $track-color;
|
||||||
|
border: $track-border-width solid $track-border-color;
|
||||||
|
border-radius: $track-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
@include thumb();
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin-top: ((-$track-border-width * 2 + $track-height) / 2) - ($thumb-height / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-track {
|
||||||
|
@include track();
|
||||||
|
background: $track-color;
|
||||||
|
border: $track-border-width solid $track-border-color;
|
||||||
|
border-radius: $track-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
@include thumb();
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-ms-track {
|
||||||
|
@include track();
|
||||||
|
background: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
border-width: $thumb-width 0;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-ms-fill-lower {
|
||||||
|
background: darken($track-color, $contrast);
|
||||||
|
border: $track-border-width solid $track-border-color;
|
||||||
|
border-radius: $track-radius * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-ms-fill-upper {
|
||||||
|
background: $track-color;
|
||||||
|
border: $track-border-width solid $track-border-color;
|
||||||
|
border-radius: $track-radius * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-ms-thumb {
|
||||||
|
@include thumb();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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: card-slideshow.controller.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
class CardSlideshowController
|
||||||
|
@.$inject = []
|
||||||
|
|
||||||
|
constructor: () ->
|
||||||
|
@.index = 0
|
||||||
|
|
||||||
|
next: () ->
|
||||||
|
@.index++
|
||||||
|
|
||||||
|
if @.index >= @.images.size
|
||||||
|
@.index = 0
|
||||||
|
|
||||||
|
previous: () ->
|
||||||
|
@.index--
|
||||||
|
|
||||||
|
if @.index < 0
|
||||||
|
@.index = @.images.size - 1
|
||||||
|
|
||||||
|
angular.module('taigaComponents').controller('CardSlideshow', CardSlideshowController)
|
|
@ -0,0 +1,33 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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: card.directive.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
module = angular.module("taigaComponents")
|
||||||
|
|
||||||
|
cardSlideshowDirective = () ->
|
||||||
|
return {
|
||||||
|
controller: "CardSlideshow",
|
||||||
|
templateUrl: "components/card-slideshow/card-slideshow.html",
|
||||||
|
bindToController: true,
|
||||||
|
controllerAs: "vm",
|
||||||
|
scope: {
|
||||||
|
images: "="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.directive('tgCardSlideshow', cardSlideshowDirective)
|
|
@ -0,0 +1,18 @@
|
||||||
|
.card-slideshow(ng-if="vm.images.size")
|
||||||
|
tg-svg.slideshow-icon.slideshow-left(
|
||||||
|
ng-click="vm.previous()"
|
||||||
|
ng-if="vm.images.size > 1"
|
||||||
|
svg-icon="icon-arrow-left"
|
||||||
|
)
|
||||||
|
tg-svg.slideshow-icon.slideshow-right(
|
||||||
|
ng-click="vm.next()"
|
||||||
|
ng-if="vm.images.size > 1"
|
||||||
|
svg-icon="icon-arrow-right"
|
||||||
|
)
|
||||||
|
|
||||||
|
.card-slideshow-wrapper(
|
||||||
|
ng-if="$index == vm.index"
|
||||||
|
tg-repeat="image in vm.images track by image.get('id')"
|
||||||
|
)
|
||||||
|
tg-preload-image(preload-src="{{image.get('thumbnail_card_url')}}")
|
||||||
|
img(ng-src="{{image.get('thumbnail_card_url')}}")
|
|
@ -0,0 +1,4 @@
|
||||||
|
.card-completion(ng-if="vm.visible('extra_info') && vm.item.getIn(['model', 'tasks']).size")
|
||||||
|
.card-completion-bar
|
||||||
|
.card-completion-percentage(ng-style="{width: vm.closedTasksPercent() + '%'}" )
|
||||||
|
span.card-tooltip tasks {{vm.getClosedTasks().size}}/{{vm.item.getIn(['model', 'tasks']).size}}
|
|
@ -0,0 +1,21 @@
|
||||||
|
.card-data(
|
||||||
|
ng-if="vm.visible('extra_info')"
|
||||||
|
ng-class="{'empty-tasks': !vm.item.getIn(['model', 'tasks']).size}"
|
||||||
|
)
|
||||||
|
span.card-estimation(
|
||||||
|
ng-if="vm.item.getIn(['model', 'total_points']) === null",
|
||||||
|
translate="US.NOT_ESTIMATED"
|
||||||
|
)
|
||||||
|
span.card-estimation(
|
||||||
|
ng-if="vm.item.getIn(['model', 'total_points'])"
|
||||||
|
) {{"COMMON.FIELDS.POINTS" | translate}} {{vm.item.getIn(['model', 'total_points'])}}
|
||||||
|
.card-statistics
|
||||||
|
.statistic.card-votes(ng-class="{'active': vm.item.getIn(['model', 'is_voter'])}")
|
||||||
|
tg-svg(svg-icon="icon-upvote")
|
||||||
|
span {{vm.item.getIn(['model', 'total_voters'])}}
|
||||||
|
.statistic.card-watchers
|
||||||
|
tg-svg(svg-icon="icon-watch")
|
||||||
|
span {{vm.item.getIn(['model', 'watchers']).size}}
|
||||||
|
.statistic.card-attachments(ng-if="vm.item.getIn(['model', 'attachments']).size")
|
||||||
|
tg-svg(svg-icon="icon-attachment")
|
||||||
|
span {{vm.item.getIn(['model', 'attachments']).size}}
|
|
@ -0,0 +1,43 @@
|
||||||
|
.card-owner
|
||||||
|
.card-owner-info(ng-if="vm.item.get('assigned_to')")
|
||||||
|
.card-owner-avatar
|
||||||
|
img(
|
||||||
|
ng-class="{'is-iocaine': vm.item.getIn(['model', 'is_iocaine'])}"
|
||||||
|
tg-avatar="vm.item.get('assigned_to')"
|
||||||
|
)
|
||||||
|
tg-svg(
|
||||||
|
ng-if="vm.item.getIn(['model', 'is_iocaine'])"
|
||||||
|
svg-icon="icon-iocaine"
|
||||||
|
svg-title="COMMON.IOCAINE_TEXT"
|
||||||
|
)
|
||||||
|
span.card-owner-name(ng-if="vm.visible('owner')") {{vm.item.getIn(['assigned_to', 'full_name'])}}
|
||||||
|
div(ng-if="!vm.visible('owner')")
|
||||||
|
include card-title
|
||||||
|
|
||||||
|
.card-owner-info(ng-if="!vm.item.get('assigned_to')")
|
||||||
|
img(ng-src="/#{v}/images/unnamed.png")
|
||||||
|
span.card-owner-name(
|
||||||
|
ng-if="vm.visible('owner')",
|
||||||
|
translate="COMMON.ASSIGNED_TO.NOT_ASSIGNED"
|
||||||
|
)
|
||||||
|
div(ng-if="!vm.visible('owner')")
|
||||||
|
include card-title
|
||||||
|
|
||||||
|
.card-owner-actions(
|
||||||
|
ng-if="vm.visible('owner')"
|
||||||
|
tg-check-permission="{{vm.getPermissionsKey()}}"
|
||||||
|
)
|
||||||
|
a.e2e-assign.card-owner-assign(
|
||||||
|
ng-click="vm.onClickAssignedTo({id: vm.item.get('id')})"
|
||||||
|
href=""
|
||||||
|
)
|
||||||
|
tg-svg(svg-icon="icon-add-user")
|
||||||
|
span(translate="COMMON.CARD.ASSIGN_TO")
|
||||||
|
|
||||||
|
a.e2e-edit.card-edit(
|
||||||
|
href=""
|
||||||
|
ng-click="vm.onClickEdit({id: vm.item.get('id')})"
|
||||||
|
tg-loading="vm.item.get('loading')"
|
||||||
|
)
|
||||||
|
tg-svg(svg-icon="icon-edit")
|
||||||
|
span(translate="COMMON.CARD.EDIT")
|
|
@ -0,0 +1,7 @@
|
||||||
|
.card-tags(ng-if="vm.visible('tags')")
|
||||||
|
span.card-tag(
|
||||||
|
tg-repeat="tag in vm.item.get('colorized_tags') track by tag.get('name')"
|
||||||
|
style="background-color: {{tag.get('color')}}"
|
||||||
|
title="{{tag.get('name')}}"
|
||||||
|
ng-if="tag.get('color')"
|
||||||
|
)
|
|
@ -0,0 +1,7 @@
|
||||||
|
ul.card-tasks(ng-if="vm.isRelatedTasksVisible()")
|
||||||
|
li.card-task(tg-repeat="task in vm.item.getIn(['model', 'tasks'])")
|
||||||
|
a(
|
||||||
|
href="#"
|
||||||
|
tg-nav="project-tasks-detail:project=vm.project.slug,ref=task.get('ref')",
|
||||||
|
ng-class="{'closed-task': task.get('is_closed'), 'blocked-task': task.get('is_blocked')}"
|
||||||
|
) {{"#" + task.get('ref')}} {{task.get('subject')}}
|
|
@ -0,0 +1,9 @@
|
||||||
|
h2.card-title
|
||||||
|
a(
|
||||||
|
href=""
|
||||||
|
tg-nav="{{vm.getNavKey()}}:project=vm.project.slug,ref=vm.item.getIn(['model', 'ref'])",
|
||||||
|
tg-nav-get-params="{\"kanban-status\": {{vm.item.getIn(['model', 'status'])}}}"
|
||||||
|
title="#{{ ::vm.item.getIn(['model', 'ref']) }} {{ vm.item.getIn(['model', 'subject'])}}"
|
||||||
|
)
|
||||||
|
span(ng-if="vm.visible('ref')") {{::"#" + vm.item.getIn(['model', 'ref'])}}
|
||||||
|
span.e2e-title(ng-if="vm.visible('subject')") {{vm.item.getIn(['model', 'subject'])}}
|
|
@ -0,0 +1,6 @@
|
||||||
|
.card-unfold.ng-animate-disabled(
|
||||||
|
ng-click="vm.toggleFold()"
|
||||||
|
ng-if="vm.visible('unfold') && (vm.item.getIn(['model', 'tasks']).size || vm.item.get('images').size)"
|
||||||
|
role="button"
|
||||||
|
)
|
||||||
|
tg-svg(svg-icon="icon-view-more")
|
|
@ -0,0 +1,82 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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: card.controller.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
class CardController
|
||||||
|
@.$inject = []
|
||||||
|
|
||||||
|
visible: (name) ->
|
||||||
|
return @.zoom.indexOf(name) != -1
|
||||||
|
|
||||||
|
toggleFold: () ->
|
||||||
|
@.onToggleFold({id: @.item.get('id')})
|
||||||
|
|
||||||
|
getClosedTasks: () ->
|
||||||
|
return @.item.getIn(['model', 'tasks']).filter (task) -> return task.get('is_closed');
|
||||||
|
|
||||||
|
closedTasksPercent: () ->
|
||||||
|
return @.getClosedTasks().size * 100 / @.item.getIn(['model', 'tasks']).size
|
||||||
|
|
||||||
|
getPermissionsKey: () ->
|
||||||
|
if @.type == 'task'
|
||||||
|
return 'modify_task'
|
||||||
|
else
|
||||||
|
return 'modify_us'
|
||||||
|
|
||||||
|
_setVisibility: () ->
|
||||||
|
visibility = {
|
||||||
|
related: @.visible('related_tasks'),
|
||||||
|
slides: @.visible('attachments')
|
||||||
|
}
|
||||||
|
|
||||||
|
if!_.isUndefined(@.item.get('foldStatusChanged'))
|
||||||
|
if @.visible('related_tasks') && @.visible('attachments')
|
||||||
|
visibility.related = !@.item.get('foldStatusChanged')
|
||||||
|
visibility.slides = !@.item.get('foldStatusChanged')
|
||||||
|
else if @.visible('attachments')
|
||||||
|
visibility.related = @.item.get('foldStatusChanged')
|
||||||
|
visibility.slides = @.item.get('foldStatusChanged')
|
||||||
|
else if !@.visible('related_tasks') && !@.visible('attachments')
|
||||||
|
visibility.related = @.item.get('foldStatusChanged')
|
||||||
|
visibility.slides = @.item.get('foldStatusChanged')
|
||||||
|
|
||||||
|
if !@.item.getIn(['model', 'tasks']) || !@.item.getIn(['model', 'tasks']).size
|
||||||
|
visibility.related = false
|
||||||
|
|
||||||
|
if !@.item.get('images') || !@.item.get('images').size
|
||||||
|
visibility.slides = false
|
||||||
|
|
||||||
|
return visibility
|
||||||
|
|
||||||
|
isRelatedTasksVisible: () ->
|
||||||
|
visibility = @._setVisibility()
|
||||||
|
|
||||||
|
return visibility.related
|
||||||
|
|
||||||
|
isSlideshowVisible: () ->
|
||||||
|
visibility = @._setVisibility()
|
||||||
|
|
||||||
|
return visibility.slides
|
||||||
|
|
||||||
|
getNavKey: () ->
|
||||||
|
if @.type == 'task'
|
||||||
|
return 'project-tasks-detail'
|
||||||
|
else
|
||||||
|
return 'project-userstories-detail'
|
||||||
|
|
||||||
|
angular.module('taigaComponents').controller('Card', CardController)
|
|
@ -0,0 +1,142 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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: card.controller.spec.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
describe "Card", ->
|
||||||
|
$provide = null
|
||||||
|
$controller = null
|
||||||
|
mocks = {}
|
||||||
|
|
||||||
|
_inject = ->
|
||||||
|
inject (_$controller_) ->
|
||||||
|
$controller = _$controller_
|
||||||
|
|
||||||
|
_setup = ->
|
||||||
|
_inject()
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
module "taigaComponents"
|
||||||
|
|
||||||
|
_setup()
|
||||||
|
|
||||||
|
it "toggle fold callback", () ->
|
||||||
|
ctrl = $controller("Card")
|
||||||
|
|
||||||
|
ctrl.item = Immutable.fromJS({id: 2})
|
||||||
|
ctrl.onToggleFold = sinon.spy()
|
||||||
|
|
||||||
|
ctrl.toggleFold()
|
||||||
|
|
||||||
|
expect(ctrl.onToggleFold).to.have.been.calledWith({id: 2})
|
||||||
|
|
||||||
|
it "get closed tasks", () ->
|
||||||
|
ctrl = $controller("Card")
|
||||||
|
|
||||||
|
ctrl.item = Immutable.fromJS({
|
||||||
|
id: 2,
|
||||||
|
model: {
|
||||||
|
tasks: [
|
||||||
|
{is_closed: true},
|
||||||
|
{is_closed: false},
|
||||||
|
{is_closed: true}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tasks = ctrl.getClosedTasks()
|
||||||
|
expect(tasks.size).to.be.equal(2)
|
||||||
|
|
||||||
|
it "get closed percent", () ->
|
||||||
|
ctrl = $controller("Card")
|
||||||
|
|
||||||
|
ctrl.item = Immutable.fromJS({
|
||||||
|
id: 2,
|
||||||
|
model: {
|
||||||
|
tasks: [
|
||||||
|
{is_closed: true},
|
||||||
|
{is_closed: false},
|
||||||
|
{is_closed: false},
|
||||||
|
{is_closed: true}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
percent = ctrl.closedTasksPercent()
|
||||||
|
expect(percent).to.be.equal(50)
|
||||||
|
|
||||||
|
describe "check if related task and slides visibility", () ->
|
||||||
|
it "no content", () ->
|
||||||
|
ctrl = $controller("Card")
|
||||||
|
|
||||||
|
ctrl.item = Immutable.fromJS({
|
||||||
|
id: 2,
|
||||||
|
images: [],
|
||||||
|
model: {
|
||||||
|
tasks: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ctrl.visible = () => return true
|
||||||
|
|
||||||
|
visibility = ctrl._setVisibility()
|
||||||
|
|
||||||
|
expect(visibility).to.be.eql({
|
||||||
|
related: false,
|
||||||
|
slides: false
|
||||||
|
})
|
||||||
|
|
||||||
|
it "with content", () ->
|
||||||
|
ctrl = $controller("Card")
|
||||||
|
|
||||||
|
ctrl.item = Immutable.fromJS({
|
||||||
|
id: 2,
|
||||||
|
images: [3,4],
|
||||||
|
model: {
|
||||||
|
tasks: [1,2]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ctrl.visible = () => return true
|
||||||
|
|
||||||
|
visibility = ctrl._setVisibility()
|
||||||
|
|
||||||
|
expect(visibility).to.be.eql({
|
||||||
|
related: true,
|
||||||
|
slides: true
|
||||||
|
})
|
||||||
|
|
||||||
|
it "fold", () ->
|
||||||
|
ctrl = $controller("Card")
|
||||||
|
|
||||||
|
ctrl.item = Immutable.fromJS({
|
||||||
|
foldStatusChanged: true,
|
||||||
|
id: 2,
|
||||||
|
images: [3,4],
|
||||||
|
model: {
|
||||||
|
tasks: [1,2]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ctrl.visible = () => return true
|
||||||
|
|
||||||
|
visibility = ctrl._setVisibility()
|
||||||
|
|
||||||
|
expect(visibility).to.be.eql({
|
||||||
|
related: false,
|
||||||
|
slides: false
|
||||||
|
})
|
|
@ -0,0 +1,43 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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: card.directive.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
module = angular.module("taigaComponents")
|
||||||
|
|
||||||
|
cardDirective = () ->
|
||||||
|
return {
|
||||||
|
link: (scope) ->
|
||||||
|
|
||||||
|
controller: "Card",
|
||||||
|
controllerAs: "vm",
|
||||||
|
bindToController: true,
|
||||||
|
templateUrl: "components/card/card.html",
|
||||||
|
scope: {
|
||||||
|
onToggleFold: "&",
|
||||||
|
onClickAssignedTo: "&",
|
||||||
|
onClickEdit: "&",
|
||||||
|
project: "=",
|
||||||
|
item: "=",
|
||||||
|
zoom: "=",
|
||||||
|
zoomLevel: "=",
|
||||||
|
archived: "=",
|
||||||
|
type: "@"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.directive('tgCard', cardDirective)
|
|
@ -0,0 +1,16 @@
|
||||||
|
.card-inner(
|
||||||
|
class="{{'zoom-' + vm.zoomLevel}}"
|
||||||
|
ng-class="{'card-blocked': vm.item.getIn(['model', 'is_blocked']), 'archived': vm.archived}"
|
||||||
|
)
|
||||||
|
include card-templates/card-tags
|
||||||
|
include card-templates/card-owner
|
||||||
|
div(ng-if="vm.visible('owner')")
|
||||||
|
include card-templates/card-title
|
||||||
|
include card-templates/card-data
|
||||||
|
include card-templates/card-completion
|
||||||
|
include card-templates/card-tasks
|
||||||
|
tg-card-slideshow(
|
||||||
|
ng-if="vm.isSlideshowVisible()"
|
||||||
|
images="vm.item.get('images')"
|
||||||
|
)
|
||||||
|
include card-templates/card-unfold
|
|
@ -0,0 +1,326 @@
|
||||||
|
.card {
|
||||||
|
box-shadow: 2px 2px 4px darken($whitish, 10%);
|
||||||
|
cursor: move;
|
||||||
|
display: block;
|
||||||
|
margin: 0 .6rem .6rem;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow .2s ease-in;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 3px 3px 6px darken($whitish, 10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-inner {
|
||||||
|
background: $white;
|
||||||
|
border-radius: .25rem;
|
||||||
|
&.zoom-0,
|
||||||
|
&.zoom-1 {
|
||||||
|
.card-title {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
padding: .25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.zoom-1 {
|
||||||
|
.card-owner-info {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.card-blocked {
|
||||||
|
background: $red-light;
|
||||||
|
.statistic,
|
||||||
|
.card-title a,
|
||||||
|
.card-owner-name,
|
||||||
|
.card-estimation {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
.card-owner-actions {
|
||||||
|
background: rgba($red-light, .9);
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
fill: $white;
|
||||||
|
}
|
||||||
|
.statistic {
|
||||||
|
&.active {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card-unfold {
|
||||||
|
&:hover {
|
||||||
|
background: rgba($red-light, .9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.zoom-0,
|
||||||
|
&.zoom-1 {
|
||||||
|
.card-title {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-tags {
|
||||||
|
display: flex;
|
||||||
|
.card-tag {
|
||||||
|
display: block;
|
||||||
|
flex: 1;
|
||||||
|
height: .5rem;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-owner {
|
||||||
|
position: relative;
|
||||||
|
.card-owner-info {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.card-owner-avatar {
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.icon-iocaine {
|
||||||
|
@include svg-size(1.2rem);
|
||||||
|
background: rgba($blackish, .8);
|
||||||
|
border-radius: 4px 0 0;
|
||||||
|
bottom: .25rem;
|
||||||
|
fill: $whitish;
|
||||||
|
padding: .25rem;
|
||||||
|
position: absolute;
|
||||||
|
right: .5rem;
|
||||||
|
}
|
||||||
|
.is-iocaine {
|
||||||
|
filter: hue-rotate(265deg) saturate(3);
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
.card-owner-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 2.5rem;
|
||||||
|
margin-right: .5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
}
|
||||||
|
.card-owner-name {
|
||||||
|
color: $gray-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-owner-actions {
|
||||||
|
background: rgba($white, .9);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
transition: all .2s;
|
||||||
|
width: 100%;
|
||||||
|
&:hover {
|
||||||
|
color: $primary-light;
|
||||||
|
svg {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
@include svg-size(1.2rem);
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: .25rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
padding: .6rem 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
@include font-size(normal);
|
||||||
|
line-height: 1.25;
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
padding: 1rem 1rem 0;
|
||||||
|
span {
|
||||||
|
padding-right: .25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-data {
|
||||||
|
color: $gray-light;
|
||||||
|
display: flex;
|
||||||
|
font-size: 14px;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 1rem .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-statistics {
|
||||||
|
@include font-size(small);
|
||||||
|
color: lighten($gray-light, 25%);
|
||||||
|
display: flex;
|
||||||
|
margin-left: auto;
|
||||||
|
.statistic {
|
||||||
|
align-content: center;
|
||||||
|
display: flex;
|
||||||
|
margin-left: .75rem;
|
||||||
|
&.active {
|
||||||
|
color: $primary-light;
|
||||||
|
svg {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
@include svg-size(.75rem);
|
||||||
|
fill: lighten($gray-light, 25%);
|
||||||
|
margin-right: .2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-completion {
|
||||||
|
margin: 0 1rem .5rem;
|
||||||
|
position: relative;
|
||||||
|
.card-completion-bar {
|
||||||
|
background: $whitish;
|
||||||
|
height: .4rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.card-completion-percentage {
|
||||||
|
background: $primary-light;
|
||||||
|
cursor: pointer;
|
||||||
|
height: .4rem;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
&:hover {
|
||||||
|
+ .card-tooltip {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card-tooltip {
|
||||||
|
background: $blackish;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: $white;
|
||||||
|
font-size: 14px;
|
||||||
|
left: calc(25% - 50px);
|
||||||
|
opacity: 0;
|
||||||
|
padding: .25rem 1rem;
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
top: -2.25rem;
|
||||||
|
transition: opacity .2s;
|
||||||
|
width: 100px;
|
||||||
|
&::after {
|
||||||
|
background: $black;
|
||||||
|
content: '';
|
||||||
|
height: 10px;
|
||||||
|
left: 50%;
|
||||||
|
position: absolute;
|
||||||
|
top: 70%;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-unfold {
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: .25rem;
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(to bottom, $white, darken($white, 1%));
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
@include svg-size($width: 2rem, $height: .3rem);
|
||||||
|
fill: $whitish;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-tasks {
|
||||||
|
border-top: 1px solid $whitish;
|
||||||
|
margin: 0;
|
||||||
|
margin-top: .5rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-task {
|
||||||
|
@include font-size(xsmall);
|
||||||
|
border-bottom: 1px solid $whitish;
|
||||||
|
list-style: none;
|
||||||
|
a {
|
||||||
|
color: $gray-light;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: .5rem .75rem;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transition: color .2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
&.blocked-task {
|
||||||
|
color: $red-light;
|
||||||
|
}
|
||||||
|
&.closed-task {
|
||||||
|
color: $gray-light;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-slideshow {
|
||||||
|
position: relative;
|
||||||
|
&:hover {
|
||||||
|
.slideshow-left,
|
||||||
|
.slideshow-right {
|
||||||
|
background: rgba($white, .2);
|
||||||
|
padding: .25rem;
|
||||||
|
transition: background .2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.slideshow-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 35%;
|
||||||
|
&:hover {
|
||||||
|
background: rgba($primary-light, .5);
|
||||||
|
transition: background .2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
@include svg-size(1.2rem);
|
||||||
|
transition: fill .2s;
|
||||||
|
}
|
||||||
|
.slideshow-left,
|
||||||
|
.slideshow-right {
|
||||||
|
background: transparent;
|
||||||
|
padding: .25rem;
|
||||||
|
}
|
||||||
|
.slideshow-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.slideshow-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-slideshow-wrapper {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 120px;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
.loading-spinner {
|
||||||
|
min-height: 3rem;
|
||||||
|
min-width: 3rem;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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: filter-utils.service.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
generateHash = taiga.generateHash
|
||||||
|
|
||||||
|
class FilterRemoteStorageService extends taiga.Service
|
||||||
|
@.$inject = [
|
||||||
|
"$q",
|
||||||
|
"$tgUrls",
|
||||||
|
"$tgHttp"
|
||||||
|
]
|
||||||
|
|
||||||
|
constructor: (@q, @urls, @http) ->
|
||||||
|
|
||||||
|
storeFilters: (projectId, myFilters, filtersHashSuffix) ->
|
||||||
|
deferred = @q.defer()
|
||||||
|
url = @urls.resolve("user-storage")
|
||||||
|
ns = "#{projectId}:#{filtersHashSuffix}"
|
||||||
|
hash = generateHash([projectId, ns])
|
||||||
|
if _.isEmpty(myFilters)
|
||||||
|
promise = @http.delete("#{url}/#{hash}", {key: hash, value:myFilters})
|
||||||
|
promise.then ->
|
||||||
|
deferred.resolve()
|
||||||
|
promise.then null, ->
|
||||||
|
deferred.reject()
|
||||||
|
else
|
||||||
|
promise = @http.put("#{url}/#{hash}", {key: hash, value:myFilters})
|
||||||
|
promise.then (data) ->
|
||||||
|
deferred.resolve()
|
||||||
|
promise.then null, (data) =>
|
||||||
|
innerPromise = @http.post("#{url}", {key: hash, value:myFilters})
|
||||||
|
innerPromise.then ->
|
||||||
|
deferred.resolve()
|
||||||
|
innerPromise.then null, ->
|
||||||
|
deferred.reject()
|
||||||
|
return deferred.promise
|
||||||
|
|
||||||
|
getFilters: (projectId, filtersHashSuffix) ->
|
||||||
|
deferred = @q.defer()
|
||||||
|
url = @urls.resolve("user-storage")
|
||||||
|
ns = "#{projectId}:#{filtersHashSuffix}"
|
||||||
|
hash = generateHash([projectId, ns])
|
||||||
|
|
||||||
|
promise = @http.get("#{url}/#{hash}")
|
||||||
|
promise.then (data) ->
|
||||||
|
deferred.resolve(data.data.value)
|
||||||
|
promise.then null, (data) ->
|
||||||
|
deferred.resolve({})
|
||||||
|
|
||||||
|
return deferred.promise
|
||||||
|
|
||||||
|
angular.module("taigaComponents").service("tgFilterRemoteStorageService", FilterRemoteStorageService)
|
|
@ -0,0 +1,45 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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: filter.-slide-down.controller.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
FilterSlideDownDirective = () ->
|
||||||
|
link = (scope, el, attrs, ctrl) ->
|
||||||
|
filter = $('tg-filter')
|
||||||
|
|
||||||
|
scope.$watch attrs.ngIf, (value) ->
|
||||||
|
if value
|
||||||
|
filter.find('.filter-list').hide()
|
||||||
|
|
||||||
|
wrapperHeight = filter.height()
|
||||||
|
contentHeight = 0
|
||||||
|
|
||||||
|
filter.children().each () ->
|
||||||
|
contentHeight += $(this).outerHeight(true)
|
||||||
|
|
||||||
|
$(el.context.nextSibling)
|
||||||
|
.css({
|
||||||
|
"max-height": wrapperHeight - contentHeight,
|
||||||
|
"display": "block"
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
priority: 900,
|
||||||
|
link: link
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('taigaComponents').directive("tgFilterSlideDown", [FilterSlideDownDirective])
|
|
@ -0,0 +1,70 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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: filter.controller.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
class FilterController
|
||||||
|
@.$inject = []
|
||||||
|
|
||||||
|
constructor: () ->
|
||||||
|
@.opened = null
|
||||||
|
@.customFilterForm = false
|
||||||
|
@.customFilterName = ''
|
||||||
|
|
||||||
|
toggleFilterCategory: (filterName) ->
|
||||||
|
if @.opened == filterName
|
||||||
|
@.opened = null
|
||||||
|
else
|
||||||
|
@.opened = filterName
|
||||||
|
|
||||||
|
isOpen: (filterName) ->
|
||||||
|
return @.opened == filterName
|
||||||
|
|
||||||
|
saveCustomFilter: () ->
|
||||||
|
@.onSaveCustomFilter({name: @.customFilterName})
|
||||||
|
@.customFilterForm = false
|
||||||
|
@.opened = 'custom-filter'
|
||||||
|
@.customFilterName = ''
|
||||||
|
|
||||||
|
changeQ: () ->
|
||||||
|
@.onChangeQ({q: @.q})
|
||||||
|
|
||||||
|
unselectFilter: (filter) ->
|
||||||
|
@.onRemoveFilter({filter: filter})
|
||||||
|
|
||||||
|
unselectFilter: (filter) ->
|
||||||
|
@.onRemoveFilter({filter: filter})
|
||||||
|
|
||||||
|
selectFilter: (filterCategory, filter) ->
|
||||||
|
filter = {
|
||||||
|
category: filterCategory
|
||||||
|
filter: filter
|
||||||
|
}
|
||||||
|
|
||||||
|
@.onAddFilter({filter: filter})
|
||||||
|
|
||||||
|
removeCustomFilter: (filter) ->
|
||||||
|
@.onRemoveCustomFilter({filter: filter})
|
||||||
|
|
||||||
|
selectCustomFilter: (filter) ->
|
||||||
|
@.onSelectCustomFilter({filter: filter})
|
||||||
|
|
||||||
|
isFilterSelected: (filterCategory, filter) ->
|
||||||
|
return !!_.find @.selectedFilters, (it) ->
|
||||||
|
return filter.id == it.id && filterCategory.dataType == it.dataType
|
||||||
|
|
||||||
|
angular.module('taigaComponents').controller('Filter', FilterController)
|
|
@ -0,0 +1,87 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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: filter.controller.spec.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
describe "Filter", ->
|
||||||
|
$provide = null
|
||||||
|
$controller = null
|
||||||
|
mocks = {}
|
||||||
|
|
||||||
|
_inject = ->
|
||||||
|
inject (_$controller_) ->
|
||||||
|
$controller = _$controller_
|
||||||
|
|
||||||
|
_setup = ->
|
||||||
|
_inject()
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
module "taigaComponents"
|
||||||
|
|
||||||
|
_setup()
|
||||||
|
|
||||||
|
it "toggle filter category", () ->
|
||||||
|
ctrl = $controller("Filter")
|
||||||
|
|
||||||
|
ctrl.toggleFilterCategory('filter1')
|
||||||
|
|
||||||
|
expect(ctrl.opened).to.be.equal('filter1')
|
||||||
|
|
||||||
|
ctrl.toggleFilterCategory('filter1')
|
||||||
|
|
||||||
|
expect(ctrl.opened).to.be.null
|
||||||
|
|
||||||
|
it "is filter open", () ->
|
||||||
|
ctrl = $controller("Filter")
|
||||||
|
ctrl.opened = 'filter1'
|
||||||
|
|
||||||
|
isOpen = ctrl.isOpen('filter1')
|
||||||
|
|
||||||
|
expect(isOpen).to.be.true;
|
||||||
|
|
||||||
|
it "save custom filter", () ->
|
||||||
|
ctrl = $controller("Filter")
|
||||||
|
ctrl.customFilterName = "custom-name"
|
||||||
|
ctrl.customFilterForm = true
|
||||||
|
ctrl.onSaveCustomFilter = sinon.spy()
|
||||||
|
|
||||||
|
ctrl.saveCustomFilter()
|
||||||
|
|
||||||
|
expect(ctrl.onSaveCustomFilter).to.have.been.calledWith({name: "custom-name"})
|
||||||
|
expect(ctrl.customFilterForm).to.be.false
|
||||||
|
expect(ctrl.opened).to.be.equal('custom-filter')
|
||||||
|
expect(ctrl.customFilterName).to.be.equal('')
|
||||||
|
|
||||||
|
it "is filter selected", () ->
|
||||||
|
ctrl = $controller("Filter")
|
||||||
|
ctrl.selectedFilters = [
|
||||||
|
{id: 1, dataType: "1"},
|
||||||
|
{id: 2, dataType: "2"},
|
||||||
|
{id: 3, dataType: "3"}
|
||||||
|
]
|
||||||
|
|
||||||
|
filterCategory = {dataType: "x"}
|
||||||
|
filter = {id: 1}
|
||||||
|
isFilterSelected = ctrl.isFilterSelected(filterCategory, filter)
|
||||||
|
|
||||||
|
expect(isFilterSelected).to.be.false
|
||||||
|
|
||||||
|
filterCategory = {dataType: "1"}
|
||||||
|
filter = {id: 1}
|
||||||
|
isFilterSelected = ctrl.isFilterSelected(filterCategory, filter)
|
||||||
|
|
||||||
|
expect(isFilterSelected).to.be.true
|
|
@ -0,0 +1,44 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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: filter.directive.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
FilterDirective = () ->
|
||||||
|
link = (scope, el, attrs, ctrl) ->
|
||||||
|
|
||||||
|
return {
|
||||||
|
scope: {
|
||||||
|
onChangeQ: "&",
|
||||||
|
onAddFilter: "&",
|
||||||
|
onSelectCustomFilter: "&",
|
||||||
|
onRemoveFilter: "&",
|
||||||
|
onRemoveCustomFilter: "&",
|
||||||
|
onSaveCustomFilter: "&",
|
||||||
|
customFilters: "<",
|
||||||
|
q: "<",
|
||||||
|
filters: "<"
|
||||||
|
customFilters: "<"
|
||||||
|
selectedFilters: "<"
|
||||||
|
},
|
||||||
|
bindToController: true,
|
||||||
|
controller: "Filter",
|
||||||
|
controllerAs: "vm",
|
||||||
|
templateUrl: 'components/filter/filter.html',
|
||||||
|
link: link
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('taigaComponents').directive("tgFilter", [FilterDirective])
|
|
@ -0,0 +1,110 @@
|
||||||
|
h1
|
||||||
|
span.title(translate="COMMON.FILTERS.TITLE")
|
||||||
|
|
||||||
|
form
|
||||||
|
fieldset
|
||||||
|
input.e2e-filter-q(
|
||||||
|
type="text",
|
||||||
|
placeholder="{{'COMMON.FILTERS.INPUT_PLACEHOLDER' | translate}}",
|
||||||
|
ng-model="vm.q"
|
||||||
|
ng-model-options="{ debounce: 200 }"
|
||||||
|
ng-change="vm.changeQ()"
|
||||||
|
)
|
||||||
|
tg-svg.search-action(
|
||||||
|
svg-icon="icon-search",
|
||||||
|
title="{{'COMMON.FILTERS.TITLE_ACTION_SEARCH' | translate}}"
|
||||||
|
)
|
||||||
|
|
||||||
|
.filters-step-cat
|
||||||
|
.filters-applied
|
||||||
|
.single-filter.ng-animate-disabled(ng-repeat="it in vm.selectedFilters track by it.key")
|
||||||
|
span.name(ng-attr-style="{{it.color ? 'border-left: 3px solid ' + it.color: ''}}") {{it.name}}
|
||||||
|
a.remove-filter.e2e-remove-filter(
|
||||||
|
ng-click="vm.unselectFilter(it)"
|
||||||
|
href=""
|
||||||
|
)
|
||||||
|
tg-svg(svg-icon="icon-close")
|
||||||
|
|
||||||
|
a.button.button-gray.save-filters.ng-animate-disabled.e2e-open-custom-filter-form(
|
||||||
|
ng-click="vm.customFilterForm = true"
|
||||||
|
ng-if="vm.selectedFilters.length && !vm.customFilterForm"
|
||||||
|
href="",
|
||||||
|
title="{{'COMMON.SAVE' | translate}}",
|
||||||
|
translate="COMMON.FILTERS.ACTION_SAVE_CUSTOM_FILTER"
|
||||||
|
)
|
||||||
|
|
||||||
|
form(
|
||||||
|
ng-if="vm.customFilterForm"
|
||||||
|
ng-submit="vm.saveCustomFilter()"
|
||||||
|
)
|
||||||
|
input.my-filter-name.e2e-filter-name-input(
|
||||||
|
tg-autofocus
|
||||||
|
ng-model="vm.customFilterName"
|
||||||
|
type="text"
|
||||||
|
placeholder="{{'COMMON.FILTERS.PLACEHOLDER_FILTER_NAME' | translate}}"
|
||||||
|
)
|
||||||
|
|
||||||
|
.filters-cats
|
||||||
|
ul
|
||||||
|
li(
|
||||||
|
ng-class="{selected: vm.isOpen(filter.dataType)}"
|
||||||
|
ng-repeat="filter in vm.filters track by filter.dataType"
|
||||||
|
)
|
||||||
|
a.filters-cat-single.e2e-category(
|
||||||
|
ng-class="{selected: vm.isOpen(filter.dataType)}"
|
||||||
|
ng-click="vm.toggleFilterCategory(filter.dataType)"
|
||||||
|
href=""
|
||||||
|
title="{{::filter.title}}"
|
||||||
|
)
|
||||||
|
span.title {{::filter.title}}
|
||||||
|
tg-svg.ng-animate-disabled(
|
||||||
|
ng-if="!vm.isOpen(filter.dataType)"
|
||||||
|
svg-icon="icon-arrow-right"
|
||||||
|
)
|
||||||
|
tg-svg.ng-animate-disabled(
|
||||||
|
ng-if="vm.isOpen(filter.dataType)"
|
||||||
|
svg-icon="icon-arrow-down"
|
||||||
|
)
|
||||||
|
|
||||||
|
.filter-list(
|
||||||
|
ng-if="vm.isOpen(filter.dataType)",
|
||||||
|
tg-filter-slide-down
|
||||||
|
)
|
||||||
|
.single-filter.ng-animate-disabled(
|
||||||
|
ng-repeat="it in filter.content"
|
||||||
|
ng-if="!vm.isFilterSelected(filter, it) && !(it.count == 0 && filter.hideEmpty)"
|
||||||
|
ng-click="vm.selectFilter(filter, it)"
|
||||||
|
)
|
||||||
|
span.name(ng-attr-style="{{it.color ? 'border-left: 3px solid ' + it.color: ''}}") {{it.name}}
|
||||||
|
span.number.e2e-filter-count(ng-if="it.count > 0") {{it.count}}
|
||||||
|
|
||||||
|
li.custom-filters.e2e-custom-filters(ng-class="{selected: vm.isOpen('custom-filter')}")
|
||||||
|
a.filters-cat-single(
|
||||||
|
ng-class="{selected: vm.isOpen('custom-filter')}"
|
||||||
|
ng-click="vm.toggleFilterCategory('custom-filter')"
|
||||||
|
href=""
|
||||||
|
title="{{'COMMON.FILTERS.CATEGORIES.CUSTOM_FILTERS' | translate}}"
|
||||||
|
)
|
||||||
|
span.title(translate="COMMON.FILTERS.CATEGORIES.CUSTOM_FILTERS")
|
||||||
|
tg-svg.ng-animate-disabled(
|
||||||
|
ng-if="!vm.isOpen('custom-filter')"
|
||||||
|
svg-icon="icon-arrow-right"
|
||||||
|
)
|
||||||
|
tg-svg.ng-animate-disabled(
|
||||||
|
ng-if="vm.isOpen('custom-filter')"
|
||||||
|
svg-icon="icon-arrow-down"
|
||||||
|
)
|
||||||
|
.filter-list(
|
||||||
|
ng-if="vm.isOpen('custom-filter')",
|
||||||
|
tg-filter-slide-down
|
||||||
|
)
|
||||||
|
.single-filter.ng-animate-disabled.e2e-custom-filter(
|
||||||
|
ng-repeat="it in vm.customFilters"
|
||||||
|
ng-click="vm.selectCustomFilter(it)"
|
||||||
|
)
|
||||||
|
span.name {{it.name}}
|
||||||
|
a.remove-filter.e2e-remove-custom-filter(
|
||||||
|
ng-click="vm.removeCustomFilter(it)"
|
||||||
|
href=""
|
||||||
|
)
|
||||||
|
tg-svg(svg-icon="icon-trash")
|
|
@ -0,0 +1,150 @@
|
||||||
|
tg-filter {
|
||||||
|
background-color: $whitish;
|
||||||
|
display: block;
|
||||||
|
left: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 1rem 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 260px;
|
||||||
|
z-index: 1;
|
||||||
|
.filters-applied {
|
||||||
|
padding: 0 1rem 1rem;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
form {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
background: $grayer;
|
||||||
|
color: $white;
|
||||||
|
@include placeholder {
|
||||||
|
color: $gray-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.search-action {
|
||||||
|
position: absolute;
|
||||||
|
right: .7rem;
|
||||||
|
top: .7rem;
|
||||||
|
}
|
||||||
|
&.ng-hide-add {
|
||||||
|
transform: translateX(0);
|
||||||
|
transition-duration: .5s;
|
||||||
|
}
|
||||||
|
&.ng-hide-add-active {
|
||||||
|
transform: translateX(-260px);
|
||||||
|
}
|
||||||
|
&.ng-hide-remove {
|
||||||
|
transform: translateX(-260px);
|
||||||
|
transition-duration: .5s;
|
||||||
|
}
|
||||||
|
&.ng-hide-remove-active {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-list {
|
||||||
|
display: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-step-cat {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-cats {
|
||||||
|
ul {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
border-bottom: 1px solid $gray-light;
|
||||||
|
text-transform: uppercase;
|
||||||
|
&.selected {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.custom-filters {
|
||||||
|
.title {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filters-cat-single {
|
||||||
|
align-items: center;
|
||||||
|
color: $grayer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: .5rem .5rem .5rem 1.5rem;
|
||||||
|
transition: color .2s ease-in;
|
||||||
|
&:hover,
|
||||||
|
&.selected {
|
||||||
|
background-color: rgba(darken($whitish, 20%), 1);
|
||||||
|
color: $grayer;
|
||||||
|
transition: background-color .2s ease-in;
|
||||||
|
.icon {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity .2s ease-in;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.icon-arrow-down {
|
||||||
|
fill: currentColor;
|
||||||
|
float: right;
|
||||||
|
height: .9rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .2s ease-in;
|
||||||
|
width: .9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-filter {
|
||||||
|
@include font-type(text);
|
||||||
|
@include clearfix;
|
||||||
|
align-items: center;
|
||||||
|
background: darken($whitish, 10%); // Fallback
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
opacity: .5;
|
||||||
|
padding-right: .5rem;
|
||||||
|
position: relative;
|
||||||
|
&:hover {
|
||||||
|
color: $grayer;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity .2s linear;
|
||||||
|
}
|
||||||
|
&.selected,
|
||||||
|
&.active {
|
||||||
|
color: $grayer;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity .2s linear;
|
||||||
|
}
|
||||||
|
.name,
|
||||||
|
.number {
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
@include ellipsis(100%);
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.number {
|
||||||
|
background: darken($whitish, 20%); // Fallback
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.remove-filter {
|
||||||
|
display: block;
|
||||||
|
svg {
|
||||||
|
fill: $gray;
|
||||||
|
transition: fill .2s linear;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
svg {
|
||||||
|
fill: $red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -138,7 +138,7 @@ class JoyRideService extends taiga.Service
|
||||||
|
|
||||||
if @checkPermissionsService.check('add_us')
|
if @checkPermissionsService.check('add_us')
|
||||||
steps.push({
|
steps.push({
|
||||||
element: '.icon-plus',
|
element: '.add-action',
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
joyride: {
|
joyride: {
|
||||||
title: @translate.instant('JOYRIDE.KANBAN.STEP3.TITLE')
|
title: @translate.instant('JOYRIDE.KANBAN.STEP3.TITLE')
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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: kanban-board-zoom.directive.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
KanbanBoardZoomDirective = (storage, projectService) ->
|
||||||
|
link = (scope, el, attrs, ctrl) ->
|
||||||
|
scope.zoomIndex = storage.get("kanban_zoom") or 2
|
||||||
|
scope.levels = 5
|
||||||
|
|
||||||
|
zooms = [
|
||||||
|
["ref"],
|
||||||
|
["subject"],
|
||||||
|
["owner", "tags", "extra_info", "unfold"],
|
||||||
|
["attachments"],
|
||||||
|
["related_tasks"]
|
||||||
|
]
|
||||||
|
|
||||||
|
getZoomView = (zoomIndex = 0) ->
|
||||||
|
if storage.get("kanban_zoom") != zoomIndex
|
||||||
|
storage.set("kanban_zoom", zoomIndex)
|
||||||
|
|
||||||
|
return _.reduce zooms, (result, value, key) ->
|
||||||
|
if key <= zoomIndex
|
||||||
|
result = result.concat(value)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
scope.$watch 'zoomIndex', (zoomLevel) ->
|
||||||
|
zoom = getZoomView(zoomLevel)
|
||||||
|
scope.onZoomChange({zoomLevel: zoomLevel, zoom: zoom})
|
||||||
|
|
||||||
|
unwatch = scope.$watch () ->
|
||||||
|
return projectService.project
|
||||||
|
, (project) ->
|
||||||
|
if project
|
||||||
|
if project.get('my_permissions').indexOf("view_tasks") == -1
|
||||||
|
scope.levels = 4
|
||||||
|
unwatch()
|
||||||
|
|
||||||
|
return {
|
||||||
|
scope: {
|
||||||
|
onZoomChange: "&"
|
||||||
|
},
|
||||||
|
template: """
|
||||||
|
<tg-board-zoom
|
||||||
|
class="board-zoom"
|
||||||
|
value="zoomIndex"
|
||||||
|
levels="levels"
|
||||||
|
></tg-board-zoom>
|
||||||
|
""",
|
||||||
|
link: link
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('taigaComponents').directive("tgKanbanBoardZoom", ["$tgStorage", "tgProjectService", KanbanBoardZoomDirective])
|
|
@ -0,0 +1,62 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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: taskboard-zoom.directive.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
TaskboardZoomDirective = (storage) ->
|
||||||
|
link = (scope, el, attrs, ctrl) ->
|
||||||
|
scope.zoomIndex = storage.get("taskboard_zoom") or 2
|
||||||
|
|
||||||
|
scope.levels = 4
|
||||||
|
|
||||||
|
zooms = [
|
||||||
|
["ref"],
|
||||||
|
["subject"],
|
||||||
|
["owner", "tags", "extra_info", "unfold"],
|
||||||
|
["attachments"],
|
||||||
|
["related_tasks"]
|
||||||
|
]
|
||||||
|
|
||||||
|
getZoomView = (zoomIndex = 0) ->
|
||||||
|
if storage.get("taskboard_zoom") != zoomIndex
|
||||||
|
storage.set("taskboard_zoom", zoomIndex)
|
||||||
|
|
||||||
|
return _.reduce zooms, (result, value, key) ->
|
||||||
|
if key <= zoomIndex
|
||||||
|
result = result.concat(value)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
scope.$watch 'zoomIndex', (zoomLevel) ->
|
||||||
|
zoom = getZoomView(zoomLevel)
|
||||||
|
scope.onZoomChange({zoomLevel: zoomLevel, zoom: zoom})
|
||||||
|
|
||||||
|
return {
|
||||||
|
scope: {
|
||||||
|
onZoomChange: "&"
|
||||||
|
},
|
||||||
|
template: """
|
||||||
|
<tg-board-zoom
|
||||||
|
levels="levels"
|
||||||
|
class="board-zoom"
|
||||||
|
value="zoomIndex"
|
||||||
|
></tg-board-zoom>
|
||||||
|
""",
|
||||||
|
link: link
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('taigaComponents').directive("tgTaskboardZoom", ["$tgStorage", TaskboardZoomDirective])
|
|
@ -3,8 +3,21 @@ doctype html
|
||||||
div.wrapper(tg-backlog, ng-controller="BacklogController as ctrl",
|
div.wrapper(tg-backlog, ng-controller="BacklogController as ctrl",
|
||||||
ng-init="section='backlog'")
|
ng-init="section='backlog'")
|
||||||
tg-project-menu
|
tg-project-menu
|
||||||
sidebar.menu-secondary.extrabar.filters-bar(tg-backlog-filters)
|
|
||||||
include ../includes/modules/backlog-filters
|
sidebar.backlog-filter
|
||||||
|
tg-filter(
|
||||||
|
q="ctrl.filterQ"
|
||||||
|
filters="ctrl.filters"
|
||||||
|
custom-filters="ctrl.customFilters"
|
||||||
|
selected-filters="ctrl.selectedFilters"
|
||||||
|
customFilters="ctl.customFilters"
|
||||||
|
on-save-custom-filter="ctrl.saveCustomFilter(name)"
|
||||||
|
on-add-filter="ctrl.addFilter(filter)"
|
||||||
|
on-select-custom-filter="ctrl.selectCustomFilter(filter)"
|
||||||
|
on-remove-custom-filter="ctrl.removeCustomFilter(filter)"
|
||||||
|
on-remove-filter="ctrl.removeFilter(filter)"
|
||||||
|
on-change-q="ctrl.changeQ(q)"
|
||||||
|
)
|
||||||
section.main.backlog
|
section.main.backlog
|
||||||
include ../includes/components/mainTitle
|
include ../includes/components/mainTitle
|
||||||
|
|
||||||
|
@ -39,13 +52,20 @@ div.wrapper(tg-backlog, ng-controller="BacklogController as ctrl",
|
||||||
)
|
)
|
||||||
tg-svg(svg-icon="icon-move")
|
tg-svg(svg-icon="icon-move")
|
||||||
span.text(translate="BACKLOG.MOVE_US_TO_LATEST_SPRINT")
|
span.text(translate="BACKLOG.MOVE_US_TO_LATEST_SPRINT")
|
||||||
a.trans-button(
|
a.trans-button.e2e-open-filter(
|
||||||
ng-if="userstories.length"
|
ng-if="!ctrl.activeFilters"
|
||||||
href=""
|
href=""
|
||||||
title="{{'BACKLOG.FILTERS.TOGGLE' | translate}}"
|
title="{{'BACKLOG.FILTERS.TOGGLE' | translate}}"
|
||||||
id="show-filters-button"
|
id="show-filters-button"
|
||||||
translate="BACKLOG.FILTERS.SHOW"
|
translate="BACKLOG.FILTERS.SHOW"
|
||||||
)
|
)
|
||||||
|
a.trans-button.active.e2e-open-filter(
|
||||||
|
ng-if="ctrl.activeFilters"
|
||||||
|
href=""
|
||||||
|
title="{{'BACKLOG.FILTERS.HIDE' | translate}}"
|
||||||
|
id="show-filters-button"
|
||||||
|
translate="BACKLOG.FILTERS.HIDE"
|
||||||
|
)
|
||||||
a.trans-button(
|
a.trans-button(
|
||||||
ng-if="userstories.length"
|
ng-if="userstories.length"
|
||||||
href=""
|
href=""
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
<% _.each(filters, function(f) { %>
|
|
||||||
.single-filter.selected(
|
|
||||||
data-type!="<%- f.type %>"
|
|
||||||
data-id!="<%- f.id %>"
|
|
||||||
)
|
|
||||||
span.name(style!="<%- f.style %>") <%- f.name %>
|
|
||||||
a.remove-filter(href="")
|
|
||||||
tg-svg(svg-icon="icon-close")
|
|
||||||
<% }) %>
|
|
|
@ -1,17 +0,0 @@
|
||||||
<% _.each(filters, function(f) { %>
|
|
||||||
<% if (f.selected) { %>
|
|
||||||
a.single-filter.active(data-type!="<%- f.type %>", data-id!="<%- f.id %>")
|
|
||||||
span.name(style!="<%- f.style %>")
|
|
||||||
| <%- f.name %>
|
|
||||||
<% if (f.count){ %>
|
|
||||||
span.number <%- f.count %>
|
|
||||||
<% } %>
|
|
||||||
<% } else { %>
|
|
||||||
a.single-filter(data-type!="<%- f.type %>", data-id!="<%- f.id %>")
|
|
||||||
span.name(style!="<%- f.style %>")
|
|
||||||
| <%- f.name %>
|
|
||||||
<% if (f.count){ %>
|
|
||||||
span.number <%- f.count %>
|
|
||||||
<% } %>
|
|
||||||
<% } %>
|
|
||||||
<% }) %>
|
|
|
@ -1,18 +0,0 @@
|
||||||
div.taskboard-tagline(tg-colorize-tags="task.tags", tg-colorize-tags-type="taskboard")
|
|
||||||
div.taskboard-task-inner
|
|
||||||
div.taskboard-user-avatar(tg-taskboard-user-avatar, users="usersById", task="task", project="project")
|
|
||||||
tg-svg.iocaine(
|
|
||||||
ng-if="task.is_iocaine"
|
|
||||||
svg-icon="icon-iocaine",
|
|
||||||
svg-title="{{'COMMON.IOCAINE_TEXT' | translate}}"
|
|
||||||
)
|
|
||||||
p.taskboard-text
|
|
||||||
a.task-assigned(href="", title="{{'TASKBOARD.TITLE_ACTION_ASSIGN' | translate}}")
|
|
||||||
span.task-num(tg-bo-ref="task.ref")
|
|
||||||
a.task-name(href="", title="#{{ ::task.ref }} {{ ::task.subject }}", ng-bind="task.subject",
|
|
||||||
tg-nav="project-tasks-detail:project=project.slug,ref=task.ref")
|
|
||||||
tg-svg.edit-task(
|
|
||||||
tg-check-permission="modify_task"
|
|
||||||
svg-icon="icon-edit",
|
|
||||||
svg-title-translate="TASKBOARD.TITLE_ACTION_EDIT"
|
|
||||||
)
|
|
|
@ -1,36 +0,0 @@
|
||||||
section.filters
|
|
||||||
div.filters-inner
|
|
||||||
h1
|
|
||||||
span.title(translate="COMMON.FILTERS.TITLE")
|
|
||||||
|
|
||||||
form
|
|
||||||
fieldset
|
|
||||||
input(type="text", placeholder="{{'COMMON.FILTERS.INPUT_PLACEHOLDER' | translate}}", ng-model="filtersQ")
|
|
||||||
tg-svg.search-action(
|
|
||||||
svg-icon="icon-search",
|
|
||||||
title="{{'COMMON.FILTERS.TITLE_ACTION_FILTER_BUTTON' | translate}}"
|
|
||||||
)
|
|
||||||
|
|
||||||
div.filters-step-cat
|
|
||||||
div.filters-applied
|
|
||||||
h2.hidden.breadcrumb
|
|
||||||
a.back(
|
|
||||||
href=""
|
|
||||||
title="{{'COMMON.FILTERS.BREADCRUMB_TITLE' | translate}}"
|
|
||||||
translate="BACKLOG.FILTERS.TITLE"
|
|
||||||
)
|
|
||||||
tg-svg(svg-icon="icon-arrow-right")
|
|
||||||
a.subfilter(href="")
|
|
||||||
span.title(translate="COMMON.FILTERS.BREADCRUMB_STATUS")
|
|
||||||
div.filters-cats
|
|
||||||
ul
|
|
||||||
li
|
|
||||||
a(href="", title="{{'BACKLOG.FILTERS.FILTER_CATEGORY_STATUS' | translate}}", data-type="status")
|
|
||||||
span.title(translate="BACKLOG.FILTERS.FILTER_CATEGORY_STATUS")
|
|
||||||
tg-svg(svg-icon="icon-arrow-right")
|
|
||||||
li
|
|
||||||
a(href="", title="{{'BACKLOG.FILTERS.FILTER_CATEGORY_TAGS' | translate}}", data-type="tags")
|
|
||||||
span.title(translate="BACKLOG.FILTERS.FILTER_CATEGORY_TAGS")
|
|
||||||
tg-svg(svg-icon="icon-arrow-right")
|
|
||||||
|
|
||||||
div.filter-list.hidden
|
|
|
@ -1,84 +0,0 @@
|
||||||
section.filters
|
|
||||||
div.filters-inner
|
|
||||||
h1
|
|
||||||
span.title(translate="ISSUES.FILTERS.TITLE")
|
|
||||||
form
|
|
||||||
fieldset
|
|
||||||
input(type="text", placeholder="{{'ISSUES.FILTERS.INPUT_SEARCH_PLACEHOLDER' | translate}}",
|
|
||||||
ng-model="filtersQ")
|
|
||||||
tg-svg.search-action(svg-icon="icon-search", title="{{'ISSUES.FILTERS.TITLE_ACTION_SEARCH' | translate}}")
|
|
||||||
div.filters-step-cat
|
|
||||||
div.filters-applied
|
|
||||||
a.hide.button.button-gray.save-filters(href="", title="{{'COMMON.SAVE' | translate}}", ng-class="{hide: filters.length}", translate="ISSUES.FILTERS.ACTION_SAVE_CUSTOM_FILTER")
|
|
||||||
h2.hidden.breadcrumb
|
|
||||||
a.back(href="", title="{{'ISSUES.FILTERS.TITLE_BREADCRUMB' | translate}}", translate="ISSUES.FILTERS.BREADCRUMB")
|
|
||||||
tg-svg(svg-icon="icon-arrow-right")
|
|
||||||
a.subfilter(href="", title="cat-name")
|
|
||||||
span.title(translate="COMMON.FILTERS.BREADCRUMB_STATUS")
|
|
||||||
div.filters-cats
|
|
||||||
ul
|
|
||||||
li
|
|
||||||
a.filters-cat-single(
|
|
||||||
href=""
|
|
||||||
title="{{ 'ISSUES.FILTERS.CATEGORIES.TYPE' | translate}}"
|
|
||||||
data-type="types"
|
|
||||||
)
|
|
||||||
span.title(translate="ISSUES.FILTERS.CATEGORIES.TYPE")
|
|
||||||
tg-svg(svg-icon="icon-arrow-right")
|
|
||||||
li
|
|
||||||
a.filters-cat-single(
|
|
||||||
href=""
|
|
||||||
title="{{ 'ISSUES.FILTERS.CATEGORIES.STATUS' | translate}}"
|
|
||||||
data-type="status"
|
|
||||||
)
|
|
||||||
span.title(translate="ISSUES.FILTERS.CATEGORIES.STATUS")
|
|
||||||
tg-svg(svg-icon="icon-arrow-right")
|
|
||||||
li
|
|
||||||
a.filters-cat-single(
|
|
||||||
href=""
|
|
||||||
title="{{ 'ISSUES.FILTERS.CATEGORIES.SEVERITY' | translate}}"
|
|
||||||
data-type="severities"
|
|
||||||
)
|
|
||||||
span.title(translate="ISSUES.FILTERS.CATEGORIES.SEVERITY")
|
|
||||||
tg-svg(svg-icon="icon-arrow-right")
|
|
||||||
li
|
|
||||||
a.filters-cat-single(
|
|
||||||
href=""
|
|
||||||
title="{{ 'ISSUES.FILTERS.CATEGORIES.PRIORITIES' | translate}}"
|
|
||||||
data-type="priorities"
|
|
||||||
)
|
|
||||||
span.title(translate="ISSUES.FILTERS.CATEGORIES.PRIORITIES")
|
|
||||||
tg-svg(svg-icon="icon-arrow-right")
|
|
||||||
li
|
|
||||||
a.filters-cat-single(
|
|
||||||
href=""
|
|
||||||
title="{{ 'ISSUES.FILTERS.CATEGORIES.TAGS' | translate}}"
|
|
||||||
data-type="tags"
|
|
||||||
)
|
|
||||||
span.title(translate="ISSUES.FILTERS.CATEGORIES.TAGS")
|
|
||||||
tg-svg(svg-icon="icon-arrow-right")
|
|
||||||
li
|
|
||||||
a.filters-cat-single(href=""
|
|
||||||
title="{{ 'ISSUES.FILTERS.CATEGORIES.ASSIGNED_TO' | translate}}"
|
|
||||||
data-type="assignedTo"
|
|
||||||
)
|
|
||||||
span.title(translate="ISSUES.FILTERS.CATEGORIES.ASSIGNED_TO")
|
|
||||||
tg-svg(svg-icon="icon-arrow-right")
|
|
||||||
li
|
|
||||||
a.filters-cat-single(
|
|
||||||
href=""
|
|
||||||
title="{{ 'ISSUES.FILTERS.CATEGORIES.CREATED_BY' | translate}}"
|
|
||||||
data-type="createdBy"
|
|
||||||
)
|
|
||||||
span.title(translate="ISSUES.FILTERS.CATEGORIES.CREATED_BY")
|
|
||||||
tg-svg(svg-icon="icon-arrow-right")
|
|
||||||
li.custom-filters(ng-if="filters.myFilters.length")
|
|
||||||
a.filters-cat-single(
|
|
||||||
href=""
|
|
||||||
title="{{ 'ISSUES.FILTERS.CATEGORIES.CUSTOM_FILTERS' | translate}}"
|
|
||||||
data-type="myFilters"
|
|
||||||
)
|
|
||||||
span.title(translate="ISSUES.FILTERS.CATEGORIES.CUSTOM_FILTERS")
|
|
||||||
tg-svg(svg-icon="icon-arrow-right")
|
|
||||||
|
|
||||||
div.filter-list.hidden
|
|
|
@ -1,13 +1,21 @@
|
||||||
section.issues-table.basic-table(ng-class="{empty: !issues.length}")
|
section.issues-table.basic-table(ng-class="{empty: !issues.length}")
|
||||||
div.row.title
|
div.row.title
|
||||||
div.level-field(data-fieldname="type", translate="ISSUES.TABLE.COLUMNS.TYPE")
|
div.level-field(data-fieldname="type")
|
||||||
div.level-field(data-fieldname="severity", translate="ISSUES.TABLE.COLUMNS.SEVERITY")
|
| {{"ISSUES.TABLE.COLUMNS.TYPE" | translate}}
|
||||||
div.level-field(data-fieldname="priority", translate="ISSUES.TABLE.COLUMNS.PRIORITY")
|
div.level-field(data-fieldname="severity")
|
||||||
div.votes(data-fieldname="total_voters", translate="ISSUES.TABLE.COLUMNS.VOTES")
|
| {{"ISSUES.TABLE.COLUMNS.SEVERITY" | translate}}
|
||||||
div.subject(data-fieldname="subject", translate="ISSUES.TABLE.COLUMNS.SUBJECT")
|
div.level-field(data-fieldname="priority")
|
||||||
div.issue-field(data-fieldname="status", translate="ISSUES.TABLE.COLUMNS.STATUS")
|
| {{"ISSUES.TABLE.COLUMNS.PRIORITY" | translate}}
|
||||||
div.created-field(data-fieldname="created_date", translate="ISSUES.TABLE.COLUMNS.CREATED")
|
div.votes(data-fieldname="total_voters")
|
||||||
div.assigned-field(data-fieldname="assigned_to", translate="ISSUES.TABLE.COLUMNS.ASSIGNED_TO")
|
| {{"ISSUES.TABLE.COLUMNS.VOTES" | translate}}
|
||||||
|
div.subject(data-fieldname="subject")
|
||||||
|
| {{"ISSUES.TABLE.COLUMNS.SUBJECT" | translate}}
|
||||||
|
div.issue-field(data-fieldname="status")
|
||||||
|
| {{"ISSUES.TABLE.COLUMNS.STATUS" | translate}}
|
||||||
|
div.created-field(data-fieldname="created_date")
|
||||||
|
| {{"ISSUES.TABLE.COLUMNS.CREATED" | translate}}
|
||||||
|
div.assigned-field(data-fieldname="assigned_to")
|
||||||
|
| {{"ISSUES.TABLE.COLUMNS.ASSIGNED_TO" | translate}}
|
||||||
|
|
||||||
div.row.table-main(
|
div.row.table-main(
|
||||||
ng-repeat="issue in issues track by issue.id"
|
ng-repeat="issue in issues track by issue.id"
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
div.kanban-table(tg-kanban-squish-column, tg-kanban-sortable)
|
div.kanban-table(
|
||||||
|
tg-kanban-squish-column,
|
||||||
|
tg-kanban-sortable,
|
||||||
|
ng-class="{'zoom-0': ctrl.zoomLevel == 0}"
|
||||||
|
)
|
||||||
div.kanban-table-header
|
div.kanban-table-header
|
||||||
div.kanban-table-inner
|
div.kanban-table-inner
|
||||||
h2.task-colum-name(ng-repeat="s in usStatusList track by s.id",
|
h2.task-colum-name(ng-repeat="s in usStatusList track by s.id",
|
||||||
ng-style="{'border-top-color':s.color}", tg-bo-title="s.name",
|
ng-style="{'border-top-color':s.color}",
|
||||||
|
tg-bo-title="s.name",
|
||||||
ng-class='{vfold:folds[s.id]}',
|
ng-class='{vfold:folds[s.id]}',
|
||||||
tg-class-permission="{'readonly': '!modify_task'}")
|
tg-class-permission="{'readonly': '!modify_task'}")
|
||||||
span(tg-bo-bind="s.name")
|
span(tg-bo-bind="s.name")
|
||||||
|
@ -21,21 +26,6 @@ div.kanban-table(tg-kanban-squish-column, tg-kanban-sortable)
|
||||||
ng-class='{hidden:!folds[s.id]}'
|
ng-class='{hidden:!folds[s.id]}'
|
||||||
)
|
)
|
||||||
tg-svg(svg-icon="icon-unfold-column")
|
tg-svg(svg-icon="icon-unfold-column")
|
||||||
a.option(
|
|
||||||
href=""
|
|
||||||
title="{{'KANBAN.TITLE_ACTION_FOLD_CARDS' | translate}}"
|
|
||||||
ng-class="{hidden:statusViewModes[s.id] == 'minimized'}"
|
|
||||||
ng-click="ctrl.updateStatusViewMode(s.id, 'minimized')"
|
|
||||||
)
|
|
||||||
tg-svg.fold-action(svg-icon="icon-fold-row")
|
|
||||||
a.option(
|
|
||||||
href=""
|
|
||||||
title="{{'KANBAN.TITLE_ACTION_UNFOLD_CARDS' | translate}}"
|
|
||||||
ng-class="{hidden:statusViewModes[s.id] == 'maximized'}"
|
|
||||||
ng-click="ctrl.updateStatusViewMode(s.id, 'maximized')"
|
|
||||||
)
|
|
||||||
tg-svg.fold-action(svg-icon="icon-unfold-row")
|
|
||||||
|
|
||||||
a.option(
|
a.option(
|
||||||
href=""
|
href=""
|
||||||
title="{{'KANBAN.TITLE_ACTION_ADD_US' | translate}}"
|
title="{{'KANBAN.TITLE_ACTION_ADD_US' | translate}}"
|
||||||
|
@ -65,18 +55,29 @@ div.kanban-table(tg-kanban-squish-column, tg-kanban-sortable)
|
||||||
div.kanban-table-body
|
div.kanban-table-body
|
||||||
div.kanban-table-inner
|
div.kanban-table-inner
|
||||||
div.kanban-uses-box.task-column(ng-class='{vfold:folds[s.id]}',
|
div.kanban-uses-box.task-column(ng-class='{vfold:folds[s.id]}',
|
||||||
ng-repeat="s in usStatusList track by s.id",
|
ng-repeat="s in ::usStatusList track by s.id",
|
||||||
tg-kanban-wip-limit="s",
|
tg-kanban-wip-limit="s",
|
||||||
tg-kanban-column-height-fixer,
|
tg-kanban-column-height-fixer,
|
||||||
tg-bind-scope
|
tg-bind-scope
|
||||||
)
|
)
|
||||||
div.kanban-task(
|
.card-placeholder(
|
||||||
ng-repeat="us in usByStatus[s.id] track by us.id",
|
ng-if="ctrl.showPlaceHolder(s.id)"
|
||||||
tg-kanban-userstory,
|
ng-include="'common/components/kanban-placeholder.html'"
|
||||||
ng-model="us",
|
|
||||||
tg-bind-scope,
|
|
||||||
tg-class-permission="{'readonly': '!modify_task'}"
|
|
||||||
ng-class="{'kanban-task-maximized': ctrl.isMaximized(s.id), 'kanban-task-minimized': ctrl.isMinimized(s.id), 'card-placeholder': us.isPlaceholder}"
|
|
||||||
placeholder="{{us.isPlaceholder}}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tg-card.card.ng-animate-disabled(
|
||||||
|
tg-repeat="us in usByStatus.get(s.id.toString()) track by us.getIn(['model', 'id'])",
|
||||||
|
ng-class="{'kanban-task-maximized': ctrl.isMaximized(s.id), 'kanban-task-minimized': ctrl.isMinimized(s.id)}"
|
||||||
|
tg-class-permission="{'readonly': '!modify_task'}"
|
||||||
|
tg-bind-scope,
|
||||||
|
on-toggle-fold="ctrl.toggleFold(id)"
|
||||||
|
on-click-edit="ctrl.editUs(id)"
|
||||||
|
on-click-assigned-to="ctrl.changeUsAssignedTo(id)"
|
||||||
|
project="project"
|
||||||
|
item="us"
|
||||||
|
zoom="ctrl.zoom"
|
||||||
|
zoom-level="ctrl.zoomLevel"
|
||||||
|
archived="ctrl.isUsInArchivedHiddenStatus(us.get('id'))"
|
||||||
|
)
|
||||||
|
|
||||||
div.kanban-column-intro(ng-if="s.is_archived", tg-kanban-archived-status-intro="s")
|
div.kanban-column-intro(ng-if="s.is_archived", tg-kanban-archived-status-intro="s")
|
||||||
|
|
|
@ -1,8 +1,18 @@
|
||||||
div.taskboard-table(tg-taskboard-squish-column, tg-taskboard-sortable)
|
div.taskboard-table(
|
||||||
|
tg-taskboard-squish-column,
|
||||||
|
tg-taskboard-sortable,
|
||||||
|
ng-class="{'zoom-0': ctrl.zoomLevel == 0}"
|
||||||
|
)
|
||||||
div.taskboard-table-header
|
div.taskboard-table-header
|
||||||
div.taskboard-table-inner
|
div.taskboard-table-inner
|
||||||
h2.task-colum-name(translate="TASKBOARD.TABLE.COLUMN")
|
h2.task-colum-name(translate="TASKBOARD.TABLE.COLUMN")
|
||||||
h2.task-colum-name(ng-repeat="s in taskStatusList track by s.id", ng-style="{'border-top-color':s.color}", ng-class="{'column-fold':statusesFolded[s.id]}", class="squish-status-{{s.id}}", tg-bo-title="s.name")
|
h2.task-colum-name(
|
||||||
|
ng-repeat="s in ::taskStatusList track by s.id"
|
||||||
|
ng-style="{'border-top-color':s.color}"
|
||||||
|
ng-class="{'column-fold':statusesFolded[s.id]}"
|
||||||
|
class="squish-status-{{s.id}}"
|
||||||
|
tg-bo-title="s.name"
|
||||||
|
)
|
||||||
span(tg-bo-bind="s.name")
|
span(tg-bo-bind="s.name")
|
||||||
|
|
||||||
tg-svg.hfold.fold-action(
|
tg-svg.hfold.fold-action(
|
||||||
|
@ -49,20 +59,31 @@ div.taskboard-table(tg-taskboard-squish-column, tg-taskboard-sortable)
|
||||||
span(translate="TASKBOARD.TABLE.FIELD_POINTS")
|
span(translate="TASKBOARD.TABLE.FIELD_POINTS")
|
||||||
include ../components/addnewtask
|
include ../components/addnewtask
|
||||||
|
|
||||||
div.taskboard-tasks-box.task-column(ng-repeat="st in taskStatusList track by st.id", class="squish-status-{{st.id}}", ng-class="{'column-fold':statusesFolded[st.id]}", tg-bind-scope)
|
|
||||||
div.taskboard-task(
|
div.taskboard-tasks-box.task-column(
|
||||||
ng-repeat="task in usTasks[us.id][st.id] track by task.id"
|
ng-repeat="st in ::taskStatusList track by st.id",
|
||||||
tg-bind-scope
|
class="squish-status-{{st.id}}",
|
||||||
tg-class-permission="{'readonly': '!modify_task'}"
|
ng-class="{'column-fold':statusesFolded[st.id]}",
|
||||||
ng-class="{'card-placeholder': task.isPlaceholder}"
|
tg-bind-scope
|
||||||
|
)
|
||||||
|
.card-placeholder(
|
||||||
|
ng-if="ctrl.showPlaceHolder(st.id, us.id)"
|
||||||
|
ng-include="'common/components/taskboard-placeholder.html'"
|
||||||
|
)
|
||||||
|
tg-card.card.ng-animate-disabled(
|
||||||
|
tg-repeat="task in usTasks.getIn([us.id.toString(), st.id.toString()]) track by task.get('id')"
|
||||||
|
ng-class="{'kanban-task-maximized': ctrl.isMaximized(s.id), 'kanban-task-minimized': ctrl.isMinimized(s.id)}"
|
||||||
|
tg-class-permission="{'readonly': '!modify_task'}"
|
||||||
|
tg-bind-scope,
|
||||||
|
on-toggle-fold="ctrl.toggleFold(id)"
|
||||||
|
on-click-edit="ctrl.editTask(id)"
|
||||||
|
on-click-assigned-to="ctrl.changeTaskAssignedTo(id)"
|
||||||
|
project="project"
|
||||||
|
item="task"
|
||||||
|
zoom="ctrl.zoom"
|
||||||
|
zoom-level="ctrl.zoomLevel"
|
||||||
|
type="task"
|
||||||
)
|
)
|
||||||
div(ng-if="!task.isPlaceholder", tg-taskboard-task)
|
|
||||||
include ../components/taskboard-task
|
|
||||||
|
|
||||||
div(ng-if="task.isPlaceholder")
|
|
||||||
- var card = 'task'
|
|
||||||
include ../../common/components/taskboard-placeholder
|
|
||||||
|
|
||||||
div.task-row(ng-init="us = null", ng-class="{'row-fold':usFolded[null]}")
|
div.task-row(ng-init="us = null", ng-class="{'row-fold':usFolded[null]}")
|
||||||
div.taskboard-userstory-box.task-column
|
div.taskboard-userstory-box.task-column
|
||||||
a.vfold(
|
a.vfold(
|
||||||
|
@ -82,15 +103,29 @@ div.taskboard-table(tg-taskboard-squish-column, tg-taskboard-sortable)
|
||||||
h3.us-title
|
h3.us-title
|
||||||
span(translate="TASKBOARD.TABLE.ROW_UNASSIGED_TASKS_TITLE")
|
span(translate="TASKBOARD.TABLE.ROW_UNASSIGED_TASKS_TITLE")
|
||||||
include ../components/addnewtask.jade
|
include ../components/addnewtask.jade
|
||||||
div.taskboard-tasks-box.task-column(ng-repeat="st in taskStatusList track by st.id", class="squish-status-{{st.id}}", ng-class="{'column-fold':statusesFolded[st.id]}", tg-bind-scope)
|
|
||||||
div.taskboard-task(
|
|
||||||
ng-repeat="task in usTasks[null][st.id] track by task.id"
|
|
||||||
tg-bind-scope
|
|
||||||
tg-class-permission="{'readonly': '!modify_task'}"
|
|
||||||
ng-class="{'card-placeholder': task.isPlaceholder}"
|
|
||||||
)
|
|
||||||
div(ng-if="!task.isPlaceholder", tg-taskboard-task)
|
|
||||||
include ../components/taskboard-task
|
|
||||||
|
|
||||||
div(ng-if="task.isPlaceholder")
|
div.taskboard-tasks-box.task-column(
|
||||||
include ../../common/components/taskboard-placeholder
|
ng-repeat="st in ::taskStatusList track by st.id",
|
||||||
|
class="squish-status-{{st.id}}",
|
||||||
|
ng-class="{'column-fold':statusesFolded[st.id]}",
|
||||||
|
tg-bind-scope
|
||||||
|
)
|
||||||
|
.card-placeholder(
|
||||||
|
ng-if="ctrl.showPlaceHolder(st.id, us.id)"
|
||||||
|
ng-include="'common/components/taskboard-placeholder.html'"
|
||||||
|
)
|
||||||
|
|
||||||
|
tg-card.card.ng-animate-disabled(
|
||||||
|
tg-bind-scope,
|
||||||
|
tg-repeat="task in usTasks.getIn(['null', st.id.toString()]) track by task.get('id')"
|
||||||
|
ng-class="{'kanban-task-maximized': ctrl.isMaximized(s.id), 'kanban-task-minimized': ctrl.isMinimized(s.id)}"
|
||||||
|
tg-class-permission="{'readonly': '!modify_task'}"
|
||||||
|
on-toggle-fold="ctrl.toggleFold(id)"
|
||||||
|
on-click-edit="ctrl.editTask(id)"
|
||||||
|
on-click-assigned-to="ctrl.changeTaskAssignedTo(id)"
|
||||||
|
project="project"
|
||||||
|
item="task"
|
||||||
|
zoom="ctrl.zoom"
|
||||||
|
zoom-level="ctrl.zoomLevel"
|
||||||
|
type="task"
|
||||||
|
)
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
<% _.each(filters, function(f) { %>
|
|
||||||
.single-filter.selected(
|
|
||||||
data-type!="<%- f.type %>"
|
|
||||||
data-id!="<%- f.id %>"
|
|
||||||
)
|
|
||||||
span.name(style!="<%- f.style %>") <%- f.name %>
|
|
||||||
a.remove-filter(href="")
|
|
||||||
tg-svg(svg-icon="icon-close")
|
|
||||||
<% }) %>
|
|
|
@ -1,21 +0,0 @@
|
||||||
<% _.each(filters, function(f) { %>
|
|
||||||
<% if (!f.selected) { %>
|
|
||||||
.single-filter(
|
|
||||||
data-type!="<%- f.type %>"
|
|
||||||
data-id!="<%- f.id %>"
|
|
||||||
)
|
|
||||||
span.name(style!="<%- f.style %>") <%- f.name %>
|
|
||||||
<% if (f.count){ %>
|
|
||||||
span.number <%- f.count %>
|
|
||||||
<% } %>
|
|
||||||
<% if (f.type == "myFilters"){ %>
|
|
||||||
a.remove-filter(href="")
|
|
||||||
tg-svg(svg-icon="icon-trash")
|
|
||||||
<% } %>
|
|
||||||
<% } %>
|
|
||||||
<% }) %>
|
|
||||||
span(class="new")
|
|
||||||
input.hidden.my-filter-name(
|
|
||||||
type="text"
|
|
||||||
placeholder="{{'ISSUES.PLACEHOLDER_FILTER_NAME' | translate}}"
|
|
||||||
)
|
|
|
@ -6,8 +6,20 @@ div.wrapper.issues.lightbox-generic-form(
|
||||||
ng-init="section='issues'"
|
ng-init="section='issues'"
|
||||||
)
|
)
|
||||||
tg-project-menu
|
tg-project-menu
|
||||||
sidebar.menu-secondary.extrabar.filters-bar(tg-issues-filters)
|
sidebar.filters-bar
|
||||||
include ../includes/modules/issues-filters
|
tg-filter(
|
||||||
|
q="ctrl.filterQ"
|
||||||
|
filters="ctrl.filters"
|
||||||
|
custom-filters="ctrl.customFilters"
|
||||||
|
selected-filters="ctrl.selectedFilters"
|
||||||
|
customFilters="ctl.customFilters"
|
||||||
|
on-save-custom-filter="ctrl.saveCustomFilter(name)"
|
||||||
|
on-add-filter="ctrl.addFilter(filter)"
|
||||||
|
on-select-custom-filter="ctrl.selectCustomFilter(filter)"
|
||||||
|
on-remove-custom-filter="ctrl.removeCustomFilter(filter)"
|
||||||
|
on-remove-filter="ctrl.removeFilter(filter)"
|
||||||
|
on-change-q="ctrl.changeQ(q)"
|
||||||
|
)
|
||||||
|
|
||||||
section.main.issues-page
|
section.main.issues-page
|
||||||
header
|
header
|
||||||
|
|
|
@ -1,33 +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="{{'US.ASSIGN' | translate}}")
|
|
||||||
span.task-num(tg-bo-ref="us.ref")
|
|
||||||
a.task-name(href="", title="#{{ ::us.ref }} {{ us.subject }}", ng-bind="us.subject",
|
|
||||||
tg-nav="project-userstories-detail:project=project.slug,ref=us.ref",
|
|
||||||
tg-nav-get-params="{\"kanban-status\": {{us.status}}}")
|
|
||||||
|
|
||||||
p.task-points(href="", title="{{'US.TOTAL_US_POINTS' | translate}}")
|
|
||||||
span(ng-if="us.total_points !== null", ng-bind="us.total_points")
|
|
||||||
span.points-text(ng-if="us.total_points !== null", translate="COMMON.FIELDS.POINTS")
|
|
||||||
span(ng-if="us.total_points === null", translate="US.NOT_ESTIMATED")
|
|
||||||
|
|
||||||
div.task-archived-text(ng-show="us.isArchived")
|
|
||||||
p(translate="KANBAN.ARCHIVED")
|
|
||||||
p
|
|
||||||
span.task-num(tg-bo-ref="us.ref")
|
|
||||||
span.task-name(ng-bind="us.subject")
|
|
||||||
p(translate="KANBAN.UNDO_ARCHIVED")
|
|
||||||
|
|
||||||
a.edit-us(
|
|
||||||
href="",
|
|
||||||
title="{{'COMMON.EDIT' | translate}}",
|
|
||||||
tg-check-permission="modify_us",
|
|
||||||
ng-hide="us.isArchived"
|
|
||||||
)
|
|
||||||
tg-svg(svg-icon="icon-edit")
|
|
|
@ -5,7 +5,35 @@ div.wrapper(tg-kanban, ng-controller="KanbanController as ctrl"
|
||||||
tg-project-menu
|
tg-project-menu
|
||||||
|
|
||||||
section.main.kanban
|
section.main.kanban
|
||||||
include ../includes/components/mainTitle
|
tg-filter(
|
||||||
|
ng-show="ctrl.openFilter"
|
||||||
|
q="ctrl.filterQ"
|
||||||
|
filters="ctrl.filters"
|
||||||
|
custom-filters="ctrl.customFilters"
|
||||||
|
selected-filters="ctrl.selectedFilters"
|
||||||
|
customFilters="ctl.customFilters"
|
||||||
|
on-save-custom-filter="ctrl.saveCustomFilter(name)"
|
||||||
|
on-add-filter="ctrl.addFilter(filter)"
|
||||||
|
on-select-custom-filter="ctrl.selectCustomFilter(filter)"
|
||||||
|
on-remove-custom-filter="ctrl.removeCustomFilter(filter)"
|
||||||
|
on-remove-filter="ctrl.removeFilter(filter)"
|
||||||
|
on-change-q="ctrl.changeQ(q)"
|
||||||
|
)
|
||||||
|
|
||||||
|
.kanban-header
|
||||||
|
include ../includes/components/mainTitle
|
||||||
|
.taskboard-actions
|
||||||
|
tg-kanban-board-zoom(
|
||||||
|
ng-if="usByStatus.size",
|
||||||
|
on-zoom-change="ctrl.setZoom(zoomLevel, zoom)"
|
||||||
|
)
|
||||||
|
|
||||||
|
button.button-filter.e2e-open-filter(
|
||||||
|
ng-class="{'button-filters-applied': !!ctrl.selectedFilters.length}"
|
||||||
|
ng-click="ctrl.openFilter = !ctrl.openFilter"
|
||||||
|
)
|
||||||
|
tg-svg(svg-icon="icon-filters")
|
||||||
|
|
||||||
include ../includes/modules/kanban-table
|
include ../includes/modules/kanban-table
|
||||||
|
|
||||||
div.lightbox.lightbox-generic-form.lb-create-edit-userstory(tg-lb-create-edit-userstory)
|
div.lightbox.lightbox-generic-form.lb-create-edit-userstory(tg-lb-create-edit-userstory)
|
||||||
|
|
|
@ -4,11 +4,38 @@ div.wrapper(tg-taskboard, ng-controller="TaskboardController as ctrl",
|
||||||
ng-init="section='backlog'")
|
ng-init="section='backlog'")
|
||||||
tg-project-menu
|
tg-project-menu
|
||||||
section.main.taskboard
|
section.main.taskboard
|
||||||
.taskboard-inner
|
tg-filter(
|
||||||
|
ng-show="ctrl.openFilter"
|
||||||
|
q="ctrl.filterQ"
|
||||||
|
filters="ctrl.filters"
|
||||||
|
custom-filters="ctrl.customFilters"
|
||||||
|
selected-filters="ctrl.selectedFilters"
|
||||||
|
customFilters="ctl.customFilters"
|
||||||
|
on-save-custom-filter="ctrl.saveCustomFilter(name)"
|
||||||
|
on-add-filter="ctrl.addFilter(filter)"
|
||||||
|
on-select-custom-filter="ctrl.selectCustomFilter(filter)"
|
||||||
|
on-remove-custom-filter="ctrl.removeCustomFilter(filter)"
|
||||||
|
on-remove-filter="ctrl.removeFilter(filter)"
|
||||||
|
on-change-q="ctrl.changeQ(q)"
|
||||||
|
)
|
||||||
|
.taskboard-header
|
||||||
h1
|
h1
|
||||||
span(tg-bo-bind="project.name", class="project-name-short")
|
span(tg-bo-bind="project.name", class="project-name-short")
|
||||||
span.green(tg-bo-bind="sprint.name")
|
span.green(tg-bo-bind="sprint.name")
|
||||||
span.date(tg-date-range="sprint.estimated_start,sprint.estimated_finish")
|
span.date(tg-date-range="sprint.estimated_start,sprint.estimated_finish")
|
||||||
|
.taskboard-actions
|
||||||
|
tg-taskboard-zoom(
|
||||||
|
ng-if="usTasks.size",
|
||||||
|
on-zoom-change="ctrl.setZoom(zoomLevel, zoom)"
|
||||||
|
)
|
||||||
|
button.button-filter.e2e-open-filter(
|
||||||
|
ng-class="{'button-filters-applied': !!ctrl.selectedFilters.length}"
|
||||||
|
ng-click="ctrl.openFilter = !ctrl.openFilter"
|
||||||
|
)
|
||||||
|
tg-svg(svg-icon="icon-filters")
|
||||||
|
|
||||||
|
.taskboard-inner
|
||||||
|
|
||||||
include ../includes/components/sprint-summary
|
include ../includes/components/sprint-summary
|
||||||
|
|
||||||
div.graphics-container
|
div.graphics-container
|
||||||
|
|
|
@ -155,3 +155,14 @@ a.button-gray {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: .5rem;
|
margin-top: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-filter {
|
||||||
|
@extend %button;
|
||||||
|
background: $whitish;
|
||||||
|
margin-left: 1rem;
|
||||||
|
padding: .4rem .5rem;
|
||||||
|
&:hover {
|
||||||
|
background: $gray-light;
|
||||||
|
fill: $whitish;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
.single-filter {
|
|
||||||
@include font-type(text);
|
|
||||||
@include clearfix;
|
|
||||||
align-items: center;
|
|
||||||
background: darken($whitish, 10%); // Fallback
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: .5rem;
|
|
||||||
opacity: .5;
|
|
||||||
padding-right: .5rem;
|
|
||||||
position: relative;
|
|
||||||
&:hover {
|
|
||||||
color: $grayer;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity .2s linear;
|
|
||||||
}
|
|
||||||
&.selected,
|
|
||||||
&.active {
|
|
||||||
color: $grayer;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity .2s linear;
|
|
||||||
}
|
|
||||||
.name,
|
|
||||||
.number {
|
|
||||||
padding: 8px 10px;
|
|
||||||
}
|
|
||||||
.name {
|
|
||||||
@include ellipsis(100%);
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.number {
|
|
||||||
background: darken($whitish, 20%); // Fallback
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
.remove-filter {
|
|
||||||
display: block;
|
|
||||||
svg {
|
|
||||||
fill: $gray;
|
|
||||||
transition: fill .2s linear;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
svg {
|
|
||||||
fill: $red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,224 +0,0 @@
|
||||||
.kanban-task {
|
|
||||||
background: $card;
|
|
||||||
border: 1px solid $card-hover;
|
|
||||||
box-shadow: none;
|
|
||||||
cursor: move;
|
|
||||||
margin: .2rem;
|
|
||||||
position: relative;
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
.edit-us {
|
|
||||||
display: block;
|
|
||||||
fill: $card-dark;
|
|
||||||
opacity: 1;
|
|
||||||
transition: color .3s linear, opacity .3s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.gu-mirror {
|
|
||||||
box-shadow: 1px 1px 15px rgba($black, .4);
|
|
||||||
opacity: 1;
|
|
||||||
transition: box-shadow .3s linear;
|
|
||||||
}
|
|
||||||
&.blocked {
|
|
||||||
background: $red;
|
|
||||||
border: 1px solid darken($red, 10%);
|
|
||||||
color: $white;
|
|
||||||
a,
|
|
||||||
span {
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.card-placeholder {
|
|
||||||
background: darken($whitish, 2%);
|
|
||||||
border: 3px dashed darken($whitish, 8%);
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
.kanban-tagline {
|
|
||||||
border-color: $card-hover;
|
|
||||||
display: flex;
|
|
||||||
height: .6rem;
|
|
||||||
}
|
|
||||||
.kanban-tag {
|
|
||||||
border-top: .3rem solid $card-hover;
|
|
||||||
flex-basis: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
height: .6rem;
|
|
||||||
z-index: 90;
|
|
||||||
}
|
|
||||||
.kanban-task-inner {
|
|
||||||
display: flex;
|
|
||||||
padding: .5rem;
|
|
||||||
}
|
|
||||||
.avatar-wrapper {
|
|
||||||
flex-basis: 55px;
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 55px;
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.avatar {
|
|
||||||
a {
|
|
||||||
@include font-size(small);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
margin: 0 auto;
|
|
||||||
&:hover {
|
|
||||||
border: 2px solid $primary;
|
|
||||||
transition: border .3s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.task-text {
|
|
||||||
@include font-size(small);
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: 0 .5rem 0 .8rem;
|
|
||||||
}
|
|
||||||
.task-assigned {
|
|
||||||
color: $card-dark;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.task-num {
|
|
||||||
color: $grayer;
|
|
||||||
margin-right: .3rem;
|
|
||||||
}
|
|
||||||
.task-name {
|
|
||||||
@include font-type(bold);
|
|
||||||
}
|
|
||||||
.loading {
|
|
||||||
bottom: .5rem;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
.edit-us {
|
|
||||||
display: block;
|
|
||||||
opacity: 0;
|
|
||||||
position: absolute;
|
|
||||||
svg {
|
|
||||||
@include svg-size(1.1rem);
|
|
||||||
fill: $card-hover;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
svg {
|
|
||||||
fill: darken($card-hover, 15%);
|
|
||||||
transition: color .3s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.kanban-task-maximized {
|
|
||||||
.task-archived {
|
|
||||||
background: darken($whitish, 5%);
|
|
||||||
padding: .5rem;
|
|
||||||
text-align: left;
|
|
||||||
transition: background .3s linear;
|
|
||||||
&:hover {
|
|
||||||
background: darken($whitish, 8%);
|
|
||||||
transition: background .3s linear;
|
|
||||||
}
|
|
||||||
.task-archived-text {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
color: $gray-light;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
@include font-size(small);
|
|
||||||
color: $gray-light;
|
|
||||||
margin: 0;
|
|
||||||
&:last-child {
|
|
||||||
color: $gray;
|
|
||||||
margin: .5rem 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.task-name {
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
.loading,
|
|
||||||
.edit-us {
|
|
||||||
bottom: .2rem;
|
|
||||||
right: .5rem;
|
|
||||||
}
|
|
||||||
.task-points {
|
|
||||||
@include font-size(small);
|
|
||||||
color: darken($card-hover, 15%);
|
|
||||||
margin: 0;
|
|
||||||
span {
|
|
||||||
display: inline-block;
|
|
||||||
&:first-child {
|
|
||||||
padding-right: .2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.points-text {
|
|
||||||
text-transform: lowercase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.kanban-tag {
|
|
||||||
border-top: .3rem solid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.kanban-task-minimized {
|
|
||||||
.kanban-task-inner {
|
|
||||||
padding: 0 .3rem;
|
|
||||||
}
|
|
||||||
.task-archived {
|
|
||||||
@include font-size(small);
|
|
||||||
background: darken($whitish, 5%);
|
|
||||||
padding: .3rem;
|
|
||||||
text-align: left;
|
|
||||||
.task-archived-text {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
color: $gray-light;
|
|
||||||
}
|
|
||||||
.task-name {
|
|
||||||
display: inline-block;
|
|
||||||
max-width: 70%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
color: $gray-light;
|
|
||||||
margin: 0;
|
|
||||||
&:last-child {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.task-num {
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
.task-name {
|
|
||||||
display: inline-block;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
width: 135px;
|
|
||||||
}
|
|
||||||
.task-points {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.icon-edit {
|
|
||||||
bottom: .2rem;
|
|
||||||
right: 1rem;
|
|
||||||
top: 1.4rem;
|
|
||||||
}
|
|
||||||
.kanban-tag {
|
|
||||||
border-top: .2rem solid;
|
|
||||||
}
|
|
||||||
.edit-us {
|
|
||||||
bottom: .2rem;
|
|
||||||
right: .5rem;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,140 +0,0 @@
|
||||||
.taskboard-task {
|
|
||||||
background: $card;
|
|
||||||
border: 1px solid $card-hover;
|
|
||||||
box-shadow: none;
|
|
||||||
cursor: move;
|
|
||||||
margin: .2rem;
|
|
||||||
position: relative;
|
|
||||||
&:hover {
|
|
||||||
.icon-edit {
|
|
||||||
display: block;
|
|
||||||
fill: $card-dark;
|
|
||||||
opacity: 1;
|
|
||||||
transition: color .3s linear, opacity .3s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.gu-mirror {
|
|
||||||
box-shadow: 1px 1px 15px rgba($black, .4);
|
|
||||||
transition: box-shadow .3s linear;
|
|
||||||
}
|
|
||||||
.blocked {
|
|
||||||
background: $red;
|
|
||||||
border: 1px solid darken($red, 10%);
|
|
||||||
color: $white;
|
|
||||||
svg,
|
|
||||||
span {
|
|
||||||
color: $white;
|
|
||||||
fill: $white;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
.icon-edit {
|
|
||||||
fill: currentColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.card-placeholder {
|
|
||||||
background: darken($whitish, 2%);
|
|
||||||
border: 3px dashed darken($whitish, 8%);
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
.taskboard-tagline {
|
|
||||||
border-color: $card-hover;
|
|
||||||
display: flex;
|
|
||||||
height: .6rem;
|
|
||||||
}
|
|
||||||
.taskboard-tag {
|
|
||||||
border-top: .3rem solid $card-hover;
|
|
||||||
flex-basis: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
height: .6rem;
|
|
||||||
z-index: 90;
|
|
||||||
}
|
|
||||||
.taskboard-task-inner {
|
|
||||||
display: flex;
|
|
||||||
padding: .5rem;
|
|
||||||
}
|
|
||||||
.taskboard-user-avatar {
|
|
||||||
flex-basis: 50px;
|
|
||||||
flex-grow: 1;
|
|
||||||
max-width: 55px;
|
|
||||||
a {
|
|
||||||
@include font-size(small);
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
margin: 0 auto;
|
|
||||||
&:hover {
|
|
||||||
border: 2px solid $primary;
|
|
||||||
transition: border .3s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.iocaine {
|
|
||||||
left: .2rem;
|
|
||||||
position: absolute;
|
|
||||||
top: 1rem;
|
|
||||||
img {
|
|
||||||
filter: hue-rotate(150deg) saturate(200%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.icon-iocaine {
|
|
||||||
background: $black;
|
|
||||||
border-radius: 5px;
|
|
||||||
fill: $white;
|
|
||||||
height: 1.75rem;
|
|
||||||
padding: .25rem;
|
|
||||||
width: 1.75rem;
|
|
||||||
}
|
|
||||||
.task-assigned {
|
|
||||||
@include font-size(small);
|
|
||||||
color: $card-dark;
|
|
||||||
display: block;
|
|
||||||
&:hover {
|
|
||||||
color: $primary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.task-num {
|
|
||||||
color: $grayer;
|
|
||||||
margin-right: .5em;
|
|
||||||
}
|
|
||||||
.task-name {
|
|
||||||
@include font-type(bold);
|
|
||||||
}
|
|
||||||
.taskboard-text {
|
|
||||||
@include font-size(small);
|
|
||||||
flex-basis: 50px;
|
|
||||||
flex-grow: 10;
|
|
||||||
padding: 0 .5rem 0 1rem;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
.icon {
|
|
||||||
transition: color .3s linear, opacity .3s linear;
|
|
||||||
}
|
|
||||||
.loading {
|
|
||||||
bottom: .5rem;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
.edit-task {
|
|
||||||
bottom: .5rem;
|
|
||||||
position: absolute;
|
|
||||||
top: auto;
|
|
||||||
}
|
|
||||||
.icon-edit {
|
|
||||||
@include svg-size(1.1rem);
|
|
||||||
cursor: pointer;
|
|
||||||
fill: $card-hover;
|
|
||||||
opacity: 0;
|
|
||||||
&:hover {
|
|
||||||
fill: $card-dark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.icon-edit,
|
|
||||||
.loading {
|
|
||||||
right: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-drag {
|
|
||||||
@include box-shadow();
|
|
||||||
}
|
|
|
@ -56,21 +56,6 @@ body {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
&.filters-bar {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
padding: 0;
|
|
||||||
transition: all .2s linear;
|
|
||||||
width: 0;
|
|
||||||
&.active {
|
|
||||||
padding: 2em 1em;
|
|
||||||
transition: all .2s linear;
|
|
||||||
width: 260px;
|
|
||||||
.filters-inner {
|
|
||||||
opacity: 1;
|
|
||||||
transition: all .4s ease-in;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.search-in {
|
.search-in {
|
||||||
margin-top: .5rem;
|
margin-top: .5rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
$navbar: 40px;
|
$navbar: 40px;
|
||||||
$main-height: calc(100vh - 40px);
|
$main-height: calc(100vh - #{$navbar});
|
||||||
|
|
|
@ -1,3 +1,24 @@
|
||||||
|
.backlog-filter {
|
||||||
|
align-items: stretch;
|
||||||
|
display: flex;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
transition: all .2s linear;
|
||||||
|
width: 0;
|
||||||
|
tg-filter {
|
||||||
|
transform: translateX(-260px);
|
||||||
|
transition: all .2s linear;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
opacity: 1;
|
||||||
|
transition: all .2s linear;
|
||||||
|
width: 260px;
|
||||||
|
tg-filter {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.backlog-menu {
|
.backlog-menu {
|
||||||
background: $mass-white;
|
background: $mass-white;
|
||||||
color: $blackish;
|
color: $blackish;
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
.issues {
|
.issues {
|
||||||
.filters-bar {
|
.filters-bar {
|
||||||
flex: 0 0 auto;
|
position: relative;
|
||||||
width: 260px;
|
width: 260px;
|
||||||
}
|
}
|
||||||
.filters-inner {
|
|
||||||
opacity: 1;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
height: $main-height;
|
height: $main-height;
|
||||||
max-height: $main-height;
|
max-height: $main-height;
|
||||||
max-width: calc(100vw - 50px);
|
max-width: calc(100vw - 50px);
|
||||||
|
position: relative;
|
||||||
header {
|
header {
|
||||||
min-height: 70px;
|
min-height: 70px;
|
||||||
}
|
}
|
||||||
|
@ -14,3 +15,12 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kanban-header {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
.options {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
.taskboard {
|
.taskboard {
|
||||||
height: $main-height;
|
height: $main-height;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
h1,
|
h1,
|
||||||
.graphics-container,
|
.graphics-container,
|
||||||
.summary {
|
.summary {
|
||||||
|
@ -11,6 +12,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.taskboard-header {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.taskboard-inner {
|
.taskboard-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -3,27 +3,29 @@
|
||||||
$column-width: 300px;
|
$column-width: 300px;
|
||||||
$column-flex: 1;
|
$column-flex: 1;
|
||||||
$column-shrink: 0;
|
$column-shrink: 0;
|
||||||
$column-margin: 0 10px 0 0;
|
$column-margin: 0 5px 0 0;
|
||||||
|
$column-padding: .5rem 1rem;
|
||||||
|
|
||||||
@mixin fold {
|
@mixin fold {
|
||||||
.taskboard-task {
|
.card {
|
||||||
background: none;
|
align-self: flex-start;
|
||||||
border: 0;
|
margin-top: .5rem;
|
||||||
margin: 0;
|
tg-card-slideshow,
|
||||||
min-height: 0;
|
.card-unfold,
|
||||||
.taskboard-task-inner {
|
.card-tag,
|
||||||
padding: .1rem;
|
.card-title,
|
||||||
}
|
.card-owner-actions,
|
||||||
.taskboard-tagline,
|
.card-data,
|
||||||
.taskboard-text {
|
.card-statistics,
|
||||||
|
.card-owner-name {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.avatar {
|
.card-owner {
|
||||||
height: 35px;
|
img {
|
||||||
width: 35px;
|
height: 1.3rem;
|
||||||
}
|
margin-right: 0;
|
||||||
.icon {
|
width: 1.3rem;
|
||||||
display: none;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.task-column,
|
&.task-column,
|
||||||
|
@ -44,25 +46,20 @@ $column-margin: 0 10px 0 0;
|
||||||
.taskboard-table {
|
.taskboard-table {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
.taskboard-task {
|
&.zoom-0 {
|
||||||
&.readonly {
|
.task-colum-name span {
|
||||||
cursor: auto;
|
padding-right: 1rem;
|
||||||
}
|
|
||||||
&.gu-mirror {
|
|
||||||
opacity: 1;
|
|
||||||
.avatar-task-link {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.taskboard-table-header {
|
.taskboard-table-header {
|
||||||
margin-bottom: .5rem;
|
flex-basis: 38px;
|
||||||
min-height: 40px;
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-height: 38px;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
.taskboard-table-inner {
|
.taskboard-table-inner {
|
||||||
|
@ -83,7 +80,7 @@ $column-margin: 0 10px 0 0;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: $column-margin;
|
margin: $column-margin;
|
||||||
max-width: $column-width;
|
max-width: $column-width;
|
||||||
padding: .5rem 1rem;
|
padding: $column-padding;
|
||||||
position: relative;
|
position: relative;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
width: $column-width;
|
width: $column-width;
|
||||||
|
@ -102,6 +99,9 @@ $column-margin: 0 10px 0 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
span {
|
||||||
|
@include ellipsis(65%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tg-svg {
|
tg-svg {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -128,7 +128,8 @@ $column-margin: 0 10px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.taskboard-table-body {
|
.taskboard-table-body {
|
||||||
height: 100%;
|
flex: 1;
|
||||||
|
margin-bottom: 5rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
.task-column {
|
.task-column {
|
||||||
|
@ -147,14 +148,10 @@ $column-margin: 0 10px 0 0;
|
||||||
}
|
}
|
||||||
.column-fold {
|
.column-fold {
|
||||||
@include fold;
|
@include fold;
|
||||||
.taskboard-task {
|
|
||||||
max-width: 40px;
|
|
||||||
width: 40px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.task-row {
|
.task-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: .5rem;
|
margin-bottom: .25rem;
|
||||||
min-height: 10rem;
|
min-height: 10rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
&.blocked {
|
&.blocked {
|
||||||
|
@ -167,6 +164,7 @@ $column-margin: 0 10px 0 0;
|
||||||
.points-value,
|
.points-value,
|
||||||
.points-value:hover {
|
.points-value:hover {
|
||||||
color: $white;
|
color: $white;
|
||||||
|
fill: $white;
|
||||||
transition: color .3s linear;
|
transition: color .3s linear;
|
||||||
}
|
}
|
||||||
.taskboard-tasks-box {
|
.taskboard-tasks-box {
|
||||||
|
@ -185,18 +183,26 @@ $column-margin: 0 10px 0 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.taskboard-userstory-box {
|
.taskboard-userstory-box {
|
||||||
padding: .5rem .5rem .5rem 1.5rem;
|
padding: .5rem .5rem .5rem 1.5rem;
|
||||||
}
|
}
|
||||||
.avatar-task-link {
|
|
||||||
display: none;
|
}
|
||||||
|
|
||||||
|
.taskboard-userstory-box {
|
||||||
|
position: relative;
|
||||||
|
.us-title {
|
||||||
|
@include font-size(normal);
|
||||||
|
@include font-type(text);
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-right: 3rem;
|
||||||
}
|
}
|
||||||
.avatar-assigned-to {
|
.points-value {
|
||||||
display: block;
|
@include font-size(small);
|
||||||
}
|
color: $gray-light;
|
||||||
.icon {
|
span {
|
||||||
transition: fill .2s linear;
|
margin-right: .1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tg-svg {
|
tg-svg {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -219,20 +225,3 @@ $column-margin: 0 10px 0 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.taskboard-userstory-box {
|
|
||||||
position: relative;
|
|
||||||
.us-title {
|
|
||||||
@include font-size(normal);
|
|
||||||
@include font-type(text);
|
|
||||||
margin-bottom: 0;
|
|
||||||
margin-right: 3rem;
|
|
||||||
}
|
|
||||||
.points-value {
|
|
||||||
@include font-size(small);
|
|
||||||
color: $gray-light;
|
|
||||||
span {
|
|
||||||
margin-right: .1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
.filters {
|
|
||||||
h1 {
|
|
||||||
vertical-align: baseline;
|
|
||||||
.icon {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.breadcrumb {
|
|
||||||
@include font-size(large);
|
|
||||||
margin-top: 1rem;
|
|
||||||
.icon-arrow-right {
|
|
||||||
@include svg-size(.7rem);
|
|
||||||
margin: 0 .25rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.back {
|
|
||||||
color: $gray-light;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
background: $grayer;
|
|
||||||
color: $white;
|
|
||||||
@include placeholder {
|
|
||||||
color: $gray-light;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.search-action {
|
|
||||||
position: absolute;
|
|
||||||
right: .7rem;
|
|
||||||
top: .7rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters-inner {
|
|
||||||
opacity: 0;
|
|
||||||
transition: all .1s ease-in;
|
|
||||||
.loading {
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px;
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
.loading-spinner {
|
|
||||||
@include loading-spinner;
|
|
||||||
max-height: 1rem;
|
|
||||||
max-width: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters-applied {
|
|
||||||
margin-top: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters-step-cat {
|
|
||||||
.save-filters {
|
|
||||||
color: $white;
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.my-filter-name {
|
|
||||||
background: $grayer;
|
|
||||||
color: $whitish;
|
|
||||||
width: 100%;
|
|
||||||
@include placeholder {
|
|
||||||
color: $gray-light;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-list {
|
|
||||||
.single-filter {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters-cats {
|
|
||||||
margin-top: 2rem;
|
|
||||||
li {
|
|
||||||
border-bottom: 1px solid $gray-light;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
.custom-filters {
|
|
||||||
.title {
|
|
||||||
color: $primary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
align-items: center;
|
|
||||||
color: $grayer;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: .5rem 0 .5rem .5rem;
|
|
||||||
transition: color .2s ease-in;
|
|
||||||
&:hover {
|
|
||||||
color: $primary;
|
|
||||||
transition: color .2s ease-in;
|
|
||||||
.icon {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity .2s ease-in;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.icon {
|
|
||||||
fill: currentColor;
|
|
||||||
float: right;
|
|
||||||
height: .9rem;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity .2s ease-in;
|
|
||||||
width: .9rem;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,11 @@
|
||||||
//Table basic shared vars
|
//Table basic shared vars
|
||||||
|
|
||||||
$column-width: 300px;
|
$column-width: 296px;
|
||||||
$column-folded-width: 30px;
|
$column-folded-width: 30px;
|
||||||
$column-flex: 0;
|
$column-flex: 0;
|
||||||
$column-shrink: 0;
|
$column-shrink: 0;
|
||||||
$column-margin: 0 10px 0 0;
|
$column-margin: 0 5px 0 0;
|
||||||
|
$column-padding: .5rem 1rem;
|
||||||
|
|
||||||
.kanban-table {
|
.kanban-table {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -12,7 +13,19 @@ $column-margin: 0 10px 0 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
&.zoom-0 {
|
||||||
|
.task-column,
|
||||||
|
.task-colum-name {
|
||||||
|
max-width: $column-width / 2;
|
||||||
|
}
|
||||||
|
.task-colum-name span {
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
.vfold {
|
.vfold {
|
||||||
|
tg-card {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
&.task-colum-name {
|
&.task-colum-name {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -36,9 +49,6 @@ $column-margin: 0 10px 0 0;
|
||||||
min-width: $column-folded-width;
|
min-width: $column-folded-width;
|
||||||
width: $column-folded-width;
|
width: $column-folded-width;
|
||||||
}
|
}
|
||||||
.kanban-task {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.kanban-column-intro {
|
.kanban-column-intro {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -46,11 +56,11 @@ $column-margin: 0 10px 0 0;
|
||||||
.readonly {
|
.readonly {
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanban-table-header {
|
.kanban-table-header {
|
||||||
margin-bottom: .5rem;
|
min-height: 38px;
|
||||||
min-height: 40px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
.kanban-table-inner {
|
.kanban-table-inner {
|
||||||
|
@ -58,6 +68,9 @@ $column-margin: 0 10px 0 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
.options {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
.task-colum-name {
|
.task-colum-name {
|
||||||
@include font-size(medium);
|
@include font-size(medium);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -71,7 +84,7 @@ $column-margin: 0 10px 0 0;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: $column-margin;
|
margin: $column-margin;
|
||||||
max-width: $column-width;
|
max-width: $column-width;
|
||||||
padding: .5rem .5rem .5rem 1rem;
|
padding: $column-padding;
|
||||||
position: relative;
|
position: relative;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
&:last-child {
|
&:last-child {
|
||||||
|
@ -110,6 +123,7 @@ $column-margin: 0 10px 0 0;
|
||||||
max-width: $column-width;
|
max-width: $column-width;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
widows: $column-width;
|
widows: $column-width;
|
||||||
|
width: $column-width;
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,14 +28,8 @@ a[ng-click] svg {
|
||||||
}
|
}
|
||||||
|
|
||||||
// chrome url break
|
// chrome url break
|
||||||
.kanban-task {
|
tg-card {
|
||||||
.task-name {
|
.card-title span:last-child {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.taskboard-task {
|
|
||||||
.task-name {
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -429,5 +429,14 @@
|
||||||
fill="#fff"
|
fill="#fff"
|
||||||
d="M511.998 107.939c-222.856 0-404.061 181.204-404.061 404.061s181.205 404.061 404.061 404.061c222.856 0 404.061-181.203 404.061-404.061s-181.205-404.061-404.061-404.061zM511.998 158.447c88.671 0 169.621 32.484 231.616 86.222l-498.947 498.948c-53.74-61.998-86.223-142.945-86.223-231.617 0-195.561 157.992-353.553 353.553-353.553zM779.328 280.383c53.74 61.998 86.223 142.945 86.223 231.617 0 195.561-157.992 353.553-353.553 353.553-88.671 0-169.617-32.484-231.616-86.222l498.947-498.948z"></path>
|
d="M511.998 107.939c-222.856 0-404.061 181.204-404.061 404.061s181.205 404.061 404.061 404.061c222.856 0 404.061-181.203 404.061-404.061s-181.205-404.061-404.061-404.061zM511.998 158.447c88.671 0 169.621 32.484 231.616 86.222l-498.947 498.948c-53.74-61.998-86.223-142.945-86.223-231.617 0-195.561 157.992-353.553 353.553-353.553zM779.328 280.383c53.74 61.998 86.223 142.945 86.223 231.617 0 195.561-157.992 353.553-353.553 353.553-88.671 0-169.617-32.484-231.616-86.222l498.947-498.948z"></path>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
<symbol id="icon-add-user" viewBox="0 0 470 350">
|
||||||
|
<title>Add user</title>
|
||||||
|
<path
|
||||||
|
d="M298.5 174.7c47.5 0 86-37.7 86-85.2 0-46.6-39.3-85-87-85-46.5 0-85 38.4-85 85 0 47.5 38.5 85.2 85 85.2zm-191-42V68H63v64.6H0v42h62.8v64.6h44.8v-64.5h62.7v-42h-62.7zm191 85c-56.4 0-170.3 28.7-170.3 85.2v43H469v-43c0-56.8-113-85.5-170.5-85.5z"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="icon-view-more" viewBox="0 0 66.3 16">
|
||||||
|
<title>View more</title>
|
||||||
|
<path d="M16 8a8 8 0 0 1-8 8 8 8 0 0 1-8-8 8 8 0 0 1 8-8 8 8 0 0 1 8 8zM41.2 8a8 8 0 0 1-8 8 8 8 0 0 1-8-8 8 8 0 0 1 8-8 8 8 0 0 1 8 8zM66.3 8a8 8 0 0 1-8 8 8 8 0 0 1-8-8 8 8 0 0 1 8-8 8 8 0 0 1 8 8z"/>
|
||||||
|
</symbol>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 68 KiB |
|
@ -0,0 +1,81 @@
|
||||||
|
var utils = require('../utils');
|
||||||
|
|
||||||
|
var helper = module.exports;
|
||||||
|
|
||||||
|
helper.getFilter = function() {
|
||||||
|
return $('tg-filter');
|
||||||
|
};
|
||||||
|
|
||||||
|
helper.open = async function() {
|
||||||
|
let isPresent = await $('.e2e-open-filter').isPresent();
|
||||||
|
|
||||||
|
if(isPresent) {
|
||||||
|
$('.e2e-open-filter').click();
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var filter = helper.getFilter();
|
||||||
|
|
||||||
|
return utils.common.transitionend('.e2e-open-filter')
|
||||||
|
};
|
||||||
|
|
||||||
|
helper.byText = function(text) {
|
||||||
|
return $('.e2e-filter-q').sendKeys(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
helper.clearByTextInput = function() {
|
||||||
|
return utils.common.clear($('.e2e-filter-q'));
|
||||||
|
};
|
||||||
|
|
||||||
|
helper.clearFilters = async function() {
|
||||||
|
let filters = $$('.e2e-remove-filter');
|
||||||
|
let filtersSize = await filters.count()
|
||||||
|
|
||||||
|
for(var i = 0; i < filtersSize; i++) {
|
||||||
|
filters.get(i).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
await helper.clearByTextInput();
|
||||||
|
let isPresent = await $('.e2e-category.selected').isPresent();
|
||||||
|
|
||||||
|
if(isPresent) {
|
||||||
|
$('.e2e-category.selected').click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
helper.getFiltersCounters = function() {
|
||||||
|
return $$('.e2e-filter-count');
|
||||||
|
};
|
||||||
|
|
||||||
|
helper.getCustomFilters = function() {
|
||||||
|
return $$('.e2e-custom-filter');
|
||||||
|
};
|
||||||
|
|
||||||
|
helper.firterByLastCustomFilter = function() {
|
||||||
|
helper.openCustomFiltersCategory();
|
||||||
|
helper.getCustomFilters().last().click();
|
||||||
|
};
|
||||||
|
|
||||||
|
helper.openCustomFiltersCategory = function() {
|
||||||
|
$('.e2e-custom-filters').click();
|
||||||
|
};
|
||||||
|
|
||||||
|
helper.removeLastCustomFilter = function() {
|
||||||
|
$$('.e2e-remove-custom-filter').last().click();
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.firterByCategoryWithContent = function() {
|
||||||
|
$$('.e2e-category').first().click();
|
||||||
|
|
||||||
|
let filter = helper.getFiltersCounters().first().element(by.xpath('..'));
|
||||||
|
|
||||||
|
return filter.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
helper.saveFilter = async function(name) {
|
||||||
|
$('.e2e-open-custom-filter-form').click();
|
||||||
|
|
||||||
|
await $('.e2e-filter-name-input').sendKeys(name);
|
||||||
|
await $('.e2e-filter-name-input').sendKeys(protractor.Key.ENTER);
|
||||||
|
};
|
|
@ -90,57 +90,3 @@ helper.parseIssue = async function(elm) {
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
helper.getFilterInput = function() {
|
|
||||||
return $$('sidebar[tg-issues-filters] input').get(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
helper.filtersCats = function() {
|
|
||||||
return $$('.filters-cats li');
|
|
||||||
};
|
|
||||||
|
|
||||||
helper.filtersList = function() {
|
|
||||||
return $$('.filter-list .single-filter');
|
|
||||||
};
|
|
||||||
|
|
||||||
helper.selectFilter = async function(index) {
|
|
||||||
helper.filtersList().get(index).click();
|
|
||||||
};
|
|
||||||
|
|
||||||
helper.saveFilter = async function(name) {
|
|
||||||
$('.filters-step-cat .save-filters').click();
|
|
||||||
|
|
||||||
await $('.filter-list input').sendKeys(name);
|
|
||||||
|
|
||||||
return browser.actions().sendKeys(protractor.Key.ENTER).perform();
|
|
||||||
};
|
|
||||||
|
|
||||||
helper.backToFilters = function() {
|
|
||||||
$$('.breadcrumb a').get(0).click();
|
|
||||||
};
|
|
||||||
|
|
||||||
helper.removeFilters = async function() {
|
|
||||||
let count = await $$('.filters-applied .single-filter.selected').count();
|
|
||||||
|
|
||||||
while(count) {
|
|
||||||
$$('.single-filter.selected').get(0).$('.remove-filter').click();
|
|
||||||
|
|
||||||
count = await $$('.single-filter.selected').count();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
helper.getCustomFilters = function() {
|
|
||||||
return $$('.filter-list div[data-type="myFilters"]');
|
|
||||||
};
|
|
||||||
|
|
||||||
helper.removeCustomFilters = async function() {
|
|
||||||
let count = await $$('.filter-list .remove-filter').count();
|
|
||||||
|
|
||||||
while(count) {
|
|
||||||
$$('.filter-list .remove-filter').get(0).click();
|
|
||||||
|
|
||||||
await utils.lightbox.confirm.ok();
|
|
||||||
|
|
||||||
count = await $$('.filter-list .remove-filter').count();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -15,15 +15,31 @@ helper.getColumns = function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
helper.getColumnUssTitles = function(column) {
|
helper.getColumnUssTitles = function(column) {
|
||||||
return helper.getColumns().$$('.task-name').getText();
|
return helper.getColumns().$$('.e2e-title').getText();
|
||||||
};
|
};
|
||||||
|
|
||||||
helper.getBoxUss = function(column) {
|
helper.getBoxUss = function(column) {
|
||||||
return helper.getColumns().get(column).$$('.kanban-task');
|
return helper.getColumns().get(column).$$('tg-card');
|
||||||
};
|
};
|
||||||
|
|
||||||
helper.editUs = function(column, us) {
|
helper.getUss = function() {
|
||||||
helper.getColumns().get(column).$$('.edit-us').get(us).click();
|
return $$('tg-card')
|
||||||
|
};
|
||||||
|
|
||||||
|
helper.editUs = async function(column, us) {
|
||||||
|
let editionZone = helper.getColumns().get(column).$$('.card-owner-actions').get(us);
|
||||||
|
|
||||||
|
await browser
|
||||||
|
.actions()
|
||||||
|
.mouseMove(editionZone)
|
||||||
|
.perform();
|
||||||
|
|
||||||
|
return browser
|
||||||
|
.actions()
|
||||||
|
.mouseMove(editionZone)
|
||||||
|
.mouseMove(editionZone.$('.e2e-edit'))
|
||||||
|
.click()
|
||||||
|
.perform();
|
||||||
};
|
};
|
||||||
|
|
||||||
helper.openBulkUsLb = function(column) {
|
helper.openBulkUsLb = function(column) {
|
||||||
|
@ -59,5 +75,13 @@ helper.scrollRight = function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
helper.watchersLinks = function() {
|
helper.watchersLinks = function() {
|
||||||
return $$('.task-assigned');
|
return $$('.e2e-assign');
|
||||||
|
};
|
||||||
|
|
||||||
|
helper.zoom = async function(level) {
|
||||||
|
return browser
|
||||||
|
.actions()
|
||||||
|
.mouseMove($('tg-board-zoom'), {y: 14, x: level * 49})
|
||||||
|
.click()
|
||||||
|
.perform();
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,7 +13,11 @@ helper.getBox = function(row, column) {
|
||||||
helper.getBoxTasks = function(row, column) {
|
helper.getBoxTasks = function(row, column) {
|
||||||
let box = helper.getBox(row, column);
|
let box = helper.getBox(row, column);
|
||||||
|
|
||||||
return box.$$('.taskboard-task');
|
return box.$$('tg-card');
|
||||||
|
};
|
||||||
|
|
||||||
|
helper.getTasks = function() {
|
||||||
|
return $$('tg-card');
|
||||||
};
|
};
|
||||||
|
|
||||||
helper.openNewTaskLb = function(row) {
|
helper.openNewTaskLb = function(row) {
|
||||||
|
@ -52,8 +56,20 @@ helper.unFoldColumn = function(row) {
|
||||||
icon.click();
|
icon.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
helper.editTask = function(row, column, task) {
|
helper.editTask = async function(row, column, task) {
|
||||||
helper.getBoxTasks(row, column).get(task).$('.edit-task').click();
|
let editionZone = helper.getBoxTasks(row, column).$$('.card-owner-actions').get(task);
|
||||||
|
|
||||||
|
await browser
|
||||||
|
.actions()
|
||||||
|
.mouseMove(editionZone)
|
||||||
|
.perform();
|
||||||
|
|
||||||
|
return browser
|
||||||
|
.actions()
|
||||||
|
.mouseMove(editionZone)
|
||||||
|
.mouseMove(editionZone.$('.e2e-edit'))
|
||||||
|
.click()
|
||||||
|
.perform();
|
||||||
};
|
};
|
||||||
|
|
||||||
helper.toggleGraph = function() {
|
helper.toggleGraph = function() {
|
||||||
|
@ -114,5 +130,13 @@ helper.getBulkCreateTask = function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
helper.watchersLinks = function() {
|
helper.watchersLinks = function() {
|
||||||
return $$('.task-assigned');
|
return $$('.e2e-assign');
|
||||||
|
};
|
||||||
|
|
||||||
|
helper.zoom = async function(level) {
|
||||||
|
return browser
|
||||||
|
.actions()
|
||||||
|
.mouseMove($('tg-board-zoom'), {y: 10, x: level * 74})
|
||||||
|
.click()
|
||||||
|
.perform();
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
var filterHelper = require('../helpers/filters-helper');
|
||||||
|
var utils = require('../utils');
|
||||||
|
|
||||||
|
var chai = require('chai');
|
||||||
|
var chaiAsPromised = require('chai-as-promised');
|
||||||
|
|
||||||
|
chai.use(chaiAsPromised);
|
||||||
|
var expect = chai.expect;
|
||||||
|
|
||||||
|
module.exports = function(name, counter) {
|
||||||
|
before(async () => {
|
||||||
|
await filterHelper.open();
|
||||||
|
|
||||||
|
utils.common.takeScreenshot(name, 'filters');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filter by ref', async () => {
|
||||||
|
await filterHelper.byText('xxxxyy123123123');
|
||||||
|
|
||||||
|
let len = await counter();
|
||||||
|
len = await counter();
|
||||||
|
|
||||||
|
await filterHelper.clearFilters();
|
||||||
|
|
||||||
|
expect(len).to.be.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filter by category', async () => {
|
||||||
|
let len = await counter();
|
||||||
|
|
||||||
|
await filterHelper.firterByCategoryWithContent();
|
||||||
|
|
||||||
|
let newLength = await counter();
|
||||||
|
|
||||||
|
expect(len).to.be.above(newLength);
|
||||||
|
|
||||||
|
await filterHelper.clearFilters();
|
||||||
|
|
||||||
|
newLength = await counter();
|
||||||
|
|
||||||
|
expect(len).to.be.equal(newLength);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('save custom filters', async () => {
|
||||||
|
let len = await counter();
|
||||||
|
|
||||||
|
filterHelper.openCustomFiltersCategory();
|
||||||
|
|
||||||
|
let customFiltersSize = await filterHelper.getCustomFilters().count();
|
||||||
|
|
||||||
|
await filterHelper.firterByCategoryWithContent();
|
||||||
|
await filterHelper.saveFilter("custom-filter");
|
||||||
|
await filterHelper.clearFilters();
|
||||||
|
await filterHelper.firterByLastCustomFilter();
|
||||||
|
|
||||||
|
let newLength = await counter();
|
||||||
|
let newCustomFiltersSize = await filterHelper.getCustomFilters().count();
|
||||||
|
|
||||||
|
expect(newLength).to.be.below(len);
|
||||||
|
expect(newCustomFiltersSize).to.be.equal(customFiltersSize + 1);
|
||||||
|
|
||||||
|
await filterHelper.clearFilters();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('remove custom filters', async () => {
|
||||||
|
filterHelper.openCustomFiltersCategory();
|
||||||
|
|
||||||
|
let customFiltersSize = await filterHelper.getCustomFilters().count();
|
||||||
|
|
||||||
|
filterHelper.removeLastCustomFilter();
|
||||||
|
|
||||||
|
let newCustomFiltersSize = await filterHelper.getCustomFilters().count();
|
||||||
|
|
||||||
|
expect(newCustomFiltersSize).to.be.equal(customFiltersSize - 1);
|
||||||
|
});
|
||||||
|
};
|
|
@ -5,6 +5,8 @@ var commonHelper = require('../helpers').common;
|
||||||
var chai = require('chai');
|
var chai = require('chai');
|
||||||
var chaiAsPromised = require('chai-as-promised');
|
var chaiAsPromised = require('chai-as-promised');
|
||||||
|
|
||||||
|
var sharedFilters = require('../shared/filters');
|
||||||
|
|
||||||
chai.use(chaiAsPromised);
|
chai.use(chaiAsPromised);
|
||||||
var expect = chai.expect;
|
var expect = chai.expect;
|
||||||
|
|
||||||
|
@ -243,7 +245,7 @@ describe('backlog', function() {
|
||||||
expect(elementRef1).to.be.equal(draggedRefs[1]);
|
expect(elementRef1).to.be.equal(draggedRefs[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.only('drag multiple us to milestone', async function() {
|
it('drag multiple us to milestone', async function() {
|
||||||
let sprint = backlogHelper.sprints().get(0);
|
let sprint = backlogHelper.sprints().get(0);
|
||||||
let initUssSprintCount = await backlogHelper.getSprintUsertories(sprint).count();
|
let initUssSprintCount = await backlogHelper.getSprintUsertories(sprint).count();
|
||||||
|
|
||||||
|
@ -453,143 +455,9 @@ describe('backlog', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('filters', function() {
|
describe('backlog filters', sharedFilters.bind(this, 'backlog', () => {
|
||||||
it('show filters', async function() {
|
return backlogHelper.userStories().count();
|
||||||
let transition = utils.common.transitionend('.menu-secondary.filters-bar', 'opacity');
|
}));
|
||||||
|
|
||||||
$('#show-filters-button').click();
|
|
||||||
|
|
||||||
await transition();
|
|
||||||
|
|
||||||
utils.common.takeScreenshot('backlog', 'backlog-filters');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('filter by subject', async function() {
|
|
||||||
let usCount = await backlogHelper.userStories().count();
|
|
||||||
let filterQ = element(by.model('filtersQ'));
|
|
||||||
|
|
||||||
let htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body');
|
|
||||||
|
|
||||||
await filterQ.sendKeys('add');
|
|
||||||
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
let newUsCount = await backlogHelper.userStories().count();
|
|
||||||
|
|
||||||
expect(newUsCount).to.be.below(usCount);
|
|
||||||
|
|
||||||
htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body');
|
|
||||||
|
|
||||||
// clear status
|
|
||||||
await filterQ.clear();
|
|
||||||
|
|
||||||
await htmlChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('filter by ref', async function() {
|
|
||||||
let userstories = backlogHelper.userStories();
|
|
||||||
let filterQ = element(by.model('filtersQ'));
|
|
||||||
let htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body');
|
|
||||||
|
|
||||||
let ref = await backlogHelper.getTestingFilterRef();
|
|
||||||
|
|
||||||
ref = ref.replace('#', '');
|
|
||||||
|
|
||||||
await filterQ.sendKeys(ref);
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
let newUsCount = await userstories.count();
|
|
||||||
expect(newUsCount).to.be.equal(1);
|
|
||||||
|
|
||||||
htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body');
|
|
||||||
|
|
||||||
// clear status
|
|
||||||
await filterQ.clear();
|
|
||||||
|
|
||||||
await htmlChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('filter by status', async function() {
|
|
||||||
let usCount = await backlogHelper.userStories().count();
|
|
||||||
|
|
||||||
let htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body');
|
|
||||||
|
|
||||||
$$('.filters-cats a').first().click();
|
|
||||||
$$('.filter-list a').first().click();
|
|
||||||
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
let newUsCount = await backlogHelper.userStories().count();
|
|
||||||
|
|
||||||
expect(newUsCount).to.be.below(usCount);
|
|
||||||
|
|
||||||
//remove status
|
|
||||||
htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body');
|
|
||||||
|
|
||||||
$$('.filters-applied a').first().click();
|
|
||||||
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
newUsCount = await backlogHelper.userStories().count();
|
|
||||||
|
|
||||||
expect(newUsCount).to.be.equal(usCount);
|
|
||||||
|
|
||||||
backlogHelper.goBackFilters();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('filter by tags', async function() {
|
|
||||||
let usCount = await backlogHelper.userStories().count();
|
|
||||||
let htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body');
|
|
||||||
|
|
||||||
$$('.filters-cats a').get(1).click();
|
|
||||||
await browser.waitForAngular();
|
|
||||||
|
|
||||||
$$('.filter-list a').first().click();
|
|
||||||
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
let newUsCount = await backlogHelper.userStories().count();
|
|
||||||
|
|
||||||
expect(newUsCount).to.be.below(usCount);
|
|
||||||
|
|
||||||
//remove tags
|
|
||||||
htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body');
|
|
||||||
|
|
||||||
$$('.filters-applied a').first().click();
|
|
||||||
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
newUsCount = await backlogHelper.userStories().count();
|
|
||||||
|
|
||||||
expect(newUsCount).to.be.equal(usCount);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('trying drag with filters open', async function() {
|
|
||||||
let dragableElements = backlogHelper.userStories();
|
|
||||||
let dragElement = dragableElements.get(5);
|
|
||||||
|
|
||||||
await utils.common.drag(dragElement, dragableElements.get(0));
|
|
||||||
|
|
||||||
let waitErrorOpen = await utils.notifications.error.open();
|
|
||||||
|
|
||||||
expect(waitErrorOpen).to.be.true;
|
|
||||||
|
|
||||||
await utils.notifications.error.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('hide filters', async function() {
|
|
||||||
let menu = $('.menu-secondary.filters-bar');
|
|
||||||
let transition = utils.common.transitionend('.menu-secondary.filters-bar', 'width');
|
|
||||||
|
|
||||||
$('#show-filters-button').click();
|
|
||||||
|
|
||||||
await transition();
|
|
||||||
|
|
||||||
let waitWidth = await menu.getCssValue('width');
|
|
||||||
|
|
||||||
expect(waitWidth).to.be.equal('0px');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('closed sprints', function() {
|
describe('closed sprints', function() {
|
||||||
async function createEmptyMilestone() {
|
async function createEmptyMilestone() {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
var utils = require('../../utils');
|
var utils = require('../../utils');
|
||||||
var issuesHelper = require('../../helpers').issues;
|
var issuesHelper = require('../../helpers').issues;
|
||||||
var commonHelper = require('../../helpers').common;
|
var commonHelper = require('../../helpers').common;
|
||||||
|
var sharedFilters = require('../../shared/filters');
|
||||||
|
|
||||||
var chai = require('chai');
|
var chai = require('chai');
|
||||||
var chaiAsPromised = require('chai-as-promised');
|
var chaiAsPromised = require('chai-as-promised');
|
||||||
|
@ -126,195 +127,7 @@ describe('issues list', function() {
|
||||||
expect(issueUserName).to.be.equal(newUserName);
|
expect(issueUserName).to.be.equal(newUserName);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('filters', function() {
|
describe('issues filters', sharedFilters.bind(this, 'issues', () => {
|
||||||
it('by ref', async function() {
|
return issuesHelper.getIssues().count();
|
||||||
let table = issuesHelper.getTable();
|
}));
|
||||||
let issues = issuesHelper.getIssues();
|
|
||||||
let issue = issues.get(0);
|
|
||||||
issue = await issuesHelper.parseIssue(issue);
|
|
||||||
let filterInput = issuesHelper.getFilterInput();
|
|
||||||
|
|
||||||
let htmlChanges = await utils.common.outerHtmlChanges(table);
|
|
||||||
await filterInput.sendKeys(issue.ref);
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
let newIssuesCount = await issues.count();
|
|
||||||
|
|
||||||
expect(newIssuesCount).to.be.equal(1);
|
|
||||||
|
|
||||||
htmlChanges = await utils.common.outerHtmlChanges(table);
|
|
||||||
await utils.common.clear(filterInput);
|
|
||||||
await htmlChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('by subject', async function() {
|
|
||||||
let table = issuesHelper.getTable();
|
|
||||||
let issues = issuesHelper.getIssues();
|
|
||||||
let issue = issues.get(0);
|
|
||||||
issue = await issuesHelper.parseIssue(issue);
|
|
||||||
let filterInput = issuesHelper.getFilterInput();
|
|
||||||
|
|
||||||
let oldIssuesCount = await $$('.row.table-main').count();
|
|
||||||
|
|
||||||
let htmlChanges = await utils.common.outerHtmlChanges(table);
|
|
||||||
await filterInput.sendKeys(issue.subject);
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
let newIssuesCount = await issues.count();
|
|
||||||
|
|
||||||
expect(newIssuesCount).not.to.be.equal(oldIssuesCount);
|
|
||||||
expect(newIssuesCount).to.be.above(0);
|
|
||||||
|
|
||||||
htmlChanges = await utils.common.outerHtmlChanges(table);
|
|
||||||
await utils.common.clear(filterInput);
|
|
||||||
await htmlChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('by type', async function() {
|
|
||||||
let table = issuesHelper.getTable();
|
|
||||||
|
|
||||||
let htmlChanges = await utils.common.outerHtmlChanges(table);
|
|
||||||
issuesHelper.filtersCats().get(0).$('a').click();
|
|
||||||
issuesHelper.selectFilter(0);
|
|
||||||
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
issuesHelper.backToFilters();
|
|
||||||
|
|
||||||
await issuesHelper.removeFilters();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('by status', async function() {
|
|
||||||
let table = issuesHelper.getTable();
|
|
||||||
|
|
||||||
let htmlChanges = await utils.common.outerHtmlChanges(table);
|
|
||||||
issuesHelper.filtersCats().get(1).$('a').click();
|
|
||||||
issuesHelper.selectFilter(0);
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
issuesHelper.backToFilters();
|
|
||||||
|
|
||||||
await issuesHelper.removeFilters();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('by severity', async function() {
|
|
||||||
let table = issuesHelper.getTable();
|
|
||||||
|
|
||||||
let htmlChanges = await utils.common.outerHtmlChanges(table);
|
|
||||||
issuesHelper.filtersCats().get(2).$('a').click();
|
|
||||||
issuesHelper.selectFilter(0);
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
issuesHelper.backToFilters();
|
|
||||||
|
|
||||||
await issuesHelper.removeFilters();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('by priorities', async function() {
|
|
||||||
let table = issuesHelper.getTable();
|
|
||||||
|
|
||||||
let htmlChanges = await utils.common.outerHtmlChanges(table);
|
|
||||||
issuesHelper.filtersCats().get(3).$('a').click();
|
|
||||||
issuesHelper.selectFilter(0);
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
issuesHelper.backToFilters();
|
|
||||||
|
|
||||||
await issuesHelper.removeFilters();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('by tags', async function() {
|
|
||||||
let table = issuesHelper.getTable();
|
|
||||||
|
|
||||||
let htmlChanges = await utils.common.outerHtmlChanges(table);
|
|
||||||
issuesHelper.filtersCats().get(4).$('a').click();
|
|
||||||
issuesHelper.selectFilter(1);
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
issuesHelper.backToFilters();
|
|
||||||
|
|
||||||
await issuesHelper.removeFilters();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('by assigned to', async function() {
|
|
||||||
let table = issuesHelper.getTable();
|
|
||||||
|
|
||||||
let htmlChanges = await utils.common.outerHtmlChanges(table);
|
|
||||||
issuesHelper.filtersCats().get(5).$('a').click();
|
|
||||||
issuesHelper.selectFilter(0);
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
issuesHelper.backToFilters();
|
|
||||||
|
|
||||||
await issuesHelper.removeFilters();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('by created by', async function() {
|
|
||||||
let table = issuesHelper.getTable();
|
|
||||||
|
|
||||||
let htmlChanges = await utils.common.outerHtmlChanges(table);
|
|
||||||
issuesHelper.filtersCats().get(6).$('a').click();
|
|
||||||
issuesHelper.selectFilter(0);
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
issuesHelper.backToFilters();
|
|
||||||
|
|
||||||
await issuesHelper.removeFilters();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('empty', async function() {
|
|
||||||
let table = issuesHelper.getTable();
|
|
||||||
let htmlChanges = await utils.common.outerHtmlChanges(table);
|
|
||||||
|
|
||||||
let filterInput = issuesHelper.getFilterInput();
|
|
||||||
|
|
||||||
await filterInput.sendKeys(new Date().getTime());
|
|
||||||
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
let newIssuesCount = await issuesHelper.getIssues().count();
|
|
||||||
|
|
||||||
expect(newIssuesCount).to.be.equal(0);
|
|
||||||
|
|
||||||
await utils.common.takeScreenshot('issues', 'empty-issues');
|
|
||||||
await utils.common.clear(filterInput);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('save custom filter', async function() {
|
|
||||||
issuesHelper.filtersCats().get(1).$('a').click();
|
|
||||||
issuesHelper.selectFilter(0);
|
|
||||||
|
|
||||||
await browser.waitForAngular();
|
|
||||||
|
|
||||||
await issuesHelper.saveFilter('custom');
|
|
||||||
|
|
||||||
let customFilters = await issuesHelper.getCustomFilters().count();
|
|
||||||
|
|
||||||
expect(customFilters).to.be.equal(1);
|
|
||||||
|
|
||||||
await issuesHelper.removeFilters();
|
|
||||||
issuesHelper.backToFilters();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('apply custom filter', async function() {
|
|
||||||
let table = issuesHelper.getTable();
|
|
||||||
let htmlChanges = await utils.common.outerHtmlChanges(table);
|
|
||||||
|
|
||||||
issuesHelper.filtersCats().get(7).$('a').click();
|
|
||||||
|
|
||||||
issuesHelper.selectFilter(0);
|
|
||||||
|
|
||||||
await htmlChanges();
|
|
||||||
|
|
||||||
await issuesHelper.removeFilters();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('remove custom filter', async function() {
|
|
||||||
await issuesHelper.removeCustomFilters();
|
|
||||||
|
|
||||||
let customFilterCount = await issuesHelper.getCustomFilters().count();
|
|
||||||
|
|
||||||
expect(customFilterCount).to.be.equal(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,6 +2,7 @@ var utils = require('../utils');
|
||||||
var kanbanHelper = require('../helpers').kanban;
|
var kanbanHelper = require('../helpers').kanban;
|
||||||
var backlogHelper = require('../helpers').backlog;
|
var backlogHelper = require('../helpers').backlog;
|
||||||
var commonHelper = require('../helpers').common;
|
var commonHelper = require('../helpers').common;
|
||||||
|
var filterHelper = require('../helpers/filters-helper');
|
||||||
|
|
||||||
var chai = require('chai');
|
var chai = require('chai');
|
||||||
var chaiAsPromised = require('chai-as-promised');
|
var chaiAsPromised = require('chai-as-promised');
|
||||||
|
@ -18,6 +19,24 @@ describe('kanban', function() {
|
||||||
utils.common.takeScreenshot('kanban', 'kanban');
|
utils.common.takeScreenshot('kanban', 'kanban');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('zoom', async function() {
|
||||||
|
kanbanHelper.zoom(1);
|
||||||
|
await browser.sleep(1000);
|
||||||
|
utils.common.takeScreenshot('kanban', 'zoom1');
|
||||||
|
|
||||||
|
kanbanHelper.zoom(2);
|
||||||
|
await browser.sleep(1000);
|
||||||
|
utils.common.takeScreenshot('kanban', 'zoom2');
|
||||||
|
|
||||||
|
kanbanHelper.zoom(3);
|
||||||
|
await browser.sleep(1000);
|
||||||
|
utils.common.takeScreenshot('kanban', 'zoom3');
|
||||||
|
|
||||||
|
kanbanHelper.zoom(4);
|
||||||
|
await browser.sleep(1000);
|
||||||
|
utils.common.takeScreenshot('kanban', 'zoom4');
|
||||||
|
});
|
||||||
|
|
||||||
describe('create us', function() {
|
describe('create us', function() {
|
||||||
let createUSLightbox = null;
|
let createUSLightbox = null;
|
||||||
let formFields = {};
|
let formFields = {};
|
||||||
|
@ -148,7 +167,6 @@ describe('kanban', function() {
|
||||||
await utils.lightbox.close(createUSLightbox.el);
|
await utils.lightbox.close(createUSLightbox.el);
|
||||||
|
|
||||||
let ussTitles = await kanbanHelper.getColumnUssTitles(0);
|
let ussTitles = await kanbanHelper.getColumnUssTitles(0);
|
||||||
|
|
||||||
let findSubject = ussTitles.indexOf(formFields.subject) !== -1;
|
let findSubject = ussTitles.indexOf(formFields.subject) !== -1;
|
||||||
|
|
||||||
expect(findSubject).to.be.true;
|
expect(findSubject).to.be.true;
|
||||||
|
@ -297,8 +315,12 @@ describe('kanban', function() {
|
||||||
|
|
||||||
await lightbox.waitClose();
|
await lightbox.waitClose();
|
||||||
|
|
||||||
let usAssignedTo = await kanbanHelper.getBoxUss(0).get(0).$('.task-assigned').getText();
|
let usAssignedTo = await kanbanHelper.getBoxUss(0).get(0).$('.card-owner-name').getText();
|
||||||
|
|
||||||
expect(assgnedToName).to.be.equal(usAssignedTo);
|
expect(assgnedToName).to.be.equal(usAssignedTo);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('kanban filters', sharedFilters.bind(this, 'kanban', () => {
|
||||||
|
return kanbanHelper.getUss().count();
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,6 +2,8 @@ var utils = require('../../utils');
|
||||||
var backlogHelper = require('../../helpers').backlog;
|
var backlogHelper = require('../../helpers').backlog;
|
||||||
var taskboardHelper = require('../../helpers').taskboard;
|
var taskboardHelper = require('../../helpers').taskboard;
|
||||||
var commonHelper = require('../../helpers').common;
|
var commonHelper = require('../../helpers').common;
|
||||||
|
var filterHelper = require('../../helpers/filters-helper');
|
||||||
|
var sharedFilters = require('../../shared/filters');
|
||||||
|
|
||||||
var chai = require('chai');
|
var chai = require('chai');
|
||||||
var chaiAsPromised = require('chai-as-promised');
|
var chaiAsPromised = require('chai-as-promised');
|
||||||
|
@ -21,6 +23,24 @@ describe('taskboard', function() {
|
||||||
utils.common.takeScreenshot('taskboard', 'taskboard');
|
utils.common.takeScreenshot('taskboard', 'taskboard');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('zoom', async function() {
|
||||||
|
taskboardHelper.zoom(1);
|
||||||
|
await browser.sleep(1000);
|
||||||
|
utils.common.takeScreenshot('taskboard', 'zoom1');
|
||||||
|
|
||||||
|
taskboardHelper.zoom(2);
|
||||||
|
await browser.sleep(1000);
|
||||||
|
utils.common.takeScreenshot('taskboard', 'zoom2');
|
||||||
|
|
||||||
|
taskboardHelper.zoom(3);
|
||||||
|
await browser.sleep(1000);
|
||||||
|
utils.common.takeScreenshot('taskboard', 'zoom3');
|
||||||
|
|
||||||
|
taskboardHelper.zoom(4);
|
||||||
|
await browser.sleep(1000);
|
||||||
|
utils.common.takeScreenshot('taskboard', 'zoom4');
|
||||||
|
});
|
||||||
|
|
||||||
describe('create task', function() {
|
describe('create task', function() {
|
||||||
let createTaskLightbox = null;
|
let createTaskLightbox = null;
|
||||||
let formFields = {};
|
let formFields = {};
|
||||||
|
@ -65,7 +85,7 @@ describe('taskboard', function() {
|
||||||
|
|
||||||
let tasks = taskboardHelper.getBoxTasks(0, 0);
|
let tasks = taskboardHelper.getBoxTasks(0, 0);
|
||||||
|
|
||||||
let tasksSubject = await $$('.task-name').getText();
|
let tasksSubject = await $$('.e2e-title').getText();
|
||||||
|
|
||||||
let findSubject = tasksSubject.indexOf(formFields.subject) !== -1;
|
let findSubject = tasksSubject.indexOf(formFields.subject) !== -1;
|
||||||
|
|
||||||
|
@ -111,7 +131,7 @@ describe('taskboard', function() {
|
||||||
|
|
||||||
let tasks = taskboardHelper.getBoxTasks(0, 0);
|
let tasks = taskboardHelper.getBoxTasks(0, 0);
|
||||||
|
|
||||||
let tasksSubject = await $$('.task-name').getText();
|
let tasksSubject = await $$('.e2e-title').getText();
|
||||||
|
|
||||||
let findSubject = tasksSubject.indexOf(formFields.subject) !== 1;
|
let findSubject = tasksSubject.indexOf(formFields.subject) !== 1;
|
||||||
|
|
||||||
|
@ -296,4 +316,8 @@ describe('taskboard', function() {
|
||||||
expect(open).to.be.false;
|
expect(open).to.be.false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('taskboard filters', sharedFilters.bind(this, 'taskboard', () => {
|
||||||
|
return taskboardHelper.getTasks().count();
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
|
@ -291,7 +291,11 @@ gulp.task("css-lint-app", function() {
|
||||||
return gulp.src(cssFiles)
|
return gulp.src(cssFiles)
|
||||||
.pipe(gulpif(!isDeploy, cache(csslint("csslintrc.json"), {
|
.pipe(gulpif(!isDeploy, cache(csslint("csslintrc.json"), {
|
||||||
success: function(csslintFile) {
|
success: function(csslintFile) {
|
||||||
return csslintFile.csslint.success;
|
if (csslintFile.csslint) {
|
||||||
|
return csslintFile.csslint.success;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
value: function(csslintFile) {
|
value: function(csslintFile) {
|
||||||
return {
|
return {
|
||||||
|
|
Loading…
Reference in New Issue