Favorite lists (#237)
Remove/show favorites namespace if a task/list is the first to being marked as favorite Add special case to prevent marking an archived list as favorite Add marking a task as favorite on namespaces page Prevent toggling the favorite state for the favorites list Add method to toggle list favorite in the menu Add favorite icon to lists in menu Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/237
This commit is contained in:
parent
5a0ef73b54
commit
f2fcf42639
7 changed files with 162 additions and 29 deletions
23
src/App.vue
23
src/App.vue
|
@ -207,16 +207,26 @@
|
||||||
are nested inside of the namespaces makes it a lot harder.-->
|
are nested inside of the namespaces makes it a lot harder.-->
|
||||||
<li :key="l.id" v-if="!l.isArchived">
|
<li :key="l.id" v-if="!l.isArchived">
|
||||||
<router-link
|
<router-link
|
||||||
|
class="list-menu-link"
|
||||||
:class="{'router-link-exact-active': currentList.id === l.id}"
|
:class="{'router-link-exact-active': currentList.id === l.id}"
|
||||||
:to="{ name: 'list.index', params: { listId: l.id} }">
|
:to="{ name: 'list.index', params: { listId: l.id} }"
|
||||||
<span class="name">
|
tag="span"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
:style="{ backgroundColor: l.hexColor }"
|
:style="{ backgroundColor: l.hexColor }"
|
||||||
class="color-bubble"
|
class="color-bubble"
|
||||||
v-if="l.hexColor !== ''">
|
v-if="l.hexColor !== ''">
|
||||||
</span>
|
</span>
|
||||||
|
<span class="list-menu-title">
|
||||||
{{ l.title }}
|
{{ l.title }}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
:class="{'is-favorite': l.isFavorite}"
|
||||||
|
@click.stop="toggleFavoriteList(l)"
|
||||||
|
class="favorite">
|
||||||
|
<icon icon="star" v-if="l.isFavorite"/>
|
||||||
|
<icon :icon="['far', 'star']" v-else/>
|
||||||
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
@ -508,6 +518,15 @@ export default {
|
||||||
// Notify the service worker to actually do the update
|
// Notify the service worker to actually do the update
|
||||||
this.registration.waiting.postMessage('skipWaiting')
|
this.registration.waiting.postMessage('skipWaiting')
|
||||||
},
|
},
|
||||||
|
toggleFavoriteList(list) {
|
||||||
|
// The favorites pseudo list is always favorite
|
||||||
|
// Archived lists cannot be marked favorite
|
||||||
|
if (list.id === -1 || list.isArchived) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$store.dispatch('lists/toggleListFavorite', list)
|
||||||
|
.catch(e => this.error(e, this))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -35,6 +35,7 @@ export default class ListModel extends AbstractModel {
|
||||||
hexColor: '',
|
hexColor: '',
|
||||||
identifier: '',
|
identifier: '',
|
||||||
backgroundInformation: null,
|
backgroundInformation: null,
|
||||||
|
isFavorite: false,
|
||||||
|
|
||||||
created: null,
|
created: null,
|
||||||
updated: null,
|
updated: null,
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
import ListService from '@/services/list'
|
||||||
|
|
||||||
|
const FavoriteListsNamespace = -2
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
|
@ -22,4 +25,32 @@ export default {
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
actions: {
|
||||||
|
toggleListFavorite(ctx, list) {
|
||||||
|
list.isFavorite = !list.isFavorite
|
||||||
|
const listService = new ListService()
|
||||||
|
|
||||||
|
return listService.update(list)
|
||||||
|
.then(r => {
|
||||||
|
if (r.isFavorite) {
|
||||||
|
ctx.commit('addList', r)
|
||||||
|
r.namespaceId = FavoriteListsNamespace
|
||||||
|
ctx.commit('namespaces/addListToNamespace', r, {root: true})
|
||||||
|
} else {
|
||||||
|
ctx.commit('namespaces/setListInNamespaceById', r, {root: true})
|
||||||
|
r.namespaceId = FavoriteListsNamespace
|
||||||
|
ctx.commit('namespaces/removeListFromNamespaceById', r, {root: true})
|
||||||
|
}
|
||||||
|
ctx.dispatch('namespaces/loadNamespacesIfFavoritesDontExist', null, {root: true})
|
||||||
|
ctx.dispatch('namespaces/removeFavoritesNamespaceIfEmpty', null, {root: true})
|
||||||
|
return Promise.resolve(r)
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
// Reset the list state to the initial one to avoid confusion for the user
|
||||||
|
list.isFavorite = !list.isFavorite
|
||||||
|
ctx.commit('addList', list)
|
||||||
|
return Promise.reject(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
|
@ -24,6 +24,7 @@ export default {
|
||||||
for (const n in state.namespaces) {
|
for (const n in state.namespaces) {
|
||||||
// We don't have the namespace id on the list which means we need to loop over all lists until we find it.
|
// We don't have the namespace id on the list which means we need to loop over all lists until we find it.
|
||||||
// FIXME: Not ideal at all - we should fix that at the api level.
|
// FIXME: Not ideal at all - we should fix that at the api level.
|
||||||
|
if (state.namespaces[n].id === list.namespaceId) {
|
||||||
for (const l in state.namespaces[n].lists) {
|
for (const l in state.namespaces[n].lists) {
|
||||||
if (state.namespaces[n].lists[l].id === list.id) {
|
if (state.namespaces[n].lists[l].id === list.id) {
|
||||||
const namespace = state.namespaces[n]
|
const namespace = state.namespaces[n]
|
||||||
|
@ -33,6 +34,7 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
addNamespace(state, namespace) {
|
addNamespace(state, namespace) {
|
||||||
state.namespaces.push(namespace)
|
state.namespaces.push(namespace)
|
||||||
|
@ -45,6 +47,20 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
removeListFromNamespaceById(state, list) {
|
||||||
|
for (const n in state.namespaces) {
|
||||||
|
// We don't have the namespace id on the list which means we need to loop over all lists until we find it.
|
||||||
|
// FIXME: Not ideal at all - we should fix that at the api level.
|
||||||
|
if (state.namespaces[n].id === list.namespaceId) {
|
||||||
|
for (const l in state.namespaces[n].lists) {
|
||||||
|
if (state.namespaces[n].lists[l].id === list.id) {
|
||||||
|
state.namespaces[n].lists.splice(l, 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
getListAndNamespaceById: state => listId => {
|
getListAndNamespaceById: state => listId => {
|
||||||
|
@ -99,5 +115,11 @@ export default {
|
||||||
return ctx.dispatch('loadNamespaces')
|
return ctx.dispatch('loadNamespaces')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
removeFavoritesNamespaceIfEmpty(ctx) {
|
||||||
|
if (ctx.state.namespaces[0].id === -2 && ctx.state.namespaces[0].lists.length === 0) {
|
||||||
|
ctx.state.namespaces.splice(0, 1)
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
|
@ -30,7 +30,6 @@ $lists-per-row: 5;
|
||||||
border: 1px solid $grey;
|
border: 1px solid $grey;
|
||||||
color: $grey !important;
|
color: $grey !important;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
margin-left: .5rem;
|
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-family: $vikunja-font;
|
font-family: $vikunja-font;
|
||||||
background: rgba($white, 0.75);
|
background: rgba($white, 0.75);
|
||||||
|
@ -41,6 +40,7 @@ $lists-per-row: 5;
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
|
cursor: pointer;
|
||||||
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
|
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
|
||||||
height: 150px;
|
height: 150px;
|
||||||
background: $white;
|
background: $white;
|
||||||
|
@ -100,6 +100,7 @@ $lists-per-row: 5;
|
||||||
|
|
||||||
.is-archived {
|
.is-archived {
|
||||||
font-size: .75em;
|
font-size: .75em;
|
||||||
|
float: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,6 +128,29 @@ $lists-per-row: 5;
|
||||||
color: $white;
|
color: $white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.favorite {
|
||||||
|
transition: opacity $transition, color $transition;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-archived {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-favorite {
|
||||||
|
display: inline-block;
|
||||||
|
opacity: 1;
|
||||||
|
color: $orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .favorite {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,25 +164,44 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-label, .menu-list a {
|
.menu-label, .menu-list span.list-menu-link, .menu-list a {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
span.name:not(.icon) {
|
.list-menu-title {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.color-bubble {
|
.color-bubble {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: initial;
|
width: 14px; // Without this, the bubble is only 10.2342357612px wide and seems squashed.
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
height: 12px;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
margin-right: 2px;
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite {
|
||||||
|
margin-left: .25rem;
|
||||||
|
transition: opacity $transition, color $transition;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-favorite {
|
||||||
|
opacity: 1;
|
||||||
|
color: $orange;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover .favorite {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-label {
|
.menu-label {
|
||||||
|
@ -201,7 +220,7 @@
|
||||||
padding: 10px 0.3em 0;
|
padding: 10px 0.3em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-label, .nsettings, .menu-list a {
|
.menu-label, .nsettings, .menu-list span.list-menu-link, .menu-list a {
|
||||||
color: $vikunja-nav-color;
|
color: $vikunja-nav-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,7 +262,7 @@
|
||||||
height: 44px;
|
height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
span.list-menu-link, a {
|
||||||
padding: 0.75em .5em 0.75em $navbar-padding * 1.5;
|
padding: 0.75em .5em 0.75em $navbar-padding * 1.5;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
@ -299,7 +318,7 @@
|
||||||
font-family: $vikunja-font;
|
font-family: $vikunja-font;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
span.list-menu-link, a {
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,12 +33,20 @@
|
||||||
}"
|
}"
|
||||||
:to="{ name: 'list.index', params: { listId: l.id} }"
|
:to="{ name: 'list.index', params: { listId: l.id} }"
|
||||||
class="list"
|
class="list"
|
||||||
|
tag="span"
|
||||||
v-if="showArchived ? true : !l.isArchived"
|
v-if="showArchived ? true : !l.isArchived"
|
||||||
>
|
>
|
||||||
<div class="is-archived-container">
|
<div class="is-archived-container">
|
||||||
<span class="is-archived" v-if="l.isArchived">
|
<span class="is-archived" v-if="l.isArchived">
|
||||||
Archived
|
Archived
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
:class="{'is-favorite': l.isFavorite, 'is-archived': l.isArchived}"
|
||||||
|
@click.stop="toggleFavoriteList(l)"
|
||||||
|
class="favorite">
|
||||||
|
<icon icon="star" v-if="l.isFavorite"/>
|
||||||
|
<icon :icon="['far', 'star']" v-else/>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="title">{{ l.title }}</div>
|
<div class="title">{{ l.title }}</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
@ -93,6 +101,15 @@ export default {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
toggleFavoriteList(list) {
|
||||||
|
// The favorites pseudo list is always favorite
|
||||||
|
// Archived lists cannot be marked favorite
|
||||||
|
if (list.id === -1 || list.isArchived) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$store.dispatch('lists/toggleListFavorite', list)
|
||||||
|
.catch(e => this.error(e, this))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in a new issue