This repository has been archived on 2024-02-08. You can view files and clone it, but cannot push or open issues or pull requests.
frontend/src/views/list/ListKanban.vue

374 lines
9.7 KiB
Vue
Raw Normal View History

Kanban (#118) Add error message when trying to create an invalid new task in a bucket Prevent creation of new buckets if the bucket title is empty Disable deleting a bucket if it's the last one Disable dragging tasks when they are being updated Fix transition when opening tasks Send the user to list view by default Show loading spinner when updating multiple tasks Add loading spinner when moving tasks Add loading animation when bucket is loading / updating etc Add bucket title edit Fix creating new buckets Add loading animation Add removing buckets Fix creating a new bucket after tasks were moved Fix warning about labels on tasks Fix labels on tasks not updating after retrieval from api Fix property width Add closing and mobile design Make the task detail popup look good Move list views Move task detail view in a popup Add link to tasks Add saving the new task position after it was moved Fix creating new bucket Fix creating a new task Cleanup Disable user selection for task cards Fix drag placeholder Add dragging style to task Add placeholder + change animation duration More cleanup Cleanup / docs Working of dragging and dropping tasks Adjust markup and styling for new library Change kanban library to something that works Add basic calculation of new positions Don't try to create empty tasks Add indicator if a task is done Add moving tasks between buckets Make empty buckets a little smaller Add gimmick for button description Fix color Fix scrolling bucket layout Add creating a new bucket Add hiding the task input field Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/118
2020-04-25 23:11:34 +00:00
<template>
2021-12-10 14:29:28 +00:00
<ListWrapper class="list-kanban" :list-id="listId" viewName="kanban">
2021-11-14 20:33:53 +00:00
<template #header>
<div class="filter-container" v-if="!isSavedFilter">
<div class="items">
<filter-popup v-model="params" />
</div>
2021-11-14 20:33:53 +00:00
</div>
</template>
<template #default>
<div
:class="{ 'is-loading': loading && !oneTaskUpdating}"
class="kanban kanban-bucket-container loader-container"
>
<draggable
v-bind="DRAG_OPTIONS"
:modelValue="buckets"
@update:modelValue="updateBuckets"
@start="() => isDraggingBucket = true"
@end="updateBucketPosition"
group="buckets"
:disabled="!canWrite"
tag="transition-group"
:item-key="(id: number) => `bucket${id}`"
:component-data="bucketDraggableComponentData"
>
<template #item="{element: bucket, index: bucketIndex }">
<Bucket
class="bucket"
:bucket-index="bucketIndex"
:is-collapsed="collapsedBuckets[bucket.id]"
:can-write="canWrite"
:bucket="bucket"
:isOnlyBucketLeft="buckets.length <= 1"
:drag-options="DRAG_OPTIONS"
:params="params"
:should-accept-drop="shouldAcceptDrop(bucket)"
:isDraggingTask="isDraggingTask"
:taskUpdating="tasksUpdating"
@dragstart="dragstart"
@dragend="updateTaskPosition"
@openDeleteBucketModal="openDeleteBucketModal"
/>
</template>
</draggable>
<BucketNew
v-if="canWrite && !loading && buckets.length > 0"
class="bucket"
:listId="listId"
/>
</div>
<transition name="modal">
<modal
v-if="hasBucketDeleteModal"
@close="hasBucketDeleteModal = false"
@submit="deleteBucket()"
>
<template #header><span>{{ $t('list.kanban.deleteHeaderBucket') }}</span></template>
<template #text>
<p>{{ $t('list.kanban.deleteBucketText1') }}<br/>
{{ $t('list.kanban.deleteBucketText2') }}</p>
</template>
</modal>
</transition>
2021-11-14 20:33:53 +00:00
</template>
</ListWrapper>
Kanban (#118) Add error message when trying to create an invalid new task in a bucket Prevent creation of new buckets if the bucket title is empty Disable deleting a bucket if it's the last one Disable dragging tasks when they are being updated Fix transition when opening tasks Send the user to list view by default Show loading spinner when updating multiple tasks Add loading spinner when moving tasks Add loading animation when bucket is loading / updating etc Add bucket title edit Fix creating new buckets Add loading animation Add removing buckets Fix creating a new bucket after tasks were moved Fix warning about labels on tasks Fix labels on tasks not updating after retrieval from api Fix property width Add closing and mobile design Make the task detail popup look good Move list views Move task detail view in a popup Add link to tasks Add saving the new task position after it was moved Fix creating new bucket Fix creating a new task Cleanup Disable user selection for task cards Fix drag placeholder Add dragging style to task Add placeholder + change animation duration More cleanup Cleanup / docs Working of dragging and dropping tasks Adjust markup and styling for new library Change kanban library to something that works Add basic calculation of new positions Don't try to create empty tasks Add indicator if a task is done Add moving tasks between buckets Make empty buckets a little smaller Add gimmick for button description Fix color Fix scrolling bucket layout Add creating a new bucket Add hiding the task input field Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/118
2020-04-25 23:11:34 +00:00
</template>
<script setup lang="ts">
import {ref, computed, watch} from 'vue'
import draggable from 'zhyswan-vuedraggable'
import cloneDeep from 'lodash.clonedeep'
import {useI18n} from 'vue-i18n'
import {SortableEvent} from 'sortablejs'
import {success} from '@/message'
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState'
import ListModel from '@/models/list'
import BucketModel from '@/models/bucket'
import TaskModel from '@/models/task'
import Rights from '@/models/constants/rights.json'
import ListWrapper from './ListWrapper.vue'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
import Bucket from '@/features/kanban/Bucket.vue'
import BucketNew from '@/features/kanban/BucketNew.vue'
const DRAG_OPTIONS = {
// sortable options
animation: 150,
ghostClass: 'ghost',
dragClass: 'task-dragging',
delayOnTouchOnly: true,
delay: 150,
}
const props = defineProps<{
listId: number
}>()
const {t} = useI18n()
/**
* Load collapsed Buckets
*/
const collapsedBuckets = ref<{ [listId: number]: any}>({})
watch(collapsedBuckets, (collapsedBuckets) => saveCollapsedBucketState(props.listId, collapsedBuckets))
/**
* Load Bucket Data
*/
const params = ref({
filter_by: [],
filter_value: [],
filter_comparator: [],
filter_concat: 'and',
2022-02-15 12:07:59 +00:00
})
watch(() => ({
listId: props.listId,
params: params.value,
}), ({listId, params}) => {
store.dispatch('kanban/loadBucketsForList', {listId, params})
collapsedBuckets.value = getCollapsedBucketState(listId)
}, {immediate: true, deep: true})
const list = computed(() => store.state.currentList as ListModel)
const buckets = computed({
get: () => store.state.kanban.buckets,
set(value: BucketModel[]) {
// (1) buckets get updated in store and tasks positions get invalidated
store.commit('kanban/setBuckets', value)
},
})
/**
* Template helpers
*/
const isSavedFilter = computed(() => list.value.isSavedFilter && list.value.isSavedFilter())
const canWrite = computed(() => store.state.currentList.maxRight > Rights.READ)
const loading = computed(() => store.state.loading && store.state.loadingModule === 'kanban')
// FIXME: seems unused ?
// const taskLoading = computed(() => store.state.loading && store.state.loadingModule === 'tasks')
/**
* Manage list of updateing tasks
*/
// FIXME: save globally if a specific task is updated.
// We're using this to show the loading animation only at the task when updating it
const tasksUpdating = ref<{ [taskId: TaskModel['id']]: boolean }>({})
function setTaskUpdating(id: TaskModel['id'], isUpdating: boolean) {
tasksUpdating.value[id] = isUpdating
}
const oneTaskUpdating = computed(() => Object.values(tasksUpdating.value).some((isUpdating) => isUpdating))
/**
* Delete Bucket
*/
const bucketToDeleteId = ref<BucketModel['id']>(0)
const hasBucketDeleteModal = ref(false)
function openDeleteBucketModal(bucketId: BucketModel['id']) {
if (buckets.value.length <= 1) {
return
}
bucketToDeleteId.value = bucketId
hasBucketDeleteModal.value = true
}
async function deleteBucket() {
try {
await store.dispatch('kanban/deleteBucket', {
bucket: new BucketModel({
id: bucketToDeleteId.value,
listId: props.listId,
}),
params: params.value,
})
success({message: t('list.kanban.deleteBucketSuccess')})
} finally {
hasBucketDeleteModal.value = false
}
}
/**
* Move / Drag Bucket
*/
const isDraggingBucket = ref(false)
const bucketDraggableComponentData = computed(() => ({
type: 'transition',
tag: 'div',
name: !isDraggingBucket.value ? 'move-bucket' : null,
class: [
'kanban-bucket-container',
{'dragging-disabled': !canWrite.value},
],
}))
function updateBucketPosition({newDraggableIndex}: SortableEvent) {
// (2) bucket positon is changed
isDraggingBucket.value = false
const bucket = buckets.value[newDraggableIndex]
const bucketBefore = buckets.value[newDraggableIndex - 1] ?? null
const bucketAfter = buckets.value[newDraggableIndex + 1] ?? null
store.dispatch('kanban/updateBucket', {
id: bucket.id,
position: calculateItemPosition(
bucketBefore !== null ? bucketBefore.position : null,
bucketAfter !== null ? bucketAfter.position : null,
),
})
}
/**
* Move / Drag Task
*/
const sourceBucketId = ref<number>()
function shouldAcceptDrop(bucket: BucketModel) {
return !isDraggingBucket.value && (
// It's always possible to drag a task inside the same a bucket
bucket.id === sourceBucketId.value ||
// 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
)
}
const isDraggingTask = ref(false)
function dragstart(bucketId: BucketModel['id']) {
isDraggingTask.value = true
sourceBucketId.value = bucketId
}
async function updateTaskPosition({newDraggableIndex, to}: SortableEvent) {
isDraggingTask.value = false
// FIXME: Cases
// e.to.dataset.bucketIndex === undefined
// e.newIndex === undefined
// 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.
const bucketIndex = parseInt(to.dataset.bucketIndex)
const newBucket = buckets.value[bucketIndex]
// HACK:
// 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 taskBefore = newBucket.tasks[newDraggableIndex - 1] ?? null
const taskAfter = newBucket.tasks[newDraggableIndex + 1] ?? null
const task = {
// cloning the task to avoid vuex store mutations
...cloneDeep(newBucket.tasks[newDraggableIndex]),
bucketId: newBucket.id,
kanbanPosition: calculateItemPosition(
taskBefore !== null ? taskBefore.kanbanPosition : null,
taskAfter !== null ? taskAfter.kanbanPosition : null,
),
} as TaskModel
setTaskUpdating(task.id, false)
try {
await store.dispatch('tasks/update', task)
} finally {
setTaskUpdating(task.id, false)
}
}
Kanban (#118) Add error message when trying to create an invalid new task in a bucket Prevent creation of new buckets if the bucket title is empty Disable deleting a bucket if it's the last one Disable dragging tasks when they are being updated Fix transition when opening tasks Send the user to list view by default Show loading spinner when updating multiple tasks Add loading spinner when moving tasks Add loading animation when bucket is loading / updating etc Add bucket title edit Fix creating new buckets Add loading animation Add removing buckets Fix creating a new bucket after tasks were moved Fix warning about labels on tasks Fix labels on tasks not updating after retrieval from api Fix property width Add closing and mobile design Make the task detail popup look good Move list views Move task detail view in a popup Add link to tasks Add saving the new task position after it was moved Fix creating new bucket Fix creating a new task Cleanup Disable user selection for task cards Fix drag placeholder Add dragging style to task Add placeholder + change animation duration More cleanup Cleanup / docs Working of dragging and dropping tasks Adjust markup and styling for new library Change kanban library to something that works Add basic calculation of new positions Don't try to create empty tasks Add indicator if a task is done Add moving tasks between buckets Make empty buckets a little smaller Add gimmick for button description Fix color Fix scrolling bucket layout Add creating a new bucket Add hiding the task input field Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/118
2020-04-25 23:11:34 +00:00
</script>
<style lang="scss">
// FIXME:
.app-content.list\.kanban {
padding-bottom: 0 !important;
}
$ease-out: all .3s cubic-bezier(0.23, 1, 0.32, 1);
// $crazy-height-calculation: '100vh - 4.5rem - 1.5rem - 1rem - 1.5rem - 11px';
// $filter-container-height: '1rem - #{$switch-view-height}';
.kanban {
--bucket-width: 300px;
--bucket-right-margin: 1rem;
overflow-x: auto;
overflow-y: hidden;
// height: calc(#{$crazy-height-calculation});
margin: 0 -1.5rem;
padding: 0 1.5rem;
scroll-snap-type: x mandatory;
// @media screen and (max-width: $tablet) {
// height: calc(#{$crazy-height-calculation} - #{$filter-container-height});
// }
}
.kanban-bucket-container {
display: flex;
}
.bucket {
margin-right: var(--bucket-right-margin);
max-height: 100%;
min-height: 20px;
width: var(--bucket-width);
}
.ghost {
position: relative;
* {
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;
}
}
.task-dragging {
transform: rotateZ(3deg);
transition: transform 0.18s ease;
}
2021-11-08 14:46:39 +00:00
@include modal-transition();
</style>