feat: importer starting point
This commit is contained in:
@@ -37,6 +37,12 @@ const ChatIcon = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ImportIcon = () => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const SyncIcon = () => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/>
|
||||
@@ -60,6 +66,9 @@ export const ActivityBar: React.FC = () => {
|
||||
// Check if chat sidebar is active (activeView === 'chat' and sidebar is visible)
|
||||
const isChatActive = activeView === 'chat' && sidebarVisible;
|
||||
|
||||
// Check if import tab is currently active
|
||||
const isImportTabActive = tabs.some(t => t.type === 'import' && t.id === activeTabId);
|
||||
|
||||
// Handle view click - toggle sidebar if clicking on active view, otherwise switch view
|
||||
const handleViewClick = (view: 'posts' | 'media' | 'chat') => {
|
||||
if (activeView === view && sidebarVisible) {
|
||||
@@ -96,6 +105,11 @@ export const ActivityBar: React.FC = () => {
|
||||
openTab({ type: 'tags', id: 'tags', isTransient: false });
|
||||
};
|
||||
|
||||
const handleImportClick = () => {
|
||||
// Open import as a dedicated (non-transient) tab
|
||||
openTab({ type: 'import', id: 'import', isTransient: false });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="activity-bar">
|
||||
<div className="activity-bar-top">
|
||||
@@ -127,6 +141,13 @@ export const ActivityBar: React.FC = () => {
|
||||
>
|
||||
<ChatIcon />
|
||||
</button>
|
||||
<button
|
||||
className={`activity-bar-item ${isImportTabActive ? 'active' : ''}`}
|
||||
onClick={handleImportClick}
|
||||
title="Import Analysis"
|
||||
>
|
||||
<ImportIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="activity-bar-bottom">
|
||||
|
||||
@@ -12,6 +12,7 @@ import { SettingsView } from '../SettingsView';
|
||||
import { TagsView } from '../TagsView';
|
||||
import { TagInput } from '../TagInput';
|
||||
import { ChatPanel } from '../ChatPanel';
|
||||
import { ImportAnalysisView } from '../ImportAnalysisView';
|
||||
import { AutoSaveManager } from '../../utils';
|
||||
import { parseMacros, getMacro } from '../../macros/registry';
|
||||
import { PostSearchModal } from '../PostSearchModal';
|
||||
@@ -1531,6 +1532,7 @@ export const Editor: React.FC = () => {
|
||||
const showSettings = activeTab?.type === 'settings' || (activeView === 'settings' && !activeTab);
|
||||
const showTags = activeTab?.type === 'tags' || (activeView === 'tags' && !activeTab);
|
||||
const showChat = activeTab?.type === 'chat';
|
||||
const showImport = activeTab?.type === 'import';
|
||||
|
||||
// Clear selectedPostId if the post doesn't exist (e.g., after project switch)
|
||||
useEffect(() => {
|
||||
@@ -1619,6 +1621,17 @@ export const Editor: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Show import analysis if import tab is active
|
||||
if (showImport) {
|
||||
return (
|
||||
<div className="editor">
|
||||
<ImportAnalysisView />
|
||||
{renderErrorModal()}
|
||||
{renderConfirmDeleteModal()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show post editor if a post tab is active
|
||||
if (showPost && activeTabId) {
|
||||
const post = posts.find(p => p.id === activeTabId);
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
.import-analysis {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.import-analysis-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.import-analysis-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.import-analysis-header p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
/* File selection area */
|
||||
.import-file-selectors {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
background: var(--vscode-sideBar-background);
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.import-file-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.import-file-row label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
min-width: 100px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.import-file-path {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-foreground);
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-input-border, transparent);
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.import-file-path.placeholder {
|
||||
color: var(--vscode-input-placeholderForeground);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.import-file-row button {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.import-file-row button:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.import-analyze-btn {
|
||||
padding: 8px 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.import-analyze-btn:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.import-analyze-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
.import-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 20px;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.import-spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid var(--vscode-descriptionForeground);
|
||||
border-top-color: var(--vscode-button-background);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Site info card */
|
||||
.import-site-info {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
background: var(--vscode-sideBar-background);
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.import-site-info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.import-site-info-item .info-label {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.import-site-info-item .info-value {
|
||||
font-size: 13px;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
/* Stat cards grid */
|
||||
.import-stat-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.import-stat-card {
|
||||
background: var(--vscode-sideBar-background);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.import-stat-card h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.import-stat-number {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.import-stat-label {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.import-stat-breakdown {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.import-stat-tag {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--vscode-badge-background);
|
||||
color: var(--vscode-badge-foreground);
|
||||
}
|
||||
|
||||
.import-stat-tag.stat-new {
|
||||
background: rgba(115, 201, 145, 0.15);
|
||||
color: #73c991;
|
||||
}
|
||||
|
||||
.import-stat-tag.stat-update {
|
||||
background: rgba(117, 190, 255, 0.15);
|
||||
color: #75beff;
|
||||
}
|
||||
|
||||
.import-stat-tag.stat-conflict {
|
||||
background: rgba(244, 135, 113, 0.15);
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.import-stat-tag.stat-duplicate {
|
||||
background: rgba(204, 167, 0, 0.15);
|
||||
color: #cca700;
|
||||
}
|
||||
|
||||
.import-stat-tag.stat-missing {
|
||||
background: rgba(150, 150, 150, 0.15);
|
||||
color: #969696;
|
||||
}
|
||||
|
||||
/* Detail sections */
|
||||
.import-detail-section {
|
||||
background: var(--vscode-sideBar-background);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.import-detail-section h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.import-detail-section h3 .toggle-icon {
|
||||
font-size: 10px;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
.import-detail-section h3 .toggle-icon.open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Detail tables */
|
||||
.import-detail-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.import-detail-table th {
|
||||
text-align: left;
|
||||
padding: 6px 10px;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
border-bottom: 1px solid var(--vscode-panel-border, rgba(255,255,255,0.1));
|
||||
}
|
||||
|
||||
.import-detail-table td {
|
||||
padding: 6px 10px;
|
||||
color: var(--vscode-foreground);
|
||||
border-bottom: 1px solid var(--vscode-panel-border, rgba(255,255,255,0.06));
|
||||
}
|
||||
|
||||
.import-detail-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.import-detail-table .status-badge {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.import-detail-table .status-badge.new {
|
||||
background: rgba(115, 201, 145, 0.15);
|
||||
color: #73c991;
|
||||
}
|
||||
|
||||
.import-detail-table .status-badge.update {
|
||||
background: rgba(117, 190, 255, 0.15);
|
||||
color: #75beff;
|
||||
}
|
||||
|
||||
.import-detail-table .status-badge.conflict {
|
||||
background: rgba(244, 135, 113, 0.15);
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.import-detail-table .status-badge.content-duplicate {
|
||||
background: rgba(204, 167, 0, 0.15);
|
||||
color: #cca700;
|
||||
}
|
||||
|
||||
.import-detail-table .status-badge.missing {
|
||||
background: rgba(150, 150, 150, 0.15);
|
||||
color: #969696;
|
||||
}
|
||||
|
||||
.import-detail-table .slug-cell {
|
||||
font-family: var(--vscode-editor-font-family, monospace);
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.import-detail-table .existing-match {
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
/* Tag/category pills */
|
||||
.import-taxonomy-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.import-taxonomy-pill {
|
||||
font-size: 11px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 10px;
|
||||
background: var(--vscode-badge-background);
|
||||
color: var(--vscode-badge-foreground);
|
||||
}
|
||||
|
||||
.import-taxonomy-pill.exists {
|
||||
background: rgba(115, 201, 145, 0.15);
|
||||
color: #73c991;
|
||||
}
|
||||
|
||||
.import-taxonomy-pill.new-tax {
|
||||
background: rgba(117, 190, 255, 0.15);
|
||||
color: #75beff;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.import-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
gap: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.import-empty-state svg {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.import-empty-state p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useAppStore } from '../../store';
|
||||
import './ImportAnalysisView.css';
|
||||
|
||||
interface AnalysisReport {
|
||||
sourceFile: string;
|
||||
site: { title: string; link: string; description: string; language: string };
|
||||
analyzedAt: string;
|
||||
posts: ItemSection;
|
||||
pages: ItemSection;
|
||||
media: MediaSection;
|
||||
categories: TaxonomyItem[];
|
||||
tags: TaxonomyItem[];
|
||||
}
|
||||
|
||||
interface ItemSection {
|
||||
total: number;
|
||||
new: number;
|
||||
updates: number;
|
||||
conflicts: number;
|
||||
contentDuplicates: number;
|
||||
items: AnalyzedPostItem[];
|
||||
}
|
||||
|
||||
interface MediaSection {
|
||||
total: number;
|
||||
new: number;
|
||||
updates: number;
|
||||
conflicts: number;
|
||||
contentDuplicates: number;
|
||||
missing: number;
|
||||
items: AnalyzedMediaItem[];
|
||||
}
|
||||
|
||||
interface AnalyzedPostItem {
|
||||
wxrPost: { wpId: number; title: string; slug: string; status: string };
|
||||
status: string;
|
||||
contentHash: string;
|
||||
markdownPreview: string;
|
||||
existingPost?: { id: string; title: string; slug: string };
|
||||
}
|
||||
|
||||
interface AnalyzedMediaItem {
|
||||
wxrMedia: { wpId: number; title: string; filename: string; url: string; relativePath: string };
|
||||
status: string;
|
||||
fileHash: string | null;
|
||||
existingMedia?: { id: string; originalName: string };
|
||||
}
|
||||
|
||||
interface TaxonomyItem {
|
||||
name: string;
|
||||
slug: string;
|
||||
existsInProject: boolean;
|
||||
}
|
||||
|
||||
export const ImportAnalysisView: React.FC = () => {
|
||||
const { importAnalysis, importAnalysisLoading, setImportAnalysis, setImportAnalysisLoading } = useAppStore();
|
||||
const [uploadsFolder, setUploadsFolder] = useState<string | null>(null);
|
||||
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({});
|
||||
|
||||
const report = importAnalysis as AnalysisReport | null;
|
||||
|
||||
const handleSelectUploadsFolder = useCallback(async () => {
|
||||
const folder = await window.electronAPI?.import.selectUploadsFolder();
|
||||
if (folder) {
|
||||
setUploadsFolder(folder);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSelectAndAnalyze = useCallback(async () => {
|
||||
setImportAnalysisLoading(true);
|
||||
setImportAnalysis(null);
|
||||
try {
|
||||
const result = await window.electronAPI?.import.selectAndAnalyze(uploadsFolder || undefined);
|
||||
if (result) {
|
||||
setImportAnalysis(result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Import analysis failed:', error);
|
||||
} finally {
|
||||
setImportAnalysisLoading(false);
|
||||
}
|
||||
}, [uploadsFolder, setImportAnalysis, setImportAnalysisLoading]);
|
||||
|
||||
const toggleSection = useCallback((section: string) => {
|
||||
setExpandedSections(prev => ({ ...prev, [section]: !prev[section] }));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="import-analysis">
|
||||
<div className="import-analysis-header">
|
||||
<h2>Import Analysis</h2>
|
||||
<p>Select a WordPress export file (WXR) and an uploads folder to analyze what would be imported.</p>
|
||||
</div>
|
||||
|
||||
<div className="import-file-selectors">
|
||||
<div className="import-file-row">
|
||||
<label>Uploads Folder</label>
|
||||
<div className={`import-file-path ${!uploadsFolder ? 'placeholder' : ''}`}>
|
||||
{uploadsFolder || 'No folder selected'}
|
||||
</div>
|
||||
<button onClick={handleSelectUploadsFolder}>Browse...</button>
|
||||
</div>
|
||||
<div className="import-file-row">
|
||||
<label>WXR File</label>
|
||||
<div className={`import-file-path ${!report ? 'placeholder' : ''}`}>
|
||||
{report?.sourceFile || 'Select a file to analyze'}
|
||||
</div>
|
||||
<button
|
||||
className="import-analyze-btn"
|
||||
onClick={handleSelectAndAnalyze}
|
||||
disabled={importAnalysisLoading}
|
||||
>
|
||||
{importAnalysisLoading ? 'Analyzing...' : 'Select & Analyze'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{importAnalysisLoading && (
|
||||
<div className="import-loading">
|
||||
<div className="import-spinner" />
|
||||
Analyzing WXR file...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!report && !importAnalysisLoading && (
|
||||
<div className="import-empty-state">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
|
||||
</svg>
|
||||
<p>Select a WordPress export file to begin analysis.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{report && !importAnalysisLoading && (
|
||||
<>
|
||||
<SiteInfoCard site={report.site} sourceFile={report.sourceFile} />
|
||||
<StatCards report={report} />
|
||||
|
||||
{report.posts.conflicts > 0 && (
|
||||
<ConflictsSection
|
||||
title="Post Slug Conflicts"
|
||||
items={report.posts.items.filter(i => i.status === 'conflict')}
|
||||
expanded={expandedSections['post-conflicts'] ?? true}
|
||||
onToggle={() => toggleSection('post-conflicts')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{report.pages.conflicts > 0 && (
|
||||
<ConflictsSection
|
||||
title="Page Slug Conflicts"
|
||||
items={report.pages.items.filter(i => i.status === 'conflict')}
|
||||
expanded={expandedSections['page-conflicts'] ?? true}
|
||||
onToggle={() => toggleSection('page-conflicts')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PostDetailSection
|
||||
title={`Posts (${report.posts.total})`}
|
||||
items={report.posts.items}
|
||||
expanded={expandedSections['posts'] ?? false}
|
||||
onToggle={() => toggleSection('posts')}
|
||||
/>
|
||||
|
||||
{report.pages.total > 0 && (
|
||||
<PostDetailSection
|
||||
title={`Pages (${report.pages.total})`}
|
||||
items={report.pages.items}
|
||||
expanded={expandedSections['pages'] ?? false}
|
||||
onToggle={() => toggleSection('pages')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<MediaDetailSection
|
||||
title={`Media (${report.media.total})`}
|
||||
items={report.media.items}
|
||||
expanded={expandedSections['media'] ?? false}
|
||||
onToggle={() => toggleSection('media')}
|
||||
/>
|
||||
|
||||
{(report.categories.length > 0 || report.tags.length > 0) && (
|
||||
<TaxonomySection
|
||||
categories={report.categories}
|
||||
tags={report.tags}
|
||||
expanded={expandedSections['taxonomy'] ?? false}
|
||||
onToggle={() => toggleSection('taxonomy')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SiteInfoCard: React.FC<{ site: AnalysisReport['site']; sourceFile: string }> = ({ site, sourceFile }) => (
|
||||
<div className="import-site-info">
|
||||
<div className="import-site-info-item">
|
||||
<span className="info-label">Site</span>
|
||||
<span className="info-value">{site.title || 'Untitled'}</span>
|
||||
</div>
|
||||
<div className="import-site-info-item">
|
||||
<span className="info-label">URL</span>
|
||||
<span className="info-value">{site.link || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="import-site-info-item">
|
||||
<span className="info-label">Language</span>
|
||||
<span className="info-value">{site.language || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="import-site-info-item">
|
||||
<span className="info-label">File</span>
|
||||
<span className="info-value">{sourceFile.split(/[/\\]/).pop()}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const StatCards: React.FC<{ report: AnalysisReport }> = ({ report }) => (
|
||||
<div className="import-stat-cards">
|
||||
<div className="import-stat-card">
|
||||
<h3>Posts</h3>
|
||||
<div className="import-stat-number">{report.posts.total}</div>
|
||||
<div className="import-stat-breakdown">
|
||||
{report.posts.new > 0 && <span className="import-stat-tag stat-new">{report.posts.new} new</span>}
|
||||
{report.posts.updates > 0 && <span className="import-stat-tag stat-update">{report.posts.updates} update</span>}
|
||||
{report.posts.conflicts > 0 && <span className="import-stat-tag stat-conflict">{report.posts.conflicts} conflict</span>}
|
||||
{report.posts.contentDuplicates > 0 && <span className="import-stat-tag stat-duplicate">{report.posts.contentDuplicates} duplicate</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="import-stat-card">
|
||||
<h3>Pages</h3>
|
||||
<div className="import-stat-number">{report.pages.total}</div>
|
||||
<div className="import-stat-breakdown">
|
||||
{report.pages.new > 0 && <span className="import-stat-tag stat-new">{report.pages.new} new</span>}
|
||||
{report.pages.updates > 0 && <span className="import-stat-tag stat-update">{report.pages.updates} update</span>}
|
||||
{report.pages.conflicts > 0 && <span className="import-stat-tag stat-conflict">{report.pages.conflicts} conflict</span>}
|
||||
{report.pages.contentDuplicates > 0 && <span className="import-stat-tag stat-duplicate">{report.pages.contentDuplicates} duplicate</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="import-stat-card">
|
||||
<h3>Media</h3>
|
||||
<div className="import-stat-number">{report.media.total}</div>
|
||||
<div className="import-stat-breakdown">
|
||||
{report.media.new > 0 && <span className="import-stat-tag stat-new">{report.media.new} new</span>}
|
||||
{report.media.updates > 0 && <span className="import-stat-tag stat-update">{report.media.updates} update</span>}
|
||||
{report.media.conflicts > 0 && <span className="import-stat-tag stat-conflict">{report.media.conflicts} conflict</span>}
|
||||
{report.media.contentDuplicates > 0 && <span className="import-stat-tag stat-duplicate">{report.media.contentDuplicates} duplicate</span>}
|
||||
{report.media.missing > 0 && <span className="import-stat-tag stat-missing">{report.media.missing} missing</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="import-stat-card">
|
||||
<h3>Categories</h3>
|
||||
<div className="import-stat-number">{report.categories.length}</div>
|
||||
<div className="import-stat-breakdown">
|
||||
{report.categories.filter(c => c.existsInProject).length > 0 && (
|
||||
<span className="import-stat-tag stat-update">{report.categories.filter(c => c.existsInProject).length} existing</span>
|
||||
)}
|
||||
{report.categories.filter(c => !c.existsInProject).length > 0 && (
|
||||
<span className="import-stat-tag stat-new">{report.categories.filter(c => !c.existsInProject).length} new</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="import-stat-card">
|
||||
<h3>Tags</h3>
|
||||
<div className="import-stat-number">{report.tags.length}</div>
|
||||
<div className="import-stat-breakdown">
|
||||
{report.tags.filter(t => t.existsInProject).length > 0 && (
|
||||
<span className="import-stat-tag stat-update">{report.tags.filter(t => t.existsInProject).length} existing</span>
|
||||
)}
|
||||
{report.tags.filter(t => !t.existsInProject).length > 0 && (
|
||||
<span className="import-stat-tag stat-new">{report.tags.filter(t => !t.existsInProject).length} new</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ConflictsSection: React.FC<{
|
||||
title: string;
|
||||
items: AnalyzedPostItem[];
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
}> = ({ title, items, expanded, onToggle }) => (
|
||||
<div className="import-detail-section">
|
||||
<h3 onClick={onToggle}>
|
||||
<span className={`toggle-icon ${expanded ? 'open' : ''}`}>▶</span>
|
||||
{title} ({items.length})
|
||||
</h3>
|
||||
{expanded && (
|
||||
<table className="import-detail-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Slug</th>
|
||||
<th>WXR Title</th>
|
||||
<th>Existing Title</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="slug-cell">{item.wxrPost.slug}</td>
|
||||
<td>{item.wxrPost.title}</td>
|
||||
<td className="existing-match">{item.existingPost?.title || '--'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const PostDetailSection: React.FC<{
|
||||
title: string;
|
||||
items: AnalyzedPostItem[];
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
}> = ({ title, items, expanded, onToggle }) => (
|
||||
<div className="import-detail-section">
|
||||
<h3 onClick={onToggle}>
|
||||
<span className={`toggle-icon ${expanded ? 'open' : ''}`}>▶</span>
|
||||
{title}
|
||||
</h3>
|
||||
{expanded && (
|
||||
<table className="import-detail-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Title</th>
|
||||
<th>Slug</th>
|
||||
<th>WP Status</th>
|
||||
<th>Existing Match</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, idx) => (
|
||||
<tr key={idx}>
|
||||
<td><span className={`status-badge ${item.status}`}>{item.status}</span></td>
|
||||
<td>{item.wxrPost.title}</td>
|
||||
<td className="slug-cell">{item.wxrPost.slug}</td>
|
||||
<td>{item.wxrPost.status}</td>
|
||||
<td className="existing-match">{item.existingPost?.title || '--'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const MediaDetailSection: React.FC<{
|
||||
title: string;
|
||||
items: AnalyzedMediaItem[];
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
}> = ({ title, items, expanded, onToggle }) => (
|
||||
<div className="import-detail-section">
|
||||
<h3 onClick={onToggle}>
|
||||
<span className={`toggle-icon ${expanded ? 'open' : ''}`}>▶</span>
|
||||
{title}
|
||||
</h3>
|
||||
{expanded && (
|
||||
<table className="import-detail-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Filename</th>
|
||||
<th>Path</th>
|
||||
<th>Existing Match</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, idx) => (
|
||||
<tr key={idx}>
|
||||
<td><span className={`status-badge ${item.status}`}>{item.status}</span></td>
|
||||
<td>{item.wxrMedia.filename}</td>
|
||||
<td className="slug-cell">{item.wxrMedia.relativePath}</td>
|
||||
<td className="existing-match">{item.existingMedia?.originalName || '--'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const TaxonomySection: React.FC<{
|
||||
categories: TaxonomyItem[];
|
||||
tags: TaxonomyItem[];
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
}> = ({ categories, tags, expanded, onToggle }) => (
|
||||
<div className="import-detail-section">
|
||||
<h3 onClick={onToggle}>
|
||||
<span className={`toggle-icon ${expanded ? 'open' : ''}`}>▶</span>
|
||||
Categories & Tags
|
||||
</h3>
|
||||
{expanded && (
|
||||
<>
|
||||
{categories.length > 0 && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--vscode-descriptionForeground)', marginBottom: 6 }}>
|
||||
Categories
|
||||
</div>
|
||||
<div className="import-taxonomy-list">
|
||||
{categories.map((cat, idx) => (
|
||||
<span key={idx} className={`import-taxonomy-pill ${cat.existsInProject ? 'exists' : 'new-tax'}`}>
|
||||
{cat.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{tags.length > 0 && (
|
||||
<div>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--vscode-descriptionForeground)', marginBottom: 6 }}>
|
||||
Tags
|
||||
</div>
|
||||
<div className="import-taxonomy-list">
|
||||
{tags.map((tag, idx) => (
|
||||
<span key={idx} className={`import-taxonomy-pill ${tag.existsInProject ? 'exists' : 'new-tax'}`}>
|
||||
{tag.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
1
src/renderer/components/ImportAnalysisView/index.ts
Normal file
1
src/renderer/components/ImportAnalysisView/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { ImportAnalysisView } from './ImportAnalysisView';
|
||||
@@ -32,13 +32,17 @@ const getTabTitle = (
|
||||
const title = chatTitles.get(tab.id);
|
||||
if (title && title !== 'New Chat') {
|
||||
// Truncate long titles for display
|
||||
return title.length > MAX_CHAT_TITLE_LENGTH
|
||||
return title.length > MAX_CHAT_TITLE_LENGTH
|
||||
? title.substring(0, MAX_CHAT_TITLE_LENGTH) + '…'
|
||||
: title;
|
||||
}
|
||||
return 'New Chat';
|
||||
}
|
||||
|
||||
|
||||
if (tab.type === 'import') {
|
||||
return 'Import Analysis';
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
};
|
||||
|
||||
@@ -74,6 +78,12 @@ const getTabIcon = (tab: Tab): React.ReactNode => {
|
||||
<path d="M14 1H2a1 1 0 00-1 1v10a1 1 0 001 1h3v2.5l4-2.5h5a1 1 0 001-1V2a1 1 0 00-1-1zm0 11H8.5L5 14v-2H2V2h12v10z"/>
|
||||
</svg>
|
||||
);
|
||||
case 'import':
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
|
||||
@@ -19,3 +19,4 @@ export { LinkedMediaPanel } from './LinkedMediaPanel';
|
||||
export { ErrorModal, type ErrorDetails } from './ErrorModal';
|
||||
export { ConfirmDeleteModal, type ConfirmDeleteDetails, type DeleteReference } from './ConfirmDeleteModal';
|
||||
export { ChatPanel } from './ChatPanel';
|
||||
export { ImportAnalysisView } from './ImportAnalysisView';
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { DeleteReference, ConfirmDeleteDetails } from '../components/Confir
|
||||
const STORAGE_KEY = 'bds-app-state';
|
||||
|
||||
// Tab types
|
||||
export type TabType = 'post' | 'media' | 'settings' | 'tags' | 'chat';
|
||||
export type TabType = 'post' | 'media' | 'settings' | 'tags' | 'chat' | 'import';
|
||||
|
||||
export interface Tab {
|
||||
type: TabType;
|
||||
@@ -93,7 +93,7 @@ interface AppState {
|
||||
activeTabId: string | null;
|
||||
|
||||
// UI State
|
||||
activeView: 'posts' | 'media' | 'settings' | 'tags' | 'chat';
|
||||
activeView: 'posts' | 'media' | 'settings' | 'tags' | 'chat' | 'import';
|
||||
sidebarVisible: boolean;
|
||||
panelVisible: boolean;
|
||||
selectedPostId: string | null;
|
||||
@@ -126,7 +126,11 @@ interface AppState {
|
||||
// Loading states
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
|
||||
// Import Analysis
|
||||
importAnalysis: unknown | null;
|
||||
importAnalysisLoading: boolean;
|
||||
|
||||
// Project Actions
|
||||
setProjects: (projects: ProjectData[]) => void;
|
||||
setActiveProject: (project: ProjectData | null) => void;
|
||||
@@ -144,7 +148,7 @@ interface AppState {
|
||||
restoreTabState: (state: TabState) => void;
|
||||
|
||||
// Actions
|
||||
setActiveView: (view: 'posts' | 'media' | 'settings' | 'tags' | 'chat') => void;
|
||||
setActiveView: (view: 'posts' | 'media' | 'settings' | 'tags' | 'chat' | 'import') => void;
|
||||
toggleSidebar: () => void;
|
||||
togglePanel: () => void;
|
||||
setSelectedPost: (id: string | null) => void;
|
||||
@@ -184,6 +188,10 @@ interface AppState {
|
||||
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
|
||||
// Import Analysis Actions
|
||||
setImportAnalysis: (report: unknown | null) => void;
|
||||
setImportAnalysisLoading: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>()(
|
||||
@@ -231,7 +239,11 @@ export const useAppStore = create<AppState>()(
|
||||
// Initial Loading State
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
|
||||
// Import Analysis State
|
||||
importAnalysis: null,
|
||||
importAnalysisLoading: false,
|
||||
|
||||
// Project Actions
|
||||
setProjects: (projects) => set({ projects }),
|
||||
setActiveProject: (activeProject) => set({ activeProject }),
|
||||
@@ -405,6 +417,10 @@ export const useAppStore = create<AppState>()(
|
||||
// Loading Actions
|
||||
setLoading: (isLoading) => set({ isLoading }),
|
||||
setError: (error) => set({ error }),
|
||||
|
||||
// Import Analysis Actions
|
||||
setImportAnalysis: (importAnalysis) => set({ importAnalysis }),
|
||||
setImportAnalysisLoading: (importAnalysisLoading) => set({ importAnalysisLoading }),
|
||||
}),
|
||||
{
|
||||
name: STORAGE_KEY,
|
||||
|
||||
5
src/renderer/types/electron.d.ts
vendored
5
src/renderer/types/electron.d.ts
vendored
@@ -381,6 +381,11 @@ export interface ElectronAPI {
|
||||
getPostsWithTag: (tagId: string) => Promise<string[]>;
|
||||
syncFromPosts: () => Promise<SyncTagsResult>;
|
||||
};
|
||||
import: {
|
||||
selectAndAnalyze: (uploadsFolder?: string) => Promise<unknown>;
|
||||
analyzeFile: (filePath: string, uploadsFolder?: string) => Promise<unknown>;
|
||||
selectUploadsFolder: () => Promise<string | null>;
|
||||
};
|
||||
chat: {
|
||||
// API Key Management
|
||||
checkReady: () => Promise<ChatReadyStatus>;
|
||||
|
||||
Reference in New Issue
Block a user