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 8dd4ae819..f02347518 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 5feebc143..7a7983354 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 e32cc7620..48ed4f12f 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 026462773..8db4b1837 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 ebb37d79d..3ac6d7b2c 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 9cd30c084..b291d4657 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 000000000..ebec1a051 --- /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 000000000..a8b7207af --- /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