feat: separate NavigationNamespace
continuous-integration/drone/pr Build is failing Details

This commit is contained in:
Dominik Pschenitschni 2022-07-07 20:28:39 +02:00
parent d11fae1c38
commit d35505ec09
Signed by: dpschen
GPG Key ID: B257AC0149F43A77
2 changed files with 405 additions and 327 deletions

View File

@ -0,0 +1,281 @@
<template>
<div class="namespace-title" :class="{'has-menu': n.id > 0}">
<span
@click="$emit('toggleLists' n.id)"
class="menu-label"
v-tooltip="title"
>
<span
v-if="n.hexColor !== ''"
:style="{ backgroundColor: n.hexColor }"
class="color-bubble"
/>
<span class="name">
{{ title }}
</span>
<a
class="icon is-small toggle-lists-icon pl-2"
:class="{'active': typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true}"
@click="$emit('toggleLists' n.id)"
>
<icon icon="chevron-down"/>
</a>
<span class="count" :class="{'ml-2 mr-0': n.id > 0}">
({{ namespaceListsCount[nk] }})
</span>
</span>
<namespace-settings-dropdown :namespace="n" v-if="n.id > 0"/>
</div>
<div
v-if="listsVisible[n.id] ?? true"
:key="n.id + 'child'"
class="more-container"
>
<!--
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
-->
<draggable
v-bind="dragOptions"
:modelValue="activeLists[nk]"
@update:modelValue="(lists) => updateActiveLists(n, lists)"
group="namespace-lists"
@start="() => drag = true"
@end="saveListPosition"
handle=".handle"
:disabled="n.id < 0 || null"
tag="transition-group"
item-key="id"
:data-namespace-id="n.id"
:data-namespace-index="nk"
:component-data="{
type: 'transition',
tag: 'ul',
name: !drag ? 'flip-list' : null,
class: [
'menu-list can-be-hidden',
{ 'dragging-disabled': n.id < 0 }
]
}"
>
<template #item="{element: l}">
<li
class="loader-container is-loading-small"
:class="{'is-loading': listUpdating[l.id]}"
>
<router-link
:to="{ name: 'list.index', params: { listId: l.id} }"
v-slot="{ href, navigate, isActive }"
custom
>
<a
@click="navigate"
:href="href"
class="list-menu-link"
:class="{'router-link-exact-active': isActive || currentList?.id === l.id}"
>
<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.prevent.stop="toggleFavoriteList(l)"
class="favorite">
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
</span>
</a>
</router-link>
<list-settings-dropdown :list="l" v-if="l.id > 0"/>
<span class="list-setting-spacer" v-else></span>
</li>
</template>
</draggable>
</div>
</template>
<script setup lang="ts">
import {ref, computed, onMounted, onBeforeMount, type PropType} from 'vue'
import {useStore} from 'vuex'
import draggable from 'vuedraggable'
import {SortableEvent} from 'sortablejs'
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue'
import {MENU_ACTIVE} from '@/store/mutation-types'
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
import {useEventListener} from '@vueuse/core'
import NamespaceModel from '@/models/namespace'
import ListModel from '@/models/list'
defineProps({
title: {
type: String,
required: true,
},
namespace: {
type: Object as PropType<NamespaceModel>,
required: true,
},
})
defineEmits(['toggleLists'])
const drag = ref(false)
const dragOptions = {
animation: 100,
ghostClass: 'ghost',
}
const store = useStore()
const isActiveList = computed(() => props.namespace?.lists && !props.namespace?.lists.isArchived)
const listCount = computed()
const currentList = computed(() => store.state.currentList)
function updateActiveLists(namespace: NamespaceModel, activeLists: ListModel[]) {
// 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.
// To work around this, we merge the active lists with the archived ones. Doing so breaks the order
// because now all archived lists are sorted after the active ones. This is fine because they are sorted
// later when showing them anyway, and it makes the merging happening here a lot easier.
const lists = [
...activeLists,
...namespace.lists.filter(l => l.isArchived),
]
store.commit('namespaces/setNamespaceById', {
...namespace,
lists,
})
}
const listUpdating = ref<{ [id: NamespaceModel['id']]: boolean }>({})
async function saveListPosition(e: SortableEvent) {
if (!e.newIndex) return
const namespaceId = parseInt(e.to.dataset.namespaceId as string)
const newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex as string)
const listsActive = activeLists.value[newNamespaceIndex]
const list = listsActive[e.newIndex]
const listBefore = listsActive[e.newIndex - 1] ?? null
const listAfter = listsActive[e.newIndex + 1] ?? null
listUpdating.value[list.id] = true
const position = calculateItemPosition(
listBefore !== null ? listBefore.position : null,
listAfter !== null ? listAfter.position : null,
)
try {
// create a copy of the list in order to not violate vuex mutations
await store.dispatch('lists/updateList', {
...list,
position,
namespaceId,
})
} finally {
listUpdating.value[list.id] = false
}
}
function toggleFavoriteList(list: ListModel) {
// The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite
if (list.id === -1 || list.isArchived) {
return
}
store.dispatch('lists/toggleListFavorite', list)
}
</script>
<style lang="scss" scoped>
.namespace-title {
display: flex;
align-items: center;
justify-content: space-between;
.menu-label {
margin-bottom: 0;
flex: 1 1 auto;
.name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: auto;
}
.count {
color: var(--grey-500);
margin-right: .5rem;
}
}
a:not(.dropdown-item) {
color: $vikunja-nav-color;
padding: 0 .25rem;
}
:deep(.dropdown-trigger) {
padding: .5rem;
cursor: pointer;
}
.toggle-lists-icon {
svg {
transition: all $transition;
transform: rotate(90deg);
opacity: 1;
}
&.active svg {
transform: rotate(0deg);
opacity: 0;
}
}
&:hover .toggle-lists-icon svg {
opacity: 1;
}
&:not(.has-menu) .toggle-lists-icon {
padding-right: 1rem;
}
}
.menu-label {
font-size: 1rem;
font-weight: 700;
font-weight: bold;
font-family: $vikunja-font;
color: $vikunja-nav-color;
font-weight: 500;
min-height: 2.5rem;
padding-top: 0;
padding-left: $navbar-padding;
overflow: hidden;
}
.menu-label,
.nsettings,
.menu-list span.list-menu-link,
.menu-list a {
color: $vikunja-nav-color;
}
</style>

View File

@ -1,187 +1,90 @@
<template>
<aside :class="{'is-active': menuActive}" class="namespace-container">
<nav class="menu top-menu">
<nav class="menu">
<router-link :to="{name: 'home'}" class="logo">
<Logo width="164" height="48"/>
</router-link>
<ul class="menu-list">
<li>
<router-link :to="{ name: 'home'}" v-shortcut="'g o'">
<span class="icon">
<icon icon="calendar"/>
</span>
{{ $t('navigation.overview') }}
</router-link>
</li>
<li>
<router-link :to="{ name: 'tasks.range'}" v-shortcut="'g u'">
<span class="icon">
<icon :icon="['far', 'calendar-alt']"/>
</span>
{{ $t('navigation.upcoming') }}
</router-link>
</li>
<li>
<router-link :to="{ name: 'namespaces.index'}" v-shortcut="'g n'">
<span class="icon">
<icon icon="layer-group"/>
</span>
{{ $t('namespace.title') }}
</router-link>
</li>
<li>
<router-link :to="{ name: 'labels.index'}" v-shortcut="'g a'">
<span class="icon">
<icon icon="tags"/>
</span>
{{ $t('label.title') }}
</router-link>
</li>
<li>
<router-link :to="{ name: 'teams.index'}" v-shortcut="'g m'">
<span class="icon">
<icon icon="users"/>
</span>
{{ $t('team.title') }}
<li
v-for="({title, to, shortcut, icon, }, index) in MAIN_NAV_ITEMS"
:key="index"
>
<router-link :to="to" v-shortcut="shortcut">
<icon class="icon" :icon="icon"/>
{{ title }}
</router-link>
</li>
</ul>
</nav>
<nav class="menu namespaces-lists loader-container is-loading-small" :class="{'is-loading': loading}">
<template v-for="(n, nk) in namespaces" :key="n.id">
<div class="namespace-title" :class="{'has-menu': n.id > 0}">
<span
@click="toggleLists(n.id)"
class="menu-label"
v-tooltip="namespaceTitles[nk]"
>
<span
v-if="n.hexColor !== ''"
:style="{ backgroundColor: n.hexColor }"
class="color-bubble"
/>
<span class="name">
{{ namespaceTitles[nk] }}
</span>
<a
class="icon is-small toggle-lists-icon pl-2"
:class="{'active': typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true}"
@click="toggleLists(n.id)"
>
<icon icon="chevron-down"/>
</a>
<span class="count" :class="{'ml-2 mr-0': n.id > 0}">
({{ namespaceListsCount[nk] }})
</span>
</span>
<namespace-settings-dropdown :namespace="n" v-if="n.id > 0"/>
</div>
<div
v-if="listsVisible[n.id] ?? true"
:key="n.id + 'child'"
class="more-container"
>
<!--
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
-->
<draggable
v-bind="dragOptions"
:modelValue="activeLists[nk]"
@update:modelValue="(lists) => updateActiveLists(n, lists)"
group="namespace-lists"
@start="() => drag = true"
@end="saveListPosition"
handle=".handle"
:disabled="n.id < 0 || null"
tag="transition-group"
item-key="id"
:data-namespace-id="n.id"
:data-namespace-index="nk"
:component-data="{
type: 'transition',
tag: 'ul',
name: !drag ? 'flip-list' : null,
class: [
'menu-list can-be-hidden',
{ 'dragging-disabled': n.id < 0 }
]
}"
>
<template #item="{element: l}">
<li
class="loader-container is-loading-small"
:class="{'is-loading': listUpdating[l.id]}"
>
<router-link
:to="{ name: 'list.index', params: { listId: l.id} }"
v-slot="{ href, navigate, isActive }"
custom
>
<a
@click="navigate"
:href="href"
class="list-menu-link"
:class="{'router-link-exact-active': isActive || currentList?.id === l.id}"
>
<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.prevent.stop="toggleFavoriteList(l)"
class="favorite">
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
</span>
</a>
</router-link>
<list-settings-dropdown :list="l" v-if="l.id > 0"/>
<span class="list-setting-spacer" v-else></span>
</li>
</template>
</draggable>
</div>
</template>
</nav>
<PoweredByLink/>
<LoadingIndicator
class="namespaces-lists"
:isLoading="loading"
element="nav"
>
<NavigationNamespace
v-for="(n, nk) in namespaces"
:key="n.id"
:namespace="n"
:title="namespaceTitles[nk]"
:showList="listsVisible[n.id] ?? true"
/>
</LoadingIndicator>
<PoweredByLink class="navigation__powered-by-link " />
</aside>
</template>
<script setup lang="ts">
import {ref, computed, onMounted, onBeforeMount} from 'vue'
import {ref, computed, onBeforeMount} from 'vue'
import {useStore} from 'vuex'
import draggable from 'vuedraggable'
import {SortableEvent} from 'sortablejs'
import {useEventListener} from '@vueuse/core'
import {useI18n} from 'vue-i18n'
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue'
import PoweredByLink from '@/components/home/PoweredByLink.vue'
import Logo from '@/components/home/Logo.vue'
import PoweredByLink from '@/components/home/PoweredByLink.vue'
import NavigationNamespace from './NavigationNamespace.vue'
import {MENU_ACTIVE} from '@/store/mutation-types'
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
import {useEventListener} from '@vueuse/core'
import NamespaceModel from '@/models/namespace'
import ListModel from '@/models/list'
const drag = ref(false)
const dragOptions = {
animation: 100,
ghostClass: 'ghost',
}
const {t} = useI18n()
const MAIN_NAV_ITEMS = [
{
title: t('navigation.overview'),
icon: 'calendar',
to: { name: 'home'},
shortcut: 'g o',
},
{
title: t('navigation.upcoming'),
icon: ['far', 'calendar-alt'],
to: { name: 'tasks.range'},
shortcut: 'g u',
},
{
title: t('namespace.title'),
icon: 'layer-group',
to: { name: 'namespaces.index'},
shortcut: 'g n',
},
{
title: t('label.title'),
icon: 'tags',
to: { name: 'labels.index'},
shortcut: 'g a',
},
{
title: t('team.title'),
icon: 'users',
to: { name: 'teams.index'},
shortcut: 'g m',
},
]
const store = useStore()
const currentList = computed(() => store.state.currentList)
const menuActive = computed(() => store.state.menuActive)
const loading = computed(() => store.state.loading && store.state.loadingModule === 'namespaces')
@ -206,24 +109,16 @@ const namespaceListsCount = computed(() => {
})
useEventListener('resize', resize)
onMounted(() => resize())
function toggleFavoriteList(list: ListModel) {
// The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite
if (list.id === -1 || list.isArchived) {
return
}
store.dispatch('lists/toggleListFavorite', list)
}
function resize() {
// Hide the menu by default on mobile
store.commit(MENU_ACTIVE, window.innerWidth >= 770)
}
useEventListener('resize', resize)
resize()
function toggleLists(namespaceId: number) {
listsVisible.value[namespaceId] = !listsVisible.value[namespaceId]
}
@ -238,54 +133,6 @@ onBeforeMount(async () => {
}
})
})
function updateActiveLists(namespace: NamespaceModel, activeLists: ListModel[]) {
// 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.
// To work around this, we merge the active lists with the archived ones. Doing so breaks the order
// because now all archived lists are sorted after the active ones. This is fine because they are sorted
// later when showing them anyway, and it makes the merging happening here a lot easier.
const lists = [
...activeLists,
...namespace.lists.filter(l => l.isArchived),
]
store.commit('namespaces/setNamespaceById', {
...namespace,
lists,
})
}
const listUpdating = ref<{ [id: NamespaceModel['id']]: boolean }>({})
async function saveListPosition(e: SortableEvent) {
if (!e.newIndex) return
const namespaceId = parseInt(e.to.dataset.namespaceId as string)
const newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex as string)
const listsActive = activeLists.value[newNamespaceIndex]
const list = listsActive[e.newIndex]
const listBefore = listsActive[e.newIndex - 1] ?? null
const listAfter = listsActive[e.newIndex + 1] ?? null
listUpdating.value[list.id] = true
const position = calculateItemPosition(
listBefore !== null ? listBefore.position : null,
listAfter !== null ? listAfter.position : null,
)
try {
// create a copy of the list in order to not violate vuex mutations
await store.dispatch('lists/updateList', {
...list,
position,
namespaceId,
})
} finally {
listUpdating.value[list.id] = false
}
}
</script>
<style lang="scss" scoped>
@ -294,15 +141,12 @@ $vikunja-nav-background: var(--site-background);
$vikunja-nav-color: var(--grey-700);
$vikunja-nav-selected-width: 0.4rem;
.namespace-container {
background: $vikunja-nav-background;
color: $vikunja-nav-color;
padding: 0 0 1rem;
transition: transform $transition-duration ease-in;
.navigation {
position: fixed;
top: $navbar-height;
bottom: 0;
left: 0;
transition: transform $transition-duration ease-in;
transform: translateX(-100%);
overflow-x: auto;
width: $navbar-width;
@ -317,21 +161,17 @@ $vikunja-nav-selected-width: 0.4rem;
transform: translateX(0);
transition: transform $transition-duration ease-out;
}
}
.navigation {
background: $vikunja-nav-background;
color: $vikunja-nav-color;
display: flex;
flex-direction: column;
.menu {
.menu-label {
font-size: 1rem;
font-weight: 700;
font-weight: bold;
font-family: $vikunja-font;
color: $vikunja-nav-color;
font-weight: 500;
min-height: 2.5rem;
padding-top: 0;
padding-left: $navbar-padding;
overflow: hidden;
}
.menu-label,
.menu-list span.list-menu-link,
@ -341,31 +181,7 @@ $vikunja-nav-selected-width: 0.4rem;
justify-content: space-between;
cursor: pointer;
.list-menu-title {
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.color-bubble {
height: 12px;
flex: 0 0 12px;
}
.favorite {
margin-left: .25rem;
transition: opacity $transition, color $transition;
opacity: 0;
&:hover {
color: var(--warning);
}
&.is-favorite {
opacity: 1;
color: var(--warning);
}
}
&:hover .favorite {
opacity: 1;
@ -376,6 +192,32 @@ $vikunja-nav-selected-width: 0.4rem;
}
}
.list-menu-title {
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.color-bubble {
height: 12px;
flex: 0 0 12px;
}
.favorite {
margin-left: .25rem;
transition: opacity $transition, color $transition;
opacity: 0;
&:hover {
color: var(--warning);
}
&.is-favorite {
opacity: 1;
color: var(--warning);
}
}
.menu-label {
.color-bubble {
width: 14px;
@ -388,66 +230,9 @@ $vikunja-nav-selected-width: 0.4rem;
}
}
.namespace-title {
display: flex;
align-items: center;
justify-content: space-between;
.menu-label {
margin-bottom: 0;
flex: 1 1 auto;
.name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: auto;
}
.count {
color: var(--grey-500);
margin-right: .5rem;
}
}
a:not(.dropdown-item) {
color: $vikunja-nav-color;
padding: 0 .25rem;
}
:deep(.dropdown-trigger) {
padding: .5rem;
cursor: pointer;
}
.toggle-lists-icon {
svg {
transition: all $transition;
transform: rotate(90deg);
opacity: 1;
}
&.active svg {
transform: rotate(0deg);
opacity: 0;
}
}
&:hover .toggle-lists-icon svg {
opacity: 1;
}
&:not(.has-menu) .toggle-lists-icon {
padding-right: 1rem;
}
}
.menu-label,
.nsettings,
.menu-list span.list-menu-link,
.menu-list a {
color: $vikunja-nav-color;
}
.menu-list {
li {
@ -526,6 +311,17 @@ $vikunja-nav-selected-width: 0.4rem;
}
}
&.namespaces-lists {
padding-top: math.div($navbar-padding, 2);
}
.icon {
color: var(--grey-400) !important;
}
}
.logo {
display: block;
@ -537,15 +333,6 @@ $vikunja-nav-selected-width: 0.4rem;
}
}
&.namespaces-lists {
padding-top: math.div($navbar-padding, 2);
}
.icon {
color: var(--grey-400) !important;
}
}
.top-menu {
margin-top: math.div($navbar-padding, 2);
@ -579,4 +366,14 @@ $vikunja-nav-selected-width: 0.4rem;
a.dropdown-item:hover {
background: var(--dropdown-item-hover-background-color) !important;
}
.navigation__powered-by-link {
margin-top: auto;
color: var(--grey-300);
text-align: center;
display: block;
padding-top: 1rem;
padding-bottom: 1rem;
font-size: .8rem;
}
</style>