From c58df4b107c3300376a1d1de8f726bc411131204 Mon Sep 17 00:00:00 2001 From: hugo Date: Sun, 22 Feb 2026 07:47:36 +0100 Subject: [PATCH] fix: category titles in the menu outline --- .../MenuEditorView/MenuEditorView.tsx | 70 +++++++++++++------ .../components/MenuEditorView.test.tsx | 37 ++++++++++ 2 files changed, 86 insertions(+), 21 deletions(-) diff --git a/src/renderer/components/MenuEditorView/MenuEditorView.tsx b/src/renderer/components/MenuEditorView/MenuEditorView.tsx index 78e6ef0..0798b94 100644 --- a/src/renderer/components/MenuEditorView/MenuEditorView.tsx +++ b/src/renderer/components/MenuEditorView/MenuEditorView.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Tree } from 'react-arborist'; import { useI18n } from '../../i18n'; import { showToast } from '../Toast'; @@ -192,6 +192,7 @@ export const MenuEditorView: React.FC = () => { const [isLoadingPages, setIsLoadingPages] = useState(false); const [pagePosts, setPagePosts] = useState([]); const [categories, setCategories] = useState([]); + const [categoryTitlesByName, setCategoryTitlesByName] = useState>({}); const [isLoadingCategories, setIsLoadingCategories] = useState(false); const [editingEntryId, setEditingEntryId] = useState(null); const [editingEntryType, setEditingEntryType] = useState<'page' | 'category' | null>(null); @@ -202,12 +203,33 @@ export const MenuEditorView: React.FC = () => { const toolbarRef = useRef(null); const recentInsertTimerRef = useRef | null>(null); const autoExpandController = useMemo(() => createAutoExpandController(450), []); + const recalculateTreeHeight = useCallback((): void => { + const wrap = treeWrapRef.current; + const toolbar = toolbarRef.current; + if (!wrap) { + return; + } + + const wrapHeight = wrap.clientHeight; + const toolbarHeight = toolbar?.offsetHeight ?? 0; + const next = Math.max(120, wrapHeight - toolbarHeight - 8); + setTreeHeight(next); + }, []); useEffect(() => { const load = async () => { setIsLoading(true); try { - const menu = await window.electronAPI.menu.get(); + const [menu, projectMetadata] = await Promise.all([ + window.electronAPI.menu.get(), + window.electronAPI.meta.getProjectMetadata().catch(() => null), + ]); + + const metadataEntries = Object.entries(projectMetadata?.categoryMetadata ?? {}); + setCategoryTitlesByName(Object.fromEntries( + metadataEntries.map(([name, metadata]) => [name, metadata.title?.trim() || name]), + )); + setItems(menu.items); setSelectedId(menu.items[0]?.id ?? null); } catch (error) { @@ -280,34 +302,21 @@ export const MenuEditorView: React.FC = () => { }, [editingEntryId, editingEntryType, isLoadingPages, isLoadingCategories]); useEffect(() => { - const updateTreeHeight = (): void => { - const wrap = treeWrapRef.current; - const toolbar = toolbarRef.current; - if (!wrap) { - return; - } - - const wrapHeight = wrap.clientHeight; - const toolbarHeight = toolbar?.offsetHeight ?? 0; - const next = Math.max(120, wrapHeight - toolbarHeight - 8); - setTreeHeight(next); - }; - - updateTreeHeight(); + recalculateTreeHeight(); if (typeof ResizeObserver === 'undefined') { if (typeof window.addEventListener !== 'function') { return; } - window.addEventListener('resize', updateTreeHeight); + window.addEventListener('resize', recalculateTreeHeight); return () => { - window.removeEventListener('resize', updateTreeHeight); + window.removeEventListener('resize', recalculateTreeHeight); }; } const observer = new ResizeObserver(() => { - updateTreeHeight(); + recalculateTreeHeight(); }); if (treeWrapRef.current) { @@ -320,7 +329,21 @@ export const MenuEditorView: React.FC = () => { return () => { observer.disconnect(); }; - }, [editingEntryId]); + }, [editingEntryId, isLoading, recalculateTreeHeight]); + + useEffect(() => { + if (isLoading) { + return; + } + + const timer = setTimeout(() => { + recalculateTreeHeight(); + }, 0); + + return () => { + clearTimeout(timer); + }; + }, [isLoading, recalculateTreeHeight]); const selectedPath = useMemo(() => { if (!selectedId) { @@ -360,6 +383,9 @@ export const MenuEditorView: React.FC = () => { ]); const categoryMetadata = projectMetadata?.categoryMetadata ?? {}; + setCategoryTitlesByName(Object.fromEntries( + Object.entries(categoryMetadata).map(([name, metadata]) => [name, metadata.title?.trim() || name]), + )); setCategories(nextCategories.map((categoryName) => ({ name: categoryName, title: categoryMetadata[categoryName]?.title?.trim() || categoryName, @@ -818,7 +844,9 @@ export const MenuEditorView: React.FC = () => { inlinePlain /> ) - ) : node.data.title} + ) : node.data.kind === 'category-archive' && node.data.categoryName + ? (categoryTitlesByName[node.data.categoryName] || node.data.title) + : node.data.title} diff --git a/tests/renderer/components/MenuEditorView.test.tsx b/tests/renderer/components/MenuEditorView.test.tsx index 51b3356..88b904e 100644 --- a/tests/renderer/components/MenuEditorView.test.tsx +++ b/tests/renderer/components/MenuEditorView.test.tsx @@ -216,6 +216,33 @@ describe('MenuEditorView entry editor', () => { expect(await screen.findByPlaceholderText(/type a category name/i)).toBeInTheDocument(); }); + it('shows category metadata title in outline rows for category archives', async () => { + (window as any).electronAPI.menu.get = vi.fn().mockResolvedValue({ + items: [ + { + id: 'menu-home', + title: 'Home', + kind: 'home', + pageSlug: 'home', + children: [], + }, + { + id: 'cat-news', + title: 'news', + kind: 'category-archive', + categoryName: 'news', + children: [], + }, + ], + }); + + render(); + + await screen.findByText('Home'); + expect(screen.getByText('Newsroom')).toBeInTheDocument(); + expect(screen.queryByText(/^news$/i)).not.toBeInTheDocument(); + }); + it('disables delete action when Home entry is selected', async () => { render(); @@ -235,6 +262,16 @@ describe('MenuEditorView entry editor', () => { expect(screen.queryByText(/^submenu$/i)).not.toBeInTheDocument(); }); + it('recalculates tree viewport height after loading so outline uses available space', async () => { + const { container } = render(); + await screen.findByText('Home'); + await testUtils.wait(0); + + const tree = container.querySelector('[role="tree"]') as HTMLElement | null; + expect(tree).not.toBeNull(); + expect(tree?.style.height).not.toBe('460px'); + }); + it('uses category titles for suggestions and outline while saving category slug internally', async () => { render();