diff --git a/src/stores/kanban.ts b/src/stores/kanban.ts index cd5dfc65a..70201935d 100644 --- a/src/stores/kanban.ts +++ b/src/stores/kanban.ts @@ -1,3 +1,4 @@ +import {computed, readonly, ref} from 'vue' import {defineStore, acceptHMRUpdate} from 'pinia' import cloneDeep from 'lodash.clonedeep' @@ -10,15 +11,15 @@ import TaskCollectionService from '@/services/taskCollection' import {setModuleLoading} from '@/stores/helper' -import type { ITask } from '@/modelTypes/ITask' -import type { IList } from '@/modelTypes/IList' -import type { IBucket } from '@/modelTypes/IBucket' +import type {ITask} from '@/modelTypes/ITask' +import type {IList} from '@/modelTypes/IList' +import type {IBucket} from '@/modelTypes/IBucket' const TASKS_PER_BUCKET = 25 -function getTaskIndicesById(state: KanbanState, taskId: ITask['id']) { +function getTaskIndicesById(buckets: IBucket[], taskId: ITask['id']) { let taskIndex - const bucketIndex = state.buckets.findIndex(({ tasks }) => { + const bucketIndex = buckets.findIndex(({ tasks }) => { taskIndex = findIndexById(tasks, taskId) return taskIndex !== -1 }) @@ -29,382 +30,365 @@ function getTaskIndicesById(state: KanbanState, taskId: ITask['id']) { } } -const addTaskToBucketAndSort = (state: KanbanState, task: ITask) => { - const bucketIndex = findIndexById(state.buckets, task.bucketId) - if(typeof state.buckets[bucketIndex] === 'undefined') { +const addTaskToBucketAndSort = (buckets: IBucket[], task: ITask) => { + const bucketIndex = findIndexById(buckets, task.bucketId) + if(typeof buckets[bucketIndex] === 'undefined') { return } - state.buckets[bucketIndex].tasks.push(task) - state.buckets[bucketIndex].tasks.sort((a, b) => a.kanbanPosition > b.kanbanPosition ? 1 : -1) -} - -export interface KanbanState { - buckets: IBucket[], - listId: IList['id'], - bucketLoading: { - [id: IBucket['id']]: boolean - }, - taskPagesPerBucket: { - [id: IBucket['id']]: number - }, - allTasksLoadedForBucket: { - [id: IBucket['id']]: boolean - }, - isLoading: boolean, + 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', { - state: () : KanbanState => ({ - buckets: [], - listId: 0, - bucketLoading: {}, - taskPagesPerBucket: {}, - allTasksLoadedForBucket: {}, - isLoading: false, - }), +export const useKanbanStore = defineStore('kanban', () => { + const buckets = ref([]) + const listId = ref(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) - getters: { - getBucketById(state) { - return (bucketId: IBucket['id']): IBucket | undefined => findById(state.buckets, bucketId) - }, - - getTaskById(state) { - return (id: ITask['id']) => { - const { bucketIndex, taskIndex } = getTaskIndicesById(state, id) - - return { - bucketIndex, - taskIndex, - task: bucketIndex !== null && taskIndex !== null && state.buckets[bucketIndex]?.tasks?.[taskIndex] || null, - } + 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, } - }, - }, + } + }) - actions: { - setIsLoading(isLoading: boolean) { - this.isLoading = isLoading - }, + function setIsLoading(newIsLoading: boolean) { + isLoading.value = newIsLoading + } - setListId(listId: IList['id']) { - this.listId = Number(listId) - }, + function setListId(newListId: IList['id']) { + listId.value = Number(newListId) + } - setBuckets(buckets: IBucket[]) { - this.buckets = buckets - buckets.forEach(b => { - this.taskPagesPerBucket[b.id] = 1 - this.allTasksLoadedForBucket[b.id] = false - }) - }, + function setBuckets(newBuckets: IBucket[]) { + buckets.value = newBuckets + newBuckets.forEach(b => { + taskPagesPerBucket.value[b.id] = 1 + allTasksLoadedForBucket.value[b.id] = false + }) + } - addBucket(bucket: IBucket) { - this.buckets.push(bucket) - }, + function addBucket(bucket: IBucket) { + buckets.value.push(bucket) + } - removeBucket(bucket: IBucket) { - const bucketIndex = findIndexById(this.buckets, bucket.id) - this.buckets.splice(bucketIndex, 1) - }, + function removeBucket(newBucket: IBucket) { + const bucketIndex = findIndexById(buckets.value, newBucket.id) + buckets.value.splice(bucketIndex, 1) + } - setBucketById(bucket: IBucket) { - const bucketIndex = findIndexById(this.buckets, bucket.id) - this.buckets[bucketIndex] = bucket - }, + function setBucketById(newBucket: IBucket) { + const bucketIndex = findIndexById(buckets.value, newBucket.id) + buckets.value[bucketIndex] = newBucket + } - setBucketByIndex({ - bucketIndex, - bucket, - } : { - bucketIndex: number, - bucket: IBucket - }) { - this.buckets[bucketIndex] = bucket - }, + function setBucketByIndex({ + bucketIndex, + bucket, + } : { + bucketIndex: number, + bucket: IBucket + }) { + buckets.value[bucketIndex] = bucket + } - setTaskInBucketByIndex({ - bucketIndex, - taskIndex, - task, - } : { - bucketIndex: number, - taskIndex: number, - task: ITask - }) { - const bucket = this.buckets[bucketIndex] - bucket.tasks[taskIndex] = task - this.buckets[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 + } - setTasksInBucketByBucketId({ - bucketId, - tasks, - } : { - bucketId: IBucket['id'], - tasks: ITask[], - }) { - const bucketIndex = findIndexById(this.buckets, bucketId) - this.buckets[bucketIndex] = { - ...this.buckets[bucketIndex], - tasks, - } - }, - - setTaskInBucket(task: ITask) { - // If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task - if (this.buckets.length === 0) { - return - } + 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 + let found = false - const findAndUpdate = b => { - for (const t in this.buckets[b].tasks) { - if (this.buckets[b].tasks[t].id === task.id) { - const bucket = this.buckets[b] - bucket.tasks[t] = task + 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(this, task) - } - - this.buckets[b] = bucket - - found = true - return + 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 this.buckets) { - if (this.buckets[b].id === task.bucketId) { - findAndUpdate(b) - if (found) { - return - } - } - } - - for (const b in this.buckets) { + for (const b in buckets.value) { + if (buckets.value[b].id === task.bucketId) { findAndUpdate(b) if (found) { return } } - }, + } - addTaskToBucket(task: ITask) { - const bucketIndex = findIndexById(this.buckets, task.bucketId) - const oldBucket = this.buckets[bucketIndex] - const newBucket = { - ...oldBucket, - tasks: [ - ...oldBucket.tasks, - task, - ], - } - this.buckets[bucketIndex] = newBucket - }, - - addTasksToBucket({tasks, bucketId}: { - tasks: ITask[]; - bucketId: IBucket['id']; - }) { - const bucketIndex = findIndexById(this.buckets, bucketId) - const oldBucket = this.buckets[bucketIndex] - const newBucket = { - ...oldBucket, - tasks: [ - ...oldBucket.tasks, - ...tasks, - ], - } - this.buckets[bucketIndex] = newBucket - }, - - removeTaskInBucket(task: ITask) { - // If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task - if (this.buckets.length === 0) { + for (const b in buckets.value) { + findAndUpdate(b) + if (found) { return } + } + } - const { bucketIndex, taskIndex } = getTaskIndicesById(this, task.id) + function addTaskToBucket(task: ITask) { + const bucketIndex = findIndexById(buckets.value, task.bucketId) + const oldBucket = buckets.value[bucketIndex] + const newBucket = { + ...oldBucket, + tasks: [ + ...oldBucket.tasks, + task, + ], + } + buckets.value[bucketIndex] = newBucket + } - if ( - !bucketIndex || - this.buckets[bucketIndex]?.id !== task.bucketId || - !taskIndex || - (this.buckets[bucketIndex]?.tasks[taskIndex]?.id !== task.id) - ) { - return - } - - this.buckets[bucketIndex].tasks.splice(taskIndex, 1) - }, + 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 + } - setBucketLoading({bucketId, loading}: {bucketId: IBucket['id'], loading: boolean}) { - this.bucketLoading[bucketId] = loading - }, + 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 + } - setTasksLoadedForBucketPage({bucketId, page}: {bucketId: IBucket['id'], page: number}) { - this.taskPagesPerBucket[bucketId] = page - }, + const { bucketIndex, taskIndex } = getTaskIndicesById(buckets.value, task.id) - setAllTasksLoadedForBucket(bucketId: IBucket['id']) { - this.allTasksLoadedForBucket[bucketId] = true - }, - - async loadBucketsForList({listId, params}: {listId: IList['id'], params}) { - const cancel = setModuleLoading(this) - - // Clear everything to prevent having old buckets in the list if loading the buckets from this list takes a few moments - this.setBuckets([]) - - const bucketService = new BucketService() - try { - const buckets = await bucketService.getAll({listId}, { - ...params, - per_page: TASKS_PER_BUCKET, - }) - this.setBuckets(buckets) - this.setListId(listId) - return buckets - } finally { - cancel() - } - }, - - async loadNextTasksForBucket( - {listId, ps = {}, bucketId} : - {listId: IList['id'], ps, bucketId: IBucket['id']}, + if ( + !bucketIndex || + buckets.value[bucketIndex]?.id !== task.bucketId || + !taskIndex || + (buckets.value[bucketIndex]?.tasks[taskIndex]?.id !== task.id) ) { - const isLoading = this.bucketLoading[bucketId] ?? false - if (isLoading) { - return - } + return + } + + buckets.value[bucketIndex].tasks.splice(taskIndex, 1) + } - const page = (this.taskPagesPerBucket[bucketId] ?? 1) + 1 + function setBucketLoading({bucketId, loading}: {bucketId: IBucket['id'], loading: boolean}) { + bucketLoading.value[bucketId] = loading + } - const alreadyLoaded = this.allTasksLoadedForBucket[bucketId] ?? false - if (alreadyLoaded) { - return - } + function setTasksLoadedForBucketPage({bucketId, page}: {bucketId: IBucket['id'], page: number}) { + taskPagesPerBucket.value[bucketId] = page + } - const cancel = setModuleLoading(this) - this.setBucketLoading({bucketId: bucketId, loading: true}) + function setAllTasksLoadedForBucket(bucketId: IBucket['id']) { + allTasksLoadedForBucket.value[bucketId] = true + } - const params = JSON.parse(JSON.stringify(ps)) + async function loadBucketsForList({listId, params}: {listId: IList['id'], params}) { + const cancel = setModuleLoading(this, setIsLoading) - params.sort_by = 'kanban_position' - params.order_by = 'asc' + // Clear everything to prevent having old buckets in the list if loading the buckets from this list takes a few moments + setBuckets([]) - 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 + const bucketService = new BucketService() + try { + const newBuckets = await bucketService.getAll({listId}, { + ...params, + per_page: TASKS_PER_BUCKET, + }) + setBuckets(newBuckets) + setListId(listId) + return newBuckets + } finally { + cancel() + } + } + + async function loadNextTasksForBucket( + {listId, ps = {}, bucketId} : + {listId: IList['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(this, 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'] + 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({listId}, params, page) + addTasksToBucket({tasks, bucketId: bucketId}) + setTasksLoadedForBucketPage({bucketId, page}) + if (taskService.totalPages <= page) { + setAllTasksLoadedForBucket(bucketId) } + return tasks + } finally { + cancel() + setBucketLoading({bucketId, loading: false}) + } + } - params.per_page = TASKS_PER_BUCKET + async function createBucket(bucket: IBucket) { + const cancel = setModuleLoading(this, setIsLoading) - const taskService = new TaskCollectionService() - try { - const tasks = await taskService.getAll({listId}, params, page) - this.addTasksToBucket({tasks, bucketId: bucketId}) - this.setTasksLoadedForBucketPage({bucketId, page}) - if (taskService.totalPages <= page) { - this.setAllTasksLoadedForBucket(bucketId) - } - return tasks - } finally { - cancel() - this.setBucketLoading({bucketId, loading: false}) - } - }, + const bucketService = new BucketService() + try { + const createdBucket = await bucketService.create(bucket) + addBucket(createdBucket) + return createdBucket + } finally { + cancel() + } + } - async createBucket(bucket: IBucket) { - const cancel = setModuleLoading(this) + async function deleteBucket({bucket, params}: {bucket: IBucket, params}) { + const cancel = setModuleLoading(this, setIsLoading) - const bucketService = new BucketService() - try { - const createdBucket = await bucketService.create(bucket) - this.addBucket(createdBucket) - return createdBucket - } finally { - cancel() - } - }, + 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 + loadBucketsForList({listId: bucket.listId, params}) + return response + } finally { + cancel() + } + } - async deleteBucket({bucket, params}: {bucket: IBucket, params}) { - const cancel = setModuleLoading(this) + async function updateBucket(updatedBucketData: Partial) { + const cancel = setModuleLoading(this, setIsLoading) - const bucketService = new BucketService() - try { - const response = await bucketService.delete(bucket) - this.removeBucket(bucket) - // We reload all buckets because tasks are being moved from the deleted bucket - this.loadBucketsForList({listId: bucket.listId, params}) - return response - } finally { - cancel() - } - }, + const bucketIndex = findIndexById(buckets.value, updatedBucketData.id) + const oldBucket = cloneDeep(buckets.value[bucketIndex]) - async updateBucket(updatedBucketData: Partial) { - const cancel = setModuleLoading(this) + const updatedBucket = { + ...oldBucket, + ...updatedBucketData, + } - const bucketIndex = findIndexById(this.buckets, updatedBucketData.id) - const oldBucket = cloneDeep(this.buckets[bucketIndex]) + 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}) - const updatedBucket = { - ...oldBucket, - ...updatedBucketData, - } + throw e + } finally { + cancel() + } + } - this.setBucketByIndex({bucketIndex, bucket: updatedBucket}) - - const bucketService = new BucketService() - try { - const returnedBucket = await bucketService.update(updatedBucket) - this.setBucketByIndex({bucketIndex, bucket: returnedBucket}) - return returnedBucket - } catch(e) { - // restore original state - this.setBucketByIndex({bucketIndex, bucket: oldBucket}) + async function updateBucketTitle({ id, title }: { id: IBucket['id'], title: IBucket['title'] }) { + const bucket = findById(buckets.value, id) - throw e - } finally { - cancel() - } - }, + if (bucket?.title === title) { + // bucket title has not changed + return + } - async updateBucketTitle({ id, title }: { id: IBucket['id'], title: IBucket['title'] }) { - const bucket = findById(this.buckets, id) + await updateBucket({ id, title }) + success({message: i18n.global.t('list.kanban.bucketTitleSavedSuccess')}) + } + + return { + buckets: readonly(buckets), + isLoading: readonly(isLoading), + + getBucketById, + getTaskById, - if (bucket?.title === title) { - // bucket title has not changed - return - } - - await this.updateBucket({ id, title }) - success({message: i18n.global.t('list.kanban.bucketTitleSavedSuccess')}) - }, - }, + setBuckets, + setBucketById, + setTaskInBucketByIndex, + setTaskInBucket, + addTaskToBucket, + removeTaskInBucket, + loadBucketsForList, + loadNextTasksForBucket, + createBucket, + deleteBucket, + updateBucket, + updateBucketTitle, + } }) // support hot reloading