Gantt Charts (#29)
This commit is contained in:
parent
0aa0a39620
commit
d03f0211a3
23 changed files with 1454 additions and 565 deletions
|
@ -12,7 +12,8 @@
|
|||
"lodash": "^4.17.11",
|
||||
"v-tooltip": "^2.0.0-rc.33",
|
||||
"verte": "^0.0.10",
|
||||
"vue": "^2.5.17"
|
||||
"vue": "^2.5.17",
|
||||
"vue-drag-resize": "^1.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.4",
|
||||
|
|
|
@ -51,9 +51,7 @@ export default {
|
|||
email: creds.email,
|
||||
password: creds.password
|
||||
})
|
||||
.then(response => {
|
||||
// eslint-disable-next-line
|
||||
console.log(response)
|
||||
.then(() => {
|
||||
this.login(context, creds, redirect)
|
||||
})
|
||||
.catch(e => {
|
||||
|
|
|
@ -5,301 +5,14 @@
|
|||
<icon icon="cog" size="2x"/>
|
||||
</router-link>
|
||||
<h1>{{ list.title }}</h1>
|
||||
</div>
|
||||
<form @submit.prevent="addTask()">
|
||||
<div class="field is-grouped">
|
||||
<p class="control has-icons-left is-expanded" :class="{ 'is-loading': taskService.loading}">
|
||||
<input v-focus class="input" :class="{ 'disabled': taskService.loading}" v-model="newTask.text" type="text" placeholder="Add a new task...">
|
||||
<span class="icon is-small is-left">
|
||||
<icon icon="tasks"/>
|
||||
</span>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button type="submit" class="button is-success">
|
||||
<span class="icon is-small">
|
||||
<icon icon="plus"/>
|
||||
</span>
|
||||
Add
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="tasks" v-if="this.list.tasks && this.list.tasks.length > 0" :class="{'short': isTaskEdit}">
|
||||
<div class="task" v-for="l in list.tasks" :key="l.id">
|
||||
<label :for="l.id">
|
||||
<div class="fancycheckbox">
|
||||
<input @change="markAsDone" type="checkbox" :id="l.id" :checked="l.done" style="display: none;">
|
||||
<label :for="l.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': l.done}">
|
||||
{{l.text}}
|
||||
<span class="tag" v-for="label in l.labels" :style="{'background': label.hex_color, 'color': label.textColor}" :key="label.id">
|
||||
<span>{{ label.title }}</span>
|
||||
</span>
|
||||
<i v-if="l.dueDate > 0" :class="{'overdue': (l.dueDate <= new Date())}"> - Due on {{new Date(l.dueDate).toLocaleString()}}</i>
|
||||
<span v-if="l.priority >= priorities.HIGH" class="high-priority" :class="{'not-so-high': l.priority === priorities.HIGH}">
|
||||
<span class="icon">
|
||||
<icon icon="exclamation"/>
|
||||
</span>
|
||||
<template v-if="l.priority === priorities.HIGH">High</template>
|
||||
<template v-if="l.priority === priorities.URGENT">Urgent</template>
|
||||
<template v-if="l.priority === priorities.DO_NOW">DO NOW</template>
|
||||
<span class="icon" v-if="l.priority === priorities.DO_NOW">
|
||||
<icon icon="exclamation"/>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div @click="editTask(l.id)" class="icon settings">
|
||||
<icon icon="cog"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4" v-if="isTaskEdit">
|
||||
<div class="card taskedit">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
Edit Task
|
||||
</p>
|
||||
<a class="card-header-icon" @click="isTaskEdit = false">
|
||||
<span class="icon">
|
||||
<icon icon="angle-right"/>
|
||||
</span>
|
||||
</a>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
<form @submit.prevent="editTaskSubmit()">
|
||||
<div class="field">
|
||||
<label class="label" for="tasktext">Task Text</label>
|
||||
<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">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="taskdescription">Description</label>
|
||||
<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>
|
||||
<div class="switch-view">
|
||||
<router-link :to="{ name: 'showList', params: { id: list.id } }" :class="{'is-active': $route.params.type !== 'gantt'}">List</router-link>
|
||||
<router-link :to="{ name: 'showListWithType', params: { id: list.id, type: 'gantt' } }" :class="{'is-active': $route.params.type === 'gantt'}">Gantt</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<flat-pickr
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
:disabled="taskService.loading"
|
||||
:v-model="taskEditTask.reminderDates"
|
||||
:config="flatPickerConfig"
|
||||
:id="'taskreminderdate' + index"
|
||||
:value="r"
|
||||
:data-index="index"
|
||||
placeholder="Add a new reminder...">
|
||||
</flat-pickr>
|
||||
<a v-if="index !== (taskEditTask.reminderDates.length - 1)" @click="removeReminderByIndex(index)"><icon icon="times"></icon></a>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="taskduedate">Due Date</label>
|
||||
<div class="control">
|
||||
<flat-pickr
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
class="input"
|
||||
:disabled="taskService.loading"
|
||||
v-model="taskEditTask.dueDate"
|
||||
:config="flatPickerConfig"
|
||||
id="taskduedate"
|
||||
placeholder="The tasks due date is here...">
|
||||
</flat-pickr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="">Duration</label>
|
||||
<div class="control columns">
|
||||
<div class="column">
|
||||
<flat-pickr
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
class="input"
|
||||
:disabled="taskService.loading"
|
||||
v-model="taskEditTask.startDate"
|
||||
:config="flatPickerConfig"
|
||||
id="taskduedate"
|
||||
placeholder="Start date">
|
||||
</flat-pickr>
|
||||
</div>
|
||||
<div class="column">
|
||||
<flat-pickr
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
class="input"
|
||||
:disabled="taskService.loading"
|
||||
v-model="taskEditTask.endDate"
|
||||
:config="flatPickerConfig"
|
||||
id="taskduedate"
|
||||
placeholder="End date">
|
||||
</flat-pickr>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="">Repeat after</label>
|
||||
<div class="control repeat-after-input columns">
|
||||
<div class="column">
|
||||
<input class="input" placeholder="Specify an amount..." v-model="taskEditTask.repeatAfter.amount"/>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="select">
|
||||
<select v-model="taskEditTask.repeatAfter.type">
|
||||
<option value="hours">Hours</option>
|
||||
<option value="days">Days</option>
|
||||
<option value="weeks">Weeks</option>
|
||||
<option value="months">Months</option>
|
||||
<option value="years">Years</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="">Priority</label>
|
||||
<div class="control priority-select">
|
||||
<div class="select">
|
||||
<select v-model="taskEditTask.priority">
|
||||
<option :value="priorities.UNSET">Unset</option>
|
||||
<option :value="priorities.LOW">Low</option>
|
||||
<option :value="priorities.MEDIUM">Medium</option>
|
||||
<option :value="priorities.HIGH">High</option>
|
||||
<option :value="priorities.URGENT">Urgent</option>
|
||||
<option :value="priorities.DO_NOW">DO NOW</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="">Assignees</label>
|
||||
<ul class="assingees">
|
||||
<li v-for="(a, index) in taskEditTask.assignees" :key="a.id">
|
||||
{{a.username}}
|
||||
<a @click="deleteAssigneeByIndex(index)"><icon icon="times"/></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<multiselect
|
||||
v-model="newAssignee"
|
||||
:options="foundUsers"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
:loading="userService.loading"
|
||||
:internal-search="true"
|
||||
@search-change="findUser"
|
||||
placeholder="Type to search"
|
||||
label="username"
|
||||
track-by="id">
|
||||
<template slot="clear" slot-scope="props">
|
||||
<div class="multiselect__clear" v-if="newAssignee !== null && newAssignee.id !== 0" @mousedown.prevent.stop="clearAllFoundUsers(props.search)"></div>
|
||||
</template>
|
||||
<span slot="noResult">Oops! No user found. Consider changing the search query.</span>
|
||||
</multiselect>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a @click="addAssignee" class="button is-primary fullheight">
|
||||
<span class="icon is-small">
|
||||
<icon icon="plus"/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Labels</label>
|
||||
<div class="control">
|
||||
<multiselect
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="true"
|
||||
:options-limit="300"
|
||||
:hide-selected="true"
|
||||
v-model="taskEditTask.labels"
|
||||
:options="foundLabels"
|
||||
:searchable="true"
|
||||
:loading="labelService.loading || labelTaskService.loading"
|
||||
:internal-search="true"
|
||||
@search-change="findLabel"
|
||||
@select="addLabel"
|
||||
placeholder="Type to search"
|
||||
label="title"
|
||||
track-by="id"
|
||||
:taggable="true"
|
||||
@tag="createAndAddLabel"
|
||||
tag-placeholder="Add this as new label"
|
||||
>
|
||||
<template slot="tag" slot-scope="{ option, remove }">
|
||||
<span class="tag" :style="{'background': option.hex_color, 'color': option.textColor}">
|
||||
<span>{{ option.title }}</span>
|
||||
<a class="delete is-small" @click="removeLabel(option)"></a>
|
||||
</span>
|
||||
</template>
|
||||
<template slot="clear" slot-scope="props">
|
||||
<div class="multiselect__clear" v-if="taskEditTask.labels.length" @mousedown.prevent.stop="clearAllLabels(props.search)"></div>
|
||||
</template>
|
||||
</multiselect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="subtasks">Subtasks</label>
|
||||
<div class="tasks noborder" v-if="taskEditTask.subtasks && taskEditTask.subtasks.length > 0">
|
||||
<div class="task" v-for="s in taskEditTask.subtasks" :key="s.id">
|
||||
<label :for="s.id">
|
||||
<div class="fancycheckbox">
|
||||
<input @change="markAsDone" type="checkbox" :id="s.id" :checked="s.done" style="display: none;">
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field has-addons">
|
||||
<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>
|
||||
<div class="control">
|
||||
<a class="button is-primary" @click="addSubtask()"><icon icon="plus"></icon></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button is-success is-fullwidth" :class="{ 'is-loading': taskService.loading}">
|
||||
Save
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<gantt :list="list" v-if="$route.params.type === 'gantt'"/>
|
||||
<show-list-task :the-list="list" v-else/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -307,76 +20,39 @@
|
|||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import message from '../../message'
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
import multiselect from 'vue-multiselect'
|
||||
import {differenceWith} from 'lodash'
|
||||
|
||||
import ListService from '../../services/list'
|
||||
import TaskService from '../../services/task'
|
||||
import TaskModel from '../../models/task'
|
||||
import ShowListTask from '../tasks/ShowListTasks'
|
||||
import Gantt from '../tasks/Gantt'
|
||||
|
||||
import ListModel from '../../models/list'
|
||||
import UserModel from '../../models/user'
|
||||
import UserService from '../../services/user'
|
||||
import priorities from '../../models/priorities'
|
||||
import LabelTaskService from '../../services/labelTask'
|
||||
import LabelService from '../../services/label'
|
||||
import LabelTaskModel from '../../models/labelTask'
|
||||
import LabelModel from '../../models/label'
|
||||
import ListService from '../../services/list'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
listID: this.$route.params.id,
|
||||
listService: ListService,
|
||||
taskService: TaskService,
|
||||
|
||||
priorities: priorities,
|
||||
list: {},
|
||||
newTask: TaskModel,
|
||||
isTaskEdit: false,
|
||||
taskEditTask: {
|
||||
subtasks: [],
|
||||
},
|
||||
lastReminder: 0,
|
||||
nowUnix: new Date(),
|
||||
flatPickerConfig:{
|
||||
altFormat: 'j M Y H:i',
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
enableTime: true,
|
||||
onOpen: this.updateLastReminderDate,
|
||||
onClose: this.addReminderDate,
|
||||
},
|
||||
|
||||
newAssignee: UserModel,
|
||||
userService: UserService,
|
||||
foundUsers: [],
|
||||
|
||||
labelService: LabelService,
|
||||
labelTaskService: LabelTaskService,
|
||||
foundLabels: [],
|
||||
labelTimeout: null,
|
||||
list: ListModel,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
flatPickr,
|
||||
multiselect,
|
||||
Gantt,
|
||||
ShowListTask,
|
||||
},
|
||||
beforeMount() {
|
||||
// Check if the user is already logged in, if so, redirect him to the homepage
|
||||
if (!auth.user.authenticated) {
|
||||
router.push({name: 'home'})
|
||||
}
|
||||
|
||||
// If the type is invalid, redirect the user
|
||||
if (this.$route.params.type !== 'gantt' && this.$route.params.type !== '') {
|
||||
router.push({name: 'showList', params: { id: this.$route.params.id }})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.listService = new ListService()
|
||||
this.taskService = new TaskService()
|
||||
this.newTask = new TaskModel()
|
||||
this.userService = new UserService()
|
||||
this.newAssignee = new UserModel()
|
||||
this.labelService = new LabelService()
|
||||
this.labelTaskService = new LabelTaskService()
|
||||
this.list = new ListModel()
|
||||
this.loadList()
|
||||
},
|
||||
watch: {
|
||||
|
@ -385,7 +61,6 @@
|
|||
},
|
||||
methods: {
|
||||
loadList() {
|
||||
this.isTaskEdit = false
|
||||
// We create an extra list object instead of creating it in this.list because that would trigger a ui update which would result in bad ux.
|
||||
let list = new ListModel({id: this.$route.params.id})
|
||||
this.listService.get(list)
|
||||
|
@ -396,195 +71,6 @@
|
|||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
addTask() {
|
||||
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 = {}
|
||||
},
|
||||
markAsDone(e) {
|
||||
let updateFunc = () => {
|
||||
// We get the task, update the 'done' property and then push it to the api.
|
||||
let task = this.list.getTaskByID(e.target.id)
|
||||
task.done = e.target.checked
|
||||
this.taskService.update(task)
|
||||
.then(r => {
|
||||
this.updateTaskInList(r)
|
||||
message.success({message: 'The task was successfully ' + (task.done ? '' : 'un-') + 'marked as done.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
}
|
||||
|
||||
if (e.target.checked) {
|
||||
setTimeout(updateFunc(), 300); // Delay it to show the animation when marking a task as done
|
||||
} else {
|
||||
updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
|
||||
}
|
||||
},
|
||||
editTask(id) {
|
||||
// Find the selected task and set it to the current object
|
||||
let theTask = this.list.getTaskByID(id) // Somehow this does not work if we directly assign this to this.taskEditTask
|
||||
this.taskEditTask = theTask
|
||||
this.isTaskEdit = true
|
||||
},
|
||||
editTaskSubmit() {
|
||||
this.taskService.update(this.taskEditTask)
|
||||
.then(r => {
|
||||
this.updateTaskInList(r)
|
||||
this.$set(this, 'taskEditTask', r)
|
||||
message.success({message: 'The task was successfully updated.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
addSubtask() {
|
||||
this.newTask.parentTaskID = this.taskEditTask.id
|
||||
this.addTask()
|
||||
},
|
||||
updateTaskInList(task) {
|
||||
// We need to do the update here in the component, because there is no way of notifiying vue of the change otherwise.
|
||||
for (const t in this.list.tasks) {
|
||||
if (this.list.tasks[t].id === task.id) {
|
||||
this.$set(this.list.tasks, t, task)
|
||||
break
|
||||
}
|
||||
|
||||
if (this.list.tasks[t].id === task.parentTaskID) {
|
||||
for (const s in this.list.tasks[t].subtasks) {
|
||||
if (this.list.tasks[t].subtasks[s].id === task.id) {
|
||||
this.$set(this.list.tasks[t].subtasks, s, task)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.list.sortTasks()
|
||||
},
|
||||
updateLastReminderDate(selectedDates) {
|
||||
this.lastReminder = +new Date(selectedDates[0])
|
||||
},
|
||||
addReminderDate(selectedDates, dateStr, instance) {
|
||||
let newDate = +new Date(selectedDates[0])
|
||||
|
||||
// Don't update if nothing changed
|
||||
if (newDate === this.lastReminder) {
|
||||
return
|
||||
}
|
||||
|
||||
let index = parseInt(instance.input.dataset.index)
|
||||
this.taskEditTask.reminderDates[index] = newDate
|
||||
|
||||
let lastIndex = this.taskEditTask.reminderDates.length - 1
|
||||
// put a new null at the end if we changed something
|
||||
if (lastIndex === index && !isNaN(newDate)) {
|
||||
this.taskEditTask.reminderDates.push(null)
|
||||
}
|
||||
},
|
||||
removeReminderByIndex(index) {
|
||||
this.taskEditTask.reminderDates.splice(index, 1)
|
||||
// Reset the last to 0 to have the "add reminder" button
|
||||
this.taskEditTask.reminderDates[this.taskEditTask.reminderDates.length - 1] = null
|
||||
},
|
||||
addAssignee() {
|
||||
this.taskEditTask.assignees.push(this.newAssignee)
|
||||
},
|
||||
deleteAssigneeByIndex(index) {
|
||||
this.taskEditTask.assignees.splice(index, 1)
|
||||
},
|
||||
findUser(query) {
|
||||
if(query === '') {
|
||||
this.clearAllFoundUsers()
|
||||
return
|
||||
}
|
||||
|
||||
this.userService.getAll({}, {s: query})
|
||||
.then(response => {
|
||||
// Filter the results to not include users who are already assigned
|
||||
this.$set(this, 'foundUsers', differenceWith(response, this.taskEditTask.assignees, (first, second) => {
|
||||
return first.id === second.id
|
||||
}))
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
clearAllFoundUsers () {
|
||||
this.$set(this, 'foundUsers', [])
|
||||
},
|
||||
findLabel(query) {
|
||||
if(query === '') {
|
||||
this.clearAllLabels()
|
||||
return
|
||||
}
|
||||
|
||||
if(this.labelTimeout !== null) {
|
||||
clearTimeout(this.labelTimeout)
|
||||
}
|
||||
|
||||
// Delay the search 300ms to not send a request on every keystroke
|
||||
this.labelTimeout = setTimeout(() => {
|
||||
this.labelService.getAll({}, {s: query})
|
||||
.then(response => {
|
||||
this.$set(this, 'foundLabels', differenceWith(response, this.taskEditTask.labels, (first, second) => {
|
||||
return first.id === second.id
|
||||
}))
|
||||
this.labelTimeout = null
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
}, 300)
|
||||
},
|
||||
clearAllLabels () {
|
||||
this.$set(this, 'foundLabels', [])
|
||||
},
|
||||
addLabel(label) {
|
||||
let labelTask = new LabelTaskModel({taskID: this.taskEditTask.id, label_id: label.id})
|
||||
this.labelTaskService.create(labelTask)
|
||||
.then(() => {
|
||||
message.success({message: 'The label was successfully added.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
removeLabel(label) {
|
||||
let labelTask = new LabelTaskModel({taskID: this.taskEditTask.id, label_id: label.id})
|
||||
this.labelTaskService.delete(labelTask)
|
||||
.then(() => {
|
||||
// Remove the label from the list
|
||||
for (const l in this.taskEditTask.labels) {
|
||||
if (this.taskEditTask.labels[l].id === label.id) {
|
||||
this.taskEditTask.labels.splice(l, 1)
|
||||
}
|
||||
}
|
||||
message.success({message: 'The label was successfully removed.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
createAndAddLabel(title) {
|
||||
let newLabel = new LabelModel({title: title})
|
||||
this.labelService.create(newLabel)
|
||||
.then(r => {
|
||||
this.addLabel(r)
|
||||
this.taskEditTask.labels.push(r)
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
101
src/components/tasks/Gantt.vue
Normal file
101
src/components/tasks/Gantt.vue
Normal file
|
@ -0,0 +1,101 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="gantt-options">
|
||||
<div class="fancycheckbox is-block">
|
||||
<input id="showTaskswithoutDates" type="checkbox" style="display: none;" v-model="showTaskswithoutDates">
|
||||
<label for="showTaskswithoutDates" 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>
|
||||
<span>
|
||||
Show tasks which don't have dates set
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="range-picker">
|
||||
<div class="field">
|
||||
<label class="label" for="dayWidth">Size</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select id="dayWidth" v-model.number="dayWidth">
|
||||
<option value="35">Default</option>
|
||||
<option value="10">Month</option>
|
||||
<option value="80">Day</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="fromDate">From</label>
|
||||
<div class="control">
|
||||
<flat-pickr
|
||||
class="input"
|
||||
v-model="dateFrom"
|
||||
:config="flatPickerConfig"
|
||||
id="fromDate"
|
||||
placeholder="From"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="toDate">To</label>
|
||||
<div class="control">
|
||||
<flat-pickr
|
||||
class="input"
|
||||
v-model="dateTo"
|
||||
:config="flatPickerConfig"
|
||||
id="toDate"
|
||||
placeholder="To"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<gantt-chart
|
||||
:list="list"
|
||||
:show-taskswithout-dates="showTaskswithoutDates"
|
||||
:date-from="dateFrom"
|
||||
:date-to="dateTo"
|
||||
:day-width="dayWidth"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GanttChart from './gantt-component'
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import ListModel from '../../models/list'
|
||||
|
||||
export default {
|
||||
name: 'Gantt',
|
||||
components: {
|
||||
flatPickr,
|
||||
GanttChart
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showTaskswithoutDates: false,
|
||||
dayWidth: 35,
|
||||
dateFrom: null,
|
||||
dateTo: null,
|
||||
flatPickerConfig:{
|
||||
altFormat: 'j M Y',
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d',
|
||||
enableTime: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.dateFrom = new Date((new Date()).setDate((new Date()).getDate() - 15))
|
||||
this.dateTo = new Date((new Date()).setDate((new Date()).getDate() + 30))
|
||||
},
|
||||
props: {
|
||||
list: {
|
||||
type: ListModel,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
177
src/components/tasks/ShowListTasks.vue
Normal file
177
src/components/tasks/ShowListTasks.vue
Normal file
|
@ -0,0 +1,177 @@
|
|||
<template>
|
||||
<div class="loader-container" :class="{ 'is-loading': listService.loading}">
|
||||
<form @submit.prevent="addTask()">
|
||||
<div class="field is-grouped">
|
||||
<p class="control has-icons-left is-expanded" :class="{ 'is-loading': taskService.loading}">
|
||||
<input v-focus class="input" :class="{ 'disabled': taskService.loading}" v-model="newTaskText" type="text" placeholder="Add a new task...">
|
||||
<span class="icon is-small is-left">
|
||||
<icon icon="tasks"/>
|
||||
</span>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button type="submit" class="button is-success">
|
||||
<span class="icon is-small">
|
||||
<icon icon="plus"/>
|
||||
</span>
|
||||
Add
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="tasks" v-if="this.list.tasks && this.list.tasks.length > 0" :class="{'short': isTaskEdit}">
|
||||
<div class="task" v-for="l in list.tasks" :key="l.id">
|
||||
<label :for="l.id">
|
||||
<div class="fancycheckbox">
|
||||
<input @change="markAsDone" type="checkbox" :id="l.id" :checked="l.done" style="display: none;">
|
||||
<label :for="l.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': l.done}">
|
||||
{{l.text}}
|
||||
<span class="tag" v-for="label in l.labels" :style="{'background': label.hex_color, 'color': label.textColor}" :key="label.id">
|
||||
<span>{{ label.title }}</span>
|
||||
</span>
|
||||
<i v-if="l.dueDate > 0" :class="{'overdue': (l.dueDate <= new Date())}"> - Due on {{new Date(l.dueDate).toLocaleString()}}</i>
|
||||
<span v-if="l.priority >= priorities.HIGH" class="high-priority" :class="{'not-so-high': l.priority === priorities.HIGH}">
|
||||
<span class="icon">
|
||||
<icon icon="exclamation"/>
|
||||
</span>
|
||||
<template v-if="l.priority === priorities.HIGH">High</template>
|
||||
<template v-if="l.priority === priorities.URGENT">Urgent</template>
|
||||
<template v-if="l.priority === priorities.DO_NOW">DO NOW</template>
|
||||
<span class="icon" v-if="l.priority === priorities.DO_NOW">
|
||||
<icon icon="exclamation"/>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div @click="editTask(l.id)" class="icon settings">
|
||||
<icon icon="cog"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4" v-if="isTaskEdit">
|
||||
<div class="card taskedit">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
Edit Task
|
||||
</p>
|
||||
<a class="card-header-icon" @click="isTaskEdit = false">
|
||||
<span class="icon">
|
||||
<icon icon="angle-right"/>
|
||||
</span>
|
||||
</a>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
<edit-task :task="taskEditTask"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import message from '../../message'
|
||||
|
||||
import ListService from '../../services/list'
|
||||
import TaskService from '../../services/task'
|
||||
import ListModel from '../../models/list'
|
||||
import EditTask from './edit-task'
|
||||
import TaskModel from '../../models/task'
|
||||
import priorities from '../../models/priorities'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
listID: this.$route.params.id,
|
||||
listService: ListService,
|
||||
taskService: TaskService,
|
||||
list: {},
|
||||
isTaskEdit: false,
|
||||
taskEditTask: TaskModel,
|
||||
newTaskText: '',
|
||||
priorities: {},
|
||||
}
|
||||
},
|
||||
components: {
|
||||
EditTask,
|
||||
},
|
||||
props: {
|
||||
theList: {
|
||||
type: ListModel,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
theList() {
|
||||
this.list = this.theList
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
// Check if the user is already logged in, if so, redirect him to the homepage
|
||||
if (!auth.user.authenticated) {
|
||||
router.push({name: 'home'})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.listService = new ListService()
|
||||
this.taskService = new TaskService()
|
||||
this.priorities = priorities
|
||||
this.taskEditTask = null
|
||||
this.isTaskEdit = false
|
||||
},
|
||||
methods: {
|
||||
addTask() {
|
||||
let task = new TaskModel({text: this.newTaskText, listID: this.$route.params.id})
|
||||
this.taskService.create(task)
|
||||
.then(r => {
|
||||
this.list.addTaskToList(r)
|
||||
message.success({message: 'The task was successfully created.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
markAsDone(e) {
|
||||
let updateFunc = () => {
|
||||
// We get the task, update the 'done' property and then push it to the api.
|
||||
let task = this.list.getTaskByID(e.target.id)
|
||||
task.done = e.target.checked
|
||||
this.taskService.update(task)
|
||||
.then(() => {
|
||||
this.list.sortTasks()
|
||||
message.success({message: 'The task was successfully ' + (task.done ? '' : 'un-') + 'marked as done.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
}
|
||||
|
||||
if (e.target.checked) {
|
||||
setTimeout(updateFunc(), 300); // Delay it to show the animation when marking a task as done
|
||||
} else {
|
||||
updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
|
||||
}
|
||||
},
|
||||
editTask(id) {
|
||||
// Find the selected task and set it to the current object
|
||||
let theTask = this.list.getTaskByID(id) // Somehow this does not work if we directly assign this to this.taskEditTask
|
||||
this.taskEditTask = theTask
|
||||
this.isTaskEdit = true
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
446
src/components/tasks/edit-task.vue
Normal file
446
src/components/tasks/edit-task.vue
Normal file
|
@ -0,0 +1,446 @@
|
|||
<template>
|
||||
<form @submit.prevent="editTaskSubmit()">
|
||||
<div class="field">
|
||||
<label class="label" for="tasktext">Task Text</label>
|
||||
<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">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="taskdescription">Description</label>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<flat-pickr
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
:disabled="taskService.loading"
|
||||
:v-model="taskEditTask.reminderDates"
|
||||
:config="flatPickerConfig"
|
||||
:id="'taskreminderdate' + index"
|
||||
:value="r"
|
||||
:data-index="index"
|
||||
placeholder="Add a new reminder...">
|
||||
</flat-pickr>
|
||||
<a v-if="index !== (taskEditTask.reminderDates.length - 1)" @click="removeReminderByIndex(index)"><icon icon="times"></icon></a>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="taskduedate">Due Date</label>
|
||||
<div class="control">
|
||||
<flat-pickr
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
class="input"
|
||||
:disabled="taskService.loading"
|
||||
v-model="taskEditTask.dueDate"
|
||||
:config="flatPickerConfig"
|
||||
id="taskduedate"
|
||||
placeholder="The tasks due date is here...">
|
||||
</flat-pickr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="">Duration</label>
|
||||
<div class="control columns">
|
||||
<div class="column">
|
||||
<flat-pickr
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
class="input"
|
||||
:disabled="taskService.loading"
|
||||
v-model="taskEditTask.startDate"
|
||||
:config="flatPickerConfig"
|
||||
id="taskduedate"
|
||||
placeholder="Start date">
|
||||
</flat-pickr>
|
||||
</div>
|
||||
<div class="column">
|
||||
<flat-pickr
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
class="input"
|
||||
:disabled="taskService.loading"
|
||||
v-model="taskEditTask.endDate"
|
||||
:config="flatPickerConfig"
|
||||
id="taskduedate"
|
||||
placeholder="End date">
|
||||
</flat-pickr>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="">Repeat after</label>
|
||||
<div class="control repeat-after-input columns">
|
||||
<div class="column">
|
||||
<input class="input" placeholder="Specify an amount..." v-model="taskEditTask.repeatAfter.amount"/>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="select">
|
||||
<select v-model="taskEditTask.repeatAfter.type">
|
||||
<option value="hours">Hours</option>
|
||||
<option value="days">Days</option>
|
||||
<option value="weeks">Weeks</option>
|
||||
<option value="months">Months</option>
|
||||
<option value="years">Years</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="">Priority</label>
|
||||
<div class="control priority-select">
|
||||
<div class="select">
|
||||
<select v-model="taskEditTask.priority">
|
||||
<option :value="priorities.UNSET">Unset</option>
|
||||
<option :value="priorities.LOW">Low</option>
|
||||
<option :value="priorities.MEDIUM">Medium</option>
|
||||
<option :value="priorities.HIGH">High</option>
|
||||
<option :value="priorities.URGENT">Urgent</option>
|
||||
<option :value="priorities.DO_NOW">DO NOW</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="">Assignees</label>
|
||||
<ul class="assingees">
|
||||
<li v-for="(a, index) in taskEditTask.assignees" :key="a.id">
|
||||
{{a.username}}
|
||||
<a @click="deleteAssigneeByIndex(index)"><icon icon="times"/></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<multiselect
|
||||
v-model="newAssignee"
|
||||
:options="foundUsers"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
:loading="userService.loading"
|
||||
:internal-search="true"
|
||||
@search-change="findUser"
|
||||
placeholder="Type to search"
|
||||
label="username"
|
||||
track-by="id">
|
||||
<template slot="clear" slot-scope="props">
|
||||
<div class="multiselect__clear" v-if="newAssignee !== null && newAssignee.id !== 0" @mousedown.prevent.stop="clearAllFoundUsers(props.search)"></div>
|
||||
</template>
|
||||
<span slot="noResult">Oops! No user found. Consider changing the search query.</span>
|
||||
</multiselect>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a @click="addAssignee" class="button is-primary fullheight">
|
||||
<span class="icon is-small">
|
||||
<icon icon="plus"/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Labels</label>
|
||||
<div class="control">
|
||||
<multiselect
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="true"
|
||||
:options-limit="300"
|
||||
:hide-selected="true"
|
||||
v-model="taskEditTask.labels"
|
||||
:options="foundLabels"
|
||||
:searchable="true"
|
||||
:loading="labelService.loading || labelTaskService.loading"
|
||||
:internal-search="true"
|
||||
@search-change="findLabel"
|
||||
@select="addLabel"
|
||||
placeholder="Type to search"
|
||||
label="title"
|
||||
track-by="id"
|
||||
:taggable="true"
|
||||
@tag="createAndAddLabel"
|
||||
tag-placeholder="Add this as new label"
|
||||
>
|
||||
<template slot="tag" slot-scope="{ option, remove }">
|
||||
<span class="tag" :style="{'background': option.hex_color, 'color': option.textColor}">
|
||||
<span>{{ option.title }}</span>
|
||||
<a class="delete is-small" @click="removeLabel(option)"></a>
|
||||
</span>
|
||||
</template>
|
||||
<template slot="clear" slot-scope="props">
|
||||
<div class="multiselect__clear" v-if="taskEditTask.labels.length" @mousedown.prevent.stop="clearAllLabels(props.search)"></div>
|
||||
</template>
|
||||
</multiselect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="subtasks">Subtasks</label>
|
||||
<div class="tasks noborder" v-if="taskEditTask.subtasks && taskEditTask.subtasks.length > 0">
|
||||
<div class="task" v-for="s in taskEditTask.subtasks" :key="s.id">
|
||||
<label :for="s.id">
|
||||
<div class="fancycheckbox">
|
||||
<input @change="markAsDone" type="checkbox" :id="s.id" :checked="s.done" style="display: none;">
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field has-addons">
|
||||
<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>
|
||||
<div class="control">
|
||||
<a class="button is-primary" @click="addSubtask()"><icon icon="plus"></icon></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button is-success is-fullwidth" :class="{ 'is-loading': taskService.loading}">
|
||||
Save
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import message from '../../message'
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
import multiselect from 'vue-multiselect'
|
||||
import {differenceWith} from 'lodash'
|
||||
|
||||
import ListService from '../../services/list'
|
||||
import TaskService from '../../services/task'
|
||||
import TaskModel from '../../models/task'
|
||||
import UserModel from '../../models/user'
|
||||
import UserService from '../../services/user'
|
||||
import priorities from '../../models/priorities'
|
||||
import LabelTaskService from '../../services/labelTask'
|
||||
import LabelService from '../../services/label'
|
||||
import LabelTaskModel from '../../models/labelTask'
|
||||
import LabelModel from '../../models/label'
|
||||
|
||||
export default {
|
||||
name: 'edit-task',
|
||||
data() {
|
||||
return {
|
||||
listID: this.$route.params.id,
|
||||
listService: ListService,
|
||||
taskService: TaskService,
|
||||
|
||||
priorities: priorities,
|
||||
list: {},
|
||||
newTask: TaskModel,
|
||||
isTaskEdit: false,
|
||||
taskEditTask: TaskModel,
|
||||
lastReminder: 0,
|
||||
nowUnix: new Date(),
|
||||
flatPickerConfig:{
|
||||
altFormat: 'j M Y H:i',
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
enableTime: true,
|
||||
onOpen: this.updateLastReminderDate,
|
||||
onClose: this.addReminderDate,
|
||||
},
|
||||
|
||||
newAssignee: UserModel,
|
||||
userService: UserService,
|
||||
foundUsers: [],
|
||||
|
||||
labelService: LabelService,
|
||||
labelTaskService: LabelTaskService,
|
||||
foundLabels: [],
|
||||
labelTimeout: null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
flatPickr,
|
||||
multiselect,
|
||||
},
|
||||
props: {
|
||||
task: {
|
||||
type: TaskModel,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
task() {
|
||||
this.taskEditTask = this.task
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.listService = new ListService()
|
||||
this.taskService = new TaskService()
|
||||
this.newTask = new TaskModel()
|
||||
this.userService = new UserService()
|
||||
this.newAssignee = new UserModel()
|
||||
this.labelService = new LabelService()
|
||||
this.labelTaskService = new LabelTaskService()
|
||||
this.taskEditTask = this.task
|
||||
},
|
||||
methods: {
|
||||
editTaskSubmit() {
|
||||
this.taskService.update(this.taskEditTask)
|
||||
.then(r => {
|
||||
this.$set(this, 'taskEditTask', r)
|
||||
message.success({message: 'The task was successfully updated.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
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) {
|
||||
this.lastReminder = +new Date(selectedDates[0])
|
||||
},
|
||||
addReminderDate(selectedDates, dateStr, instance) {
|
||||
let newDate = +new Date(selectedDates[0])
|
||||
|
||||
// Don't update if nothing changed
|
||||
if (newDate === this.lastReminder) {
|
||||
return
|
||||
}
|
||||
|
||||
let index = parseInt(instance.input.dataset.index)
|
||||
this.taskEditTask.reminderDates[index] = newDate
|
||||
|
||||
let lastIndex = this.taskEditTask.reminderDates.length - 1
|
||||
// put a new null at the end if we changed something
|
||||
if (lastIndex === index && !isNaN(newDate)) {
|
||||
this.taskEditTask.reminderDates.push(null)
|
||||
}
|
||||
},
|
||||
removeReminderByIndex(index) {
|
||||
this.taskEditTask.reminderDates.splice(index, 1)
|
||||
// Reset the last to 0 to have the "add reminder" button
|
||||
this.taskEditTask.reminderDates[this.taskEditTask.reminderDates.length - 1] = null
|
||||
},
|
||||
addAssignee() {
|
||||
this.taskEditTask.assignees.push(this.newAssignee)
|
||||
},
|
||||
deleteAssigneeByIndex(index) {
|
||||
this.taskEditTask.assignees.splice(index, 1)
|
||||
},
|
||||
findUser(query) {
|
||||
if(query === '') {
|
||||
this.clearAllFoundUsers()
|
||||
return
|
||||
}
|
||||
|
||||
this.userService.getAll({}, {s: query})
|
||||
.then(response => {
|
||||
// Filter the results to not include users who are already assigned
|
||||
this.$set(this, 'foundUsers', differenceWith(response, this.taskEditTask.assignees, (first, second) => {
|
||||
return first.id === second.id
|
||||
}))
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
clearAllFoundUsers () {
|
||||
this.$set(this, 'foundUsers', [])
|
||||
},
|
||||
findLabel(query) {
|
||||
if(query === '') {
|
||||
this.clearAllLabels()
|
||||
return
|
||||
}
|
||||
|
||||
if(this.labelTimeout !== null) {
|
||||
clearTimeout(this.labelTimeout)
|
||||
}
|
||||
|
||||
// Delay the search 300ms to not send a request on every keystroke
|
||||
this.labelTimeout = setTimeout(() => {
|
||||
this.labelService.getAll({}, {s: query})
|
||||
.then(response => {
|
||||
this.$set(this, 'foundLabels', differenceWith(response, this.taskEditTask.labels, (first, second) => {
|
||||
return first.id === second.id
|
||||
}))
|
||||
this.labelTimeout = null
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
}, 300)
|
||||
},
|
||||
clearAllLabels () {
|
||||
this.$set(this, 'foundLabels', [])
|
||||
},
|
||||
addLabel(label) {
|
||||
let labelTask = new LabelTaskModel({taskID: this.taskEditTask.id, label_id: label.id})
|
||||
this.labelTaskService.create(labelTask)
|
||||
.then(() => {
|
||||
message.success({message: 'The label was successfully added.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
removeLabel(label) {
|
||||
let labelTask = new LabelTaskModel({taskID: this.taskEditTask.id, label_id: label.id})
|
||||
this.labelTaskService.delete(labelTask)
|
||||
.then(() => {
|
||||
// Remove the label from the list
|
||||
for (const l in this.taskEditTask.labels) {
|
||||
if (this.taskEditTask.labels[l].id === label.id) {
|
||||
this.taskEditTask.labels.splice(l, 1)
|
||||
}
|
||||
}
|
||||
message.success({message: 'The label was successfully removed.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
createAndAddLabel(title) {
|
||||
let newLabel = new LabelModel({title: title})
|
||||
this.labelService.create(newLabel)
|
||||
.then(r => {
|
||||
this.addLabel(r)
|
||||
this.taskEditTask.labels.push(r)
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
form {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
350
src/components/tasks/gantt-component.vue
Normal file
350
src/components/tasks/gantt-component.vue
Normal file
|
@ -0,0 +1,350 @@
|
|||
<template>
|
||||
<div class="gantt-chart box">
|
||||
<div class="dates">
|
||||
<template v-for="(y, yk) in days">
|
||||
<div class="months" :key="yk + 'year'">
|
||||
<div class="month" v-for="(m, mk) in days[yk]" :key="mk + 'month'">
|
||||
{{new Date((new Date(yk)).setMonth(mk)).toLocaleString('en-us', { month: 'long' })}}, {{(new Date(yk)).getFullYear()}}
|
||||
<div class="days">
|
||||
<div
|
||||
class="day"
|
||||
v-for="(d, dk) in days[yk][mk]"
|
||||
:key="dk + 'day'"
|
||||
:style="{'width': dayWidth + 'px'}"
|
||||
:class="{'today': d.toDateString() === now.toDateString()}">
|
||||
<span class="theday" v-if="dayWidth > 25">
|
||||
{{d.getDate()}}
|
||||
</span>
|
||||
<span class="weekday" v-if="dayWidth > 25">
|
||||
{{d.toLocaleString('en-us', { weekday: 'short' })}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="tasks" :style="{'width': fullWidth + 'px'}">
|
||||
<div class="row" v-for="(t, k) in theTasks" :key="t.id" :style="{background: 'repeating-linear-gradient(90deg, #ededed, #ededed 1px, ' + (k % 2 === 0 ? '#fafafa 1px, #fafafa ' : '#fff 1px, #fff ') + dayWidth + 'px)'}">
|
||||
<VueDragResize
|
||||
class="task"
|
||||
:class="{'done': t.done, 'is-current-edit': taskToEdit !== null && taskToEdit.id === t.id}"
|
||||
:isActive="true"
|
||||
:x="t.offsetDays * dayWidth - 6"
|
||||
:y="0"
|
||||
:w="t.durationDays * dayWidth"
|
||||
:h="31"
|
||||
:minw="dayWidth"
|
||||
:snapToGrid="true"
|
||||
:gridX="dayWidth"
|
||||
:sticks="['mr', 'ml']"
|
||||
axis="x"
|
||||
:parentLimitation="true"
|
||||
:parentW="fullWidth"
|
||||
@resizestop="resizeTask"
|
||||
@dragstop="resizeTask"
|
||||
@clicked="taskDragged = t"
|
||||
>
|
||||
<span :class="{
|
||||
'has-high-priority': t.priority >= priorities.HIGH,
|
||||
'has-not-so-high-priority': t.priority === priorities.HIGH,
|
||||
'has-super-high-priority': t.priority === priorities.DO_NOW
|
||||
}">{{t.text}}</span>
|
||||
<span v-if="t.priority >= priorities.HIGH" class="high-priority" :class="{'not-so-high': t.priority === priorities.HIGH}">
|
||||
<span class="icon">
|
||||
<icon icon="exclamation"/>
|
||||
</span>
|
||||
<template v-if="t.priority === priorities.HIGH">High</template>
|
||||
<template v-if="t.priority === priorities.URGENT">Urgent</template>
|
||||
<template v-if="t.priority === priorities.DO_NOW">DO NOW</template>
|
||||
<span class="icon" v-if="t.priority === priorities.DO_NOW">
|
||||
<icon icon="exclamation"/>
|
||||
</span>
|
||||
</span>
|
||||
<!-- using the key here forces vue to use the updated version model and not the response returned by the api -->
|
||||
<a @click="editTask(theTasks[k])" class="edit-toggle">
|
||||
<icon icon="pen"/>
|
||||
</a>
|
||||
</VueDragResize>
|
||||
</div>
|
||||
<template v-if="showTaskswithoutDates">
|
||||
<div class="row" v-for="t in tasksWithoutDates" :key="t.id">
|
||||
<VueDragResize
|
||||
class="task nodate"
|
||||
:isActive="true"
|
||||
:x="dayOffsetUntilToday * dayWidth - 6"
|
||||
:y="0"
|
||||
:h="31"
|
||||
:minw="dayWidth"
|
||||
:snapToGrid="true"
|
||||
:gridX="dayWidth"
|
||||
:sticks="['mr', 'ml']"
|
||||
axis="x"
|
||||
:parentLimitation="true"
|
||||
:parentW="fullWidth"
|
||||
@resizestop="resizeTask"
|
||||
@dragstop="resizeTask"
|
||||
@clicked="taskDragged = t"
|
||||
v-tooltip="'This task has no dates set.'"
|
||||
>
|
||||
<span>{{t.text}}</span>
|
||||
</VueDragResize>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<form @submit.prevent="addNewTask()" class="add-new-task">
|
||||
<transition name="width">
|
||||
<input
|
||||
type="text"
|
||||
v-model="newTaskTitle"
|
||||
class="input"
|
||||
v-if="newTaskFieldActive"
|
||||
ref="newTaskTitleField"
|
||||
@keyup.esc="newTaskFieldActive = false"
|
||||
@blur="hideCrateNewTask"
|
||||
/>
|
||||
</transition>
|
||||
<button class="button is-primary noshadow" @click="showCreateNewTask">
|
||||
<span class="icon is-small">
|
||||
<icon icon="plus"/>
|
||||
</span>
|
||||
Add a new task
|
||||
</button>
|
||||
</form>
|
||||
<transition name="fade">
|
||||
<div class="card taskedit" v-if="isTaskEdit">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
Edit Task
|
||||
</p>
|
||||
<a class="card-header-icon" @click="isTaskEdit = false;taskToEdit = null">
|
||||
<span class="icon">
|
||||
<icon icon="times"/>
|
||||
</span>
|
||||
</a>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
<edit-task :task="taskToEdit"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueDragResize from 'vue-drag-resize'
|
||||
import message from '../../message'
|
||||
import EditTask from './edit-task'
|
||||
|
||||
import TaskService from '../../services/task'
|
||||
import TaskModel from '../../models/task'
|
||||
import ListModel from '../../models/list'
|
||||
import priorities from '../../models/priorities'
|
||||
|
||||
export default {
|
||||
name: 'GanttChart',
|
||||
components: {
|
||||
EditTask,
|
||||
VueDragResize,
|
||||
},
|
||||
props: {
|
||||
list: {
|
||||
type: ListModel,
|
||||
required: true,
|
||||
},
|
||||
showTaskswithoutDates: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
dateFrom: {
|
||||
default: new Date((new Date()).setDate((new Date()).getDate() - 15))
|
||||
},
|
||||
dateTo: {
|
||||
default: new Date((new Date()).setDate((new Date()).getDate() + 30))
|
||||
},
|
||||
// The width of a day in pixels, used to calculate all sorts of things.
|
||||
dayWidth: {
|
||||
type: Number,
|
||||
default: 35,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
days: [],
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
theTasks: [], // Pretty much a copy of the prop, since we cant mutate the prop directly
|
||||
tasksWithoutDates: [],
|
||||
taskService: TaskService,
|
||||
taskDragged: null, // Saves to currently dragged task to be able to update it
|
||||
fullWidth: 0,
|
||||
now: null,
|
||||
dayOffsetUntilToday: 0,
|
||||
isTaskEdit: false,
|
||||
taskToEdit: null,
|
||||
newTaskTitle: '',
|
||||
newTaskFieldActive: false,
|
||||
priorities: {},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
list() {
|
||||
this.parseTasks()
|
||||
},
|
||||
dateFrom() {
|
||||
this.buildTheGanttChart()
|
||||
},
|
||||
dateTo() {
|
||||
this.buildTheGanttChart()
|
||||
},
|
||||
},
|
||||
beforeMount() {
|
||||
this.now = new Date()
|
||||
this.taskService = new TaskService()
|
||||
this.priorities = priorities
|
||||
this.buildTheGanttChart()
|
||||
},
|
||||
methods: {
|
||||
buildTheGanttChart() {
|
||||
this.setDates()
|
||||
this.prepareGanttDays()
|
||||
this.parseTasks()
|
||||
},
|
||||
setDates() {
|
||||
this.startDate = new Date(this.dateFrom)
|
||||
this.endDate = new Date(this.dateTo)
|
||||
|
||||
this.dayOffsetUntilToday = Math.floor((this.now - this.startDate) / 1000 / 60 / 60 / 24) +1
|
||||
},
|
||||
prepareGanttDays() {
|
||||
// Layout: years => [months => [days]]
|
||||
let years = {};
|
||||
for (let d = this.startDate; d <= this.endDate; d.setDate(d.getDate() + 1)) {
|
||||
let date = new Date(d)
|
||||
if (years[date.getFullYear() + ''] === undefined) {
|
||||
years[date.getFullYear() + ''] = {}
|
||||
}
|
||||
if (years[date.getFullYear() + ''][date.getMonth() + ''] === undefined) {
|
||||
years[date.getFullYear() + ''][date.getMonth() + ''] = []
|
||||
}
|
||||
years[date.getFullYear() + ''][date.getMonth() + ''].push(date)
|
||||
this.fullWidth += this.dayWidth
|
||||
}
|
||||
this.$set(this, 'days', years)
|
||||
},
|
||||
parseTasks() {
|
||||
this.setDates()
|
||||
this.prepareTasks()
|
||||
},
|
||||
prepareTasks() {
|
||||
this.theTasks = this.list.tasks
|
||||
.filter(t => {
|
||||
if(t.startDate === null && !t.done) {
|
||||
this.tasksWithoutDates.push(t)
|
||||
}
|
||||
return t.startDate >= this.startDate && t.endDate <= this.endDate
|
||||
})
|
||||
.map(t => {
|
||||
return this.addGantAttributes(t)
|
||||
})
|
||||
.sort(function(a,b) {
|
||||
if (a.startDate < b.startDate)
|
||||
return -1
|
||||
if (a.startDate > b.startDate)
|
||||
return 1
|
||||
return 0
|
||||
})
|
||||
},
|
||||
addGantAttributes(t) {
|
||||
t.endDate === null ? this.endDate : t.endDate
|
||||
t.durationDays = Math.floor((t.endDate - t.startDate) / 1000 / 60 / 60 / 24) + 1
|
||||
t.offsetDays = Math.floor((t.startDate - this.startDate) / 1000 / 60 / 60 / 24) + 1
|
||||
return t
|
||||
},
|
||||
resizeTask(newRect) {
|
||||
|
||||
// Timeout to definitly catch if the user clicked on taskedit
|
||||
setTimeout(() => {
|
||||
|
||||
if(this.isTaskEdit) {
|
||||
return
|
||||
}
|
||||
|
||||
let didntHaveDates = this.taskDragged.startDate === null ? true : false
|
||||
|
||||
let startDate = new Date(this.startDate)
|
||||
startDate.setDate(startDate.getDate() + newRect.left / this.dayWidth)
|
||||
startDate.setUTCHours(0)
|
||||
startDate.setUTCMinutes(0)
|
||||
startDate.setUTCSeconds(0)
|
||||
startDate.setUTCMilliseconds(0)
|
||||
this.taskDragged.startDate = startDate
|
||||
let endDate = new Date(startDate)
|
||||
endDate.setDate(startDate.getDate() + newRect.width / this.dayWidth)
|
||||
this.taskDragged.startDate = startDate
|
||||
this.taskDragged.endDate = endDate
|
||||
|
||||
this.taskService.update(this.taskDragged)
|
||||
.then(r => {
|
||||
// If the task didn't have dates before, we'll update the list
|
||||
if(didntHaveDates) {
|
||||
for (const t in this.tasksWithoutDates) {
|
||||
if (this.tasksWithoutDates[t].id === r.id) {
|
||||
this.tasksWithoutDates.splice(t, 1)
|
||||
}
|
||||
}
|
||||
this.theTasks.push(this.addGantAttributes(r))
|
||||
} else {
|
||||
for (const tt in this.theTasks) {
|
||||
if (this.theTasks[tt].id === r.id) {
|
||||
this.theTasks[tt] = this.addGantAttributes(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message.success({message: 'The task was successfully updated.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
}, 100)
|
||||
},
|
||||
editTask(task) {
|
||||
this.taskToEdit = task
|
||||
this.isTaskEdit = true
|
||||
},
|
||||
showCreateNewTask() {
|
||||
if(!this.newTaskFieldActive) {
|
||||
// Timeout to not send the form if the field isn't even shown
|
||||
setTimeout(() => {
|
||||
this.newTaskFieldActive = true
|
||||
this.$nextTick(() => this.$refs.newTaskTitleField.focus())
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
hideCrateNewTask() {
|
||||
if(this.newTaskTitle === '') {
|
||||
this.$nextTick(() => this.newTaskFieldActive = false)
|
||||
}
|
||||
},
|
||||
addNewTask() {
|
||||
if (!this.newTaskFieldActive) {
|
||||
return
|
||||
}
|
||||
let task = new TaskModel({text: this.newTaskTitle, listID: this.list.id})
|
||||
this.taskService.create(task)
|
||||
.then(r => {
|
||||
this.tasksWithoutDates.push(this.addGantAttributes(r))
|
||||
this.newTaskTitle = ''
|
||||
this.hideCrateNewTask()
|
||||
message.success({message: 'The task was successfully created.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -8,7 +8,7 @@ import Modal from './components/modal/Modal'
|
|||
Vue.component('modal', Modal)
|
||||
|
||||
// Register the task overview component
|
||||
import TaskOverview from './components/lists/ShowTasks'
|
||||
import TaskOverview from './components/tasks/ShowTasks'
|
||||
Vue.component('TaskOverview', TaskOverview)
|
||||
|
||||
// Add CSS
|
||||
|
@ -43,6 +43,7 @@ import { faCalendarWeek } from '@fortawesome/free-solid-svg-icons'
|
|||
import { faExclamation } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faTags } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faTimesCircle } from '@fortawesome/free-regular-svg-icons'
|
||||
import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
|
@ -70,6 +71,7 @@ library.add(faCalendarAlt)
|
|||
library.add(faExclamation)
|
||||
library.add(faTags)
|
||||
library.add(faChevronDown)
|
||||
library.add(faCheck)
|
||||
|
||||
Vue.component('icon', FontAwesomeIcon)
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import GetPasswordResetComponent from '@/components/user/RequestPasswordReset'
|
|||
import ShowListComponent from '@/components/lists/ShowList'
|
||||
import NewListComponent from '@/components/lists/NewList'
|
||||
import EditListComponent from '@/components/lists/EditList'
|
||||
import ShowTasksInRangeComponent from '@/components/lists/ShowTasksInRange'
|
||||
import ShowTasksInRangeComponent from '@/components/tasks/ShowTasksInRange'
|
||||
// Namespace Handling
|
||||
import NewNamespaceComponent from '@/components/namespaces/NewNamespace'
|
||||
import EditNamespaceComponent from '@/components/namespaces/EditNamespace'
|
||||
|
@ -62,6 +62,11 @@ export default new Router({
|
|||
name: 'editList',
|
||||
component: EditListComponent
|
||||
},
|
||||
{
|
||||
path: '/lists/:id/:type',
|
||||
name: 'showListWithType',
|
||||
component: ShowListComponent,
|
||||
},
|
||||
{
|
||||
path: '/namespaces/:id/list',
|
||||
name: 'newList',
|
||||
|
|
|
@ -16,10 +16,21 @@ export default class TaskService extends AbstractService {
|
|||
}
|
||||
|
||||
beforeUpdate(model) {
|
||||
return this.processModel(model)
|
||||
}
|
||||
|
||||
beforeCreate(model) {
|
||||
return this.processModel(model)
|
||||
}
|
||||
|
||||
processModel(model) {
|
||||
// Ensure the listID is an int
|
||||
model.listID = Number(model.listID)
|
||||
|
||||
// Convert the date in a unix timestamp
|
||||
model.dueDate = +new Date(model.dueDate) / 1000
|
||||
model.startDate = +new Date(model.startDate) / 1000
|
||||
model.endDate = +new Date(model.endDate) / 1000
|
||||
model.dueDate = Math.round(+new Date(model.dueDate) / 1000)
|
||||
model.startDate = Math.round(+new Date(model.startDate) / 1000)
|
||||
model.endDate = Math.round(+new Date(model.endDate) / 1000)
|
||||
|
||||
// remove all nulls, these would create empty reminders
|
||||
for (const index in model.reminderDates) {
|
||||
|
@ -29,9 +40,11 @@ export default class TaskService extends AbstractService {
|
|||
}
|
||||
|
||||
// Make normal timestamps from js dates
|
||||
if(model.reminderDates.length > 0) {
|
||||
model.reminderDates = model.reminderDates.map(r => {
|
||||
return Math.round(+new Date(r) / 1000)
|
||||
})
|
||||
}
|
||||
|
||||
// Make the repeating amount to seconds
|
||||
let repeatAfterSeconds = 0
|
||||
|
|
|
@ -17,6 +17,7 @@ $dropdown-content-shadow: none;
|
|||
$navbar-dropdown-boxed-shadow: $dropdown-content-shadow;
|
||||
$bulmaswatch-import-font: false !default;
|
||||
$light-background: #F1F5F8;
|
||||
$transition-duration: 100ms;
|
||||
|
||||
$vikunja-font: 'Quicksand', sans-serif;
|
||||
$vikunja-light-text: darken(#fff, 10%);
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
padding-right: 5px;
|
||||
padding-top: 3px;
|
||||
|
||||
&.is-block {
|
||||
margin: .5em .2em;
|
||||
}
|
||||
|
||||
.check {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
@ -18,6 +22,11 @@
|
|||
stroke: $primary;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.8em;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
|
215
src/styles/gantt.scss
Normal file
215
src/styles/gantt.scss
Normal file
|
@ -0,0 +1,215 @@
|
|||
$gantt-border: 1px solid $grey-lighter;
|
||||
$gantt-vertical-border-color: lighten($grey, 45);
|
||||
|
||||
.gantt-chart {
|
||||
padding: 5px 0;
|
||||
overflow-x: auto;
|
||||
|
||||
.dates {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
border-bottom: $gantt-border;
|
||||
|
||||
.months {
|
||||
display: flex;
|
||||
|
||||
.month {
|
||||
padding: 0.5em 0 0;
|
||||
border-right: $gantt-border;
|
||||
font-family: $vikunja-font;
|
||||
font-weight: bold;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.days {
|
||||
display: flex;
|
||||
|
||||
.day {
|
||||
padding: 0.5em 0;
|
||||
font-weight: normal;
|
||||
|
||||
&.today {
|
||||
background: $primary;
|
||||
color: $white;
|
||||
border-radius: 5px 5px 0 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.theday {
|
||||
padding: 0 .5em;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.weekday {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tasks {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
|
||||
.row {
|
||||
height: 45px;
|
||||
/*background: repeating-linear-gradient(90deg, $gantt-vertical-border-color, $gantt-vertical-border-color 1px, lighten($grey, 50) 1px, lighten($grey, 50) 35px);
|
||||
|
||||
&:nth-child(even) {
|
||||
background: repeating-linear-gradient(90deg, $gantt-vertical-border-color, $gantt-vertical-border-color 1px, lighten($grey, 55) 1px, lighten($grey, 55) 35px);
|
||||
}*/
|
||||
|
||||
.task {
|
||||
display: inline-block;
|
||||
border: 2px solid $primary;
|
||||
background: lighten($primary, 40);
|
||||
font-size: 0.85em;
|
||||
margin: 0.5em;
|
||||
border-radius: 6px;
|
||||
padding: 0.25em 0.5em;
|
||||
cursor: grab;
|
||||
position: relative;
|
||||
|
||||
-webkit-touch-callout: none; // iOS Safari
|
||||
-webkit-user-select: none; // Safari
|
||||
-khtml-user-select: none; // Konqueror HTML
|
||||
-moz-user-select: none; // Firefox
|
||||
-ms-user-select: none; // Internet Explorer/Edge
|
||||
user-select: none; // Non-prefixed version, currently supported by Chrome and Opera
|
||||
|
||||
&.is-current-edit {
|
||||
border-color: $orange;
|
||||
background: lighten($orange, 40);
|
||||
}
|
||||
|
||||
&.done span {
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
border-top: 1px solid $dark;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 57%;
|
||||
}
|
||||
}
|
||||
|
||||
span:not(.high-priority) {
|
||||
max-width: calc(100% - 20px);
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
&.has-high-priority {
|
||||
max-width: calc(100% - 90px);
|
||||
}
|
||||
|
||||
&.has-not-so-high-priority {
|
||||
max-width: calc(100% - 70px);
|
||||
}
|
||||
|
||||
&.has-super-high-priority {
|
||||
max-width: calc(100% - 111px);
|
||||
}
|
||||
|
||||
&.icon {
|
||||
width: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.high-priority{
|
||||
margin: 0 0 0 .5em;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.edit-toggle {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&.nodate {
|
||||
border: 2px dashed $grey-light;
|
||||
background: lighten($grey-light, 40);
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.taskedit {
|
||||
position: fixed;
|
||||
min-height: 0;
|
||||
top: 10vh;
|
||||
right: 10vw;
|
||||
|
||||
.card-content {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.add-new-task {
|
||||
padding: 1em .7em .4em .7em;
|
||||
display: flex;
|
||||
max-width: 450px;
|
||||
|
||||
.input {
|
||||
margin-right: .7em;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: .68em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gantt-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.range-picker {
|
||||
display: flex;
|
||||
margin-bottom: 1em;
|
||||
|
||||
.field {
|
||||
margin: 0 0 0 .5em;
|
||||
max-width: 100px;
|
||||
|
||||
&, .input {
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
.select {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
select {
|
||||
height: auto;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: .9em;
|
||||
padding-left: .4em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vdr.active::before {
|
||||
display: none;
|
||||
}
|
|
@ -194,13 +194,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 150ms;
|
||||
}
|
||||
.fade-enter, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dropdown-trigger .button {
|
||||
background: none;
|
||||
|
||||
|
|
14
src/styles/scrollbars.scss
Normal file
14
src/styles/scrollbars.scss
Normal file
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
//::-webkit-scrollbar {
|
||||
// width: 8px;
|
||||
//}
|
||||
//
|
||||
//::-webkit-scrollbar-track {
|
||||
// -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
|
||||
// border-radius: 10px;
|
||||
//}
|
||||
//
|
||||
//::-webkit-scrollbar-thumb {
|
||||
// border-radius: 10px;
|
||||
// -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.5);
|
||||
//}
|
|
@ -55,6 +55,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.high-priority{
|
||||
color: $red;
|
||||
|
||||
|
@ -67,11 +72,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
|
|
@ -241,3 +241,43 @@ h1,h2,h3,h4,h5,h6{
|
|||
.navbar.main-theme {
|
||||
background: $light-background;
|
||||
}
|
||||
|
||||
.switch-view {
|
||||
background: $white;
|
||||
display: inline-block;
|
||||
margin: 1em 0;
|
||||
border-radius: $radius;
|
||||
font-size: .8em;
|
||||
box-shadow: 0.3em 0.3em 0.8em darken($light, 6);
|
||||
|
||||
a {
|
||||
padding: .5em;
|
||||
display: inline-block;
|
||||
margin: .4em;
|
||||
border-radius: $radius;
|
||||
|
||||
-webkit-transition: all 100ms;
|
||||
-moz-transition: all 100ms;
|
||||
-ms-transition: all 100ms;
|
||||
-o-transition: all 100ms;
|
||||
transition: all 100ms;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.is-active, &:hover {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: $primary;
|
||||
font-weight: bold;
|
||||
box-shadow: 0.3em 0.3em 0.8em darken($light, 6);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: lighten($primary, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
13
src/styles/transitions.scss
Normal file
13
src/styles/transitions.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity $transition-duration;
|
||||
}
|
||||
.fade-enter, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.width-enter-active, .width-leave-active {
|
||||
transition: width $transition-duration;
|
||||
}
|
||||
.width-enter, .width-leave-to {
|
||||
width: 0;
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
@import 'styles/theme';
|
||||
@import 'styles/scrollbars';
|
||||
@import 'styles/general';
|
||||
@import 'styles/transitions';
|
||||
|
||||
@import 'styles/tasks';
|
||||
@import 'styles/teams';
|
||||
|
@ -8,5 +10,6 @@
|
|||
|
||||
@import 'styles/fancycheckbox';
|
||||
@import 'styles/tooltip';
|
||||
@import 'styles/gantt';
|
||||
|
||||
@import 'styles/multiselect';
|
17
todo.md
17
todo.md
|
@ -36,6 +36,7 @@
|
|||
* [x] Btns für Teams und neuer Namespace nach oben in die Leiste verschieben
|
||||
* [ ] Card-like overview of all lists with the first 3-5 tasks, undone first
|
||||
* [ ] Be able to collapse all lists in a namespace by clicking on the menu entry
|
||||
* [ ] Fancy Scrollbars
|
||||
|
||||
## Funktionales
|
||||
|
||||
|
@ -49,6 +50,22 @@
|
|||
* [x] Overdue rot anzeigen
|
||||
* [x] Beim Team bearbeiten Nutzer suchen einbauen
|
||||
* [ ] Keyboard shortcuts
|
||||
* [ ] Gantt chart
|
||||
* [x] Basics
|
||||
* [x] Add tasks without dates set
|
||||
* [x] Edit tasks with a kind of popup when clicking on them - needs refactoring edit task into an own component
|
||||
* [x] Add a new task with a button or so
|
||||
* [x] Be able to choose the range for the chart
|
||||
* [x] Show task priority
|
||||
* [x] Show task done or not done
|
||||
* [ ] Colors - needs api change before
|
||||
* [x] More view modes
|
||||
* [x] Month: "The big picture"
|
||||
* [x] Day: 3-hour slices of a day
|
||||
* [ ] Table view (list view, bit with more details)
|
||||
* [ ] Calender view
|
||||
* [ ] Kanaban
|
||||
* [ ] Group list view by almost all fields
|
||||
|
||||
## Funktionen aus der API
|
||||
|
||||
|
|
|
@ -8973,6 +8973,11 @@ vm-browserify@0.0.4:
|
|||
dependencies:
|
||||
indexof "0.0.1"
|
||||
|
||||
vue-drag-resize@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/vue-drag-resize/-/vue-drag-resize-1.3.2.tgz#99132a99746c878e4596fad08d36c98f604930a3"
|
||||
integrity sha512-XiSEep3PPh9IPQqa4vIy/YENBpYch2SIPNipcPAEGhaSa0V8A8gSq9s7JQ66p/hiINdnR7f5ZqAkOdm6zU/4Gw==
|
||||
|
||||
vue-eslint-parser@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz#c268c96c6d94cfe3d938a5f7593959b0ca3360d1"
|
||||
|
|
Loading…
Reference in a new issue