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:
parent
2270272a8f
commit
4e42810522
8 changed files with 304 additions and 42 deletions
|
@ -198,6 +198,7 @@
|
|||
|
||||
import {filterObject} from '../../../helpers/filterObject'
|
||||
import {applyDrag} from '../../../helpers/applyDrag'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'Kanban',
|
||||
|
@ -211,7 +212,6 @@
|
|||
data() {
|
||||
return {
|
||||
bucketService: BucketService,
|
||||
buckets: [],
|
||||
taskService: TaskService,
|
||||
|
||||
dropPlaceholderOptions: {
|
||||
|
@ -240,12 +240,12 @@
|
|||
this.loadBuckets()
|
||||
setTimeout(() => document.addEventListener('click', this.closeBucketDropdowns), 0)
|
||||
},
|
||||
computed: mapState({
|
||||
buckets: state => state.kanban.buckets,
|
||||
}),
|
||||
methods: {
|
||||
loadBuckets() {
|
||||
this.bucketService.getAll({listId: this.$route.params.listId})
|
||||
.then(r => {
|
||||
this.buckets = r
|
||||
})
|
||||
this.$store.dispatch('kanban/loadBucketsForList', this.$route.params.listId)
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
|
@ -271,7 +271,9 @@
|
|||
delete buckets[bucketIndex]
|
||||
buckets[bucketIndex] = bucket
|
||||
// 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) {
|
||||
|
@ -297,10 +299,10 @@
|
|||
|
||||
task.bucketId = bucketId
|
||||
|
||||
this.taskService.update(task)
|
||||
.then(t => {
|
||||
this.$store.dispatch('tasks/update', task)
|
||||
.then(() => {
|
||||
// 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)
|
||||
})
|
||||
.catch(e => {
|
||||
|
@ -317,14 +319,6 @@
|
|||
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) {
|
||||
this.$set(this.showNewTaskInput, bucket, !this.showNewTaskInput[bucket])
|
||||
},
|
||||
|
@ -360,7 +354,7 @@
|
|||
this.taskService.create(task)
|
||||
.then(r => {
|
||||
this.newTaskText = ''
|
||||
this.buckets[bi].tasks.push(r)
|
||||
this.$store.commit('kanban/addTaskToBucket', r)
|
||||
this.success({message: 'The task was created successfully!'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
|
@ -374,15 +368,10 @@
|
|||
|
||||
const newBucket = new BucketModel({title: this.newBucketTitle, listId: parseInt(this.$route.params.listId)})
|
||||
|
||||
this.bucketService.create(newBucket)
|
||||
.then(r => {
|
||||
this.$store.dispatch('kanban/createBucket', newBucket)
|
||||
.then(() => {
|
||||
this.newBucketTitle = ''
|
||||
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)
|
||||
})
|
||||
.catch(e => {
|
||||
|
@ -402,9 +391,9 @@
|
|||
id: this.bucketToDelete,
|
||||
listId: this.$route.params.listId,
|
||||
})
|
||||
this.bucketService.delete(bucket)
|
||||
|
||||
this.$store.dispatch('kanban/deleteBucket', bucket)
|
||||
.then(r => {
|
||||
this.loadBuckets()
|
||||
this.success(r, this)
|
||||
})
|
||||
.catch(e => {
|
||||
|
@ -430,7 +419,7 @@
|
|||
return
|
||||
}
|
||||
|
||||
this.bucketService.update(bucket)
|
||||
this.$store.dispatch('kanban/updateBucket', bucket)
|
||||
.then(r => {
|
||||
this.success({message: 'The bucket title was updated successfully!'}, this)
|
||||
realBucket.title = r.title
|
||||
|
|
|
@ -54,14 +54,14 @@
|
|||
:class="{ 'disabled': taskService.loading}"
|
||||
class="input"
|
||||
:disabled="taskService.loading"
|
||||
v-model="task.dueDate"
|
||||
v-model="dueDate"
|
||||
:config="flatPickerConfig"
|
||||
@on-close="saveTask"
|
||||
placeholder="Click here to set a due date"
|
||||
ref="dueDate"
|
||||
>
|
||||
</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">
|
||||
<icon icon="times"></icon>
|
||||
</span>
|
||||
|
@ -345,6 +345,9 @@
|
|||
taskService: TaskService,
|
||||
task: TaskModel,
|
||||
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,
|
||||
showDeleteModal: false,
|
||||
|
@ -401,7 +404,7 @@
|
|||
},
|
||||
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.endDate = +new Date(this.task.endDate) === 0 ? null : this.task.endDate
|
||||
|
||||
|
@ -439,13 +442,15 @@
|
|||
},
|
||||
saveTask(undoCallback = null) {
|
||||
|
||||
this.task.dueDate = this.dueDate
|
||||
|
||||
// If no end date is being set, but a start date and due date,
|
||||
// use the due date as the end date
|
||||
if (this.task.endDate === null && this.task.startDate !== null && this.task.dueDate !== null) {
|
||||
this.task.endDate = this.task.dueDate
|
||||
}
|
||||
|
||||
this.taskService.update(this.task)
|
||||
this.$store.dispatch('tasks/update', this.task)
|
||||
.then(r => {
|
||||
this.$set(this, 'task', r)
|
||||
let actions = []
|
||||
|
@ -455,6 +460,7 @@
|
|||
callback: undoCallback,
|
||||
}]
|
||||
}
|
||||
this.dueDate = this.task.dueDate
|
||||
this.success({message: 'The task was saved successfully.'}, this, actions)
|
||||
this.setActiveFields()
|
||||
})
|
||||
|
|
|
@ -160,6 +160,7 @@
|
|||
r.success.forEach(a => {
|
||||
this.success({message: 'Successfully uploaded ' + a.file.name}, this)
|
||||
this.attachments.push(a)
|
||||
this.$store.dispatch('tasks/addTaskAttachment', {taskId: this.taskId, attachment: a})
|
||||
})
|
||||
}
|
||||
if(r.errors !== null) {
|
||||
|
|
|
@ -39,7 +39,6 @@
|
|||
import UserModel from '../../../models/user'
|
||||
import ListUserService from '../../../services/listUsers'
|
||||
import TaskAssigneeService from '../../../services/taskAssignee'
|
||||
import TaskAssigneeModel from '../../../models/taskAssignee'
|
||||
import User from '../../global/user'
|
||||
|
||||
export default {
|
||||
|
@ -84,8 +83,7 @@
|
|||
},
|
||||
methods: {
|
||||
addAssignee(user) {
|
||||
const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: this.taskId})
|
||||
this.taskAssigneeService.create(taskAssignee)
|
||||
this.$store.dispatch('tasks/addAssignee', {user: user, taskId: this.taskId})
|
||||
.then(() => {
|
||||
this.success({message: 'The user was successfully assigned.'}, this)
|
||||
})
|
||||
|
@ -94,8 +92,7 @@
|
|||
})
|
||||
},
|
||||
removeAssignee(user) {
|
||||
const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: this.taskId})
|
||||
this.taskAssigneeService.delete(taskAssignee)
|
||||
this.$store.dispatch('tasks/removeAssignee', {user: user, taskId: this.taskId})
|
||||
.then(() => {
|
||||
// Remove the assignee from the list
|
||||
for (const a in this.assignees) {
|
||||
|
|
|
@ -41,7 +41,6 @@
|
|||
import LabelService from '../../../services/label'
|
||||
import LabelModel from '../../../models/label'
|
||||
import LabelTaskService from '../../../services/labelTask'
|
||||
import LabelTaskModel from '../../../models/labelTask'
|
||||
|
||||
export default {
|
||||
name: 'edit-labels',
|
||||
|
@ -108,8 +107,7 @@
|
|||
this.$set(this, 'foundLabels', [])
|
||||
},
|
||||
addLabel(label) {
|
||||
let labelTask = new LabelTaskModel({taskId: this.taskId, labelId: label.id})
|
||||
this.labelTaskService.create(labelTask)
|
||||
this.$store.dispatch('tasks/addLabel', {label: label, taskId: this.taskId})
|
||||
.then(() => {
|
||||
this.success({message: 'The label was successfully added.'}, this)
|
||||
this.$emit('input', this.labels)
|
||||
|
@ -119,8 +117,7 @@
|
|||
})
|
||||
},
|
||||
removeLabel(label) {
|
||||
let labelTask = new LabelTaskModel({taskId: this.taskId, labelId: label.id})
|
||||
this.labelTaskService.delete(labelTask)
|
||||
this.$store.dispatch('tasks/removeLabel', {label: label, taskId: this.taskId})
|
||||
.then(() => {
|
||||
// Remove the label from the list
|
||||
for (const l in this.labels) {
|
||||
|
|
|
@ -5,6 +5,8 @@ Vue.use(Vuex)
|
|||
import config from './modules/config'
|
||||
import auth from './modules/auth'
|
||||
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'
|
||||
|
||||
export const store = new Vuex.Store({
|
||||
|
@ -12,6 +14,8 @@ export const store = new Vuex.Store({
|
|||
config,
|
||||
auth,
|
||||
namespaces,
|
||||
kanban,
|
||||
tasks,
|
||||
},
|
||||
state: {
|
||||
loading: false,
|
||||
|
|
141
src/store/modules/kanban.js
Normal file
141
src/store/modules/kanban.js
Normal 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
127
src/store/modules/tasks.js
Normal 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)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
Loading…
Reference in a new issue