Handle task relations the right way (#36)

This commit is contained in:
konrad 2019-10-28 21:45:37 +00:00
parent 7a997b52a6
commit 2705c1571e
7 changed files with 398 additions and 262 deletions

View file

@ -3,18 +3,23 @@
<div class="field"> <div class="field">
<label class="label" for="tasktext">Task Text</label> <label class="label" for="tasktext">Task Text</label>
<div class="control"> <div class="control">
<input v-focus :class="{ 'disabled': taskService.loading}" :disabled="taskService.loading" class="input" type="text" id="tasktext" placeholder="The task text is here..." v-model="taskEditTask.text"> <input v-focus :class="{ 'disabled': taskService.loading}" :disabled="taskService.loading" class="input"
type="text" id="tasktext" placeholder="The task text is here..." v-model="taskEditTask.text">
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="taskdescription">Description</label> <label class="label" for="taskdescription">Description</label>
<div class="control"> <div class="control">
<textarea :class="{ 'disabled': taskService.loading}" :disabled="taskService.loading" class="textarea" placeholder="The tasks description goes here..." id="taskdescription" v-model="taskEditTask.description"></textarea> <textarea :class="{ 'disabled': taskService.loading}" :disabled="taskService.loading" class="textarea"
placeholder="The tasks description goes here..." id="taskdescription"
v-model="taskEditTask.description"></textarea>
</div> </div>
</div> </div>
<b>Reminder Dates</b> <b>Reminder Dates</b>
<div class="reminder-input" :class="{ 'overdue': (r < nowUnix && index !== (taskEditTask.reminderDates.length - 1))}" v-for="(r, index) in taskEditTask.reminderDates" :key="index"> <div class="reminder-input"
:class="{ 'overdue': (r < nowUnix && index !== (taskEditTask.reminderDates.length - 1))}"
v-for="(r, index) in taskEditTask.reminderDates" :key="index">
<flat-pickr <flat-pickr
:class="{ 'disabled': taskService.loading}" :class="{ 'disabled': taskService.loading}"
:disabled="taskService.loading" :disabled="taskService.loading"
@ -25,7 +30,9 @@
:data-index="index" :data-index="index"
placeholder="Add a new reminder..."> placeholder="Add a new reminder...">
</flat-pickr> </flat-pickr>
<a v-if="index !== (taskEditTask.reminderDates.length - 1)" @click="removeReminderByIndex(index)"><icon icon="times"></icon></a> <a v-if="index !== (taskEditTask.reminderDates.length - 1)" @click="removeReminderByIndex(index)">
<icon icon="times"></icon>
</a>
</div> </div>
<div class="field"> <div class="field">
@ -147,7 +154,9 @@
<ul class="assingees"> <ul class="assingees">
<li v-for="(a, index) in taskEditTask.assignees" :key="a.id"> <li v-for="(a, index) in taskEditTask.assignees" :key="a.id">
{{a.username}} {{a.username}}
<a @click="deleteAssigneeByIndex(index)"><icon icon="times"/></a> <a @click="deleteAssigneeByIndex(index)">
<icon icon="times"/>
</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -166,7 +175,8 @@
label="username" label="username"
track-by="id"> track-by="id">
<template slot="clear" slot-scope="props"> <template slot="clear" slot-scope="props">
<div class="multiselect__clear" v-if="newAssignee !== null && newAssignee.id !== 0" @mousedown.prevent.stop="clearAllFoundUsers(props.search)"></div> <div class="multiselect__clear" v-if="newAssignee !== null && newAssignee.id !== 0"
@mousedown.prevent.stop="clearAllFoundUsers(props.search)"></div>
</template> </template>
<span slot="noResult">Oops! No user found. Consider changing the search query.</span> <span slot="noResult">Oops! No user found. Consider changing the search query.</span>
</multiselect> </multiselect>
@ -204,45 +214,75 @@
tag-placeholder="Add this as new label" tag-placeholder="Add this as new label"
> >
<template slot="tag" slot-scope="{ option, remove }"> <template slot="tag" slot-scope="{ option, remove }">
<span class="tag" :style="{'background': option.hex_color, 'color': option.textColor}"> <span class="tag"
:style="{'background': option.hex_color, 'color': option.textColor}">
<span>{{ option.title }}</span> <span>{{ option.title }}</span>
<a class="delete is-small" @click="removeLabel(option)"></a> <a class="delete is-small" @click="removeLabel(option)"></a>
</span> </span>
</template> </template>
<template slot="clear" slot-scope="props"> <template slot="clear" slot-scope="props">
<div class="multiselect__clear" v-if="taskEditTask.labels.length" @mousedown.prevent.stop="clearAllLabels(props.search)"></div> <div class="multiselect__clear" v-if="taskEditTask.labels.length"
@mousedown.prevent.stop="clearAllLabels(props.search)"></div>
</template> </template>
</multiselect> </multiselect>
</div> </div>
</div> </div>
<div class="field"> <div class="field" v-for="(rts, kind ) in task.related_tasks" :key="kind" v-if="rts.length > 0">
<label class="label" for="subtasks">Subtasks</label> <label class="label">{{ relationKinds[kind] }}</label>
<div class="tasks noborder" v-if="taskEditTask.subtasks && taskEditTask.subtasks.length > 0"> <div class="tasks noborder">
<div class="task" v-for="s in taskEditTask.subtasks" :key="s.id"> <div class="task" v-for="t in rts" :key="t.id">
<label :for="s.id"> <label>
<div class="fancycheckbox"> <span class="tasktext" :class="{ 'done': t.done}">
<input @change="markAsDone" type="checkbox" :id="s.id" :checked="s.done" style="display: none;"> {{t.text}}
<label :for="s.id" class="check">
<svg width="18px" height="18px" viewBox="0 0 18 18">
<path d="M1,9 L1,3.5 C1,2 2,1 3.5,1 L14.5,1 C16,1 17,2 17,3.5 L17,14.5 C17,16 16,17 14.5,17 L3.5,17 C2,17 1,16 1,14.5 L1,9 Z"></path>
<polyline points="1 9 7 14 15 4"></polyline>
</svg>
</label>
</div>
<span class="tasktext" :class="{ 'done': s.done}">
{{s.text}}
</span> </span>
</label> </label>
<a class="remove" @click="removeTaskRelation({relation_kind: kind, other_task_id: t.id})">
<icon icon="trash-alt"/>
</a>
</div> </div>
</div> </div>
</div>
<div class="field">
<label class="label">New Task Relation</label>
<div class="field">
<div class="control is-expanded">
<multiselect
v-model="newTaskRelationTask"
:options="foundTasks"
:multiple="false"
:searchable="true"
:loading="taskService.loading"
:internal-search="true"
@search-change="findTasks"
placeholder="Type to search"
label="text"
track-by="id">
<template slot="clear" slot-scope="props">
<div class="multiselect__clear"
v-if="newTaskRelationTask !== null && newTaskRelationTask.id !== 0"
@mousedown.prevent.stop="clearAllFoundTasks(props.search)"></div>
</template>
<span slot="noResult">No task found. Consider changing the search query.</span>
</multiselect>
</div>
</div> </div>
<div class="field has-addons"> <div class="field has-addons">
<div class="control is-expanded"> <div class="control is-expanded">
<input @keyup.enter="addSubtask()" :class="{ 'disabled': taskService.loading}" :disabled="taskService.loading" class="input" type="text" id="tasktext" placeholder="New subtask" v-model="newTask.text"/> <div class="select is-fullwidth">
<select v-model="newTaskRelationKind">
<option value="unset">Select a kind of relation</option>
<option v-for="(label, rk) in relationKinds" :key="rk" :value="rk">
{{ label }}
</option>
</select>
</div>
</div> </div>
<div class="control"> <div class="control">
<a class="button is-primary" @click="addSubtask()"><icon icon="plus"></icon></a> <a class="button is-primary" @click="addTaskRelation()">Add task Relation</a>
</div>
</div> </div>
</div> </div>
@ -272,6 +312,9 @@
import LabelService from '../../services/label' import LabelService from '../../services/label'
import LabelTaskModel from '../../models/labelTask' import LabelTaskModel from '../../models/labelTask'
import LabelModel from '../../models/label' import LabelModel from '../../models/label'
import relationKinds from '../../models/relationKinds'
import TaskRelationModel from '../../models/taskRelation'
import TaskRelationService from '../../services/taskRelation'
export default { export default {
name: 'edit-task', name: 'edit-task',
@ -301,6 +344,12 @@
listUserService: ListUserService, listUserService: ListUserService,
foundUsers: [], foundUsers: [],
foundTasks: [],
relationKinds: relationKinds,
newTaskRelationTask: TaskModel,
newTaskRelationKind: 'unset',
taskRelationService: TaskRelationService,
labelService: LabelService, labelService: LabelService,
labelTaskService: LabelTaskService, labelTaskService: LabelTaskService,
foundLabels: [], foundLabels: [],
@ -331,6 +380,8 @@
this.newAssignee = new UserModel() this.newAssignee = new UserModel()
this.labelService = new LabelService() this.labelService = new LabelService()
this.labelTaskService = new LabelTaskService() this.labelTaskService = new LabelTaskService()
this.taskRelationService = new TaskRelationService()
this.newTaskRelationTask = new TaskModel()
this.taskEditTask = this.task this.taskEditTask = this.task
}, },
methods: { methods: {
@ -344,20 +395,6 @@
message.error(e, this) message.error(e, this)
}) })
}, },
addSubtask() {
this.newTask.parentTaskID = this.taskEditTask.id
this.newTask.listID = this.$route.params.id
this.taskService.create(this.newTask)
.then(r => {
this.list.addTaskToList(r)
message.success({message: 'The task was successfully created.'}, this)
})
.catch(e => {
message.error(e, this)
})
this.newTask = {}
},
updateLastReminderDate(selectedDates) { updateLastReminderDate(selectedDates) {
this.lastReminder = +new Date(selectedDates[0]) this.lastReminder = +new Date(selectedDates[0])
}, },
@ -472,7 +509,65 @@
.catch(e => { .catch(e => {
message.error(e, this) message.error(e, this)
}) })
},
findTasks(query) {
if (query === '') {
this.clearAllFoundTasks()
return
} }
this.taskService.getAll({}, {s: query})
.then(response => {
this.$set(this, 'foundTasks', response)
})
.catch(e => {
message.error(e, this)
})
},
clearAllFoundTasks() {
this.$set(this, 'foundTasks', [])
},
addTaskRelation() {
let rel = new TaskRelationModel({
task_id: this.taskEditTask.id,
other_task_id: this.newTaskRelationTask.id,
relation_kind: this.newTaskRelationKind,
})
this.taskRelationService.create(rel)
.then(() => {
if (!this.taskEditTask.related_tasks[this.newTaskRelationKind]) {
this.$set(this.taskEditTask.related_tasks, this.newTaskRelationKind, [])
}
this.taskEditTask.related_tasks[this.newTaskRelationKind].push(this.newTaskRelationTask)
this.newTaskRelationKind = 'unset'
this.newTaskRelationTask = new TaskModel()
message.success({message: 'The task relation was created successfully'}, this)
})
.catch(e => {
message.error(e, this)
})
},
removeTaskRelation(relation) {
let rel = new TaskRelationModel({
relation_kind: relation.relation_kind,
task_id: this.taskEditTask.id,
other_task_id: relation.other_task_id,
})
this.taskRelationService.delete(rel)
.then(r => {
Object.keys(this.taskEditTask.related_tasks).forEach(relationKind => {
for (const t in this.taskEditTask.related_tasks[relationKind]) {
if (this.taskEditTask.related_tasks[relationKind][t].id === relation.other_task_id && relationKind === relation.relation_kind) {
this.taskEditTask.related_tasks[relationKind].splice(t, 1)
}
}
})
message.success(r, this)
})
.catch(e => {
message.error(e, this)
})
},
}, },
} }
</script> </script>

View file

@ -62,17 +62,7 @@ export default class ListModel extends AbstractModel {
* @param task * @param task
*/ */
addTaskToList(task) { addTaskToList(task) {
// If it's a subtask, add it to its parent, otherwise append it to the list of tasks
if (task.parentTaskID === 0) {
this.tasks.push(task) this.tasks.push(task)
} else {
for (const t in this.tasks) {
if (this.tasks[t].id === task.parentTaskID) {
this.tasks[t].subtasks.push(task)
break
}
}
}
this.sortTasks() this.sortTasks()
} }
@ -101,15 +91,6 @@ export default class ListModel extends AbstractModel {
this.tasks[t] = task this.tasks[t] = task
break break
} }
if (this.tasks[t].id === task.parentTaskID) {
for (const s in this.tasks[t].subtasks) {
if (this.tasks[t].subtasks[s].id === task.id) {
this.tasks[t].subtasks[s] = task
break
}
}
}
} }
this.sortTasks() this.sortTasks()
} }

View file

@ -0,0 +1,13 @@
{
"subtask": "Subtask",
"parenttask": "Parent Task",
"related": "Related Task",
"duplicateof": "Duplicate Of",
"duplicates": "Duplicates",
"blocking": "Blocking",
"blocked": "Blocked By",
"precedes": "Preceds",
"follows": "Follows",
"copiedfrom": "Copied From",
"copiedto": "Copied To"
}

View file

@ -37,6 +37,13 @@ export default class TaskModel extends AbstractModel {
if (this.hexColor.substring(0, 1) !== '#') { if (this.hexColor.substring(0, 1) !== '#') {
this.hexColor = '#' + this.hexColor this.hexColor = '#' + this.hexColor
} }
// Make all subtasks to task models
Object.keys(this.related_tasks).forEach(relationKind => {
this.related_tasks[relationKind] = this.related_tasks[relationKind].map(t => {
return new TaskModel(t)
})
})
} }
defaults() { defaults() {
@ -54,10 +61,10 @@ export default class TaskModel extends AbstractModel {
endDate: 0, endDate: 0,
repeatAfter: 0, repeatAfter: 0,
reminderDates: [], reminderDates: [],
subtasks: [],
parentTaskID: 0, parentTaskID: 0,
hexColor: '', hexColor: '',
percentDone: 0, percentDone: 0,
related_tasks: {},
createdBy: UserModel, createdBy: UserModel,
created: 0, created: 0,

View file

@ -0,0 +1,21 @@
import AbstractModel from './abstractModel'
import UserModel from "./user";
export default class TaskRelationModel extends AbstractModel {
constructor(data) {
super(data)
this.created_by = new UserModel(this.created_by)
}
defaults() {
return {
id: 0,
other_task_id: 0,
task_id: 0,
relation_kind: '',
created_by: UserModel,
created: 0,
}
}
}

View file

@ -0,0 +1,15 @@
import AbstractService from "./abstractService";
import TaskRelationModel from '../models/taskRelation'
export default class TaskRelationService extends AbstractService {
constructor() {
super({
create: '/tasks/{task_id}/relations',
delete: '/tasks/{task_id}/relations',
})
}
modelFactory(data) {
return new TaskRelationModel(data)
}
}

View file

@ -68,6 +68,10 @@
} }
} }
.remove {
color: $red;
}
.high-priority{ .high-priority{
color: $red; color: $red;