From f2fcf42639219907871323a02f0f62cefae07f9b Mon Sep 17 00:00:00 2001 From: konrad Date: Sun, 6 Sep 2020 14:20:48 +0000 Subject: [PATCH] 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 Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/237 --- src/App.vue | 33 +++++++++++++++---- src/models/list.js | 1 + src/store/modules/lists.js | 31 ++++++++++++++++++ src/store/modules/namespaces.js | 34 +++++++++++++++---- src/styles/components/namespaces.scss | 26 ++++++++++++++- src/styles/theme/navigation.scss | 43 ++++++++++++++++++------- src/views/namespaces/ListNamespaces.vue | 23 +++++++++++-- 7 files changed, 162 insertions(+), 29 deletions(-) diff --git a/src/App.vue b/src/App.vue index 4104cd4d9..7ca2290cf 100644 --- a/src/App.vue +++ b/src/App.vue @@ -207,16 +207,26 @@ are nested inside of the namespaces makes it a lot harder.-->
  • - - - + :to="{ name: 'list.index', params: { listId: l.id} }" + tag="span" + > + + + {{ l.title }} + + + +
  • @@ -508,6 +518,15 @@ export default { // Notify the service worker to actually do the update 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)) + }, }, } diff --git a/src/models/list.js b/src/models/list.js index 469e53ea5..8e395e871 100644 --- a/src/models/list.js +++ b/src/models/list.js @@ -35,6 +35,7 @@ export default class ListModel extends AbstractModel { hexColor: '', identifier: '', backgroundInformation: null, + isFavorite: false, created: null, updated: null, diff --git a/src/store/modules/lists.js b/src/store/modules/lists.js index 1f72f5d6d..21d6d076c 100644 --- a/src/store/modules/lists.js +++ b/src/store/modules/lists.js @@ -1,4 +1,7 @@ import Vue from 'vue' +import ListService from '@/services/list' + +const FavoriteListsNamespace = -2 export default { namespaced: true, @@ -22,4 +25,32 @@ export default { 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) + }) + }, + }, } \ No newline at end of file diff --git a/src/store/modules/namespaces.js b/src/store/modules/namespaces.js index 2248c8d3f..85a469198 100644 --- a/src/store/modules/namespaces.js +++ b/src/store/modules/namespaces.js @@ -24,12 +24,14 @@ export default { 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. - for (const l in state.namespaces[n].lists) { - if (state.namespaces[n].lists[l].id === list.id) { - const namespace = state.namespaces[n] - namespace.lists[l] = list - Vue.set(state.namespaces, n, namespace) - return + if (state.namespaces[n].id === list.namespaceId) { + for (const l in state.namespaces[n].lists) { + if (state.namespaces[n].lists[l].id === list.id) { + const namespace = state.namespaces[n] + namespace.lists[l] = list + Vue.set(state.namespaces, n, namespace) + return + } } } } @@ -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: { getListAndNamespaceById: state => listId => { @@ -99,5 +115,11 @@ export default { 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() + } + }, }, } \ No newline at end of file diff --git a/src/styles/components/namespaces.scss b/src/styles/components/namespaces.scss index 85815f62d..1c68d3bf2 100644 --- a/src/styles/components/namespaces.scss +++ b/src/styles/components/namespaces.scss @@ -30,7 +30,6 @@ $lists-per-row: 5; border: 1px solid $grey; color: $grey !important; padding: 2px 4px; - margin-left: .5rem; border-radius: 3px; font-family: $vikunja-font; background: rgba($white, 0.75); @@ -41,6 +40,7 @@ $lists-per-row: 5; flex-flow: row wrap; .list { + cursor: pointer; width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row}); height: 150px; background: $white; @@ -100,6 +100,7 @@ $lists-per-row: 5; .is-archived { font-size: .75em; + float: left; } } @@ -127,6 +128,29 @@ $lists-per-row: 5; 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; + } } } } diff --git a/src/styles/theme/navigation.scss b/src/styles/theme/navigation.scss index e03b0a52b..bc29bb82c 100644 --- a/src/styles/theme/navigation.scss +++ b/src/styles/theme/navigation.scss @@ -164,25 +164,44 @@ overflow: hidden; } - .menu-label, .menu-list a { + .menu-label, .menu-list span.list-menu-link, .menu-list a { display: flex; align-items: center; justify-content: space-between; + cursor: pointer; - span.name:not(.icon) { + .list-menu-title { overflow: hidden; text-overflow: ellipsis; + width: 100%; + } - .color-bubble { - display: inline-block; - vertical-align: initial; - width: 12px; - height: 12px; - border-radius: 100%; - margin-right: 2px; + .color-bubble { + display: inline-block; + width: 14px; // Without this, the bubble is only 10.2342357612px wide and seems squashed. + height: 12px; + border-radius: 100%; + 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 { @@ -201,7 +220,7 @@ 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; } @@ -243,7 +262,7 @@ height: 44px; } - a { + span.list-menu-link, a { padding: 0.75em .5em 0.75em $navbar-padding * 1.5; transition: all 0.2s ease; @@ -299,7 +318,7 @@ font-family: $vikunja-font; } - a { + span.list-menu-link, a { padding-left: 2em; display: inline-block; } diff --git a/src/views/namespaces/ListNamespaces.vue b/src/views/namespaces/ListNamespaces.vue index 8b275bac8..b691dbae4 100644 --- a/src/views/namespaces/ListNamespaces.vue +++ b/src/views/namespaces/ListNamespaces.vue @@ -33,12 +33,20 @@ }" :to="{ name: 'list.index', params: { listId: l.id} }" class="list" + tag="span" v-if="showArchived ? true : !l.isArchived" >
    - - Archived - + + Archived + + + + +
    {{ l.title }}
    @@ -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)) + }, }, }