feat: sitemap validattion
This commit is contained in:
@@ -304,6 +304,12 @@ const App: React.FC = () => {
|
||||
}) || (() => {})
|
||||
);
|
||||
|
||||
unsubscribers.push(
|
||||
window.electronAPI?.on('menu:validateSite', () => {
|
||||
openTab({ id: 'site-validation-report', type: 'site-validation', isTransient: true });
|
||||
}) || (() => {})
|
||||
);
|
||||
|
||||
unsubscribers.push(
|
||||
window.electronAPI?.on('menu:previewPost', async () => {
|
||||
try {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ImportAnalysisView } from '../ImportAnalysisView';
|
||||
import { MetadataDiffPanel } from '../MetadataDiffPanel';
|
||||
import { GitDiffView } from '../GitDiffView/GitDiffView';
|
||||
import { DocumentationView } from '../DocumentationView/DocumentationView';
|
||||
import { SiteValidationView } from '../SiteValidationView';
|
||||
import { AutoSaveManager, getContrastColor } from '../../utils';
|
||||
import { InsertModal } from '../InsertModal';
|
||||
import { AISuggestionsModal, AISuggestions } from '../AISuggestionsModal/AISuggestionsModal';
|
||||
@@ -1735,6 +1736,7 @@ export const Editor: React.FC = () => {
|
||||
const showMetadataDiff = activeTab?.type === 'metadata-diff';
|
||||
const showGitDiff = activeTab?.type === 'git-diff';
|
||||
const showDocumentation = activeTab?.type === 'documentation';
|
||||
const showSiteValidation = activeTab?.type === 'site-validation';
|
||||
|
||||
useEffect(() => {
|
||||
const activePostId = activeTab?.type === 'post' ? activeTab.id : null;
|
||||
@@ -1873,6 +1875,16 @@ export const Editor: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (showSiteValidation) {
|
||||
return (
|
||||
<div className="editor">
|
||||
<SiteValidationView />
|
||||
{renderErrorModal()}
|
||||
{renderConfirmDeleteModal()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show post editor if a post tab is active
|
||||
if (showPost && activeTabId) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
.site-validation-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background: var(--vscode-editor-background);
|
||||
color: var(--vscode-editor-foreground);
|
||||
}
|
||||
|
||||
.site-validation-summary h2 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.site-validation-summary p {
|
||||
margin: 0;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.site-validation-section h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.site-validation-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.site-validation-list-missing {
|
||||
color: var(--vscode-testing-iconPassed);
|
||||
}
|
||||
|
||||
.site-validation-list-extra {
|
||||
color: var(--vscode-notificationsErrorIcon-foreground);
|
||||
}
|
||||
|
||||
.site-validation-empty,
|
||||
.site-validation-status {
|
||||
margin: 0;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.site-validation-actions {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.site-validation-apply {
|
||||
background-color: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.site-validation-apply:hover:not(:disabled) {
|
||||
background-color: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.site-validation-apply:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { showToast } from '../Toast';
|
||||
import { useI18n } from '../../i18n';
|
||||
import './SiteValidationView.css';
|
||||
|
||||
type SiteValidationReport = {
|
||||
sitemapPath: string;
|
||||
sitemapChanged: boolean;
|
||||
missingUrlPaths: string[];
|
||||
extraUrlPaths: string[];
|
||||
expectedUrlCount: number;
|
||||
existingHtmlUrlCount: number;
|
||||
};
|
||||
|
||||
type SiteValidationApplyResult = {
|
||||
renderedUrlCount: number;
|
||||
deletedUrlCount: number;
|
||||
removedEmptyDirCount: number;
|
||||
};
|
||||
|
||||
export const SiteValidationView: React.FC = () => {
|
||||
const { t: tr } = useI18n();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
const [report, setReport] = useState<SiteValidationReport | null>(null);
|
||||
|
||||
const loadReport = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await window.electronAPI.blog.validateSite();
|
||||
setReport(result as SiteValidationReport);
|
||||
} catch (error) {
|
||||
console.error('Site validation failed:', error);
|
||||
showToast.error(tr('siteValidation.error.validate'));
|
||||
setReport(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadReport();
|
||||
}, []);
|
||||
|
||||
const canApply = useMemo(() => {
|
||||
if (!report) return false;
|
||||
return report.missingUrlPaths.length > 0 || report.extraUrlPaths.length > 0;
|
||||
}, [report]);
|
||||
|
||||
const handleApply = async () => {
|
||||
if (!report || !canApply) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsApplying(true);
|
||||
try {
|
||||
const result = await window.electronAPI.blog.applyValidation(report) as SiteValidationApplyResult;
|
||||
showToast.success(tr('siteValidation.toast.applySuccess', {
|
||||
rendered: result.renderedUrlCount,
|
||||
deleted: result.deletedUrlCount,
|
||||
}));
|
||||
await loadReport();
|
||||
} catch (error) {
|
||||
console.error('Applying site validation failed:', error);
|
||||
showToast.error(tr('siteValidation.error.apply'));
|
||||
} finally {
|
||||
setIsApplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="site-validation-view">
|
||||
<p className="site-validation-status">{tr('siteValidation.loading')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!report) {
|
||||
return (
|
||||
<div className="site-validation-view">
|
||||
<p className="site-validation-status">{tr('siteValidation.error.validate')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="site-validation-view">
|
||||
<div className="site-validation-summary">
|
||||
<h2>{tr('siteValidation.title')}</h2>
|
||||
<p>{tr('siteValidation.summary', {
|
||||
expected: report.expectedUrlCount,
|
||||
existing: report.existingHtmlUrlCount,
|
||||
missing: report.missingUrlPaths.length,
|
||||
extra: report.extraUrlPaths.length,
|
||||
})}</p>
|
||||
</div>
|
||||
|
||||
<section className="site-validation-section">
|
||||
<h3>{tr('siteValidation.missingTitle')}</h3>
|
||||
{report.missingUrlPaths.length === 0 ? (
|
||||
<p className="site-validation-empty">{tr('siteValidation.noneMissing')}</p>
|
||||
) : (
|
||||
<ul className="site-validation-list site-validation-list-missing">
|
||||
{report.missingUrlPaths.map((urlPath) => (
|
||||
<li key={`missing:${urlPath}`}>{urlPath}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="site-validation-section">
|
||||
<h3>{tr('siteValidation.extraTitle')}</h3>
|
||||
{report.extraUrlPaths.length === 0 ? (
|
||||
<p className="site-validation-empty">{tr('siteValidation.noneExtra')}</p>
|
||||
) : (
|
||||
<ul className="site-validation-list site-validation-list-extra">
|
||||
{report.extraUrlPaths.map((urlPath) => (
|
||||
<li key={`extra:${urlPath}`}>{urlPath}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<div className="site-validation-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="site-validation-apply"
|
||||
onClick={handleApply}
|
||||
disabled={!canApply || isApplying}
|
||||
>
|
||||
{isApplying ? tr('siteValidation.applying') : tr('siteValidation.apply')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
src/renderer/components/SiteValidationView/index.ts
Normal file
1
src/renderer/components/SiteValidationView/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { SiteValidationView } from './SiteValidationView';
|
||||
@@ -84,6 +84,10 @@ const getTabTitle = (
|
||||
return tr('docs.title');
|
||||
}
|
||||
|
||||
if (tab.type === 'site-validation') {
|
||||
return tr('siteValidation.tabTitle');
|
||||
}
|
||||
|
||||
return tr('tabBar.unknown');
|
||||
};
|
||||
|
||||
@@ -150,6 +154,12 @@ const getTabIcon = (tab: Tab): React.ReactNode => {
|
||||
<path d="M5 4h6v1H5V4zm0 2h6v1H5V6zm0 2h6v1H5V8zm0 2h4v1H5v-1z"/>
|
||||
</svg>
|
||||
);
|
||||
case 'site-validation':
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 1.5a6.5 6.5 0 1 0 6.5 6.5A6.5 6.5 0 0 0 8 1.5zm0 1a5.5 5.5 0 0 1 4.39 8.82l-.88-.88a.5.5 0 0 0-.7.7l.8.8A5.5 5.5 0 1 1 8 2.5zm2.35 3.15L7 9 5.65 7.65a.5.5 0 1 0-.7.7l1.7 1.7a.5.5 0 0 0 .7 0l3.7-3.7a.5.5 0 1 0-.7-.7z"/>
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
|
||||
@@ -25,3 +25,4 @@ export { ImportAnalysisView } from './ImportAnalysisView';
|
||||
export { InsertModal } from './InsertModal';
|
||||
export { WindowTitleBar } from './WindowTitleBar';
|
||||
export { DocumentationView } from './DocumentationView/DocumentationView';
|
||||
export { SiteValidationView } from './SiteValidationView';
|
||||
|
||||
@@ -28,6 +28,19 @@
|
||||
"app.previewOpenFailed": "Ausgewählte Beitragsvorschau konnte nicht geöffnet werden",
|
||||
"app.metadataDiff": "Metadaten-Diff",
|
||||
"app.importComplete": "Import abgeschlossen: {posts} Beiträge, {media} Mediendateien",
|
||||
"siteValidation.tabTitle": "Website-Validierung",
|
||||
"siteValidation.title": "Website validieren",
|
||||
"siteValidation.summary": "Erwartete URLs: {expected} · Vorhandene HTML-URLs: {existing} · Fehlend: {missing} · Überzählig: {extra}",
|
||||
"siteValidation.loading": "Website wird validiert...",
|
||||
"siteValidation.missingTitle": "Fehlende HTML-URLs (zum Rendern)",
|
||||
"siteValidation.extraTitle": "Nicht referenzierte HTML-URLs (zum Löschen)",
|
||||
"siteValidation.noneMissing": "Keine fehlenden URLs gefunden.",
|
||||
"siteValidation.noneExtra": "Keine überzähligen URLs gefunden.",
|
||||
"siteValidation.apply": "Anwenden",
|
||||
"siteValidation.applying": "Wird angewendet...",
|
||||
"siteValidation.error.validate": "Website-Validierung fehlgeschlagen",
|
||||
"siteValidation.error.apply": "Anwenden der Validierung fehlgeschlagen",
|
||||
"siteValidation.toast.applySuccess": "Validierung angewendet: {rendered} gerendert, {deleted} gelöscht",
|
||||
"settings.language.english": "Englisch",
|
||||
"settings.language.german": "Deutsch",
|
||||
"settings.language.french": "Französisch",
|
||||
|
||||
@@ -28,6 +28,19 @@
|
||||
"app.previewOpenFailed": "Failed to open selected post preview",
|
||||
"app.metadataDiff": "Metadata Diff",
|
||||
"app.importComplete": "Import complete: {posts} posts, {media} media files",
|
||||
"siteValidation.tabTitle": "Site Validation",
|
||||
"siteValidation.title": "Validate Site",
|
||||
"siteValidation.summary": "Expected URLs: {expected} · Existing HTML URLs: {existing} · Missing: {missing} · Extra: {extra}",
|
||||
"siteValidation.loading": "Validating site...",
|
||||
"siteValidation.missingTitle": "Missing HTML URLs (to render)",
|
||||
"siteValidation.extraTitle": "Unreferenced HTML URLs (to delete)",
|
||||
"siteValidation.noneMissing": "No missing URLs found.",
|
||||
"siteValidation.noneExtra": "No extra URLs found.",
|
||||
"siteValidation.apply": "Apply",
|
||||
"siteValidation.applying": "Applying...",
|
||||
"siteValidation.error.validate": "Site validation failed",
|
||||
"siteValidation.error.apply": "Applying validation failed",
|
||||
"siteValidation.toast.applySuccess": "Validation applied: {rendered} rendered, {deleted} deleted",
|
||||
"settings.language.english": "English",
|
||||
"settings.language.german": "German",
|
||||
"settings.language.french": "French",
|
||||
|
||||
@@ -28,6 +28,19 @@
|
||||
"app.previewOpenFailed": "No se pudo abrir la vista previa de la entrada seleccionada",
|
||||
"app.metadataDiff": "Diferencia de Metadatos",
|
||||
"app.importComplete": "Importación completada: {posts} entradas, {media} archivos multimedia",
|
||||
"siteValidation.tabTitle": "Validación del sitio",
|
||||
"siteValidation.title": "Validar sitio",
|
||||
"siteValidation.summary": "URLs esperadas: {expected} · URLs HTML existentes: {existing} · Faltantes: {missing} · Sobrantes: {extra}",
|
||||
"siteValidation.loading": "Validando el sitio...",
|
||||
"siteValidation.missingTitle": "URLs HTML faltantes (para renderizar)",
|
||||
"siteValidation.extraTitle": "URLs HTML no referenciadas (para eliminar)",
|
||||
"siteValidation.noneMissing": "No se encontraron URLs faltantes.",
|
||||
"siteValidation.noneExtra": "No se encontraron URLs sobrantes.",
|
||||
"siteValidation.apply": "Aplicar",
|
||||
"siteValidation.applying": "Aplicando...",
|
||||
"siteValidation.error.validate": "La validación del sitio falló",
|
||||
"siteValidation.error.apply": "La aplicación de la validación falló",
|
||||
"siteValidation.toast.applySuccess": "Validación aplicada: {rendered} renderizadas, {deleted} eliminadas",
|
||||
"settings.language.english": "Inglés",
|
||||
"settings.language.german": "Alemán",
|
||||
"settings.language.french": "Francés",
|
||||
|
||||
@@ -28,6 +28,19 @@
|
||||
"app.previewOpenFailed": "Impossible d’ouvrir l’aperçu de l’article sélectionné",
|
||||
"app.metadataDiff": "Diff Métadonnées",
|
||||
"app.importComplete": "Import terminé : {posts} articles, {media} fichiers média",
|
||||
"siteValidation.tabTitle": "Validation du site",
|
||||
"siteValidation.title": "Valider le site",
|
||||
"siteValidation.summary": "URLs attendues : {expected} · URLs HTML existantes : {existing} · Manquantes : {missing} · En trop : {extra}",
|
||||
"siteValidation.loading": "Validation du site en cours...",
|
||||
"siteValidation.missingTitle": "URLs HTML manquantes (à rendre)",
|
||||
"siteValidation.extraTitle": "URLs HTML non référencées (à supprimer)",
|
||||
"siteValidation.noneMissing": "Aucune URL manquante trouvée.",
|
||||
"siteValidation.noneExtra": "Aucune URL en trop trouvée.",
|
||||
"siteValidation.apply": "Appliquer",
|
||||
"siteValidation.applying": "Application en cours...",
|
||||
"siteValidation.error.validate": "Échec de la validation du site",
|
||||
"siteValidation.error.apply": "Échec de l’application de la validation",
|
||||
"siteValidation.toast.applySuccess": "Validation appliquée : {rendered} rendues, {deleted} supprimées",
|
||||
"settings.language.english": "Anglais",
|
||||
"settings.language.german": "Allemand",
|
||||
"settings.language.french": "Français",
|
||||
|
||||
@@ -28,6 +28,19 @@
|
||||
"app.previewOpenFailed": "Impossibile aprire l’anteprima del post selezionato",
|
||||
"app.metadataDiff": "Diff Metadati",
|
||||
"app.importComplete": "Import completato: {posts} post, {media} file multimediali",
|
||||
"siteValidation.tabTitle": "Validazione sito",
|
||||
"siteValidation.title": "Valida sito",
|
||||
"siteValidation.summary": "URL attesi: {expected} · URL HTML esistenti: {existing} · Mancanti: {missing} · Extra: {extra}",
|
||||
"siteValidation.loading": "Validazione del sito in corso...",
|
||||
"siteValidation.missingTitle": "URL HTML mancanti (da renderizzare)",
|
||||
"siteValidation.extraTitle": "URL HTML non referenziati (da eliminare)",
|
||||
"siteValidation.noneMissing": "Nessun URL mancante trovato.",
|
||||
"siteValidation.noneExtra": "Nessun URL extra trovato.",
|
||||
"siteValidation.apply": "Applica",
|
||||
"siteValidation.applying": "Applicazione in corso...",
|
||||
"siteValidation.error.validate": "Validazione del sito non riuscita",
|
||||
"siteValidation.error.apply": "Applicazione della validazione non riuscita",
|
||||
"siteValidation.toast.applySuccess": "Validazione applicata: {rendered} renderizzati, {deleted} eliminati",
|
||||
"settings.language.english": "Inglese",
|
||||
"settings.language.german": "Tedesco",
|
||||
"settings.language.french": "Francese",
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
const STORAGE_KEY = 'bds-app-state';
|
||||
|
||||
// Tab types
|
||||
export type TabType = 'post' | 'media' | 'settings' | 'style' | 'tags' | 'chat' | 'import' | 'metadata-diff' | 'git-diff' | 'documentation';
|
||||
export type TabType = 'post' | 'media' | 'settings' | 'style' | 'tags' | 'chat' | 'import' | 'metadata-diff' | 'git-diff' | 'documentation' | 'site-validation';
|
||||
|
||||
export interface Tab {
|
||||
type: TabType;
|
||||
|
||||
@@ -4,7 +4,15 @@ const TAB_STATE_PREFIX = 'bds-tabs-';
|
||||
|
||||
export const saveTabsForProject = (projectId: string, tabState: TabState): void => {
|
||||
try {
|
||||
localStorage.setItem(`${TAB_STATE_PREFIX}${projectId}`, JSON.stringify(tabState));
|
||||
const persistentTabs = tabState.tabs.filter((tab) => tab.isTransient !== true);
|
||||
const persistedState: TabState = {
|
||||
tabs: persistentTabs,
|
||||
activeTabId: persistentTabs.some((tab) => tab.id === tabState.activeTabId)
|
||||
? tabState.activeTabId
|
||||
: (persistentTabs[0]?.id ?? null),
|
||||
};
|
||||
|
||||
localStorage.setItem(`${TAB_STATE_PREFIX}${projectId}`, JSON.stringify(persistedState));
|
||||
} catch (error) {
|
||||
console.error('Failed to save tab state:', error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user