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>
|
||||
<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>
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue