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",
|
"lodash": "^4.17.11",
|
||||||
"v-tooltip": "^2.0.0-rc.33",
|
"v-tooltip": "^2.0.0-rc.33",
|
||||||
"verte": "^0.0.10",
|
"verte": "^0.0.10",
|
||||||
"vue": "^2.5.17"
|
"vue": "^2.5.17",
|
||||||
|
"vue-drag-resize": "^1.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.4",
|
"@fortawesome/fontawesome-svg-core": "^1.2.4",
|
||||||
|
|
|
@ -51,9 +51,7 @@ export default {
|
||||||
email: creds.email,
|
email: creds.email,
|
||||||
password: creds.password
|
password: creds.password
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(() => {
|
||||||
// eslint-disable-next-line
|
|
||||||
console.log(response)
|
|
||||||
this.login(context, creds, redirect)
|
this.login(context, creds, redirect)
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
|
|
|
@ -5,301 +5,14 @@
|
||||||
<icon icon="cog" size="2x"/>
|
<icon icon="cog" size="2x"/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<h1>{{ list.title }}</h1>
|
<h1>{{ list.title }}</h1>
|
||||||
</div>
|
<div class="switch-view">
|
||||||
<form @submit.prevent="addTask()">
|
<router-link :to="{ name: 'showList', params: { id: list.id } }" :class="{'is-active': $route.params.type !== 'gantt'}">List</router-link>
|
||||||
<div class="field is-grouped">
|
<router-link :to="{ name: 'showListWithType', params: { id: list.id, type: 'gantt' } }" :class="{'is-active': $route.params.type === 'gantt'}">Gantt</router-link>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<b>Reminder Dates</b>
|
<gantt :list="list" v-if="$route.params.type === 'gantt'"/>
|
||||||
<div class="reminder-input" :class="{ 'overdue': (r < nowUnix && index !== (taskEditTask.reminderDates.length - 1))}" v-for="(r, index) in taskEditTask.reminderDates" :key="index">
|
<show-list-task :the-list="list" v-else/>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -307,76 +20,39 @@
|
||||||
import auth from '../../auth'
|
import auth from '../../auth'
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import message from '../../message'
|
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 ShowListTask from '../tasks/ShowListTasks'
|
||||||
import TaskService from '../../services/task'
|
import Gantt from '../tasks/Gantt'
|
||||||
import TaskModel from '../../models/task'
|
|
||||||
import ListModel from '../../models/list'
|
import ListModel from '../../models/list'
|
||||||
import UserModel from '../../models/user'
|
import ListService from '../../services/list'
|
||||||
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 {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
listID: this.$route.params.id,
|
listID: this.$route.params.id,
|
||||||
listService: ListService,
|
listService: ListService,
|
||||||
taskService: TaskService,
|
list: ListModel,
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
flatPickr,
|
Gantt,
|
||||||
multiselect,
|
ShowListTask,
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
// Check if the user is already logged in, if so, redirect him to the homepage
|
// Check if the user is already logged in, if so, redirect him to the homepage
|
||||||
if (!auth.user.authenticated) {
|
if (!auth.user.authenticated) {
|
||||||
router.push({name: 'home'})
|
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() {
|
created() {
|
||||||
this.listService = new ListService()
|
this.listService = new ListService()
|
||||||
this.taskService = new TaskService()
|
this.list = new ListModel()
|
||||||
this.newTask = new TaskModel()
|
|
||||||
this.userService = new UserService()
|
|
||||||
this.newAssignee = new UserModel()
|
|
||||||
this.labelService = new LabelService()
|
|
||||||
this.labelTaskService = new LabelTaskService()
|
|
||||||
this.loadList()
|
this.loadList()
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -385,7 +61,6 @@
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadList() {
|
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.
|
// 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})
|
let list = new ListModel({id: this.$route.params.id})
|
||||||
this.listService.get(list)
|
this.listService.get(list)
|
||||||
|
@ -396,195 +71,6 @@
|
||||||
message.error(e, this)
|
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>
|
</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)
|
Vue.component('modal', Modal)
|
||||||
|
|
||||||
// Register the task overview component
|
// Register the task overview component
|
||||||
import TaskOverview from './components/lists/ShowTasks'
|
import TaskOverview from './components/tasks/ShowTasks'
|
||||||
Vue.component('TaskOverview', TaskOverview)
|
Vue.component('TaskOverview', TaskOverview)
|
||||||
|
|
||||||
// Add CSS
|
// Add CSS
|
||||||
|
@ -43,6 +43,7 @@ import { faCalendarWeek } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faExclamation } from '@fortawesome/free-solid-svg-icons'
|
import { faExclamation } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faTags } from '@fortawesome/free-solid-svg-icons'
|
import { faTags } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faChevronDown } 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 { faTimesCircle } from '@fortawesome/free-regular-svg-icons'
|
||||||
import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons'
|
import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
@ -70,6 +71,7 @@ library.add(faCalendarAlt)
|
||||||
library.add(faExclamation)
|
library.add(faExclamation)
|
||||||
library.add(faTags)
|
library.add(faTags)
|
||||||
library.add(faChevronDown)
|
library.add(faChevronDown)
|
||||||
|
library.add(faCheck)
|
||||||
|
|
||||||
Vue.component('icon', FontAwesomeIcon)
|
Vue.component('icon', FontAwesomeIcon)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import GetPasswordResetComponent from '@/components/user/RequestPasswordReset'
|
||||||
import ShowListComponent from '@/components/lists/ShowList'
|
import ShowListComponent from '@/components/lists/ShowList'
|
||||||
import NewListComponent from '@/components/lists/NewList'
|
import NewListComponent from '@/components/lists/NewList'
|
||||||
import EditListComponent from '@/components/lists/EditList'
|
import EditListComponent from '@/components/lists/EditList'
|
||||||
import ShowTasksInRangeComponent from '@/components/lists/ShowTasksInRange'
|
import ShowTasksInRangeComponent from '@/components/tasks/ShowTasksInRange'
|
||||||
// Namespace Handling
|
// Namespace Handling
|
||||||
import NewNamespaceComponent from '@/components/namespaces/NewNamespace'
|
import NewNamespaceComponent from '@/components/namespaces/NewNamespace'
|
||||||
import EditNamespaceComponent from '@/components/namespaces/EditNamespace'
|
import EditNamespaceComponent from '@/components/namespaces/EditNamespace'
|
||||||
|
@ -62,6 +62,11 @@ export default new Router({
|
||||||
name: 'editList',
|
name: 'editList',
|
||||||
component: EditListComponent
|
component: EditListComponent
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/lists/:id/:type',
|
||||||
|
name: 'showListWithType',
|
||||||
|
component: ShowListComponent,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/namespaces/:id/list',
|
path: '/namespaces/:id/list',
|
||||||
name: 'newList',
|
name: 'newList',
|
||||||
|
|
|
@ -16,10 +16,21 @@ export default class TaskService extends AbstractService {
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeUpdate(model) {
|
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
|
// Convert the date in a unix timestamp
|
||||||
model.dueDate = +new Date(model.dueDate) / 1000
|
model.dueDate = Math.round(+new Date(model.dueDate) / 1000)
|
||||||
model.startDate = +new Date(model.startDate) / 1000
|
model.startDate = Math.round(+new Date(model.startDate) / 1000)
|
||||||
model.endDate = +new Date(model.endDate) / 1000
|
model.endDate = Math.round(+new Date(model.endDate) / 1000)
|
||||||
|
|
||||||
// remove all nulls, these would create empty reminders
|
// remove all nulls, these would create empty reminders
|
||||||
for (const index in model.reminderDates) {
|
for (const index in model.reminderDates) {
|
||||||
|
@ -29,9 +40,11 @@ export default class TaskService extends AbstractService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make normal timestamps from js dates
|
// Make normal timestamps from js dates
|
||||||
|
if(model.reminderDates.length > 0) {
|
||||||
model.reminderDates = model.reminderDates.map(r => {
|
model.reminderDates = model.reminderDates.map(r => {
|
||||||
return Math.round(+new Date(r) / 1000)
|
return Math.round(+new Date(r) / 1000)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Make the repeating amount to seconds
|
// Make the repeating amount to seconds
|
||||||
let repeatAfterSeconds = 0
|
let repeatAfterSeconds = 0
|
||||||
|
|
|
@ -17,6 +17,7 @@ $dropdown-content-shadow: none;
|
||||||
$navbar-dropdown-boxed-shadow: $dropdown-content-shadow;
|
$navbar-dropdown-boxed-shadow: $dropdown-content-shadow;
|
||||||
$bulmaswatch-import-font: false !default;
|
$bulmaswatch-import-font: false !default;
|
||||||
$light-background: #F1F5F8;
|
$light-background: #F1F5F8;
|
||||||
|
$transition-duration: 100ms;
|
||||||
|
|
||||||
$vikunja-font: 'Quicksand', sans-serif;
|
$vikunja-font: 'Quicksand', sans-serif;
|
||||||
$vikunja-light-text: darken(#fff, 10%);
|
$vikunja-light-text: darken(#fff, 10%);
|
||||||
|
|
|
@ -5,6 +5,10 @@
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
padding-top: 3px;
|
padding-top: 3px;
|
||||||
|
|
||||||
|
&.is-block {
|
||||||
|
margin: .5em .2em;
|
||||||
|
}
|
||||||
|
|
||||||
.check {
|
.check {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -18,6 +22,11 @@
|
||||||
stroke: $primary;
|
stroke: $primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 0.8em;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
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 {
|
.dropdown-trigger .button {
|
||||||
background: none;
|
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{
|
.high-priority{
|
||||||
color: $red;
|
color: $red;
|
||||||
|
|
||||||
|
@ -67,11 +72,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
|
||||||
margin: 0 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
|
@ -241,3 +241,43 @@ h1,h2,h3,h4,h5,h6{
|
||||||
.navbar.main-theme {
|
.navbar.main-theme {
|
||||||
background: $light-background;
|
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/theme';
|
||||||
|
@import 'styles/scrollbars';
|
||||||
@import 'styles/general';
|
@import 'styles/general';
|
||||||
|
@import 'styles/transitions';
|
||||||
|
|
||||||
@import 'styles/tasks';
|
@import 'styles/tasks';
|
||||||
@import 'styles/teams';
|
@import 'styles/teams';
|
||||||
|
@ -8,5 +10,6 @@
|
||||||
|
|
||||||
@import 'styles/fancycheckbox';
|
@import 'styles/fancycheckbox';
|
||||||
@import 'styles/tooltip';
|
@import 'styles/tooltip';
|
||||||
|
@import 'styles/gantt';
|
||||||
|
|
||||||
@import 'styles/multiselect';
|
@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
|
* [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
|
* [ ] 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
|
* [ ] Be able to collapse all lists in a namespace by clicking on the menu entry
|
||||||
|
* [ ] Fancy Scrollbars
|
||||||
|
|
||||||
## Funktionales
|
## Funktionales
|
||||||
|
|
||||||
|
@ -49,6 +50,22 @@
|
||||||
* [x] Overdue rot anzeigen
|
* [x] Overdue rot anzeigen
|
||||||
* [x] Beim Team bearbeiten Nutzer suchen einbauen
|
* [x] Beim Team bearbeiten Nutzer suchen einbauen
|
||||||
* [ ] Keyboard shortcuts
|
* [ ] 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
|
## Funktionen aus der API
|
||||||
|
|
||||||
|
|
|
@ -8973,6 +8973,11 @@ vm-browserify@0.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
indexof "0.0.1"
|
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:
|
vue-eslint-parser@^2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz#c268c96c6d94cfe3d938a5f7593959b0ca3360d1"
|
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz#c268c96c6d94cfe3d938a5f7593959b0ca3360d1"
|
||||||
|
|
Loading…
Reference in a new issue