feat: additional metadata
This commit is contained in:
@@ -85,6 +85,9 @@ export class ImportAnalysisEngine {
|
|||||||
private currentProjectId: string = '';
|
private currentProjectId: string = '';
|
||||||
private turndown: TurndownService;
|
private turndown: TurndownService;
|
||||||
|
|
||||||
|
// Progress callback for reporting analysis steps
|
||||||
|
onProgress?: (step: string, detail?: string) => void;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.turndown = new TurndownService({
|
this.turndown = new TurndownService({
|
||||||
headingStyle: 'atx',
|
headingStyle: 'atx',
|
||||||
@@ -100,6 +103,8 @@ export class ImportAnalysisEngine {
|
|||||||
async analyzeWxr(wxrData: WxrData, sourceFile: string, uploadsFolder?: string): Promise<ImportAnalysisReport> {
|
async analyzeWxr(wxrData: WxrData, sourceFile: string, uploadsFolder?: string): Promise<ImportAnalysisReport> {
|
||||||
const db = getDatabase().getLocal();
|
const db = getDatabase().getLocal();
|
||||||
|
|
||||||
|
this.onProgress?.('Loading existing posts...');
|
||||||
|
|
||||||
// Fetch existing posts for this project
|
// Fetch existing posts for this project
|
||||||
const existingPosts = await db
|
const existingPosts = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -112,6 +117,8 @@ export class ImportAnalysisEngine {
|
|||||||
.where(eq(posts.projectId, this.currentProjectId))
|
.where(eq(posts.projectId, this.currentProjectId))
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
|
this.onProgress?.('Loading existing media...', `${existingPosts.length} posts in project`);
|
||||||
|
|
||||||
// Fetch existing media for this project
|
// Fetch existing media for this project
|
||||||
const existingMedia = await db
|
const existingMedia = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -123,6 +130,8 @@ export class ImportAnalysisEngine {
|
|||||||
.where(eq(media.projectId, this.currentProjectId))
|
.where(eq(media.projectId, this.currentProjectId))
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
|
this.onProgress?.('Loading existing tags...', `${existingMedia.length} media in project`);
|
||||||
|
|
||||||
// Fetch existing tags for this project
|
// Fetch existing tags for this project
|
||||||
const existingTags = await db
|
const existingTags = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -155,13 +164,22 @@ export class ImportAnalysisEngine {
|
|||||||
// Build tag set
|
// Build tag set
|
||||||
const existingTagNames = new Set(existingTags.map(t => t.name.toLowerCase()));
|
const existingTagNames = new Set(existingTags.map(t => t.name.toLowerCase()));
|
||||||
|
|
||||||
|
this.onProgress?.('Analyzing posts...', `${wxrData.posts.length} posts to analyze`);
|
||||||
|
|
||||||
// Analyze posts
|
// Analyze posts
|
||||||
const analyzedPosts = this.analyzePostItems(wxrData.posts, slugToPost, checksumToPost);
|
const analyzedPosts = this.analyzePostItems(wxrData.posts, slugToPost, checksumToPost);
|
||||||
|
|
||||||
|
this.onProgress?.('Analyzing pages...', `${wxrData.pages.length} pages to analyze`);
|
||||||
|
|
||||||
const analyzedPages = this.analyzePostItems(wxrData.pages, slugToPost, checksumToPost);
|
const analyzedPages = this.analyzePostItems(wxrData.pages, slugToPost, checksumToPost);
|
||||||
|
|
||||||
|
this.onProgress?.('Analyzing media files...', `${wxrData.media.length} media files to analyze`);
|
||||||
|
|
||||||
// Analyze media
|
// Analyze media
|
||||||
const analyzedMedia = await this.analyzeMediaItems(wxrData.media, nameToMedia, checksumToMedia, uploadsFolder);
|
const analyzedMedia = await this.analyzeMediaItems(wxrData.media, nameToMedia, checksumToMedia, uploadsFolder);
|
||||||
|
|
||||||
|
this.onProgress?.('Processing categories and tags...');
|
||||||
|
|
||||||
// Analyze categories
|
// Analyze categories
|
||||||
const analyzedCategories: AnalyzedCategory[] = wxrData.categories.map(cat => ({
|
const analyzedCategories: AnalyzedCategory[] = wxrData.categories.map(cat => ({
|
||||||
name: cat.name,
|
name: cat.name,
|
||||||
|
|||||||
@@ -747,7 +747,14 @@ export function registerIpcHandlers(): void {
|
|||||||
|
|
||||||
// ============ Import Analysis Handlers ============
|
// ============ Import Analysis Handlers ============
|
||||||
|
|
||||||
|
// Helper to emit progress events
|
||||||
|
const emitImportProgress = (step: string, detail?: string) => {
|
||||||
|
ipcMain.emit('forward-to-renderer', 'import:progress', { step, detail });
|
||||||
|
};
|
||||||
|
|
||||||
safeHandle('import:selectAndAnalyze', async (_, uploadsFolder?: string) => {
|
safeHandle('import:selectAndAnalyze', async (_, uploadsFolder?: string) => {
|
||||||
|
emitImportProgress('Selecting file...');
|
||||||
|
|
||||||
const result = await dialog.showOpenDialog({
|
const result = await dialog.showOpenDialog({
|
||||||
title: 'Select WordPress Export File (WXR)',
|
title: 'Select WordPress Export File (WXR)',
|
||||||
filters: [
|
filters: [
|
||||||
@@ -762,12 +769,18 @@ export function registerIpcHandlers(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filePath = result.filePaths[0];
|
const filePath = result.filePaths[0];
|
||||||
|
const fileName = filePath.split(/[/\\]/).pop() || filePath;
|
||||||
|
|
||||||
|
emitImportProgress('Parsing WXR file...', fileName);
|
||||||
|
|
||||||
const { WxrParser } = await import('../engine/WxrParser');
|
const { WxrParser } = await import('../engine/WxrParser');
|
||||||
const { ImportAnalysisEngine } = await import('../engine/ImportAnalysisEngine');
|
const { ImportAnalysisEngine } = await import('../engine/ImportAnalysisEngine');
|
||||||
|
|
||||||
const parser = new WxrParser();
|
const parser = new WxrParser();
|
||||||
const wxrData = await parser.parseFile(filePath);
|
const wxrData = await parser.parseFile(filePath);
|
||||||
|
|
||||||
|
emitImportProgress('Loading project data...', `Found ${wxrData.posts.length} posts, ${wxrData.media.length} media`);
|
||||||
|
|
||||||
const analysisEngine = new ImportAnalysisEngine();
|
const analysisEngine = new ImportAnalysisEngine();
|
||||||
const projectEngine = getProjectEngine();
|
const projectEngine = getProjectEngine();
|
||||||
const activeProject = await projectEngine.getActiveProject();
|
const activeProject = await projectEngine.getActiveProject();
|
||||||
@@ -775,16 +788,33 @@ export function registerIpcHandlers(): void {
|
|||||||
analysisEngine.setProjectContext(activeProject.id);
|
analysisEngine.setProjectContext(activeProject.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return analysisEngine.analyzeWxr(wxrData, filePath, uploadsFolder || undefined);
|
emitImportProgress('Analyzing posts...', `${wxrData.posts.length} posts`);
|
||||||
|
|
||||||
|
// Add progress callback to engine
|
||||||
|
analysisEngine.onProgress = (step: string, detail?: string) => {
|
||||||
|
emitImportProgress(step, detail);
|
||||||
|
};
|
||||||
|
|
||||||
|
const report = await analysisEngine.analyzeWxr(wxrData, filePath, uploadsFolder || undefined);
|
||||||
|
|
||||||
|
emitImportProgress('Analysis complete');
|
||||||
|
|
||||||
|
return report;
|
||||||
});
|
});
|
||||||
|
|
||||||
safeHandle('import:analyzeFile', async (_, filePath: string, uploadsFolder?: string) => {
|
safeHandle('import:analyzeFile', async (_, filePath: string, uploadsFolder?: string) => {
|
||||||
|
const fileName = filePath.split(/[/\\]/).pop() || filePath;
|
||||||
|
|
||||||
|
emitImportProgress('Parsing WXR file...', fileName);
|
||||||
|
|
||||||
const { WxrParser } = await import('../engine/WxrParser');
|
const { WxrParser } = await import('../engine/WxrParser');
|
||||||
const { ImportAnalysisEngine } = await import('../engine/ImportAnalysisEngine');
|
const { ImportAnalysisEngine } = await import('../engine/ImportAnalysisEngine');
|
||||||
|
|
||||||
const parser = new WxrParser();
|
const parser = new WxrParser();
|
||||||
const wxrData = await parser.parseFile(filePath);
|
const wxrData = await parser.parseFile(filePath);
|
||||||
|
|
||||||
|
emitImportProgress('Loading project data...', `Found ${wxrData.posts.length} posts, ${wxrData.media.length} media`);
|
||||||
|
|
||||||
const analysisEngine = new ImportAnalysisEngine();
|
const analysisEngine = new ImportAnalysisEngine();
|
||||||
const projectEngine = getProjectEngine();
|
const projectEngine = getProjectEngine();
|
||||||
const activeProject = await projectEngine.getActiveProject();
|
const activeProject = await projectEngine.getActiveProject();
|
||||||
@@ -792,7 +822,18 @@ export function registerIpcHandlers(): void {
|
|||||||
analysisEngine.setProjectContext(activeProject.id);
|
analysisEngine.setProjectContext(activeProject.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return analysisEngine.analyzeWxr(wxrData, filePath, uploadsFolder || undefined);
|
emitImportProgress('Analyzing posts...');
|
||||||
|
|
||||||
|
// Add progress callback to engine
|
||||||
|
analysisEngine.onProgress = (step: string, detail?: string) => {
|
||||||
|
emitImportProgress(step, detail);
|
||||||
|
};
|
||||||
|
|
||||||
|
const report = await analysisEngine.analyzeWxr(wxrData, filePath, uploadsFolder || undefined);
|
||||||
|
|
||||||
|
emitImportProgress('Analysis complete');
|
||||||
|
|
||||||
|
return report;
|
||||||
});
|
});
|
||||||
|
|
||||||
safeHandle('import:selectUploadsFolder', async () => {
|
safeHandle('import:selectUploadsFolder', async () => {
|
||||||
|
|||||||
@@ -155,6 +155,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
selectAndAnalyze: (uploadsFolder?: string) => ipcRenderer.invoke('import:selectAndAnalyze', uploadsFolder),
|
selectAndAnalyze: (uploadsFolder?: string) => ipcRenderer.invoke('import:selectAndAnalyze', uploadsFolder),
|
||||||
analyzeFile: (filePath: string, uploadsFolder?: string) => ipcRenderer.invoke('import:analyzeFile', filePath, uploadsFolder),
|
analyzeFile: (filePath: string, uploadsFolder?: string) => ipcRenderer.invoke('import:analyzeFile', filePath, uploadsFolder),
|
||||||
selectUploadsFolder: () => ipcRenderer.invoke('import:selectUploadsFolder'),
|
selectUploadsFolder: () => ipcRenderer.invoke('import:selectUploadsFolder'),
|
||||||
|
onProgress: (callback: (data: { step: string; detail?: string }) => void) => {
|
||||||
|
const subscription = (_event: Electron.IpcRendererEvent, data: { step: string; detail?: string }) => callback(data);
|
||||||
|
ipcRenderer.on('import:progress', subscription);
|
||||||
|
return () => ipcRenderer.removeListener('import:progress', subscription);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Import Definition CRUD
|
// Import Definition CRUD
|
||||||
@@ -340,6 +345,7 @@ export interface ElectronAPI {
|
|||||||
selectAndAnalyze: (uploadsFolder?: string) => Promise<unknown>;
|
selectAndAnalyze: (uploadsFolder?: string) => Promise<unknown>;
|
||||||
analyzeFile: (filePath: string, uploadsFolder?: string) => Promise<unknown>;
|
analyzeFile: (filePath: string, uploadsFolder?: string) => Promise<unknown>;
|
||||||
selectUploadsFolder: () => Promise<string | null>;
|
selectUploadsFolder: () => Promise<string | null>;
|
||||||
|
onProgress: (callback: (data: { step: string; detail?: string }) => void) => () => void;
|
||||||
};
|
};
|
||||||
importDefinitions: {
|
importDefinitions: {
|
||||||
create: (name?: string) => Promise<unknown>;
|
create: (name?: string) => Promise<unknown>;
|
||||||
|
|||||||
@@ -145,6 +145,23 @@
|
|||||||
border-top-color: var(--vscode-button-background);
|
border-top-color: var(--vscode-button-background);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 0.8s linear infinite;
|
animation: spin 0.8s linear infinite;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-progress {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-progress-step {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-progress-detail {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
@@ -594,6 +611,42 @@
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Post and Media rows with tooltip - enhanced hover state */
|
||||||
|
.import-detail-table tr.post-row-with-tooltip,
|
||||||
|
.import-detail-table tr.media-row-with-tooltip {
|
||||||
|
cursor: help;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-detail-table tr.post-row-with-tooltip:hover,
|
||||||
|
.import-detail-table tr.media-row-with-tooltip:hover {
|
||||||
|
background-color: var(--vscode-list-hoverBackground, rgba(255, 255, 255, 0.04));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Categories column styling */
|
||||||
|
.import-detail-table .categories-cell {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
max-width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MIME type column styling */
|
||||||
|
.import-detail-table .mime-type-cell {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
font-family: var(--vscode-editor-font-family, monospace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Post type column styling */
|
||||||
|
.import-detail-table .post-type-cell {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
font-family: var(--vscode-editor-font-family, monospace);
|
||||||
|
}
|
||||||
|
|
||||||
/* Empty state */
|
/* Empty state */
|
||||||
.import-empty-state {
|
.import-empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -33,7 +33,18 @@ interface MediaSection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AnalyzedPostItem {
|
interface AnalyzedPostItem {
|
||||||
wxrPost: { wpId: number; title: string; slug: string; status: string };
|
wxrPost: {
|
||||||
|
wpId: number;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
status: string;
|
||||||
|
excerpt: string;
|
||||||
|
pubDate: string | null;
|
||||||
|
creator: string;
|
||||||
|
postType: string;
|
||||||
|
categories: string[];
|
||||||
|
tags: string[];
|
||||||
|
};
|
||||||
status: string;
|
status: string;
|
||||||
contentHash: string;
|
contentHash: string;
|
||||||
markdownPreview: string;
|
markdownPreview: string;
|
||||||
@@ -41,7 +52,17 @@ interface AnalyzedPostItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AnalyzedMediaItem {
|
interface AnalyzedMediaItem {
|
||||||
wxrMedia: { wpId: number; title: string; filename: string; url: string; relativePath: string };
|
wxrMedia: {
|
||||||
|
wpId: number;
|
||||||
|
title: string;
|
||||||
|
filename: string;
|
||||||
|
url: string;
|
||||||
|
relativePath: string;
|
||||||
|
pubDate: string | null;
|
||||||
|
parentId: number;
|
||||||
|
mimeType: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
status: string;
|
status: string;
|
||||||
fileHash: string | null;
|
fileHash: string | null;
|
||||||
existingMedia?: { id: string; originalName: string };
|
existingMedia?: { id: string; originalName: string };
|
||||||
@@ -66,8 +87,19 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isLoadingDefinition, setIsLoadingDefinition] = useState(true);
|
const [isLoadingDefinition, setIsLoadingDefinition] = useState(true);
|
||||||
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({});
|
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({});
|
||||||
|
const [progressStep, setProgressStep] = useState<string>('');
|
||||||
|
const [progressDetail, setProgressDetail] = useState<string>('');
|
||||||
const nameInputRef = useRef<HTMLInputElement>(null);
|
const nameInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Subscribe to progress events
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = window.electronAPI?.import.onProgress(({ step, detail }) => {
|
||||||
|
setProgressStep(step);
|
||||||
|
setProgressDetail(detail || '');
|
||||||
|
});
|
||||||
|
return () => unsubscribe?.();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Save the current report to the definition
|
// Save the current report to the definition
|
||||||
const persistReport = useCallback(async (updatedReport: AnalysisReport) => {
|
const persistReport = useCallback(async (updatedReport: AnalysisReport) => {
|
||||||
await window.electronAPI?.importDefinitions.update(definitionId, {
|
await window.electronAPI?.importDefinitions.update(definitionId, {
|
||||||
@@ -167,6 +199,8 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
|||||||
const handleSelectAndAnalyze = useCallback(async () => {
|
const handleSelectAndAnalyze = useCallback(async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setReport(null);
|
setReport(null);
|
||||||
|
setProgressStep('');
|
||||||
|
setProgressDetail('');
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI?.import.selectAndAnalyze(uploadsFolder || undefined) as AnalysisReport | null;
|
const result = await window.electronAPI?.import.selectAndAnalyze(uploadsFolder || undefined) as AnalysisReport | null;
|
||||||
if (result) {
|
if (result) {
|
||||||
@@ -181,6 +215,8 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
|||||||
console.error('Import analysis failed:', error);
|
console.error('Import analysis failed:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
setProgressStep('');
|
||||||
|
setProgressDetail('');
|
||||||
}
|
}
|
||||||
}, [definitionId, uploadsFolder]);
|
}, [definitionId, uploadsFolder]);
|
||||||
|
|
||||||
@@ -240,7 +276,10 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
|||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="import-loading">
|
<div className="import-loading">
|
||||||
<div className="import-spinner" />
|
<div className="import-spinner" />
|
||||||
Analyzing WXR file...
|
<div className="import-progress">
|
||||||
|
<div className="import-progress-step">{progressStep || 'Analyzing WXR file...'}</div>
|
||||||
|
{progressDetail && <div className="import-progress-detail">{progressDetail}</div>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -276,12 +315,32 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Posts section - only items with postType 'post' */}
|
||||||
|
{(() => {
|
||||||
|
const postsOnly = report.posts.items.filter(i => i.wxrPost.postType === 'post');
|
||||||
|
return postsOnly.length > 0 && (
|
||||||
<PostDetailSection
|
<PostDetailSection
|
||||||
title={`Posts (${report.posts.total})`}
|
title={`Posts (${postsOnly.length})`}
|
||||||
items={report.posts.items}
|
items={postsOnly}
|
||||||
expanded={expandedSections['posts'] ?? false}
|
expanded={expandedSections['posts'] ?? false}
|
||||||
onToggle={() => toggleSection('posts')}
|
onToggle={() => toggleSection('posts')}
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{/* Other post types section */}
|
||||||
|
{(() => {
|
||||||
|
const otherPosts = report.posts.items.filter(i => i.wxrPost.postType !== 'post');
|
||||||
|
return otherPosts.length > 0 && (
|
||||||
|
<PostDetailSection
|
||||||
|
title={`Other (${otherPosts.length})`}
|
||||||
|
items={otherPosts}
|
||||||
|
expanded={expandedSections['other'] ?? false}
|
||||||
|
onToggle={() => toggleSection('other')}
|
||||||
|
showType
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{report.pages.total > 0 && (
|
{report.pages.total > 0 && (
|
||||||
<PostDetailSection
|
<PostDetailSection
|
||||||
@@ -336,19 +395,56 @@ const SiteInfoCard: React.FC<{ site: AnalysisReport['site']; sourceFile: string
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const StatCards: React.FC<{ report: AnalysisReport }> = ({ report }) => (
|
const StatCards: React.FC<{ report: AnalysisReport }> = ({ report }) => {
|
||||||
|
// Split posts by type
|
||||||
|
const postsOnly = report.posts.items.filter(i => i.wxrPost.postType === 'post');
|
||||||
|
const otherPosts = report.posts.items.filter(i => i.wxrPost.postType !== 'post');
|
||||||
|
|
||||||
|
const postsStats = {
|
||||||
|
total: postsOnly.length,
|
||||||
|
new: postsOnly.filter(i => i.status === 'new').length,
|
||||||
|
updates: postsOnly.filter(i => i.status === 'update').length,
|
||||||
|
conflicts: postsOnly.filter(i => i.status === 'conflict').length,
|
||||||
|
contentDuplicates: postsOnly.filter(i => i.status === 'content-duplicate').length,
|
||||||
|
};
|
||||||
|
|
||||||
|
const otherStats = {
|
||||||
|
total: otherPosts.length,
|
||||||
|
new: otherPosts.filter(i => i.status === 'new').length,
|
||||||
|
updates: otherPosts.filter(i => i.status === 'update').length,
|
||||||
|
conflicts: otherPosts.filter(i => i.status === 'conflict').length,
|
||||||
|
contentDuplicates: otherPosts.filter(i => i.status === 'content-duplicate').length,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get unique other post types for display
|
||||||
|
const otherTypes = [...new Set(otherPosts.map(i => i.wxrPost.postType))].join(', ');
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="import-stat-cards">
|
<div className="import-stat-cards">
|
||||||
<div className="import-stat-card">
|
<div className="import-stat-card">
|
||||||
<h3>Posts</h3>
|
<h3>Posts</h3>
|
||||||
<div className="import-stat-number">{report.posts.total}</div>
|
<div className="import-stat-number">{postsStats.total}</div>
|
||||||
<div className="import-stat-breakdown">
|
<div className="import-stat-breakdown">
|
||||||
{report.posts.new > 0 && <span className="import-stat-tag stat-new">{report.posts.new} new</span>}
|
{postsStats.new > 0 && <span className="import-stat-tag stat-new">{postsStats.new} new</span>}
|
||||||
{report.posts.updates > 0 && <span className="import-stat-tag stat-update">{report.posts.updates} update</span>}
|
{postsStats.updates > 0 && <span className="import-stat-tag stat-update">{postsStats.updates} update</span>}
|
||||||
{report.posts.conflicts > 0 && <span className="import-stat-tag stat-conflict">{report.posts.conflicts} conflict</span>}
|
{postsStats.conflicts > 0 && <span className="import-stat-tag stat-conflict">{postsStats.conflicts} conflict</span>}
|
||||||
{report.posts.contentDuplicates > 0 && <span className="import-stat-tag stat-duplicate">{report.posts.contentDuplicates} duplicate</span>}
|
{postsStats.contentDuplicates > 0 && <span className="import-stat-tag stat-duplicate">{postsStats.contentDuplicates} duplicate</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{otherStats.total > 0 && (
|
||||||
|
<div className="import-stat-card">
|
||||||
|
<h3 title={otherTypes}>Other</h3>
|
||||||
|
<div className="import-stat-number">{otherStats.total}</div>
|
||||||
|
<div className="import-stat-breakdown">
|
||||||
|
{otherStats.new > 0 && <span className="import-stat-tag stat-new">{otherStats.new} new</span>}
|
||||||
|
{otherStats.updates > 0 && <span className="import-stat-tag stat-update">{otherStats.updates} update</span>}
|
||||||
|
{otherStats.conflicts > 0 && <span className="import-stat-tag stat-conflict">{otherStats.conflicts} conflict</span>}
|
||||||
|
{otherStats.contentDuplicates > 0 && <span className="import-stat-tag stat-duplicate">{otherStats.contentDuplicates} duplicate</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="import-stat-card">
|
<div className="import-stat-card">
|
||||||
<h3>Pages</h3>
|
<h3>Pages</h3>
|
||||||
<div className="import-stat-number">{report.pages.total}</div>
|
<div className="import-stat-number">{report.pages.total}</div>
|
||||||
@@ -404,7 +500,50 @@ const StatCards: React.FC<{ report: AnalysisReport }> = ({ report }) => (
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to format post metadata for tooltip
|
||||||
|
function formatPostTooltip(wxrPost: AnalyzedPostItem['wxrPost']): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(`WordPress ID: ${wxrPost.wpId}`);
|
||||||
|
lines.push(`Type: ${wxrPost.postType}`);
|
||||||
|
lines.push(`Author: ${wxrPost.creator || 'Unknown'}`);
|
||||||
|
if (wxrPost.pubDate) {
|
||||||
|
lines.push(`Published: ${new Date(wxrPost.pubDate).toLocaleDateString()}`);
|
||||||
|
}
|
||||||
|
if (wxrPost.excerpt) {
|
||||||
|
const shortExcerpt = wxrPost.excerpt.length > 100
|
||||||
|
? wxrPost.excerpt.substring(0, 100) + '...'
|
||||||
|
: wxrPost.excerpt;
|
||||||
|
lines.push(`Excerpt: ${shortExcerpt}`);
|
||||||
|
}
|
||||||
|
if (wxrPost.tags.length > 0) {
|
||||||
|
lines.push(`Tags: ${wxrPost.tags.join(', ')}`);
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to format media metadata for tooltip
|
||||||
|
function formatMediaTooltip(wxrMedia: AnalyzedMediaItem['wxrMedia']): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(`WordPress ID: ${wxrMedia.wpId}`);
|
||||||
|
lines.push(`MIME Type: ${wxrMedia.mimeType || 'Unknown'}`);
|
||||||
|
if (wxrMedia.pubDate) {
|
||||||
|
lines.push(`Uploaded: ${new Date(wxrMedia.pubDate).toLocaleDateString()}`);
|
||||||
|
}
|
||||||
|
if (wxrMedia.parentId) {
|
||||||
|
lines.push(`Parent Post ID: ${wxrMedia.parentId}`);
|
||||||
|
}
|
||||||
|
lines.push(`URL: ${wxrMedia.url}`);
|
||||||
|
if (wxrMedia.description) {
|
||||||
|
const shortDesc = wxrMedia.description.length > 100
|
||||||
|
? wxrMedia.description.substring(0, 100) + '...'
|
||||||
|
: wxrMedia.description;
|
||||||
|
lines.push(`Description: ${shortDesc}`);
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
const ConflictsSection: React.FC<{
|
const ConflictsSection: React.FC<{
|
||||||
title: string;
|
title: string;
|
||||||
@@ -423,14 +562,20 @@ const ConflictsSection: React.FC<{
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Slug</th>
|
<th>Slug</th>
|
||||||
<th>WXR Title</th>
|
<th>WXR Title</th>
|
||||||
|
<th>Categories</th>
|
||||||
<th>Existing Title</th>
|
<th>Existing Title</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{items.map((item, idx) => (
|
{items.map((item, idx) => (
|
||||||
<tr key={idx}>
|
<tr key={idx} className="post-row-with-tooltip" title={formatPostTooltip(item.wxrPost)}>
|
||||||
<td className="slug-cell">{item.wxrPost.slug}</td>
|
<td className="slug-cell">{item.wxrPost.slug}</td>
|
||||||
<td>{item.wxrPost.title}</td>
|
<td>{item.wxrPost.title}</td>
|
||||||
|
<td className="categories-cell">
|
||||||
|
{item.wxrPost.categories.length > 0
|
||||||
|
? item.wxrPost.categories.join(', ')
|
||||||
|
: '--'}
|
||||||
|
</td>
|
||||||
<td className="existing-match">{item.existingPost?.title || '--'}</td>
|
<td className="existing-match">{item.existingPost?.title || '--'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -445,7 +590,8 @@ const PostDetailSection: React.FC<{
|
|||||||
items: AnalyzedPostItem[];
|
items: AnalyzedPostItem[];
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
}> = ({ title, items, expanded, onToggle }) => (
|
showType?: boolean;
|
||||||
|
}> = ({ title, items, expanded, onToggle, showType }) => (
|
||||||
<div className="import-detail-section">
|
<div className="import-detail-section">
|
||||||
<h3 onClick={onToggle}>
|
<h3 onClick={onToggle}>
|
||||||
<span className={`toggle-icon ${expanded ? 'open' : ''}`}>▶</span>
|
<span className={`toggle-icon ${expanded ? 'open' : ''}`}>▶</span>
|
||||||
@@ -456,18 +602,26 @@ const PostDetailSection: React.FC<{
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
{showType && <th>Type</th>}
|
||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
<th>Slug</th>
|
<th>Slug</th>
|
||||||
|
<th>Categories</th>
|
||||||
<th>WP Status</th>
|
<th>WP Status</th>
|
||||||
<th>Existing Match</th>
|
<th>Existing Match</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{items.map((item, idx) => (
|
{items.map((item, idx) => (
|
||||||
<tr key={idx}>
|
<tr key={idx} className="post-row-with-tooltip" title={formatPostTooltip(item.wxrPost)}>
|
||||||
<td><span className={`status-badge ${item.status}`}>{item.status}</span></td>
|
<td><span className={`status-badge ${item.status}`}>{item.status}</span></td>
|
||||||
|
{showType && <td className="post-type-cell">{item.wxrPost.postType}</td>}
|
||||||
<td>{item.wxrPost.title}</td>
|
<td>{item.wxrPost.title}</td>
|
||||||
<td className="slug-cell">{item.wxrPost.slug}</td>
|
<td className="slug-cell">{item.wxrPost.slug}</td>
|
||||||
|
<td className="categories-cell">
|
||||||
|
{item.wxrPost.categories.length > 0
|
||||||
|
? item.wxrPost.categories.join(', ')
|
||||||
|
: '--'}
|
||||||
|
</td>
|
||||||
<td>{item.wxrPost.status}</td>
|
<td>{item.wxrPost.status}</td>
|
||||||
<td className="existing-match">{item.existingPost?.title || '--'}</td>
|
<td className="existing-match">{item.existingPost?.title || '--'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -495,15 +649,17 @@ const MediaDetailSection: React.FC<{
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Filename</th>
|
<th>Filename</th>
|
||||||
|
<th>Type</th>
|
||||||
<th>Path</th>
|
<th>Path</th>
|
||||||
<th>Existing Match</th>
|
<th>Existing Match</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{items.map((item, idx) => (
|
{items.map((item, idx) => (
|
||||||
<tr key={idx}>
|
<tr key={idx} className="media-row-with-tooltip" title={formatMediaTooltip(item.wxrMedia)}>
|
||||||
<td><span className={`status-badge ${item.status}`}>{item.status}</span></td>
|
<td><span className={`status-badge ${item.status}`}>{item.status}</span></td>
|
||||||
<td>{item.wxrMedia.filename}</td>
|
<td>{item.wxrMedia.filename}</td>
|
||||||
|
<td className="mime-type-cell">{item.wxrMedia.mimeType || '--'}</td>
|
||||||
<td className="slug-cell">{item.wxrMedia.relativePath}</td>
|
<td className="slug-cell">{item.wxrMedia.relativePath}</td>
|
||||||
<td className="existing-match">{item.existingMedia?.originalName || '--'}</td>
|
<td className="existing-match">{item.existingMedia?.originalName || '--'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
1
src/renderer/types/electron.d.ts
vendored
1
src/renderer/types/electron.d.ts
vendored
@@ -396,6 +396,7 @@ export interface ElectronAPI {
|
|||||||
selectAndAnalyze: (uploadsFolder?: string) => Promise<unknown>;
|
selectAndAnalyze: (uploadsFolder?: string) => Promise<unknown>;
|
||||||
analyzeFile: (filePath: string, uploadsFolder?: string) => Promise<unknown>;
|
analyzeFile: (filePath: string, uploadsFolder?: string) => Promise<unknown>;
|
||||||
selectUploadsFolder: () => Promise<string | null>;
|
selectUploadsFolder: () => Promise<string | null>;
|
||||||
|
onProgress: (callback: (data: { step: string; detail?: string }) => void) => () => void;
|
||||||
};
|
};
|
||||||
importDefinitions: {
|
importDefinitions: {
|
||||||
create: (name?: string) => Promise<ImportDefinitionData>;
|
create: (name?: string) => Promise<ImportDefinitionData>;
|
||||||
|
|||||||
Reference in New Issue
Block a user