From 4e428105222404958d43ac8d619aea71f471df06 Mon Sep 17 00:00:00 2001 From: konrad Date: Sat, 9 May 2020 17:00:54 +0000 Subject: [PATCH] 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 Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/130 --- src/components/lists/views/Kanban.vue | 45 +++--- src/components/tasks/TaskDetailView.vue | 14 +- src/components/tasks/reusable/attachments.vue | 1 + .../tasks/reusable/editAssignees.vue | 7 +- src/components/tasks/reusable/editLabels.vue | 7 +- src/store/index.js | 4 + src/store/modules/kanban.js | 141 ++++++++++++++++++ src/store/modules/tasks.js | 127 ++++++++++++++++ 8 files changed, 304 insertions(+), 42 deletions(-) create mode 100644 src/store/modules/kanban.js create mode 100644 src/store/modules/tasks.js diff --git a/src/components/lists/views/Kanban.vue b/src/components/lists/views/Kanban.vue index 8dd4ae81..f0234751 100644 --- a/src/components/lists/views/Kanban.vue +++ b/src/components/lists/views/Kanban.vue @@ -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 diff --git a/src/components/tasks/TaskDetailView.vue b/src/components/tasks/TaskDetailView.vue index 5feebc14..7a798335 100644 --- a/src/components/tasks/TaskDetailView.vue +++ b/src/components/tasks/TaskDetailView.vue @@ -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" > - + @@ -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() }) diff --git a/src/components/tasks/reusable/attachments.vue b/src/components/tasks/reusable/attachments.vue index e32cc762..48ed4f12 100644 --- a/src/components/tasks/reusable/attachments.vue +++ b/src/components/tasks/reusable/attachments.vue @@ -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) { diff --git a/src/components/tasks/reusable/editAssignees.vue b/src/components/tasks/reusable/editAssignees.vue index 02646277..8db4b183 100644 --- a/src/components/tasks/reusable/editAssignees.vue +++ b/src/components/tasks/reusable/editAssignees.vue @@ -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) { diff --git a/src/components/tasks/reusable/editLabels.vue b/src/components/tasks/reusable/editLabels.vue index ebb37d79..3ac6d7b2 100644 --- a/src/components/tasks/reusable/editLabels.vue +++ b/src/components/tasks/reusable/editLabels.vue @@ -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) { diff --git a/src/store/index.js b/src/store/index.js index 9cd30c08..b291d465 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -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, diff --git a/src/store/modules/kanban.js b/src/store/modules/kanban.js new file mode 100644 index 00000000..ebec1a05 --- /dev/null +++ b/src/store/modules/kanban.js @@ -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) + }) + }, + }, +} \ No newline at end of file diff --git a/src/store/modules/tasks.js b/src/store/modules/tasks.js new file mode 100644 index 00000000..a8b7207a --- /dev/null +++ b/src/store/modules/tasks.js @@ -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) + }) + }, + }, +} \ No newline at end of file