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)) + }, }, }