Skip to content

Commit 7793e0d

Browse files
committed
feat: implement persistent state for sidebar group expansion using local storage
1 parent 53858a6 commit 7793e0d

2 files changed

Lines changed: 21 additions & 30 deletions

File tree

‎adminforth/spa/src/components/Sidebar.vue‎

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@
7070
<div v-else-if="item.type === 'heading'" class="flex items-center justify-left px-3 pt-3 pb-1 text-xs font-medium tracking-wide text-lightSidebarHeading dark:text-darkSidebarHeading
7171
">{{ item.label }}</div>
7272
<li v-else-if="item.children" class="af-sidebar-expand-container">
73-
<button @click="clickOnMenuItem(i)" type="button" class="af-sidebar-expand-button flex items-center w-full px-3 py-1.5 text-sm text-lightSidebarText rounded-default transition duration-75 group hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:text-darkSidebarText dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover"
74-
:class="opened.includes(i) ? 'af-sidebar-dropdown-expanded' : 'af-sidebar-dropdown-collapsed'"
73+
<button @click="clickOnMenuItem(item)" type="button" class="af-sidebar-expand-button flex items-center w-full px-3 py-1.5 text-sm text-lightSidebarText rounded-default transition duration-75 group hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:text-darkSidebarText dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover"
74+
:class="isGroupOpened(item) ? 'af-sidebar-dropdown-expanded' : 'af-sidebar-dropdown-collapsed'"
7575
:aria-controls="`dropdown-example${i}`"
7676
>
7777
<component v-if="item.icon" :is="getIcon(item.icon)" class="w-4 h-4 text-lightSidebarIcons group-hover:text-lightSidebarIconsHover transition duration-75 dark:group-hover:text-darkSidebarIconsHover dark:text-darkSidebarIcons" ></component>
@@ -90,12 +90,12 @@
9090
</span>
9191
</span>
9292

93-
<IconChevronDown :class="{'rotate-180': opened.includes(i) }" class="w-3 h-3 text-lightSidebarIcons dark:text-darkSidebarIcons transition-transform duration-200" aria-hidden="true" />
93+
<IconChevronDown :class="{'rotate-180': isGroupOpened(item) }" class="w-3 h-3 text-lightSidebarIcons dark:text-darkSidebarIcons transition-transform duration-200" aria-hidden="true" />
9494
</button>
9595

9696
<div class="grid transition-[grid-template-rows,opacity] duration-200 ease-in-out"
97-
:class="opened.includes(i) ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'"
98-
:inert="!opened.includes(i)"
97+
:class="isGroupOpened(item) ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'"
98+
:inert="!isGroupOpened(item)"
9999
>
100100
<div class="min-h-0 overflow-hidden">
101101
<ul :id="`dropdown-example${i}`" role="none" class="af-sidebar-dropdown mt-0.5 ml-[1.25rem] pl-2 border-l border-lightSidebarDevider dark:border-darkSidebarDevider space-y-0.5">
@@ -119,9 +119,9 @@
119119
">{{ item.label }}</div>
120120
<div v-else-if="item.type === 'heading' && !isSidebarHovering" class="opacity-0 w-1 h-8">{{ item.label }}</div>
121121
<li v-else-if="item.children" class="af-sidebar-expand-container">
122-
<button @click="clickOnMenuItem(i)" type="button" class="af-sidebar-expand-button relative flex items-center h-9 py-2 text-sm text-lightSidebarText rounded-default group hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:text-darkSidebarText dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover"
122+
<button @click="clickOnMenuItem(item)" type="button" class="af-sidebar-expand-button relative flex items-center h-9 py-2 text-sm text-lightSidebarText rounded-default group hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:text-darkSidebarText dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover"
123123
:class="[
124-
opened.includes(i) ? 'af-sidebar-dropdown-expanded' : 'af-sidebar-dropdown-collapsed',
124+
isGroupOpened(item) ? 'af-sidebar-dropdown-expanded' : 'af-sidebar-dropdown-collapsed',
125125
isSidebarIconOnly && !isSidebarHovering ? 'w-10 ml-1 px-3' : 'w-full px-3'
126126
]"
127127
:aria-controls="`dropdown-example${i}`"
@@ -155,10 +155,10 @@
155155
</span>
156156
</span>
157157

158-
<IconChevronDown :class="{'rotate-180': opened.includes(i) }" class="w-3 h-3 ml-2 absolute right-3 text-lightSidebarIcons dark:text-darkSidebarIcons transition-transform duration-200" aria-hidden="true" />
158+
<IconChevronDown :class="{'rotate-180': isGroupOpened(item) }" class="w-3 h-3 ml-2 absolute right-3 text-lightSidebarIcons dark:text-darkSidebarIcons transition-transform duration-200" aria-hidden="true" />
159159
</button>
160160

161-
<ul :id="`dropdown-example${i}`" role="none" class="af-sidebar-dropdown pt-0.5 space-y-0.5" :class="{ 'hidden': !opened.includes(i) }">
161+
<ul :id="`dropdown-example${i}`" role="none" class="af-sidebar-dropdown pt-0.5 space-y-0.5" :class="{ 'hidden': !isGroupOpened(item) }">
162162
<template v-for="(child, j) in item.children" :key="`menu-${i}-${j}`">
163163
<li class="af-sidebar-menu-link">
164164
<MenuLink :item="child" isChild="true" @click="$emit('hideSidebar')" :isSidebarIconOnly="isSidebarIconOnly" :isSidebarHovering="isSidebarHovering"/>
@@ -314,7 +314,7 @@ import MenuLink from './MenuLink.vue';
314314
import { IconPanelLeftClose, IconPanelLeftOpen, IconChevronDown, IconX, IconCircleAlert } from '@iconify-prerendered/vue-lucide';
315315
import { getIcon, verySimpleHash, loadFile, getCustomComponent } from '@/utils';
316316
import { Tooltip } from '@/afcl';
317-
import type { AnnouncementBadgeResponse } from '@/types/Common';
317+
import type { AnnouncementBadgeResponse, AdminForthConfigMenuItem } from '@/types/Common';
318318
import { useAdminforth } from '@/adminforth';
319319
import { useI18n } from 'vue-i18n';
320320
@@ -338,8 +338,9 @@ const emit = defineEmits<{
338338
339339
const coreStore = useCoreStore();
340340
341-
//create a ref to store the opened menu items with ts type;
342-
const opened = ref<(string|number)[]>([]);
341+
const OPENED_GROUPS_LS_KEY = 'afSidebarOpenedGroups';
342+
// expanded/collapsed state of groups toggled by user, by itemId, untouched groups fall back to item.open
343+
const openedGroups = ref<Record<string, boolean>>(JSON.parse(localStorage.getItem(OPENED_GROUPS_LS_KEY) || '{}'));
343344
const sidebarAside = ref(null);
344345
345346
const smQuery = window.matchMedia('(min-width: 640px)');
@@ -390,21 +391,14 @@ function toggleSidebar() {
390391
}, 100);
391392
}
392393
393-
function clickOnMenuItem(label: string | number) {
394-
if (opened.value.includes(label)) {
395-
opened.value = opened.value.filter((item) => item !== label);
396-
} else {
397-
opened.value.push(label);
398-
}
394+
function isGroupOpened(item: AdminForthConfigMenuItem) {
395+
return openedGroups.value[item.itemId!] ?? item.open;
399396
}
400397
401-
watch(()=>coreStore.menu, () => {
402-
coreStore.menu.forEach((item, i) => {
403-
if (item.open) {
404-
opened.value.push(i);
405-
};
406-
});
407-
})
398+
function clickOnMenuItem(item: AdminForthConfigMenuItem) {
399+
openedGroups.value[item.itemId!] = !isGroupOpened(item);
400+
localStorage.setItem(OPENED_GROUPS_LS_KEY, JSON.stringify(openedGroups.value));
401+
}
408402
409403
410404
@@ -455,11 +449,6 @@ onMounted(() => {
455449
isSidebarIconOnly.value = false;
456450
}
457451
458-
coreStore.menu.forEach((item, i) => {
459-
if (item.open) {
460-
opened.value.push(i);
461-
};
462-
});
463452
// Emit initial state
464453
emit('sidebarStateChange', { isSidebarIconOnly: isSidebarIconOnly.value, isSidebarHovering: isSidebarHovering.value });
465454
})

‎documentation/docs/tutorial/03-Customization/10-menuConfiguration.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ E.g. create group "Blog" with Items who link to resource "posts" and "categories
5757

5858
If it is rare Group you can make it `open: false` so it would not take extra space in menu, but admin users will be able to open it by clicking on the group name.
5959

60+
`open` sets only the default state: once a user expands or collapses a group, their choice is saved in the browser local storage and kept after page reloads.
61+
6062
## Adding menu items from plugins
6163

6264
Plugins can add top-level menu items without mutating the user-defined `config.menu`. This keeps the application menu owned by the app configuration, while plugins can contribute their own entries.

0 commit comments

Comments
 (0)