2020-11-01 18:36:00 +01:00
|
|
|
<template>
|
|
|
|
<div :class="{'is-active': menuActive}" class="namespace-container">
|
|
|
|
<div class="menu top-menu">
|
|
|
|
<router-link :to="{name: 'home'}" class="logo">
|
2021-10-03 20:48:02 +02:00
|
|
|
<img alt="Vikunja" :src="logoUrl" width="164" height="48"/>
|
2020-11-01 18:36:00 +01:00
|
|
|
</router-link>
|
|
|
|
<ul class="menu-list">
|
|
|
|
<li>
|
|
|
|
<router-link :to="{ name: 'home'}">
|
|
|
|
<span class="icon">
|
|
|
|
<icon icon="calendar"/>
|
|
|
|
</span>
|
2021-06-24 01:24:57 +02:00
|
|
|
{{ $t('navigation.overview') }}
|
2020-11-01 18:36:00 +01:00
|
|
|
</router-link>
|
|
|
|
</li>
|
|
|
|
<li>
|
2020-11-22 18:05:25 +01:00
|
|
|
<router-link :to="{ name: 'tasks.range'}">
|
2020-11-01 18:36:00 +01:00
|
|
|
<span class="icon">
|
|
|
|
<icon :icon="['far', 'calendar-alt']"/>
|
|
|
|
</span>
|
2021-06-24 01:24:57 +02:00
|
|
|
{{ $t('navigation.upcoming') }}
|
2020-11-01 18:36:00 +01:00
|
|
|
</router-link>
|
|
|
|
</li>
|
|
|
|
<li>
|
|
|
|
<router-link :to="{ name: 'namespaces.index'}">
|
|
|
|
<span class="icon">
|
|
|
|
<icon icon="layer-group"/>
|
|
|
|
</span>
|
2021-06-24 01:24:57 +02:00
|
|
|
{{ $t('namespace.title') }}
|
2020-11-01 18:36:00 +01:00
|
|
|
</router-link>
|
|
|
|
</li>
|
|
|
|
<li>
|
|
|
|
<router-link :to="{ name: 'labels.index'}">
|
|
|
|
<span class="icon">
|
|
|
|
<icon icon="tags"/>
|
|
|
|
</span>
|
2021-06-24 01:24:57 +02:00
|
|
|
{{ $t('label.title') }}
|
2020-11-01 18:36:00 +01:00
|
|
|
</router-link>
|
|
|
|
</li>
|
2020-12-23 21:28:32 +01:00
|
|
|
<li>
|
|
|
|
<router-link :to="{ name: 'teams.index'}">
|
|
|
|
<span class="icon">
|
|
|
|
<icon icon="users"/>
|
|
|
|
</span>
|
2021-06-24 01:24:57 +02:00
|
|
|
{{ $t('team.title') }}
|
2020-12-23 21:28:32 +01:00
|
|
|
</router-link>
|
|
|
|
</li>
|
2020-11-01 18:36:00 +01:00
|
|
|
</ul>
|
|
|
|
</div>
|
|
|
|
|
2020-11-12 20:57:39 +01:00
|
|
|
<aside class="menu namespaces-lists loader-container" :class="{'is-loading': loading}">
|
2021-08-19 21:10:29 +02:00
|
|
|
<template v-for="(n, nk) in namespaces" :key="n.id" >
|
|
|
|
<div class="namespace-title" :class="{'has-menu': n.id > 0}">
|
2021-02-20 16:28:45 +01:00
|
|
|
<span
|
|
|
|
@click="toggleLists(n.id)"
|
2020-11-01 18:36:00 +01:00
|
|
|
class="menu-label"
|
2021-07-09 10:22:20 +02:00
|
|
|
v-tooltip="getNamespaceTitle(n) + ' (' + n.lists.filter(l => !l.isArchived).length + ')'">
|
2020-11-01 18:36:00 +01:00
|
|
|
<span class="name">
|
|
|
|
<span
|
|
|
|
:style="{ backgroundColor: n.hexColor }"
|
|
|
|
class="color-bubble"
|
|
|
|
v-if="n.hexColor !== ''">
|
|
|
|
</span>
|
2021-07-09 10:22:20 +02:00
|
|
|
{{ getNamespaceTitle(n) }} ({{ n.lists.filter(l => !l.isArchived).length }})
|
2020-11-01 18:36:00 +01:00
|
|
|
</span>
|
2021-02-20 16:28:45 +01:00
|
|
|
</span>
|
2021-04-19 23:56:00 +02:00
|
|
|
<a
|
|
|
|
class="icon is-small toggle-lists-icon"
|
|
|
|
:class="{'active': typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true}"
|
2021-05-19 17:26:05 +02:00
|
|
|
@click="toggleLists(n.id)"
|
|
|
|
>
|
2021-04-19 23:56:00 +02:00
|
|
|
<icon icon="chevron-down"/>
|
|
|
|
</a>
|
2021-01-30 17:17:04 +01:00
|
|
|
<namespace-settings-dropdown :namespace="n" v-if="n.id > 0"/>
|
2020-11-01 18:36:00 +01:00
|
|
|
</div>
|
2021-07-28 21:56:29 +02:00
|
|
|
<div
|
|
|
|
:key="n.id + 'child'"
|
|
|
|
class="more-container"
|
|
|
|
v-if="typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true"
|
|
|
|
>
|
2021-08-23 21:24:52 +02:00
|
|
|
<!--
|
|
|
|
NOTE: a v-model / computed setter is not possible, since the updateActiveLists function
|
|
|
|
triggered by the change needs to have access to the current namespace
|
|
|
|
-->
|
2021-07-28 22:58:12 +02:00
|
|
|
<draggable
|
2021-09-08 11:59:46 +02:00
|
|
|
v-bind="dragOptions"
|
2021-08-23 21:18:12 +02:00
|
|
|
:modelValue="activeLists[nk]"
|
|
|
|
@update:modelValue="(lists) => updateActiveLists(n, lists)"
|
2021-07-28 22:58:12 +02:00
|
|
|
:group="`namespace-${n.id}-lists`"
|
|
|
|
@start="() => drag = true"
|
|
|
|
@end="e => saveListPosition(e, nk)"
|
|
|
|
handle=".handle"
|
2021-07-29 13:05:33 +02:00
|
|
|
:disabled="n.id < 0"
|
2021-08-20 15:46:41 +02:00
|
|
|
tag="transition-group"
|
|
|
|
item-key="id"
|
|
|
|
:component-data="{
|
|
|
|
type: 'transition',
|
|
|
|
tag: 'ul',
|
|
|
|
name: !drag ? 'flip-list' : null,
|
|
|
|
class: [
|
|
|
|
'menu-list can-be-hidden',
|
|
|
|
{ 'dragging-disabled': n.id < 0 }
|
|
|
|
]
|
|
|
|
}"
|
2021-07-28 22:58:12 +02:00
|
|
|
>
|
2021-08-20 15:46:41 +02:00
|
|
|
<template #item="{element: l}">
|
2021-07-28 22:58:12 +02:00
|
|
|
<li
|
|
|
|
class="loader-container"
|
|
|
|
:class="{'is-loading': listUpdating[l.id]}"
|
|
|
|
>
|
|
|
|
<router-link
|
|
|
|
:to="{ name: 'list.index', params: { listId: l.id} }"
|
2021-08-20 15:17:19 +02:00
|
|
|
v-slot="{ href, navigate, isActive }"
|
|
|
|
custom
|
2020-11-01 18:36:00 +01:00
|
|
|
>
|
2021-08-20 15:17:19 +02:00
|
|
|
<a
|
|
|
|
@click="navigate"
|
|
|
|
:href="href"
|
|
|
|
class="list-menu-link"
|
2021-10-16 13:54:01 +02:00
|
|
|
:class="{'router-link-exact-active': isActive || currentList.id === l.id}"
|
2021-08-20 15:17:19 +02:00
|
|
|
>
|
|
|
|
<span class="icon handle">
|
|
|
|
<icon icon="grip-lines"/>
|
|
|
|
</span>
|
|
|
|
<span
|
|
|
|
:style="{ backgroundColor: l.hexColor }"
|
|
|
|
class="color-bubble"
|
|
|
|
v-if="l.hexColor !== ''">
|
|
|
|
</span>
|
|
|
|
<span class="list-menu-title">
|
|
|
|
{{ getListTitle(l) }}
|
|
|
|
</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>
|
|
|
|
</a>
|
2021-07-28 22:58:12 +02:00
|
|
|
</router-link>
|
|
|
|
<list-settings-dropdown :list="l" v-if="l.id > 0"/>
|
|
|
|
<span class="list-setting-spacer" v-else></span>
|
|
|
|
</li>
|
2021-08-20 15:46:41 +02:00
|
|
|
</template>
|
2021-07-28 22:58:12 +02:00
|
|
|
</draggable>
|
2020-11-01 18:36:00 +01:00
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
</aside>
|
2021-07-28 22:58:12 +02:00
|
|
|
<a class="menu-bottom-link" href="https://vikunja.io" target="_blank" rel="noreferrer noopener nofollow">
|
2021-06-24 01:24:57 +02:00
|
|
|
{{ $t('misc.poweredBy') }}
|
|
|
|
</a>
|
2020-11-01 18:36:00 +01:00
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
import {mapState} from 'vuex'
|
2021-01-21 23:33:16 +01:00
|
|
|
import {CURRENT_LIST, MENU_ACTIVE, LOADING, LOADING_MODULE} from '@/store/mutation-types'
|
2021-07-25 15:27:15 +02:00
|
|
|
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
2021-01-30 17:17:04 +01:00
|
|
|
import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue'
|
2021-07-28 21:56:29 +02:00
|
|
|
import draggable from 'vuedraggable'
|
|
|
|
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
2020-11-01 18:36:00 +01:00
|
|
|
|
2021-10-03 20:48:02 +02:00
|
|
|
import logoUrl from '@/assets/logo-full.svg'
|
|
|
|
|
2020-11-01 18:36:00 +01:00
|
|
|
export default {
|
|
|
|
name: 'navigation',
|
2021-02-20 16:28:45 +01:00
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
listsVisible: {},
|
2021-07-28 21:56:29 +02:00
|
|
|
drag: false,
|
|
|
|
dragOptions: {
|
|
|
|
animation: 100,
|
|
|
|
ghostClass: 'ghost',
|
|
|
|
},
|
|
|
|
listUpdating: {},
|
2021-10-03 20:48:02 +02:00
|
|
|
logoUrl,
|
2021-02-20 16:28:45 +01:00
|
|
|
}
|
|
|
|
},
|
2021-01-30 17:17:04 +01:00
|
|
|
components: {
|
|
|
|
ListSettingsDropdown,
|
|
|
|
NamespaceSettingsDropdown,
|
2021-07-28 21:56:29 +02:00
|
|
|
draggable,
|
2021-01-30 17:17:04 +01:00
|
|
|
},
|
2021-08-23 21:24:52 +02:00
|
|
|
computed: {
|
|
|
|
...mapState({
|
|
|
|
namespaces: state => state.namespaces.namespaces.filter(n => !n.isArchived),
|
|
|
|
currentList: CURRENT_LIST,
|
|
|
|
background: 'background',
|
|
|
|
menuActive: MENU_ACTIVE,
|
|
|
|
loading: state => state[LOADING] && state[LOADING_MODULE] === 'namespaces',
|
|
|
|
}),
|
|
|
|
activeLists() {
|
|
|
|
return this.namespaces.map(({lists}) => lists.filter(item => !item.isArchived))
|
|
|
|
},
|
|
|
|
},
|
2020-11-01 18:36:00 +01:00
|
|
|
beforeCreate() {
|
2021-10-11 19:37:20 +02:00
|
|
|
// FIXME: async action in beforeCreate, might be unfinished when component mounts
|
2020-11-01 18:36:00 +01:00
|
|
|
this.$store.dispatch('namespaces/loadNamespaces')
|
2021-02-20 16:28:45 +01:00
|
|
|
.then(namespaces => {
|
|
|
|
namespaces.forEach(n => {
|
2021-04-19 22:36:31 +02:00
|
|
|
if (typeof this.listsVisible[n.id] === 'undefined') {
|
2021-08-19 21:35:38 +02:00
|
|
|
this.listsVisible[n.id] = true
|
2021-04-19 22:36:31 +02:00
|
|
|
}
|
2021-02-20 16:28:45 +01:00
|
|
|
})
|
|
|
|
})
|
2020-11-01 18:36:00 +01:00
|
|
|
},
|
|
|
|
created() {
|
2020-12-10 14:52:35 +01:00
|
|
|
window.addEventListener('resize', this.resize)
|
2020-11-01 18:36:00 +01:00
|
|
|
},
|
2020-12-17 12:45:26 +01:00
|
|
|
mounted() {
|
|
|
|
this.resize()
|
|
|
|
},
|
2020-11-01 18:36:00 +01:00
|
|
|
methods: {
|
|
|
|
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)
|
|
|
|
},
|
2020-12-10 14:52:35 +01:00
|
|
|
resize() {
|
|
|
|
// Hide the menu by default on mobile
|
|
|
|
if (window.innerWidth < 770) {
|
|
|
|
this.$store.commit(MENU_ACTIVE, false)
|
|
|
|
} else {
|
|
|
|
this.$store.commit(MENU_ACTIVE, true)
|
|
|
|
}
|
|
|
|
},
|
2021-02-20 16:28:45 +01:00
|
|
|
toggleLists(namespaceId) {
|
2021-08-19 21:35:38 +02:00
|
|
|
this.listsVisible[namespaceId] = !this.listsVisible[namespaceId] ?? false
|
2021-02-20 16:28:45 +01:00
|
|
|
},
|
2021-08-23 21:24:52 +02:00
|
|
|
updateActiveLists(namespace, activeLists) {
|
|
|
|
// this is a bit hacky: since we do have to filter out the archived items from the list
|
|
|
|
// for vue draggable updating it is not as simple as replacing it.
|
|
|
|
// instead we iterate over the non archived items in the old list and replace them with the ones in their new order
|
|
|
|
const lists = namespace.lists.map((item) => {
|
|
|
|
if (item.isArchived) {
|
|
|
|
return item
|
|
|
|
}
|
|
|
|
return activeLists.shift()
|
|
|
|
})
|
|
|
|
|
|
|
|
const newNamespace = {
|
|
|
|
...namespace,
|
|
|
|
lists,
|
|
|
|
}
|
|
|
|
|
|
|
|
this.$store.commit('namespaces/setNamespaceById', newNamespace)
|
|
|
|
},
|
2021-10-11 19:37:20 +02:00
|
|
|
|
|
|
|
async saveListPosition(e, namespaceIndex) {
|
2021-08-23 21:24:52 +02:00
|
|
|
const listsActive = this.activeLists[namespaceIndex]
|
|
|
|
const list = listsActive[e.newIndex]
|
|
|
|
const listBefore = listsActive[e.newIndex - 1] ?? null
|
|
|
|
const listAfter = listsActive[e.newIndex + 1] ?? null
|
2021-08-19 21:35:38 +02:00
|
|
|
this.listUpdating[list.id] = true
|
2021-07-28 21:56:29 +02:00
|
|
|
|
2021-08-23 21:24:52 +02:00
|
|
|
const position = calculateItemPosition(listBefore !== null ? listBefore.position : null, listAfter !== null ? listAfter.position : null)
|
2021-07-28 21:56:29 +02:00
|
|
|
|
2021-10-11 19:37:20 +02:00
|
|
|
try {
|
|
|
|
// create a copy of the list in order to not violate vuex mutations
|
|
|
|
await this.$store.dispatch('lists/updateList', {
|
|
|
|
...list,
|
|
|
|
position,
|
2021-07-28 21:56:29 +02:00
|
|
|
})
|
2021-10-11 19:37:20 +02:00
|
|
|
} finally {
|
|
|
|
this.listUpdating[list.id] = false
|
|
|
|
}
|
2021-07-28 21:56:29 +02:00
|
|
|
},
|
2020-11-01 18:36:00 +01:00
|
|
|
},
|
|
|
|
}
|
|
|
|
</script>
|
2021-05-19 17:26:05 +02:00
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
.list-setting-spacer {
|
|
|
|
width: 32px;
|
|
|
|
flex-shrink: 0;
|
|
|
|
}
|
|
|
|
</style>
|