feat: diff tool to see discrepancies
This commit is contained in:
@@ -276,6 +276,13 @@ const App: React.FC = () => {
|
||||
}) || (() => {})
|
||||
);
|
||||
|
||||
unsubscribers.push(
|
||||
window.electronAPI?.on('menu:metadataDiff', () => {
|
||||
// Open metadata diff tool tab
|
||||
openTab({ id: 'metadata-diff', type: 'metadata-diff', title: 'Metadata Diff' });
|
||||
}) || (() => {})
|
||||
);
|
||||
|
||||
// Import completion event - refresh posts and media stores
|
||||
unsubscribers.push(
|
||||
window.electronAPI?.import.onComplete(async (data) => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { TagsView } from '../TagsView';
|
||||
import { TagInput } from '../TagInput';
|
||||
import { ChatPanel } from '../ChatPanel';
|
||||
import { ImportAnalysisView } from '../ImportAnalysisView';
|
||||
import { MetadataDiffPanel } from '../MetadataDiffPanel';
|
||||
import { AutoSaveManager } from '../../utils';
|
||||
import { parseMacros, getMacro } from '../../macros/registry';
|
||||
import { InsertModal } from '../InsertModal';
|
||||
@@ -2319,6 +2320,7 @@ export const Editor: React.FC = () => {
|
||||
const showTags = activeTab?.type === 'tags';
|
||||
const showChat = activeTab?.type === 'chat';
|
||||
const showImport = activeTab?.type === 'import';
|
||||
const showMetadataDiff = activeTab?.type === 'metadata-diff';
|
||||
|
||||
// Clear selectedPostId if the post doesn't exist (e.g., after project switch)
|
||||
useEffect(() => {
|
||||
@@ -2407,6 +2409,17 @@ export const Editor: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Show metadata diff if metadata-diff tab is active
|
||||
if (showMetadataDiff) {
|
||||
return (
|
||||
<div className="editor">
|
||||
<MetadataDiffPanel />
|
||||
{renderErrorModal()}
|
||||
{renderConfirmDeleteModal()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show post editor if a post tab is active
|
||||
if (showPost && activeTabId) {
|
||||
return (
|
||||
|
||||
342
src/renderer/components/MetadataDiffPanel/MetadataDiffPanel.css
Normal file
342
src/renderer/components/MetadataDiffPanel/MetadataDiffPanel.css
Normal file
@@ -0,0 +1,342 @@
|
||||
.metadata-diff-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
background: var(--editor-background);
|
||||
color: var(--editor-foreground);
|
||||
}
|
||||
|
||||
.metadata-diff-panel h2 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--editor-foreground);
|
||||
}
|
||||
|
||||
.metadata-diff-panel h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--editor-foreground);
|
||||
}
|
||||
|
||||
/* Stats Section */
|
||||
.diff-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
padding: 12px;
|
||||
background: var(--sidebar-background);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--sidebar-border);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 11px;
|
||||
color: var(--descriptionForeground);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--editor-foreground);
|
||||
}
|
||||
|
||||
/* Progress Section */
|
||||
.diff-progress {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background: var(--sidebar-background);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--sidebar-border);
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
height: 6px;
|
||||
background: var(--input-background);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: var(--button-background);
|
||||
border-radius: 3px;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
color: var(--descriptionForeground);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Actions Section */
|
||||
.diff-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.diff-actions button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.diff-actions button.primary {
|
||||
background: var(--button-background);
|
||||
color: var(--button-foreground);
|
||||
}
|
||||
|
||||
.diff-actions button.primary:hover:not(:disabled) {
|
||||
background: var(--button-hoverBackground);
|
||||
}
|
||||
|
||||
.diff-actions button.secondary {
|
||||
background: var(--input-background);
|
||||
color: var(--editor-foreground);
|
||||
border: 1px solid var(--input-border);
|
||||
}
|
||||
|
||||
.diff-actions button.secondary:hover:not(:disabled) {
|
||||
background: var(--list-hoverBackground);
|
||||
}
|
||||
|
||||
.diff-actions button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Results Section */
|
||||
.diff-results {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.diff-summary {
|
||||
padding: 12px;
|
||||
background: var(--sidebar-background);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--sidebar-border);
|
||||
}
|
||||
|
||||
.diff-summary.no-differences {
|
||||
background: color-mix(in srgb, var(--testing-iconPassed) 10%, var(--sidebar-background));
|
||||
border-color: var(--testing-iconPassed);
|
||||
}
|
||||
|
||||
.diff-summary.has-differences {
|
||||
background: color-mix(in srgb, var(--testing-iconFailed) 10%, var(--sidebar-background));
|
||||
border-color: var(--testing-iconFailed);
|
||||
}
|
||||
|
||||
/* Collapsible Groups */
|
||||
.diff-group {
|
||||
background: var(--sidebar-background);
|
||||
border: 1px solid var(--sidebar-border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.diff-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
background: var(--list-hoverBackground);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.diff-group-header:hover {
|
||||
background: var(--list-activeSelectionBackground);
|
||||
}
|
||||
|
||||
.diff-group-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.diff-group-title .chevron {
|
||||
font-size: 10px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.diff-group-title .chevron.expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.diff-group-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.diff-group-count .badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
background: var(--badge-background);
|
||||
color: var(--badge-foreground);
|
||||
}
|
||||
|
||||
.diff-group-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.diff-group-actions button {
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 3px;
|
||||
background: var(--input-background);
|
||||
color: var(--editor-foreground);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.diff-group-actions button:hover:not(:disabled) {
|
||||
background: var(--list-hoverBackground);
|
||||
}
|
||||
|
||||
.diff-group-actions button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.diff-group-actions button.db-to-file {
|
||||
border-color: var(--button-background);
|
||||
color: var(--button-background);
|
||||
}
|
||||
|
||||
.diff-group-actions button.file-to-db {
|
||||
border-color: var(--testing-iconQueued);
|
||||
color: var(--testing-iconQueued);
|
||||
}
|
||||
|
||||
.diff-group-content {
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--sidebar-border);
|
||||
}
|
||||
|
||||
.diff-group-content.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Post Items */
|
||||
.diff-post-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 12px;
|
||||
padding: 8px;
|
||||
margin-bottom: 8px;
|
||||
background: var(--editor-background);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.diff-post-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.diff-post-title {
|
||||
font-weight: 500;
|
||||
color: var(--editor-foreground);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.diff-value {
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
font-family: var(--editor-font-family);
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.diff-value.db-value {
|
||||
background: color-mix(in srgb, var(--button-background) 15%, transparent);
|
||||
border: 1px solid var(--button-background);
|
||||
}
|
||||
|
||||
.diff-value.file-value {
|
||||
background: color-mix(in srgb, var(--testing-iconQueued) 15%, transparent);
|
||||
border: 1px solid var(--testing-iconQueued);
|
||||
}
|
||||
|
||||
.diff-value-label {
|
||||
font-size: 10px;
|
||||
color: var(--descriptionForeground);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.diff-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 40px;
|
||||
color: var(--descriptionForeground);
|
||||
}
|
||||
|
||||
.diff-loading .spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid var(--input-border);
|
||||
border-top-color: var(--button-background);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.diff-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 40px;
|
||||
color: var(--descriptionForeground);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.diff-empty .icon {
|
||||
font-size: 32px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
322
src/renderer/components/MetadataDiffPanel/MetadataDiffPanel.tsx
Normal file
322
src/renderer/components/MetadataDiffPanel/MetadataDiffPanel.tsx
Normal file
@@ -0,0 +1,322 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { showToast } from '../Toast';
|
||||
import './MetadataDiffPanel.css';
|
||||
|
||||
interface TableStats {
|
||||
totalPosts: number;
|
||||
publishedPosts: number;
|
||||
draftPosts: number;
|
||||
totalMedia: number;
|
||||
}
|
||||
|
||||
interface DiffPost {
|
||||
postId: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
dbValue: unknown;
|
||||
fileValue: unknown;
|
||||
}
|
||||
|
||||
interface DiffGroup {
|
||||
field: string;
|
||||
label: string;
|
||||
posts: DiffPost[];
|
||||
}
|
||||
|
||||
interface ScanResult {
|
||||
totalScanned: number;
|
||||
postsWithDifferences: number;
|
||||
differences: Array<{
|
||||
postId: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
filePath?: string;
|
||||
hasDifferences: boolean;
|
||||
differences: Record<string, { dbValue: unknown; fileValue: unknown }>;
|
||||
}>;
|
||||
groups: DiffGroup[];
|
||||
}
|
||||
|
||||
type ScanPhase = 'idle' | 'loading-stats' | 'scanning' | 'complete';
|
||||
|
||||
export const MetadataDiffPanel: React.FC = () => {
|
||||
const [stats, setStats] = useState<TableStats | null>(null);
|
||||
const [scanResult, setScanResult] = useState<ScanResult | null>(null);
|
||||
const [scanPhase, setScanPhase] = useState<ScanPhase>('idle');
|
||||
const [progress, setProgress] = useState({ current: 0, total: 0, message: '' });
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||
const [syncingGroups, setSyncingGroups] = useState<Set<string>>(new Set());
|
||||
|
||||
// Load initial stats
|
||||
useEffect(() => {
|
||||
const loadStats = async () => {
|
||||
setScanPhase('loading-stats');
|
||||
try {
|
||||
const result = await window.electronAPI?.metadataDiff.getStats();
|
||||
if (result) {
|
||||
setStats(result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load stats:', error);
|
||||
showToast.error('Failed to load database statistics');
|
||||
}
|
||||
setScanPhase('idle');
|
||||
};
|
||||
loadStats();
|
||||
}, []);
|
||||
|
||||
// Subscribe to task progress
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electronAPI?.on('task:progress', (data: unknown) => {
|
||||
const progress = data as { id: string; progress: number; message?: string };
|
||||
if (progress.id.startsWith('metadata-diff-scan')) {
|
||||
setProgress({
|
||||
current: Math.round(progress.progress),
|
||||
total: 100,
|
||||
message: progress.message || '',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleScan = useCallback(async () => {
|
||||
setScanPhase('scanning');
|
||||
setProgress({ current: 0, total: 100, message: 'Starting scan...' });
|
||||
setScanResult(null);
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI?.metadataDiff.scan();
|
||||
if (result) {
|
||||
setScanResult(result);
|
||||
// Auto-expand groups with differences
|
||||
const groupsWithDiffs = new Set(result.groups.map(g => g.field));
|
||||
setExpandedGroups(groupsWithDiffs);
|
||||
}
|
||||
setScanPhase('complete');
|
||||
} catch (error) {
|
||||
console.error('Scan failed:', error);
|
||||
showToast.error('Failed to scan for differences');
|
||||
setScanPhase('idle');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleGroup = (field: string) => {
|
||||
setExpandedGroups(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(field)) {
|
||||
next.delete(field);
|
||||
} else {
|
||||
next.add(field);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSyncDbToFile = useCallback(async (group: DiffGroup) => {
|
||||
const postIds = group.posts.map(p => p.postId);
|
||||
setSyncingGroups(prev => new Set(prev).add(group.field));
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI?.metadataDiff.syncDbToFile(postIds, group.label);
|
||||
if (result) {
|
||||
showToast.success(`Synced ${result.success} posts to files${result.failed > 0 ? `, ${result.failed} failed` : ''}`);
|
||||
// Re-scan to update the view
|
||||
handleScan();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Sync failed:', error);
|
||||
showToast.error('Failed to sync to files');
|
||||
} finally {
|
||||
setSyncingGroups(prev => {
|
||||
const next = new Set(prev);
|
||||
next.delete(group.field);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}, [handleScan]);
|
||||
|
||||
const handleSyncFileToDb = useCallback(async (group: DiffGroup) => {
|
||||
const postIds = group.posts.map(p => p.postId);
|
||||
setSyncingGroups(prev => new Set(prev).add(group.field));
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI?.metadataDiff.syncFileToDb(postIds, group.field, group.label);
|
||||
if (result) {
|
||||
showToast.success(`Synced ${result.success} files to database${result.failed > 0 ? `, ${result.failed} failed` : ''}`);
|
||||
// Re-scan to update the view
|
||||
handleScan();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Sync failed:', error);
|
||||
showToast.error('Failed to sync to database');
|
||||
} finally {
|
||||
setSyncingGroups(prev => {
|
||||
const next = new Set(prev);
|
||||
next.delete(group.field);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}, [handleScan]);
|
||||
|
||||
const formatValue = (value: unknown): string => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length > 0 ? value.join(', ') : '(empty)';
|
||||
}
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '(empty)';
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="metadata-diff-panel">
|
||||
<h2>Metadata Diff Tool</h2>
|
||||
<p style={{ marginBottom: 16, color: 'var(--descriptionForeground)', fontSize: 13 }}>
|
||||
Compare post metadata between database and markdown files. Fix inconsistencies caused by bugs or manual edits.
|
||||
</p>
|
||||
|
||||
{/* Stats Section */}
|
||||
{stats && (
|
||||
<div className="diff-stats">
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Total Posts</span>
|
||||
<span className="stat-value">{stats.totalPosts}</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Published</span>
|
||||
<span className="stat-value">{stats.publishedPosts}</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Drafts</span>
|
||||
<span className="stat-value">{stats.draftPosts}</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Media Files</span>
|
||||
<span className="stat-value">{stats.totalMedia}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress Section */}
|
||||
{scanPhase === 'scanning' && (
|
||||
<div className="diff-progress">
|
||||
<h3>Scanning published posts...</h3>
|
||||
<div className="progress-bar-container">
|
||||
<div
|
||||
className="progress-bar"
|
||||
style={{ width: `${progress.current}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="progress-text">{progress.message}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions Section */}
|
||||
<div className="diff-actions">
|
||||
<button
|
||||
className="primary"
|
||||
onClick={handleScan}
|
||||
disabled={scanPhase === 'scanning' || scanPhase === 'loading-stats'}
|
||||
>
|
||||
{scanPhase === 'scanning' ? (
|
||||
<>
|
||||
<span className="spinner" style={{ width: 14, height: 14 }} />
|
||||
Scanning...
|
||||
</>
|
||||
) : scanResult ? (
|
||||
'🔄 Re-scan'
|
||||
) : (
|
||||
'🔍 Scan for Differences'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Results Section */}
|
||||
{scanPhase === 'complete' && scanResult && (
|
||||
<div className="diff-results">
|
||||
<div className={`diff-summary ${scanResult.postsWithDifferences > 0 ? 'has-differences' : 'no-differences'}`}>
|
||||
{scanResult.postsWithDifferences === 0 ? (
|
||||
<>✅ No differences found! All {scanResult.totalScanned} published posts are in sync.</>
|
||||
) : (
|
||||
<>
|
||||
⚠️ Found <strong>{scanResult.postsWithDifferences}</strong> posts with differences
|
||||
out of {scanResult.totalScanned} published posts.
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Groups */}
|
||||
{scanResult.groups.map(group => (
|
||||
<div key={group.field} className="diff-group">
|
||||
<div
|
||||
className="diff-group-header"
|
||||
onClick={() => toggleGroup(group.field)}
|
||||
>
|
||||
<div className="diff-group-title">
|
||||
<span className={`chevron ${expandedGroups.has(group.field) ? 'expanded' : ''}`}>
|
||||
▶
|
||||
</span>
|
||||
{group.label} Differences
|
||||
</div>
|
||||
<div className="diff-group-count">
|
||||
<span className="badge">{group.posts.length} posts</span>
|
||||
<div className="diff-group-actions" onClick={e => e.stopPropagation()}>
|
||||
<button
|
||||
className="db-to-file"
|
||||
onClick={() => handleSyncDbToFile(group)}
|
||||
disabled={syncingGroups.has(group.field)}
|
||||
title="Update files with database values"
|
||||
>
|
||||
DB → File
|
||||
</button>
|
||||
<button
|
||||
className="file-to-db"
|
||||
onClick={() => handleSyncFileToDb(group)}
|
||||
disabled={syncingGroups.has(group.field)}
|
||||
title="Update database with file values"
|
||||
>
|
||||
File → DB
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`diff-group-content ${!expandedGroups.has(group.field) ? 'collapsed' : ''}`}>
|
||||
{group.posts.map(post => (
|
||||
<div key={post.postId} className="diff-post-item">
|
||||
<div className="diff-post-title" title={post.title}>
|
||||
{post.title || post.slug}
|
||||
</div>
|
||||
<div>
|
||||
<div className="diff-value-label">Database</div>
|
||||
<div className="diff-value db-value" title={formatValue(post.dbValue)}>
|
||||
{formatValue(post.dbValue)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="diff-value-label">File</div>
|
||||
<div className="diff-value file-value" title={formatValue(post.fileValue)}>
|
||||
{formatValue(post.fileValue)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty state */}
|
||||
{scanPhase === 'idle' && !scanResult && (
|
||||
<div className="diff-empty">
|
||||
<div className="icon">📊</div>
|
||||
<div>Click "Scan for Differences" to compare database metadata with file metadata.</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
src/renderer/components/MetadataDiffPanel/index.ts
Normal file
1
src/renderer/components/MetadataDiffPanel/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { MetadataDiffPanel } from './MetadataDiffPanel';
|
||||
@@ -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' | 'import';
|
||||
export type TabType = 'post' | 'media' | 'settings' | 'tags' | 'chat' | 'import' | 'metadata-diff';
|
||||
|
||||
export interface Tab {
|
||||
type: TabType;
|
||||
|
||||
Reference in New Issue
Block a user