From d03f0211a372b4997be38c3d891d31a26a538388 Mon Sep 17 00:00:00 2001 From: konrad Date: Mon, 29 Apr 2019 21:41:39 +0000 Subject: [PATCH] Gantt Charts (#29) --- package.json | 3 +- src/auth/index.js | 4 +- src/components/lists/ShowList.vue | 552 +----------------- src/components/tasks/Gantt.vue | 101 ++++ src/components/tasks/ShowListTasks.vue | 177 ++++++ src/components/{lists => tasks}/ShowTasks.vue | 0 .../{lists => tasks}/ShowTasksInRange.vue | 0 src/components/tasks/edit-task.vue | 446 ++++++++++++++ src/components/tasks/gantt-component.vue | 350 +++++++++++ src/main.js | 4 +- src/router/index.js | 7 +- src/services/task.js | 25 +- src/styles/_variables.scss | 1 + src/styles/fancycheckbox.scss | 9 + src/styles/gantt.scss | 215 +++++++ src/styles/general.scss | 7 - src/styles/scrollbars.scss | 14 + src/styles/tasks.scss | 24 +- src/styles/theme.scss | 42 +- src/styles/transitions.scss | 13 + src/vikunja.scss | 3 + todo.md | 17 + yarn.lock | 5 + 23 files changed, 1454 insertions(+), 565 deletions(-) create mode 100644 src/components/tasks/Gantt.vue create mode 100644 src/components/tasks/ShowListTasks.vue rename src/components/{lists => tasks}/ShowTasks.vue (100%) rename src/components/{lists => tasks}/ShowTasksInRange.vue (100%) create mode 100644 src/components/tasks/edit-task.vue create mode 100644 src/components/tasks/gantt-component.vue create mode 100644 src/styles/gantt.scss create mode 100644 src/styles/scrollbars.scss create mode 100644 src/styles/transitions.scss diff --git a/package.json b/package.json index c6fd04e99..293c20c34 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "lodash": "^4.17.11", "v-tooltip": "^2.0.0-rc.33", "verte": "^0.0.10", - "vue": "^2.5.17" + "vue": "^2.5.17", + "vue-drag-resize": "^1.3.2" }, "devDependencies": { "@fortawesome/fontawesome-svg-core": "^1.2.4", diff --git a/src/auth/index.js b/src/auth/index.js index 38a11a554..dbc4bce31 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -51,9 +51,7 @@ export default { email: creds.email, password: creds.password }) - .then(response => { - // eslint-disable-next-line - console.log(response) + .then(() => { this.login(context, creds, redirect) }) .catch(e => { diff --git a/src/components/lists/ShowList.vue b/src/components/lists/ShowList.vue index e366104e7..14ec3fa41 100644 --- a/src/components/lists/ShowList.vue +++ b/src/components/lists/ShowList.vue @@ -5,301 +5,14 @@

{{ list.title }}

- -
-
-

- - - - -

-

- -

-
-
- -
-
-
-
- -
- -
-
-
-
-
-
-
-

- Edit Task -

- - - - - -
-
-
-
-
- -
- -
-
-
- -
- -
-
- - Reminder Dates -
- - - -
- -
- -
- - -
-
- -
- -
-
- - -
-
- - -
-
-
- -
- -
-
- -
-
-
- -
-
-
-
- -
- -
-
- -
-
-
- -
- -
    -
  • - {{a.username}} - -
  • -
-
- -
-
- - - Oops! No user found. Consider changing the search query. - -
- -
- -
- -
- - - - -
-
- -
- -
-
- -
-
-
-
-
- -
-
- -
-
- - - -
-
-
-
+
+ List + Gantt
+ + +
@@ -307,76 +20,39 @@ import auth from '../../auth' import router from '../../router' import message from '../../message' - import flatPickr from 'vue-flatpickr-component' - import 'flatpickr/dist/flatpickr.css' - import multiselect from 'vue-multiselect' - import {differenceWith} from 'lodash' - import ListService from '../../services/list' - import TaskService from '../../services/task' - import TaskModel from '../../models/task' + import ShowListTask from '../tasks/ShowListTasks' + import Gantt from '../tasks/Gantt' + import ListModel from '../../models/list' - import UserModel from '../../models/user' - import UserService from '../../services/user' - import priorities from '../../models/priorities' - import LabelTaskService from '../../services/labelTask' - import LabelService from '../../services/label' - import LabelTaskModel from '../../models/labelTask' - import LabelModel from '../../models/label' + import ListService from '../../services/list' export default { data() { return { listID: this.$route.params.id, listService: ListService, - taskService: TaskService, - - priorities: priorities, - list: {}, - newTask: TaskModel, - isTaskEdit: false, - taskEditTask: { - subtasks: [], - }, - lastReminder: 0, - nowUnix: new Date(), - flatPickerConfig:{ - altFormat: 'j M Y H:i', - altInput: true, - dateFormat: 'Y-m-d H:i', - enableTime: true, - onOpen: this.updateLastReminderDate, - onClose: this.addReminderDate, - }, - - newAssignee: UserModel, - userService: UserService, - foundUsers: [], - - labelService: LabelService, - labelTaskService: LabelTaskService, - foundLabels: [], - labelTimeout: null, + list: ListModel, } }, components: { - flatPickr, - multiselect, + Gantt, + ShowListTask, }, beforeMount() { // Check if the user is already logged in, if so, redirect him to the homepage if (!auth.user.authenticated) { router.push({name: 'home'}) } + + // If the type is invalid, redirect the user + if (this.$route.params.type !== 'gantt' && this.$route.params.type !== '') { + router.push({name: 'showList', params: { id: this.$route.params.id }}) + } }, created() { this.listService = new ListService() - this.taskService = new TaskService() - this.newTask = new TaskModel() - this.userService = new UserService() - this.newAssignee = new UserModel() - this.labelService = new LabelService() - this.labelTaskService = new LabelTaskService() + this.list = new ListModel() this.loadList() }, watch: { @@ -385,7 +61,6 @@ }, methods: { loadList() { - this.isTaskEdit = false // We create an extra list object instead of creating it in this.list because that would trigger a ui update which would result in bad ux. let list = new ListModel({id: this.$route.params.id}) this.listService.get(list) @@ -396,195 +71,6 @@ message.error(e, this) }) }, - addTask() { - this.newTask.listID = this.$route.params.id - this.taskService.create(this.newTask) - .then(r => { - this.list.addTaskToList(r) - message.success({message: 'The task was successfully created.'}, this) - }) - .catch(e => { - message.error(e, this) - }) - - this.newTask = {} - }, - markAsDone(e) { - let updateFunc = () => { - // We get the task, update the 'done' property and then push it to the api. - let task = this.list.getTaskByID(e.target.id) - task.done = e.target.checked - this.taskService.update(task) - .then(r => { - this.updateTaskInList(r) - message.success({message: 'The task was successfully ' + (task.done ? '' : 'un-') + 'marked as done.'}, this) - }) - .catch(e => { - message.error(e, this) - }) - } - - if (e.target.checked) { - setTimeout(updateFunc(), 300); // Delay it to show the animation when marking a task as done - } else { - updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around - } - }, - editTask(id) { - // Find the selected task and set it to the current object - let theTask = this.list.getTaskByID(id) // Somehow this does not work if we directly assign this to this.taskEditTask - this.taskEditTask = theTask - this.isTaskEdit = true - }, - editTaskSubmit() { - this.taskService.update(this.taskEditTask) - .then(r => { - this.updateTaskInList(r) - this.$set(this, 'taskEditTask', r) - message.success({message: 'The task was successfully updated.'}, this) - }) - .catch(e => { - message.error(e, this) - }) - }, - addSubtask() { - this.newTask.parentTaskID = this.taskEditTask.id - this.addTask() - }, - updateTaskInList(task) { - // We need to do the update here in the component, because there is no way of notifiying vue of the change otherwise. - for (const t in this.list.tasks) { - if (this.list.tasks[t].id === task.id) { - this.$set(this.list.tasks, t, task) - break - } - - if (this.list.tasks[t].id === task.parentTaskID) { - for (const s in this.list.tasks[t].subtasks) { - if (this.list.tasks[t].subtasks[s].id === task.id) { - this.$set(this.list.tasks[t].subtasks, s, task) - break - } - } - } - } - this.list.sortTasks() - }, - updateLastReminderDate(selectedDates) { - this.lastReminder = +new Date(selectedDates[0]) - }, - addReminderDate(selectedDates, dateStr, instance) { - let newDate = +new Date(selectedDates[0]) - - // Don't update if nothing changed - if (newDate === this.lastReminder) { - return - } - - let index = parseInt(instance.input.dataset.index) - this.taskEditTask.reminderDates[index] = newDate - - let lastIndex = this.taskEditTask.reminderDates.length - 1 - // put a new null at the end if we changed something - if (lastIndex === index && !isNaN(newDate)) { - this.taskEditTask.reminderDates.push(null) - } - }, - removeReminderByIndex(index) { - this.taskEditTask.reminderDates.splice(index, 1) - // Reset the last to 0 to have the "add reminder" button - this.taskEditTask.reminderDates[this.taskEditTask.reminderDates.length - 1] = null - }, - addAssignee() { - this.taskEditTask.assignees.push(this.newAssignee) - }, - deleteAssigneeByIndex(index) { - this.taskEditTask.assignees.splice(index, 1) - }, - findUser(query) { - if(query === '') { - this.clearAllFoundUsers() - return - } - - this.userService.getAll({}, {s: query}) - .then(response => { - // Filter the results to not include users who are already assigned - this.$set(this, 'foundUsers', differenceWith(response, this.taskEditTask.assignees, (first, second) => { - return first.id === second.id - })) - }) - .catch(e => { - message.error(e, this) - }) - }, - clearAllFoundUsers () { - this.$set(this, 'foundUsers', []) - }, - findLabel(query) { - if(query === '') { - this.clearAllLabels() - return - } - - if(this.labelTimeout !== null) { - clearTimeout(this.labelTimeout) - } - - // Delay the search 300ms to not send a request on every keystroke - this.labelTimeout = setTimeout(() => { - this.labelService.getAll({}, {s: query}) - .then(response => { - this.$set(this, 'foundLabels', differenceWith(response, this.taskEditTask.labels, (first, second) => { - return first.id === second.id - })) - this.labelTimeout = null - }) - .catch(e => { - message.error(e, this) - }) - }, 300) - }, - clearAllLabels () { - this.$set(this, 'foundLabels', []) - }, - addLabel(label) { - let labelTask = new LabelTaskModel({taskID: this.taskEditTask.id, label_id: label.id}) - this.labelTaskService.create(labelTask) - .then(() => { - message.success({message: 'The label was successfully added.'}, this) - }) - .catch(e => { - message.error(e, this) - }) - }, - removeLabel(label) { - let labelTask = new LabelTaskModel({taskID: this.taskEditTask.id, label_id: label.id}) - this.labelTaskService.delete(labelTask) - .then(() => { - // Remove the label from the list - for (const l in this.taskEditTask.labels) { - if (this.taskEditTask.labels[l].id === label.id) { - this.taskEditTask.labels.splice(l, 1) - } - } - message.success({message: 'The label was successfully removed.'}, this) - }) - .catch(e => { - message.error(e, this) - }) - }, - createAndAddLabel(title) { - let newLabel = new LabelModel({title: title}) - this.labelService.create(newLabel) - .then(r => { - this.addLabel(r) - this.taskEditTask.labels.push(r) - }) - .catch(e => { - message.error(e, this) - }) - } } } \ No newline at end of file diff --git a/src/components/tasks/Gantt.vue b/src/components/tasks/Gantt.vue new file mode 100644 index 000000000..9aee37d31 --- /dev/null +++ b/src/components/tasks/Gantt.vue @@ -0,0 +1,101 @@ + + + \ No newline at end of file diff --git a/src/components/tasks/ShowListTasks.vue b/src/components/tasks/ShowListTasks.vue new file mode 100644 index 000000000..dfbc919ad --- /dev/null +++ b/src/components/tasks/ShowListTasks.vue @@ -0,0 +1,177 @@ + + + \ No newline at end of file diff --git a/src/components/lists/ShowTasks.vue b/src/components/tasks/ShowTasks.vue similarity index 100% rename from src/components/lists/ShowTasks.vue rename to src/components/tasks/ShowTasks.vue diff --git a/src/components/lists/ShowTasksInRange.vue b/src/components/tasks/ShowTasksInRange.vue similarity index 100% rename from src/components/lists/ShowTasksInRange.vue rename to src/components/tasks/ShowTasksInRange.vue diff --git a/src/components/tasks/edit-task.vue b/src/components/tasks/edit-task.vue new file mode 100644 index 000000000..617c996b8 --- /dev/null +++ b/src/components/tasks/edit-task.vue @@ -0,0 +1,446 @@ + + + + + \ No newline at end of file diff --git a/src/components/tasks/gantt-component.vue b/src/components/tasks/gantt-component.vue new file mode 100644 index 000000000..25b62f7ee --- /dev/null +++ b/src/components/tasks/gantt-component.vue @@ -0,0 +1,350 @@ + + + diff --git a/src/main.js b/src/main.js index e0e49f1f7..e7b467b1e 100644 --- a/src/main.js +++ b/src/main.js @@ -8,7 +8,7 @@ import Modal from './components/modal/Modal' Vue.component('modal', Modal) // Register the task overview component -import TaskOverview from './components/lists/ShowTasks' +import TaskOverview from './components/tasks/ShowTasks' Vue.component('TaskOverview', TaskOverview) // Add CSS @@ -43,6 +43,7 @@ import { faCalendarWeek } from '@fortawesome/free-solid-svg-icons' import { faExclamation } from '@fortawesome/free-solid-svg-icons' import { faTags } from '@fortawesome/free-solid-svg-icons' import { faChevronDown } from '@fortawesome/free-solid-svg-icons' +import { faCheck } from '@fortawesome/free-solid-svg-icons' import { faTimesCircle } from '@fortawesome/free-regular-svg-icons' import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' @@ -70,6 +71,7 @@ library.add(faCalendarAlt) library.add(faExclamation) library.add(faTags) library.add(faChevronDown) +library.add(faCheck) Vue.component('icon', FontAwesomeIcon) diff --git a/src/router/index.js b/src/router/index.js index 6fd237c33..cbd8964d4 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -11,7 +11,7 @@ import GetPasswordResetComponent from '@/components/user/RequestPasswordReset' import ShowListComponent from '@/components/lists/ShowList' import NewListComponent from '@/components/lists/NewList' import EditListComponent from '@/components/lists/EditList' -import ShowTasksInRangeComponent from '@/components/lists/ShowTasksInRange' +import ShowTasksInRangeComponent from '@/components/tasks/ShowTasksInRange' // Namespace Handling import NewNamespaceComponent from '@/components/namespaces/NewNamespace' import EditNamespaceComponent from '@/components/namespaces/EditNamespace' @@ -62,6 +62,11 @@ export default new Router({ name: 'editList', component: EditListComponent }, + { + path: '/lists/:id/:type', + name: 'showListWithType', + component: ShowListComponent, + }, { path: '/namespaces/:id/list', name: 'newList', diff --git a/src/services/task.js b/src/services/task.js index c72da13c4..1bbecbe57 100644 --- a/src/services/task.js +++ b/src/services/task.js @@ -16,10 +16,21 @@ export default class TaskService extends AbstractService { } beforeUpdate(model) { + return this.processModel(model) + } + + beforeCreate(model) { + return this.processModel(model) + } + + processModel(model) { + // Ensure the listID is an int + model.listID = Number(model.listID) + // Convert the date in a unix timestamp - model.dueDate = +new Date(model.dueDate) / 1000 - model.startDate = +new Date(model.startDate) / 1000 - model.endDate = +new Date(model.endDate) / 1000 + model.dueDate = Math.round(+new Date(model.dueDate) / 1000) + model.startDate = Math.round(+new Date(model.startDate) / 1000) + model.endDate = Math.round(+new Date(model.endDate) / 1000) // remove all nulls, these would create empty reminders for (const index in model.reminderDates) { @@ -29,9 +40,11 @@ export default class TaskService extends AbstractService { } // Make normal timestamps from js dates - model.reminderDates = model.reminderDates.map(r => { - return Math.round(+new Date(r) / 1000) - }) + if(model.reminderDates.length > 0) { + model.reminderDates = model.reminderDates.map(r => { + return Math.round(+new Date(r) / 1000) + }) + } // Make the repeating amount to seconds let repeatAfterSeconds = 0 diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 897bd2617..89f9f3a55 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -17,6 +17,7 @@ $dropdown-content-shadow: none; $navbar-dropdown-boxed-shadow: $dropdown-content-shadow; $bulmaswatch-import-font: false !default; $light-background: #F1F5F8; +$transition-duration: 100ms; $vikunja-font: 'Quicksand', sans-serif; $vikunja-light-text: darken(#fff, 10%); diff --git a/src/styles/fancycheckbox.scss b/src/styles/fancycheckbox.scss index cd051fbc9..f12045cc7 100644 --- a/src/styles/fancycheckbox.scss +++ b/src/styles/fancycheckbox.scss @@ -5,6 +5,10 @@ padding-right: 5px; padding-top: 3px; + &.is-block { + margin: .5em .2em; + } + .check { cursor: pointer; position: relative; @@ -18,6 +22,11 @@ stroke: $primary; } + span { + font-size: 0.8em; + vertical-align: top; + } + svg { position: relative; z-index: 1; diff --git a/src/styles/gantt.scss b/src/styles/gantt.scss new file mode 100644 index 000000000..86339f7b7 --- /dev/null +++ b/src/styles/gantt.scss @@ -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; +} \ No newline at end of file diff --git a/src/styles/general.scss b/src/styles/general.scss index 2f84630df..aeb4dd859 100644 --- a/src/styles/general.scss +++ b/src/styles/general.scss @@ -194,13 +194,6 @@ } } - .fade-enter-active, .fade-leave-active { - transition: opacity 150ms; - } - .fade-enter, .fade-leave-to { - opacity: 0; - } - .dropdown-trigger .button { background: none; diff --git a/src/styles/scrollbars.scss b/src/styles/scrollbars.scss new file mode 100644 index 000000000..0ef4dbcc2 --- /dev/null +++ b/src/styles/scrollbars.scss @@ -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); +//} diff --git a/src/styles/tasks.scss b/src/styles/tasks.scss index f167fd4ae..29890d420 100644 --- a/src/styles/tasks.scss +++ b/src/styles/tasks.scss @@ -55,23 +55,23 @@ } } - .high-priority{ - color: $red; - - .icon { - vertical-align: middle; - } - - &.not-so-high { - color: $orange; - } - } - .tag { margin: 0 0.5em; } } + .high-priority{ + color: $red; + + .icon { + vertical-align: middle; + } + + &.not-so-high { + color: $orange; + } + } + input[type="checkbox"] { vertical-align: middle; } diff --git a/src/styles/theme.scss b/src/styles/theme.scss index 2f482411a..4afd5c735 100644 --- a/src/styles/theme.scss +++ b/src/styles/theme.scss @@ -240,4 +240,44 @@ h1,h2,h3,h4,h5,h6{ .navbar.main-theme { background: $light-background; -} \ No newline at end of file +} + +.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); + } + } +} diff --git a/src/styles/transitions.scss b/src/styles/transitions.scss new file mode 100644 index 000000000..6e53d81be --- /dev/null +++ b/src/styles/transitions.scss @@ -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; +} \ No newline at end of file diff --git a/src/vikunja.scss b/src/vikunja.scss index 3a7658873..c30f9a124 100644 --- a/src/vikunja.scss +++ b/src/vikunja.scss @@ -1,5 +1,7 @@ @import 'styles/theme'; +@import 'styles/scrollbars'; @import 'styles/general'; +@import 'styles/transitions'; @import 'styles/tasks'; @import 'styles/teams'; @@ -8,5 +10,6 @@ @import 'styles/fancycheckbox'; @import 'styles/tooltip'; +@import 'styles/gantt'; @import 'styles/multiselect'; \ No newline at end of file diff --git a/todo.md b/todo.md index e573cffa3..ba589dca8 100644 --- a/todo.md +++ b/todo.md @@ -36,6 +36,7 @@ * [x] Btns für Teams und neuer Namespace nach oben in die Leiste verschieben * [ ] Card-like overview of all lists with the first 3-5 tasks, undone first * [ ] Be able to collapse all lists in a namespace by clicking on the menu entry +* [ ] Fancy Scrollbars ## Funktionales @@ -49,6 +50,22 @@ * [x] Overdue rot anzeigen * [x] Beim Team bearbeiten Nutzer suchen einbauen * [ ] Keyboard shortcuts +* [ ] Gantt chart + * [x] Basics + * [x] Add tasks without dates set + * [x] Edit tasks with a kind of popup when clicking on them - needs refactoring edit task into an own component + * [x] Add a new task with a button or so + * [x] Be able to choose the range for the chart + * [x] Show task priority + * [x] Show task done or not done + * [ ] Colors - needs api change before + * [x] More view modes + * [x] Month: "The big picture" + * [x] Day: 3-hour slices of a day +* [ ] Table view (list view, bit with more details) +* [ ] Calender view +* [ ] Kanaban +* [ ] Group list view by almost all fields ## Funktionen aus der API diff --git a/yarn.lock b/yarn.lock index 70e302c78..5fb34092d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8973,6 +8973,11 @@ vm-browserify@0.0.4: dependencies: indexof "0.0.1" +vue-drag-resize@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/vue-drag-resize/-/vue-drag-resize-1.3.2.tgz#99132a99746c878e4596fad08d36c98f604930a3" + integrity sha512-XiSEep3PPh9IPQqa4vIy/YENBpYch2SIPNipcPAEGhaSa0V8A8gSq9s7JQ66p/hiINdnR7f5ZqAkOdm6zU/4Gw== + vue-eslint-parser@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz#c268c96c6d94cfe3d938a5f7593959b0ca3360d1"