
Before this change, applying a filter and then scrolling a bucket would not use that filter when lazy loading the tasks in that bucket. That resulted in all tasks being loaded, regardless if the filter applied to them.
400 lines
10 KiB
TypeScript
400 lines
10 KiB
TypeScript
import {computed, readonly, ref} from 'vue'
|
|
import {defineStore, acceptHMRUpdate} from 'pinia'
|
|
import {klona} from 'klona/lite'
|
|
|
|
import {findById, findIndexById} from '@/helpers/utils'
|
|
import {i18n} from '@/i18n'
|
|
import {success} from '@/message'
|
|
|
|
import BucketService from '@/services/bucket'
|
|
import TaskCollectionService from '@/services/taskCollection'
|
|
|
|
import {setModuleLoading} from '@/stores/helper'
|
|
|
|
import type {ITask} from '@/modelTypes/ITask'
|
|
import type {IProject} from '@/modelTypes/IProject'
|
|
import type {IBucket} from '@/modelTypes/IBucket'
|
|
|
|
const TASKS_PER_BUCKET = 25
|
|
|
|
function getTaskIndicesById(buckets: IBucket[], taskId: ITask['id']) {
|
|
let taskIndex
|
|
const bucketIndex = buckets.findIndex(({ tasks }) => {
|
|
taskIndex = findIndexById(tasks, taskId)
|
|
return taskIndex !== -1
|
|
})
|
|
|
|
return {
|
|
bucketIndex: bucketIndex !== -1 ? bucketIndex : null,
|
|
taskIndex: taskIndex !== -1 ? taskIndex : null,
|
|
}
|
|
}
|
|
|
|
const addTaskToBucketAndSort = (buckets: IBucket[], task: ITask) => {
|
|
const bucketIndex = findIndexById(buckets, task.bucketId)
|
|
if(typeof buckets[bucketIndex] === 'undefined') {
|
|
return
|
|
}
|
|
buckets[bucketIndex].tasks.push(task)
|
|
buckets[bucketIndex].tasks.sort((a, b) => a.kanbanPosition > b.kanbanPosition ? 1 : -1)
|
|
}
|
|
|
|
/**
|
|
* This store is intended to hold the currently active kanban view.
|
|
* It should hold only the current buckets.
|
|
*/
|
|
export const useKanbanStore = defineStore('kanban', () => {
|
|
const buckets = ref<IBucket[]>([])
|
|
const projectId = ref<IProject['id']>(0)
|
|
const bucketLoading = ref<{[id: IBucket['id']]: boolean}>({})
|
|
const taskPagesPerBucket = ref<{[id: IBucket['id']]: number}>({})
|
|
const allTasksLoadedForBucket = ref<{[id: IBucket['id']]: boolean}>({})
|
|
const isLoading = ref(false)
|
|
|
|
const getBucketById = computed(() => (bucketId: IBucket['id']): IBucket | undefined => findById(buckets.value, bucketId))
|
|
const getTaskById = computed(() => {
|
|
return (id: ITask['id']) => {
|
|
const { bucketIndex, taskIndex } = getTaskIndicesById(buckets.value, id)
|
|
|
|
return {
|
|
bucketIndex,
|
|
taskIndex,
|
|
task: bucketIndex !== null && taskIndex !== null && buckets.value[bucketIndex]?.tasks?.[taskIndex] || null,
|
|
}
|
|
}
|
|
})
|
|
|
|
function setIsLoading(newIsLoading: boolean) {
|
|
isLoading.value = newIsLoading
|
|
}
|
|
|
|
function setProjectId(newProjectId: IProject['id']) {
|
|
projectId.value = Number(newProjectId)
|
|
}
|
|
|
|
function setBuckets(newBuckets: IBucket[]) {
|
|
buckets.value = newBuckets
|
|
newBuckets.forEach(b => {
|
|
taskPagesPerBucket.value[b.id] = 1
|
|
allTasksLoadedForBucket.value[b.id] = false
|
|
})
|
|
}
|
|
|
|
function addBucket(bucket: IBucket) {
|
|
buckets.value.push(bucket)
|
|
}
|
|
|
|
function removeBucket(newBucket: IBucket) {
|
|
const bucketIndex = findIndexById(buckets.value, newBucket.id)
|
|
buckets.value.splice(bucketIndex, 1)
|
|
}
|
|
|
|
function setBucketById(newBucket: IBucket) {
|
|
const bucketIndex = findIndexById(buckets.value, newBucket.id)
|
|
buckets.value[bucketIndex] = newBucket
|
|
}
|
|
|
|
function setBucketByIndex({
|
|
bucketIndex,
|
|
bucket,
|
|
} : {
|
|
bucketIndex: number,
|
|
bucket: IBucket
|
|
}) {
|
|
buckets.value[bucketIndex] = bucket
|
|
}
|
|
|
|
function setTaskInBucketByIndex({
|
|
bucketIndex,
|
|
taskIndex,
|
|
task,
|
|
} : {
|
|
bucketIndex: number,
|
|
taskIndex: number,
|
|
task: ITask
|
|
}) {
|
|
const bucket = buckets.value[bucketIndex]
|
|
bucket.tasks[taskIndex] = task
|
|
buckets.value[bucketIndex] = bucket
|
|
}
|
|
|
|
function setTaskInBucket(task: ITask) {
|
|
// If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task
|
|
if (buckets.value.length === 0) {
|
|
return
|
|
}
|
|
|
|
let found = false
|
|
|
|
const findAndUpdate = b => {
|
|
for (const t in buckets.value[b].tasks) {
|
|
if (buckets.value[b].tasks[t].id === task.id) {
|
|
const bucket = buckets.value[b]
|
|
bucket.tasks[t] = task
|
|
|
|
if (bucket.id !== task.bucketId) {
|
|
bucket.tasks.splice(t, 1)
|
|
addTaskToBucketAndSort(buckets.value, task)
|
|
}
|
|
|
|
buckets.value[b] = bucket
|
|
|
|
found = true
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const b in buckets.value) {
|
|
if (buckets.value[b].id === task.bucketId) {
|
|
findAndUpdate(b)
|
|
if (found) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const b in buckets.value) {
|
|
findAndUpdate(b)
|
|
if (found) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
function addTaskToBucket(task: ITask) {
|
|
const bucketIndex = findIndexById(buckets.value, task.bucketId)
|
|
const oldBucket = buckets.value[bucketIndex]
|
|
const newBucket = {
|
|
...oldBucket,
|
|
count: oldBucket.count + 1,
|
|
tasks: [
|
|
...oldBucket.tasks,
|
|
task,
|
|
],
|
|
}
|
|
buckets.value[bucketIndex] = newBucket
|
|
}
|
|
|
|
function addTasksToBucket({tasks, bucketId}: {
|
|
tasks: ITask[];
|
|
bucketId: IBucket['id'];
|
|
}) {
|
|
const bucketIndex = findIndexById(buckets.value, bucketId)
|
|
const oldBucket = buckets.value[bucketIndex]
|
|
const newBucket = {
|
|
...oldBucket,
|
|
tasks: [
|
|
...oldBucket.tasks,
|
|
...tasks,
|
|
],
|
|
}
|
|
buckets.value[bucketIndex] = newBucket
|
|
}
|
|
|
|
function removeTaskInBucket(task: ITask) {
|
|
// If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task
|
|
if (buckets.value.length === 0) {
|
|
return
|
|
}
|
|
|
|
const { bucketIndex, taskIndex } = getTaskIndicesById(buckets.value, task.id)
|
|
|
|
if (
|
|
bucketIndex === null ||
|
|
buckets.value[bucketIndex]?.id !== task.bucketId ||
|
|
taskIndex === null ||
|
|
(buckets.value[bucketIndex]?.tasks[taskIndex]?.id !== task.id)
|
|
) {
|
|
return
|
|
}
|
|
|
|
buckets.value[bucketIndex].tasks.splice(taskIndex, 1)
|
|
buckets.value[bucketIndex].count--
|
|
}
|
|
|
|
function setBucketLoading({bucketId, loading}: {bucketId: IBucket['id'], loading: boolean}) {
|
|
bucketLoading.value[bucketId] = loading
|
|
}
|
|
|
|
function setTasksLoadedForBucketPage({bucketId, page}: {bucketId: IBucket['id'], page: number}) {
|
|
taskPagesPerBucket.value[bucketId] = page
|
|
}
|
|
|
|
function setAllTasksLoadedForBucket(bucketId: IBucket['id']) {
|
|
allTasksLoadedForBucket.value[bucketId] = true
|
|
}
|
|
|
|
async function loadBucketsForProject({projectId, params}: {projectId: IProject['id'], params}) {
|
|
const cancel = setModuleLoading(setIsLoading)
|
|
|
|
// Clear everything to prevent having old buckets in the project if loading the buckets from this project takes a few moments
|
|
setBuckets([])
|
|
|
|
const bucketService = new BucketService()
|
|
try {
|
|
const newBuckets = await bucketService.getAll({projectId}, {
|
|
...params,
|
|
per_page: TASKS_PER_BUCKET,
|
|
})
|
|
setBuckets(newBuckets)
|
|
setProjectId(projectId)
|
|
return newBuckets
|
|
} finally {
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
async function loadNextTasksForBucket(
|
|
projectId: IProject['id'],
|
|
ps,
|
|
bucketId: IBucket['id'],
|
|
) {
|
|
const isLoading = bucketLoading.value[bucketId] ?? false
|
|
if (isLoading) {
|
|
return
|
|
}
|
|
|
|
const page = (taskPagesPerBucket.value[bucketId] ?? 1) + 1
|
|
|
|
const alreadyLoaded = allTasksLoadedForBucket.value[bucketId] ?? false
|
|
if (alreadyLoaded) {
|
|
return
|
|
}
|
|
|
|
const cancel = setModuleLoading(setIsLoading)
|
|
setBucketLoading({bucketId: bucketId, loading: true})
|
|
|
|
const params = JSON.parse(JSON.stringify(ps))
|
|
|
|
params.sort_by = 'kanban_position'
|
|
params.order_by = 'asc'
|
|
|
|
let hasBucketFilter = false
|
|
for (const f in params.filter_by) {
|
|
if (params.filter_by[f] === 'bucket_id') {
|
|
hasBucketFilter = true
|
|
if (params.filter_value[f] !== bucketId) {
|
|
params.filter_value[f] = bucketId
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
if (!hasBucketFilter) {
|
|
params.filter_by = [...(params.filter_by ?? []), 'bucket_id']
|
|
params.filter_value = [...(params.filter_value ?? []), bucketId]
|
|
params.filter_comparator = [...(params.filter_comparator ?? []), 'equals']
|
|
}
|
|
|
|
params.per_page = TASKS_PER_BUCKET
|
|
|
|
const taskService = new TaskCollectionService()
|
|
try {
|
|
const tasks = await taskService.getAll({projectId}, params, page)
|
|
addTasksToBucket({tasks, bucketId: bucketId})
|
|
setTasksLoadedForBucketPage({bucketId, page})
|
|
if (taskService.totalPages <= page) {
|
|
setAllTasksLoadedForBucket(bucketId)
|
|
}
|
|
return tasks
|
|
} finally {
|
|
cancel()
|
|
setBucketLoading({bucketId, loading: false})
|
|
}
|
|
}
|
|
|
|
async function createBucket(bucket: IBucket) {
|
|
const cancel = setModuleLoading(setIsLoading)
|
|
|
|
const bucketService = new BucketService()
|
|
try {
|
|
const createdBucket = await bucketService.create(bucket)
|
|
addBucket(createdBucket)
|
|
return createdBucket
|
|
} finally {
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
async function deleteBucket({bucket, params}: {bucket: IBucket, params}) {
|
|
const cancel = setModuleLoading(setIsLoading)
|
|
|
|
const bucketService = new BucketService()
|
|
try {
|
|
const response = await bucketService.delete(bucket)
|
|
removeBucket(bucket)
|
|
// We reload all buckets because tasks are being moved from the deleted bucket
|
|
loadBucketsForProject({projectId: bucket.projectId, params})
|
|
return response
|
|
} finally {
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
async function updateBucket(updatedBucketData: Partial<IBucket>) {
|
|
const cancel = setModuleLoading(setIsLoading)
|
|
|
|
const bucketIndex = findIndexById(buckets.value, updatedBucketData.id)
|
|
const oldBucket = klona(buckets.value[bucketIndex])
|
|
|
|
const updatedBucket = {
|
|
...oldBucket,
|
|
...updatedBucketData,
|
|
}
|
|
|
|
setBucketByIndex({bucketIndex, bucket: updatedBucket})
|
|
|
|
const bucketService = new BucketService()
|
|
try {
|
|
const returnedBucket = await bucketService.update(updatedBucket)
|
|
setBucketByIndex({bucketIndex, bucket: returnedBucket})
|
|
return returnedBucket
|
|
} catch(e) {
|
|
// restore original state
|
|
setBucketByIndex({bucketIndex, bucket: oldBucket})
|
|
|
|
throw e
|
|
} finally {
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
async function updateBucketTitle({ id, title }: { id: IBucket['id'], title: IBucket['title'] }) {
|
|
const bucket = findById(buckets.value, id)
|
|
|
|
if (bucket?.title === title) {
|
|
// bucket title has not changed
|
|
return
|
|
}
|
|
|
|
await updateBucket({ id, title })
|
|
success({message: i18n.global.t('project.kanban.bucketTitleSavedSuccess')})
|
|
}
|
|
|
|
return {
|
|
buckets,
|
|
isLoading: readonly(isLoading),
|
|
|
|
getBucketById,
|
|
getTaskById,
|
|
|
|
setBuckets,
|
|
setBucketById,
|
|
setTaskInBucketByIndex,
|
|
setTaskInBucket,
|
|
addTaskToBucket,
|
|
removeTaskInBucket,
|
|
loadBucketsForProject,
|
|
loadNextTasksForBucket,
|
|
createBucket,
|
|
deleteBucket,
|
|
updateBucket,
|
|
updateBucketTitle,
|
|
}
|
|
})
|
|
|
|
// support hot reloading
|
|
if (import.meta.hot) {
|
|
import.meta.hot.accept(acceptHMRUpdate(useKanbanStore, import.meta.hot))
|
|
} |