feat: proper menu editor now
This commit is contained in:
@@ -5,14 +5,14 @@ import { randomUUID } from 'crypto';
|
|||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import { XMLBuilder, XMLParser } from 'fast-xml-parser';
|
import { XMLBuilder, XMLParser } from 'fast-xml-parser';
|
||||||
|
|
||||||
export type MenuItemKind = 'page' | 'submenu' | 'category-archive';
|
export type MenuItemKind = 'page' | 'submenu' | 'category-archive' | 'home';
|
||||||
|
|
||||||
const HOME_MENU_ID = 'menu-home';
|
const HOME_MENU_ID = 'menu-home';
|
||||||
|
|
||||||
const DEFAULT_HOME_ITEM: MenuItemData = {
|
const DEFAULT_HOME_ITEM: MenuItemData = {
|
||||||
id: HOME_MENU_ID,
|
id: HOME_MENU_ID,
|
||||||
title: 'Home',
|
title: 'Home',
|
||||||
kind: 'page',
|
kind: 'home',
|
||||||
pageId: undefined,
|
pageId: undefined,
|
||||||
pageSlug: 'home',
|
pageSlug: 'home',
|
||||||
categoryName: undefined,
|
categoryName: undefined,
|
||||||
@@ -79,6 +79,8 @@ function sanitizeMenuItem(input: unknown): MenuItemData {
|
|||||||
? 'submenu'
|
? 'submenu'
|
||||||
: candidate.kind === 'category-archive'
|
: candidate.kind === 'category-archive'
|
||||||
? 'category-archive'
|
? 'category-archive'
|
||||||
|
: candidate.kind === 'home'
|
||||||
|
? 'home'
|
||||||
: 'page';
|
: 'page';
|
||||||
const childrenSource = Array.isArray(candidate.children) ? candidate.children : [];
|
const childrenSource = Array.isArray(candidate.children) ? candidate.children : [];
|
||||||
const title = normalizeNonEmptyString(candidate.title) || 'Untitled';
|
const title = normalizeNonEmptyString(candidate.title) || 'Untitled';
|
||||||
@@ -88,7 +90,7 @@ function sanitizeMenuItem(input: unknown): MenuItemData {
|
|||||||
title,
|
title,
|
||||||
kind,
|
kind,
|
||||||
pageId: kind === 'page' ? normalizeNonEmptyString(candidate.pageId) : undefined,
|
pageId: kind === 'page' ? normalizeNonEmptyString(candidate.pageId) : undefined,
|
||||||
pageSlug: kind === 'page' ? normalizeNonEmptyString(candidate.pageSlug) : undefined,
|
pageSlug: kind === 'page' || kind === 'home' ? normalizeNonEmptyString(candidate.pageSlug) : undefined,
|
||||||
categoryName: kind === 'category-archive' ? normalizeNonEmptyString(candidate.categoryName) : undefined,
|
categoryName: kind === 'category-archive' ? normalizeNonEmptyString(candidate.categoryName) : undefined,
|
||||||
children: childrenSource.map((child) => sanitizeMenuItem(child)),
|
children: childrenSource.map((child) => sanitizeMenuItem(child)),
|
||||||
};
|
};
|
||||||
@@ -99,7 +101,7 @@ function normalizeHomeItem(item: MenuItemData): MenuItemData {
|
|||||||
...item,
|
...item,
|
||||||
id: HOME_MENU_ID,
|
id: HOME_MENU_ID,
|
||||||
title: 'Home',
|
title: 'Home',
|
||||||
kind: 'page',
|
kind: 'home',
|
||||||
pageId: undefined,
|
pageId: undefined,
|
||||||
pageSlug: 'home',
|
pageSlug: 'home',
|
||||||
categoryName: undefined,
|
categoryName: undefined,
|
||||||
@@ -167,6 +169,8 @@ function parseOutlineNode(node: OpmlOutlineNode): MenuItemData {
|
|||||||
? 'submenu'
|
? 'submenu'
|
||||||
: rawType === 'category-archive'
|
: rawType === 'category-archive'
|
||||||
? 'category-archive'
|
? 'category-archive'
|
||||||
|
: rawType === 'home'
|
||||||
|
? 'home'
|
||||||
: 'page';
|
: 'page';
|
||||||
const title = normalizeNonEmptyString(node['@_text']) || normalizeNonEmptyString(node['@_title']) || 'Untitled';
|
const title = normalizeNonEmptyString(node['@_text']) || normalizeNonEmptyString(node['@_title']) || 'Untitled';
|
||||||
|
|
||||||
@@ -175,7 +179,7 @@ function parseOutlineNode(node: OpmlOutlineNode): MenuItemData {
|
|||||||
title,
|
title,
|
||||||
kind,
|
kind,
|
||||||
pageId: kind === 'page' ? normalizeNonEmptyString(node['@_pageId']) : undefined,
|
pageId: kind === 'page' ? normalizeNonEmptyString(node['@_pageId']) : undefined,
|
||||||
pageSlug: kind === 'page' ? normalizeNonEmptyString(node['@_pageSlug']) : undefined,
|
pageSlug: kind === 'page' || kind === 'home' ? normalizeNonEmptyString(node['@_pageSlug']) : undefined,
|
||||||
categoryName: kind === 'category-archive' ? normalizeNonEmptyString(node['@_categoryName']) : undefined,
|
categoryName: kind === 'category-archive' ? normalizeNonEmptyString(node['@_categoryName']) : undefined,
|
||||||
children: normalizeOutlineNodes(node.outline).map((child) => parseOutlineNode(child)),
|
children: normalizeOutlineNodes(node.outline).map((child) => parseOutlineNode(child)),
|
||||||
};
|
};
|
||||||
@@ -192,7 +196,7 @@ function toOpmlOutlineNode(item: MenuItemData): OpmlOutlineNode {
|
|||||||
outlineNode['@_pageId'] = item.pageId;
|
outlineNode['@_pageId'] = item.pageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.kind === 'page' && item.pageSlug) {
|
if ((item.kind === 'page' || item.kind === 'home') && item.pageSlug) {
|
||||||
outlineNode['@_pageSlug'] = item.pageSlug;
|
outlineNode['@_pageSlug'] = item.pageSlug;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -422,7 +422,7 @@ export interface SiteValidationApplyResult {
|
|||||||
removedEmptyDirCount: number;
|
removedEmptyDirCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MenuItemKind = 'page' | 'submenu' | 'category-archive';
|
export type MenuItemKind = 'page' | 'submenu' | 'category-archive' | 'home';
|
||||||
|
|
||||||
export interface MenuItemData {
|
export interface MenuItemData {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -124,8 +124,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.menu-editor-row-kind {
|
.menu-editor-row-kind {
|
||||||
font-size: 0.75rem;
|
display: inline-flex;
|
||||||
opacity: 0.85;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1rem;
|
||||||
|
min-width: 1rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-editor-row-kind-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-editor-row-title {
|
.menu-editor-row-title {
|
||||||
|
|||||||
@@ -167,6 +167,22 @@ function createDraftEntry(kind: MenuItemData['kind'] = 'submenu'): MenuItemData
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderMenuKindIcon(kind: MenuItemData['kind']): React.ReactNode {
|
||||||
|
if (kind === 'home') {
|
||||||
|
return <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 2 2 7v7h4V9h4v5h4V7L8 2z" /></svg>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === 'page') {
|
||||||
|
return <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M3 2h7l3 3v9H3V2zm7 1.5V6h2.5L10 3.5z" /></svg>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === 'category-archive') {
|
||||||
|
return <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 3h12v3H2V3zm1 4h10v6H3V7zm2 1v1h6V8H5z" /></svg>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 3h12v2H2V3zm0 4h12v2H2V7zm0 4h12v2H2v-2z" /></svg>;
|
||||||
|
}
|
||||||
|
|
||||||
export const MenuEditorView: React.FC = () => {
|
export const MenuEditorView: React.FC = () => {
|
||||||
const { t: tr } = useI18n();
|
const { t: tr } = useI18n();
|
||||||
const [items, setItems] = useState<MenuItemData[]>([]);
|
const [items, setItems] = useState<MenuItemData[]>([]);
|
||||||
@@ -718,11 +734,30 @@ export const MenuEditorView: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<span className="menu-editor-row-kind">
|
<span className="menu-editor-row-kind">
|
||||||
{node.data.kind === 'page'
|
<span
|
||||||
? tr('menuEditor.type.page')
|
className="menu-editor-row-kind-icon"
|
||||||
: node.data.kind === 'category-archive'
|
data-kind={node.data.kind}
|
||||||
? tr('menuEditor.type.categoryArchive')
|
aria-label={
|
||||||
: tr('menuEditor.type.submenu')}
|
node.data.kind === 'home'
|
||||||
|
? tr('menuEditor.type.home')
|
||||||
|
: node.data.kind === 'page'
|
||||||
|
? tr('menuEditor.type.page')
|
||||||
|
: node.data.kind === 'category-archive'
|
||||||
|
? tr('menuEditor.type.categoryArchive')
|
||||||
|
: tr('menuEditor.type.submenu')
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
node.data.kind === 'home'
|
||||||
|
? tr('menuEditor.type.home')
|
||||||
|
: node.data.kind === 'page'
|
||||||
|
? tr('menuEditor.type.page')
|
||||||
|
: node.data.kind === 'category-archive'
|
||||||
|
? tr('menuEditor.type.categoryArchive')
|
||||||
|
: tr('menuEditor.type.submenu')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{renderMenuKindIcon(node.data.kind)}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className={`menu-editor-row-title ${editingEntryId === node.data.id ? 'is-editing' : ''}`}>
|
<span className={`menu-editor-row-title ${editingEntryId === node.data.id ? 'is-editing' : ''}`}>
|
||||||
{editingEntryId === node.data.id ? (
|
{editingEntryId === node.data.id ? (
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
"menuEditor.field.pageSlug": "Seiten-Slug",
|
"menuEditor.field.pageSlug": "Seiten-Slug",
|
||||||
"menuEditor.field.pageId": "Seiten-ID",
|
"menuEditor.field.pageId": "Seiten-ID",
|
||||||
"menuEditor.type.page": "Seite",
|
"menuEditor.type.page": "Seite",
|
||||||
|
"menuEditor.type.home": "Startseite",
|
||||||
"menuEditor.type.submenu": "Untermenü",
|
"menuEditor.type.submenu": "Untermenü",
|
||||||
"menuEditor.type.categoryArchive": "Kategorie-Archiv",
|
"menuEditor.type.categoryArchive": "Kategorie-Archiv",
|
||||||
"menuEditor.empty": "Noch keine Menüeinträge. Füge eine Seite oder ein Untermenü hinzu.",
|
"menuEditor.empty": "Noch keine Menüeinträge. Füge eine Seite oder ein Untermenü hinzu.",
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
"menuEditor.field.pageSlug": "Page Slug",
|
"menuEditor.field.pageSlug": "Page Slug",
|
||||||
"menuEditor.field.pageId": "Page ID",
|
"menuEditor.field.pageId": "Page ID",
|
||||||
"menuEditor.type.page": "Page",
|
"menuEditor.type.page": "Page",
|
||||||
|
"menuEditor.type.home": "Home",
|
||||||
"menuEditor.type.submenu": "Submenu",
|
"menuEditor.type.submenu": "Submenu",
|
||||||
"menuEditor.type.categoryArchive": "Category Archive",
|
"menuEditor.type.categoryArchive": "Category Archive",
|
||||||
"menuEditor.empty": "No menu entries yet. Add a page or submenu to start.",
|
"menuEditor.empty": "No menu entries yet. Add a page or submenu to start.",
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
"menuEditor.field.pageSlug": "Slug de página",
|
"menuEditor.field.pageSlug": "Slug de página",
|
||||||
"menuEditor.field.pageId": "ID de página",
|
"menuEditor.field.pageId": "ID de página",
|
||||||
"menuEditor.type.page": "Página",
|
"menuEditor.type.page": "Página",
|
||||||
|
"menuEditor.type.home": "Inicio",
|
||||||
"menuEditor.type.submenu": "Submenú",
|
"menuEditor.type.submenu": "Submenú",
|
||||||
"menuEditor.type.categoryArchive": "Archivo de categoría",
|
"menuEditor.type.categoryArchive": "Archivo de categoría",
|
||||||
"menuEditor.empty": "Aún no hay entradas de menú. Añade una página o un submenú para empezar.",
|
"menuEditor.empty": "Aún no hay entradas de menú. Añade una página o un submenú para empezar.",
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
"menuEditor.field.pageSlug": "Slug de page",
|
"menuEditor.field.pageSlug": "Slug de page",
|
||||||
"menuEditor.field.pageId": "ID de page",
|
"menuEditor.field.pageId": "ID de page",
|
||||||
"menuEditor.type.page": "Page",
|
"menuEditor.type.page": "Page",
|
||||||
|
"menuEditor.type.home": "Accueil",
|
||||||
"menuEditor.type.submenu": "Sous-menu",
|
"menuEditor.type.submenu": "Sous-menu",
|
||||||
"menuEditor.type.categoryArchive": "Archive de catégorie",
|
"menuEditor.type.categoryArchive": "Archive de catégorie",
|
||||||
"menuEditor.empty": "Aucune entrée de menu. Ajoutez une page ou un sous-menu pour commencer.",
|
"menuEditor.empty": "Aucune entrée de menu. Ajoutez une page ou un sous-menu pour commencer.",
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
"menuEditor.field.pageSlug": "Slug pagina",
|
"menuEditor.field.pageSlug": "Slug pagina",
|
||||||
"menuEditor.field.pageId": "ID pagina",
|
"menuEditor.field.pageId": "ID pagina",
|
||||||
"menuEditor.type.page": "Pagina",
|
"menuEditor.type.page": "Pagina",
|
||||||
|
"menuEditor.type.home": "Home",
|
||||||
"menuEditor.type.submenu": "Sottomenu",
|
"menuEditor.type.submenu": "Sottomenu",
|
||||||
"menuEditor.type.categoryArchive": "Archivio categoria",
|
"menuEditor.type.categoryArchive": "Archivio categoria",
|
||||||
"menuEditor.empty": "Nessuna voce menu. Aggiungi una pagina o un sottomenu per iniziare.",
|
"menuEditor.empty": "Nessuna voce menu. Aggiungi una pagina o un sottomenu per iniziare.",
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ describe('MenuEngine', () => {
|
|||||||
expect(result.items[0]).toMatchObject({
|
expect(result.items[0]).toMatchObject({
|
||||||
id: 'menu-home',
|
id: 'menu-home',
|
||||||
title: 'Home',
|
title: 'Home',
|
||||||
kind: 'page',
|
kind: 'home',
|
||||||
pageSlug: 'home',
|
pageSlug: 'home',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -72,7 +72,7 @@ describe('MenuEngine', () => {
|
|||||||
expect(result.items[0]).toMatchObject({
|
expect(result.items[0]).toMatchObject({
|
||||||
id: 'menu-home',
|
id: 'menu-home',
|
||||||
title: 'Home',
|
title: 'Home',
|
||||||
kind: 'page',
|
kind: 'home',
|
||||||
pageSlug: 'home',
|
pageSlug: 'home',
|
||||||
});
|
});
|
||||||
expect(result.items[1]).toMatchObject({
|
expect(result.items[1]).toMatchObject({
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ describe('MenuEditorView entry editor', () => {
|
|||||||
{
|
{
|
||||||
id: 'menu-home',
|
id: 'menu-home',
|
||||||
title: 'Home',
|
title: 'Home',
|
||||||
kind: 'page',
|
kind: 'home',
|
||||||
pageSlug: 'home',
|
pageSlug: 'home',
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
@@ -217,4 +217,15 @@ describe('MenuEditorView entry editor', () => {
|
|||||||
expect(deleteButton).toBeDisabled();
|
expect(deleteButton).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows type as icon only (no visible type text label)', async () => {
|
||||||
|
const { container } = render(<MenuEditorView />);
|
||||||
|
|
||||||
|
await screen.findByText('Home');
|
||||||
|
|
||||||
|
const icon = container.querySelector('.menu-editor-row-kind-icon[data-kind="home"]');
|
||||||
|
expect(icon).not.toBeNull();
|
||||||
|
expect(screen.queryByText(/^page$/i)).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(/^submenu$/i)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user