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