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(`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)">