feat: increase task drop area size for bucket list (#1512)

Reviewed-on: vikunja/frontend#1512
Reviewed-by: konrad <k@knt.li>
This commit is contained in:
konrad 2022-02-13 13:12:58 +00:00
commit cb395f3f69
3 changed files with 147 additions and 104 deletions

View File

@ -132,7 +132,7 @@ describe('List View Kanban', () => {
cy.getSettled('.kanban .bucket .tasks .task') cy.getSettled('.kanban .bucket .tasks .task')
.contains(tasks[0].title) .contains(tasks[0].title)
.first() .first()
.drag('.kanban .bucket:nth-child(2) .tasks .dropper') .drag('.kanban .bucket:nth-child(2) .tasks')
cy.get('.kanban .bucket:nth-child(2) .tasks') cy.get('.kanban .bucket:nth-child(2) .tasks')
.should('contain', tasks[0].title) .should('contain', tasks[0].title)

View File

@ -138,7 +138,6 @@ $task-background: var(--white);
border: 3px solid transparent; border: 3px solid transparent;
font-size: .9rem; font-size: .9rem;
margin: .5rem;
padding: .4rem; padding: .4rem;
border-radius: $radius; border-radius: $radius;
background: $task-background; background: $task-background;

View File

@ -2,12 +2,12 @@
<ListWrapper class="list-kanban" :list-id="listId" viewName="kanban"> <ListWrapper class="list-kanban" :list-id="listId" viewName="kanban">
<template #header> <template #header>
<div class="filter-container" v-if="isSavedFilter"> <div class="filter-container" v-if="isSavedFilter">
<div class="items"> <div class="items">
<filter-popup <filter-popup
v-model="params" v-model="params"
@update:modelValue="loadBuckets" @update:modelValue="loadBuckets"
/> />
</div> </div>
</div> </div>
</template> </template>
@ -123,61 +123,59 @@
</a> </a>
</dropdown> </dropdown>
</div> </div>
<div
:ref="(el) => setTaskContainerRef(bucket.id, el)" <draggable
@scroll="($event) => handleTaskContainerScroll(bucket.id, bucket.listId, $event.target)" v-bind="dragOptions"
class="tasks" :modelValue="bucket.tasks"
@update:modelValue="(tasks) => updateTasks(bucket.id, tasks)"
@start="() => dragstart(bucket)"
@end="updateTaskPosition"
:group="{name: 'tasks', put: shouldAcceptDrop(bucket) && !dragBucket}"
:disabled="!canWrite"
:data-bucket-index="bucketIndex"
tag="transition-group"
:item-key="(task) => `bucket${bucket.id}-task${task.id}`"
:component-data="getTaskDraggableTaskComponentData(bucket)"
> >
<draggable <template #footer>
v-bind="dragOptions" <div class="bucket-footer" v-if="canWrite">
:modelValue="bucket.tasks" <div class="field" v-if="showNewTaskInput[bucket.id]">
@update:modelValue="(tasks) => updateTasks(bucket.id, tasks)" <div class="control" :class="{'is-loading': loading}">
@start="() => dragstart(bucket)" <input
@end="updateTaskPosition" class="input"
:group="{name: 'tasks', put: shouldAcceptDrop(bucket) && !dragBucket}" :disabled="loading || undefined"
:disabled="!canWrite" @focusout="toggleShowNewTaskInput(bucket.id)"
:data-bucket-index="bucketIndex" @keyup.enter="addTaskToBucket(bucket.id)"
tag="transition-group" @keyup.esc="toggleShowNewTaskInput(bucket.id)"
:item-key="(task) => `bucket${bucket.id}-task${task.id}`" :placeholder="$t('list.kanban.addTaskPlaceholder')"
:component-data="taskDraggableTaskComponentData" type="text"
> v-focus.always
<template #item="{element: task}"> v-model="newTaskText"
<kanban-card :task="task"/> />
</template> </div>
</draggable> <p class="help is-danger" v-if="newTaskError[bucket.id] && newTaskText === ''">
</div> {{ $t('list.create.addTitleRequired') }}
<div class="bucket-footer" v-if="canWrite"> </p>
<div class="field" v-if="showNewTaskInput[bucket.id]"> </div>
<div class="control" :class="{'is-loading': loading}"> <x-button
<input @click="toggleShowNewTaskInput(bucket.id)"
class="input" class="is-fullwidth has-text-centered"
:disabled="loading || null" :shadow="false"
@focusout="toggleShowNewTaskInput(bucket.id)" v-else
@keyup.enter="addTaskToBucket(bucket.id)" icon="plus"
@keyup.esc="toggleShowNewTaskInput(bucket.id)" variant="secondary"
:placeholder="$t('list.kanban.addTaskPlaceholder')" >
type="text" {{ bucket.tasks.length === 0 ? $t('list.kanban.addTask') : $t('list.kanban.addAnotherTask') }}
v-focus.always </x-button>
v-model="newTaskText"
/>
</div> </div>
<p class="help is-danger" v-if="newTaskError[bucket.id] && newTaskText === ''"> </template>
{{ $t('list.create.addTitleRequired') }}
</p> <template #item="{element: task}">
</div> <div class="task-item">
<x-button <kanban-card class="kanban-card" :task="task"/>
@click="toggleShowNewTaskInput(bucket.id)" </div>
class="is-transparent is-fullwidth has-text-centered" </template>
:shadow="false" </draggable>
v-if="!showNewTaskInput[bucket.id]"
icon="plus"
variant="secondary"
>
{{
bucket.tasks.length === 0 ? $t('list.kanban.addTask') : $t('list.kanban.addAnotherTask')
}}
</x-button>
</div>
</div> </div>
</template> </template>
</draggable> </draggable>
@ -197,10 +195,10 @@
v-model="newBucketTitle" v-model="newBucketTitle"
/> />
<x-button <x-button
v-else
@click="() => showNewBucketInput = true" @click="() => showNewBucketInput = true"
:shadow="false" :shadow="false"
class="is-transparent is-fullwidth has-text-centered" class="is-transparent is-fullwidth has-text-centered"
v-else
variant="secondary" variant="secondary"
icon="plus" icon="plus"
> >
@ -313,6 +311,20 @@ export default {
}, },
}, },
computed: { computed: {
getTaskDraggableTaskComponentData() {
return (bucket) => ({
ref: (el) => this.setTaskContainerRef(bucket.id, el),
onScroll: (event) => this.handleTaskContainerScroll(bucket.id, bucket.listId, event.target),
type: 'transition',
tag: 'div',
name: !this.drag ? 'move-card' : null,
class: [
'tasks',
{'dragging-disabled': !this.canWrite},
],
})
},
isSavedFilter() { isSavedFilter() {
return this.list.isSavedFilter && !this.list.isSavedFilter() return this.list.isSavedFilter && !this.list.isSavedFilter()
}, },
@ -333,17 +345,6 @@ export default {
], ],
} }
}, },
taskDraggableTaskComponentData() {
return {
type: 'transition',
tag: 'div',
name: !this.drag ? 'move-card' : null,
class: [
'dropper',
{'dragging-disabled': !this.canWrite},
],
}
},
buckets() { buckets() {
return this.$store.state.kanban.buckets return this.$store.state.kanban.buckets
}, },
@ -406,10 +407,25 @@ export default {
// of the drop target works all the time. // of the drop target works all the time.
const bucketIndex = parseInt(e.to.dataset.bucketIndex) const bucketIndex = parseInt(e.to.dataset.bucketIndex)
const newBucket = this.buckets[bucketIndex] const newBucket = this.buckets[bucketIndex]
const task = newBucket.tasks[e.newIndex]
const taskBefore = newBucket.tasks[e.newIndex - 1] ?? null // HACK:
const taskAfter = newBucket.tasks[e.newIndex + 1] ?? null // this is a hacky workaround for a known problem of vue.draggable.next when using the footer slot
// the problem: https://github.com/SortableJS/vue.draggable.next/issues/108
// This hack doesn't remove the problem that the ghost item is still displayed below the footer
// It just makes releasing the item possible.
// The newIndex of the event doesn't count in the elements of the footer slot.
// This is why in case the length of the tasks is identical with the newIndex
// we have to remove 1 to get the correct index.
const newTaskIndex = newBucket.tasks.length === e.newIndex
? e.newIndex - 1
: e.newIndex
const task = newBucket.tasks[newTaskIndex]
const taskBefore = newBucket.tasks[newTaskIndex - 1] ?? null
const taskAfter = newBucket.tasks[newTaskIndex + 1] ?? null
const newTask = cloneDeep(task) // cloning the task to avoid vuex store mutations const newTask = cloneDeep(task) // cloning the task to avoid vuex store mutations
newTask.bucketId = newBucket.id, newTask.bucketId = newBucket.id,
@ -525,7 +541,10 @@ export default {
const updatedData = { const updatedData = {
id: bucket.id, id: bucket.id,
position: calculateItemPosition(bucketBefore !== null ? bucketBefore.position : null, bucketAfter !== null ? bucketAfter.position : null), position: calculateItemPosition(
bucketBefore !== null ? bucketBefore.position : null,
bucketAfter !== null ? bucketAfter.position : null,
),
} }
this.$store.dispatch('kanban/updateBucket', updatedData) this.$store.dispatch('kanban/updateBucket', updatedData)
@ -546,9 +565,14 @@ export default {
}, },
shouldAcceptDrop(bucket) { shouldAcceptDrop(bucket) {
return bucket.id === this.sourceBucket || // When dragging from a bucket who has its limit reached, dragging should still be possible return (
bucket.limit === 0 || // If there is no limit set, dragging & dropping should always work // When dragging from a bucket who has its limit reached, dragging should still be possible
bucket.tasks.length < bucket.limit // Disallow dropping to buckets which have their limit reached bucket.id === this.sourceBucket ||
// If there is no limit set, dragging & dropping should always work
bucket.limit === 0 ||
// Disallow dropping to buckets which have their limit reached
bucket.tasks.length < bucket.limit
)
}, },
dragstart(bucket) { dragstart(bucket) {
@ -597,7 +621,6 @@ $filter-container-height: '1rem - #{$switch-view-height}';
} }
.kanban { .kanban {
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
height: calc(#{$crazy-height-calculation}); height: calc(#{$crazy-height-calculation});
@ -610,21 +633,28 @@ $filter-container-height: '1rem - #{$switch-view-height}';
&-bucket-container { &-bucket-container {
display: flex; display: flex;
align-items: flex-start;
} }
.ghost { .ghost {
background: transparent !important; position: relative;
border: 3px dashed var(--grey-300) !important;
box-shadow: none !important;
* { * {
opacity: 0; opacity: 0;
} }
&::after {
content: '';
position: absolute;
display: block;
top: 0.25rem;
right: 0.5rem;
bottom: 0.25rem;
left: 0.5rem;
border: 3px dashed var(--grey-300);
border-radius: $radius;
}
} }
.bucket { .bucket {
background-color: var(--grey-100);
border-radius: $radius; border-radius: $radius;
position: relative; position: relative;
@ -632,24 +662,24 @@ $filter-container-height: '1rem - #{$switch-view-height}';
max-height: 100%; max-height: 100%;
min-height: 20px; min-height: 20px;
width: $bucket-width; width: $bucket-width;
display: flex;
flex-direction: column;
.tasks { .tasks {
max-height: calc(#{$crazy-height-calculation-tasks}); overflow: hidden auto;
overflow: auto; height: 100%;
@media screen and (max-width: $tablet) {
max-height: calc(#{$crazy-height-calculation-tasks} - #{$filter-container-height});
}
.dropper {
&, > div {
min-height: 40px;
}
}
} }
.move-card-move { .task-item {
transition: transform $transition-duration; background-color: var(--grey-100);
padding: .25rem .5rem;
&:first-of-type {
padding-top: .5rem;
}
&:last-of-type {
padding-bottom: .5rem;
}
} }
.no-move { .no-move {
@ -682,10 +712,11 @@ $filter-container-height: '1rem - #{$switch-view-height}';
} }
&.is-collapsed { &.is-collapsed {
transform: rotate(90deg) translateX(math.div($bucket-width, 2) - math.div($bucket-header-height, 2)); align-self: flex-start;
transform: rotate(90deg) translateY(-100%);
transform-origin: top left;
// Using negative margins instead of translateY here to make all other buckets fill the empty space // Using negative margins instead of translateY here to make all other buckets fill the empty space
margin-left: (math.div($bucket-width, 2) - math.div($bucket-header-height, 2)) * -1; margin-right: calc((#{$bucket-width} - #{$bucket-header-height} - #{$bucket-right-margin}) * -1);
margin-right: calc(#{(math.div($bucket-width, 2) - math.div($bucket-header-height, 2)) * -1} + #{$bucket-right-margin});
cursor: pointer; cursor: pointer;
.tasks, .bucket-footer { .tasks, .bucket-footer {
@ -695,6 +726,8 @@ $filter-container-height: '1rem - #{$switch-view-height}';
} }
.bucket-header { .bucket-header {
background-color: var(--grey-100);
height: min-content;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@ -724,7 +757,13 @@ $filter-container-height: '1rem - #{$switch-view-height}';
} }
.bucket-footer { .bucket-footer {
position: sticky;
bottom: 0;
height: min-content;
padding: .5rem; padding: .5rem;
background-color: var(--grey-100);
border-bottom-left-radius: $radius;
border-bottom-right-radius: $radius;
.button { .button {
background-color: transparent; background-color: transparent;
@ -737,8 +776,13 @@ $filter-container-height: '1rem - #{$switch-view-height}';
} }
.task-dragging { .task-dragging {
transform: rotateZ(3deg);
transition: transform 0.18s ease; transition: transform 0.18s ease;
transform: rotateZ(3deg) }
.move-card-move {
transform: rotateZ(3deg);
transition: transform $transition-duration;
} }
.move-card-leave-from, .move-card-leave-from,