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:
konrad 2020-09-05 20:16:17 +00:00
parent cac8b09263
commit 4a8b15e7be
6 changed files with 138 additions and 62 deletions

View file

@ -1,5 +1,5 @@
<template>
<span>
<div class="task loader-container" :class="{'is-loading': taskService.loading}">
<fancycheckbox v-model="task.done" @change="markAsDone" :disabled="isArchived || disabled"/>
<span class="tasktext" :class="{ 'done': task.done}">
<router-link :to="{ name: taskDetailRoute, params: { id: task.id } }">
@ -43,7 +43,22 @@
</transition>
<priority-label :priority="task.priority"/>
</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>
<script>
@ -105,6 +120,11 @@
this.task = new TaskModel()
this.taskService = new TaskService()
},
computed: {
currentList() {
return typeof this.$store.state.currentList === 'undefined' ? {id: 0, title: ''} : this.$store.state.currentList
},
},
methods: {
markAsDone(checked) {
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
}
},
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>

View file

@ -54,6 +54,7 @@ import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons'
import { faCloudDownloadAlt } from '@fortawesome/free-solid-svg-icons'
import { faCloudUploadAlt } 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 { faAlignLeft } 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(faKeyboard)
library.add(faSave)
library.add(faStarSolid)
Vue.component('icon', FontAwesomeIcon)

View file

@ -1,4 +1,4 @@
import AbstractModel from './abstractModel';
import AbstractModel from './abstractModel'
import UserModel from './user'
import LabelModel from './label'
import AttachmentModel from './attachment'
@ -95,6 +95,7 @@ export default class TaskModel extends AbstractModel {
attachments: [],
identifier: '',
index: 0,
isFavorite: false,
createdBy: UserModel,
created: null,
@ -167,7 +168,7 @@ export default class TaskModel extends AbstractModel {
return
}
const {state} = await navigator.permissions.request({name: 'notifications'});
const {state} = await navigator.permissions.request({name: 'notifications'})
if (state !== 'granted') {
console.debug('Notification permission not granted, not showing notifications')
return
@ -191,11 +192,11 @@ export default class TaskModel extends AbstractModel {
actions: [
{
action: 'mark-as-done',
title: 'Done'
title: 'Done',
},
{
action: 'show-task',
title: 'Show task'
title: 'Show task',
},
],
})

View file

@ -93,5 +93,11 @@ export default {
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')
}
},
},
}

View file

@ -22,66 +22,86 @@
padding: 0.5rem 1rem;
border-bottom: 1px solid darken(#fff, 10%);
transition: background-color $transition;
align-items: center;
cursor: pointer;
&:hover {
background-color: darken($light-background, 3);
}
span:not(.tag) {
width: 100%;
.tasktext,
&.tasktext {
vertical-align: top;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
display: inline-block;
cursor: pointer;
width: 100%;
.tasktext,
&.tasktext {
vertical-align: top;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
display: inline-block;
max-width: calc(#{$desktop} - 27px - 2rem); // The max width of the outer container minus the padding
width: calc(100% - 2rem); // The max width of the outer container minus the padding
.overdue {
color: $red;
}
}
@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
}
.task-list {
width: auto;
color: lighten($grey, 25%);
font-size: .9em;
white-space: nowrap;
}
.overdue {
color: $red;
}
.fancycheckbox span {
display: none;
}
.task-list {
width: auto;
color: lighten($grey, 25%);
font-size: .9em;
vertical-align: text-bottom;
}
.tag {
margin: 0 0.5em;
}
.avatar {
border-radius: 50%;
vertical-align: bottom;
margin-left: 5px;
height: 27px;
width: 27px;
}
a {
color: $text;
transition: color ease $transition-duration;
&:hover {
color: darken($text, 40%);
}
}
.favorite {
opacity: 0;
text-align: center;
width: 27px;
transition: opacity $transition, color $transition;
&:hover {
color: $orange;
}
.fancycheckbox span {
&.is-favorite {
opacity: 1;
color: $orange;
}
}
&:hover .favorite {
opacity: 1;
}
.fancycheckbox {
height: 18px;
padding-top: 0;
span {
display: none;
}
.tag {
margin: 0 0.5em;
}
.avatar {
border-radius: 50%;
vertical-align: bottom;
margin-left: 5px;
height: 27px;
width: 27px;
}
a {
color: $text;
transition: color ease $transition-duration;
&:hover {
color: darken($text, 40%);
}
}
}
.tasktext.done {
@ -107,6 +127,15 @@
width: 24px;
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 {
@ -242,3 +271,8 @@
}
}
}
.is-max-width-desktop .tasks .task {
width: 100%;
max-width: $desktop;
}

View file

@ -84,17 +84,18 @@
<div class="columns">
<div class="column">
<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
:the-task="t"
@taskUpdated="updateTasks"
task-detail-route="task.detail"
:disabled="!canWrite"
/>
<div @click="editTask(t.id)" class="icon settings" v-if="!list.isArchived && canWrite">
<icon icon="pencil-alt"/>
</div>
<single-task-in-list
v-for="t in tasks"
:key="t.id"
:the-task="t"
@taskUpdated="updateTasks"
task-detail-route="task.detail"
:disabled="!canWrite"
>
<div @click="editTask(t.id)" class="icon settings" v-if="!list.isArchived && canWrite">
<icon icon="pencil-alt"/>
</div>
</single-task-in-list>
</div>
</div>
<div class="column is-4" v-if="isTaskEdit">