fix: category titles in the menu outline
This commit is contained in:
@@ -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 { Tree } from 'react-arborist';
|
||||||
import { useI18n } from '../../i18n';
|
import { useI18n } from '../../i18n';
|
||||||
import { showToast } from '../Toast';
|
import { showToast } from '../Toast';
|
||||||
@@ -192,6 +192,7 @@ export const MenuEditorView: React.FC = () => {
|
|||||||
const [isLoadingPages, setIsLoadingPages] = useState(false);
|
const [isLoadingPages, setIsLoadingPages] = useState(false);
|
||||||
const [pagePosts, setPagePosts] = useState<PostData[]>([]);
|
const [pagePosts, setPagePosts] = useState<PostData[]>([]);
|
||||||
const [categories, setCategories] = useState<CategoryOption[]>([]);
|
const [categories, setCategories] = useState<CategoryOption[]>([]);
|
||||||
|
const [categoryTitlesByName, setCategoryTitlesByName] = useState<Record<string, string>>({});
|
||||||
const [isLoadingCategories, setIsLoadingCategories] = useState(false);
|
const [isLoadingCategories, setIsLoadingCategories] = useState(false);
|
||||||
const [editingEntryId, setEditingEntryId] = useState<string | null>(null);
|
const [editingEntryId, setEditingEntryId] = useState<string | null>(null);
|
||||||
const [editingEntryType, setEditingEntryType] = useState<'page' | 'category' | null>(null);
|
const [editingEntryType, setEditingEntryType] = useState<'page' | 'category' | null>(null);
|
||||||
@@ -202,12 +203,33 @@ export const MenuEditorView: React.FC = () => {
|
|||||||
const toolbarRef = useRef<HTMLDivElement | null>(null);
|
const toolbarRef = useRef<HTMLDivElement | null>(null);
|
||||||
const recentInsertTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const recentInsertTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const autoExpandController = useMemo(() => createAutoExpandController(450), []);
|
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(() => {
|
useEffect(() => {
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
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);
|
setItems(menu.items);
|
||||||
setSelectedId(menu.items[0]?.id ?? null);
|
setSelectedId(menu.items[0]?.id ?? null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -280,34 +302,21 @@ export const MenuEditorView: React.FC = () => {
|
|||||||
}, [editingEntryId, editingEntryType, isLoadingPages, isLoadingCategories]);
|
}, [editingEntryId, editingEntryType, isLoadingPages, isLoadingCategories]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateTreeHeight = (): void => {
|
recalculateTreeHeight();
|
||||||
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();
|
|
||||||
|
|
||||||
if (typeof ResizeObserver === 'undefined') {
|
if (typeof ResizeObserver === 'undefined') {
|
||||||
if (typeof window.addEventListener !== 'function') {
|
if (typeof window.addEventListener !== 'function') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', updateTreeHeight);
|
window.addEventListener('resize', recalculateTreeHeight);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', updateTreeHeight);
|
window.removeEventListener('resize', recalculateTreeHeight);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
updateTreeHeight();
|
recalculateTreeHeight();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (treeWrapRef.current) {
|
if (treeWrapRef.current) {
|
||||||
@@ -320,7 +329,21 @@ export const MenuEditorView: React.FC = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
};
|
};
|
||||||
}, [editingEntryId]);
|
}, [editingEntryId, isLoading, recalculateTreeHeight]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
recalculateTreeHeight();
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [isLoading, recalculateTreeHeight]);
|
||||||
|
|
||||||
const selectedPath = useMemo(() => {
|
const selectedPath = useMemo(() => {
|
||||||
if (!selectedId) {
|
if (!selectedId) {
|
||||||
@@ -360,6 +383,9 @@ export const MenuEditorView: React.FC = () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const categoryMetadata = projectMetadata?.categoryMetadata ?? {};
|
const categoryMetadata = projectMetadata?.categoryMetadata ?? {};
|
||||||
|
setCategoryTitlesByName(Object.fromEntries(
|
||||||
|
Object.entries(categoryMetadata).map(([name, metadata]) => [name, metadata.title?.trim() || name]),
|
||||||
|
));
|
||||||
setCategories(nextCategories.map((categoryName) => ({
|
setCategories(nextCategories.map((categoryName) => ({
|
||||||
name: categoryName,
|
name: categoryName,
|
||||||
title: categoryMetadata[categoryName]?.title?.trim() || categoryName,
|
title: categoryMetadata[categoryName]?.title?.trim() || categoryName,
|
||||||
@@ -818,7 +844,9 @@ export const MenuEditorView: React.FC = () => {
|
|||||||
inlinePlain
|
inlinePlain
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : node.data.title}
|
) : node.data.kind === 'category-archive' && node.data.categoryName
|
||||||
|
? (categoryTitlesByName[node.data.categoryName] || node.data.title)
|
||||||
|
: node.data.title}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -216,6 +216,33 @@ describe('MenuEditorView entry editor', () => {
|
|||||||
expect(await screen.findByPlaceholderText(/type a category name/i)).toBeInTheDocument();
|
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(<MenuEditorView />);
|
||||||
|
|
||||||
|
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 () => {
|
it('disables delete action when Home entry is selected', async () => {
|
||||||
render(<MenuEditorView />);
|
render(<MenuEditorView />);
|
||||||
|
|
||||||
@@ -235,6 +262,16 @@ describe('MenuEditorView entry editor', () => {
|
|||||||
expect(screen.queryByText(/^submenu$/i)).not.toBeInTheDocument();
|
expect(screen.queryByText(/^submenu$/i)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('recalculates tree viewport height after loading so outline uses available space', async () => {
|
||||||
|
const { container } = render(<MenuEditorView />);
|
||||||
|
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 () => {
|
it('uses category titles for suggestions and outline while saving category slug internally', async () => {
|
||||||
render(<MenuEditorView />);
|
render(<MenuEditorView />);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user