Favorite tasks (#236)
Add loading spinner when updating a task Show favorites namespace if the favorited task is the first favorite Show the list favorited tasks belong to Fix task width Add method to mark a function as favorite Make favorite clickable Format Hide favorite button when not hovered Add button to mark a task as favorite Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/236
This commit is contained in:
parent
cac8b09263
commit
4a8b15e7be
6 changed files with 138 additions and 62 deletions
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<span>
|
<div class="task loader-container" :class="{'is-loading': taskService.loading}">
|
||||||
<fancycheckbox v-model="task.done" @change="markAsDone" :disabled="isArchived || disabled"/>
|
<fancycheckbox v-model="task.done" @change="markAsDone" :disabled="isArchived || disabled"/>
|
||||||
<span class="tasktext" :class="{ 'done': task.done}">
|
<span class="tasktext" :class="{ 'done': task.done}">
|
||||||
<router-link :to="{ name: taskDetailRoute, params: { id: task.id } }">
|
<router-link :to="{ name: taskDetailRoute, params: { id: task.id } }">
|
||||||
|
@ -43,7 +43,22 @@
|
||||||
</transition>
|
</transition>
|
||||||
<priority-label :priority="task.priority"/>
|
<priority-label :priority="task.priority"/>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
<router-link
|
||||||
|
v-if="currentList.id !== task.listId && $store.getters['lists/getListById'](task.listId) !== null"
|
||||||
|
v-tooltip="`This task belongs to list '${$store.getters['lists/getListById'](task.listId).title}'`"
|
||||||
|
:to="{ name: 'list.list', params: { listId: task.listId } }"
|
||||||
|
class="task-list">
|
||||||
|
{{ $store.getters['lists/getListById'](task.listId).title }}
|
||||||
|
</router-link>
|
||||||
|
<a
|
||||||
|
class="favorite"
|
||||||
|
:class="{'is-favorite': task.isFavorite}"
|
||||||
|
@click="toggleFavorite">
|
||||||
|
<icon icon="star" v-if="task.isFavorite"/>
|
||||||
|
<icon :icon="['far', 'star']" v-else/>
|
||||||
|
</a>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -105,6 +120,11 @@
|
||||||
this.task = new TaskModel()
|
this.task = new TaskModel()
|
||||||
this.taskService = new TaskService()
|
this.taskService = new TaskService()
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
currentList() {
|
||||||
|
return typeof this.$store.state.currentList === 'undefined' ? {id: 0, title: ''} : this.$store.state.currentList
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
markAsDone(checked) {
|
markAsDone(checked) {
|
||||||
const updateFunc = () => {
|
const updateFunc = () => {
|
||||||
|
@ -136,6 +156,18 @@
|
||||||
updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
|
updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
toggleFavorite() {
|
||||||
|
this.task.isFavorite = !this.task.isFavorite
|
||||||
|
this.taskService.update(this.task)
|
||||||
|
.then(t => {
|
||||||
|
this.task = t
|
||||||
|
this.$emit('taskUpdated', t)
|
||||||
|
this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist')
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
this.error(e, this)
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -54,6 +54,7 @@ import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons'
|
||||||
import { faCloudDownloadAlt } from '@fortawesome/free-solid-svg-icons'
|
import { faCloudDownloadAlt } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'
|
import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faPercent } from '@fortawesome/free-solid-svg-icons'
|
import { faPercent } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { faStar as faStarSolid } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faStar } from '@fortawesome/free-regular-svg-icons'
|
import { faStar } from '@fortawesome/free-regular-svg-icons'
|
||||||
import { faAlignLeft } from '@fortawesome/free-solid-svg-icons'
|
import { faAlignLeft } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faPaperclip } from '@fortawesome/free-solid-svg-icons'
|
import { faPaperclip } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
@ -119,6 +120,7 @@ library.add(faFilter)
|
||||||
library.add(faFillDrip)
|
library.add(faFillDrip)
|
||||||
library.add(faKeyboard)
|
library.add(faKeyboard)
|
||||||
library.add(faSave)
|
library.add(faSave)
|
||||||
|
library.add(faStarSolid)
|
||||||
|
|
||||||
Vue.component('icon', FontAwesomeIcon)
|
Vue.component('icon', FontAwesomeIcon)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import AbstractModel from './abstractModel';
|
import AbstractModel from './abstractModel'
|
||||||
import UserModel from './user'
|
import UserModel from './user'
|
||||||
import LabelModel from './label'
|
import LabelModel from './label'
|
||||||
import AttachmentModel from './attachment'
|
import AttachmentModel from './attachment'
|
||||||
|
@ -95,6 +95,7 @@ export default class TaskModel extends AbstractModel {
|
||||||
attachments: [],
|
attachments: [],
|
||||||
identifier: '',
|
identifier: '',
|
||||||
index: 0,
|
index: 0,
|
||||||
|
isFavorite: false,
|
||||||
|
|
||||||
createdBy: UserModel,
|
createdBy: UserModel,
|
||||||
created: null,
|
created: null,
|
||||||
|
@ -167,7 +168,7 @@ export default class TaskModel extends AbstractModel {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const {state} = await navigator.permissions.request({name: 'notifications'});
|
const {state} = await navigator.permissions.request({name: 'notifications'})
|
||||||
if (state !== 'granted') {
|
if (state !== 'granted') {
|
||||||
console.debug('Notification permission not granted, not showing notifications')
|
console.debug('Notification permission not granted, not showing notifications')
|
||||||
return
|
return
|
||||||
|
@ -191,11 +192,11 @@ export default class TaskModel extends AbstractModel {
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
action: 'mark-as-done',
|
action: 'mark-as-done',
|
||||||
title: 'Done'
|
title: 'Done',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'show-task',
|
action: 'show-task',
|
||||||
title: 'Show task'
|
title: 'Show task',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -93,5 +93,11 @@ export default {
|
||||||
return Promise.reject(e)
|
return Promise.reject(e)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
loadNamespacesIfFavoritesDontExist(ctx) {
|
||||||
|
// The first namespace should be the one holding all favorites
|
||||||
|
if(ctx.state.namespaces[0].id !== -2) {
|
||||||
|
return ctx.dispatch('loadNamespaces')
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
|
@ -22,16 +22,13 @@
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-bottom: 1px solid darken(#fff, 10%);
|
border-bottom: 1px solid darken(#fff, 10%);
|
||||||
transition: background-color $transition;
|
transition: background-color $transition;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: darken($light-background, 3);
|
background-color: darken($light-background, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
span:not(.tag) {
|
|
||||||
width: 100%;
|
|
||||||
display: inline-block;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.tasktext,
|
.tasktext,
|
||||||
&.tasktext {
|
&.tasktext {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
@ -39,23 +36,18 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: calc(#{$desktop} - 27px - 2rem); // The max width of the outer container minus the padding
|
width: 100%;
|
||||||
width: calc(100% - 2rem); // The max width of the outer container minus the padding
|
|
||||||
|
|
||||||
@media screen and (max-width: $desktop) {
|
|
||||||
max-width: calc(100vw - 27px - 2rem - 1.5rem - 3rem); // 1.5rem is the padding of the tasks container, 3rem is the padding of .app-container
|
|
||||||
}
|
|
||||||
|
|
||||||
.overdue {
|
.overdue {
|
||||||
color: $red;
|
color: $red;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.task-list {
|
.task-list {
|
||||||
width: auto;
|
width: auto;
|
||||||
color: lighten($grey, 25%);
|
color: lighten($grey, 25%);
|
||||||
font-size: .9em;
|
font-size: .9em;
|
||||||
vertical-align: text-bottom;
|
white-space: nowrap;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fancycheckbox span {
|
.fancycheckbox span {
|
||||||
|
@ -82,6 +74,34 @@
|
||||||
color: darken($text, 40%);
|
color: darken($text, 40%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.favorite {
|
||||||
|
opacity: 0;
|
||||||
|
text-align: center;
|
||||||
|
width: 27px;
|
||||||
|
transition: opacity $transition, color $transition;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-favorite {
|
||||||
|
opacity: 1;
|
||||||
|
color: $orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .favorite {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancycheckbox {
|
||||||
|
height: 18px;
|
||||||
|
padding-top: 0;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tasktext.done {
|
.tasktext.done {
|
||||||
|
@ -107,6 +127,15 @@
|
||||||
width: 24px;
|
width: 24px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.loader-container.is-loading:after {
|
||||||
|
top: calc(50% - 1em);
|
||||||
|
left: calc(50% - 1em);
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
border-left-color: $grey-light;
|
||||||
|
border-bottom-color: $grey-light;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.task:last-child {
|
.task:last-child {
|
||||||
|
@ -242,3 +271,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-max-width-desktop .tasks .task {
|
||||||
|
width: 100%;
|
||||||
|
max-width: $desktop;
|
||||||
|
}
|
||||||
|
|
|
@ -84,17 +84,18 @@
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="tasks" v-if="tasks && tasks.length > 0" :class="{'short': isTaskEdit}">
|
<div class="tasks" v-if="tasks && tasks.length > 0" :class="{'short': isTaskEdit}">
|
||||||
<div class="task" v-for="t in tasks" :key="t.id">
|
|
||||||
<single-task-in-list
|
<single-task-in-list
|
||||||
|
v-for="t in tasks"
|
||||||
|
:key="t.id"
|
||||||
:the-task="t"
|
:the-task="t"
|
||||||
@taskUpdated="updateTasks"
|
@taskUpdated="updateTasks"
|
||||||
task-detail-route="task.detail"
|
task-detail-route="task.detail"
|
||||||
:disabled="!canWrite"
|
:disabled="!canWrite"
|
||||||
/>
|
>
|
||||||
<div @click="editTask(t.id)" class="icon settings" v-if="!list.isArchived && canWrite">
|
<div @click="editTask(t.id)" class="icon settings" v-if="!list.isArchived && canWrite">
|
||||||
<icon icon="pencil-alt"/>
|
<icon icon="pencil-alt"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</single-task-in-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-4" v-if="isTaskEdit">
|
<div class="column is-4" v-if="isTaskEdit">
|
||||||
|
|
Loading…
Add table
Reference in a new issue