Define main menu as a js object structure
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ref, useTemplateRef, onMounted } from 'vue';
|
||||
import { onSwipe } from '@cloudron/pankow/gestures.js';
|
||||
import SideBarItem from './SideBarItem.vue';
|
||||
|
||||
defineProps({
|
||||
cloudronAvatarUrl: {
|
||||
@@ -12,34 +13,14 @@ defineProps({
|
||||
type: String,
|
||||
default: 'Cloudron',
|
||||
},
|
||||
VIEWS: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
activeView: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
profile: {
|
||||
type: Object,
|
||||
required: true,
|
||||
items: {
|
||||
type: Array
|
||||
}
|
||||
});
|
||||
|
||||
const SIDEBAR_GROUPS = Object.freeze({
|
||||
BACKUP: 'backup',
|
||||
EMAIL: 'email',
|
||||
SYSTEM: 'system',
|
||||
USERS: 'users'
|
||||
});
|
||||
|
||||
const sideBar = useTemplateRef('sideBar');
|
||||
const isVisible = ref(false);
|
||||
|
||||
const activeSidebarGroups = ref({});
|
||||
function onToggleGroup(group) {
|
||||
activeSidebarGroups.value[group] = !activeSidebarGroups.value[group];
|
||||
}
|
||||
const isCollapsed = ref(false);
|
||||
|
||||
function open() {
|
||||
isVisible.value = true;
|
||||
@@ -58,7 +39,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar" ref="sideBar" :class="{ 'sidebar-closed': !isVisible }">
|
||||
<div class="sidebar" ref="sideBar" :class="{ 'sidebar-closed': !isVisible, 'sidebar-collapsed': isCollapsed }">
|
||||
<Transition name="pankow-scale">
|
||||
<div class="sidebar-close-action" v-if="isVisible" @click="close()"><i class="fa-solid fa-xmark"></i></div>
|
||||
<div class="sidebar-open-action" v-else @click="open()"><i class="fa-solid fa-bars"></i></div>
|
||||
@@ -68,62 +49,16 @@ onMounted(() => {
|
||||
<img :src="cloudronAvatarUrl" :alt="cloudronName + ' icon'"/> {{ cloudronName }}
|
||||
</a>
|
||||
<div class="sidebar-list">
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.APPS || activeView === VIEWS.APP }" :href="VIEWS.APPS" @click="close()"><i class="fa fa-grip fa-fw"></i> {{ $t('apps.title') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.APPSTORE }" v-show="profile.isAtLeastAdmin" :href="VIEWS.APPSTORE" @click="close()"><i class="fa fa-cloud-download-alt fa-fw"></i> {{ $t('appstore.title') }}</a>
|
||||
<hr/>
|
||||
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.DOMAINS }" v-show="profile.isAtLeastAdmin" :href="VIEWS.DOMAINS" @click="close()"><i class="fa fa-globe fa-fw"></i> {{ $t('domains.title') }}</a>
|
||||
|
||||
<div class="sidebar-item" v-show="profile.isAtLeastUserManager" @click="onToggleGroup(SIDEBAR_GROUPS.USERS)"><i class="fa fa-users-gear fa-fw"></i> {{ $t('users.title') }} <i class="collapse fa-solid fa-angle-right" :class="{ expanded: activeSidebarGroups[SIDEBAR_GROUPS.USERS] }" style="margin-left: 6px;"></i></div>
|
||||
<Transition name="sidebar-item-group-animation">
|
||||
<div class="sidebar-item-group" v-if="activeSidebarGroups[SIDEBAR_GROUPS.USERS]">
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.USERS }" v-show="profile.isAtLeastUserManager" :href="VIEWS.USERS" @click="close()"><i class="fa fa-user fa-fw"></i> {{ $t('main.navbar.users') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.GROUPS }" v-show="profile.isAtLeastUserManager" :href="VIEWS.GROUPS" @click="close()"><i class="fa fa-users fa-fw"></i> {{ $t('main.navbar.groups') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.LDAP }" v-show="profile.isAtLeastAdmin" :href="VIEWS.LDAP" @click="close()"><i class="fa fa-fw fa-users-rays"></i> LDAP</a>
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.OPENID }" v-show="profile.isAtLeastAdmin" :href="VIEWS.OPENID" @click="close()"><i class="fa fa-fw fa-brands fa-openid"></i> OpenID</a>
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.USER_DIRECTORY_SETTINGS }" v-show="profile.isAtLeastAdmin" :href="VIEWS.USER_DIRECTORY_SETTINGS" @click="close()"><i class="fa fa-fw fa-screwdriver-wrench"></i> {{ $t('userdirectory.settings.title') }}</a>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<div class="sidebar-item" v-show="profile.isAtLeastMailManager" @click="onToggleGroup(SIDEBAR_GROUPS.EMAIL)"><i class="fa fa-envelope fa-fw"></i> {{ $t('emails.title') }} <i class="collapse fa-solid fa-angle-right" :class="{ expanded: activeSidebarGroups[SIDEBAR_GROUPS.EMAIL] }" style="margin-left: 6px;"></i></div>
|
||||
<Transition name="sidebar-item-group-animation">
|
||||
<div class="sidebar-item-group" v-if="activeSidebarGroups[SIDEBAR_GROUPS.EMAIL]">
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.EMAIL_DOMAINS || activeView === VIEWS.EMAIL_DOMAIN }" v-show="profile.isAtLeastAdmin" :href="VIEWS.EMAIL_DOMAINS" @click="close()"><i class="fa fa-fw fa-globe"></i> Domains</a>
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.MAILBOXES }" :href="VIEWS.MAILBOXES" @click="close()"><i class="fa fa-fw fa-inbox"></i> {{ $t('email.incoming.mailboxes.title') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.MAILINGLISTS }" :href="VIEWS.MAILINGLISTS" @click="close()"><i class="fa fa-fw-solid fa-envelopes-bulk"></i> {{ $t('email.incoming.mailinglists.title') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.EMAIL_EVENTLOG }" v-show="profile.isAtLeastAdmin" :href="VIEWS.EMAIL_EVENTLOG" @click="close()"><i class="fa fa-fw fa-list-alt"></i> {{ $t('emails.eventlog.title') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.EMAIL_SETTINGS }" v-show="profile.isAtLeastAdmin" :href="VIEWS.EMAIL_SETTINGS" @click="close()"><i class="fa fa-fw fa-screwdriver-wrench"></i> {{ $t('emails.settings.title') }}</a>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.NETWORK }" v-show="profile.isAtLeastAdmin" :href="VIEWS.NETWORK" @click="close()"><i class="fas fa-network-wired fa-fw"></i> {{ $t('network.title') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.VOLUMES }" v-show="profile.isAtLeastAdmin" :href="VIEWS.VOLUMES" @click="close()"><i class="fa fa-hdd fa-fw"></i> {{ $t('volumes.title') }}</a>
|
||||
|
||||
<div class="sidebar-item" v-show="profile.isAtLeastAdmin" @click="onToggleGroup(SIDEBAR_GROUPS.BACKUP)"><i class="fa fa-archive fa-fw"></i> {{ $t('backups.title') }} <i class="collapse fa-solid fa-angle-right" :class="{ expanded: activeSidebarGroups[SIDEBAR_GROUPS.BACKUP] }" style="margin-left: 6px;"></i></div>
|
||||
<Transition name="sidebar-item-group-animation">
|
||||
<div class="sidebar-item-group" v-if="activeSidebarGroups[SIDEBAR_GROUPS.BACKUP]">
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.BACKUP_SITES }" :href="VIEWS.BACKUP_SITES" @click="close()"><i class="fa fa-fw fa-hard-drive"></i> {{ $t('backups.sites.title') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.APP_ARCHIVE }" :href="VIEWS.APP_ARCHIVE" @click="close()"><i class="fa fa-fw fa-grip"></i> {{ $t('backups.archives.title') }}</a>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.APPEARANCE }" v-show="profile.isAtLeastAdmin" :href="VIEWS.APPEARANCE" @click="close()"><i class="fa fa-pen-ruler fa-fw"></i> {{ $t('appearance.title') }}</a>
|
||||
|
||||
<div class="sidebar-item" v-show="profile.isAtLeastAdmin" @click="onToggleGroup(SIDEBAR_GROUPS.SYSTEM)"><i class="fa fa-server fa-fw"></i> {{ $t('system.title') }} <i class="collapse fa-solid fa-angle-right" :class="{ expanded: activeSidebarGroups[SIDEBAR_GROUPS.SYSTEM] }" style="margin-left: 6px;"></i></div>
|
||||
<Transition name="sidebar-item-group-animation">
|
||||
<div class="sidebar-item-group" v-if="activeSidebarGroups[SIDEBAR_GROUPS.SYSTEM]">
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.DOCKER }" :href="VIEWS.DOCKER" @click="close()"><i class="fa-brands fa-fw fa-docker"></i> Docker</a>
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.SERVICES }" v-show="profile.isAtLeastAdmin" :href="VIEWS.SERVICES" @click="close()"><i class="fa fa-diagram-project fa-fw"></i> {{ $t('services.title') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.SYSTEM_EVENTLOG }" :href="VIEWS.SYSTEM_EVENTLOG" @click="close()"><i class="fa fa-list-alt fa-fw"></i> {{ $t('eventlog.title') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.SYSTEM_UPDATE }" :href="VIEWS.SYSTEM_UPDATE" @click="close()"><i class="fa fa-fw fa-square-up-right"></i> {{ $t('settings.updates.title') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.SYSTEM_SETTINGS }" :href="VIEWS.SYSTEM_SETTINGS" @click="close()"><i class="fa fa-fw fa-screwdriver-wrench"></i> {{ $t('system.settings.title') }}</a>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<hr v-show="profile.isAtLeastAdmin"/>
|
||||
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.SERVER }" v-show="profile.isAtLeastAdmin" :href="VIEWS.SERVER" @click="close()"><i class="fa fa-microchip fa-fw"></i> {{ $t('server.title') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: activeView === VIEWS.CLOUDRON_ACCOUNT }" v-show="profile.isAtLeastOwner" :href="VIEWS.CLOUDRON_ACCOUNT" @click="close()"><i class="fa fa-crown fa-fw"></i> {{ $t('settings.appstoreAccount.title') }}</a>
|
||||
<SideBarItem v-for="item in items" :key="item"
|
||||
:label="item.label"
|
||||
:icon="item.icon"
|
||||
:route="item.route"
|
||||
:visible="item.visible"
|
||||
:active="item.active"
|
||||
:separator="item.separator"
|
||||
:child-items="item.childItems"
|
||||
@close="close"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -199,77 +134,6 @@ onMounted(() => {
|
||||
scrollbar-color: var(--color-neutral-border) transparent;
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
display: block;
|
||||
color: var(--pankow-text-color);
|
||||
border-radius: 3px;
|
||||
padding: 10px 15px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 180ms ease-out;
|
||||
}
|
||||
|
||||
.sidebar-item i {
|
||||
opacity: 0.5;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.sidebar-item.active {
|
||||
color: var(--pankow-color-primary);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sidebar-item:hover {
|
||||
background-color: #e9ecef;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.sidebar-item:hover {
|
||||
background-color: var(--card-background);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-item.active i ,
|
||||
.sidebar-item:hover i {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar-item-group {
|
||||
padding-left: 20px;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
/* we need height to auto so we animate max-height. needs to be bigger than we need */
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.sidebar-item-group-animation-enter-active,
|
||||
.sidebar-item-group-animation-leave-active {
|
||||
transition: all 0.2s linear;
|
||||
}
|
||||
|
||||
.sidebar-item-group-animation-leave-to,
|
||||
.sidebar-item-group-animation-enter-from {
|
||||
transform: translateX(-100px);
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.1s ease-out;
|
||||
}
|
||||
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.1s ease-out;
|
||||
}
|
||||
|
||||
.slide-fade-enter-from,
|
||||
.slide-fade-leave-to {
|
||||
transform: translateX(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
import SideBarItem from './SideBarItem.vue';
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
},
|
||||
route: {
|
||||
type: String,
|
||||
},
|
||||
active: {
|
||||
type: [ Boolean, Function ],
|
||||
default: false,
|
||||
},
|
||||
separator: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
visible: {
|
||||
type: [ Boolean, Function ],
|
||||
default: true,
|
||||
},
|
||||
childItems: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
}
|
||||
});
|
||||
|
||||
const isActive = computed(() => {
|
||||
const active = props.active;
|
||||
return typeof active === 'function' ? active() : active ?? true;
|
||||
});
|
||||
|
||||
const isVisible = computed(() => {
|
||||
const visible = props.visible;
|
||||
return typeof visible === 'function' ? visible() : visible ?? true;
|
||||
});
|
||||
|
||||
const isExpanded = ref(false);
|
||||
|
||||
function close() {
|
||||
emit('close');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isVisible">
|
||||
<hr v-if="separator"/>
|
||||
<a v-else-if="!childItems?.length" class="sidebar-item" :class="{ active: isActive }" :href="route" @click="close()"><i :class="icon"></i> {{ label }}</a>
|
||||
<div v-else-if="childItems.length" class="sidebar-item" @click="isExpanded = !isExpanded"><i :class="icon"></i> {{ $t(label) }} <i class="collapse fa-solid fa-angle-right" :class="{ expanded: isExpanded }" style="margin-left: 6px;"></i></div>
|
||||
|
||||
<Transition name="sidebar-item-group-animation">
|
||||
<div class="sidebar-item-group" v-if="isExpanded">
|
||||
<SideBarItem v-for="item in childItems" :key="item"
|
||||
:label="item.label"
|
||||
:icon="item.icon"
|
||||
:route="item.route"
|
||||
:visible="item.visible"
|
||||
:active="item.active"
|
||||
:separator="item.separator"
|
||||
:child-items="item.childItems"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.sidebar-item {
|
||||
display: block;
|
||||
color: var(--pankow-text-color);
|
||||
border-radius: 3px;
|
||||
padding: 10px 15px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 180ms ease-out;
|
||||
}
|
||||
|
||||
.sidebar-item i {
|
||||
opacity: 0.5;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.sidebar-item.active {
|
||||
color: var(--pankow-color-primary);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sidebar-item:hover {
|
||||
background-color: #e9ecef;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.sidebar-item:hover {
|
||||
background-color: var(--card-background);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-item.active i ,
|
||||
.sidebar-item:hover i {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar-item-group {
|
||||
padding-left: 20px;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
/* we need height to auto so we animate max-height. needs to be bigger than we need */
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.sidebar-item-group-animation-enter-active,
|
||||
.sidebar-item-group-animation-leave-active {
|
||||
transition: all 0.2s linear;
|
||||
}
|
||||
|
||||
.sidebar-item-group-animation-leave-to,
|
||||
.sidebar-item-group-animation-enter-from {
|
||||
transform: translateX(-100px);
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.1s ease-out;
|
||||
}
|
||||
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.1s ease-out;
|
||||
}
|
||||
|
||||
.slide-fade-enter-from,
|
||||
.slide-fade-leave-to {
|
||||
transform: translateX(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user