feat: kanban store with composition api #2608

Merged
dpschen merged 1 commits from dpschen/frontend:feature/feat-pinia-composition-kanban-store into main 2022-11-07 17:37:35 +00:00
1 changed files with 302 additions and 318 deletions

View File

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