WIP: feat: separate NavigationNamespace #2108
281
src/components/home/NavigationNamespace.vue
Normal file
281
src/components/home/NavigationNamespace.vue
Normal 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>
|
|
@ -1,187 +1,90 @@
|
||||||
<template>
|
<template>
|
||||||
<aside :class="{'is-active': menuActive}" class="namespace-container">
|
<aside :class="{'is-active': menuActive}" class="namespace-container">
|
||||||
<nav class="menu top-menu">
|
<nav class="menu">
|
||||||
<router-link :to="{name: 'home'}" class="logo">
|
<router-link :to="{name: 'home'}" class="logo">
|
||||||
<Logo width="164" height="48"/>
|
<Logo width="164" height="48"/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>
|
<li
|
||||||
<router-link :to="{ name: 'home'}" v-shortcut="'g o'">
|
v-for="({title, to, shortcut, icon, }, index) in MAIN_NAV_ITEMS"
|
||||||
<span class="icon">
|
:key="index"
|
||||||
<icon icon="calendar"/>
|
>
|
||||||
</span>
|
<router-link :to="to" v-shortcut="shortcut">
|
||||||
{{ $t('navigation.overview') }}
|
<icon class="icon" :icon="icon"/>
|
||||||
</router-link>
|
{{ title }}
|
||||||
</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') }}
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<nav class="menu namespaces-lists loader-container is-loading-small" :class="{'is-loading': loading}">
|
<LoadingIndicator
|
||||||
<template v-for="(n, nk) in namespaces" :key="n.id">
|
class="namespaces-lists"
|
||||||
<div class="namespace-title" :class="{'has-menu': n.id > 0}">
|
:isLoading="loading"
|
||||||
<span
|
element="nav"
|
||||||
@click="toggleLists(n.id)"
|
>
|
||||||
class="menu-label"
|
<NavigationNamespace
|
||||||
v-tooltip="namespaceTitles[nk]"
|
v-for="(n, nk) in namespaces"
|
||||||
>
|
:key="n.id"
|
||||||
<span
|
:namespace="n"
|
||||||
v-if="n.hexColor !== ''"
|
:title="namespaceTitles[nk]"
|
||||||
:style="{ backgroundColor: n.hexColor }"
|
:showList="listsVisible[n.id] ?? true"
|
||||||
class="color-bubble"
|
/>
|
||||||
/>
|
</LoadingIndicator>
|
||||||
<span class="name">
|
<PoweredByLink class="navigation__powered-by-link " />
|
||||||
{{ 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/>
|
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, computed, onMounted, onBeforeMount} from 'vue'
|
import {ref, computed, onBeforeMount} from 'vue'
|
||||||
import {useStore} from 'vuex'
|
import {useStore} from 'vuex'
|
||||||
import draggable from 'vuedraggable'
|
import {useEventListener} from '@vueuse/core'
|
||||||
import {SortableEvent} from 'sortablejs'
|
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 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 {MENU_ACTIVE} from '@/store/mutation-types'
|
||||||
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
|
||||||
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
|
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
|
||||||
import {useEventListener} from '@vueuse/core'
|
|
||||||
import NamespaceModel from '@/models/namespace'
|
import NamespaceModel from '@/models/namespace'
|
||||||
import ListModel from '@/models/list'
|
|
||||||
|
|
||||||
const drag = ref(false)
|
const {t} = useI18n()
|
||||||
const dragOptions = {
|
|
||||||
animation: 100,
|
const MAIN_NAV_ITEMS = [
|
||||||
ghostClass: 'ghost',
|
{
|
||||||
}
|
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 store = useStore()
|
||||||
const currentList = computed(() => store.state.currentList)
|
|
||||||
const menuActive = computed(() => store.state.menuActive)
|
const menuActive = computed(() => store.state.menuActive)
|
||||||
const loading = computed(() => store.state.loading && store.state.loadingModule === 'namespaces')
|
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() {
|
function resize() {
|
||||||
// Hide the menu by default on mobile
|
// Hide the menu by default on mobile
|
||||||
store.commit(MENU_ACTIVE, window.innerWidth >= 770)
|
store.commit(MENU_ACTIVE, window.innerWidth >= 770)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEventListener('resize', resize)
|
||||||
|
resize()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function toggleLists(namespaceId: number) {
|
function toggleLists(namespaceId: number) {
|
||||||
listsVisible.value[namespaceId] = !listsVisible.value[namespaceId]
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -294,15 +141,12 @@ $vikunja-nav-background: var(--site-background);
|
||||||
$vikunja-nav-color: var(--grey-700);
|
$vikunja-nav-color: var(--grey-700);
|
||||||
$vikunja-nav-selected-width: 0.4rem;
|
$vikunja-nav-selected-width: 0.4rem;
|
||||||
|
|
||||||
.namespace-container {
|
.navigation {
|
||||||
background: $vikunja-nav-background;
|
|
||||||
color: $vikunja-nav-color;
|
|
||||||
padding: 0 0 1rem;
|
|
||||||
transition: transform $transition-duration ease-in;
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: $navbar-height;
|
top: $navbar-height;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
transition: transform $transition-duration ease-in;
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
width: $navbar-width;
|
width: $navbar-width;
|
||||||
|
@ -317,21 +161,17 @@ $vikunja-nav-selected-width: 0.4rem;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
transition: transform $transition-duration ease-out;
|
transition: transform $transition-duration ease-out;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation {
|
||||||
|
background: $vikunja-nav-background;
|
||||||
|
color: $vikunja-nav-color;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
|
||||||
.menu {
|
.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-label,
|
||||||
.menu-list span.list-menu-link,
|
.menu-list span.list-menu-link,
|
||||||
|
@ -341,31 +181,7 @@ $vikunja-nav-selected-width: 0.4rem;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
cursor: pointer;
|
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 {
|
&:hover .favorite {
|
||||||
opacity: 1;
|
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 {
|
.menu-label {
|
||||||
.color-bubble {
|
.color-bubble {
|
||||||
width: 14px;
|
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 {
|
.menu-list {
|
||||||
li {
|
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 {
|
.logo {
|
||||||
display: block;
|
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 {
|
.top-menu {
|
||||||
margin-top: math.div($navbar-padding, 2);
|
margin-top: math.div($navbar-padding, 2);
|
||||||
|
|
||||||
|
@ -579,4 +366,14 @@ $vikunja-nav-selected-width: 0.4rem;
|
||||||
a.dropdown-item:hover {
|
a.dropdown-item:hover {
|
||||||
background: var(--dropdown-item-hover-background-color) !important;
|
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>
|
</style>
|
||||||
|
|
Reference in New Issue
Block a user