fix: tab title for templates

This commit is contained in:
2026-02-27 21:47:45 +01:00
parent e8c801e5db
commit 0ce6da57ce
7 changed files with 172 additions and 1 deletions

View File

@@ -1,6 +1,7 @@
import React, { useRef, useState, useEffect, useCallback } from 'react'; import React, { useRef, useState, useEffect, useCallback } from 'react';
import { useAppStore, Tab } from '../../store'; import { useAppStore, Tab } from '../../store';
import { parseGitDiffTabId } from '../../navigation/tabPolicy'; import { parseGitDiffTabId } from '../../navigation/tabPolicy';
import { BDS_EVENT_TEMPLATES_CHANGED, addWindowEventListener } from '../../utils';
import { useI18n } from '../../i18n'; import { useI18n } from '../../i18n';
import './TabBar.css'; import './TabBar.css';
@@ -14,6 +15,7 @@ const getTabTitle = (
chatTitles: Map<string, string>, chatTitles: Map<string, string>,
importDefTitles: Map<string, string>, importDefTitles: Map<string, string>,
commitTitles: Map<string, string>, commitTitles: Map<string, string>,
templateTitles: Map<string, string>,
tr: (key: string, vars?: Record<string, string | number>) => string, tr: (key: string, vars?: Record<string, string | number>) => string,
): string => { ): string => {
if (tab.type === 'git-diff') { if (tab.type === 'git-diff') {
@@ -89,6 +91,10 @@ const getTabTitle = (
return scriptTitles.get(tab.id) || tr('tabBar.scripts'); return scriptTitles.get(tab.id) || tr('tabBar.scripts');
} }
if (tab.type === 'templates') {
return templateTitles.get(tab.id) || tr('editor.untitled');
}
return tr('tabBar.unknown'); return tr('tabBar.unknown');
}; };
@@ -180,6 +186,12 @@ const getTabIcon = (tab: Tab): React.ReactNode => {
<path d="M20 3H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h7v2H8v2h8v-2h-3v-2h7a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zM5 14V5h14v9H5zm2-7.5L9.5 9 7 11.5l1.4 1.4L12.3 9 8.4 5.1 7 6.5zm6.5 5.5h4v-2h-4v2z"/> <path d="M20 3H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h7v2H8v2h8v-2h-3v-2h7a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zM5 14V5h14v9H5zm2-7.5L9.5 9 7 11.5l1.4 1.4L12.3 9 8.4 5.1 7 6.5zm6.5 5.5h4v-2h-4v2z"/>
</svg> </svg>
); );
case 'templates':
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M14 2H2v4h12V2zM3 3h10v2H3V3zm-1 5h5v7H2V8zm1 1v5h3V9H3zm5-1h6v3H8V8zm1 1v1h4V9H9zm0 3h6v3H9v-3zm1 1v1h4v-1h-4z"/>
</svg>
);
default: default:
return ( return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
@@ -230,6 +242,7 @@ export const TabBar: React.FC = () => {
const [chatTitles, setChatTitles] = useState<Map<string, string>>(new Map()); const [chatTitles, setChatTitles] = useState<Map<string, string>>(new Map());
const [importDefTitles, setImportDefTitles] = useState<Map<string, string>>(new Map()); const [importDefTitles, setImportDefTitles] = useState<Map<string, string>>(new Map());
const [commitTitles, setCommitTitles] = useState<Map<string, string>>(new Map()); const [commitTitles, setCommitTitles] = useState<Map<string, string>>(new Map());
const [templateTitles, setTemplateTitles] = useState<Map<string, string>>(new Map());
// Fetch post titles from database for post tabs // Fetch post titles from database for post tabs
useEffect(() => { useEffect(() => {
@@ -552,6 +565,94 @@ export const TabBar: React.FC = () => {
}; };
}, [tabs, activeProject, tr]); }, [tabs, activeProject, tr]);
// Fetch template titles for template tabs
useEffect(() => {
const templateTabs = tabs.filter((t) => t.type === 'templates');
const templateTabIds = new Set(templateTabs.map((t) => t.id));
setTemplateTitles((previous) => {
const next = new Map(previous);
let changed = false;
for (const id of Array.from(next.keys())) {
if (!templateTabIds.has(id)) {
next.delete(id);
changed = true;
}
}
return changed ? next : previous;
});
if (templateTabs.length === 0) {
return;
}
const fetchTemplateTitles = async () => {
const newTitles = new Map(templateTitles);
let changed = false;
for (const tab of templateTabs) {
if (templateTitles.has(tab.id)) {
continue;
}
try {
const tmpl = await window.electronAPI?.templates.get(tab.id);
if (tmpl) {
const title = tmpl.title || tr('editor.untitled');
if (newTitles.get(tab.id) !== title) {
newTitles.set(tab.id, title);
changed = true;
}
}
} catch (error) {
console.error(tr('tabBar.error.fetchTemplateTitle'), error);
}
}
if (changed) {
setTemplateTitles(newTitles);
}
};
void fetchTemplateTitles();
}, [tabs, tr]); // Note: intentionally not including templateTitles to avoid infinite loops
// Listen for template updates to refresh titles
useEffect(() => {
const handleTemplatesChanged = async () => {
const templateTabs = tabs.filter((t) => t.type === 'templates');
if (templateTabs.length === 0) {
return;
}
const updated = new Map(templateTitles);
let changed = false;
for (const tab of templateTabs) {
try {
const tmpl = await window.electronAPI?.templates.get(tab.id);
if (tmpl) {
const title = tmpl.title || tr('editor.untitled');
if (updated.get(tab.id) !== title) {
updated.set(tab.id, title);
changed = true;
}
}
} catch (error) {
console.error(tr('tabBar.error.fetchTemplateTitle'), error);
}
}
if (changed) {
setTemplateTitles(updated);
}
};
return addWindowEventListener(BDS_EVENT_TEMPLATES_CHANGED, handleTemplatesChanged);
}, [tabs, templateTitles, tr]);
// Check if arrows are needed based on scroll position // Check if arrows are needed based on scroll position
const updateArrowVisibility = useCallback(() => { const updateArrowVisibility = useCallback(() => {
const container = tabsContainerRef.current; const container = tabsContainerRef.current;
@@ -674,7 +775,7 @@ export const TabBar: React.FC = () => {
{tabs.map((tab) => { {tabs.map((tab) => {
const isActive = tab.id === activeTabId; const isActive = tab.id === activeTabId;
const isDirty = tab.type === 'post' && dirtyPosts.has(tab.id); const isDirty = tab.type === 'post' && dirtyPosts.has(tab.id);
const title = getTabTitle(tab, postTitles, media, scriptTitles, chatTitles, importDefTitles, commitTitles, tr); const title = getTabTitle(tab, postTitles, media, scriptTitles, chatTitles, importDefTitles, commitTitles, templateTitles, tr);
const icon = getTabIcon(tab); const icon = getTabIcon(tab);
return ( return (

View File

@@ -371,6 +371,7 @@
"tabBar.error.fetchChatTitle": "Chat-Titel konnte nicht geladen werden:", "tabBar.error.fetchChatTitle": "Chat-Titel konnte nicht geladen werden:",
"tabBar.error.fetchImportTitle": "Titel der Importdefinition konnte nicht geladen werden:", "tabBar.error.fetchImportTitle": "Titel der Importdefinition konnte nicht geladen werden:",
"tabBar.error.fetchScriptTitle": "Skript-Titel konnte nicht geladen werden:", "tabBar.error.fetchScriptTitle": "Skript-Titel konnte nicht geladen werden:",
"tabBar.error.fetchTemplateTitle": "Vorlagen-Titel konnte nicht geladen werden:",
"tabBar.error.fetchCommitTitle": "Commit-Titel konnten nicht geladen werden:", "tabBar.error.fetchCommitTitle": "Commit-Titel konnten nicht geladen werden:",
"metadataDiff.title": "Metadaten-Diff-Werkzeug", "metadataDiff.title": "Metadaten-Diff-Werkzeug",
"metadataDiff.description": "Vergleicht Beitragsmetadaten zwischen Datenbank und Markdown-Dateien. Behebt Abweichungen durch Bugs oder manuelle Änderungen.", "metadataDiff.description": "Vergleicht Beitragsmetadaten zwischen Datenbank und Markdown-Dateien. Behebt Abweichungen durch Bugs oder manuelle Änderungen.",

View File

@@ -371,6 +371,7 @@
"tabBar.error.fetchChatTitle": "Failed to fetch chat title:", "tabBar.error.fetchChatTitle": "Failed to fetch chat title:",
"tabBar.error.fetchImportTitle": "Failed to fetch import definition title:", "tabBar.error.fetchImportTitle": "Failed to fetch import definition title:",
"tabBar.error.fetchScriptTitle": "Failed to fetch script title:", "tabBar.error.fetchScriptTitle": "Failed to fetch script title:",
"tabBar.error.fetchTemplateTitle": "Failed to fetch template title:",
"tabBar.error.fetchCommitTitle": "Failed to fetch commit titles:", "tabBar.error.fetchCommitTitle": "Failed to fetch commit titles:",
"metadataDiff.title": "Metadata Diff Tool", "metadataDiff.title": "Metadata Diff Tool",
"metadataDiff.description": "Compare post metadata between database and markdown files. Fix inconsistencies caused by bugs or manual edits.", "metadataDiff.description": "Compare post metadata between database and markdown files. Fix inconsistencies caused by bugs or manual edits.",

View File

@@ -371,6 +371,7 @@
"tabBar.error.fetchChatTitle": "No se pudo cargar el título del chat:", "tabBar.error.fetchChatTitle": "No se pudo cargar el título del chat:",
"tabBar.error.fetchImportTitle": "No se pudo cargar el título de la definición de importación:", "tabBar.error.fetchImportTitle": "No se pudo cargar el título de la definición de importación:",
"tabBar.error.fetchScriptTitle": "No se pudo cargar el título del script:", "tabBar.error.fetchScriptTitle": "No se pudo cargar el título del script:",
"tabBar.error.fetchTemplateTitle": "No se pudo cargar el título de la plantilla:",
"tabBar.error.fetchCommitTitle": "No se pudieron cargar los títulos de los commits:", "tabBar.error.fetchCommitTitle": "No se pudieron cargar los títulos de los commits:",
"metadataDiff.title": "Herramienta diff de metadatos", "metadataDiff.title": "Herramienta diff de metadatos",
"metadataDiff.description": "Compara los metadatos de las entradas entre la base de datos y los archivos Markdown. Corrige inconsistencias causadas por errores o ediciones manuales.", "metadataDiff.description": "Compara los metadatos de las entradas entre la base de datos y los archivos Markdown. Corrige inconsistencias causadas por errores o ediciones manuales.",

View File

@@ -369,6 +369,7 @@
"tabBar.error.fetchChatTitle": "Impossible de charger le titre du chat :", "tabBar.error.fetchChatTitle": "Impossible de charger le titre du chat :",
"tabBar.error.fetchImportTitle": "Impossible de charger le titre de la définition dimport :", "tabBar.error.fetchImportTitle": "Impossible de charger le titre de la définition dimport :",
"tabBar.error.fetchScriptTitle": "Impossible de charger le titre du script :", "tabBar.error.fetchScriptTitle": "Impossible de charger le titre du script :",
"tabBar.error.fetchTemplateTitle": "Impossible de charger le titre du modèle :",
"tabBar.error.fetchCommitTitle": "Impossible de charger les titres des commits :", "tabBar.error.fetchCommitTitle": "Impossible de charger les titres des commits :",
"metadataDiff.title": "Outil de diff des métadonnées", "metadataDiff.title": "Outil de diff des métadonnées",
"metadataDiff.description": "Compare les métadonnées des articles entre la base de données et les fichiers Markdown. Corrige les incohérences causées par des bugs ou des modifications manuelles.", "metadataDiff.description": "Compare les métadonnées des articles entre la base de données et les fichiers Markdown. Corrige les incohérences causées par des bugs ou des modifications manuelles.",

View File

@@ -369,6 +369,7 @@
"tabBar.error.fetchChatTitle": "Impossibile caricare il titolo della chat:", "tabBar.error.fetchChatTitle": "Impossibile caricare il titolo della chat:",
"tabBar.error.fetchImportTitle": "Impossibile caricare il titolo della definizione di importazione:", "tabBar.error.fetchImportTitle": "Impossibile caricare il titolo della definizione di importazione:",
"tabBar.error.fetchScriptTitle": "Impossibile caricare il titolo dello script:", "tabBar.error.fetchScriptTitle": "Impossibile caricare il titolo dello script:",
"tabBar.error.fetchTemplateTitle": "Impossibile caricare il titolo del modello:",
"tabBar.error.fetchCommitTitle": "Impossibile caricare i titoli dei commit:", "tabBar.error.fetchCommitTitle": "Impossibile caricare i titoli dei commit:",
"metadataDiff.title": "Strumento diff metadati", "metadataDiff.title": "Strumento diff metadati",
"metadataDiff.description": "Confronta i metadati dei post tra database e file markdown. Correggi incongruenze causate da bug o modifiche manuali.", "metadataDiff.description": "Confronta i metadati dei post tra database e file markdown. Correggi incongruenze causate da bug o modifiche manuali.",

View File

@@ -73,6 +73,10 @@ describe('TabBar', () => {
...(window as any).electronAPI?.scripts, ...(window as any).electronAPI?.scripts,
get: vi.fn(), get: vi.fn(),
}, },
templates: {
...(window as any).electronAPI?.templates,
get: vi.fn(),
},
}; };
}); });
@@ -160,4 +164,65 @@ describe('TabBar', () => {
expect(await screen.findByText('Publish Macro')).toBeInTheDocument(); expect(await screen.findByText('Publish Macro')).toBeInTheDocument();
expect((window as any).electronAPI.scripts.get).toHaveBeenCalledWith('script-1'); expect((window as any).electronAPI.scripts.get).toHaveBeenCalledWith('script-1');
}); });
it('renders template title for template tab', async () => {
useAppStore.setState({
tabs: [{ type: 'templates', id: 'template-1', isTransient: false }],
activeTabId: 'template-1',
posts: [],
media: [],
dirtyPosts: new Set<string>(),
});
(window as any).electronAPI.templates.get = vi.fn().mockResolvedValue({
id: 'template-1',
title: 'Blog Post Layout',
});
render(<TabBar />);
expect(await screen.findByText('Blog Post Layout')).toBeInTheDocument();
expect((window as any).electronAPI.templates.get).toHaveBeenCalledWith('template-1');
});
it('updates template tab title when template changes', async () => {
useAppStore.setState({
tabs: [{ type: 'templates', id: 'template-1', isTransient: false }],
activeTabId: 'template-1',
posts: [],
media: [],
dirtyPosts: new Set<string>(),
});
(window as any).electronAPI.templates.get = vi.fn().mockResolvedValue({
id: 'template-1',
title: 'Old Title',
});
// Capture the bds:templates-changed listener
let templatesChangedHandler: (() => void) | null = null;
(window as any).addEventListener = vi.fn((event: string, handler: () => void) => {
if (event === 'bds:templates-changed') {
templatesChangedHandler = handler;
}
});
(window as any).removeEventListener = vi.fn();
render(<TabBar />);
expect(await screen.findByText('Old Title')).toBeInTheDocument();
// Now simulate the template being updated
(window as any).electronAPI.templates.get = vi.fn().mockResolvedValue({
id: 'template-1',
title: 'New Title',
});
// Trigger the templates-changed event
await act(async () => {
templatesChangedHandler?.();
});
expect(await screen.findByText('New Title')).toBeInTheDocument();
});
}); });