feat: ListKanban script setup #2643

Merged
konrad merged 1 commits from dpschen/frontend:feature/feat-ListKanban-script-setup into main 2022-11-07 17:23:13 +00:00
7 changed files with 391 additions and 418 deletions

View File

@ -1,19 +1,20 @@
import type {IBucket} from '@/modelTypes/IBucket'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
const key = 'collapsedBuckets' const key = 'collapsedBuckets'
const getAllState = () => { export type CollapsedBuckets = {[id: IBucket['id']]: boolean}
const saved = localStorage.getItem(key)
if (saved === null) {
return {}
}
return JSON.parse(saved) function getAllState() {
const saved = localStorage.getItem(key)
return saved === null
? {}
: JSON.parse(saved)
} }
export const saveCollapsedBucketState = ( export const saveCollapsedBucketState = (
listId: IList['id'], listId: IList['id'],
collapsedBuckets, collapsedBuckets: CollapsedBuckets,
) => { ) => {
const state = getAllState() const state = getAllState()
state[listId] = collapsedBuckets state[listId] = collapsedBuckets
@ -25,11 +26,9 @@ export const saveCollapsedBucketState = (
localStorage.setItem(key, JSON.stringify(state)) localStorage.setItem(key, JSON.stringify(state))
} }
export const getCollapsedBucketState = (listId : IList['id']) => { export function getCollapsedBucketState(listId : IList['id']) {
const state = getAllState() const state = getAllState()
if (typeof state[listId] !== 'undefined') { return typeof state[listId] !== 'undefined'
return state[listId] ? state[listId]
} : {}
return {}
} }

View File

@ -69,7 +69,7 @@ export const useKanbanStore = defineStore('kanban', {
getters: { getters: {
getBucketById(state) { getBucketById(state) {
return (bucketId: IBucket['id']) => findById(state.buckets, bucketId) return (bucketId: IBucket['id']): IBucket | undefined => findById(state.buckets, bucketId)
}, },
getTaskById(state) { getTaskById(state) {
@ -265,11 +265,12 @@ export const useKanbanStore = defineStore('kanban', {
// Clear everything to prevent having old buckets in the list if loading the buckets from this list takes a few moments // Clear everything to prevent having old buckets in the list if loading the buckets from this list takes a few moments
this.setBuckets([]) this.setBuckets([])
params.per_page = TASKS_PER_BUCKET
const bucketService = new BucketService() const bucketService = new BucketService()
try { try {
const buckets = await bucketService.getAll({listId}, params) const buckets = await bucketService.getAll({listId}, {
...params,
per_page: TASKS_PER_BUCKET,
})
this.setBuckets(buckets) this.setBuckets(buckets)
this.setListId(listId) this.setListId(listId)
return buckets return buckets

View File

@ -57,7 +57,7 @@ import {useBaseStore} from '@/stores/base'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import Foo from '@/components/misc/flatpickr/Flatpickr.vue' import Foo from '@/components/misc/flatpickr/Flatpickr.vue'
import ListWrapper from './ListWrapper.vue' import ListWrapper from '@/components/list/ListWrapper.vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue' import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import TaskForm from '@/components/tasks/TaskForm.vue' import TaskForm from '@/components/tasks/TaskForm.vue'

View File

@ -1,12 +1,13 @@
<template> <template>
<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(listId)"> <div class="filter-container" v-if="!isSavedFilter(list)">
Review

Shouldn't this use the listId?

Shouldn't this use the `listId`?
Review

Currently it wants the list. Typescript helped to find this :)

Maybe we should refactor it to something like isSavedFilterById.

Currently [it wants the list](https://kolaente.dev/vikunja/frontend/src/branch/main/src/services/savedFilter.ts#L41). Typescript helped to find this :) Maybe we should refactor it to something like `isSavedFilterById`.
Review

Refactoring this sounds like a good idea.

Refactoring this sounds like a good idea.
Review

Or don't refactor it, this was just something I stumbled upon.

Or don't refactor it, this was just something I stumbled upon.
<div class="items"> <div class="items">
<filter-popup <filter-popup v-model="params" />
v-model="params"
@update:modelValue="loadBuckets"
/>
</div> </div>
</div> </div>
</template> </template>
@ -18,7 +19,7 @@
class="kanban kanban-bucket-container loader-container" class="kanban kanban-bucket-container loader-container"
> >
<draggable <draggable
v-bind="dragOptions" v-bind="DRAG_OPTIONS"
:modelValue="buckets" :modelValue="buckets"
@update:modelValue="updateBuckets" @update:modelValue="updateBuckets"
@end="updateBucketPosition" @end="updateBucketPosition"
@ -26,7 +27,7 @@
group="buckets" group="buckets"
:disabled="!canWrite || newTaskInputFocused" :disabled="!canWrite || newTaskInputFocused"
tag="ul" tag="ul"
:item-key="({id}) => `bucket${id}`" :item-key="({id}: IBucket) => `bucket${id}`"
:component-data="bucketDraggableComponentData" :component-data="bucketDraggableComponentData"
> >
<template #item="{element: bucket, index: bucketIndex }"> <template #item="{element: bucket, index: bucketIndex }">
@ -43,9 +44,9 @@
<icon icon="check-double"/> <icon icon="check-double"/>
</span> </span>
<h2 <h2
@keydown.enter.prevent.stop="$event.target.blur()" @keydown.enter.prevent.stop="($event.target as HTMLElement).blur()"
@keydown.esc.prevent.stop="$event.target.blur()" @keydown.esc.prevent.stop="($event.target as HTMLElement).blur()"
@blur="saveBucketTitle(bucket.id, $event.target.textContent)" @blur="saveBucketTitle(bucket.id, ($event.target as HTMLElement).textContent as string)"
@click="focusBucketTitle" @click="focusBucketTitle"
class="title input" class="title input"
:contenteditable="(bucketTitleEditable && canWrite && !collapsedBuckets[bucket.id]) ? true : undefined" :contenteditable="(bucketTitleEditable && canWrite && !collapsedBuckets[bucket.id]) ? true : undefined"
@ -71,7 +72,7 @@
@keyup.esc="() => showSetLimitInput = false" @keyup.esc="() => showSetLimitInput = false"
@keyup.enter="() => showSetLimitInput = false" @keyup.enter="() => showSetLimitInput = false"
:value="bucket.limit" :value="bucket.limit"
@input="(event) => setBucketLimit(bucket.id, parseInt(event.target.value))" @input="(event) => setBucketLimit(bucket.id, parseInt((event.target as HTMLInputElement).value))"
class="input" class="input"
type="number" type="number"
min="0" min="0"
@ -122,7 +123,7 @@
</div> </div>
<draggable <draggable
v-bind="dragOptions" v-bind="DRAG_OPTIONS"
:modelValue="bucket.tasks" :modelValue="bucket.tasks"
@update:modelValue="(tasks) => updateTasks(bucket.id, tasks)" @update:modelValue="(tasks) => updateTasks(bucket.id, tasks)"
@start="() => dragstart(bucket)" @start="() => dragstart(bucket)"
@ -131,7 +132,7 @@
:disabled="!canWrite" :disabled="!canWrite"
:data-bucket-index="bucketIndex" :data-bucket-index="bucketIndex"
tag="ul" tag="ul"
:item-key="(task) => `bucket${bucket.id}-task${task.id}`" :item-key="(task: ITask) => `bucket${bucket.id}-task${task.id}`"
:component-data="getTaskDraggableTaskComponentData(bucket)" :component-data="getTaskDraggableTaskComponentData(bucket)"
> >
<template #footer> <template #footer>
@ -184,7 +185,7 @@
:disabled="loading || undefined" :disabled="loading || undefined"
@blur="() => showNewBucketInput = false" @blur="() => showNewBucketInput = false"
@keyup.enter="createNewBucket" @keyup.enter="createNewBucket"
@keyup.esc="$event.target.blur()" @keyup.esc="($event.target as HTMLInputElement).blur()"
class="input" class="input"
:placeholder="$t('list.kanban.addBucketPlaceholder')" :placeholder="$t('list.kanban.addBucketPlaceholder')"
type="text" type="text"
@ -224,27 +225,35 @@
</ListWrapper> </ListWrapper>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {defineComponent} from 'vue' import {computed, nextTick, ref, watch, type PropType} from 'vue'
import {useI18n} from 'vue-i18n'
import draggable from 'zhyswan-vuedraggable' import draggable from 'zhyswan-vuedraggable'
import cloneDeep from 'lodash.clonedeep' import cloneDeep from 'lodash.clonedeep'
import {mapState} from 'pinia'
import BucketModel from '../../models/bucket'
import {RIGHTS as Rights} from '@/constants/rights' import {RIGHTS as Rights} from '@/constants/rights'
import ListWrapper from './ListWrapper.vue' import BucketModel from '@/models/bucket'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
import Dropdown from '@/components/misc/dropdown.vue' import type {IBucket} from '@/modelTypes/IBucket'
import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState' import type {IList} from '@/modelTypes/IList'
import {calculateItemPosition} from '../../helpers/calculateItemPosition' import type {ITask} from '@/modelTypes/ITask'
import KanbanCard from '@/components/tasks/partials/kanban-card.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import {isSavedFilter} from '@/services/savedFilter'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import {useKanbanStore} from '@/stores/kanban' import {useKanbanStore} from '@/stores/kanban'
import ListWrapper from '@/components/list/ListWrapper.vue'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
import KanbanCard from '@/components/tasks/partials/kanban-card.vue'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import {getCollapsedBucketState, saveCollapsedBucketState, type CollapsedBuckets} from '@/helpers/saveCollapsedBucketState'
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import {isSavedFilter} from '@/services/savedFilter'
import {success} from '@/message'
const DRAG_OPTIONS = { const DRAG_OPTIONS = {
// sortable options // sortable options
animation: 150, animation: 150,
@ -252,133 +261,106 @@ const DRAG_OPTIONS = {
dragClass: 'task-dragging', dragClass: 'task-dragging',
delayOnTouchOnly: true, delayOnTouchOnly: true,
delay: 150, delay: 150,
} } as const
const MIN_SCROLL_HEIGHT_PERCENT = 0.25 const MIN_SCROLL_HEIGHT_PERCENT = 0.25
export default defineComponent({ const props = defineProps({
name: 'Kanban',
components: {
DropdownItem,
ListWrapper,
KanbanCard,
Dropdown,
FilterPopup,
draggable,
},
props: {
listId: { listId: {
type: Number, type: Number as PropType<IList['id']>,
required: true, required: true,
}, },
}, })
data() { const {t} = useI18n({useScope: 'global'})
return {
taskContainerRefs: {},
dragOptions: DRAG_OPTIONS, const baseStore = useBaseStore()
const kanbanStore = useKanbanStore()
const taskStore = useTaskStore()
drag: false, const taskContainerRefs = ref<{[id: IBucket['id']]: HTMLElement}>({})
dragBucket: false,
sourceBucket: 0,
showBucketDeleteModal: false, const drag = ref(false)
bucketToDelete: 0, const dragBucket = ref(false)
bucketTitleEditable: false, const sourceBucket = ref(0)
newTaskText: '', const showBucketDeleteModal = ref(false)
showNewTaskInput: {}, const bucketToDelete = ref(0)
newBucketTitle: '', const bucketTitleEditable = ref(false)
showNewBucketInput: false,
newTaskError: {},
showSetLimitInput: false,
collapsedBuckets: {},
newTaskInputFocused: false,
// We're using this to show the loading animation only at the task when updating it const newTaskText = ref('')
taskUpdating: {}, const showNewTaskInput = ref<{[id: IBucket['id']]: boolean}>({})
oneTaskUpdating: false,
params: { const newBucketTitle = ref('')
const showNewBucketInput = ref(false)
const newTaskError = ref<{[id: IBucket['id']]: boolean}>({})
const newTaskInputFocused = ref(false)
const showSetLimitInput = ref(false)
const collapsedBuckets = ref<CollapsedBuckets>({})
// We're using this to show the loading animation only at the task when updating it
const taskUpdating = ref<{[id: ITask['id']]: boolean}>({})
const oneTaskUpdating = ref(false)
const params = ref({
filter_by: [], filter_by: [],
filter_value: [], filter_value: [],
filter_comparator: [], filter_comparator: [],
filter_concat: 'and', filter_concat: 'and',
}, })
}
},
watch: { const getTaskDraggableTaskComponentData = computed(() => (bucket: IBucket) => {
loadBucketParameter: { return {
handler: 'loadBuckets', ref: (el: HTMLElement) => setTaskContainerRef(bucket.id, el),
immediate: true, onScroll: (event: Event) => handleTaskContainerScroll(bucket.id, bucket.listId, event.target as HTMLElement),
},
},
computed: {
getTaskDraggableTaskComponentData() {
return (bucket) => ({
ref: (el) => this.setTaskContainerRef(bucket.id, el),
onScroll: (event) => this.handleTaskContainerScroll(bucket.id, bucket.listId, event.target),
type: 'transition-group', type: 'transition-group',
name: !this.drag ? 'move-card' : null, name: !drag.value ? 'move-card' : null,
class: [ class: [
'tasks', 'tasks',
{'dragging-disabled': !this.canWrite}, {'dragging-disabled': !canWrite.value},
], ],
})
},
loadBucketParameter() {
return {
listId: this.listId,
params: this.params,
} }
}, })
bucketDraggableComponentData() {
return { const bucketDraggableComponentData = computed(() => ({
type: 'transition-group', type: 'transition-group',
name: !this.dragBucket ? 'move-bucket' : null, name: !dragBucket.value ? 'move-bucket' : null,
class: [ class: [
'kanban-bucket-container', 'kanban-bucket-container',
{'dragging-disabled': !this.canWrite}, {'dragging-disabled': !canWrite.value},
], ],
} }))
},
...mapState(useBaseStore, { const canWrite = computed(() => baseStore.currentList.maxRight > Rights.READ)
canWrite: state => state.currentList.maxRight > Rights.READ, const list = computed(() => baseStore.currentList)
list: state => state.currentList,
}), const buckets = computed(() => kanbanStore.buckets)
...mapState(useKanbanStore, { const loading = computed(() => kanbanStore.isLoading)
buckets: state => state.buckets,
loadedListId: state => state.listId, const taskLoading = computed(() => taskStore.isLoading)
loading: state => state.isLoading,
}), watch(
...mapState(useTaskStore, { () => ({
taskLoading: state => state.isLoading, listId: props.listId,
params: params.value,
}), }),
({listId, params}) => {
collapsedBuckets.value = getCollapsedBucketState(listId)
kanbanStore.loadBucketsForList({listId, params})
}, },
{
methods: { immediate: true,
isSavedFilter, deep: true,
loadBuckets() {
const {listId, params} = this.loadBucketParameter
this.collapsedBuckets = getCollapsedBucketState(listId)
console.debug(`Loading buckets, loadedListId = ${this.loadedListId}, $attrs = ${this.$attrs} $route.params =`, this.$route.params)
useKanbanStore().loadBucketsForList({listId, params})
}, },
)
setTaskContainerRef(id, el) { function setTaskContainerRef(id: IBucket['id'], el: HTMLElement) {
if (!el) return if (!el) return
this.taskContainerRefs[id] = el taskContainerRefs.value[id] = el
}, }
handleTaskContainerScroll(id, listId, el) { function handleTaskContainerScroll(id: IBucket['id'], listId: IList['id'], el: HTMLElement) {
if (!el) { if (!el) {
return return
} }
@ -388,32 +370,35 @@ export default defineComponent({
return return
} }
useKanbanStore().loadNextTasksForBucket({ kanbanStore.loadNextTasksForBucket({
listId: listId, listId: listId,
params: this.params, params: params.value,
bucketId: id, bucketId: id,
}) })
}, }
updateTasks(bucketId, tasks) { function updateTasks(bucketId: IBucket['id'], tasks: IBucket['tasks']) {
const kanbanStore = useKanbanStore() const bucket = kanbanStore.getBucketById(bucketId)
const newBucket = {
...kanbanStore.getBucketById(bucketId), if (bucket === undefined) {
tasks, return
} }
kanbanStore.setBucketById(newBucket) kanbanStore.setBucketById({
}, ...bucket,
tasks,
})
}
async updateTaskPosition(e) { async function updateTaskPosition(e) {
this.drag = false drag.value = false
// While we could just pass the bucket index in through the function call, this would not give us the // 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 // 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. // 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 = buckets.value[bucketIndex]
// HACK: // HACK:
// this is a hacky workaround for a known problem of vue.draggable.next when using the footer slot // this is a hacky workaround for a known problem of vue.draggable.next when using the footer slot
@ -431,7 +416,7 @@ export default defineComponent({
const task = newBucket.tasks[newTaskIndex] const task = newBucket.tasks[newTaskIndex]
const taskBefore = newBucket.tasks[newTaskIndex - 1] ?? null const taskBefore = newBucket.tasks[newTaskIndex - 1] ?? null
const taskAfter = newBucket.tasks[newTaskIndex + 1] ?? null const taskAfter = newBucket.tasks[newTaskIndex + 1] ?? null
this.taskUpdating[task.id] = true taskUpdating.value[task.id] = true
const newTask = cloneDeep(task) // cloning the task to avoid pinia store manipulation const newTask = cloneDeep(task) // cloning the task to avoid pinia store manipulation
newTask.bucketId = newBucket.id newTask.bucketId = newBucket.id
@ -441,7 +426,6 @@ export default defineComponent({
) )
try { try {
const taskStore = useTaskStore()
await taskStore.update(newTask) await taskStore.update(newTask)
// Make sure the first and second task don't both get position 0 assigned // Make sure the first and second task don't both get position 0 assigned
@ -457,177 +441,166 @@ export default defineComponent({
await taskStore.update(newTaskAfter) await taskStore.update(newTaskAfter)
} }
} finally { } finally {
this.taskUpdating[task.id] = false taskUpdating.value[task.id] = false
this.oneTaskUpdating = false oneTaskUpdating.value = false
} }
}, }
toggleShowNewTaskInput(bucketId) { function toggleShowNewTaskInput(bucketId: IBucket['id']) {
this.showNewTaskInput[bucketId] = !this.showNewTaskInput[bucketId] showNewTaskInput.value[bucketId] = !showNewTaskInput.value[bucketId]
this.newTaskInputFocused = false newTaskInputFocused.value = false
}, }
async addTaskToBucket(bucketId) { async function addTaskToBucket(bucketId: IBucket['id']) {
if (this.newTaskText === '') { if (newTaskText.value === '') {
this.newTaskError[bucketId] = true newTaskError.value[bucketId] = true
return return
} }
this.newTaskError[bucketId] = false newTaskError.value[bucketId] = false
const task = await useTaskStore().createNewTask({ const task = await taskStore.createNewTask({
title: this.newTaskText, title: newTaskText.value,
bucketId, bucketId,
listId: this.listId, listId: props.listId,
}) })
this.newTaskText = '' newTaskText.value = ''
useKanbanStore().addTaskToBucket(task) kanbanStore.addTaskToBucket(task)
this.scrollTaskContainerToBottom(bucketId) scrollTaskContainerToBottom(bucketId)
}, }
scrollTaskContainerToBottom(bucketId) { function scrollTaskContainerToBottom(bucketId: IBucket['id']) {
const bucketEl = this.taskContainerRefs[bucketId] const bucketEl = taskContainerRefs.value[bucketId]
if (!bucketEl) { if (!bucketEl) {
return return
} }
bucketEl.scrollTop = bucketEl.scrollHeight bucketEl.scrollTop = bucketEl.scrollHeight
}, }
async createNewBucket() { async function createNewBucket() {
if (this.newBucketTitle === '') { if (newBucketTitle.value === '') {
return return
} }
const newBucket = new BucketModel({ await kanbanStore.createBucket(new BucketModel({
title: this.newBucketTitle, title: newBucketTitle.value,
listId: this.listId, listId: props.listId,
}) }))
newBucketTitle.value = ''
showNewBucketInput.value = false
}
await useKanbanStore().createBucket(newBucket) function deleteBucketModal(bucketId: IBucket['id']) {
this.newBucketTitle = '' if (buckets.value.length <= 1) {
this.showNewBucketInput = false
},
deleteBucketModal(bucketId) {
if (this.buckets.length <= 1) {
return return
} }
this.bucketToDelete = bucketId bucketToDelete.value = bucketId
this.showBucketDeleteModal = true showBucketDeleteModal.value = true
}, }
async deleteBucket() {
const bucket = new BucketModel({
id: this.bucketToDelete,
listId: this.listId,
})
async function deleteBucket() {
try { try {
await useKanbanStore().deleteBucket({ await kanbanStore.deleteBucket({
bucket, bucket: new BucketModel({
params: this.params, id: bucketToDelete.value,
listId: props.listId,
}),
params: params.value,
}) })
this.$message.success({message: this.$t('list.kanban.deleteBucketSuccess')}) success({message: t('list.kanban.deleteBucketSuccess')})
} finally { } finally {
this.showBucketDeleteModal = false showBucketDeleteModal.value = false
} }
}, }
focusBucketTitle(e) { /** This little helper allows us to drag a bucket around at the title without focusing on it right away. */
// This little helper allows us to drag a bucket around at the title without focusing on it right away. async function focusBucketTitle(e: Event) {
this.bucketTitleEditable = true bucketTitleEditable.value = true
this.$nextTick(() => e.target.focus()) await nextTick()
}, const target = e.target as HTMLInputElement
target.focus()
}
async saveBucketTitle(bucketId, bucketTitle) { async function saveBucketTitle(bucketId: IBucket['id'], bucketTitle: string) {
const updatedBucketData = { await kanbanStore.updateBucketTitle({
id: bucketId, id: bucketId,
title: bucketTitle, title: bucketTitle,
} })
bucketTitleEditable.value = false
}
await useKanbanStore().updateBucketTitle(updatedBucketData) function updateBuckets(value: IBucket[]) {
this.bucketTitleEditable = false
},
updateBuckets(value) {
// (1) buckets get updated in store and tasks positions get invalidated // (1) buckets get updated in store and tasks positions get invalidated
useKanbanStore().setBuckets(value) kanbanStore.setBuckets(value)
}, }
updateBucketPosition(e) { // TODO: fix type
Review

What type should be used here?

What type should be used here?
Review

This was intended for the short future when I apply the changes of #1571 on top of this branch.

This was intended for the short future when I apply the changes of https://kolaente.dev/vikunja/frontend/pulls/1571 on top of this branch.
Review

Should be fine then.

Should be fine then.
function updateBucketPosition(e: {newIndex: number}) {
// (2) bucket positon is changed // (2) bucket positon is changed
this.dragBucket = false dragBucket.value = false
const bucket = this.buckets[e.newIndex] const bucket = buckets.value[e.newIndex]
const bucketBefore = this.buckets[e.newIndex - 1] ?? null const bucketBefore = buckets.value[e.newIndex - 1] ?? null
const bucketAfter = this.buckets[e.newIndex + 1] ?? null const bucketAfter = buckets.value[e.newIndex + 1] ?? null
const updatedData = { kanbanStore.updateBucket({
id: bucket.id, id: bucket.id,
position: calculateItemPosition( position: calculateItemPosition(
bucketBefore !== null ? bucketBefore.position : null, bucketBefore !== null ? bucketBefore.position : null,
bucketAfter !== null ? bucketAfter.position : null, bucketAfter !== null ? bucketAfter.position : null,
), ),
} })
}
useKanbanStore().updateBucket(updatedData) async function setBucketLimit(bucketId: IBucket['id'], limit: number) {
},
async setBucketLimit(bucketId, limit) {
if (limit < 0) { if (limit < 0) {
return return
} }
const kanbanStore = useKanbanStore() await kanbanStore.updateBucket({
const newBucket = {
...kanbanStore.getBucketById(bucketId), ...kanbanStore.getBucketById(bucketId),
limit, limit,
} })
success({message: t('list.kanban.bucketLimitSavedSuccess')})
}
await kanbanStore.updateBucket(newBucket) function shouldAcceptDrop(bucket: IBucket) {
this.$message.success({message: this.$t('list.kanban.bucketLimitSavedSuccess')})
},
shouldAcceptDrop(bucket) {
return ( return (
// When dragging from a bucket who has its limit reached, dragging should still be possible // When dragging from a bucket who has its limit reached, dragging should still be possible
bucket.id === this.sourceBucket || bucket.id === sourceBucket.value ||
// If there is no limit set, dragging & dropping should always work // If there is no limit set, dragging & dropping should always work
bucket.limit === 0 || bucket.limit === 0 ||
// Disallow dropping to buckets which have their limit reached // Disallow dropping to buckets which have their limit reached
bucket.tasks.length < bucket.limit bucket.tasks.length < bucket.limit
) )
}, }
dragstart(bucket) { function dragstart(bucket: IBucket) {
this.drag = true drag.value = true
this.sourceBucket = bucket.id sourceBucket.value = bucket.id
}, }
async toggleDoneBucket(bucket) { async function toggleDoneBucket(bucket: IBucket) {
const newBucket = { await kanbanStore.updateBucket({
...bucket, ...bucket,
isDoneBucket: !bucket.isDoneBucket, isDoneBucket: !bucket.isDoneBucket,
} })
await useKanbanStore().updateBucket(newBucket) success({message: t('list.kanban.doneBucketSavedSuccess')})
this.$message.success({message: this.$t('list.kanban.doneBucketSavedSuccess')}) }
},
collapseBucket(bucket) { function collapseBucket(bucket: IBucket) {
this.collapsedBuckets[bucket.id] = true collapsedBuckets.value[bucket.id] = true
saveCollapsedBucketState(this.listId, this.collapsedBuckets) saveCollapsedBucketState(props.listId, collapsedBuckets.value)
}, }
unCollapseBucket(bucket) {
if (!this.collapsedBuckets[bucket.id]) { function unCollapseBucket(bucket: IBucket) {
if (!collapsedBuckets.value[bucket.id]) {
return return
} }
this.collapsedBuckets[bucket.id] = false collapsedBuckets.value[bucket.id] = false
saveCollapsedBucketState(this.listId, this.collapsedBuckets) saveCollapsedBucketState(props.listId, collapsedBuckets.value)
}, }
},
})
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -143,7 +143,7 @@ import {ref, computed, toRef, nextTick, onMounted, type PropType, watch} from 'v
import draggable from 'zhyswan-vuedraggable' import draggable from 'zhyswan-vuedraggable'
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
import ListWrapper from './ListWrapper.vue' import ListWrapper from '@/components/list/ListWrapper.vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import ButtonLink from '@/components/misc/ButtonLink.vue' import ButtonLink from '@/components/misc/ButtonLink.vue'
import EditTask from '@/components/tasks/edit-task.vue' import EditTask from '@/components/tasks/edit-task.vue'

View File

@ -184,7 +184,7 @@ import {toRef, computed, type Ref} from 'vue'
import {useStorage} from '@vueuse/core' import {useStorage} from '@vueuse/core'
import ListWrapper from './ListWrapper.vue' import ListWrapper from '@/components/list/ListWrapper.vue'
import Done from '@/components/misc/Done.vue' import Done from '@/components/misc/Done.vue'
import User from '@/components/misc/user.vue' import User from '@/components/misc/user.vue'
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue' import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'