kanban drag multiple

stable
Jesús Espino 2017-11-08 09:38:01 +01:00 committed by Alex Hermida
parent 6fe84f9acd
commit 2cd47a578c
13 changed files with 198 additions and 68 deletions

View File

@ -68,7 +68,7 @@ BacklogSortableDirective = () ->
window.dragMultiple.start(item, container) window.dragMultiple.start(item, container)
drake.on 'cloned', (item) -> drake.on 'cloned', (item) ->
$(item).addClass('backlog-us-mirror') $(item).addClass('multiple-drag-mirror')
drake.on 'dragend', (item) -> drake.on 'dragend', (item) ->
parent = $(item).parent() parent = $(item).parent()

View File

@ -251,8 +251,6 @@ Qqueue = ($q) ->
bindAdd: (fn) => bindAdd: (fn) =>
return (args...) => return (args...) =>
lastPromise = lastPromise.then () => fn.apply(@, args) lastPromise = lastPromise.then () => fn.apply(@, args)
return qqueue
add: (fn) => add: (fn) =>
if !lastPromise if !lastPromise
lastPromise = fn() lastPromise = fn()

View File

@ -93,15 +93,17 @@ class KanbanUserstoriesService extends taiga.Service
@.refresh() @.refresh()
move: (id, statusId, index) -> move: (usList, statusId, index) ->
us = @.getUsModel(id) initialLength = usList.length
usByStatus = _.filter @.userstoriesRaw, (it) => usByStatus = _.filter @.userstoriesRaw, (it) =>
return it.status == statusId return it.status == statusId
usByStatus = _.sortBy usByStatus, (it) => @.order[it.id] usByStatus = _.sortBy usByStatus, (it) => @.order[it.id]
usByStatusWithoutMoved = _.filter usByStatus, (it) => it.id != id usByStatusWithoutMoved = _.filter usByStatus, (listIt) ->
return !_.find usList, (moveIt) -> return listIt.id == moveIt.id
beforeDestination = _.slice(usByStatusWithoutMoved, 0, index) beforeDestination = _.slice(usByStatusWithoutMoved, 0, index)
afterDestination = _.slice(usByStatusWithoutMoved, index) afterDestination = _.slice(usByStatusWithoutMoved, index)
@ -112,26 +114,54 @@ class KanbanUserstoriesService extends taiga.Service
previousWithTheSameOrder = _.filter beforeDestination, (it) => previousWithTheSameOrder = _.filter beforeDestination, (it) =>
@.order[it.id] == @.order[previous.id] @.order[it.id] == @.order[previous.id]
if previousWithTheSameOrder.length > 1 if previousWithTheSameOrder.length > 1
for it in previousWithTheSameOrder for it in previousWithTheSameOrder
setOrders[it.id] = @.order[it.id] setOrders[it.id] = @.order[it.id]
if !previous and (!afterDestination or afterDestination.length == 0) modifiedUs = []
@.order[us.id] = 0 setPreviousOrders = []
else if !previous and afterDestination and afterDestination.length > 0 setNextOrders = []
@.order[us.id] = @.order[afterDestination[0].id] - 1
if !previous
startIndex = 0
else if previous else if previous
@.order[us.id] = @.order[previous.id] + 1 startIndex = @.order[previous.id] + 1
for it, key in afterDestination previousWithTheSameOrder = _.filter(beforeDestination, (it) =>
@.order[it.id] = @.order[us.id] + key + 1 it.kanban_order == @.order[previous.id]
)
for it, key in afterDestination # increase position of the us after the dragged us's
@.order[it.id] = @.order[previous.id] + key + initialLength + 1
it.kanban_order = @.order[it.id]
setNextOrders = _.map(afterDestination, (it) =>
{us_id: it.id, order: @.order[it.id]}
)
# 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: @.order[it.id]}
)
for us, key in usList
us.status = statusId us.status = statusId
us.kanban_order = @.order[us.id] us.kanban_order = startIndex + key
@.order[us.id] = us.kanban_order
modifiedUs.push({us_id: us.id, order: us.kanban_order})
@.refresh() @.refresh()
return {"us_id": us.id, "order": @.order[us.id], "set_orders": setOrders} return {
bulkOrders: modifiedUs.concat(setPreviousOrders, setNextOrders),
usList: modifiedUs,
set_orders: setOrders
}
moveToEnd: (id, statusId) -> moveToEnd: (id, statusId) ->
us = @.getUsModel(id) us = @.getUsModel(id)

View File

@ -71,6 +71,7 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
bindMethods(@) bindMethods(@)
@kanbanUserstoriesService.reset() @kanbanUserstoriesService.reset()
@.openFilter = false @.openFilter = false
@.selectedUss = {}
return if @.applyStoredFilters(@params.pslug, "kanban-filters") return if @.applyStoredFilters(@params.pslug, "kanban-filters")
@ -80,6 +81,13 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
taiga.defineImmutableProperty @.scope, "usByStatus", () => taiga.defineImmutableProperty @.scope, "usByStatus", () =>
return @kanbanUserstoriesService.usByStatus return @kanbanUserstoriesService.usByStatus
cleanSelectedUss: () ->
for key of @.selectedUss
@.selectedUss[key] = false
toggleSelectedUs: (usId) ->
@.selectedUss[usId] = !@.selectedUss[usId]
firstLoad: () -> firstLoad: () ->
promise = @.loadInitialData() promise = @.loadInitialData()
@ -291,28 +299,36 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
prepareBulkUpdateData: (uses, field="kanban_order") -> prepareBulkUpdateData: (uses, field="kanban_order") ->
return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]}) return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]})
moveUs: (ctx, us, oldStatusId, newStatusId, index) -> moveUs: (ctx, usList, newStatusId, index) ->
us = @kanbanUserstoriesService.getUsModel(us.get('id')) @.cleanSelectedUss()
newStatus = @scope.usStatusById[newStatusId]
if newStatus.is_archived and !@scope.usByStatus.get(newStatusId.toString()) usList = _.map usList, (us) =>
moveUpdateData = @kanbanUserstoriesService.moveToEnd(us.id, newStatusId) return @kanbanUserstoriesService.getUsModel(us.id)
else
moveUpdateData = @kanbanUserstoriesService.move(us.id, newStatusId, index) data = @kanbanUserstoriesService.move(usList, newStatusId, index)
promise = @rs.userstories.bulkUpdateKanbanOrder(@scope.projectId, data.bulkOrders)
promise.then () =>
# saving
# drag single or different status
options = {
headers: {
"set-orders": JSON.stringify(data.setOrders)
}
}
params = { params = {
include_attachments: true, include_attachments: true,
include_tasks: true include_tasks: true
} }
options = { promises = _.map usList, (us) =>
headers: { @repo.save(us, true, params, options, true)
"set-orders": JSON.stringify(moveUpdateData.set_orders)
}
}
promise = @repo.save(us, true, params, options, true) promise = @q.all(promises)
promise = promise.then (result) => promise.then (result) =>
headers = result[1] headers = result[1]
if headers && headers['taiga-info-order-updated'] if headers && headers['taiga-info-order-updated']
@ -320,8 +336,6 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
@kanbanUserstoriesService.assignOrders(order) @kanbanUserstoriesService.assignOrders(order)
@scope.$broadcast("redraw:wip") @scope.$broadcast("redraw:wip")
return promise
module.controller("KanbanController", KanbanController) module.controller("KanbanController", KanbanController)
############################################################################# #############################################################################

View File

@ -48,7 +48,6 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) ->
if not ($scope.project.my_permissions.indexOf("modify_us") > -1) if not ($scope.project.my_permissions.indexOf("modify_us") > -1)
return return
oldParentScope = null
newParentScope = null newParentScope = null
itemEl = null itemEl = null
tdom = $el tdom = $el
@ -70,23 +69,44 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) ->
}) })
drake.on 'drag', (item) -> drake.on 'drag', (item) ->
oldParentScope = $(item).parent().scope() window.dragMultiple.start(item, containers)
drake.on 'cloned', (item, dropTarget) ->
$(item).addClass('multiple-drag-mirror')
drake.on 'dragend', (item) -> drake.on 'dragend', (item) ->
parentEl = $(item).parent() parentEl = $(item).parent()
itemEl = $(item)
itemUs = itemEl.scope().us
itemIndex = itemEl.index()
newParentScope = parentEl.scope() newParentScope = parentEl.scope()
newStatusId = newParentScope.s.id newStatusId = newParentScope.s.id
oldStatusId = oldParentScope.s.id dragMultipleItems = window.dragMultiple.stop()
if newStatusId != oldStatusId # if it is not drag multiple
deleteElement(itemEl) if !dragMultipleItems.length
dragMultipleItems = [item]
firstElement = dragMultipleItems[0]
index = $(firstElement).index()
newStatus = newParentScope.s.id
usList = _.map dragMultipleItems, (item) -> $(item).scope().us
finalUsList = _.map usList, (item) ->
return {
id: item.get('id'),
oldStatusId: item.getIn(['model', 'status'])
}
$scope.$apply -> $scope.$apply ->
$rootscope.$broadcast("kanban:us:move", itemUs, itemUs.getIn(['model', 'status']), newStatusId, itemIndex) _.each usList, (item, key) =>
oldStatus = item.getIn(['model', 'status'])
sameContainer = newStatus == oldStatus
if !sameContainer
itemEl = $(dragMultipleItems[key])
deleteElement(itemEl)
$rootscope.$broadcast("kanban:us:move", finalUsList, newStatus, index)
scroll = autoScroll(containers, { scroll = autoScroll(containers, {
margin: 100, margin: 100,

View File

@ -7,8 +7,8 @@
var reset = function(elm) { var reset = function(elm) {
$(elm) $(elm)
.removeAttr('style') .removeAttr('style')
.removeClass('tg-backlog-us-mirror') .removeClass('tg-multiple-drag-mirror')
.removeClass('backlog-us-mirror') .removeClass('multiple-drag-mirror')
.data('dragMultipleIndex', null) .data('dragMultipleIndex', null)
.data('dragMultipleActive', false); .data('dragMultipleActive', false);
}; };
@ -40,6 +40,8 @@
var currentTop = shadow.position().top; var currentTop = shadow.position().top;
var height = shadow.outerHeight(); var height = shadow.outerHeight();
$('.gu-transit').addClass('gu-transit-multi');
_.forEach(dragMultiple.items.draggingItems, function(elm, index) { _.forEach(dragMultiple.items.draggingItems, function(elm, index) {
var elmIndex = parseInt(elm.data('dragMultipleIndex'), 10); var elmIndex = parseInt(elm.data('dragMultipleIndex'), 10);
var top = currentTop + (elmIndex * height); var top = currentTop + (elmIndex * height);
@ -57,22 +59,21 @@
refreshOriginal(); refreshOriginal();
var current = dragMultiple.items.elm;
var container = dragMultiple.items.container;
document.documentElement.removeEventListener('mousemove', removeEventFn); document.documentElement.removeEventListener('mousemove', removeEventFn);
// reset // reset
dragMultiple.items = {}; dragMultiple.items = {};
$('.' + mainClass).removeClass(mainClass); $('.' + mainClass).removeClass(mainClass);
$('.tg-backlog-us-mirror').remove(); $('.tg-multiple-drag-mirror').remove();
$('.backlog-us-mirror').removeClass('backlog-us-mirror'); $('.multiple-drag-mirror').removeClass('multiple-drag-mirror');
$('.tg-backlog-us-dragging') $('.tg-multiple-drag-dragging')
.removeClass('tg-backlog-us-dragging') .removeClass('tg-multiple-drag-dragging')
.show(); .show();
$('.gu-transit-multi').removeClass('gu-transit-multi');
return $('.' + multipleSortableClass); return $('.' + multipleSortableClass);
}; };
@ -180,8 +181,8 @@
clone = $(item).clone(true); clone = $(item).clone(true);
clone clone
.addClass('backlog-us-mirror') .addClass('multiple-drag-mirror')
.addClass('tg-backlog-us-mirror') .addClass('tg-multiple-drag-mirror')
.data('dragmultiple:originalPosition', $(item).position()) .data('dragmultiple:originalPosition', $(item).position())
.data('dragMultipleActive', true) .data('dragMultipleActive', true)
.css({ .css({
@ -194,7 +195,7 @@
$(item) $(item)
.hide() .hide()
.addClass('tg-backlog-us-dragging'); .addClass('tg-multiple-drag-dragging');
return clone; return clone;
}); });

View File

@ -23,7 +23,7 @@
tg-check-permission="{{vm.getPermissionsKey()}}" tg-check-permission="{{vm.getPermissionsKey()}}"
) )
a.e2e-assign.card-owner-assign( a.e2e-assign.card-owner-assign(
ng-click="vm.onClickAssignedTo({id: vm.item.get('id')})" ng-click="!$event.ctrlKey && vm.onClickAssignedTo({id: vm.item.get('id')})"
href="" href=""
) )
tg-svg(svg-icon="icon-add-user") tg-svg(svg-icon="icon-add-user")
@ -31,7 +31,7 @@
a.e2e-edit.card-edit( a.e2e-edit.card-edit(
href="" href=""
ng-click="vm.onClickEdit({id: vm.item.get('id')})" ng-click="!$event.ctrlKey && vm.onClickEdit({id: vm.item.get('id')})"
tg-loading="vm.item.get('loading')" tg-loading="vm.item.get('loading')"
) )
tg-svg(svg-icon="icon-edit") tg-svg(svg-icon="icon-edit")

View File

@ -1,5 +1,5 @@
.card-unfold.ng-animate-disabled( .card-unfold.ng-animate-disabled(
ng-click="vm.toggleFold()" ng-click="!$event.ctrlKey && vm.toggleFold()"
ng-if="vm.visible('unfold') && (vm.hasTasks() || vm.hasVisibleAttachments())" ng-if="vm.visible('unfold') && (vm.hasTasks() || vm.hasVisibleAttachments())"
role="button" role="button"
) )

View File

@ -14,3 +14,15 @@
images="vm.item.get('images')" images="vm.item.get('images')"
) )
include card-templates/card-unfold include card-templates/card-unfold
.card-transit-multi
div.fake-us
div.fake-img
div.column
div.fake-text
div.fake-text
div.fake-us
div.fake-img
div.column
div.fake-text
div.fake-text

View File

@ -67,7 +67,7 @@ div.kanban-table(
tg-card.card.ng-animate-disabled( tg-card.card.ng-animate-disabled(
tg-repeat="us in usByStatus.get(s.id.toString()) track by us.getIn(['model', 'id'])", 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)}" ng-class="{'kanban-task-maximized': ctrl.isMaximized(s.id), 'kanban-task-minimized': ctrl.isMinimized(s.id), 'kanban-task-selected': ctrl.selectedUss[us.get('id')], 'ui-multisortable-multiple': ctrl.selectedUss[us.get('id')]}"
tg-class-permission="{'readonly': '!modify_task'}" tg-class-permission="{'readonly': '!modify_task'}"
tg-bind-scope, tg-bind-scope,
on-toggle-fold="ctrl.toggleFold(id)" on-toggle-fold="ctrl.toggleFold(id)"
@ -78,6 +78,7 @@ div.kanban-table(
zoom="ctrl.zoom" zoom="ctrl.zoom"
zoom-level="ctrl.zoomLevel" zoom-level="ctrl.zoomLevel"
archived="ctrl.isUsInArchivedHiddenStatus(us.get('id'))" archived="ctrl.isUsInArchivedHiddenStatus(us.get('id'))"
ng-click="$event.ctrlKey && ctrl.toggleSelectedUs(us.get('id'))"
) )
div.kanban-column-intro(ng-if="s.is_archived", tg-kanban-archived-status-intro="s") div.kanban-column-intro(ng-if="s.is_archived", tg-kanban-archived-status-intro="s")

View File

@ -41,7 +41,7 @@
} }
} }
.backlog-us-mirror { .multiple-drag-mirror.us-item-row {
background: $white; background: $white;
border-radius: 4px; border-radius: 4px;
box-shadow: 2px 2px 5px $gray; box-shadow: 2px 2px 5px $gray;

View File

@ -148,9 +148,63 @@ $column-padding: .5rem 1rem;
.kanban-uses-box { .kanban-uses-box {
background: $mass-white; background: $mass-white;
} }
.kanban-task-selected {
&.card:not(.gu-transit-multi) {
// border: 1px solid $primary-light;
box-shadow: 0 0 0 1px $primary-light, 2px 2px 4px darken($whitish, 10%);
}
}
} }
.kanban-table-inner { .kanban-table-inner {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
} }
.card-transit-multi {
background: darken($whitish, 2%);
border: 1px dashed darken($whitish, 8%);
display: none;
opacity: 1;
padding: 1rem;
.fake-img,
.fake-text {
background: darken($whitish, 8%);
}
.fake-us {
display: flex;
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
.column {
padding-left: 0.5rem;
width: 100%;
}
.fake-img {
flex-basis: 48px;
flex-shrink: 0;
height: 48px;
width: 48px;
}
.fake-text {
height: 1rem;
margin-bottom: 1rem;
width: 80%;
&:last-child {
margin-bottom: 0;
width: 40%;
}
}
}
.card.gu-transit-multi {
.card-transit-multi {
display: block;
}
.card-inner {
display: none;
}
}

File diff suppressed because one or more lines are too long