2020-04-26 01:11:34 +02:00
|
|
|
<template>
|
2021-11-17 18:11:19 +01:00
|
|
|
<ListWrapper class="list-kanban">
|
2021-11-14 21:33:53 +01:00
|
|
|
<template #header>
|
|
|
|
<div class="filter-container" v-if="isSavedFilter">
|
2020-12-22 12:49:34 +01:00
|
|
|
<div class="items">
|
2021-11-13 20:48:06 +01:00
|
|
|
<filter-popup
|
|
|
|
v-model="params"
|
|
|
|
@update:modelValue="loadBuckets"
|
|
|
|
/>
|
2020-12-22 12:49:34 +01:00
|
|
|
</div>
|
2021-11-14 21:33:53 +01:00
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<template #default>
|
|
|
|
<div class="kanban-view">
|
|
|
|
<div
|
|
|
|
:class="{ 'is-loading': loading && !oneTaskUpdating}"
|
|
|
|
class="kanban kanban-bucket-container loader-container"
|
|
|
|
>
|
2021-07-28 21:56:29 +02:00
|
|
|
<draggable
|
2021-09-08 11:59:46 +02:00
|
|
|
v-bind="dragOptions"
|
2021-09-11 17:53:03 +02:00
|
|
|
:modelValue="buckets"
|
2021-10-02 20:10:49 +02:00
|
|
|
@update:modelValue="updateBuckets"
|
|
|
|
@end="updateBucketPosition"
|
2021-07-28 21:56:29 +02:00
|
|
|
@start="() => dragBucket = true"
|
|
|
|
group="buckets"
|
|
|
|
:disabled="!canWrite"
|
2021-08-20 15:46:41 +02:00
|
|
|
tag="transition-group"
|
|
|
|
:item-key="({id}) => `bucket${id}`"
|
|
|
|
:component-data="bucketDraggableComponentData"
|
2021-07-07 21:58:29 +02:00
|
|
|
>
|
2021-08-20 15:46:41 +02:00
|
|
|
<template #item="{element: bucket, index: bucketIndex }">
|
2021-07-28 21:56:29 +02:00
|
|
|
<div
|
|
|
|
class="bucket"
|
|
|
|
:class="{'is-collapsed': collapsedBuckets[bucket.id]}"
|
2021-03-24 21:16:56 +01:00
|
|
|
>
|
2021-07-28 21:56:29 +02:00
|
|
|
<div class="bucket-header" @click="() => unCollapseBucket(bucket)">
|
|
|
|
<span
|
|
|
|
v-if="bucket.isDoneBucket"
|
|
|
|
class="icon is-small has-text-success mr-2"
|
|
|
|
v-tooltip="$t('list.kanban.doneBucketHint')"
|
|
|
|
>
|
|
|
|
<icon icon="check-double"/>
|
|
|
|
</span>
|
|
|
|
<h2
|
2021-10-01 21:26:47 +02:00
|
|
|
@keydown.enter.prevent.stop="$event.target.blur()"
|
|
|
|
@keydown.esc.prevent.stop="$event.target.blur()"
|
|
|
|
@blur="saveBucketTitle(bucket.id, $event.target.textContent)"
|
2021-07-28 21:56:29 +02:00
|
|
|
@click="focusBucketTitle"
|
|
|
|
class="title input"
|
2021-11-18 14:00:54 +01:00
|
|
|
:contenteditable="(bucketTitleEditable && canWrite && !collapsedBuckets[bucket.id]) ? true : undefined"
|
2021-10-17 17:21:33 +02:00
|
|
|
:spellcheck="false">{{ bucket.title }}</h2>
|
2021-07-28 21:56:29 +02:00
|
|
|
<span
|
|
|
|
:class="{'is-max': bucket.tasks.length >= bucket.limit}"
|
|
|
|
class="limit"
|
|
|
|
v-if="bucket.limit > 0">
|
|
|
|
{{ bucket.tasks.length }}/{{ bucket.limit }}
|
|
|
|
</span>
|
|
|
|
<dropdown
|
|
|
|
class="is-right options"
|
|
|
|
v-if="canWrite && !collapsedBuckets[bucket.id]"
|
|
|
|
trigger-icon="ellipsis-v"
|
|
|
|
@close="() => showSetLimitInput = false"
|
|
|
|
>
|
|
|
|
<a
|
|
|
|
@click.stop="showSetLimitInput = true"
|
|
|
|
class="dropdown-item"
|
|
|
|
>
|
|
|
|
<div class="field has-addons" v-if="showSetLimitInput">
|
|
|
|
<div class="control">
|
|
|
|
<input
|
|
|
|
@keyup.esc="() => showSetLimitInput = false"
|
2021-09-11 17:53:03 +02:00
|
|
|
@keyup.enter="() => showSetLimitInput = false"
|
|
|
|
:value="bucket.limit"
|
|
|
|
@input="(event) => setBucketLimit(bucket.id, parseInt(event.target.value))"
|
2021-07-28 21:56:29 +02:00
|
|
|
class="input"
|
|
|
|
type="number"
|
|
|
|
min="0"
|
|
|
|
v-focus.always
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div class="control">
|
|
|
|
<x-button
|
|
|
|
:disabled="bucket.limit < 0"
|
|
|
|
:icon="['far', 'save']"
|
|
|
|
:shadow="false"
|
2022-01-04 19:58:06 +01:00
|
|
|
v-cy="'setBucketLimit'"
|
2021-07-28 21:56:29 +02:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<template v-else>
|
|
|
|
{{
|
|
|
|
$t('list.kanban.limit', {limit: bucket.limit > 0 ? bucket.limit : $t('list.kanban.noLimit')})
|
|
|
|
}}
|
|
|
|
</template>
|
|
|
|
</a>
|
|
|
|
<a
|
|
|
|
@click.stop="toggleDoneBucket(bucket)"
|
|
|
|
class="dropdown-item"
|
|
|
|
v-tooltip="$t('list.kanban.doneBucketHintExtended')"
|
|
|
|
>
|
|
|
|
<span class="icon is-small" :class="{'has-text-success': bucket.isDoneBucket}">
|
|
|
|
<icon icon="check-double"/>
|
|
|
|
</span>
|
|
|
|
{{ $t('list.kanban.doneBucket') }}
|
|
|
|
</a>
|
|
|
|
<a
|
|
|
|
class="dropdown-item"
|
|
|
|
@click.stop="() => collapseBucket(bucket)"
|
|
|
|
>
|
|
|
|
{{ $t('list.kanban.collapse') }}
|
|
|
|
</a>
|
|
|
|
<a
|
|
|
|
:class="{'is-disabled': buckets.length <= 1}"
|
|
|
|
@click.stop="() => deleteBucketModal(bucket.id)"
|
|
|
|
class="dropdown-item has-text-danger"
|
|
|
|
v-tooltip="buckets.length <= 1 ? $t('list.kanban.deleteLast') : ''"
|
|
|
|
>
|
|
|
|
<span class="icon is-small">
|
|
|
|
<icon icon="trash-alt"/>
|
|
|
|
</span>
|
|
|
|
{{ $t('misc.delete') }}
|
|
|
|
</a>
|
|
|
|
</dropdown>
|
|
|
|
</div>
|
2021-10-01 21:26:47 +02:00
|
|
|
<div
|
|
|
|
:ref="(el) => setTaskContainerRef(bucket.id, el)"
|
2021-10-11 16:59:21 +02:00
|
|
|
@scroll="($event) => handleTaskContainerScroll(bucket.id, bucket.listId, $event.target)"
|
2021-10-01 21:26:47 +02:00
|
|
|
class="tasks"
|
|
|
|
>
|
2021-07-28 21:56:29 +02:00
|
|
|
<draggable
|
2021-09-08 11:59:46 +02:00
|
|
|
v-bind="dragOptions"
|
2021-09-11 17:53:03 +02:00
|
|
|
:modelValue="bucket.tasks"
|
|
|
|
@update:modelValue="(tasks) => updateTasks(bucket.id, tasks)"
|
2021-09-07 18:38:53 +02:00
|
|
|
@start="() => dragstart(bucket)"
|
2021-07-28 21:56:29 +02:00
|
|
|
@end="updateTaskPosition"
|
|
|
|
:group="{name: 'tasks', put: shouldAcceptDrop(bucket) && !dragBucket}"
|
|
|
|
:disabled="!canWrite"
|
2021-08-20 15:46:41 +02:00
|
|
|
:data-bucket-index="bucketIndex"
|
|
|
|
tag="transition-group"
|
|
|
|
:item-key="(task) => `bucket${bucket.id}-task${task.id}`"
|
|
|
|
:component-data="taskDraggableTaskComponentData"
|
2021-07-28 21:56:29 +02:00
|
|
|
>
|
2021-08-20 15:46:41 +02:00
|
|
|
<template #item="{element: task}">
|
2021-11-13 20:48:06 +01:00
|
|
|
<kanban-card :task="task"/>
|
2021-08-20 15:46:41 +02:00
|
|
|
</template>
|
2021-07-28 21:56:29 +02:00
|
|
|
</draggable>
|
|
|
|
</div>
|
|
|
|
<div class="bucket-footer" v-if="canWrite">
|
|
|
|
<div class="field" v-if="showNewTaskInput[bucket.id]">
|
|
|
|
<div class="control" :class="{'is-loading': loading}">
|
2021-01-30 17:17:04 +01:00
|
|
|
<input
|
|
|
|
class="input"
|
2021-08-20 17:00:03 +02:00
|
|
|
:disabled="loading || null"
|
2021-07-28 21:56:29 +02:00
|
|
|
@focusout="toggleShowNewTaskInput(bucket.id)"
|
|
|
|
@keyup.enter="addTaskToBucket(bucket.id)"
|
|
|
|
@keyup.esc="toggleShowNewTaskInput(bucket.id)"
|
|
|
|
:placeholder="$t('list.kanban.addTaskPlaceholder')"
|
|
|
|
type="text"
|
2021-01-30 17:17:04 +01:00
|
|
|
v-focus.always
|
2021-07-28 21:56:29 +02:00
|
|
|
v-model="newTaskText"
|
2021-01-30 17:17:04 +01:00
|
|
|
/>
|
2021-01-23 18:54:22 +01:00
|
|
|
</div>
|
2021-07-28 21:56:29 +02:00
|
|
|
<p class="help is-danger" v-if="newTaskError[bucket.id] && newTaskText === ''">
|
2021-09-08 17:49:10 +02:00
|
|
|
{{ $t('list.create.addTitleRequired') }}
|
2021-07-28 21:56:29 +02:00
|
|
|
</p>
|
2020-11-22 17:32:35 +01:00
|
|
|
</div>
|
2021-07-28 21:56:29 +02:00
|
|
|
<x-button
|
|
|
|
@click="toggleShowNewTaskInput(bucket.id)"
|
|
|
|
class="is-transparent is-fullwidth has-text-centered"
|
|
|
|
:shadow="false"
|
|
|
|
v-if="!showNewTaskInput[bucket.id]"
|
|
|
|
icon="plus"
|
2022-01-04 19:58:06 +01:00
|
|
|
variant="secondary"
|
2021-07-28 21:56:29 +02:00
|
|
|
>
|
2021-07-05 12:29:04 +02:00
|
|
|
{{
|
2021-07-28 21:56:29 +02:00
|
|
|
bucket.tasks.length === 0 ? $t('list.kanban.addTask') : $t('list.kanban.addAnotherTask')
|
2021-07-05 12:29:04 +02:00
|
|
|
}}
|
2021-07-28 21:56:29 +02:00
|
|
|
</x-button>
|
2020-11-22 17:32:35 +01:00
|
|
|
</div>
|
2020-04-26 01:11:34 +02:00
|
|
|
</div>
|
2021-08-20 15:46:41 +02:00
|
|
|
</template>
|
2021-07-28 21:56:29 +02:00
|
|
|
</draggable>
|
2020-04-26 01:11:34 +02:00
|
|
|
|
2021-01-16 20:28:10 +01:00
|
|
|
<div class="bucket new-bucket" v-if="canWrite && !loading && buckets.length > 0">
|
2020-11-22 17:32:35 +01:00
|
|
|
<input
|
|
|
|
:class="{'is-loading': loading}"
|
2021-08-20 17:00:03 +02:00
|
|
|
:disabled="loading || null"
|
2021-10-01 21:26:47 +02:00
|
|
|
@blur="() => showNewBucketInput = false"
|
2020-11-22 17:32:35 +01:00
|
|
|
@keyup.enter="createNewBucket"
|
2021-10-01 21:26:47 +02:00
|
|
|
@keyup.esc="$event.target.blur()"
|
2020-11-22 17:32:35 +01:00
|
|
|
class="input"
|
2021-06-24 01:24:57 +02:00
|
|
|
:placeholder="$t('list.kanban.addBucketPlaceholder')"
|
2020-11-22 17:32:35 +01:00
|
|
|
type="text"
|
|
|
|
v-focus.always
|
|
|
|
v-if="showNewBucketInput"
|
|
|
|
v-model="newBucketTitle"
|
|
|
|
/>
|
2021-01-17 18:57:57 +01:00
|
|
|
<x-button
|
2020-11-22 17:32:35 +01:00
|
|
|
@click="() => showNewBucketInput = true"
|
2021-01-17 18:57:57 +01:00
|
|
|
:shadow="false"
|
|
|
|
class="is-transparent is-fullwidth has-text-centered"
|
2021-10-01 21:26:47 +02:00
|
|
|
v-else
|
2022-01-04 19:58:06 +01:00
|
|
|
variant="secondary"
|
2021-01-17 18:57:57 +01:00
|
|
|
icon="plus"
|
|
|
|
>
|
2021-06-24 01:24:57 +02:00
|
|
|
{{ $t('list.kanban.addBucket') }}
|
2021-01-17 18:57:57 +01:00
|
|
|
</x-button>
|
2020-11-22 17:32:35 +01:00
|
|
|
</div>
|
2020-04-26 01:11:34 +02:00
|
|
|
</div>
|
|
|
|
|
2021-01-23 18:54:22 +01:00
|
|
|
<transition name="modal">
|
|
|
|
<modal
|
2021-11-01 18:19:59 +01:00
|
|
|
v-if="showBucketDeleteModal"
|
2021-01-23 18:54:22 +01:00
|
|
|
@close="showBucketDeleteModal = false"
|
|
|
|
@submit="deleteBucket()"
|
2021-08-19 19:55:13 +02:00
|
|
|
>
|
|
|
|
<template #header><span>{{ $t('list.kanban.deleteHeaderBucket') }}</span></template>
|
2021-11-13 20:48:06 +01:00
|
|
|
|
2021-08-19 19:55:13 +02:00
|
|
|
<template #text>
|
|
|
|
<p>{{ $t('list.kanban.deleteBucketText1') }}<br/>
|
2021-11-13 20:48:06 +01:00
|
|
|
{{ $t('list.kanban.deleteBucketText2') }}</p>
|
2021-08-19 19:55:13 +02:00
|
|
|
</template>
|
2021-01-23 18:54:22 +01:00
|
|
|
</modal>
|
|
|
|
</transition>
|
2021-11-14 21:33:53 +01:00
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
</ListWrapper>
|
2020-04-26 01:11:34 +02:00
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
2021-07-29 13:08:38 +02:00
|
|
|
import draggable from 'vuedraggable'
|
2021-10-17 16:21:55 +02:00
|
|
|
import cloneDeep from 'lodash.clonedeep'
|
2020-09-05 22:35:52 +02:00
|
|
|
|
2021-11-14 21:33:53 +01:00
|
|
|
import BucketModel from '../../models/bucket'
|
2020-09-05 22:35:52 +02:00
|
|
|
import {mapState} from 'vuex'
|
2021-11-14 21:33:53 +01:00
|
|
|
import Rights from '../../models/constants/rights.json'
|
2021-01-30 17:17:04 +01:00
|
|
|
import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
|
2021-11-14 21:33:53 +01:00
|
|
|
import ListWrapper from './ListWrapper'
|
2021-07-25 15:27:15 +02:00
|
|
|
import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
|
|
|
import Dropdown from '@/components/misc/dropdown.vue'
|
2021-07-07 21:58:29 +02:00
|
|
|
import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState'
|
2021-11-14 21:33:53 +01:00
|
|
|
import {calculateItemPosition} from '../../helpers/calculateItemPosition'
|
2021-09-11 17:53:03 +02:00
|
|
|
import KanbanCard from '@/components/tasks/partials/kanban-card'
|
2020-09-05 22:35:52 +02:00
|
|
|
|
2021-08-20 15:46:41 +02:00
|
|
|
const DRAG_OPTIONS = {
|
|
|
|
// sortable options
|
|
|
|
animation: 150,
|
|
|
|
ghostClass: 'ghost',
|
|
|
|
dragClass: 'task-dragging',
|
|
|
|
delayOnTouchOnly: true,
|
|
|
|
delay: 150,
|
|
|
|
}
|
|
|
|
|
2021-10-01 21:26:47 +02:00
|
|
|
const MIN_SCROLL_HEIGHT_PERCENT = 0.25
|
|
|
|
|
2020-09-05 22:35:52 +02:00
|
|
|
export default {
|
|
|
|
name: 'Kanban',
|
|
|
|
components: {
|
2021-11-14 21:33:53 +01:00
|
|
|
ListWrapper,
|
2021-07-28 21:56:29 +02:00
|
|
|
KanbanCard,
|
2021-01-30 17:17:04 +01:00
|
|
|
Dropdown,
|
2021-01-17 11:36:57 +01:00
|
|
|
FilterPopup,
|
2021-07-28 21:56:29 +02:00
|
|
|
draggable,
|
2020-09-05 22:35:52 +02:00
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
2021-10-01 21:26:47 +02:00
|
|
|
taskContainerRefs: {},
|
|
|
|
|
2021-08-20 15:46:41 +02:00
|
|
|
dragOptions: DRAG_OPTIONS,
|
|
|
|
|
2021-07-28 21:56:29 +02:00
|
|
|
drag: false,
|
|
|
|
dragBucket: false,
|
2020-09-05 22:35:52 +02:00
|
|
|
sourceBucket: 0,
|
|
|
|
|
|
|
|
showBucketDeleteModal: false,
|
|
|
|
bucketToDelete: 0,
|
2021-07-28 21:56:29 +02:00
|
|
|
bucketTitleEditable: false,
|
2020-09-05 22:35:52 +02:00
|
|
|
|
|
|
|
newTaskText: '',
|
|
|
|
showNewTaskInput: {},
|
|
|
|
newBucketTitle: '',
|
|
|
|
showNewBucketInput: false,
|
|
|
|
newTaskError: {},
|
|
|
|
showSetLimitInput: false,
|
2021-07-07 21:58:29 +02:00
|
|
|
collapsedBuckets: {},
|
2020-09-05 22:35:52 +02:00
|
|
|
|
|
|
|
// We're using this to show the loading animation only at the task when updating it
|
|
|
|
taskUpdating: {},
|
2020-12-08 18:49:28 +01:00
|
|
|
oneTaskUpdating: false,
|
2020-12-22 12:49:34 +01:00
|
|
|
|
|
|
|
params: {
|
|
|
|
filter_by: [],
|
|
|
|
filter_value: [],
|
|
|
|
filter_comparator: [],
|
|
|
|
filter_concat: 'and',
|
|
|
|
},
|
2020-09-05 22:35:52 +02:00
|
|
|
}
|
|
|
|
},
|
2021-10-03 15:54:24 +02:00
|
|
|
|
2020-09-05 22:35:52 +02:00
|
|
|
watch: {
|
2021-09-11 17:53:03 +02:00
|
|
|
loadBucketParameter: {
|
|
|
|
handler: 'loadBuckets',
|
|
|
|
immediate: true,
|
|
|
|
},
|
2020-09-05 22:35:52 +02:00
|
|
|
},
|
2021-07-28 21:56:29 +02:00
|
|
|
computed: {
|
2021-09-11 17:53:03 +02:00
|
|
|
isSavedFilter() {
|
|
|
|
return this.list.isSavedFilter && !this.list.isSavedFilter()
|
|
|
|
},
|
|
|
|
loadBucketParameter() {
|
|
|
|
return {
|
|
|
|
listId: this.$route.params.listId,
|
|
|
|
params: this.params,
|
|
|
|
}
|
|
|
|
},
|
2021-08-20 15:46:41 +02:00
|
|
|
bucketDraggableComponentData() {
|
|
|
|
return {
|
|
|
|
type: 'transition',
|
|
|
|
tag: 'div',
|
2021-11-13 20:48:06 +01:00
|
|
|
name: !this.dragBucket ? 'move-bucket' : null,
|
2021-10-01 21:26:47 +02:00
|
|
|
class: [
|
|
|
|
'kanban-bucket-container',
|
2021-11-13 20:48:06 +01:00
|
|
|
{'dragging-disabled': !this.canWrite},
|
2021-10-01 21:26:47 +02:00
|
|
|
],
|
2021-08-20 15:46:41 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
taskDraggableTaskComponentData() {
|
|
|
|
return {
|
|
|
|
type: 'transition',
|
|
|
|
tag: 'div',
|
2021-11-13 20:48:06 +01:00
|
|
|
name: !this.drag ? 'move-card' : null,
|
2021-10-01 21:26:47 +02:00
|
|
|
class: [
|
|
|
|
'dropper',
|
2021-11-13 20:48:06 +01:00
|
|
|
{'dragging-disabled': !this.canWrite},
|
2021-10-01 21:26:47 +02:00
|
|
|
],
|
2021-08-20 15:46:41 +02:00
|
|
|
}
|
|
|
|
},
|
2021-10-02 20:10:49 +02:00
|
|
|
buckets() {
|
|
|
|
return this.$store.state.kanban.buckets
|
2021-07-28 21:56:29 +02:00
|
|
|
},
|
|
|
|
...mapState({
|
|
|
|
loadedListId: state => state.kanban.listId,
|
|
|
|
loading: state => state[LOADING] && state[LOADING_MODULE] === 'kanban',
|
|
|
|
taskLoading: state => state[LOADING] && state[LOADING_MODULE] === 'tasks',
|
|
|
|
canWrite: state => state.currentList.maxRight > Rights.READ,
|
|
|
|
list: state => state.currentList,
|
|
|
|
}),
|
|
|
|
},
|
2020-09-05 22:35:52 +02:00
|
|
|
|
2021-11-13 20:48:06 +01:00
|
|
|
methods: {
|
2021-09-11 17:53:03 +02:00
|
|
|
loadBuckets() {
|
2020-09-05 22:35:52 +02:00
|
|
|
// Prevent trying to load buckets if the task popup view is active
|
|
|
|
if (this.$route.name !== 'list.kanban') {
|
|
|
|
return
|
2020-04-26 01:11:34 +02:00
|
|
|
}
|
2020-05-21 11:35:09 +02:00
|
|
|
|
2021-11-13 20:48:06 +01:00
|
|
|
const {listId, params} = this.loadBucketParameter
|
2020-05-21 11:35:09 +02:00
|
|
|
|
2021-09-11 17:53:03 +02:00
|
|
|
this.collapsedBuckets = getCollapsedBucketState(listId)
|
2021-07-07 21:58:29 +02:00
|
|
|
|
2020-09-05 22:35:52 +02:00
|
|
|
console.debug(`Loading buckets, loadedListId = ${this.loadedListId}, $route.params =`, this.$route.params)
|
2020-07-24 10:42:30 +02:00
|
|
|
|
2021-09-11 17:53:03 +02:00
|
|
|
this.$store.dispatch('kanban/loadBucketsForList', {listId, params})
|
2021-10-01 21:26:47 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
setTaskContainerRef(id, el) {
|
|
|
|
if (!el) return
|
|
|
|
this.taskContainerRefs[id] = el
|
|
|
|
},
|
|
|
|
|
2021-10-11 16:59:21 +02:00
|
|
|
handleTaskContainerScroll(id, listId, el) {
|
|
|
|
if (!el) {
|
|
|
|
return
|
|
|
|
}
|
2021-10-01 21:26:47 +02:00
|
|
|
const scrollTopMax = el.scrollHeight - el.clientHeight
|
|
|
|
const threshold = el.scrollTop + el.scrollTop * MIN_SCROLL_HEIGHT_PERCENT
|
|
|
|
if (scrollTopMax > threshold) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
this.$store.dispatch('kanban/loadNextTasksForBucket', {
|
2021-10-11 16:59:21 +02:00
|
|
|
listId: listId,
|
2021-10-01 21:26:47 +02:00
|
|
|
params: this.params,
|
|
|
|
bucketId: id,
|
|
|
|
})
|
2020-09-05 22:35:52 +02:00
|
|
|
},
|
2021-09-11 17:53:03 +02:00
|
|
|
|
|
|
|
updateTasks(bucketId, tasks) {
|
|
|
|
const newBucket = {
|
|
|
|
...this.$store.getters['kanban/getBucketById'](bucketId),
|
|
|
|
tasks,
|
|
|
|
}
|
|
|
|
|
2021-10-17 14:52:48 +02:00
|
|
|
this.$store.commit('kanban/setBucketById', newBucket)
|
2021-09-11 17:53:03 +02:00
|
|
|
},
|
|
|
|
|
2021-10-11 19:37:20 +02:00
|
|
|
async updateTaskPosition(e) {
|
2021-07-28 21:56:29 +02:00
|
|
|
this.drag = false
|
2020-04-26 01:11:34 +02:00
|
|
|
|
2021-07-28 21:56:29 +02:00
|
|
|
// While we could just pass the bucket index in through the function call, this would not give us the
|
|
|
|
// new bucket id when a task has been moved between buckets, only the new bucket. Using the data-bucket-id
|
|
|
|
// of the drop target works all the time.
|
2021-10-01 21:26:47 +02:00
|
|
|
const bucketIndex = parseInt(e.to.dataset.bucketIndex)
|
2020-04-26 01:11:34 +02:00
|
|
|
|
2021-07-28 21:56:29 +02:00
|
|
|
const newBucket = this.buckets[bucketIndex]
|
|
|
|
const task = newBucket.tasks[e.newIndex]
|
|
|
|
const taskBefore = newBucket.tasks[e.newIndex - 1] ?? null
|
|
|
|
const taskAfter = newBucket.tasks[e.newIndex + 1] ?? null
|
2020-04-26 01:11:34 +02:00
|
|
|
|
2021-10-17 16:21:55 +02:00
|
|
|
const newTask = cloneDeep(task) // cloning the task to avoid vuex store mutations
|
|
|
|
newTask.bucketId = newBucket.id,
|
2021-11-13 20:48:06 +01:00
|
|
|
newTask.kanbanPosition = calculateItemPosition(taskBefore !== null ? taskBefore.kanbanPosition : null, taskAfter !== null ? taskAfter.kanbanPosition : null)
|
2021-09-11 17:53:03 +02:00
|
|
|
|
2021-10-11 19:37:20 +02:00
|
|
|
try {
|
|
|
|
await this.$store.dispatch('tasks/update', newTask)
|
|
|
|
} finally {
|
|
|
|
this.taskUpdating[task.id] = false
|
|
|
|
this.oneTaskUpdating = false
|
|
|
|
}
|
2020-09-05 22:35:52 +02:00
|
|
|
},
|
2021-10-11 19:37:20 +02:00
|
|
|
|
2021-09-11 17:53:03 +02:00
|
|
|
toggleShowNewTaskInput(bucketId) {
|
|
|
|
this.showNewTaskInput[bucketId] = !this.showNewTaskInput[bucketId]
|
2020-09-05 22:35:52 +02:00
|
|
|
},
|
2020-04-26 01:11:34 +02:00
|
|
|
|
2021-10-11 19:37:20 +02:00
|
|
|
async addTaskToBucket(bucketId) {
|
2020-09-05 22:35:52 +02:00
|
|
|
if (this.newTaskText === '') {
|
2021-08-19 21:35:38 +02:00
|
|
|
this.newTaskError[bucketId] = true
|
2020-09-05 22:35:52 +02:00
|
|
|
return
|
|
|
|
}
|
2021-08-19 21:35:38 +02:00
|
|
|
this.newTaskError[bucketId] = false
|
2020-09-05 22:35:52 +02:00
|
|
|
|
2021-10-11 19:37:20 +02:00
|
|
|
const task = await this.$store.dispatch('tasks/createNewTask', {
|
2021-09-07 14:10:52 +02:00
|
|
|
title: this.newTaskText,
|
|
|
|
bucketId,
|
|
|
|
listId: this.$route.params.listId,
|
|
|
|
})
|
2021-10-11 19:37:20 +02:00
|
|
|
this.newTaskText = ''
|
|
|
|
this.$store.commit('kanban/addTaskToBucket', task)
|
|
|
|
this.scrollTaskContainerToBottom(bucketId)
|
2021-10-01 21:26:47 +02:00
|
|
|
},
|
2021-10-11 19:37:20 +02:00
|
|
|
|
2021-10-01 21:26:47 +02:00
|
|
|
scrollTaskContainerToBottom(bucketId) {
|
|
|
|
const bucketEl = this.taskContainerRefs[bucketId]
|
|
|
|
if (!bucketEl) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
bucketEl.scrollTop = bucketEl.scrollHeight
|
2020-09-05 22:35:52 +02:00
|
|
|
},
|
2021-10-11 19:37:20 +02:00
|
|
|
|
|
|
|
async createNewBucket() {
|
2020-09-05 22:35:52 +02:00
|
|
|
if (this.newBucketTitle === '') {
|
|
|
|
return
|
|
|
|
}
|
2020-04-26 01:11:34 +02:00
|
|
|
|
2020-09-05 22:35:52 +02:00
|
|
|
const newBucket = new BucketModel({
|
|
|
|
title: this.newBucketTitle,
|
|
|
|
listId: parseInt(this.$route.params.listId),
|
|
|
|
})
|
2020-04-26 01:11:34 +02:00
|
|
|
|
2021-10-11 19:37:20 +02:00
|
|
|
await this.$store.dispatch('kanban/createBucket', newBucket)
|
|
|
|
this.newBucketTitle = ''
|
|
|
|
this.showNewBucketInput = false
|
2020-09-05 22:35:52 +02:00
|
|
|
},
|
2021-10-11 19:37:20 +02:00
|
|
|
|
2020-09-05 22:35:52 +02:00
|
|
|
deleteBucketModal(bucketId) {
|
|
|
|
if (this.buckets.length <= 1) {
|
|
|
|
return
|
|
|
|
}
|
2020-04-26 01:11:34 +02:00
|
|
|
|
2020-09-05 22:35:52 +02:00
|
|
|
this.bucketToDelete = bucketId
|
|
|
|
this.showBucketDeleteModal = true
|
|
|
|
},
|
2021-10-11 19:37:20 +02:00
|
|
|
|
|
|
|
async deleteBucket() {
|
|
|
|
const bucket = new BucketModel({
|
2020-09-05 22:35:52 +02:00
|
|
|
id: this.bucketToDelete,
|
2021-10-11 13:57:02 +02:00
|
|
|
listId: parseInt(this.$route.params.listId),
|
2021-10-11 19:37:20 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
try {
|
|
|
|
await this.$store.dispatch('kanban/deleteBucket', {
|
|
|
|
bucket,
|
|
|
|
params: this.params,
|
2020-09-05 22:35:52 +02:00
|
|
|
})
|
2021-10-11 19:37:20 +02:00
|
|
|
this.$message.success({message: this.$t('list.kanban.deleteBucketSuccess')})
|
|
|
|
} finally {
|
|
|
|
this.showBucketDeleteModal = false
|
|
|
|
}
|
2020-09-05 22:35:52 +02:00
|
|
|
},
|
2021-10-11 19:37:20 +02:00
|
|
|
|
2021-07-28 21:56:29 +02:00
|
|
|
focusBucketTitle(e) {
|
|
|
|
// This little helper allows us to drag a bucket around at the title without focusing on it right away.
|
|
|
|
this.bucketTitleEditable = true
|
|
|
|
this.$nextTick(() => e.target.focus())
|
|
|
|
},
|
2021-09-11 17:53:03 +02:00
|
|
|
|
2021-10-11 19:37:20 +02:00
|
|
|
async saveBucketTitle(bucketId, bucketTitle) {
|
2021-09-11 17:53:03 +02:00
|
|
|
const updatedBucketData = {
|
2020-09-05 22:35:52 +02:00
|
|
|
id: bucketId,
|
|
|
|
title: bucketTitle,
|
|
|
|
}
|
2020-04-26 01:11:34 +02:00
|
|
|
|
2021-10-11 19:37:20 +02:00
|
|
|
await this.$store.dispatch('kanban/updateBucketTitle', updatedBucketData)
|
|
|
|
this.bucketTitleEditable = false
|
|
|
|
this.$message.success({message: this.$t('list.kanban.bucketTitleSavedSuccess')})
|
2020-09-05 22:35:52 +02:00
|
|
|
},
|
2021-09-11 17:53:03 +02:00
|
|
|
|
2021-10-02 20:10:49 +02:00
|
|
|
updateBuckets(value) {
|
|
|
|
// (1) buckets get updated in store and tasks positions get invalidated
|
|
|
|
this.$store.commit('kanban/setBuckets', value)
|
2020-09-05 22:35:52 +02:00
|
|
|
},
|
2021-10-02 20:10:49 +02:00
|
|
|
|
2021-07-28 21:56:29 +02:00
|
|
|
updateBucketPosition(e) {
|
2021-10-02 20:10:49 +02:00
|
|
|
// (2) bucket positon is changed
|
2021-07-28 21:56:29 +02:00
|
|
|
this.dragBucket = false
|
|
|
|
|
|
|
|
const bucket = this.buckets[e.newIndex]
|
|
|
|
const bucketBefore = this.buckets[e.newIndex - 1] ?? null
|
|
|
|
const bucketAfter = this.buckets[e.newIndex + 1] ?? null
|
|
|
|
|
2021-09-11 17:53:03 +02:00
|
|
|
const updatedData = {
|
|
|
|
id: bucket.id,
|
|
|
|
position: calculateItemPosition(bucketBefore !== null ? bucketBefore.position : null, bucketAfter !== null ? bucketAfter.position : null),
|
|
|
|
}
|
2021-07-28 21:56:29 +02:00
|
|
|
|
2021-09-11 17:53:03 +02:00
|
|
|
this.$store.dispatch('kanban/updateBucket', updatedData)
|
2021-07-28 21:56:29 +02:00
|
|
|
},
|
2021-10-11 19:37:20 +02:00
|
|
|
|
|
|
|
async setBucketLimit(bucketId, limit) {
|
2021-09-11 17:53:03 +02:00
|
|
|
if (limit < 0) {
|
2021-04-15 16:58:48 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-09-11 17:53:03 +02:00
|
|
|
const newBucket = {
|
|
|
|
...this.$store.getters['kanban/getBucketById'](bucketId),
|
|
|
|
limit,
|
|
|
|
}
|
|
|
|
|
2021-10-11 19:37:20 +02:00
|
|
|
await this.$store.dispatch('kanban/updateBucket', newBucket)
|
|
|
|
this.$message.success({message: this.$t('list.kanban.bucketLimitSavedSuccess')})
|
2021-04-15 16:58:48 +02:00
|
|
|
},
|
2021-10-11 19:37:20 +02:00
|
|
|
|
2020-09-05 22:35:52 +02:00
|
|
|
shouldAcceptDrop(bucket) {
|
|
|
|
return bucket.id === this.sourceBucket || // When dragging from a bucket who has its limit reached, dragging should still be possible
|
|
|
|
bucket.limit === 0 || // If there is no limit set, dragging & dropping should always work
|
|
|
|
bucket.tasks.length < bucket.limit // Disallow dropping to buckets which have their limit reached
|
2020-04-26 01:11:34 +02:00
|
|
|
},
|
2021-10-11 19:37:20 +02:00
|
|
|
|
2021-09-07 18:38:53 +02:00
|
|
|
dragstart(bucket) {
|
|
|
|
this.drag = true
|
|
|
|
this.sourceBucket = bucket.id
|
|
|
|
},
|
2021-10-11 19:37:20 +02:00
|
|
|
|
|
|
|
async toggleDoneBucket(bucket) {
|
2021-09-11 17:53:03 +02:00
|
|
|
const newBucket = {
|
|
|
|
...bucket,
|
|
|
|
isDoneBucket: !bucket.isDoneBucket,
|
|
|
|
}
|
2021-10-11 19:37:20 +02:00
|
|
|
await this.$store.dispatch('kanban/updateBucket', newBucket)
|
|
|
|
this.$message.success({message: this.$t('list.kanban.doneBucketSavedSuccess')})
|
2021-03-24 21:16:56 +01:00
|
|
|
},
|
2021-10-11 19:37:20 +02:00
|
|
|
|
2021-07-07 21:58:29 +02:00
|
|
|
collapseBucket(bucket) {
|
2021-08-19 21:35:38 +02:00
|
|
|
this.collapsedBuckets[bucket.id] = true
|
2021-07-07 21:58:29 +02:00
|
|
|
saveCollapsedBucketState(this.$route.params.listId, this.collapsedBuckets)
|
|
|
|
},
|
|
|
|
unCollapseBucket(bucket) {
|
|
|
|
if (!this.collapsedBuckets[bucket.id]) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-08-19 21:35:38 +02:00
|
|
|
this.collapsedBuckets[bucket.id] = false
|
2021-07-07 21:58:29 +02:00
|
|
|
saveCollapsedBucketState(this.$route.params.listId, this.collapsedBuckets)
|
|
|
|
},
|
2020-09-05 22:35:52 +02:00
|
|
|
},
|
|
|
|
}
|
2020-04-26 01:11:34 +02:00
|
|
|
</script>
|
2021-10-18 14:20:31 +02:00
|
|
|
|
|
|
|
<style lang="scss">
|
|
|
|
$ease-out: all .3s cubic-bezier(0.23, 1, 0.32, 1);
|
|
|
|
$bucket-width: 300px;
|
|
|
|
$bucket-header-height: 60px;
|
|
|
|
$bucket-right-margin: 1rem;
|
|
|
|
|
|
|
|
$crazy-height-calculation: '100vh - 4.5rem - 1.5rem - 1rem - 1.5rem - 11px';
|
|
|
|
$crazy-height-calculation-tasks: '#{$crazy-height-calculation} - 1rem - 2.5rem - 2rem - #{$button-height} - 1rem';
|
|
|
|
$filter-container-height: '1rem - #{$switch-view-height}';
|
|
|
|
|
2021-10-20 14:47:06 +02:00
|
|
|
// FIXME:
|
2021-10-18 14:20:31 +02:00
|
|
|
.app-content.list\.kanban {
|
2021-10-20 14:47:06 +02:00
|
|
|
padding-bottom: 0 !important;
|
2021-10-18 14:20:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
.kanban {
|
|
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
overflow-y: hidden;
|
|
|
|
height: calc(#{$crazy-height-calculation});
|
|
|
|
margin: 0 -1.5rem;
|
|
|
|
padding: 0 1.5rem;
|
|
|
|
|
|
|
|
@media screen and (max-width: $tablet) {
|
|
|
|
height: calc(#{$crazy-height-calculation} - #{$filter-container-height});
|
|
|
|
}
|
|
|
|
|
|
|
|
&-bucket-container {
|
|
|
|
display: flex;
|
|
|
|
align-items: flex-start;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ghost {
|
|
|
|
background: transparent !important;
|
2021-11-22 22:12:54 +01:00
|
|
|
border: 3px dashed var(--grey-300) !important;
|
2021-10-18 14:20:31 +02:00
|
|
|
box-shadow: none !important;
|
|
|
|
|
|
|
|
* {
|
|
|
|
opacity: 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.bucket {
|
2021-11-22 22:12:54 +01:00
|
|
|
background-color: var(--grey-100);
|
2021-10-18 14:20:31 +02:00
|
|
|
border-radius: $radius;
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
margin: 0 $bucket-right-margin 0 0;
|
|
|
|
max-height: 100%;
|
|
|
|
min-height: 20px;
|
|
|
|
width: $bucket-width;
|
|
|
|
|
|
|
|
.tasks {
|
|
|
|
max-height: calc(#{$crazy-height-calculation-tasks});
|
|
|
|
overflow: auto;
|
|
|
|
|
|
|
|
@media screen and (max-width: $tablet) {
|
|
|
|
max-height: calc(#{$crazy-height-calculation-tasks} - #{$filter-container-height});
|
|
|
|
}
|
|
|
|
|
|
|
|
.dropper {
|
|
|
|
&, > div {
|
|
|
|
min-height: 40px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.move-card-move {
|
|
|
|
transition: transform $transition-duration;
|
|
|
|
}
|
|
|
|
|
|
|
|
.no-move {
|
|
|
|
transition: transform 0s;
|
|
|
|
}
|
|
|
|
|
|
|
|
h2 {
|
|
|
|
font-size: 1rem;
|
|
|
|
margin: 0;
|
|
|
|
font-weight: 600 !important;
|
|
|
|
}
|
|
|
|
|
|
|
|
&.new-bucket {
|
|
|
|
// Because of reasons, this button ignores the margin we gave it to the right.
|
|
|
|
// To make it still look like it has some, we modify the container to have a padding of 1rem,
|
|
|
|
// which is the same as the margin it should have. Then we make the container itself bigger
|
|
|
|
// to hide the fact we just made the button smaller.
|
|
|
|
min-width: calc(#{$bucket-width} + 1rem);
|
|
|
|
background: transparent;
|
|
|
|
padding-right: 1rem;
|
|
|
|
|
|
|
|
.button {
|
2021-11-22 22:12:54 +01:00
|
|
|
background: var(--grey-100);
|
2021-10-18 14:20:31 +02:00
|
|
|
width: 100%;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
a.dropdown-item {
|
|
|
|
padding-right: 1rem;
|
|
|
|
}
|
|
|
|
|
|
|
|
&.is-collapsed {
|
|
|
|
transform: rotate(90deg) translateX(math.div($bucket-width, 2) - math.div($bucket-header-height, 2));
|
|
|
|
// 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(#{(math.div($bucket-width, 2) - math.div($bucket-header-height, 2)) * -1} + #{$bucket-right-margin});
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
.tasks, .bucket-footer {
|
|
|
|
display: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.bucket-header {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: space-between;
|
|
|
|
padding: .5rem;
|
|
|
|
height: $bucket-header-height;
|
|
|
|
|
|
|
|
.limit {
|
2021-12-26 18:13:28 +01:00
|
|
|
padding: 0 .5rem;
|
2021-10-18 14:20:31 +02:00
|
|
|
font-weight: bold;
|
|
|
|
|
|
|
|
&.is-max {
|
2021-11-22 22:12:54 +01:00
|
|
|
color: var(--danger);
|
2021-10-18 14:20:31 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.title.input {
|
|
|
|
height: auto;
|
|
|
|
padding: .4rem .5rem;
|
|
|
|
display: inline-block;
|
|
|
|
cursor: pointer;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-20 14:33:36 +02:00
|
|
|
:deep(.dropdown-trigger) {
|
2021-10-18 14:20:31 +02:00
|
|
|
cursor: pointer;
|
|
|
|
padding: .5rem;
|
|
|
|
}
|
|
|
|
|
|
|
|
.bucket-footer {
|
|
|
|
padding: .5rem;
|
|
|
|
|
|
|
|
.button {
|
|
|
|
background-color: transparent;
|
|
|
|
|
|
|
|
&:hover {
|
2021-11-22 22:12:54 +01:00
|
|
|
background-color: var(--white);
|
2021-10-18 14:20:31 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.task-dragging {
|
|
|
|
transition: transform 0.18s ease;
|
|
|
|
transform: rotateZ(3deg)
|
|
|
|
}
|
2021-10-18 14:22:47 +02:00
|
|
|
|
|
|
|
.move-card-leave-from,
|
|
|
|
.move-card-leave-to,
|
|
|
|
.move-card-leave-active {
|
|
|
|
display: none;
|
|
|
|
}
|
2021-11-08 15:46:39 +01:00
|
|
|
|
|
|
|
@include modal-transition();
|
2021-10-18 14:20:31 +02:00
|
|
|
</style>
|