Update tasks in kanban board after editing them in task detail view (#130)

Fix due date disappearing after moving it

Fix removing labels not being updated in store

Fix adding labels not being updated in store

Fix removing assignees not being updated in store

Fix adding assignees not being updated in store

Fix due date not resetting

Fix task attachments not updating in store after being modified in popup view

Fix due date not updating in store after being modified in popup view

Fix using filters for overview views

Fix not re-loading tasks when switching between overviews

Only show undone tasks on task overview page

Update task in bucket when updating in task detail view

Put all bucket related stuff in store

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/130
This commit is contained in:
konrad 2020-05-09 17:00:54 +00:00
parent 2270272a8f
commit 4e42810522
8 changed files with 304 additions and 42 deletions

View file

@ -198,6 +198,7 @@
import {filterObject} from '../../../helpers/filterObject' import {filterObject} from '../../../helpers/filterObject'
import {applyDrag} from '../../../helpers/applyDrag' import {applyDrag} from '../../../helpers/applyDrag'
import {mapState} from 'vuex'
export default { export default {
name: 'Kanban', name: 'Kanban',
@ -211,7 +212,6 @@
data() { data() {
return { return {
bucketService: BucketService, bucketService: BucketService,
buckets: [],
taskService: TaskService, taskService: TaskService,
dropPlaceholderOptions: { dropPlaceholderOptions: {
@ -240,12 +240,12 @@
this.loadBuckets() this.loadBuckets()
setTimeout(() => document.addEventListener('click', this.closeBucketDropdowns), 0) setTimeout(() => document.addEventListener('click', this.closeBucketDropdowns), 0)
}, },
computed: mapState({
buckets: state => state.kanban.buckets,
}),
methods: { methods: {
loadBuckets() { loadBuckets() {
this.bucketService.getAll({listId: this.$route.params.listId}) this.$store.dispatch('kanban/loadBucketsForList', this.$route.params.listId)
.then(r => {
this.buckets = r
})
.catch(e => { .catch(e => {
this.error(e, this) this.error(e, this)
}) })
@ -271,7 +271,9 @@
delete buckets[bucketIndex] delete buckets[bucketIndex]
buckets[bucketIndex] = bucket buckets[bucketIndex] = bucket
// Set the buckets, triggering a state update in vue // Set the buckets, triggering a state update in vue
this.buckets = buckets // FIXME: This seems to set some task attributes (like due date) wrong. Commented out, but seems to still work?
// Not sure what to do about this.
// this.$store.commit('kanban/setBuckets', buckets)
} }
if (dropResult.addedIndex !== null) { if (dropResult.addedIndex !== null) {
@ -297,10 +299,10 @@
task.bucketId = bucketId task.bucketId = bucketId
this.taskService.update(task) this.$store.dispatch('tasks/update', task)
.then(t => { .then(() => {
// Update the block with the new task details // Update the block with the new task details
this.$set(this.buckets[bucketIndex].tasks, taskIndex, t) // this.$store.commit('kanban/setTaskInBucketByIndex', {bucketIndex, taskIndex, task: t})
this.success({message: 'The task was moved successfully!'}, this) this.success({message: 'The task was moved successfully!'}, this)
}) })
.catch(e => { .catch(e => {
@ -317,14 +319,6 @@
return bucket.tasks[index] return bucket.tasks[index]
} }
}, },
getBlockFromTask(task) {
return {
id: task.id,
status: 'bucket' + task.bucketId,
// We're putting the task in an extra property so we won't have to maintin this whole thing because of basically recreating the task model.
task: task,
}
},
toggleShowNewTaskInput(bucket) { toggleShowNewTaskInput(bucket) {
this.$set(this.showNewTaskInput, bucket, !this.showNewTaskInput[bucket]) this.$set(this.showNewTaskInput, bucket, !this.showNewTaskInput[bucket])
}, },
@ -360,7 +354,7 @@
this.taskService.create(task) this.taskService.create(task)
.then(r => { .then(r => {
this.newTaskText = '' this.newTaskText = ''
this.buckets[bi].tasks.push(r) this.$store.commit('kanban/addTaskToBucket', r)
this.success({message: 'The task was created successfully!'}, this) this.success({message: 'The task was created successfully!'}, this)
}) })
.catch(e => { .catch(e => {
@ -374,15 +368,10 @@
const newBucket = new BucketModel({title: this.newBucketTitle, listId: parseInt(this.$route.params.listId)}) const newBucket = new BucketModel({title: this.newBucketTitle, listId: parseInt(this.$route.params.listId)})
this.bucketService.create(newBucket) this.$store.dispatch('kanban/createBucket', newBucket)
.then(r => { .then(() => {
this.newBucketTitle = '' this.newBucketTitle = ''
this.showNewBucketInput = false this.showNewBucketInput = false
if (Array.isArray(this.buckets)) {
this.buckets.push(r)
} else {
this.buckets[r.id] = r
}
this.success({message: 'The bucket was created successfully!'}, this) this.success({message: 'The bucket was created successfully!'}, this)
}) })
.catch(e => { .catch(e => {
@ -402,9 +391,9 @@
id: this.bucketToDelete, id: this.bucketToDelete,
listId: this.$route.params.listId, listId: this.$route.params.listId,
}) })
this.bucketService.delete(bucket)
this.$store.dispatch('kanban/deleteBucket', bucket)
.then(r => { .then(r => {
this.loadBuckets()
this.success(r, this) this.success(r, this)
}) })
.catch(e => { .catch(e => {
@ -430,7 +419,7 @@
return return
} }
this.bucketService.update(bucket) this.$store.dispatch('kanban/updateBucket', bucket)
.then(r => { .then(r => {
this.success({message: 'The bucket title was updated successfully!'}, this) this.success({message: 'The bucket title was updated successfully!'}, this)
realBucket.title = r.title realBucket.title = r.title

View file

@ -54,14 +54,14 @@
:class="{ 'disabled': taskService.loading}" :class="{ 'disabled': taskService.loading}"
class="input" class="input"
:disabled="taskService.loading" :disabled="taskService.loading"
v-model="task.dueDate" v-model="dueDate"
:config="flatPickerConfig" :config="flatPickerConfig"
@on-close="saveTask" @on-close="saveTask"
placeholder="Click here to set a due date" placeholder="Click here to set a due date"
ref="dueDate" ref="dueDate"
> >
</flat-pickr> </flat-pickr>
<a v-if="task.dueDate" @click="() => {task.dueDate = null;saveTask()}"> <a v-if="dueDate" @click="() => {dueDate = task.dueDate = null;saveTask()}">
<span class="icon is-small"> <span class="icon is-small">
<icon icon="times"></icon> <icon icon="times"></icon>
</span> </span>
@ -345,6 +345,9 @@
taskService: TaskService, taskService: TaskService,
task: TaskModel, task: TaskModel,
relationKinds: relationKinds, relationKinds: relationKinds,
// The due date is a seperate property in the task to prevent flatpickr from modifying the task model
// in store right after updating it from the api resulting in the wrong due date format being saved in the task.
dueDate: null,
namespace: NamespaceModel, namespace: NamespaceModel,
showDeleteModal: false, showDeleteModal: false,
@ -401,7 +404,7 @@
}, },
setActiveFields() { setActiveFields() {
this.task.dueDate = +new Date(this.task.dueDate) === 0 ? null : this.task.dueDate this.dueDate = +new Date(this.task.dueDate) === 0 ? null : this.task.dueDate
this.task.startDate = +new Date(this.task.startDate) === 0 ? null : this.task.startDate this.task.startDate = +new Date(this.task.startDate) === 0 ? null : this.task.startDate
this.task.endDate = +new Date(this.task.endDate) === 0 ? null : this.task.endDate this.task.endDate = +new Date(this.task.endDate) === 0 ? null : this.task.endDate
@ -439,13 +442,15 @@
}, },
saveTask(undoCallback = null) { saveTask(undoCallback = null) {
this.task.dueDate = this.dueDate
// If no end date is being set, but a start date and due date, // If no end date is being set, but a start date and due date,
// use the due date as the end date // use the due date as the end date
if (this.task.endDate === null && this.task.startDate !== null && this.task.dueDate !== null) { if (this.task.endDate === null && this.task.startDate !== null && this.task.dueDate !== null) {
this.task.endDate = this.task.dueDate this.task.endDate = this.task.dueDate
} }
this.taskService.update(this.task) this.$store.dispatch('tasks/update', this.task)
.then(r => { .then(r => {
this.$set(this, 'task', r) this.$set(this, 'task', r)
let actions = [] let actions = []
@ -455,6 +460,7 @@
callback: undoCallback, callback: undoCallback,
}] }]
} }
this.dueDate = this.task.dueDate
this.success({message: 'The task was saved successfully.'}, this, actions) this.success({message: 'The task was saved successfully.'}, this, actions)
this.setActiveFields() this.setActiveFields()
}) })

View file

@ -160,6 +160,7 @@
r.success.forEach(a => { r.success.forEach(a => {
this.success({message: 'Successfully uploaded ' + a.file.name}, this) this.success({message: 'Successfully uploaded ' + a.file.name}, this)
this.attachments.push(a) this.attachments.push(a)
this.$store.dispatch('tasks/addTaskAttachment', {taskId: this.taskId, attachment: a})
}) })
} }
if(r.errors !== null) { if(r.errors !== null) {

View file

@ -39,7 +39,6 @@
import UserModel from '../../../models/user' import UserModel from '../../../models/user'
import ListUserService from '../../../services/listUsers' import ListUserService from '../../../services/listUsers'
import TaskAssigneeService from '../../../services/taskAssignee' import TaskAssigneeService from '../../../services/taskAssignee'
import TaskAssigneeModel from '../../../models/taskAssignee'
import User from '../../global/user' import User from '../../global/user'
export default { export default {
@ -84,8 +83,7 @@
}, },
methods: { methods: {
addAssignee(user) { addAssignee(user) {
const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: this.taskId}) this.$store.dispatch('tasks/addAssignee', {user: user, taskId: this.taskId})
this.taskAssigneeService.create(taskAssignee)
.then(() => { .then(() => {
this.success({message: 'The user was successfully assigned.'}, this) this.success({message: 'The user was successfully assigned.'}, this)
}) })
@ -94,8 +92,7 @@
}) })
}, },
removeAssignee(user) { removeAssignee(user) {
const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: this.taskId}) this.$store.dispatch('tasks/removeAssignee', {user: user, taskId: this.taskId})
this.taskAssigneeService.delete(taskAssignee)
.then(() => { .then(() => {
// Remove the assignee from the list // Remove the assignee from the list
for (const a in this.assignees) { for (const a in this.assignees) {

View file

@ -41,7 +41,6 @@
import LabelService from '../../../services/label' import LabelService from '../../../services/label'
import LabelModel from '../../../models/label' import LabelModel from '../../../models/label'
import LabelTaskService from '../../../services/labelTask' import LabelTaskService from '../../../services/labelTask'
import LabelTaskModel from '../../../models/labelTask'
export default { export default {
name: 'edit-labels', name: 'edit-labels',
@ -108,8 +107,7 @@
this.$set(this, 'foundLabels', []) this.$set(this, 'foundLabels', [])
}, },
addLabel(label) { addLabel(label) {
let labelTask = new LabelTaskModel({taskId: this.taskId, labelId: label.id}) this.$store.dispatch('tasks/addLabel', {label: label, taskId: this.taskId})
this.labelTaskService.create(labelTask)
.then(() => { .then(() => {
this.success({message: 'The label was successfully added.'}, this) this.success({message: 'The label was successfully added.'}, this)
this.$emit('input', this.labels) this.$emit('input', this.labels)
@ -119,8 +117,7 @@
}) })
}, },
removeLabel(label) { removeLabel(label) {
let labelTask = new LabelTaskModel({taskId: this.taskId, labelId: label.id}) this.$store.dispatch('tasks/removeLabel', {label: label, taskId: this.taskId})
this.labelTaskService.delete(labelTask)
.then(() => { .then(() => {
// Remove the label from the list // Remove the label from the list
for (const l in this.labels) { for (const l in this.labels) {

View file

@ -5,6 +5,8 @@ Vue.use(Vuex)
import config from './modules/config' import config from './modules/config'
import auth from './modules/auth' import auth from './modules/auth'
import namespaces from './modules/namespaces' import namespaces from './modules/namespaces'
import kanban from './modules/kanban'
import tasks from './modules/tasks'
import {CURRENT_LIST, ERROR_MESSAGE, IS_FULLPAGE, LOADING, ONLINE} from './mutation-types' import {CURRENT_LIST, ERROR_MESSAGE, IS_FULLPAGE, LOADING, ONLINE} from './mutation-types'
export const store = new Vuex.Store({ export const store = new Vuex.Store({
@ -12,6 +14,8 @@ export const store = new Vuex.Store({
config, config,
auth, auth,
namespaces, namespaces,
kanban,
tasks,
}, },
state: { state: {
loading: false, loading: false,

141
src/store/modules/kanban.js Normal file
View file

@ -0,0 +1,141 @@
import Vue from 'vue'
import BucketService from '../../services/bucket'
import {filterObject} from '../../helpers/filterObject'
/**
* This store is intended to hold the currently active kanban view.
* It should hold only the current buckets.
*/
export default {
namespaced: true,
state: () => ({
buckets: [],
}),
mutations: {
setBuckets(state, buckets) {
state.buckets = buckets
},
addBucket(state, bucket) {
state.buckets.push(bucket)
},
removeBucket(state, bucket) {
for (const b in state.buckets) {
if (state.buckets[b].id === bucket.id) {
state.buckets.splice(b, 1)
}
}
},
setBucketById(state, bucket) {
for (const b in state.buckets) {
if (state.buckets[b].id === bucket.id) {
Vue.set(state.buckets, b, bucket)
return
}
}
},
setBucketByIndex(state, {bucketIndex, bucket}) {
Vue.set(state.buckets, bucketIndex, bucket)
},
setTaskInBucketByIndex(state, {bucketIndex, taskIndex, task}) {
const bucket = state.buckets[bucketIndex]
bucket.tasks[taskIndex] = task
Vue.set(state.buckets, bucketIndex, bucket)
},
setTaskInBucket(state, task) {
// If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task
if (state.buckets.length === 0) {
return
}
for (const b in state.buckets) {
if (state.buckets[b].id === task.bucketId) {
for (const t in state.buckets[b].tasks) {
if (state.buckets[b].tasks[t].id === task.id) {
const bucket = state.buckets[b]
bucket.tasks[t] = task
Vue.set(state.buckets, b, bucket)
return
}
}
return
}
}
},
addTaskToBucket(state, task) {
const bi = filterObject(state.buckets, b => b.id === task.bucketId)
state.buckets[bi].tasks.push(task)
},
},
getters: {
getTaskById: state => id => {
for (const b in state.buckets) {
for (const t in state.buckets[b].tasks) {
if (state.buckets[b].tasks[t].id === id) {
return {
bucketIndex: b,
taskIndex: t,
task: state.buckets[b].tasks[t],
}
}
}
}
return {
bucketIndex: null,
taskIndex: null,
task: null,
}
},
},
actions: {
loadBucketsForList(ctx, listId) {
const bucketService = new BucketService()
return bucketService.getAll({listId: listId})
.then(r => {
ctx.commit('setBuckets', r)
return Promise.resolve()
})
.catch(e => {
return Promise.reject(e)
})
},
createBucket(ctx, bucket) {
const bucketService = new BucketService()
return bucketService.create(bucket)
.then(r => {
ctx.commit('addBucket', r)
return Promise.resolve(r)
})
.catch(e => {
return Promise.reject(e)
})
},
deleteBucket(ctx, bucket) {
const bucketService = new BucketService()
return bucketService.delete(bucket)
.then(r => {
ctx.commit('removeBucket', bucket)
// We reload all buckets because tasks are being moved from the deleted bucket
ctx.dispatch('loadBucketsForList', bucket.listId)
return Promise.resolve(r)
})
.catch(e => {
return Promise.reject(e)
})
},
updateBucket(ctx, bucket) {
const bucketService = new BucketService()
return bucketService.update(bucket)
.then(r => {
const bi = filterObject(ctx.state.buckets, b => b.id === r.id)
const bucket = r
bucket.tasks = ctx.state.buckets[bi].tasks
ctx.commit('setBucketByIndex', {bucketIndex: bi, bucket})
return Promise.resolve(r)
})
.catch(e => {
return Promise.reject(e)
})
},
},
}

127
src/store/modules/tasks.js Normal file
View file

@ -0,0 +1,127 @@
import TaskService from '../../services/task'
import TaskAssigneeService from '../../services/taskAssignee'
import TaskAssigneeModel from '../../models/taskAssignee'
import LabelTaskModel from '../../models/labelTask'
import LabelTaskService from '../../services/labelTask'
export default {
namespaced: true,
state: () => ({}),
actions: {
update(ctx, task) {
const taskService = new TaskService()
return taskService.update(task)
.then(t => {
ctx.commit('kanban/setTaskInBucket', t, {root: true})
return Promise.resolve(t)
})
.catch(e => {
return Promise.reject(e)
})
},
// Adds a task attachment in store.
// This is an action to be able to commit other mutations
addTaskAttachment(ctx, {taskId, attachment}) {
const t = ctx.rootGetters['kanban/getTaskById'](taskId)
if (t.task === null) {
return
}
t.task.attachments.push(attachment)
ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true})
},
addAssignee(ctx, {user, taskId}) {
const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: taskId})
const taskAssigneeService = new TaskAssigneeService()
return taskAssigneeService.create(taskAssignee)
.then(r => {
const t = ctx.rootGetters['kanban/getTaskById'](taskId)
if (t.task === null) {
return Promise.reject('Task not found.')
}
t.task.assignees.push(user)
ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true})
return Promise.resolve(r)
})
.catch(e => {
return Promise.reject(e)
})
},
removeAssignee(ctx, {user, taskId}) {
const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: taskId})
const taskAssigneeService = new TaskAssigneeService()
return taskAssigneeService.delete(taskAssignee)
.then(r => {
const t = ctx.rootGetters['kanban/getTaskById'](taskId)
if (t.task === null) {
return Promise.reject('Task not found.')
}
for (const a in t.task.assignees) {
if (t.task.assignees[a].id === user.id) {
t.task.assignees.splice(a, 1)
break
}
}
ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true})
return Promise.resolve(r)
})
.catch(e => {
return Promise.reject(e)
})
},
addLabel(ctx, {label, taskId}) {
const labelTaskService = new LabelTaskService()
const labelTask = new LabelTaskModel({taskId: taskId, labelId: label.id})
return labelTaskService.create(labelTask)
.then(r => {
const t = ctx.rootGetters['kanban/getTaskById'](taskId)
if (t.task === null) {
return Promise.reject('Task not found.')
}
t.task.labels.push(label)
ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true})
return Promise.resolve(r)
})
.catch(e => {
return Promise.reject(e)
})
},
removeLabel(ctx, {label, taskId}) {
const labelTaskService = new LabelTaskService()
const labelTask = new LabelTaskModel({taskId: taskId, labelId: label.id})
return labelTaskService.delete(labelTask)
.then(r => {
const t = ctx.rootGetters['kanban/getTaskById'](taskId)
if (t.task === null) {
return Promise.reject('Task not found.')
}
// Remove the label from the list
for (const l in t.task.labels) {
if (t.task.labels[l].id === label.id) {
t.task.labels.splice(l, 1)
break
}
}
ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true})
return Promise.resolve(r)
})
.catch(e => {
return Promise.reject(e)
})
},
},
}