kanban drag multiple
parent
6fe84f9acd
commit
2cd47a578c
|
@ -68,7 +68,7 @@ BacklogSortableDirective = () ->
|
|||
window.dragMultiple.start(item, container)
|
||||
|
||||
drake.on 'cloned', (item) ->
|
||||
$(item).addClass('backlog-us-mirror')
|
||||
$(item).addClass('multiple-drag-mirror')
|
||||
|
||||
drake.on 'dragend', (item) ->
|
||||
parent = $(item).parent()
|
||||
|
|
|
@ -251,8 +251,6 @@ Qqueue = ($q) ->
|
|||
bindAdd: (fn) =>
|
||||
return (args...) =>
|
||||
lastPromise = lastPromise.then () => fn.apply(@, args)
|
||||
|
||||
return qqueue
|
||||
add: (fn) =>
|
||||
if !lastPromise
|
||||
lastPromise = fn()
|
||||
|
|
|
@ -93,15 +93,17 @@ class KanbanUserstoriesService extends taiga.Service
|
|||
|
||||
@.refresh()
|
||||
|
||||
move: (id, statusId, index) ->
|
||||
us = @.getUsModel(id)
|
||||
move: (usList, statusId, index) ->
|
||||
initialLength = usList.length
|
||||
|
||||
usByStatus = _.filter @.userstoriesRaw, (it) =>
|
||||
return it.status == statusId
|
||||
|
||||
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)
|
||||
afterDestination = _.slice(usByStatusWithoutMoved, index)
|
||||
|
||||
|
@ -112,26 +114,54 @@ class KanbanUserstoriesService extends taiga.Service
|
|||
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 and (!afterDestination or afterDestination.length == 0)
|
||||
@.order[us.id] = 0
|
||||
else if !previous and afterDestination and afterDestination.length > 0
|
||||
@.order[us.id] = @.order[afterDestination[0].id] - 1
|
||||
modifiedUs = []
|
||||
setPreviousOrders = []
|
||||
setNextOrders = []
|
||||
|
||||
if !previous
|
||||
startIndex = 0
|
||||
else if previous
|
||||
@.order[us.id] = @.order[previous.id] + 1
|
||||
startIndex = @.order[previous.id] + 1
|
||||
|
||||
for it, key in afterDestination
|
||||
@.order[it.id] = @.order[us.id] + key + 1
|
||||
previousWithTheSameOrder = _.filter(beforeDestination, (it) =>
|
||||
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.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()
|
||||
|
||||
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) ->
|
||||
us = @.getUsModel(id)
|
||||
|
|
|
@ -71,6 +71,7 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
bindMethods(@)
|
||||
@kanbanUserstoriesService.reset()
|
||||
@.openFilter = false
|
||||
@.selectedUss = {}
|
||||
|
||||
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", () =>
|
||||
return @kanbanUserstoriesService.usByStatus
|
||||
|
||||
cleanSelectedUss: () ->
|
||||
for key of @.selectedUss
|
||||
@.selectedUss[key] = false
|
||||
|
||||
toggleSelectedUs: (usId) ->
|
||||
@.selectedUss[usId] = !@.selectedUss[usId]
|
||||
|
||||
firstLoad: () ->
|
||||
promise = @.loadInitialData()
|
||||
|
||||
|
@ -291,28 +299,36 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
prepareBulkUpdateData: (uses, field="kanban_order") ->
|
||||
return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]})
|
||||
|
||||
moveUs: (ctx, us, oldStatusId, newStatusId, index) ->
|
||||
us = @kanbanUserstoriesService.getUsModel(us.get('id'))
|
||||
newStatus = @scope.usStatusById[newStatusId]
|
||||
if newStatus.is_archived and !@scope.usByStatus.get(newStatusId.toString())
|
||||
moveUpdateData = @kanbanUserstoriesService.moveToEnd(us.id, newStatusId)
|
||||
else
|
||||
moveUpdateData = @kanbanUserstoriesService.move(us.id, newStatusId, index)
|
||||
moveUs: (ctx, usList, newStatusId, index) ->
|
||||
@.cleanSelectedUss()
|
||||
|
||||
usList = _.map usList, (us) =>
|
||||
return @kanbanUserstoriesService.getUsModel(us.id)
|
||||
|
||||
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 = {
|
||||
include_attachments: true,
|
||||
include_tasks: true
|
||||
}
|
||||
|
||||
options = {
|
||||
headers: {
|
||||
"set-orders": JSON.stringify(moveUpdateData.set_orders)
|
||||
}
|
||||
}
|
||||
promises = _.map usList, (us) =>
|
||||
@repo.save(us, true, params, options, true)
|
||||
|
||||
promise = @repo.save(us, true, params, options, true)
|
||||
promise = @q.all(promises)
|
||||
|
||||
promise = promise.then (result) =>
|
||||
promise.then (result) =>
|
||||
headers = result[1]
|
||||
|
||||
if headers && headers['taiga-info-order-updated']
|
||||
|
@ -320,8 +336,6 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
@kanbanUserstoriesService.assignOrders(order)
|
||||
@scope.$broadcast("redraw:wip")
|
||||
|
||||
return promise
|
||||
|
||||
module.controller("KanbanController", KanbanController)
|
||||
|
||||
#############################################################################
|
||||
|
|
|
@ -48,7 +48,6 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) ->
|
|||
if not ($scope.project.my_permissions.indexOf("modify_us") > -1)
|
||||
return
|
||||
|
||||
oldParentScope = null
|
||||
newParentScope = null
|
||||
itemEl = null
|
||||
tdom = $el
|
||||
|
@ -70,23 +69,44 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) ->
|
|||
})
|
||||
|
||||
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) ->
|
||||
parentEl = $(item).parent()
|
||||
itemEl = $(item)
|
||||
itemUs = itemEl.scope().us
|
||||
itemIndex = itemEl.index()
|
||||
newParentScope = parentEl.scope()
|
||||
|
||||
newStatusId = newParentScope.s.id
|
||||
oldStatusId = oldParentScope.s.id
|
||||
dragMultipleItems = window.dragMultiple.stop()
|
||||
|
||||
if newStatusId != oldStatusId
|
||||
deleteElement(itemEl)
|
||||
# if it is not drag multiple
|
||||
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 ->
|
||||
$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, {
|
||||
margin: 100,
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
var reset = function(elm) {
|
||||
$(elm)
|
||||
.removeAttr('style')
|
||||
.removeClass('tg-backlog-us-mirror')
|
||||
.removeClass('backlog-us-mirror')
|
||||
.removeClass('tg-multiple-drag-mirror')
|
||||
.removeClass('multiple-drag-mirror')
|
||||
.data('dragMultipleIndex', null)
|
||||
.data('dragMultipleActive', false);
|
||||
};
|
||||
|
@ -40,6 +40,8 @@
|
|||
var currentTop = shadow.position().top;
|
||||
var height = shadow.outerHeight();
|
||||
|
||||
$('.gu-transit').addClass('gu-transit-multi');
|
||||
|
||||
_.forEach(dragMultiple.items.draggingItems, function(elm, index) {
|
||||
var elmIndex = parseInt(elm.data('dragMultipleIndex'), 10);
|
||||
var top = currentTop + (elmIndex * height);
|
||||
|
@ -57,22 +59,21 @@
|
|||
|
||||
refreshOriginal();
|
||||
|
||||
var current = dragMultiple.items.elm;
|
||||
var container = dragMultiple.items.container;
|
||||
|
||||
document.documentElement.removeEventListener('mousemove', removeEventFn);
|
||||
|
||||
// reset
|
||||
dragMultiple.items = {};
|
||||
|
||||
$('.' + mainClass).removeClass(mainClass);
|
||||
$('.tg-backlog-us-mirror').remove();
|
||||
$('.backlog-us-mirror').removeClass('backlog-us-mirror');
|
||||
$('.tg-multiple-drag-mirror').remove();
|
||||
$('.multiple-drag-mirror').removeClass('multiple-drag-mirror');
|
||||
|
||||
$('.tg-backlog-us-dragging')
|
||||
.removeClass('tg-backlog-us-dragging')
|
||||
$('.tg-multiple-drag-dragging')
|
||||
.removeClass('tg-multiple-drag-dragging')
|
||||
.show();
|
||||
|
||||
$('.gu-transit-multi').removeClass('gu-transit-multi');
|
||||
|
||||
return $('.' + multipleSortableClass);
|
||||
};
|
||||
|
||||
|
@ -180,8 +181,8 @@
|
|||
clone = $(item).clone(true);
|
||||
|
||||
clone
|
||||
.addClass('backlog-us-mirror')
|
||||
.addClass('tg-backlog-us-mirror')
|
||||
.addClass('multiple-drag-mirror')
|
||||
.addClass('tg-multiple-drag-mirror')
|
||||
.data('dragmultiple:originalPosition', $(item).position())
|
||||
.data('dragMultipleActive', true)
|
||||
.css({
|
||||
|
@ -194,7 +195,7 @@
|
|||
|
||||
$(item)
|
||||
.hide()
|
||||
.addClass('tg-backlog-us-dragging');
|
||||
.addClass('tg-multiple-drag-dragging');
|
||||
|
||||
return clone;
|
||||
});
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
tg-check-permission="{{vm.getPermissionsKey()}}"
|
||||
)
|
||||
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=""
|
||||
)
|
||||
tg-svg(svg-icon="icon-add-user")
|
||||
|
@ -31,7 +31,7 @@
|
|||
|
||||
a.e2e-edit.card-edit(
|
||||
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-svg(svg-icon="icon-edit")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.card-unfold.ng-animate-disabled(
|
||||
ng-click="vm.toggleFold()"
|
||||
ng-click="!$event.ctrlKey && vm.toggleFold()"
|
||||
ng-if="vm.visible('unfold') && (vm.hasTasks() || vm.hasVisibleAttachments())"
|
||||
role="button"
|
||||
)
|
||||
|
|
|
@ -14,3 +14,15 @@
|
|||
images="vm.item.get('images')"
|
||||
)
|
||||
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
|
|
@ -67,7 +67,7 @@ div.kanban-table(
|
|||
|
||||
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)}"
|
||||
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-bind-scope,
|
||||
on-toggle-fold="ctrl.toggleFold(id)"
|
||||
|
@ -78,6 +78,7 @@ div.kanban-table(
|
|||
zoom="ctrl.zoom"
|
||||
zoom-level="ctrl.zoomLevel"
|
||||
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")
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.backlog-us-mirror {
|
||||
.multiple-drag-mirror.us-item-row {
|
||||
background: $white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 2px 2px 5px $gray;
|
||||
|
|
|
@ -148,9 +148,63 @@ $column-padding: .5rem 1rem;
|
|||
.kanban-uses-box {
|
||||
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 {
|
||||
display: flex;
|
||||
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
Loading…
Reference in New Issue