cards & filters ui refactor
parent
e6ef8ffa34
commit
59bf55fc30
|
@ -23,6 +23,10 @@
|
|||
- Add Wiki history
|
||||
- Third party integrations:
|
||||
- Included gogs as builtin integration.
|
||||
- Filters refactor
|
||||
- Cards ui refactor with zoom
|
||||
- Kanban filters
|
||||
- Taskboard filters
|
||||
|
||||
### Misc
|
||||
- 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
|
||||
#############################################################################
|
||||
|
||||
class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin)
|
||||
class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin, taiga.UsFiltersMixin)
|
||||
@.$inject = [
|
||||
"$scope",
|
||||
"$rootScope",
|
||||
|
@ -57,18 +57,30 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
|||
"$tgLoading",
|
||||
"tgResources",
|
||||
"$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,
|
||||
@events, @analytics, @translate, @loading, @rs2, @modelTransform, @errorHandlingService) ->
|
||||
@events, @analytics, @translate, @loading, @rs2, @modelTransform, @errorHandlingService, @storage, @filterRemoteStorageService) ->
|
||||
bindMethods(@)
|
||||
|
||||
@.backlogOrder = {}
|
||||
@.milestonesOrder = {}
|
||||
|
||||
@.page = 1
|
||||
@.disablePagination = false
|
||||
@.firstLoadComplete = false
|
||||
@scope.userstories = []
|
||||
|
||||
return if @.applyStoredFilters(@params.pslug, "backlog-filters")
|
||||
|
||||
@scope.sectionName = @translate.instant("BACKLOG.SECTION_NAME")
|
||||
@showTags = false
|
||||
@activeFilters = false
|
||||
|
@ -97,6 +109,9 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
|||
# On Error
|
||||
promise.then null, @.onInitialDataError.bind(@)
|
||||
|
||||
filtersReloadContent: () ->
|
||||
@.loadUserstories(true)
|
||||
|
||||
initializeEventHandlers: ->
|
||||
@scope.$on "usform:bulk:success", =>
|
||||
@.loadUserstories(true)
|
||||
|
@ -175,6 +190,12 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
|||
@scope.showGraphPlaceholder = !(stats.total_points? && stats.total_milestones?)
|
||||
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: ->
|
||||
@scope.$apply =>
|
||||
@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) =>
|
||||
sprints = result.milestones
|
||||
|
||||
@.setMilestonesOrder(sprints)
|
||||
|
||||
@scope.totalClosedMilestones = result.closed
|
||||
|
||||
# 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) =>
|
||||
sprints = result.milestones
|
||||
|
||||
@.setMilestonesOrder(sprints)
|
||||
|
||||
@scope.totalMilestones = sprints
|
||||
@scope.totalClosedMilestones = result.closed
|
||||
@scope.totalOpenMilestones = result.open
|
||||
|
@ -221,47 +246,6 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
|||
|
||||
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: () ->
|
||||
page = @.page
|
||||
|
||||
|
@ -273,15 +257,15 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
|||
|
||||
@.loadingUserstories = true
|
||||
@.disablePagination = true
|
||||
@scope.httpParams = @.getUrlFilters()
|
||||
@rs.userstories.storeQueryParams(@scope.projectId, @scope.httpParams)
|
||||
params = _.clone(@location.search())
|
||||
@rs.userstories.storeQueryParams(@scope.projectId, params)
|
||||
|
||||
if resetPagination
|
||||
@.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) =>
|
||||
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
|
||||
@scope.userstories = @scope.userstories.concat(_.sortBy(userstories, "backlog_order"))
|
||||
|
||||
@.setSearchDataFilters()
|
||||
for it in @scope.userstories
|
||||
@.backlogOrder[it.id] = it.backlog_order
|
||||
|
||||
@.loadingUserstories = false
|
||||
|
||||
|
@ -354,242 +339,142 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
|||
|
||||
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) ->
|
||||
oldSprintId = usList[0].milestone
|
||||
project = usList[0].project
|
||||
|
||||
movedFromClosedSprint = false
|
||||
movedToClosedSprint = false
|
||||
if oldSprintId
|
||||
sprint = @scope.sprintsById[oldSprintId] || @scope.closedSprintsById[oldSprintId]
|
||||
|
||||
sprint = @scope.sprintsById[oldSprintId]
|
||||
if newSprintId
|
||||
newSprint = @scope.sprintsById[newSprintId] || @scope.closedSprintsById[newSprintId]
|
||||
|
||||
# Move from closed sprint
|
||||
if !sprint && @scope.closedSprintsById
|
||||
sprint = @scope.closedSprintsById[oldSprintId]
|
||||
movedFromClosedSprint = true if sprint
|
||||
currentSprintId = if newSprintId != oldSprintId then newSprintId else oldSprintId
|
||||
|
||||
newSprint = @scope.sprintsById[newSprintId]
|
||||
orderList = null
|
||||
orderField = ""
|
||||
|
||||
# Move to closed sprint
|
||||
if !newSprint && newSprintId
|
||||
newSprint = @scope.closedSprintsById[newSprintId]
|
||||
movedToClosedSprint = true if newSprint
|
||||
if newSprintId != oldSprintId
|
||||
if newSprintId == null # From sprint to backlog
|
||||
for us, key in usList # delete from sprint userstories
|
||||
_.remove sprint.user_stories, (it) -> it.id == us.id
|
||||
|
||||
# In the same sprint or in the backlog
|
||||
if newSprintId == oldSprintId
|
||||
items = null
|
||||
userstories = null
|
||||
orderField = "backlog_order"
|
||||
orderList = @.backlogOrder
|
||||
|
||||
if newSprintId == null
|
||||
userstories = @scope.userstories
|
||||
else
|
||||
userstories = newSprint.user_stories
|
||||
beforeDestination = _.slice(@scope.userstories, 0, newUsIndex)
|
||||
afterDestination = _.slice(@scope.userstories, newUsIndex)
|
||||
|
||||
@scope.$apply ->
|
||||
for us, key in usList
|
||||
r = userstories.indexOf(us)
|
||||
userstories.splice(r, 1)
|
||||
@scope.userstories = @scope.userstories.concat(usList)
|
||||
else # From backlog to sprint
|
||||
for us in usList # delete from sprint userstories
|
||||
_.remove @scope.userstories, (it) -> it.id == us.id
|
||||
|
||||
args = [newUsIndex, 0].concat(usList)
|
||||
Array.prototype.splice.apply(userstories, args)
|
||||
orderField = "sprint_order"
|
||||
orderList = @.milestonesOrder[newSprint.id]
|
||||
|
||||
# If in backlog
|
||||
if newSprintId == null
|
||||
# Rehash userstories order field
|
||||
beforeDestination = _.slice(newSprint.user_stories, 0, newUsIndex)
|
||||
afterDestination = _.slice(newSprint.user_stories, newUsIndex)
|
||||
|
||||
items = @.resortUserStories(userstories, "backlog_order")
|
||||
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
|
||||
newSprint.user_stories = newSprint.user_stories.concat(usList)
|
||||
else
|
||||
us.milestone = newSprintId for us in usList
|
||||
if oldSprintId == null # backlog
|
||||
orderField = "backlog_order"
|
||||
orderList = @.backlogOrder
|
||||
|
||||
@scope.$apply =>
|
||||
args = [newUsIndex, 0].concat(usList)
|
||||
list = _.filter @scope.userstories, (listIt) -> # Remove moved US from list
|
||||
return !_.find usList, (moveIt) -> return listIt.id == moveIt.id
|
||||
|
||||
# Add new us to backlog userstories list
|
||||
Array.prototype.splice.apply(newSprint.user_stories, args)
|
||||
beforeDestination = _.slice(list, 0, newUsIndex)
|
||||
afterDestination = _.slice(list, newUsIndex)
|
||||
else # sprint
|
||||
orderField = "sprint_order"
|
||||
orderList = @.milestonesOrder[sprint.id]
|
||||
|
||||
# Remove the us from the sprint list.
|
||||
for us in usList
|
||||
r = sprint.user_stories.indexOf(us)
|
||||
sprint.user_stories.splice(r, 1)
|
||||
list = _.filter newSprint.user_stories, (listIt) -> # Remove moved US from list
|
||||
return !_.find usList, (moveIt) -> return listIt.id == moveIt.id
|
||||
|
||||
#Persist the milestone change of userstory
|
||||
promises = _.map usList, (us) => @repo.save(us)
|
||||
beforeDestination = _.slice(list, 0, newUsIndex)
|
||||
afterDestination = _.slice(list, newUsIndex)
|
||||
|
||||
#Rehash userstories order field
|
||||
#and persist in bulk all changes.
|
||||
promise = @q.all(promises).then =>
|
||||
items = @.resortUserStories(newSprint.user_stories, "sprint_order")
|
||||
data = @.prepareBulkUpdateData(items, "sprint_order")
|
||||
# previous us
|
||||
previous = beforeDestination[beforeDestination.length - 1]
|
||||
|
||||
@rs.userstories.bulkUpdateSprintOrder(project, data).then (result) =>
|
||||
@rootscope.$broadcast("sprint:us:moved")
|
||||
# this will store the previous us with the same position
|
||||
setPreviousOrders = []
|
||||
|
||||
@rs.userstories.bulkUpdateBacklogOrder(project, data).then =>
|
||||
@rootscope.$broadcast("sprint:us:moved")
|
||||
if !previous
|
||||
startIndex = 0
|
||||
else if previous
|
||||
startIndex = orderList[previous.id] + 1
|
||||
|
||||
if movedToClosedSprint || movedFromClosedSprint
|
||||
@scope.$broadcast("backlog:load-closed-sprints")
|
||||
previousWithTheSameOrder = _.filter beforeDestination, (it) -> it[orderField] == orderList[previous.id]
|
||||
|
||||
promise.then null, ->
|
||||
console.log "FAIL" # TODO
|
||||
# we must send the USs previous to the dropped USs to tell the backend which USs are before the dropped USs,
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
updateUserStoryStatus: () ->
|
||||
@.setSearchDataFilters()
|
||||
@.generateFilters().then () =>
|
||||
@rootscope.$broadcast("filters:update")
|
||||
@.loadProjectStats()
|
||||
|
@ -807,8 +692,15 @@ BacklogDirective = ($repo, $rootscope, $translate) ->
|
|||
text = $translate.instant("BACKLOG.TAGS.SHOW")
|
||||
elm.text(text)
|
||||
|
||||
openFilterInit = ($scope, $el, $ctrl) ->
|
||||
sidebar = $el.find("sidebar.backlog-filter")
|
||||
|
||||
sidebar.addClass("active")
|
||||
|
||||
$ctrl.activeFilters = true
|
||||
|
||||
showHideFilter = ($scope, $el, $ctrl) ->
|
||||
sidebar = $el.find("sidebar.filters-bar")
|
||||
sidebar = $el.find("sidebar.backlog-filter")
|
||||
sidebar.one "transitionend", () ->
|
||||
timeout 150, ->
|
||||
$rootscope.$broadcast("resize")
|
||||
|
@ -824,11 +716,6 @@ BacklogDirective = ($repo, $rootscope, $translate) ->
|
|||
|
||||
toggleText(target, [hideText, showText])
|
||||
|
||||
if !sidebar.hasClass("active")
|
||||
$ctrl.resetFilters()
|
||||
else
|
||||
$ctrl.restoreFilters()
|
||||
|
||||
$ctrl.toggleActiveFilters()
|
||||
|
||||
## Filters Link
|
||||
|
@ -847,11 +734,13 @@ BacklogDirective = ($repo, $rootscope, $translate) ->
|
|||
linkFilters($scope, $el, $attrs, $ctrl)
|
||||
linkDoomLine($scope, $el, $attrs, $ctrl)
|
||||
|
||||
filters = $ctrl.getUrlFilters()
|
||||
filters = $ctrl.location.search()
|
||||
if filters.status ||
|
||||
filters.tags ||
|
||||
filters.q
|
||||
showHideFilter($scope, $el, $ctrl)
|
||||
filters.q ||
|
||||
filters.assigned_to ||
|
||||
filters.owner
|
||||
openFilterInit($scope, $el, $ctrl)
|
||||
|
||||
$scope.$on "showTags", () ->
|
||||
showHideTags($ctrl)
|
||||
|
|
|
@ -42,7 +42,7 @@ deleteElement = (el) ->
|
|||
$(el).off()
|
||||
$(el).remove()
|
||||
|
||||
BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) ->
|
||||
BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm) ->
|
||||
link = ($scope, $el, $attrs) ->
|
||||
bindOnce $scope, "project", (project) ->
|
||||
# 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
|
||||
|
||||
filterError = ->
|
||||
text = $translate.instant("BACKLOG.SORTABLE_FILTER_ERROR")
|
||||
$tgConfirm.notify("error", text)
|
||||
|
||||
drake = dragula([$el[0], $('.empty-backlog')[0]], {
|
||||
copySortSource: false,
|
||||
copy: false,
|
||||
|
@ -63,18 +59,11 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) ->
|
|||
if !$(item).hasClass('row')
|
||||
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
|
||||
})
|
||||
|
||||
drake.on 'drag', (item, container) ->
|
||||
# it doesn't move is the filter is open
|
||||
parent = $(item).parent()
|
||||
initIsBacklog = parent.hasClass('backlog-table-body')
|
||||
|
||||
|
@ -88,6 +77,8 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) ->
|
|||
$(item).addClass('backlog-us-mirror')
|
||||
|
||||
drake.on 'dragend', (item) ->
|
||||
parent = $(item).parent()
|
||||
|
||||
$('.doom-line').remove()
|
||||
|
||||
parent = $(item).parent()
|
||||
|
@ -102,8 +93,6 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) ->
|
|||
|
||||
$(document.body).removeClass("drag-active")
|
||||
|
||||
items = $(item).parent().find('.row')
|
||||
|
||||
sprint = null
|
||||
|
||||
firstElement = if dragMultipleItems.length then dragMultipleItems[0] else item
|
||||
|
@ -131,11 +120,7 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) ->
|
|||
usList = _.map dragMultipleItems, (item) ->
|
||||
return item = $(item).scope().us
|
||||
else
|
||||
usList = _.map items, (item) ->
|
||||
item = $(item)
|
||||
itemUs = item.scope().us
|
||||
|
||||
return itemUs
|
||||
usList = [$(item).scope().us]
|
||||
|
||||
$scope.$emit("sprint:us:move", usList, index, sprint)
|
||||
|
||||
|
@ -158,6 +143,5 @@ module.directive("tgBacklogSortable", [
|
|||
"$tgResources",
|
||||
"$rootScope",
|
||||
"$tgConfirm",
|
||||
"$translate",
|
||||
BacklogSortableDirective
|
||||
])
|
||||
|
|
|
@ -41,7 +41,7 @@ class RepositoryService extends taiga.Service
|
|||
defered = @q.defer()
|
||||
url = @urls.resolve(name)
|
||||
|
||||
promise = @http.post(url, JSON.stringify(data))
|
||||
promise = @http.post(url, JSON.stringify(data), extraParams)
|
||||
promise.success (_data, _status) =>
|
||||
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))
|
||||
return @q.all(promises)
|
||||
|
||||
save: (model, patch=true) ->
|
||||
save: (model, patch=true, params = {}, options, returnHeaders = false) ->
|
||||
defered = @q.defer()
|
||||
|
||||
if not model.isModified() and patch
|
||||
|
@ -75,20 +75,25 @@ class RepositoryService extends taiga.Service
|
|||
return defered.promise
|
||||
|
||||
url = @.resolveUrlForModel(model)
|
||||
|
||||
data = JSON.stringify(model.getAttrs(patch))
|
||||
|
||||
if patch
|
||||
promise = @http.patch(url, data)
|
||||
promise = @http.patch(url, data, params, options)
|
||||
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._attrs = _.extend(model.getAttrs(), data)
|
||||
model._modifiedAttrs = {}
|
||||
|
||||
model.applyCasts()
|
||||
defered.resolve(model)
|
||||
|
||||
if returnHeaders
|
||||
defered.resolve([model, headers()])
|
||||
else
|
||||
defered.resolve(model)
|
||||
|
||||
promise.error (data, status) ->
|
||||
defered.reject(data)
|
||||
|
|
|
@ -378,22 +378,28 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
|
|||
.target(submitButton)
|
||||
.start()
|
||||
|
||||
params = {
|
||||
include_attachments: true,
|
||||
include_tasks: true
|
||||
}
|
||||
|
||||
if $scope.isNew
|
||||
promise = $repo.create("userstories", $scope.us)
|
||||
broadcastEvent = "usform:new:success"
|
||||
else
|
||||
promise = $repo.save($scope.us)
|
||||
promise = $repo.save($scope.us, true)
|
||||
broadcastEvent = "usform:edit:success"
|
||||
|
||||
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) ->
|
||||
currentLoading.finish()
|
||||
|
@ -433,7 +439,7 @@ module.directive("tgLbCreateEditUserstory", [
|
|||
"$translate",
|
||||
"$tgConfirm",
|
||||
"$q",
|
||||
"tgAttachmentsService",
|
||||
"tgAttachmentsService"
|
||||
CreateEditUserstoryDirective
|
||||
])
|
||||
|
||||
|
@ -442,7 +448,7 @@ module.directive("tgLbCreateEditUserstory", [
|
|||
## Creare Bulk Userstories Lightbox Directive
|
||||
#############################################################################
|
||||
|
||||
CreateBulkUserstoriesDirective = ($repo, $rs, $rootscope, lightboxService, $loading) ->
|
||||
CreateBulkUserstoriesDirective = ($repo, $rs, $rootscope, lightboxService, $loading, $model) ->
|
||||
link = ($scope, $el, attrs) ->
|
||||
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.then (result) ->
|
||||
result = _.map(result.data, (x) => $model.make_model('userstories', x))
|
||||
currentLoading.finish()
|
||||
$rootscope.$broadcast("usform:bulk:success", result)
|
||||
lightboxService.close($el)
|
||||
|
@ -494,6 +501,7 @@ module.directive("tgLbCreateBulkUserstories", [
|
|||
"$rootScope",
|
||||
"lightboxService",
|
||||
"$tgLoading",
|
||||
"$tgModel",
|
||||
CreateBulkUserstoriesDirective
|
||||
])
|
||||
|
||||
|
@ -535,7 +543,7 @@ AssignedToLightboxDirective = (lightboxService, lightboxKeyboardNavigationServic
|
|||
visibleUsers = _.map visibleUsers, (user) ->
|
||||
user.avatar = avatarService.getAvatar(user)
|
||||
|
||||
selected.avatar = avatarService.getAvatar(selected)
|
||||
selected.avatar = avatarService.getAvatar(selected) if selected
|
||||
|
||||
ctx = {
|
||||
selected: selected
|
||||
|
|
|
@ -110,4 +110,179 @@ class FiltersMixin
|
|||
location = if load then @location else @location.noreload(@scope)
|
||||
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
|
||||
|
||||
#############################################################################
|
||||
## 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
|
||||
debounceLeading = @.taiga.debounceLeading
|
||||
startswith = @.taiga.startswith
|
||||
bindMethods = @.taiga.bindMethods
|
||||
|
||||
module = angular.module("taigaIssues")
|
||||
|
||||
|
@ -55,21 +56,23 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
"$tgEvents",
|
||||
"$tgAnalytics",
|
||||
"$translate",
|
||||
"tgErrorHandlingService"
|
||||
"tgErrorHandlingService",
|
||||
"$tgStorage",
|
||||
"tgFilterRemoteStorageService"
|
||||
]
|
||||
|
||||
filtersHashSuffix: "issues-filters"
|
||||
myFiltersHashSuffix: "issues-my-filters"
|
||||
|
||||
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.filters = {}
|
||||
@.voting = false
|
||||
|
||||
if _.isEmpty(@location.search())
|
||||
filters = @rs.issues.getFilters(@params.pslug)
|
||||
filters.page = 1
|
||||
@location.search(filters)
|
||||
@location.replace()
|
||||
return
|
||||
return if @.applyStoredFilters(@params.pslug, @.filtersHashSuffix)
|
||||
|
||||
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)
|
||||
@.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: ->
|
||||
routingKey = "changes.project.#{@scope.projectId}.issues"
|
||||
@events.subscribe @scope, routingKey, (message) =>
|
||||
@.loadIssues()
|
||||
|
||||
storeFilters: ->
|
||||
@rs.issues.storeFilters(@params.pslug, @location.search())
|
||||
|
||||
loadProject: ->
|
||||
return @rs.projects.getBySlug(@params.pslug).then (project) =>
|
||||
|
@ -117,160 +303,15 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
|
||||
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
|
||||
# When searching by text loadIssues can be called fastly with different parameters and
|
||||
# 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
|
||||
loadIssuesRequests: 0
|
||||
loadIssues: =>
|
||||
@scope.urlFilters = @.getUrlFilters()
|
||||
params = @location.search()
|
||||
|
||||
# Convert stored filters to http parameters
|
||||
# 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)
|
||||
promise = @rs.issues.list(@scope.projectId, params)
|
||||
@.loadIssuesRequests += 1
|
||||
promise.index = @.loadIssuesRequests
|
||||
promise.then (data) =>
|
||||
|
@ -289,26 +330,10 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
return promise.then (project) =>
|
||||
@.fillUsersAndRoles(project.members, project.roles)
|
||||
@.initializeSubscription()
|
||||
@.loadFilters()
|
||||
@.generateFilters()
|
||||
|
||||
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
|
||||
addNewIssue: ->
|
||||
@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)
|
||||
|
||||
getOrderBy: ->
|
||||
if _.isString(@location.search().order_by)
|
||||
return @location.search().order_by
|
||||
else
|
||||
return "created_date"
|
||||
|
||||
module.controller("IssuesController", IssuesController)
|
||||
|
||||
#############################################################################
|
||||
|
@ -431,28 +462,40 @@ IssuesDirective = ($log, $location, $template, $compile) ->
|
|||
## Issues Filters
|
||||
linkOrdering = ($scope, $el, $attrs, $ctrl) ->
|
||||
# Draw the arrow the first time
|
||||
currentOrder = $ctrl.getUrlFilter("orderBy") or "created_date"
|
||||
|
||||
currentOrder = $ctrl.getOrderBy()
|
||||
|
||||
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.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) ->
|
||||
target = angular.element(event.currentTarget)
|
||||
|
||||
currentOrder = $ctrl.getUrlFilter("orderBy")
|
||||
currentOrder = $ctrl.getOrderBy()
|
||||
newOrder = target.data("fieldname")
|
||||
|
||||
finalOrder = if currentOrder == newOrder then "-#{newOrder}" else newOrder
|
||||
|
||||
$scope.$apply ->
|
||||
$ctrl.replaceFilter("orderBy", finalOrder)
|
||||
$ctrl.storeFilters()
|
||||
$ctrl.replaceFilter("order_by", finalOrder)
|
||||
|
||||
$ctrl.storeFilters($ctrl.params.pslug, $location.search(), $ctrl.filtersHashSuffix)
|
||||
$ctrl.loadIssues().then ->
|
||||
# Update the arrow
|
||||
$el.find(".row.title > div > span.icon").remove()
|
||||
icon = if startswith(finalOrder, "-") then "icon-arrow-up" else "icon-arrow-bottom"
|
||||
target.html("#{target.html()}<span class='icon #{icon}'></span>")
|
||||
$el.find(".row.title > div > tg-svg").remove()
|
||||
icon = if startswith(finalOrder, "-") then "icon-arrow-up" else "icon-arrow-down"
|
||||
|
||||
svg = $("<tg-svg>")
|
||||
.attr("svg-icon", icon)
|
||||
|
||||
target.append(svg)
|
||||
$compile(target.contents())($scope);
|
||||
|
||||
## Issues Link
|
||||
link = ($scope, $el, $attrs) ->
|
||||
|
@ -468,253 +511,6 @@ IssuesDirective = ($log, $location, $template, $compile) ->
|
|||
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)
|
||||
#############################################################################
|
||||
|
|
|
@ -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")
|
||||
|
||||
# Vars
|
||||
|
||||
defaultViewMode = "maximized"
|
||||
viewModes = [
|
||||
"maximized",
|
||||
"minimized"
|
||||
]
|
||||
|
||||
|
||||
#############################################################################
|
||||
## 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 = [
|
||||
"$scope",
|
||||
"$rootScope",
|
||||
"$tgRepo",
|
||||
"$tgConfirm",
|
||||
"$tgResources",
|
||||
"tgResources",
|
||||
"$routeParams",
|
||||
"$q",
|
||||
"$tgLocation",
|
||||
|
@ -62,16 +54,26 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
"$tgEvents",
|
||||
"$tgAnalytics",
|
||||
"$translate",
|
||||
"tgErrorHandlingService"
|
||||
"tgErrorHandlingService",
|
||||
"$tgModel",
|
||||
"tgKanbanUserstories",
|
||||
"$tgStorage",
|
||||
"tgFilterRemoteStorageService"
|
||||
]
|
||||
|
||||
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location,
|
||||
@appMetaService, @navUrls, @events, @analytics, @translate, @errorHandlingService) ->
|
||||
storeCustomFiltersName: 'kanban-custom-filters'
|
||||
storeFiltersName: 'kanban-filters'
|
||||
|
||||
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @rs2, @params, @q, @location,
|
||||
@appMetaService, @navUrls, @events, @analytics, @translate, @errorHandlingService,
|
||||
@model, @kanbanUserstoriesService, @storage, @filterRemoteStorageService) ->
|
||||
bindMethods(@)
|
||||
@kanbanUserstoriesService.reset()
|
||||
@.openFilter = false
|
||||
|
||||
return if @.applyStoredFilters(@params.pslug, "kanban-filters")
|
||||
|
||||
@scope.sectionName = @translate.instant("KANBAN.SECTION_NAME")
|
||||
@scope.statusViewModes = {}
|
||||
@.initializeEventHandlers()
|
||||
|
||||
promise = @.loadInitialData()
|
||||
|
@ -88,80 +90,106 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
# On Error
|
||||
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: ->
|
||||
@scope.$on "usform:new:success", =>
|
||||
@.loadUserstories()
|
||||
@.refreshTagsColors()
|
||||
@scope.$on "usform:new:success", (event, us) =>
|
||||
@.refreshTagsColors().then () =>
|
||||
@kanbanUserstoriesService.add(us)
|
||||
|
||||
@analytics.trackEvent("userstory", "create", "create userstory on kanban", 1)
|
||||
|
||||
@scope.$on "usform:bulk:success", =>
|
||||
@.loadUserstories()
|
||||
@scope.$on "usform:bulk:success", (event, uss) =>
|
||||
@.refreshTagsColors().then () =>
|
||||
@kanbanUserstoriesService.add(uss)
|
||||
|
||||
@analytics.trackEvent("userstory", "create", "bulk create userstory on kanban", 1)
|
||||
|
||||
@scope.$on "usform:edit:success", =>
|
||||
@.loadUserstories()
|
||||
@.refreshTagsColors()
|
||||
@scope.$on "usform:edit:success", (event, us) =>
|
||||
@.refreshTagsColors().then () =>
|
||||
@kanbanUserstoriesService.replaceModel(us)
|
||||
|
||||
@scope.$on("assigned-to:added", @.onAssignedToChanged)
|
||||
@scope.$on("kanban:us:move", @.moveUs)
|
||||
@scope.$on("kanban:show-userstories-for-status", @.loadUserStoriesForStatus)
|
||||
@scope.$on("kanban:hide-userstories-for-status", @.hideUserStoriesForStatus)
|
||||
|
||||
# Template actions
|
||||
|
||||
addNewUs: (type, statusId) ->
|
||||
switch type
|
||||
when "standard" then @rootscope.$broadcast("usform:new", @scope.projectId, statusId, @scope.usStatusList)
|
||||
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)
|
||||
|
||||
# Scope Events Handlers
|
||||
onAssignedToChanged: (ctx, userid, usModel) ->
|
||||
usModel.assigned_to = userid
|
||||
|
||||
onAssignedToChanged: (ctx, userid, us) ->
|
||||
us.assigned_to = userid
|
||||
@kanbanUserstoriesService.replaceModel(usModel)
|
||||
|
||||
promise = @repo.save(us)
|
||||
promise = @repo.save(usModel)
|
||||
promise.then null, ->
|
||||
console.log "FAIL" # TODO
|
||||
|
||||
# Load data methods
|
||||
refreshTagsColors: ->
|
||||
return @rs.projects.tagsColors(@scope.projectId).then (tags_colors) =>
|
||||
@scope.project.tags_colors = tags_colors
|
||||
|
||||
loadUserstories: ->
|
||||
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) =>
|
||||
@scope.userstories = userstories
|
||||
|
||||
usByStatus = _.groupBy(userstories, "status")
|
||||
us_archived = []
|
||||
for status in @scope.usStatusList
|
||||
if not usByStatus[status.id]?
|
||||
usByStatus[status.id] = []
|
||||
if @scope.usByStatus?
|
||||
for us in @scope.usByStatus[status.id]
|
||||
if us.status != status.id
|
||||
us_archived.push(us)
|
||||
|
||||
# Must preserve the archived columns if loaded
|
||||
if status.is_archived and @scope.usByStatus? 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
|
||||
@kanbanUserstoriesService.init(@scope.project, @scope.usersById)
|
||||
@kanbanUserstoriesService.set(userstories)
|
||||
|
||||
# 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
|
||||
|
@ -175,14 +203,28 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
return promise
|
||||
|
||||
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) =>
|
||||
@scope.usByStatus[statusId] = _.sortBy(userstories, "kanban_order")
|
||||
@scope.$broadcast("kanban:shown-userstories-for-status", statusId, userstories)
|
||||
|
||||
return userstories
|
||||
|
||||
hideUserStoriesForStatus: (ctx, statusId) ->
|
||||
@scope.usByStatus[statusId] = []
|
||||
@scope.$broadcast("kanban:hidden-userstories-for-status", statusId)
|
||||
|
||||
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.usStatusList = _.sortBy(project.us_statuses, "order")
|
||||
|
||||
@.generateStatusViewModes()
|
||||
|
||||
@scope.$emit("project:loaded", project)
|
||||
return project
|
||||
|
||||
|
@ -220,82 +260,40 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
@.fillUsersAndRoles(project.members, project.roles)
|
||||
@.initializeSubscription()
|
||||
@.loadKanban()
|
||||
|
||||
|
||||
## 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'
|
||||
@.generateFilters()
|
||||
|
||||
# Utils methods
|
||||
|
||||
prepareBulkUpdateData: (uses, field="kanban_order") ->
|
||||
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) ->
|
||||
if oldStatusId != newStatusId
|
||||
# Remove us from old status column
|
||||
r = @scope.usByStatus[oldStatusId].indexOf(us)
|
||||
@scope.usByStatus[oldStatusId].splice(r, 1)
|
||||
us = @kanbanUserstoriesService.getUsModel(us.get('id'))
|
||||
|
||||
# Add us to new status column.
|
||||
@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)
|
||||
moveUpdateData = @kanbanUserstoriesService.move(us.id, newStatusId, index)
|
||||
|
||||
itemsToSave = @.resortUserStories(@scope.usByStatus[newStatusId])
|
||||
@scope.usByStatus[newStatusId] = _.sortBy(@scope.usByStatus[newStatusId], "kanban_order")
|
||||
params = {
|
||||
include_attachments: true,
|
||||
include_tasks: true
|
||||
}
|
||||
|
||||
# Persist the userstory
|
||||
promise = @repo.save(us)
|
||||
options = {
|
||||
headers: {
|
||||
"set-orders": JSON.stringify(moveUpdateData.set_orders)
|
||||
}
|
||||
}
|
||||
|
||||
# Rehash userstories order field
|
||||
# and persist in bulk all changes.
|
||||
promise = promise.then =>
|
||||
itemsToSave = _.reject(itemsToSave, {"id": us.id})
|
||||
data = @.prepareBulkUpdateData(itemsToSave)
|
||||
promise = @repo.save(us, true, params, options, true)
|
||||
|
||||
return @rs.userstories.bulkUpdateKanbanOrder(us.project, data).then =>
|
||||
return itemsToSave
|
||||
promise = promise.then (result) =>
|
||||
headers = result[1]
|
||||
|
||||
if headers && headers['taiga-info-order-updated']
|
||||
order = JSON.parse(headers['taiga-info-order-updated'])
|
||||
@kanbanUserstoriesService.assignOrders(order)
|
||||
|
||||
return promise
|
||||
|
||||
|
||||
module.controller("KanbanController", KanbanController)
|
||||
|
||||
#############################################################################
|
||||
|
@ -322,7 +320,7 @@ module.directive("tgKanban", ["$tgRepo", "$rootScope", KanbanDirective])
|
|||
## Kanban Archived Status Column Header Control
|
||||
#############################################################################
|
||||
|
||||
KanbanArchivedStatusHeaderDirective = ($rootscope, $translate) ->
|
||||
KanbanArchivedStatusHeaderDirective = ($rootscope, $translate, kanbanUserstoriesService) ->
|
||||
showArchivedText = $translate.instant("KANBAN.ACTION_SHOW_ARCHIVED")
|
||||
hideArchivedText = $translate.instant("KANBAN.ACTION_HIDE_ARCHIVED")
|
||||
|
||||
|
@ -330,6 +328,9 @@ KanbanArchivedStatusHeaderDirective = ($rootscope, $translate) ->
|
|||
status = $scope.$eval($attrs.tgKanbanArchivedStatusHeader)
|
||||
hidden = true
|
||||
|
||||
kanbanUserstoriesService.addArchivedStatus(status.id)
|
||||
kanbanUserstoriesService.hideStatus(status.id)
|
||||
|
||||
$scope.class = "icon-watch"
|
||||
$scope.title = showArchivedText
|
||||
|
||||
|
@ -342,24 +343,27 @@ KanbanArchivedStatusHeaderDirective = ($rootscope, $translate) ->
|
|||
$scope.title = showArchivedText
|
||||
$rootscope.$broadcast("kanban:hide-userstories-for-status", status.id)
|
||||
|
||||
kanbanUserstoriesService.hideStatus(status.id)
|
||||
else
|
||||
$scope.class = "icon-unwatch"
|
||||
$scope.title = hideArchivedText
|
||||
$rootscope.$broadcast("kanban:show-userstories-for-status", status.id)
|
||||
|
||||
kanbanUserstoriesService.showStatus(status.id)
|
||||
|
||||
$scope.$on "$destroy", ->
|
||||
$el.off()
|
||||
|
||||
return {link:link}
|
||||
|
||||
module.directive("tgKanbanArchivedStatusHeader", [ "$rootScope", "$translate", KanbanArchivedStatusHeaderDirective])
|
||||
module.directive("tgKanbanArchivedStatusHeader", [ "$rootScope", "$translate", "tgKanbanUserstories", KanbanArchivedStatusHeaderDirective])
|
||||
|
||||
|
||||
#############################################################################
|
||||
## Kanban Archived Status Column Intro Directive
|
||||
#############################################################################
|
||||
|
||||
KanbanArchivedStatusIntroDirective = ($translate) ->
|
||||
KanbanArchivedStatusIntroDirective = ($translate, kanbanUserstoriesService) ->
|
||||
userStories = []
|
||||
|
||||
link = ($scope, $el, $attrs) ->
|
||||
|
@ -367,105 +371,40 @@ KanbanArchivedStatusIntroDirective = ($translate) ->
|
|||
status = $scope.$eval($attrs.tgKanbanArchivedStatusIntro)
|
||||
$el.text(hiddenUserStoriexText)
|
||||
|
||||
updateIntroText = ->
|
||||
if userStories.length > 0
|
||||
updateIntroText = (hasArchived) ->
|
||||
if hasArchived
|
||||
$el.text("")
|
||||
else
|
||||
$el.text(hiddenUserStoriexText)
|
||||
|
||||
$scope.$on "kanban:us:move", (ctx, itemUs, oldStatusId, newStatusId, itemIndex) ->
|
||||
# The destination columnd is this one
|
||||
if status.id == newStatusId
|
||||
# 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()
|
||||
hasArchived = !!kanbanUserstoriesService.getStatus(newStatusId).length
|
||||
updateIntroText(hasArchived)
|
||||
|
||||
$scope.$on "kanban:shown-userstories-for-status", (ctx, statusId, userStoriesLoaded) ->
|
||||
if statusId == status.id
|
||||
userStories = _.filter(userStoriesLoaded, (us) -> us.status == status.id)
|
||||
updateIntroText()
|
||||
kanbanUserstoriesService.deleteStatus(statusId)
|
||||
kanbanUserstoriesService.add(userStoriesLoaded)
|
||||
|
||||
hasArchived = !!kanbanUserstoriesService.getStatus(statusId).length
|
||||
updateIntroText(hasArchived)
|
||||
|
||||
$scope.$on "kanban:hidden-userstories-for-status", (ctx, statusId) ->
|
||||
if statusId == status.id
|
||||
userStories = []
|
||||
updateIntroText()
|
||||
updateIntroText(false)
|
||||
|
||||
$scope.$on "$destroy", ->
|
||||
$el.off()
|
||||
|
||||
return {link:link}
|
||||
|
||||
module.directive("tgKanbanArchivedStatusIntro", ["$translate", 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])
|
||||
module.directive("tgKanbanArchivedStatusIntro", ["$translate", "tgKanbanUserstories", KanbanArchivedStatusIntroDirective])
|
||||
|
||||
#############################################################################
|
||||
## Kanban Squish Column Directive
|
||||
#############################################################################
|
||||
|
||||
KanbanSquishColumnDirective = (rs) ->
|
||||
|
||||
link = ($scope, $el, $attrs) ->
|
||||
$scope.$on "project:loaded", (event, project) ->
|
||||
$scope.folds = rs.kanban.getStatusColumnModes(project.id)
|
||||
|
@ -485,6 +424,7 @@ KanbanSquishColumnDirective = (rs) ->
|
|||
return 310
|
||||
totalWidth = _.reduce columnWidths, (total, width) ->
|
||||
return total + width
|
||||
|
||||
$el.find('.kanban-table-inner').css("width", totalWidth)
|
||||
|
||||
return {link: link}
|
||||
|
@ -502,7 +442,7 @@ KanbanWipLimitDirective = ->
|
|||
redrawWipLimit = =>
|
||||
$el.find(".kanban-wip-limit").remove()
|
||||
timeout 200, =>
|
||||
element = $el.find(".kanban-task")[status.wip_limit]
|
||||
element = $el.find("tg-card")[status.wip_limit]
|
||||
if element
|
||||
angular.element(element).before("<div class='kanban-wip-limit'></div>")
|
||||
|
||||
|
@ -518,83 +458,3 @@ KanbanWipLimitDirective = ->
|
|||
return {link: link}
|
||||
|
||||
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) ->
|
||||
link = ($scope, $el, $attrs) ->
|
||||
bindOnce $scope, "project", (project) ->
|
||||
if not (project.my_permissions.indexOf("modify_us") > -1)
|
||||
unwatch = $scope.$watch "usByStatus", (usByStatus) ->
|
||||
return if !usByStatus || !usByStatus.size
|
||||
|
||||
unwatch()
|
||||
|
||||
if not ($scope.project.my_permissions.indexOf("modify_us") > -1)
|
||||
return
|
||||
|
||||
oldParentScope = null
|
||||
|
@ -63,7 +67,7 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) ->
|
|||
copy: false,
|
||||
mirrorContainer: tdom[0],
|
||||
moves: (item) ->
|
||||
return $(item).hasClass('kanban-task')
|
||||
return $(item).is('tg-card')
|
||||
})
|
||||
|
||||
drake.on 'drag', (item) ->
|
||||
|
@ -83,7 +87,7 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) ->
|
|||
deleteElement(itemEl)
|
||||
|
||||
$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, {
|
||||
margin: 100,
|
||||
|
|
|
@ -96,7 +96,8 @@ urls = {
|
|||
"userstories": "/userstories"
|
||||
"bulk-create-us": "/userstories/bulk_create"
|
||||
"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"
|
||||
"userstories-filters": "/userstories/filters_data"
|
||||
"userstory-upvote": "/userstories/%s/upvote"
|
||||
|
@ -112,6 +113,7 @@ urls = {
|
|||
"task-downvote": "/tasks/%s/downvote"
|
||||
"task-watch": "/tasks/%s/watch"
|
||||
"task-unwatch": "/tasks/%s/unwatch"
|
||||
"task-filters": "/tasks/filters_data"
|
||||
|
||||
# Issues
|
||||
"issues": "/issues"
|
||||
|
|
|
@ -30,8 +30,6 @@ generateHash = taiga.generateHash
|
|||
resourceProvider = ($repo, $http, $urls, $storage, $q) ->
|
||||
service = {}
|
||||
hashSuffix = "issues-queryparams"
|
||||
filtersHashSuffix = "issues-filters"
|
||||
myFiltersHashSuffix = "issues-my-filters"
|
||||
|
||||
service.get = (projectId, issueId) ->
|
||||
params = service.getQueryParams(projectId)
|
||||
|
@ -95,53 +93,6 @@ resourceProvider = ($repo, $http, $urls, $storage, $q) ->
|
|||
hash = generateHash([projectId, ns])
|
||||
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) ->
|
||||
instance.issues = service
|
||||
|
||||
|
|
|
@ -32,16 +32,6 @@ resourceProvider = ($storage) ->
|
|||
hashSuffixStatusViewModes = "kanban-statusviewmodels"
|
||||
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) ->
|
||||
ns = "#{projectId}:#{hashSuffixStatusColumnModes}"
|
||||
hash = generateHash([projectId, ns])
|
||||
|
|
|
@ -38,17 +38,23 @@ resourceProvider = ($repo, $http, $urls, $storage) ->
|
|||
params.project = projectId
|
||||
return $repo.queryOne("tasks", taskId, params)
|
||||
|
||||
service.getByRef = (projectId, ref) ->
|
||||
service.getByRef = (projectId, ref, extraParams) ->
|
||||
params = service.getQueryParams(projectId)
|
||||
params.project = projectId
|
||||
params.ref = ref
|
||||
|
||||
params = _.extend({}, params, extraParams)
|
||||
|
||||
return $repo.queryOne("tasks", "by_ref", params)
|
||||
|
||||
service.listInAllProjects = (filters) ->
|
||||
return $repo.queryMany("tasks", filters)
|
||||
|
||||
service.list = (projectId, sprintId=null, userStoryId=null) ->
|
||||
params = {project: projectId}
|
||||
service.filtersData = (params) ->
|
||||
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.user_story = userStoryId if userStoryId
|
||||
service.storeQueryParams(projectId, params)
|
||||
|
|
|
@ -26,7 +26,7 @@ taiga = @.taiga
|
|||
|
||||
generateHash = taiga.generateHash
|
||||
|
||||
resourceProvider = ($repo, $http, $urls, $storage) ->
|
||||
resourceProvider = ($repo, $http, $urls, $storage, $q) ->
|
||||
service = {}
|
||||
hashSuffix = "userstories-queryparams"
|
||||
|
||||
|
@ -35,10 +35,12 @@ resourceProvider = ($repo, $http, $urls, $storage) ->
|
|||
params.project = projectId
|
||||
return $repo.queryOne("userstories", usId, params)
|
||||
|
||||
service.getByRef = (projectId, ref) ->
|
||||
service.getByRef = (projectId, ref, extraParams = {}) ->
|
||||
params = service.getQueryParams(projectId)
|
||||
params.project = projectId
|
||||
params.ref = ref
|
||||
params = _.extend({}, params, extraParams)
|
||||
|
||||
return $repo.queryOne("userstories", "by_ref", params)
|
||||
|
||||
service.listInAllProjects = (filters) ->
|
||||
|
@ -96,9 +98,9 @@ resourceProvider = ($repo, $http, $urls, $storage) ->
|
|||
params = {project_id: projectId, bulk_stories: data}
|
||||
return $http.post(url, params)
|
||||
|
||||
service.bulkUpdateSprintOrder = (projectId, data) ->
|
||||
url = $urls.resolve("bulk-update-us-sprint-order")
|
||||
params = {project_id: projectId, bulk_stories: data}
|
||||
service.bulkUpdateMilestone = (projectId, milestoneId, data) ->
|
||||
url = $urls.resolve("bulk-update-us-milestone")
|
||||
params = {project_id: projectId, milestone_id: milestoneId, bulk_stories: data}
|
||||
return $http.post(url, params)
|
||||
|
||||
service.bulkUpdateKanbanOrder = (projectId, data) ->
|
||||
|
@ -133,4 +135,4 @@ resourceProvider = ($repo, $http, $urls, $storage) ->
|
|||
instance.userstories = service
|
||||
|
||||
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()
|
||||
return
|
||||
|
||||
params = {
|
||||
include_attachments: true,
|
||||
include_tasks: true
|
||||
}
|
||||
|
||||
if $scope.isNew
|
||||
promise = $repo.create("tasks", $scope.task)
|
||||
broadcastEvent = "taskform:new:success"
|
||||
|
@ -116,20 +121,22 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
|
|||
broadcastEvent = "taskform:edit:success"
|
||||
|
||||
promise.then (data) ->
|
||||
createAttachments(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()
|
||||
.target(submitButton)
|
||||
.start()
|
||||
|
||||
# FIXME: error handling?
|
||||
promise.then (data) ->
|
||||
currentLoading.finish()
|
||||
lightboxService.close($el)
|
||||
$rootscope.$broadcast(broadcastEvent, data)
|
||||
|
||||
$el.on "submit", "form", submit
|
||||
|
||||
|
@ -139,7 +146,7 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
|
|||
return {link: link}
|
||||
|
||||
|
||||
CreateBulkTasksDirective = ($repo, $rs, $rootscope, $loading, lightboxService) ->
|
||||
CreateBulkTasksDirective = ($repo, $rs, $rootscope, $loading, lightboxService, $model) ->
|
||||
link = ($scope, $el, attrs) ->
|
||||
$scope.form = {data: "", usId: null}
|
||||
|
||||
|
@ -161,6 +168,7 @@ CreateBulkTasksDirective = ($repo, $rs, $rootscope, $loading, lightboxService) -
|
|||
|
||||
promise = $rs.tasks.bulkCreate(projectId, sprintId, usId, data)
|
||||
promise.then (result) ->
|
||||
result = _.map(result, (x) => $model.make_model('userstories', x))
|
||||
currentLoading.finish()
|
||||
$rootscope.$broadcast("taskform:bulk:success", result)
|
||||
lightboxService.close($el)
|
||||
|
@ -205,5 +213,6 @@ module.directive("tgLbCreateBulkTasks", [
|
|||
"$rootScope",
|
||||
"$tgLoading",
|
||||
"lightboxService",
|
||||
"$tgModel",
|
||||
CreateBulkTasksDirective
|
||||
])
|
||||
|
|
|
@ -38,13 +38,14 @@ module = angular.module("taigaTaskboard")
|
|||
## Taskboard Controller
|
||||
#############################################################################
|
||||
|
||||
class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
|
||||
class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin)
|
||||
@.$inject = [
|
||||
"$scope",
|
||||
"$rootScope",
|
||||
"$tgRepo",
|
||||
"$tgConfirm",
|
||||
"$tgResources",
|
||||
"tgResources"
|
||||
"$routeParams",
|
||||
"$q",
|
||||
"tgAppMetaService",
|
||||
|
@ -53,12 +54,20 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
|
|||
"$tgEvents"
|
||||
"$tgAnalytics",
|
||||
"$translate",
|
||||
"tgErrorHandlingService"
|
||||
"tgErrorHandlingService",
|
||||
"tgTaskboardTasks",
|
||||
"$tgStorage",
|
||||
"tgFilterRemoteStorageService"
|
||||
]
|
||||
|
||||
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @appMetaService, @location, @navUrls,
|
||||
@events, @analytics, @translate, @errorHandlingService) ->
|
||||
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @rs2, @params, @q, @appMetaService, @location, @navUrls,
|
||||
@events, @analytics, @translate, @errorHandlingService, @taskboardTasksService, @storage, @filterRemoteStorageService) ->
|
||||
bindMethods(@)
|
||||
@taskboardTasksService.reset()
|
||||
@scope.userstories = []
|
||||
@.openFilter = false
|
||||
|
||||
return if @.applyStoredFilters(@params.pslug, "tasks-filters")
|
||||
|
||||
@scope.sectionName = @translate.instant("TASKBOARD.SECTION_NAME")
|
||||
@.initializeEventHandlers()
|
||||
|
@ -70,6 +79,150 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
|
|||
# On Error
|
||||
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: ->
|
||||
prettyDate = @translate.instant("BACKLOG.SPRINTS.DATE")
|
||||
|
||||
|
@ -92,24 +245,33 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
|
|||
@appMetaService.setAll(title, description)
|
||||
|
||||
initializeEventHandlers: ->
|
||||
# TODO: Reload entire taskboard after create/edit tasks seems
|
||||
# a big overhead. It should be optimized in near future.
|
||||
@scope.$on "taskform:bulk:success", =>
|
||||
@.loadTaskboard()
|
||||
@scope.$on "taskform:bulk:success", (event, tasks) =>
|
||||
@.refreshTagsColors().then () =>
|
||||
@taskboardTasksService.add(tasks)
|
||||
|
||||
@analytics.trackEvent("task", "create", "bulk create task on taskboard", 1)
|
||||
|
||||
@scope.$on "taskform:new:success", =>
|
||||
@.loadTaskboard()
|
||||
@scope.$on "taskform:new:success", (event, task) =>
|
||||
@.refreshTagsColors().then () =>
|
||||
@taskboardTasksService.add(task)
|
||||
|
||||
@analytics.trackEvent("task", "create", "create task on taskboard", 1)
|
||||
|
||||
@scope.$on("taskform:edit:success", => @.loadTaskboard())
|
||||
@scope.$on("taskboard:task:move", @.taskMove)
|
||||
@scope.$on "taskform:edit:success", (event, task) =>
|
||||
@.refreshTagsColors().then () =>
|
||||
@taskboardTasksService.replaceModel(task)
|
||||
|
||||
@scope.$on "assigned-to:added", (ctx, userId, task) =>
|
||||
task.assigned_to = userId
|
||||
promise = @repo.save(task)
|
||||
promise.then null, ->
|
||||
console.log "FAIL" # TODO
|
||||
@scope.$on("taskboard:task:move", @.taskMove)
|
||||
@scope.$on("assigned-to:added", @.onAssignedToChanged)
|
||||
|
||||
onAssignedToChanged: (ctx, userid, taskModel) ->
|
||||
taskModel.assigned_to = userid
|
||||
|
||||
@taskboardTasksService.replaceModel(taskModel)
|
||||
|
||||
promise = @repo.save(taskModel)
|
||||
promise.then null, ->
|
||||
console.log "FAIL" # TODO
|
||||
|
||||
initializeSubscription: ->
|
||||
routingKey = "changes.project.#{@scope.projectId}.tasks"
|
||||
|
@ -130,7 +292,6 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
|
|||
@scope.project = project
|
||||
# Not used at this momment
|
||||
@scope.pointsList = _.sortBy(project.points, "order")
|
||||
# @scope.roleList = _.sortBy(project.roles, "order")
|
||||
@scope.pointsById = groupBy(project.points, (e) -> e.id)
|
||||
@scope.roleById = groupBy(project.roles, (e) -> e.id)
|
||||
@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) =>
|
||||
@scope.sprint = sprint
|
||||
@scope.userstories = _.sortBy(sprint.user_stories, "sprint_order")
|
||||
|
||||
@taskboardTasksService.setUserstories(@scope.userstories)
|
||||
|
||||
return sprint
|
||||
|
||||
loadTasks: ->
|
||||
return @rs.tasks.list(@scope.projectId, @scope.sprintId).then (tasks) =>
|
||||
@scope.tasks = _.sortBy(tasks, 'taskboard_order')
|
||||
@scope.usTasks = {}
|
||||
params = {
|
||||
include_attachments: true,
|
||||
include_tasks: true
|
||||
}
|
||||
|
||||
# Iterate over all userstories and
|
||||
# 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] = []
|
||||
params = _.merge params, @location.search()
|
||||
|
||||
for task in @scope.tasks
|
||||
if @scope.usTasks[task.user_story]? and @scope.usTasks[task.user_story][task.status]?
|
||||
@scope.usTasks[task.user_story][task.status].push(task)
|
||||
|
||||
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
|
||||
return @rs.tasks.list(@scope.projectId, @scope.sprintId, null, params).then (tasks) =>
|
||||
@taskboardTasksService.init(@scope.project, @scope.usersById)
|
||||
@taskboardTasksService.set(tasks)
|
||||
|
||||
loadTaskboard: ->
|
||||
return @q.all([
|
||||
|
@ -219,59 +368,69 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
|
|||
return data
|
||||
|
||||
return promise.then(=> @.loadProject())
|
||||
.then(=> @.loadTaskboard())
|
||||
.then(=> @.setRolePoints())
|
||||
.then =>
|
||||
@.generateFilters()
|
||||
|
||||
refreshTasksOrder: (tasks) ->
|
||||
items = @.resortTasks(tasks)
|
||||
data = @.prepareBulkUpdateData(items)
|
||||
return @.loadTaskboard().then(=> @.setRolePoints())
|
||||
|
||||
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) ->
|
||||
items = []
|
||||
return false
|
||||
|
||||
for item, index in tasks
|
||||
item["taskboard_order"] = index
|
||||
if item.isModified()
|
||||
items.push(item)
|
||||
editTask: (id) ->
|
||||
task = @.taskboardTasksService.getTask(id)
|
||||
|
||||
return items
|
||||
task = task.set('loading', true)
|
||||
@taskboardTasksService.replace(task)
|
||||
|
||||
prepareBulkUpdateData: (uses) ->
|
||||
return _.map(uses, (x) -> {"task_id": x.id, "order": x["taskboard_order"]})
|
||||
@rs.tasks.getByRef(task.getIn(['model', 'project']), task.getIn(['model', 'ref'])).then (editingTask) =>
|
||||
@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) ->
|
||||
# Remove task from old position
|
||||
r = @scope.usTasks[task.user_story][task.status].indexOf(task)
|
||||
@scope.usTasks[task.user_story][task.status].splice(r, 1)
|
||||
taskMove: (ctx, task, oldStatusId, usId, statusId, order) ->
|
||||
task = @taskboardTasksService.getTaskModel(task.get('id'))
|
||||
|
||||
# Add task to new position
|
||||
tasks = @scope.usTasks[usId][statusId]
|
||||
tasks.splice(order, 0, task)
|
||||
moveUpdateData = @taskboardTasksService.move(task.id, usId, statusId, order)
|
||||
|
||||
task.user_story = usId
|
||||
task.status = statusId
|
||||
task.taskboard_order = order
|
||||
params = {
|
||||
status__is_archived: false,
|
||||
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()
|
||||
|
||||
promise.then null, =>
|
||||
console.log "FAIL TASK SAVE"
|
||||
|
||||
## Template actions
|
||||
addNewTask: (type, us) ->
|
||||
switch type
|
||||
when "standard" then @rootscope.$broadcast("taskform:new", @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)
|
||||
|
||||
setRolePoints: () ->
|
||||
|
@ -331,43 +490,6 @@ TaskboardDirective = ($rootscope) ->
|
|||
|
||||
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
|
||||
#############################################################################
|
||||
|
@ -377,14 +499,18 @@ TaskboardSquishColumnDirective = (rs) ->
|
|||
maxColumnWidth = 300
|
||||
|
||||
link = ($scope, $el, $attrs) ->
|
||||
$scope.$on "sprint:zoom0", () =>
|
||||
recalculateTaskboardWidth()
|
||||
|
||||
$scope.$on "sprint:task:moved", () =>
|
||||
recalculateTaskboardWidth()
|
||||
|
||||
bindOnce $scope, "usTasks", (project) ->
|
||||
$scope.statusesFolded = rs.tasks.getStatusColumnModes($scope.project.id)
|
||||
$scope.usFolded = rs.tasks.getUsRowModes($scope.project.id, $scope.sprintId)
|
||||
$scope.$watch "usTasks", () ->
|
||||
if $scope.project
|
||||
$scope.statusesFolded = rs.tasks.getStatusColumnModes($scope.project.id)
|
||||
$scope.usFolded = rs.tasks.getUsRowModes($scope.project.id, $scope.sprintId)
|
||||
|
||||
recalculateTaskboardWidth()
|
||||
recalculateTaskboardWidth()
|
||||
|
||||
$scope.foldStatus = (status) ->
|
||||
$scope.statusesFolded[status.id] = !!!$scope.statusesFolded[status.id]
|
||||
|
@ -403,7 +529,10 @@ TaskboardSquishColumnDirective = (rs) ->
|
|||
recalculateTaskboardWidth()
|
||||
|
||||
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 tasks and $scope.usFolded[usId]
|
||||
|
@ -422,7 +551,10 @@ TaskboardSquishColumnDirective = (rs) ->
|
|||
if width
|
||||
column.css('max-width', width)
|
||||
else
|
||||
column.css("max-width", maxColumnWidth)
|
||||
if $scope.ctrl.zoomLevel == '0'
|
||||
column.css("max-width", 148)
|
||||
else
|
||||
column.css("max-width", maxColumnWidth)
|
||||
|
||||
refreshTaskboardTableWidth = () =>
|
||||
columnWidths = []
|
||||
|
@ -458,67 +590,3 @@ TaskboardSquishColumnDirective = (rs) ->
|
|||
return {link: link}
|
||||
|
||||
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
|
||||
#############################################################################
|
||||
|
||||
TaskboardSortableDirective = ($repo, $rs, $rootscope) ->
|
||||
TaskboardSortableDirective = ($repo, $rs, $rootscope, $translate) ->
|
||||
link = ($scope, $el, $attrs) ->
|
||||
bindOnce $scope, "tasks", (xx) ->
|
||||
# If the user has not enough permissions we don't enable the sortable
|
||||
if not ($scope.project.my_permissions.indexOf("modify_us") > -1)
|
||||
unwatch = $scope.$watch "usTasks", (usTasks) ->
|
||||
return if !usTasks || !usTasks.size
|
||||
|
||||
unwatch()
|
||||
|
||||
if not ($scope.project.my_permissions.indexOf("modify_task") > -1)
|
||||
return
|
||||
|
||||
oldParentScope = null
|
||||
|
@ -49,6 +52,10 @@ TaskboardSortableDirective = ($repo, $rs, $rootscope) ->
|
|||
itemEl = null
|
||||
tdom = $el
|
||||
|
||||
filterError = ->
|
||||
text = $translate.instant("BACKLOG.SORTABLE_FILTER_ERROR")
|
||||
$tgConfirm.notify("error", text)
|
||||
|
||||
deleteElement = (itemEl) ->
|
||||
# Completelly remove item and its scope from dom
|
||||
itemEl.scope().$destroy()
|
||||
|
@ -63,12 +70,22 @@ TaskboardSortableDirective = ($repo, $rs, $rootscope) ->
|
|||
copy: false,
|
||||
mirrorContainer: $el[0],
|
||||
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) ->
|
||||
oldParentScope = $(item).parent().scope()
|
||||
|
||||
if $el.hasClass("active-filters")
|
||||
filterError()
|
||||
|
||||
setTimeout (() ->
|
||||
drake.cancel(true)
|
||||
), 0
|
||||
|
||||
return false
|
||||
|
||||
drake.on 'dragend', (item) ->
|
||||
parentEl = $(item).parent()
|
||||
itemEl = $(item)
|
||||
|
@ -85,7 +102,7 @@ TaskboardSortableDirective = ($repo, $rs, $rootscope) ->
|
|||
deleteElement(itemEl)
|
||||
|
||||
$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]], {
|
||||
|
@ -107,5 +124,6 @@ module.directive("tgTaskboardSortable", [
|
|||
"$tgRepo",
|
||||
"$tgResources",
|
||||
"$rootScope",
|
||||
"$translate",
|
||||
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 = []
|
||||
|
||||
_.forIn object, (value, key) =>
|
||||
if key not in dependencies
|
||||
if key not in dependencies && _.isFunction(value)
|
||||
methods.push(key)
|
||||
|
||||
_.bindAll(object, methods)
|
||||
|
|
|
@ -45,6 +45,10 @@
|
|||
"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_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": {
|
||||
"DEFAULT_MESSAGE": "This value seems to be invalid.",
|
||||
"TYPE_EMAIL": "This value should be a valid email.",
|
||||
|
@ -196,9 +200,25 @@
|
|||
"TITLE": "filters",
|
||||
"INPUT_PLACEHOLDER": "Subject or reference",
|
||||
"TITLE_ACTION_FILTER_BUTTON": "search",
|
||||
"BREADCRUMB_TITLE": "back to categories",
|
||||
"BREADCRUMB_FILTERS": "Filters",
|
||||
"BREADCRUMB_STATUS": "status"
|
||||
"TITLE": "Filters",
|
||||
"INPUT_SEARCH_PLACEHOLDER": "Subject or ref",
|
||||
"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": {
|
||||
"H1_BUTTON": "First Level Heading",
|
||||
|
@ -1169,9 +1189,7 @@
|
|||
"TITLE": "Filters",
|
||||
"REMOVE": "Remove Filters",
|
||||
"HIDE": "Hide Filters",
|
||||
"SHOW": "Show Filters",
|
||||
"FILTER_CATEGORY_STATUS": "Status",
|
||||
"FILTER_CATEGORY_TAGS": "Tags"
|
||||
"SHOW": "Show Filters"
|
||||
},
|
||||
"SPRINTS": {
|
||||
"TITLE": "SPRINTS",
|
||||
|
@ -1278,7 +1296,6 @@
|
|||
"SECTION_NAME": "Issue",
|
||||
"ACTION_NEW_ISSUE": "+ NEW ISSUE",
|
||||
"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:",
|
||||
"EXTERNAL_REFERENCE": "This issue has been created from",
|
||||
"GO_TO_EXTERNAL_REFERENCE": "Go to origin",
|
||||
|
@ -1296,28 +1313,6 @@
|
|||
"TITLE": "Promote this issue to a new user story",
|
||||
"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": {
|
||||
"COLUMNS": {
|
||||
"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')
|
||||
steps.push({
|
||||
element: '.icon-plus',
|
||||
element: '.add-action',
|
||||
position: 'bottom',
|
||||
joyride: {
|
||||
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",
|
||||
ng-init="section='backlog'")
|
||||
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
|
||||
include ../includes/components/mainTitle
|
||||
|
||||
|
@ -39,13 +52,20 @@ div.wrapper(tg-backlog, ng-controller="BacklogController as ctrl",
|
|||
)
|
||||
tg-svg(svg-icon="icon-move")
|
||||
span.text(translate="BACKLOG.MOVE_US_TO_LATEST_SPRINT")
|
||||
a.trans-button(
|
||||
ng-if="userstories.length"
|
||||
a.trans-button.e2e-open-filter(
|
||||
ng-if="!ctrl.activeFilters"
|
||||
href=""
|
||||
title="{{'BACKLOG.FILTERS.TOGGLE' | translate}}"
|
||||
id="show-filters-button"
|
||||
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(
|
||||
ng-if="userstories.length"
|
||||
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}")
|
||||
div.row.title
|
||||
div.level-field(data-fieldname="type", translate="ISSUES.TABLE.COLUMNS.TYPE")
|
||||
div.level-field(data-fieldname="severity", translate="ISSUES.TABLE.COLUMNS.SEVERITY")
|
||||
div.level-field(data-fieldname="priority", translate="ISSUES.TABLE.COLUMNS.PRIORITY")
|
||||
div.votes(data-fieldname="total_voters", translate="ISSUES.TABLE.COLUMNS.VOTES")
|
||||
div.subject(data-fieldname="subject", translate="ISSUES.TABLE.COLUMNS.SUBJECT")
|
||||
div.issue-field(data-fieldname="status", translate="ISSUES.TABLE.COLUMNS.STATUS")
|
||||
div.created-field(data-fieldname="created_date", translate="ISSUES.TABLE.COLUMNS.CREATED")
|
||||
div.assigned-field(data-fieldname="assigned_to", translate="ISSUES.TABLE.COLUMNS.ASSIGNED_TO")
|
||||
div.level-field(data-fieldname="type")
|
||||
| {{"ISSUES.TABLE.COLUMNS.TYPE" | translate}}
|
||||
div.level-field(data-fieldname="severity")
|
||||
| {{"ISSUES.TABLE.COLUMNS.SEVERITY" | translate}}
|
||||
div.level-field(data-fieldname="priority")
|
||||
| {{"ISSUES.TABLE.COLUMNS.PRIORITY" | translate}}
|
||||
div.votes(data-fieldname="total_voters")
|
||||
| {{"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(
|
||||
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-inner
|
||||
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]}',
|
||||
tg-class-permission="{'readonly': '!modify_task'}")
|
||||
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]}'
|
||||
)
|
||||
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(
|
||||
href=""
|
||||
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-inner
|
||||
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-column-height-fixer,
|
||||
tg-bind-scope
|
||||
)
|
||||
div.kanban-task(
|
||||
ng-repeat="us in usByStatus[s.id] track by us.id",
|
||||
tg-kanban-userstory,
|
||||
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}}"
|
||||
.card-placeholder(
|
||||
ng-if="ctrl.showPlaceHolder(s.id)"
|
||||
ng-include="'common/components/kanban-placeholder.html'"
|
||||
)
|
||||
|
||||
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")
|
||||
|
|
|
@ -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-inner
|
||||
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")
|
||||
|
||||
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")
|
||||
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(
|
||||
ng-repeat="task in usTasks[us.id][st.id] track by task.id"
|
||||
tg-bind-scope
|
||||
tg-class-permission="{'readonly': '!modify_task'}"
|
||||
ng-class="{'card-placeholder': task.isPlaceholder}"
|
||||
|
||||
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
|
||||
)
|
||||
.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.taskboard-userstory-box.task-column
|
||||
a.vfold(
|
||||
|
@ -82,15 +103,29 @@ div.taskboard-table(tg-taskboard-squish-column, tg-taskboard-sortable)
|
|||
h3.us-title
|
||||
span(translate="TASKBOARD.TABLE.ROW_UNASSIGED_TASKS_TITLE")
|
||||
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")
|
||||
include ../../common/components/taskboard-placeholder
|
||||
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
|
||||
)
|
||||
.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'"
|
||||
)
|
||||
tg-project-menu
|
||||
sidebar.menu-secondary.extrabar.filters-bar(tg-issues-filters)
|
||||
include ../includes/modules/issues-filters
|
||||
sidebar.filters-bar
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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'")
|
||||
tg-project-menu
|
||||
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
|
||||
span(tg-bo-bind="project.name", class="project-name-short")
|
||||
span.green(tg-bo-bind="sprint.name")
|
||||
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
|
||||
|
||||
div.graphics-container
|
||||
|
|
|
@ -155,3 +155,14 @@ a.button-gray {
|
|||
display: inline-block;
|
||||
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;
|
||||
padding: 1rem;
|
||||
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 {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
$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 {
|
||||
background: $mass-white;
|
||||
color: $blackish;
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
.issues {
|
||||
.filters-bar {
|
||||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
width: 260px;
|
||||
}
|
||||
.filters-inner {
|
||||
opacity: 1;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
height: $main-height;
|
||||
max-height: $main-height;
|
||||
max-width: calc(100vw - 50px);
|
||||
position: relative;
|
||||
header {
|
||||
min-height: 70px;
|
||||
}
|
||||
|
@ -14,3 +15,12 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.options {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
.taskboard {
|
||||
height: $main-height;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
h1,
|
||||
.graphics-container,
|
||||
.summary {
|
||||
|
@ -11,6 +12,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.taskboard-header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.taskboard-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -3,27 +3,29 @@
|
|||
$column-width: 300px;
|
||||
$column-flex: 1;
|
||||
$column-shrink: 0;
|
||||
$column-margin: 0 10px 0 0;
|
||||
$column-margin: 0 5px 0 0;
|
||||
$column-padding: .5rem 1rem;
|
||||
|
||||
@mixin fold {
|
||||
.taskboard-task {
|
||||
background: none;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
min-height: 0;
|
||||
.taskboard-task-inner {
|
||||
padding: .1rem;
|
||||
}
|
||||
.taskboard-tagline,
|
||||
.taskboard-text {
|
||||
.card {
|
||||
align-self: flex-start;
|
||||
margin-top: .5rem;
|
||||
tg-card-slideshow,
|
||||
.card-unfold,
|
||||
.card-tag,
|
||||
.card-title,
|
||||
.card-owner-actions,
|
||||
.card-data,
|
||||
.card-statistics,
|
||||
.card-owner-name {
|
||||
display: none;
|
||||
}
|
||||
.avatar {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
}
|
||||
.icon {
|
||||
display: none;
|
||||
.card-owner {
|
||||
img {
|
||||
height: 1.3rem;
|
||||
margin-right: 0;
|
||||
width: 1.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.task-column,
|
||||
|
@ -44,25 +46,20 @@ $column-margin: 0 10px 0 0;
|
|||
.taskboard-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
.taskboard-task {
|
||||
&.readonly {
|
||||
cursor: auto;
|
||||
}
|
||||
&.gu-mirror {
|
||||
opacity: 1;
|
||||
.avatar-task-link {
|
||||
display: none;
|
||||
}
|
||||
&.zoom-0 {
|
||||
.task-colum-name span {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.taskboard-table-header {
|
||||
margin-bottom: .5rem;
|
||||
min-height: 40px;
|
||||
flex-basis: 38px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
min-height: 38px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
.taskboard-table-inner {
|
||||
|
@ -83,7 +80,7 @@ $column-margin: 0 10px 0 0;
|
|||
justify-content: space-between;
|
||||
margin: $column-margin;
|
||||
max-width: $column-width;
|
||||
padding: .5rem 1rem;
|
||||
padding: $column-padding;
|
||||
position: relative;
|
||||
text-transform: uppercase;
|
||||
width: $column-width;
|
||||
|
@ -102,6 +99,9 @@ $column-margin: 0 10px 0 0;
|
|||
margin: 0;
|
||||
}
|
||||
}
|
||||
span {
|
||||
@include ellipsis(65%);
|
||||
}
|
||||
}
|
||||
tg-svg {
|
||||
display: block;
|
||||
|
@ -128,7 +128,8 @@ $column-margin: 0 10px 0 0;
|
|||
}
|
||||
|
||||
.taskboard-table-body {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
margin-bottom: 5rem;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
.task-column {
|
||||
|
@ -147,14 +148,10 @@ $column-margin: 0 10px 0 0;
|
|||
}
|
||||
.column-fold {
|
||||
@include fold;
|
||||
.taskboard-task {
|
||||
max-width: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
.task-row {
|
||||
display: flex;
|
||||
margin-bottom: .5rem;
|
||||
margin-bottom: .25rem;
|
||||
min-height: 10rem;
|
||||
width: 100%;
|
||||
&.blocked {
|
||||
|
@ -167,6 +164,7 @@ $column-margin: 0 10px 0 0;
|
|||
.points-value,
|
||||
.points-value:hover {
|
||||
color: $white;
|
||||
fill: $white;
|
||||
transition: color .3s linear;
|
||||
}
|
||||
.taskboard-tasks-box {
|
||||
|
@ -185,18 +183,26 @@ $column-margin: 0 10px 0 0;
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.taskboard-userstory-box {
|
||||
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 {
|
||||
display: block;
|
||||
}
|
||||
.icon {
|
||||
transition: fill .2s linear;
|
||||
.points-value {
|
||||
@include font-size(small);
|
||||
color: $gray-light;
|
||||
span {
|
||||
margin-right: .1rem;
|
||||
}
|
||||
}
|
||||
tg-svg {
|
||||
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
|
||||
|
||||
$column-width: 300px;
|
||||
$column-width: 296px;
|
||||
$column-folded-width: 30px;
|
||||
$column-flex: 0;
|
||||
$column-shrink: 0;
|
||||
$column-margin: 0 10px 0 0;
|
||||
$column-margin: 0 5px 0 0;
|
||||
$column-padding: .5rem 1rem;
|
||||
|
||||
.kanban-table {
|
||||
display: flex;
|
||||
|
@ -12,7 +13,19 @@ $column-margin: 0 10px 0 0;
|
|||
height: 100%;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
&.zoom-0 {
|
||||
.task-column,
|
||||
.task-colum-name {
|
||||
max-width: $column-width / 2;
|
||||
}
|
||||
.task-colum-name span {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
.vfold {
|
||||
tg-card {
|
||||
display: none;
|
||||
}
|
||||
&.task-colum-name {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
@ -36,9 +49,6 @@ $column-margin: 0 10px 0 0;
|
|||
min-width: $column-folded-width;
|
||||
width: $column-folded-width;
|
||||
}
|
||||
.kanban-task {
|
||||
display: none;
|
||||
}
|
||||
.kanban-column-intro {
|
||||
display: none;
|
||||
}
|
||||
|
@ -46,11 +56,11 @@ $column-margin: 0 10px 0 0;
|
|||
.readonly {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.kanban-table-header {
|
||||
margin-bottom: .5rem;
|
||||
min-height: 40px;
|
||||
min-height: 38px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
.kanban-table-inner {
|
||||
|
@ -58,6 +68,9 @@ $column-margin: 0 10px 0 0;
|
|||
overflow: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
.options {
|
||||
display: flex;
|
||||
}
|
||||
.task-colum-name {
|
||||
@include font-size(medium);
|
||||
align-items: center;
|
||||
|
@ -71,7 +84,7 @@ $column-margin: 0 10px 0 0;
|
|||
justify-content: space-between;
|
||||
margin: $column-margin;
|
||||
max-width: $column-width;
|
||||
padding: .5rem .5rem .5rem 1rem;
|
||||
padding: $column-padding;
|
||||
position: relative;
|
||||
text-transform: uppercase;
|
||||
&:last-child {
|
||||
|
@ -110,6 +123,7 @@ $column-margin: 0 10px 0 0;
|
|||
max-width: $column-width;
|
||||
overflow-y: auto;
|
||||
widows: $column-width;
|
||||
width: $column-width;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
|
|
@ -28,14 +28,8 @@ a[ng-click] svg {
|
|||
}
|
||||
|
||||
// chrome url break
|
||||
.kanban-task {
|
||||
.task-name {
|
||||
tg-card {
|
||||
.card-title span:last-child {
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.taskboard-task {
|
||||
.task-name {
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
|
@ -429,5 +429,14 @@
|
|||
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>
|
||||
</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>
|
||||
</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;
|
||||
};
|
||||
|
||||
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) {
|
||||
return helper.getColumns().$$('.task-name').getText();
|
||||
return helper.getColumns().$$('.e2e-title').getText();
|
||||
};
|
||||
|
||||
helper.getBoxUss = function(column) {
|
||||
return helper.getColumns().get(column).$$('.kanban-task');
|
||||
return helper.getColumns().get(column).$$('tg-card');
|
||||
};
|
||||
|
||||
helper.editUs = function(column, us) {
|
||||
helper.getColumns().get(column).$$('.edit-us').get(us).click();
|
||||
helper.getUss = function() {
|
||||
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) {
|
||||
|
@ -59,5 +75,13 @@ helper.scrollRight = 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) {
|
||||
let box = helper.getBox(row, column);
|
||||
|
||||
return box.$$('.taskboard-task');
|
||||
return box.$$('tg-card');
|
||||
};
|
||||
|
||||
helper.getTasks = function() {
|
||||
return $$('tg-card');
|
||||
};
|
||||
|
||||
helper.openNewTaskLb = function(row) {
|
||||
|
@ -52,8 +56,20 @@ helper.unFoldColumn = function(row) {
|
|||
icon.click();
|
||||
};
|
||||
|
||||
helper.editTask = function(row, column, task) {
|
||||
helper.getBoxTasks(row, column).get(task).$('.edit-task').click();
|
||||
helper.editTask = async function(row, column, task) {
|
||||
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() {
|
||||
|
@ -114,5 +130,13 @@ helper.getBulkCreateTask = 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 chaiAsPromised = require('chai-as-promised');
|
||||
|
||||
var sharedFilters = require('../shared/filters');
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
var expect = chai.expect;
|
||||
|
||||
|
@ -243,7 +245,7 @@ describe('backlog', function() {
|
|||
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 initUssSprintCount = await backlogHelper.getSprintUsertories(sprint).count();
|
||||
|
||||
|
@ -453,143 +455,9 @@ describe('backlog', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('filters', function() {
|
||||
it('show filters', async function() {
|
||||
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('backlog filters', sharedFilters.bind(this, 'backlog', () => {
|
||||
return backlogHelper.userStories().count();
|
||||
}));
|
||||
|
||||
describe('closed sprints', function() {
|
||||
async function createEmptyMilestone() {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
var utils = require('../../utils');
|
||||
var issuesHelper = require('../../helpers').issues;
|
||||
var commonHelper = require('../../helpers').common;
|
||||
var sharedFilters = require('../../shared/filters');
|
||||
|
||||
var chai = require('chai');
|
||||
var chaiAsPromised = require('chai-as-promised');
|
||||
|
@ -126,195 +127,7 @@ describe('issues list', function() {
|
|||
expect(issueUserName).to.be.equal(newUserName);
|
||||
});
|
||||
|
||||
describe('filters', function() {
|
||||
it('by ref', 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 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);
|
||||
});
|
||||
});
|
||||
describe('issues filters', sharedFilters.bind(this, 'issues', () => {
|
||||
return issuesHelper.getIssues().count();
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ var utils = require('../utils');
|
|||
var kanbanHelper = require('../helpers').kanban;
|
||||
var backlogHelper = require('../helpers').backlog;
|
||||
var commonHelper = require('../helpers').common;
|
||||
var filterHelper = require('../helpers/filters-helper');
|
||||
|
||||
var chai = require('chai');
|
||||
var chaiAsPromised = require('chai-as-promised');
|
||||
|
@ -18,6 +19,24 @@ describe('kanban', function() {
|
|||
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() {
|
||||
let createUSLightbox = null;
|
||||
let formFields = {};
|
||||
|
@ -148,7 +167,6 @@ describe('kanban', function() {
|
|||
await utils.lightbox.close(createUSLightbox.el);
|
||||
|
||||
let ussTitles = await kanbanHelper.getColumnUssTitles(0);
|
||||
|
||||
let findSubject = ussTitles.indexOf(formFields.subject) !== -1;
|
||||
|
||||
expect(findSubject).to.be.true;
|
||||
|
@ -297,8 +315,12 @@ describe('kanban', function() {
|
|||
|
||||
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);
|
||||
});
|
||||
|
||||
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 taskboardHelper = require('../../helpers').taskboard;
|
||||
var commonHelper = require('../../helpers').common;
|
||||
var filterHelper = require('../../helpers/filters-helper');
|
||||
var sharedFilters = require('../../shared/filters');
|
||||
|
||||
var chai = require('chai');
|
||||
var chaiAsPromised = require('chai-as-promised');
|
||||
|
@ -21,6 +23,24 @@ describe('taskboard', function() {
|
|||
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() {
|
||||
let createTaskLightbox = null;
|
||||
let formFields = {};
|
||||
|
@ -65,7 +85,7 @@ describe('taskboard', function() {
|
|||
|
||||
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;
|
||||
|
||||
|
@ -111,7 +131,7 @@ describe('taskboard', function() {
|
|||
|
||||
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;
|
||||
|
||||
|
@ -296,4 +316,8 @@ describe('taskboard', function() {
|
|||
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)
|
||||
.pipe(gulpif(!isDeploy, cache(csslint("csslintrc.json"), {
|
||||
success: function(csslintFile) {
|
||||
return csslintFile.csslint.success;
|
||||
if (csslintFile.csslint) {
|
||||
return csslintFile.csslint.success;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
value: function(csslintFile) {
|
||||
return {
|
||||
|
|
Loading…
Reference in New Issue