diff --git a/backend/api/navigation.ts b/backend/api/navigation.ts
index f0c09d9bf..3b89ab0f1 100644
--- a/backend/api/navigation.ts
+++ b/backend/api/navigation.ts
@@ -10,6 +10,11 @@ const navigationItem = {
icon: { type: 'string' },
target: { type: 'string' },
openInNewWindow: { type: 'boolean' },
+ expandByDefault: {
+ type: 'boolean',
+ description:
+ 'Whether a link holding children is shown expanded on load. Meaningless on any other item.'
+ },
visibilityGroups: {
type: 'array',
items: { type: 'string' },
diff --git a/backend/locales/en.json b/backend/locales/en.json
index b0f779732..cb00cee7a 100644
--- a/backend/locales/en.json
+++ b/backend/locales/en.json
@@ -2161,6 +2161,8 @@
"navEdit.clearItems": "Clear All Items",
"navEdit.editMenuItems": "Edit Menu Items",
"navEdit.emptyMenuText": "Click the Add button to add your first menu item.",
+ "navEdit.expandByDefault": "Expand by Default",
+ "navEdit.expandByDefaultHint": "Whether the submenu is already expanded when the page loads.",
"navEdit.groupsFailed": "Failed to load the list of groups. Per-group visibility cannot be changed.",
"navEdit.header": "Header",
"navEdit.icon": "Icon",
diff --git a/backend/models/navigation.ts b/backend/models/navigation.ts
index 8ffe72f02..65ad08a1a 100644
--- a/backend/models/navigation.ts
+++ b/backend/models/navigation.ts
@@ -18,6 +18,8 @@ export interface NavigationItem {
icon?: string
target?: string
openInNewWindow?: boolean
+ /** A link with children only: whether the sidebar shows its submenu already open. */
+ expandByDefault?: boolean
visibilityGroups?: string[]
children?: NavigationItem[]
}
diff --git a/frontend/src/components/NavEditOverlay.vue b/frontend/src/components/NavEditOverlay.vue
index 5e4026c28..77c363d2e 100644
--- a/frontend/src/components/NavEditOverlay.vue
+++ b/frontend/src/components/NavEditOverlay.vue
@@ -288,56 +288,81 @@
-
-
-
- {{ t(`navEdit.target`) }}
- {{ t(`navEdit.targetHint`) }}
-
-
-
-
-
-
- {{ t('common.actions.browse') }}
-
-
-
-
-
-
-
-
-
- {{ t(`navEdit.openInNewWindow`) }}
- {{ t(`navEdit.openInNewWindowHint`) }}
-
-
-
-
-
+
+
+
+
+
+ {{ t(`navEdit.expandByDefault`) }}
+ {{ t(`navEdit.expandByDefaultHint`) }}
+
+
+
+
+
+
+
+
+
+
+ {{ t(`navEdit.target`) }}
+ {{ t(`navEdit.targetHint`) }}
+
+
+
+
+
+
+ {{ t('common.actions.browse') }}
+
+
+
+
+
+
+
+
+
+ {{ t(`navEdit.openInNewWindow`) }}
+ {{ t(`navEdit.openInNewWindowHint`) }}
+
+
+
+
+
+
@@ -497,6 +522,7 @@ const state = reactive({
icon: '',
target: '/',
openInNewWindow: false,
+ expandByDefault: false,
visibilityGroups: [],
visibilityLimited: false,
isNested: false
@@ -534,6 +560,23 @@ const visibilityOptions = [
*/
const navId = computed(() => (pageStore.isHome ? pageStore.navigationId : pageStore.id))
+/**
+ * Whether the link being edited is a parent — one the sidebar draws as a submenu.
+ *
+ * Parenthood is not a property of the item: this list is flat, and `isNested` says an item belongs to
+ * whatever link comes before it, so what makes a link a parent is the item that FOLLOWS it. Which is why
+ * this is asked of the list rather than read off `state.current`, and why it answers again the moment a
+ * child is nested, unnested or dragged away.
+ */
+const currentIsParent = computed(() => {
+ const item = state.current
+ if (item?.type !== 'link' || item.isNested) {
+ return false
+ }
+ const idx = state.items.findIndex((it) => it.id === item.id)
+ return idx >= 0 && Boolean(state.items[idx + 1]?.isNested)
+})
+
const thumbStyle = {
right: '2px',
borderRadius: '5px',
@@ -595,6 +638,7 @@ function addItem(type) {
newItem.icon = DEFAULT_LINK_ICON
newItem.target = '/'
newItem.openInNewWindow = false
+ newItem.expandByDefault = false
newItem.isNested = false
break
}
@@ -659,6 +703,7 @@ async function loadMenuItems() {
'icon',
'target',
'openInNewWindow',
+ 'expandByDefault',
'visibilityGroups'
]),
visibilityLimited: item.visibilityGroups?.length > 0
@@ -702,7 +747,9 @@ function cleanMenuItem(item, isNested = false) {
return {
...pick(item, ['id', 'type', 'label', 'icon', 'target', 'openInNewWindow']),
visibilityGroups: item.visibilityLimited ? item.visibilityGroups : [],
- ...(!isNested && { children: [] })
+ // -> Only a top-level link can hold children, so only one of those can be a parent — a nested
+ // item carrying an expand flag would be a setting nothing ever reads
+ ...(!isNested && { children: [], expandByDefault: Boolean(item.expandByDefault) })
}
}
case 'separator': {
diff --git a/frontend/src/components/NavSidebar.vue b/frontend/src/components/NavSidebar.vue
index 36c902039..d64cc5ebe 100644
--- a/frontend/src/components/NavSidebar.vue
+++ b/frontend/src/components/NavSidebar.vue
@@ -15,12 +15,13 @@
>{{ item.label }}
+ by URL sees where they are in the tree -- or when the menu says this group opens that way
+ whatever is being read. Not `v-model`: after that first render the group is the reader's
+ to open and close, and a bound value would fight them -->
+ :default-opened="item.expandByDefault || containsCurrent(item)">