feat: improve kanban implementation

This commit is contained in:
Dominik Pschenitschni 2021-09-11 17:53:03 +02:00
parent 43b22360a5
commit d66ad12f5c
Signed by: dpschen
GPG Key ID: B257AC0149F43A77
11 changed files with 230 additions and 127 deletions

View File

@ -25,6 +25,7 @@
"easymde": "^2.15.0", "easymde": "^2.15.0",
"highlight.js": "11.2.0", "highlight.js": "11.2.0",
"is-touch-device": "1.0.1", "is-touch-device": "1.0.1",
"lodash.clonedeep": "^4.5.0",
"marked": "3.0.4", "marked": "3.0.4",
"register-service-worker": "1.7.2", "register-service-worker": "1.7.2",
"snake-case": "3.0.4", "snake-case": "3.0.4",

View File

@ -93,7 +93,11 @@ app.mixin({
}) })
app.config.errorHandler = (err, vm, info) => { app.config.errorHandler = (err, vm, info) => {
error(err) // if (import.meta.env.PROD) {
// error(err)
// } else {
console.error(err, vm, info)
// }
} }
app.config.globalProperties.$message = { app.config.globalProperties.$message = {

View File

@ -18,7 +18,7 @@ export default class BucketService extends AbstractService {
beforeUpdate(model) { beforeUpdate(model) {
const taskService = new TaskService() const taskService = new TaskService()
model.tasks = model.tasks.map(t => taskService.processModel(t)) model.tasks = model.tasks?.map(t => taskService.processModel(t))
return model return model
} }
} }

View File

@ -37,7 +37,8 @@ export default class TaskService extends AbstractService {
return this.processModel(model) return this.processModel(model)
} }
processModel(model) { processModel(updatedModel) {
const model = { ...updatedModel }
model.title = model.title?.trim() model.title = model.title?.trim()

View File

@ -56,7 +56,10 @@ export const store = createStore({
state.errorMessage = error state.errorMessage = error
}, },
[ONLINE](state, online) { [ONLINE](state, online) {
state.online = import.meta.env.VITE_IS_ONLINE || online if (import.meta.env.VITE_IS_ONLINE) {
console.log('Setting fake online state', import.meta.env.VITE_IS_ONLINE)
}
state.online = !!import.meta.env.VITE_IS_ONLINE || online
}, },
[CURRENT_LIST](state, currentList) { [CURRENT_LIST](state, currentList) {

View File

@ -1,11 +1,16 @@
import cloneDeep from 'lodash.clonedeep'
import {findById, findIndexById} from '@/helpers/utils' import {findById, findIndexById} from '@/helpers/utils'
import {i18n} from '@/i18n'
import {success} from '@/message'
import BucketService from '../../services/bucket' import BucketService from '../../services/bucket'
import {setLoading} from '../helper' import {setLoading} from '../helper'
import TaskCollectionService from '@/services/taskCollection' import TaskCollectionService from '@/services/taskCollection'
const TASKS_PER_BUCKET = 25 const TASKS_PER_BUCKET = 25
function getTaskPosition(state, task) { function getTaskIndices(state, task) {
const bucketIndex = findIndexById(state.buckets, task.bucketId) const bucketIndex = findIndexById(state.buckets, task.bucketId)
if (!bucketIndex) { if (!bucketIndex) {
@ -167,7 +172,7 @@ export default {
return return
} }
const { bucketIndex, taskIndex } = getTaskPosition(state, task) const { bucketIndex, taskIndex } = getTaskIndices(state, task)
state.buckets[bucketIndex].tasks.splice(taskIndex, 1) state.buckets[bucketIndex].tasks.splice(taskIndex, 1)
}, },
@ -189,23 +194,21 @@ export default {
getBucketById(state) { getBucketById(state) {
return (bucketId) => findById(state.buckets, bucketId) return (bucketId) => findById(state.buckets, bucketId)
}, },
getTaskById: state => id => {
for (const b in state.buckets) { getTaskById(state) {
for (const t in state.buckets[b].tasks) { return (id) => {
if (state.buckets[b].tasks[t].id === id) { let taskIndex
return { const bucketIndex = state.buckets.findIndex(({ tasks }) => {
bucketIndex: b, taskIndex = findIndexById(tasks, id)
taskIndex: t, return taskIndex !== undefined
task: state.buckets[b].tasks[t], })
}
} return {
bucketIndex: taskIndex || null,
taskIndex: taskIndex || null,
task: state.buckets?.[bucketIndex].tasks?.[taskIndex] || null,
} }
} }
return {
bucketIndex: null,
taskIndex: null,
task: null,
}
}, },
}, },
@ -256,6 +259,15 @@ export default {
params.sort_by = 'kanban_position' params.sort_by = 'kanban_position'
params.order_by = 'asc' params.order_by = 'asc'
// const hasBucketFilter = Object.entries(params.filter_by).some(([key, value]) => {
// const condition = value === 'bucket_id'
// if (condition) {
// if (value !== bucketId) {
// params.filter_value[key] = bucketId
// }
// }
// return condition
// })
let hasBucketFilter = false let hasBucketFilter = false
for (const f in params.filter_by) { for (const f in params.filter_by) {
if (params.filter_by[f] === 'bucket_id') { if (params.filter_by[f] === 'bucket_id') {
@ -293,6 +305,7 @@ export default {
ctx.commit('setBucketLoading', {bucketId: bucketId, loading: false}) ctx.commit('setBucketLoading', {bucketId: bucketId, loading: false})
}) })
}, },
createBucket(ctx, bucket) { createBucket(ctx, bucket) {
const cancel = setLoading(ctx, 'kanban') const cancel = setLoading(ctx, 'kanban')
@ -309,6 +322,7 @@ export default {
cancel() cancel()
}) })
}, },
deleteBucket(ctx, {bucket, params}) { deleteBucket(ctx, {bucket, params}) {
const cancel = setLoading(ctx, 'kanban') const cancel = setLoading(ctx, 'kanban')
@ -327,24 +341,96 @@ export default {
cancel() cancel()
}) })
}, },
updateBucket(ctx, bucket) {
updateBucket(ctx, updatedBucketData) {
const cancel = setLoading(ctx, 'kanban') const cancel = setLoading(ctx, 'kanban')
const bucketIndex = findIndexById(ctx.state.buckets, updatedBucketData.id)
const oldBucket = cloneDeep(ctx.state.buckets[bucketIndex])
const bucket = ctx.state.buckets[bucketIndex]
const requestData = {
id: updatedBucketData.id,
listId: updatedBucketData.listId || oldBucket.listId,
title: oldBucket.title, // can't be empty in request
// ...bucket,
...updatedBucketData,
}
const updatedBucket = {
...bucket,
...requestData,
}
ctx.commit('setBucketByIndex', {bucketIndex, bucket: updatedBucket})
const bucketService = new BucketService() const bucketService = new BucketService()
return bucketService.update(bucket) return bucketService.update(updatedBucket)
.then(r => { .then(r => {
const bi = findById(ctx.state.buckets, r.id) Promise.resolve(r)
const bucket = r
bucket.tasks = ctx.state.buckets[bi].tasks
ctx.commit('setBucketByIndex', {bucketIndex: bi, bucket})
return Promise.resolve(r)
}) })
.catch(e => { .catch(e => {
// restore original state
ctx.commit('setBucketByIndex', {bucketIndex, bucket: oldBucket})
return Promise.reject(e) return Promise.reject(e)
}) })
.finally(() => { .finally(() => cancel())
cancel() },
updateBuckets(ctx, updatedBucketsData) {
const cancel = setLoading(ctx, 'kanban')
const oldBuckets = []
const updatedBuckets = updatedBucketsData.map((updatedBucketData) => {
const bucketIndex = findIndexById(ctx.state.buckets, updatedBucketData.id)
const bucket = ctx.state.buckets[bucketIndex]
const oldBucket = cloneDeep(bucket)
oldBuckets.push(oldBucket)
const newBucket = {
// FIXME: maybe optional to set the original value as well
...bucket,
id: updatedBucketData.id,
listId: updatedBucketData.listId || oldBucket.listId,
...updatedBucketData,
}
ctx.commit('setBucketByIndex', {bucketIndex, bucket: newBucket})
const bucketService = new BucketService()
return bucketService.update(newBucket)
})
return Promise.all(updatedBuckets)
.then(r => {
Promise.resolve(r)
}) })
.catch(e => {
// restore original state
Object.values(updatedBuckets).forEach((oldBucket) => ctx.commit('setBucketById', oldBucket))
return Promise.reject(e)
})
.finally(() => cancel())
},
updateBucketTitle(ctx, { id, title }) {
const bucket = findById(ctx.state.buckets, id)
if (bucket.title === title) {
// bucket title has not changed
return
}
const updatedBucketData = {
id,
title,
}
ctx.dispatch('updateBucket', updatedBucketData).then(() => {
success({message: i18n.global.t('list.kanban.bucketTitleSavedSuccess')})
})
}, },
}, },
} }

View File

@ -117,14 +117,24 @@ export default {
addTaskAttachment(ctx, {taskId, attachment}) { addTaskAttachment(ctx, {taskId, attachment}) {
const t = ctx.rootGetters['kanban/getTaskById'](taskId) const t = ctx.rootGetters['kanban/getTaskById'](taskId)
if (t.task !== null) { if (t.task !== null) {
const newTask = { ...t } const attachments = [
newTask.task.attachments.push(attachment) ...t.task.attachments,
attachment,
]
const newTask = {
...t,
task: {
...t.task,
attachments,
},
}
ctx.commit('kanban/setTaskInBucketByIndex', newTask, {root: true}) ctx.commit('kanban/setTaskInBucketByIndex', newTask, {root: true})
} }
ctx.commit('attachments/add', attachment, {root: true}) ctx.commit('attachments/add', attachment, {root: true})
}, },
addAssignee(ctx, {user, taskId}) {
addAssignee(ctx, {user, taskId}) {
const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: taskId}) const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: taskId})
const taskAssigneeService = new TaskAssigneeService() const taskAssigneeService = new TaskAssigneeService()

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="kanban-view"> <div class="kanban-view">
<div class="filter-container" v-if="list.isSavedFilter && !list.isSavedFilter()"> <div class="filter-container" v-if="isSavedFilter">
<div class="items"> <div class="items">
<x-button <x-button
@click.prevent.stop="showFilters = !showFilters" @click.prevent.stop="toggleFilterPopup"
icon="filter" icon="filter"
type="secondary" type="secondary"
> >
@ -11,19 +11,20 @@
</x-button> </x-button>
</div> </div>
<filter-popup <filter-popup
@update:modelValue="() => {filtersChanged = true; loadBuckets()}"
:visible="showFilters" :visible="showFilters"
v-model="params" v-model="params"
/> />
</div> </div>
<div <div
:class="{ 'is-loading': loading && !oneTaskUpdating}" :class="{ 'is-loading': loading && !oneTaskUpdating}"
class="kanban kanban-bucket-container loader-container"> class="kanban kanban-bucket-container loader-container"
>
<!-- @end="updateBucketPosition" -->
<draggable <draggable
v-bind="dragOptions" v-bind="dragOptions"
v-model="buckets" :modelValue="buckets"
@update:modelValue="updateBucketPositions"
@start="() => dragBucket = true" @start="() => dragBucket = true"
@end="updateBucketPosition"
group="buckets" group="buckets"
:disabled="!canWrite" :disabled="!canWrite"
tag="transition-group" tag="transition-group"
@ -70,14 +71,14 @@
<div class="field has-addons" v-if="showSetLimitInput"> <div class="field has-addons" v-if="showSetLimitInput">
<div class="control"> <div class="control">
<input <input
@change="() => setBucketLimit(bucket)"
@keyup.enter="() => setBucketLimit(bucket)"
@keyup.esc="() => showSetLimitInput = false" @keyup.esc="() => showSetLimitInput = false"
@keyup.enter="() => showSetLimitInput = false"
:value="bucket.limit"
@input="(event) => setBucketLimit(bucket.id, parseInt(event.target.value))"
class="input" class="input"
type="number" type="number"
min="0" min="0"
v-focus.always v-focus.always
v-model="bucket.limit"
/> />
</div> </div>
<div class="control"> <div class="control">
@ -130,7 +131,8 @@
> >
<draggable <draggable
v-bind="dragOptions" v-bind="dragOptions"
v-model="bucket.tasks" :modelValue="bucket.tasks"
@update:modelValue="(tasks) => updateTasks(bucket.id, tasks)"
@start="() => dragstart(bucket)" @start="() => dragstart(bucket)"
@end="updateTaskPosition" @end="updateTaskPosition"
:group="{name: 'tasks', put: shouldAcceptDrop(bucket) && !dragBucket}" :group="{name: 'tasks', put: shouldAcceptDrop(bucket) && !dragBucket}"
@ -236,7 +238,6 @@
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
import BucketModel from '../../../models/bucket' import BucketModel from '../../../models/bucket'
import {findById} from '@/helpers/utils'
import {mapState} from 'vuex' import {mapState} from 'vuex'
import {saveListView} from '@/helpers/saveListView' import {saveListView} from '@/helpers/saveListView'
import Rights from '../../../models/constants/rights.json' import Rights from '../../../models/constants/rights.json'
@ -245,7 +246,7 @@ import FilterPopup from '@/components/list/partials/filter-popup.vue'
import Dropdown from '@/components/misc/dropdown.vue' import Dropdown from '@/components/misc/dropdown.vue'
import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState' import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState'
import {calculateItemPosition} from '../../../helpers/calculateItemPosition' import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
import KanbanCard from '../../../components/tasks/partials/kanban-card' import KanbanCard from '@/components/tasks/partials/kanban-card'
const DRAG_OPTIONS = { const DRAG_OPTIONS = {
// sortable options // sortable options
@ -299,20 +300,29 @@ export default {
filter_concat: 'and', filter_concat: 'and',
}, },
showFilters: false, showFilters: false,
filtersChanged: false, // To trigger a reload of the board
} }
}, },
created() { created() {
this.loadBuckets()
// Save the current list view to local storage // Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads. // We use local storage and not vuex here to make it persistent across reloads.
saveListView(this.$route.params.listId, this.$route.name) saveListView(this.$route.params.listId, this.$route.name)
}, },
watch: { watch: {
'$route.params.listId': 'loadBuckets', loadBucketParameter: {
handler: 'loadBuckets',
immediate: true,
},
}, },
computed: { computed: {
isSavedFilter() {
return this.list.isSavedFilter && !this.list.isSavedFilter()
},
loadBucketParameter() {
return {
listId: this.$route.params.listId,
params: this.params,
}
},
bucketDraggableComponentData() { bucketDraggableComponentData() {
return { return {
type: 'transition', type: 'transition',
@ -340,6 +350,7 @@ export default {
return this.$store.state.kanban.buckets return this.$store.state.kanban.buckets
}, },
set(value) { set(value) {
console.log('should not set buckets', value)
this.$store.commit('kanban/setBuckets', value) this.$store.commit('kanban/setBuckets', value)
}, },
}, },
@ -351,29 +362,25 @@ export default {
list: state => state.currentList, list: state => state.currentList,
}), }),
}, },
methods: { methods: {
loadBuckets() { toggleFilterPopup() {
this.showFilters = !this.showFilters
},
loadBuckets() {
// Prevent trying to load buckets if the task popup view is active // Prevent trying to load buckets if the task popup view is active
if (this.$route.name !== 'list.kanban') { if (this.$route.name !== 'list.kanban') {
return return
} }
// Only load buckets if we don't already loaded them const { listId, params } = this.loadBucketParameter
if (
!this.filtersChanged && (
this.loadedListId === this.$route.params.listId ||
this.loadedListId === parseInt(this.$route.params.listId))
) {
return
}
this.collapsedBuckets = getCollapsedBucketState(this.$route.params.listId) this.collapsedBuckets = getCollapsedBucketState(listId)
console.debug(`Loading buckets, loadedListId = ${this.loadedListId}, $route.params =`, this.$route.params) console.debug(`Loading buckets, loadedListId = ${this.loadedListId}, $route.params =`, this.$route.params)
this.filtersChanged = false
this.$store.dispatch('kanban/loadBucketsForList', {listId: this.$route.params.listId, params: this.params}) this.$store.dispatch('kanban/loadBucketsForList', {listId, params})
}, },
setTaskContainerRef(id, el) { setTaskContainerRef(id, el) {
@ -394,6 +401,16 @@ export default {
bucketId: id, bucketId: id,
}) })
}, },
updateTasks(bucketId, tasks) {
const newBucket = {
...this.$store.getters['kanban/getBucketById'](bucketId),
tasks,
}
this.$store.dispatch('kanban/updateBucket', newBucket)
},
updateTaskPosition(e) { updateTaskPosition(e) {
this.drag = false this.drag = false
@ -407,20 +424,23 @@ export default {
const taskBefore = newBucket.tasks[e.newIndex - 1] ?? null const taskBefore = newBucket.tasks[e.newIndex - 1] ?? null
const taskAfter = newBucket.tasks[e.newIndex + 1] ?? null const taskAfter = newBucket.tasks[e.newIndex + 1] ?? null
task.kanbanPosition = calculateItemPosition(taskBefore !== null ? taskBefore.kanbanPosition : null, taskAfter !== null ? taskAfter.kanbanPosition : null) // task.kanbanPosition = calculateItemPosition(taskBefore !== null ? taskBefore.kanbanPosition : null, taskAfter !== null ? taskAfter.kanbanPosition : null)
task.bucketId = newBucket.id // task.bucketId = newBucket.id
this.$store.dispatch('tasks/update', task) const newTask = {
.catch(e => { ...task,
this.$message.error(e) bucketId: newBucket.id,
}) kanbanPosition: calculateItemPosition(taskBefore !== null ? taskBefore.kanbanPosition : null, taskAfter !== null ? taskAfter.kanbanPosition : null),
.finally(() => { }
this.$store.dispatch('tasks/update', newTask)
// .finally(() => {
this.taskUpdating[task.id] = false this.taskUpdating[task.id] = false
this.oneTaskUpdating = false this.oneTaskUpdating = false
}) // })
}, },
toggleShowNewTaskInput(bucket) { toggleShowNewTaskInput(bucketId) {
this.showNewTaskInput[bucket] = !this.showNewTaskInput[bucket] this.showNewTaskInput[bucketId] = !this.showNewTaskInput[bucketId]
}, },
addTaskToBucket(bucketId) { addTaskToBucket(bucketId) {
@ -463,9 +483,6 @@ export default {
this.newBucketTitle = '' this.newBucketTitle = ''
this.showNewBucketInput = false this.showNewBucketInput = false
}) })
.catch(e => {
this.$message.error(e)
})
}, },
deleteBucketModal(bucketId) { deleteBucketModal(bucketId) {
if (this.buckets.length <= 1) { if (this.buckets.length <= 1) {
@ -478,16 +495,10 @@ export default {
deleteBucket() { deleteBucket() {
const bucket = new BucketModel({ const bucket = new BucketModel({
id: this.bucketToDelete, id: this.bucketToDelete,
listId: this.$route.params.listId,
}) })
this.$store.dispatch('kanban/deleteBucket', {bucket: bucket, params: this.params}) this.$store.dispatch('kanban/deleteBucket', {bucket: bucket, params: this.params})
.then(() => { .then(() => this.$message.success({message: this.$t('list.kanban.deleteBucketSuccess')}))
this.$message.success({message: this.$t('list.kanban.deleteBucketSuccess')})
})
.catch(e => {
this.$message.error(e)
})
.finally(() => { .finally(() => {
this.showBucketDeleteModal = false this.showBucketDeleteModal = false
}) })
@ -497,40 +508,22 @@ export default {
this.bucketTitleEditable = true this.bucketTitleEditable = true
this.$nextTick(() => e.target.focus()) this.$nextTick(() => e.target.focus())
}, },
saveBucketTitle(bucketId, bucketTitle) { saveBucketTitle(bucketId, bucketTitle) {
this.bucketTitleEditable = false const updatedBucketData = {
const bucket = new BucketModel({
id: bucketId, id: bucketId,
title: bucketTitle, title: bucketTitle,
listId: Number(this.$route.params.listId),
})
// Because the contenteditable does not have a change event,
// we're building it ourselves here and only updating the bucket
// if the title changed.
const realBucket = findById(this.buckets, bucketId)
if (realBucket.title === bucketTitle) {
return
} }
this.$store.dispatch('kanban/updateBucket', bucket) this.$store.dispatch('kanban/updateBucketTitle', updatedBucketData)
.then(r => { .then(() => {
realBucket.title = r.title this.bucketTitleEditable = false
this.$message.success({message: this.$t('list.kanban.bucketTitleSavedSuccess')}) this.$message.success({message: this.$t('list.kanban.bucketTitleSavedSuccess')})
}) })
.catch(e => {
this.$message.error(e)
})
}, },
updateBucket(bucket) {
bucket.limit = parseInt(bucket.limit) updateBucketPositions(buckets) {
this.$store.dispatch('kanban/updateBucket', bucket) this.$store.dispatch('kanban/updateBuckets', buckets)
.then(() => {
this.$message.success({message: this.$t('list.kanban.bucketLimitSavedSuccess')})
})
.catch(e => {
this.$message.error(e)
})
}, },
updateBucketPosition(e) { updateBucketPosition(e) {
this.dragBucket = false this.dragBucket = false
@ -539,19 +532,26 @@ export default {
const bucketBefore = this.buckets[e.newIndex - 1] ?? null const bucketBefore = this.buckets[e.newIndex - 1] ?? null
const bucketAfter = this.buckets[e.newIndex + 1] ?? null const bucketAfter = this.buckets[e.newIndex + 1] ?? null
bucket.position = calculateItemPosition(bucketBefore !== null ? bucketBefore.position : null, bucketAfter !== null ? bucketAfter.position : null) const updatedData = {
id: bucket.id,
position: calculateItemPosition(bucketBefore !== null ? bucketBefore.position : null, bucketAfter !== null ? bucketAfter.position : null),
}
this.$store.dispatch('kanban/updateBucket', bucket) this.$store.dispatch('kanban/updateBucket', updatedData)
.catch(e => {
this.$message.error(e)
})
}, },
setBucketLimit(bucket) { setBucketLimit(bucketId, limit) {
if (bucket.limit < 0) { if (limit < 0) {
return return
} }
this.updateBucket(bucket) const newBucket = {
...this.$store.getters['kanban/getBucketById'](bucketId),
limit,
}
this.$store.dispatch('kanban/updateBucket', newBucket)
.then(() => this.$message.success({message: this.$t('list.kanban.bucketLimitSavedSuccess')}))
}, },
shouldAcceptDrop(bucket) { shouldAcceptDrop(bucket) {
return bucket.id === this.sourceBucket || // When dragging from a bucket who has its limit reached, dragging should still be possible return bucket.id === this.sourceBucket || // When dragging from a bucket who has its limit reached, dragging should still be possible
@ -563,15 +563,13 @@ export default {
this.sourceBucket = bucket.id this.sourceBucket = bucket.id
}, },
toggleDoneBucket(bucket) { toggleDoneBucket(bucket) {
bucket.isDoneBucket = !bucket.isDoneBucket const newBucket = {
this.$store.dispatch('kanban/updateBucket', bucket) ...bucket,
.then(() => { isDoneBucket: !bucket.isDoneBucket,
this.$message.success({message: this.$t('list.kanban.doneBucketSavedSuccess')}) }
}) this.$store.dispatch('kanban/updateBucket', newBucket)
.catch(e => { .then(() => this.$message.success({message: this.$t('list.kanban.doneBucketSavedSuccess')}))
this.$message.error(e) .catch(e => this.$message.error(e))
bucket.isDoneBucket = !bucket.isDoneBucket
})
}, },
collapseBucket(bucket) { collapseBucket(bucket) {
this.collapsedBuckets[bucket.id] = true this.collapsedBuckets[bucket.id] = true

View File

@ -52,7 +52,7 @@
<filter-popup <filter-popup
:visible="showTaskFilter" :visible="showTaskFilter"
v-model="params" v-model="params"
@update:modelValue="loadTasks(1)" @update:modelValue="loadTasks()"
/> />
</div> </div>

View File

@ -61,7 +61,7 @@
<filter-popup <filter-popup
:visible="showTaskFilter" :visible="showTaskFilter"
v-model="params" v-model="params"
@update:modelValue="loadTasks(1)" @update:modelValue="loadTasks()"
/> />
</div> </div>

View File

@ -20,7 +20,7 @@ module.exports = {
template: { template: {
compilerOptions: { compilerOptions: {
compatConfig: { compatConfig: {
MODE: 2, MODE: 3,
}, },
}, },
}, },