vikunja-frontend/src/components/tasks/gantt-component.vue

383 lines
11 KiB
Vue
Raw Normal View History

2019-04-29 23:41:39 +02:00
<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,
'has-light-text': !colorIsDark(t.hexColor),
'has-dark-text': colorIsDark(t.hexColor)
}"
2019-11-03 13:44:40 +01:00
:style="{'border-color': t.hexColor, 'background-color': t.hexColor}"
:isActive="true"
2019-04-29 23:41:39 +02:00
: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="setTaskDragged(t)"
2019-04-29 23:41:39 +02:00
>
2019-11-03 13:44:40 +01:00
<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.title}}</span>
2019-11-24 14:16:24 +01:00
<priority-label :priority="t.priority"/>
2019-04-29 23:41:39 +02:00
<!-- using the key here forces vue to use the updated version model and not the response returned by the api -->
2019-11-03 13:44:40 +01:00
<a @click="editTask(theTasks[k])" class="edit-toggle">
<icon icon="pen"/>
</a>
</VueDragResize>
2019-04-29 23:41:39 +02:00
</div>
<template v-if="showTaskswithoutDates">
<div class="row" v-for="(t, k) in tasksWithoutDates" :key="t.id" :style="{background: 'repeating-linear-gradient(90deg, #ededed, #ededed 1px, ' + (k % 2 === 0 ? '#fafafa 1px, #fafafa ' : '#fff 1px, #fff ') + dayWidth + 'px)'}">
2019-04-29 23:41:39 +02:00
<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="setTaskDragged(t)"
2019-04-29 23:41:39 +02:00
v-tooltip="'This task has no dates set.'"
>
<span>{{t.title}}</span>
2019-04-29 23:41:39 +02:00
</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>
Kanban (#118) Add error message when trying to create an invalid new task in a bucket Prevent creation of new buckets if the bucket title is empty Disable deleting a bucket if it's the last one Disable dragging tasks when they are being updated Fix transition when opening tasks Send the user to list view by default Show loading spinner when updating multiple tasks Add loading spinner when moving tasks Add loading animation when bucket is loading / updating etc Add bucket title edit Fix creating new buckets Add loading animation Add removing buckets Fix creating a new bucket after tasks were moved Fix warning about labels on tasks Fix labels on tasks not updating after retrieval from api Fix property width Add closing and mobile design Make the task detail popup look good Move list views Move task detail view in a popup Add link to tasks Add saving the new task position after it was moved Fix creating new bucket Fix creating a new task Cleanup Disable user selection for task cards Fix drag placeholder Add dragging style to task Add placeholder + change animation duration More cleanup Cleanup / docs Working of dragging and dropping tasks Adjust markup and styling for new library Change kanban library to something that works Add basic calculation of new positions Don't try to create empty tasks Add indicator if a task is done Add moving tasks between buckets Make empty buckets a little smaller Add gimmick for button description Fix color Fix scrolling bucket layout Add creating a new bucket Add hiding the task input field Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/118
2020-04-26 01:11:34 +02:00
<a class="card-header-icon" @click="() => {isTaskEdit = false; taskToEdit = null}">
2019-04-29 23:41:39 +02:00
<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 EditTask from './edit-task'
import TaskService from '../../services/task'
import TaskModel from '../../models/task'
import priorities from '../../models/priorities'
import PriorityLabel from './reusable/priorityLabel'
import TaskCollectionService from '../../services/taskCollection'
2019-04-29 23:41:39 +02:00
export default {
name: 'GanttChart',
components: {
2019-11-24 14:16:24 +01:00
PriorityLabel,
2019-04-29 23:41:39 +02:00
EditTask,
VueDragResize,
},
props: {
Kanban (#118) Add error message when trying to create an invalid new task in a bucket Prevent creation of new buckets if the bucket title is empty Disable deleting a bucket if it's the last one Disable dragging tasks when they are being updated Fix transition when opening tasks Send the user to list view by default Show loading spinner when updating multiple tasks Add loading spinner when moving tasks Add loading animation when bucket is loading / updating etc Add bucket title edit Fix creating new buckets Add loading animation Add removing buckets Fix creating a new bucket after tasks were moved Fix warning about labels on tasks Fix labels on tasks not updating after retrieval from api Fix property width Add closing and mobile design Make the task detail popup look good Move list views Move task detail view in a popup Add link to tasks Add saving the new task position after it was moved Fix creating new bucket Fix creating a new task Cleanup Disable user selection for task cards Fix drag placeholder Add dragging style to task Add placeholder + change animation duration More cleanup Cleanup / docs Working of dragging and dropping tasks Adjust markup and styling for new library Change kanban library to something that works Add basic calculation of new positions Don't try to create empty tasks Add indicator if a task is done Add moving tasks between buckets Make empty buckets a little smaller Add gimmick for button description Fix color Fix scrolling bucket layout Add creating a new bucket Add hiding the task input field Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/118
2020-04-26 01:11:34 +02:00
listId: {
type: Number,
2019-04-29 23:41:39 +02:00
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,
2019-11-03 13:44:40 +01:00
isTaskEdit: false,
2019-04-29 23:41:39 +02:00
taskToEdit: null,
newTaskTitle: '',
newTaskFieldActive: false,
priorities: {},
taskCollectionService: TaskCollectionService,
2019-04-29 23:41:39 +02:00
}
},
watch: {
'dateFrom': 'buildTheGanttChart',
'dateTo': 'buildTheGanttChart',
'listId': 'parseTasks',
2019-04-29 23:41:39 +02:00
},
created() {
2019-04-29 23:41:39 +02:00
this.now = new Date()
this.taskCollectionService = new TaskCollectionService()
2019-04-29 23:41:39 +02:00
this.taskService = new TaskService()
this.priorities = priorities
},
mounted() {
2019-04-29 23:41:39 +02:00
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() {
const getAllTasks = (page = 1) => {
Kanban (#118) Add error message when trying to create an invalid new task in a bucket Prevent creation of new buckets if the bucket title is empty Disable deleting a bucket if it's the last one Disable dragging tasks when they are being updated Fix transition when opening tasks Send the user to list view by default Show loading spinner when updating multiple tasks Add loading spinner when moving tasks Add loading animation when bucket is loading / updating etc Add bucket title edit Fix creating new buckets Add loading animation Add removing buckets Fix creating a new bucket after tasks were moved Fix warning about labels on tasks Fix labels on tasks not updating after retrieval from api Fix property width Add closing and mobile design Make the task detail popup look good Move list views Move task detail view in a popup Add link to tasks Add saving the new task position after it was moved Fix creating new bucket Fix creating a new task Cleanup Disable user selection for task cards Fix drag placeholder Add dragging style to task Add placeholder + change animation duration More cleanup Cleanup / docs Working of dragging and dropping tasks Adjust markup and styling for new library Change kanban library to something that works Add basic calculation of new positions Don't try to create empty tasks Add indicator if a task is done Add moving tasks between buckets Make empty buckets a little smaller Add gimmick for button description Fix color Fix scrolling bucket layout Add creating a new bucket Add hiding the task input field Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/118
2020-04-26 01:11:34 +02:00
return this.taskCollectionService.getAll({listId: this.listId}, {}, page)
.then(tasks => {
if(page < this.taskCollectionService.totalPages) {
return getAllTasks(page + 1)
.then(nextTasks => {
return tasks.concat(nextTasks)
})
} else {
return tasks
}
})
.catch(e => {
return Promise.reject(e)
})
}
getAllTasks()
.then(tasks => {
this.theTasks = 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
})
2019-04-29 23:41:39 +02:00
})
.catch(e => {
this.error(e, this)
2019-04-29 23:41:39 +02:00
})
},
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
},
setTaskDragged(t) {
this.taskDragged = t
},
2019-04-29 23:41:39 +02:00
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
// We take the task from the overall tasks array because the one in it has bad data after it was updated once.
// FIXME: This is a workaround. We should use a better mechanism to get the task or, even better,
// prevent it from containing outdated Data in the first place.
for (const tt in this.theTasks) {
if (this.theTasks[tt].id === this.taskDragged.id) {
this.$set(this, 'taskDragged', this.theTasks[tt])
break
}
}
2019-04-29 23:41:39 +02:00
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)
break
2019-04-29 23:41:39 +02:00
}
}
this.theTasks.push(this.addGantAttributes(r))
} else {
for (const tt in this.theTasks) {
if (this.theTasks[tt].id === r.id) {
this.$set(this.theTasks, tt, this.addGantAttributes(r))
break
2019-04-29 23:41:39 +02:00
}
}
}
})
.catch(e => {
this.error(e, this)
2019-04-29 23:41:39 +02:00
})
}, 100)
},
2019-11-03 13:44:40 +01:00
editTask(task) {
2019-04-29 23:41:39 +02:00
this.taskToEdit = task
this.isTaskEdit = true
2019-11-03 13:44:40 +01:00
},
2019-04-29 23:41:39 +02:00
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({title: this.newTaskTitle, listId: this.listId})
2019-04-29 23:41:39 +02:00
this.taskService.create(task)
.then(r => {
this.tasksWithoutDates.push(this.addGantAttributes(r))
this.newTaskTitle = ''
this.hideCrateNewTask()
})
.catch(e => {
this.error(e, this)
2019-04-29 23:41:39 +02:00
})
},
},
}
</script>