feat: proper sidebar and import persistence
This commit is contained in:
@@ -465,6 +465,21 @@ export class DatabaseConnection {
|
||||
)
|
||||
`);
|
||||
await this.localClient.execute('CREATE INDEX IF NOT EXISTS idx_chat_messages_conversation_id ON chat_messages(conversation_id)');
|
||||
|
||||
// Create import_definitions table for WXR import configurations
|
||||
await this.localClient.execute(`
|
||||
CREATE TABLE IF NOT EXISTS import_definitions (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
wxr_file_path TEXT,
|
||||
uploads_folder_path TEXT,
|
||||
last_analysis_result TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
`);
|
||||
await this.localClient.execute('CREATE INDEX IF NOT EXISTS idx_import_definitions_project_id ON import_definitions(project_id)');
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
|
||||
@@ -143,6 +143,18 @@ export const chatMessages = sqliteTable('chat_messages', {
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
});
|
||||
|
||||
// Import definitions table - stores WXR import configurations
|
||||
export const importDefinitions = sqliteTable('import_definitions', {
|
||||
id: text('id').primaryKey(),
|
||||
projectId: text('project_id').notNull(),
|
||||
name: text('name').notNull(),
|
||||
wxrFilePath: text('wxr_file_path'),
|
||||
uploadsFolderPath: text('uploads_folder_path'),
|
||||
lastAnalysisResult: text('last_analysis_result'), // JSON text of ImportAnalysisReport
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||
});
|
||||
|
||||
// Types for TypeScript
|
||||
export type Project = typeof projects.$inferSelect;
|
||||
export type NewProject = typeof projects.$inferInsert;
|
||||
@@ -164,3 +176,5 @@ export type ChatConversation = typeof chatConversations.$inferSelect;
|
||||
export type NewChatConversation = typeof chatConversations.$inferInsert;
|
||||
export type ChatMessage = typeof chatMessages.$inferSelect;
|
||||
export type NewChatMessage = typeof chatMessages.$inferInsert;
|
||||
export type ImportDefinition = typeof importDefinitions.$inferSelect;
|
||||
export type NewImportDefinition = typeof importDefinitions.$inferInsert;
|
||||
|
||||
174
src/main/engine/ImportDefinitionEngine.ts
Normal file
174
src/main/engine/ImportDefinitionEngine.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* ImportDefinitionEngine - CRUD for WXR import definitions
|
||||
*
|
||||
* Manages persisted import configurations (name, WXR file path, uploads folder,
|
||||
* last analysis result) stored in the import_definitions table.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { getDatabase } from '../database';
|
||||
|
||||
export interface ImportDefinitionData {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
wxrFilePath: string | null;
|
||||
uploadsFolderPath: string | null;
|
||||
lastAnalysisResult: unknown | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export class ImportDefinitionEngine {
|
||||
private currentProjectId: string = 'default';
|
||||
|
||||
private getClient() {
|
||||
const client = getDatabase().getLocalClient();
|
||||
if (!client) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
setProjectContext(projectId: string): void {
|
||||
this.currentProjectId = projectId;
|
||||
}
|
||||
|
||||
getProjectContext(): string {
|
||||
return this.currentProjectId;
|
||||
}
|
||||
|
||||
async createDefinition(name?: string): Promise<ImportDefinitionData> {
|
||||
const client = this.getClient();
|
||||
const id = `import_${uuidv4()}`;
|
||||
const now = Date.now();
|
||||
const defName = name || 'Untitled Import';
|
||||
|
||||
await client.execute({
|
||||
sql: `INSERT INTO import_definitions (id, project_id, name, wxr_file_path, uploads_folder_path, last_analysis_result, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
args: [id, this.currentProjectId, defName, null, null, null, now, now],
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
projectId: this.currentProjectId,
|
||||
name: defName,
|
||||
wxrFilePath: null,
|
||||
uploadsFolderPath: null,
|
||||
lastAnalysisResult: null,
|
||||
createdAt: new Date(now).toISOString(),
|
||||
updatedAt: new Date(now).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async getDefinition(id: string): Promise<ImportDefinitionData | null> {
|
||||
const client = this.getClient();
|
||||
const result = await client.execute({
|
||||
sql: `SELECT * FROM import_definitions WHERE id = ? AND project_id = ?`,
|
||||
args: [id, this.currentProjectId],
|
||||
});
|
||||
|
||||
if (result.rows.length === 0) return null;
|
||||
|
||||
return this.rowToData(result.rows[0] as any);
|
||||
}
|
||||
|
||||
async getAllForProject(): Promise<ImportDefinitionData[]> {
|
||||
const client = this.getClient();
|
||||
const result = await client.execute({
|
||||
sql: `SELECT * FROM import_definitions WHERE project_id = ? ORDER BY updated_at DESC`,
|
||||
args: [this.currentProjectId],
|
||||
});
|
||||
|
||||
return result.rows.map((row: any) => this.rowToData(row));
|
||||
}
|
||||
|
||||
async updateDefinition(
|
||||
id: string,
|
||||
updates: Partial<Pick<ImportDefinitionData, 'name' | 'wxrFilePath' | 'uploadsFolderPath' | 'lastAnalysisResult'>>
|
||||
): Promise<ImportDefinitionData | null> {
|
||||
// Check existence and ownership
|
||||
const existing = await this.getDefinition(id);
|
||||
if (!existing) return null;
|
||||
|
||||
const setClauses: string[] = [];
|
||||
const args: any[] = [];
|
||||
|
||||
if (updates.name !== undefined) {
|
||||
setClauses.push('name = ?');
|
||||
args.push(updates.name);
|
||||
}
|
||||
if (updates.wxrFilePath !== undefined) {
|
||||
setClauses.push('wxr_file_path = ?');
|
||||
args.push(updates.wxrFilePath);
|
||||
}
|
||||
if (updates.uploadsFolderPath !== undefined) {
|
||||
setClauses.push('uploads_folder_path = ?');
|
||||
args.push(updates.uploadsFolderPath);
|
||||
}
|
||||
if (updates.lastAnalysisResult !== undefined) {
|
||||
setClauses.push('last_analysis_result = ?');
|
||||
args.push(typeof updates.lastAnalysisResult === 'string'
|
||||
? updates.lastAnalysisResult
|
||||
: JSON.stringify(updates.lastAnalysisResult));
|
||||
}
|
||||
|
||||
if (setClauses.length === 0) return existing;
|
||||
|
||||
const now = Date.now();
|
||||
setClauses.push('updated_at = ?');
|
||||
args.push(now);
|
||||
|
||||
// WHERE clause args
|
||||
args.push(id, this.currentProjectId);
|
||||
|
||||
const client = this.getClient();
|
||||
await client.execute({
|
||||
sql: `UPDATE import_definitions SET ${setClauses.join(', ')} WHERE id = ? AND project_id = ?`,
|
||||
args,
|
||||
});
|
||||
|
||||
return this.getDefinition(id);
|
||||
}
|
||||
|
||||
async deleteDefinition(id: string): Promise<boolean> {
|
||||
// Check existence and ownership
|
||||
const existing = await this.getDefinition(id);
|
||||
if (!existing) return false;
|
||||
|
||||
const client = this.getClient();
|
||||
await client.execute({
|
||||
sql: `DELETE FROM import_definitions WHERE id = ? AND project_id = ?`,
|
||||
args: [id, this.currentProjectId],
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private rowToData(row: any): ImportDefinitionData {
|
||||
let parsedResult: unknown | null = null;
|
||||
if (row.last_analysis_result) {
|
||||
try {
|
||||
parsedResult = JSON.parse(row.last_analysis_result);
|
||||
} catch {
|
||||
parsedResult = row.last_analysis_result;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
projectId: row.project_id,
|
||||
name: row.name,
|
||||
wxrFilePath: row.wxr_file_path ?? null,
|
||||
uploadsFolderPath: row.uploads_folder_path ?? null,
|
||||
lastAnalysisResult: parsedResult,
|
||||
createdAt: typeof row.created_at === 'number'
|
||||
? new Date(row.created_at).toISOString()
|
||||
: row.created_at,
|
||||
updatedAt: typeof row.updated_at === 'number'
|
||||
? new Date(row.updated_at).toISOString()
|
||||
: row.updated_at,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -69,3 +69,7 @@ export {
|
||||
type PostAnalysisStatus,
|
||||
type MediaAnalysisStatus,
|
||||
} from './ImportAnalysisEngine';
|
||||
export {
|
||||
ImportDefinitionEngine,
|
||||
type ImportDefinitionData,
|
||||
} from './ImportDefinitionEngine';
|
||||
|
||||
@@ -807,6 +807,63 @@ export function registerIpcHandlers(): void {
|
||||
return result.filePaths[0];
|
||||
});
|
||||
|
||||
// ============ Import Definition CRUD Handlers ============
|
||||
|
||||
safeHandle('importDefinitions:create', async (_, name?: string) => {
|
||||
const { ImportDefinitionEngine } = await import('../engine/ImportDefinitionEngine');
|
||||
const engine = new ImportDefinitionEngine();
|
||||
const projectEngine = getProjectEngine();
|
||||
const activeProject = await projectEngine.getActiveProject();
|
||||
if (activeProject) {
|
||||
engine.setProjectContext(activeProject.id);
|
||||
}
|
||||
return engine.createDefinition(name || undefined);
|
||||
});
|
||||
|
||||
safeHandle('importDefinitions:get', async (_, id: string) => {
|
||||
const { ImportDefinitionEngine } = await import('../engine/ImportDefinitionEngine');
|
||||
const engine = new ImportDefinitionEngine();
|
||||
const projectEngine = getProjectEngine();
|
||||
const activeProject = await projectEngine.getActiveProject();
|
||||
if (activeProject) {
|
||||
engine.setProjectContext(activeProject.id);
|
||||
}
|
||||
return engine.getDefinition(id);
|
||||
});
|
||||
|
||||
safeHandle('importDefinitions:getAll', async () => {
|
||||
const { ImportDefinitionEngine } = await import('../engine/ImportDefinitionEngine');
|
||||
const engine = new ImportDefinitionEngine();
|
||||
const projectEngine = getProjectEngine();
|
||||
const activeProject = await projectEngine.getActiveProject();
|
||||
if (activeProject) {
|
||||
engine.setProjectContext(activeProject.id);
|
||||
}
|
||||
return engine.getAllForProject();
|
||||
});
|
||||
|
||||
safeHandle('importDefinitions:update', async (_, id: string, updates: any) => {
|
||||
const { ImportDefinitionEngine } = await import('../engine/ImportDefinitionEngine');
|
||||
const engine = new ImportDefinitionEngine();
|
||||
const projectEngine = getProjectEngine();
|
||||
const activeProject = await projectEngine.getActiveProject();
|
||||
if (activeProject) {
|
||||
engine.setProjectContext(activeProject.id);
|
||||
}
|
||||
return engine.updateDefinition(id, updates);
|
||||
});
|
||||
|
||||
safeHandle('importDefinitions:delete', async (_, id: string) => {
|
||||
const { ImportDefinitionEngine } = await import('../engine/ImportDefinitionEngine');
|
||||
const engine = new ImportDefinitionEngine();
|
||||
const projectEngine = getProjectEngine();
|
||||
const activeProject = await projectEngine.getActiveProject();
|
||||
if (activeProject) {
|
||||
engine.setProjectContext(activeProject.id);
|
||||
}
|
||||
return engine.deleteDefinition(id);
|
||||
});
|
||||
|
||||
// ============ Event Forwarding ============
|
||||
|
||||
// Forward engine events to renderer
|
||||
|
||||
@@ -157,6 +157,15 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
selectUploadsFolder: () => ipcRenderer.invoke('import:selectUploadsFolder'),
|
||||
},
|
||||
|
||||
// Import Definition CRUD
|
||||
importDefinitions: {
|
||||
create: (name?: string) => ipcRenderer.invoke('importDefinitions:create', name),
|
||||
get: (id: string) => ipcRenderer.invoke('importDefinitions:get', id),
|
||||
getAll: () => ipcRenderer.invoke('importDefinitions:getAll'),
|
||||
update: (id: string, updates: unknown) => ipcRenderer.invoke('importDefinitions:update', id, updates),
|
||||
delete: (id: string) => ipcRenderer.invoke('importDefinitions:delete', id),
|
||||
},
|
||||
|
||||
// AI Chat (OpenCode Zen API integration)
|
||||
chat: {
|
||||
// API Key Management
|
||||
@@ -324,6 +333,13 @@ export interface ElectronAPI {
|
||||
analyzeFile: (filePath: string, uploadsFolder?: string) => Promise<unknown>;
|
||||
selectUploadsFolder: () => Promise<string | null>;
|
||||
};
|
||||
importDefinitions: {
|
||||
create: (name?: string) => Promise<unknown>;
|
||||
get: (id: string) => Promise<unknown>;
|
||||
getAll: () => Promise<unknown[]>;
|
||||
update: (id: string, updates: unknown) => Promise<unknown>;
|
||||
delete: (id: string) => Promise<boolean>;
|
||||
};
|
||||
chat: {
|
||||
// API Key Management
|
||||
checkReady: () => Promise<{ ready: boolean; error?: string; backend?: string }>;
|
||||
|
||||
@@ -66,8 +66,8 @@ 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);
|
||||
// Check if import sidebar is active
|
||||
const isImportActive = activeView === 'import' && sidebarVisible;
|
||||
|
||||
// Handle view click - toggle sidebar if clicking on active view, otherwise switch view
|
||||
const handleViewClick = (view: 'posts' | 'media' | 'chat') => {
|
||||
@@ -106,8 +106,14 @@ export const ActivityBar: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleImportClick = () => {
|
||||
// Open import as a dedicated (non-transient) tab
|
||||
openTab({ type: 'import', id: 'import', isTransient: false });
|
||||
if (activeView === 'import' && sidebarVisible) {
|
||||
toggleSidebar();
|
||||
} else {
|
||||
setActiveView('import');
|
||||
if (!sidebarVisible) {
|
||||
toggleSidebar();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -142,9 +148,9 @@ export const ActivityBar: React.FC = () => {
|
||||
<ChatIcon />
|
||||
</button>
|
||||
<button
|
||||
className={`activity-bar-item ${isImportTabActive ? 'active' : ''}`}
|
||||
className={`activity-bar-item ${isImportActive ? 'active' : ''}`}
|
||||
onClick={handleImportClick}
|
||||
title="Import Analysis"
|
||||
title="Import (click again to toggle sidebar)"
|
||||
>
|
||||
<ImportIcon />
|
||||
</button>
|
||||
|
||||
@@ -1622,10 +1622,10 @@ export const Editor: React.FC = () => {
|
||||
}
|
||||
|
||||
// Show import analysis if import tab is active
|
||||
if (showImport) {
|
||||
if (showImport && activeTabId) {
|
||||
return (
|
||||
<div className="editor">
|
||||
<ImportAnalysisView />
|
||||
<ImportAnalysisView key={activeTabId} definitionId={activeTabId} />
|
||||
{renderErrorModal()}
|
||||
{renderConfirmDeleteModal()}
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,28 @@
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.import-definition-name {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
padding: 2px 6px;
|
||||
margin: -2px -6px;
|
||||
width: calc(100% + 12px);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.import-definition-name:hover {
|
||||
border-color: var(--vscode-input-border, #3c3c3c);
|
||||
}
|
||||
|
||||
.import-definition-name:focus {
|
||||
border-color: var(--vscode-focusBorder, #007fd4);
|
||||
background: var(--vscode-input-background, #1e1e1e);
|
||||
}
|
||||
|
||||
.import-analysis-header p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useAppStore } from '../../store';
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import './ImportAnalysisView.css';
|
||||
|
||||
interface AnalysisReport {
|
||||
@@ -53,43 +52,107 @@ interface TaxonomyItem {
|
||||
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>>({});
|
||||
interface ImportAnalysisViewProps {
|
||||
definitionId: string;
|
||||
}
|
||||
|
||||
const report = importAnalysis as AnalysisReport | null;
|
||||
export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definitionId }) => {
|
||||
const [name, setName] = useState('Untitled Import');
|
||||
const [uploadsFolder, setUploadsFolder] = useState<string | null>(null);
|
||||
const [wxrFilePath, setWxrFilePath] = useState<string | null>(null);
|
||||
const [report, setReport] = useState<AnalysisReport | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingDefinition, setIsLoadingDefinition] = useState(true);
|
||||
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({});
|
||||
const nameInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Load definition on mount
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
setIsLoadingDefinition(true);
|
||||
try {
|
||||
const def = await window.electronAPI?.importDefinitions.get(definitionId);
|
||||
if (def) {
|
||||
setName(def.name);
|
||||
if (def.uploadsFolderPath) setUploadsFolder(def.uploadsFolderPath);
|
||||
if (def.wxrFilePath) setWxrFilePath(def.wxrFilePath);
|
||||
if (def.lastAnalysisResult) {
|
||||
const parsed = typeof def.lastAnalysisResult === 'string'
|
||||
? JSON.parse(def.lastAnalysisResult)
|
||||
: def.lastAnalysisResult;
|
||||
setReport(parsed as AnalysisReport);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load import definition:', error);
|
||||
} finally {
|
||||
setIsLoadingDefinition(false);
|
||||
}
|
||||
};
|
||||
load();
|
||||
}, [definitionId]);
|
||||
|
||||
const handleNameBlur = useCallback(async () => {
|
||||
const trimmed = name.trim() || 'Untitled Import';
|
||||
setName(trimmed);
|
||||
await window.electronAPI?.importDefinitions.update(definitionId, { name: trimmed });
|
||||
}, [definitionId, name]);
|
||||
|
||||
const handleSelectUploadsFolder = useCallback(async () => {
|
||||
const folder = await window.electronAPI?.import.selectUploadsFolder();
|
||||
if (folder) {
|
||||
setUploadsFolder(folder);
|
||||
await window.electronAPI?.importDefinitions.update(definitionId, { uploadsFolderPath: folder });
|
||||
}
|
||||
}, []);
|
||||
}, [definitionId]);
|
||||
|
||||
const handleSelectAndAnalyze = useCallback(async () => {
|
||||
setImportAnalysisLoading(true);
|
||||
setImportAnalysis(null);
|
||||
setIsLoading(true);
|
||||
setReport(null);
|
||||
try {
|
||||
const result = await window.electronAPI?.import.selectAndAnalyze(uploadsFolder || undefined);
|
||||
const result = await window.electronAPI?.import.selectAndAnalyze(uploadsFolder || undefined) as AnalysisReport | null;
|
||||
if (result) {
|
||||
setImportAnalysis(result);
|
||||
setReport(result);
|
||||
setWxrFilePath(result.sourceFile);
|
||||
await window.electronAPI?.importDefinitions.update(definitionId, {
|
||||
lastAnalysisResult: JSON.stringify(result),
|
||||
wxrFilePath: result.sourceFile,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Import analysis failed:', error);
|
||||
} finally {
|
||||
setImportAnalysisLoading(false);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [uploadsFolder, setImportAnalysis, setImportAnalysisLoading]);
|
||||
}, [definitionId, uploadsFolder]);
|
||||
|
||||
const toggleSection = useCallback((section: string) => {
|
||||
setExpandedSections(prev => ({ ...prev, [section]: !prev[section] }));
|
||||
}, []);
|
||||
|
||||
if (isLoadingDefinition) {
|
||||
return (
|
||||
<div className="import-analysis">
|
||||
<div className="import-loading">
|
||||
<div className="import-spinner" />
|
||||
Loading import definition...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="import-analysis">
|
||||
<div className="import-analysis-header">
|
||||
<h2>Import Analysis</h2>
|
||||
<input
|
||||
ref={nameInputRef}
|
||||
className="import-definition-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onBlur={handleNameBlur}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') nameInputRef.current?.blur(); }}
|
||||
placeholder="Import name..."
|
||||
/>
|
||||
<p>Select a WordPress export file (WXR) and an uploads folder to analyze what would be imported.</p>
|
||||
</div>
|
||||
|
||||
@@ -103,27 +166,27 @@ export const ImportAnalysisView: React.FC = () => {
|
||||
</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 className={`import-file-path ${!wxrFilePath ? 'placeholder' : ''}`}>
|
||||
{wxrFilePath || report?.sourceFile || 'Select a file to analyze'}
|
||||
</div>
|
||||
<button
|
||||
className="import-analyze-btn"
|
||||
onClick={handleSelectAndAnalyze}
|
||||
disabled={importAnalysisLoading}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{importAnalysisLoading ? 'Analyzing...' : 'Select & Analyze'}
|
||||
{isLoading ? 'Analyzing...' : 'Select & Analyze'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{importAnalysisLoading && (
|
||||
{isLoading && (
|
||||
<div className="import-loading">
|
||||
<div className="import-spinner" />
|
||||
Analyzing WXR file...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!report && !importAnalysisLoading && (
|
||||
{!report && !isLoading && (
|
||||
<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"/>
|
||||
@@ -132,7 +195,7 @@ export const ImportAnalysisView: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{report && !importAnalysisLoading && (
|
||||
{report && !isLoading && (
|
||||
<>
|
||||
<SiteInfoCard site={report.site} sourceFile={report.sourceFile} />
|
||||
<StatCards report={report} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useAppStore, PostData, MediaData } from '../../store';
|
||||
import { showToast } from '../Toast';
|
||||
import type { ChatConversation } from '../../types/electron';
|
||||
import type { ChatConversation, ImportDefinitionData } from '../../types/electron';
|
||||
import './Sidebar.css';
|
||||
|
||||
// Tag data with color information
|
||||
@@ -1268,6 +1268,127 @@ const ChatList: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ImportList: React.FC = () => {
|
||||
const { openTab, closeTab } = useAppStore();
|
||||
const [definitions, setDefinitions] = useState<ImportDefinitionData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const loadDefinitions = useCallback(async () => {
|
||||
try {
|
||||
const defs = await window.electronAPI?.importDefinitions.getAll();
|
||||
if (defs) {
|
||||
setDefinitions(defs);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load import definitions:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
setIsLoading(true);
|
||||
await loadDefinitions();
|
||||
setIsLoading(false);
|
||||
};
|
||||
init();
|
||||
}, [loadDefinitions]);
|
||||
|
||||
const handleNewDefinition = async () => {
|
||||
try {
|
||||
const def = await window.electronAPI?.importDefinitions.create();
|
||||
if (def) {
|
||||
setDefinitions(prev => [def, ...prev]);
|
||||
openTab({ type: 'import', id: def.id, isTransient: false });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create import definition:', error);
|
||||
showToast.error('Failed to create import definition');
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenDefinition = (definitionId: string) => {
|
||||
openTab({ type: 'import', id: definitionId, isTransient: false });
|
||||
};
|
||||
|
||||
const handleDeleteDefinition = async (e: React.MouseEvent, definitionId: string) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await window.electronAPI?.importDefinitions.delete(definitionId);
|
||||
setDefinitions(prev => prev.filter(d => d.id !== definitionId));
|
||||
closeTab(definitionId);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete import definition:', error);
|
||||
showToast.error('Failed to delete import definition');
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
||||
if (diffDays === 0) {
|
||||
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
||||
} else if (diffDays === 1) {
|
||||
return 'Yesterday';
|
||||
} else if (diffDays < 7) {
|
||||
return date.toLocaleDateString('en-US', { weekday: 'short' });
|
||||
}
|
||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="chat-list">
|
||||
<div className="chat-list-header">
|
||||
<span>IMPORTS</span>
|
||||
</div>
|
||||
<div className="chat-loading">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chat-list">
|
||||
<div className="chat-list-header">
|
||||
<span>IMPORTS</span>
|
||||
<button className="chat-new-button" onClick={handleNewDefinition} title="New Import Definition">
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="chat-list-items">
|
||||
{definitions.length === 0 ? (
|
||||
<div className="chat-empty">
|
||||
<p>No import definitions yet</p>
|
||||
<button className="chat-start-button" onClick={handleNewDefinition}>
|
||||
Create an import definition
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
definitions.map(def => (
|
||||
<div
|
||||
key={def.id}
|
||||
className="chat-list-item"
|
||||
onClick={() => handleOpenDefinition(def.id)}
|
||||
>
|
||||
<div className="chat-item-content">
|
||||
<div className="chat-item-title">{def.name}</div>
|
||||
<div className="chat-item-date">{formatDate(def.updatedAt)}</div>
|
||||
</div>
|
||||
<button
|
||||
className="chat-item-delete"
|
||||
onClick={(e) => handleDeleteDefinition(e, def.id)}
|
||||
title="Delete import definition"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Sidebar: React.FC = () => {
|
||||
const { activeView, sidebarVisible } = useAppStore();
|
||||
|
||||
@@ -1282,6 +1403,7 @@ export const Sidebar: React.FC = () => {
|
||||
{activeView === 'settings' && <SettingsNav />}
|
||||
{activeView === 'tags' && <TagsNav />}
|
||||
{activeView === 'chat' && <ChatList />}
|
||||
{activeView === 'import' && <ImportList />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,8 @@ const getTabTitle = (
|
||||
tab: Tab,
|
||||
posts: { id: string; title: string }[],
|
||||
media: { id: string; originalName: string }[],
|
||||
chatTitles: Map<string, string>
|
||||
chatTitles: Map<string, string>,
|
||||
importDefTitles: Map<string, string>
|
||||
): string => {
|
||||
if (tab.type === 'settings') {
|
||||
return 'Settings';
|
||||
@@ -40,7 +41,7 @@ const getTabTitle = (
|
||||
}
|
||||
|
||||
if (tab.type === 'import') {
|
||||
return 'Import Analysis';
|
||||
return importDefTitles.get(tab.id) || 'Import';
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
@@ -129,6 +130,7 @@ export const TabBar: React.FC = () => {
|
||||
const [showLeftArrow, setShowLeftArrow] = useState(false);
|
||||
const [showRightArrow, setShowRightArrow] = useState(false);
|
||||
const [chatTitles, setChatTitles] = useState<Map<string, string>>(new Map());
|
||||
const [importDefTitles, setImportDefTitles] = useState<Map<string, string>>(new Map());
|
||||
|
||||
// Fetch chat titles for chat tabs
|
||||
useEffect(() => {
|
||||
@@ -175,6 +177,33 @@ export const TabBar: React.FC = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Fetch import definition titles for import tabs
|
||||
useEffect(() => {
|
||||
const importTabs = tabs.filter(t => t.type === 'import');
|
||||
if (importTabs.length === 0) return;
|
||||
|
||||
const fetchTitles = async () => {
|
||||
const newTitles = new Map(importDefTitles);
|
||||
for (const tab of importTabs) {
|
||||
if (!importDefTitles.has(tab.id)) {
|
||||
try {
|
||||
const def = await window.electronAPI?.importDefinitions.get(tab.id);
|
||||
if (def) {
|
||||
newTitles.set(tab.id, def.name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch import definition title:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newTitles.size !== importDefTitles.size) {
|
||||
setImportDefTitles(newTitles);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTitles();
|
||||
}, [tabs]); // Note: intentionally not including importDefTitles to avoid infinite loops
|
||||
|
||||
// Check if arrows are needed based on scroll position
|
||||
const updateArrowVisibility = useCallback(() => {
|
||||
const container = tabsContainerRef.current;
|
||||
@@ -305,7 +334,7 @@ export const TabBar: React.FC = () => {
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab.id === activeTabId;
|
||||
const isDirty = tab.type === 'post' && dirtyPosts.has(tab.id);
|
||||
const title = getTabTitle(tab, posts, media, chatTitles);
|
||||
const title = getTabTitle(tab, posts, media, chatTitles, importDefTitles);
|
||||
const icon = getTabIcon(tab);
|
||||
|
||||
return (
|
||||
|
||||
@@ -127,10 +127,6 @@ interface AppState {
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Import Analysis
|
||||
importAnalysis: unknown | null;
|
||||
importAnalysisLoading: boolean;
|
||||
|
||||
// Project Actions
|
||||
setProjects: (projects: ProjectData[]) => void;
|
||||
setActiveProject: (project: ProjectData | null) => void;
|
||||
@@ -188,10 +184,6 @@ 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>()(
|
||||
@@ -240,10 +232,6 @@ export const useAppStore = create<AppState>()(
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
// Import Analysis State
|
||||
importAnalysis: null,
|
||||
importAnalysisLoading: false,
|
||||
|
||||
// Project Actions
|
||||
setProjects: (projects) => set({ projects }),
|
||||
setActiveProject: (activeProject) => set({ activeProject }),
|
||||
@@ -417,10 +405,6 @@ 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,
|
||||
|
||||
18
src/renderer/types/electron.d.ts
vendored
18
src/renderer/types/electron.d.ts
vendored
@@ -1,5 +1,16 @@
|
||||
// Type definitions for the Electron API exposed via preload
|
||||
|
||||
export interface ImportDefinitionData {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
wxrFilePath: string | null;
|
||||
uploadsFolderPath: string | null;
|
||||
lastAnalysisResult: unknown | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ProjectMetadata {
|
||||
name: string;
|
||||
description?: string;
|
||||
@@ -386,6 +397,13 @@ export interface ElectronAPI {
|
||||
analyzeFile: (filePath: string, uploadsFolder?: string) => Promise<unknown>;
|
||||
selectUploadsFolder: () => Promise<string | null>;
|
||||
};
|
||||
importDefinitions: {
|
||||
create: (name?: string) => Promise<ImportDefinitionData>;
|
||||
get: (id: string) => Promise<ImportDefinitionData | null>;
|
||||
getAll: () => Promise<ImportDefinitionData[]>;
|
||||
update: (id: string, updates: Partial<Pick<ImportDefinitionData, 'name' | 'wxrFilePath' | 'uploadsFolderPath' | 'lastAnalysisResult'>>) => Promise<ImportDefinitionData | null>;
|
||||
delete: (id: string) => Promise<boolean>;
|
||||
};
|
||||
chat: {
|
||||
// API Key Management
|
||||
checkReady: () => Promise<ChatReadyStatus>;
|
||||
|
||||
341
tests/engine/ImportDefinitionEngine.test.ts
Normal file
341
tests/engine/ImportDefinitionEngine.test.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* ImportDefinitionEngine Unit Tests
|
||||
*
|
||||
* Tests the REAL ImportDefinitionEngine class with mocked database.
|
||||
* Following TDD best practices: mock external dependencies, test real implementation.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
|
||||
// Store for mock data
|
||||
const mockDefinitions = new Map<string, any>();
|
||||
|
||||
const mockLocalClient = {
|
||||
execute: vi.fn(async (query: { sql: string; args: any[] }) => {
|
||||
const sql = query.sql.trim();
|
||||
|
||||
// INSERT
|
||||
if (sql.startsWith('INSERT')) {
|
||||
const row = {
|
||||
id: query.args[0],
|
||||
project_id: query.args[1],
|
||||
name: query.args[2],
|
||||
wxr_file_path: query.args[3] ?? null,
|
||||
uploads_folder_path: query.args[4] ?? null,
|
||||
last_analysis_result: query.args[5] ?? null,
|
||||
created_at: query.args[6],
|
||||
updated_at: query.args[7],
|
||||
};
|
||||
mockDefinitions.set(row.id, row);
|
||||
return { rows: [] };
|
||||
}
|
||||
|
||||
// SELECT by id
|
||||
if (sql.startsWith('SELECT') && sql.includes('WHERE id = ?') && sql.includes('project_id = ?')) {
|
||||
const id = query.args[0];
|
||||
const projectId = query.args[1];
|
||||
const def = mockDefinitions.get(id);
|
||||
if (def && def.project_id === projectId) {
|
||||
return { rows: [def] };
|
||||
}
|
||||
return { rows: [] };
|
||||
}
|
||||
|
||||
// SELECT all for project
|
||||
if (sql.startsWith('SELECT') && sql.includes('WHERE project_id = ?') && sql.includes('ORDER BY')) {
|
||||
const projectId = query.args[0];
|
||||
const rows = Array.from(mockDefinitions.values())
|
||||
.filter(d => d.project_id === projectId)
|
||||
.sort((a, b) => b.updated_at - a.updated_at);
|
||||
return { rows };
|
||||
}
|
||||
|
||||
// UPDATE
|
||||
if (sql.startsWith('UPDATE')) {
|
||||
// Find the id in args (last two args are id and project_id in WHERE)
|
||||
const id = query.args[query.args.length - 2];
|
||||
const projectId = query.args[query.args.length - 1];
|
||||
const def = mockDefinitions.get(id);
|
||||
if (def && def.project_id === projectId) {
|
||||
// Apply updates based on the SET clause
|
||||
// Parse set fields from the sql
|
||||
const setMatch = sql.match(/SET (.+?) WHERE/);
|
||||
if (setMatch) {
|
||||
const setParts = setMatch[1].split(', ');
|
||||
let argIdx = 0;
|
||||
for (const part of setParts) {
|
||||
const field = part.split(' = ')[0].trim();
|
||||
def[field] = query.args[argIdx];
|
||||
argIdx++;
|
||||
}
|
||||
}
|
||||
return { rowsAffected: 1, rows: [] };
|
||||
}
|
||||
return { rowsAffected: 0, rows: [] };
|
||||
}
|
||||
|
||||
// DELETE
|
||||
if (sql.startsWith('DELETE')) {
|
||||
const id = query.args[0];
|
||||
const projectId = query.args[1];
|
||||
const def = mockDefinitions.get(id);
|
||||
if (def && def.project_id === projectId) {
|
||||
mockDefinitions.delete(id);
|
||||
return { rowsAffected: 1, rows: [] };
|
||||
}
|
||||
return { rowsAffected: 0, rows: [] };
|
||||
}
|
||||
|
||||
return { rows: [] };
|
||||
}),
|
||||
};
|
||||
|
||||
// Mock the database module
|
||||
vi.mock('../../src/main/database', () => ({
|
||||
getDatabase: vi.fn(() => ({
|
||||
getLocal: vi.fn(() => null),
|
||||
getLocalClient: vi.fn(() => mockLocalClient),
|
||||
getRemote: vi.fn(() => null),
|
||||
getDataPaths: vi.fn(() => ({
|
||||
database: '/mock/userData/bds.db',
|
||||
posts: '/mock/userData/posts',
|
||||
media: '/mock/userData/media',
|
||||
})),
|
||||
initializeLocal: vi.fn(),
|
||||
initializeRemote: vi.fn(),
|
||||
close: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock electron app
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
getPath: vi.fn(() => '/mock/userData'),
|
||||
},
|
||||
}));
|
||||
|
||||
import { ImportDefinitionEngine } from '../../src/main/engine/ImportDefinitionEngine';
|
||||
|
||||
describe('ImportDefinitionEngine', () => {
|
||||
let engine: ImportDefinitionEngine;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockDefinitions.clear();
|
||||
engine = new ImportDefinitionEngine();
|
||||
engine.setProjectContext('test-project');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create a new instance', () => {
|
||||
expect(engine).toBeDefined();
|
||||
expect(engine).toBeInstanceOf(ImportDefinitionEngine);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setProjectContext', () => {
|
||||
it('should set and return the current project ID', () => {
|
||||
engine.setProjectContext('project-abc');
|
||||
expect(engine.getProjectContext()).toBe('project-abc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createDefinition', () => {
|
||||
it('should create a definition with default name', async () => {
|
||||
const def = await engine.createDefinition();
|
||||
|
||||
expect(def).toBeDefined();
|
||||
expect(def.id).toMatch(/^import_/);
|
||||
expect(def.name).toBe('Untitled Import');
|
||||
expect(def.projectId).toBe('test-project');
|
||||
expect(def.wxrFilePath).toBeNull();
|
||||
expect(def.uploadsFolderPath).toBeNull();
|
||||
expect(def.lastAnalysisResult).toBeNull();
|
||||
expect(def.createdAt).toBeDefined();
|
||||
expect(def.updatedAt).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create a definition with custom name', async () => {
|
||||
const def = await engine.createDefinition('My WordPress Blog');
|
||||
|
||||
expect(def.name).toBe('My WordPress Blog');
|
||||
});
|
||||
|
||||
it('should insert into the database', async () => {
|
||||
await engine.createDefinition('Test Import');
|
||||
|
||||
expect(mockLocalClient.execute).toHaveBeenCalledTimes(1);
|
||||
const call = mockLocalClient.execute.mock.calls[0][0];
|
||||
expect(call.sql).toContain('INSERT INTO import_definitions');
|
||||
expect(call.args[2]).toBe('Test Import');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefinition', () => {
|
||||
it('should return a definition by ID', async () => {
|
||||
const created = await engine.createDefinition('My Import');
|
||||
mockLocalClient.execute.mockClear();
|
||||
|
||||
const def = await engine.getDefinition(created.id);
|
||||
|
||||
expect(def).toBeDefined();
|
||||
expect(def!.id).toBe(created.id);
|
||||
expect(def!.name).toBe('My Import');
|
||||
});
|
||||
|
||||
it('should return null for non-existent ID', async () => {
|
||||
const def = await engine.getDefinition('non-existent-id');
|
||||
|
||||
expect(def).toBeNull();
|
||||
});
|
||||
|
||||
it('should not return definitions from other projects', async () => {
|
||||
const created = await engine.createDefinition('My Import');
|
||||
engine.setProjectContext('other-project');
|
||||
|
||||
const def = await engine.getDefinition(created.id);
|
||||
|
||||
expect(def).toBeNull();
|
||||
});
|
||||
|
||||
it('should parse lastAnalysisResult JSON', async () => {
|
||||
const created = await engine.createDefinition('My Import');
|
||||
// Manually set analysis result in mock store
|
||||
const storedDef = mockDefinitions.get(created.id);
|
||||
storedDef.last_analysis_result = JSON.stringify({ posts: { total: 5 } });
|
||||
|
||||
const def = await engine.getDefinition(created.id);
|
||||
|
||||
expect(def!.lastAnalysisResult).toEqual({ posts: { total: 5 } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllForProject', () => {
|
||||
it('should return empty array when no definitions exist', async () => {
|
||||
const defs = await engine.getAllForProject();
|
||||
|
||||
expect(defs).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return all definitions for the current project', async () => {
|
||||
await engine.createDefinition('Import 1');
|
||||
await engine.createDefinition('Import 2');
|
||||
|
||||
const defs = await engine.getAllForProject();
|
||||
|
||||
expect(defs).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should not include definitions from other projects', async () => {
|
||||
await engine.createDefinition('Import A');
|
||||
engine.setProjectContext('other-project');
|
||||
await engine.createDefinition('Import B');
|
||||
engine.setProjectContext('test-project');
|
||||
|
||||
const defs = await engine.getAllForProject();
|
||||
|
||||
expect(defs).toHaveLength(1);
|
||||
expect(defs[0].name).toBe('Import A');
|
||||
});
|
||||
|
||||
it('should return definitions ordered by updatedAt DESC', async () => {
|
||||
await engine.createDefinition('Older');
|
||||
// Small delay to ensure different timestamps
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
await engine.createDefinition('Newer');
|
||||
|
||||
const defs = await engine.getAllForProject();
|
||||
|
||||
expect(defs[0].name).toBe('Newer');
|
||||
expect(defs[1].name).toBe('Older');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateDefinition', () => {
|
||||
it('should update the name', async () => {
|
||||
const created = await engine.createDefinition('Old Name');
|
||||
|
||||
const updated = await engine.updateDefinition(created.id, { name: 'New Name' });
|
||||
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated!.name).toBe('New Name');
|
||||
});
|
||||
|
||||
it('should update wxrFilePath', async () => {
|
||||
const created = await engine.createDefinition('Test');
|
||||
|
||||
const updated = await engine.updateDefinition(created.id, { wxrFilePath: '/path/to/export.xml' });
|
||||
|
||||
expect(updated!.wxrFilePath).toBe('/path/to/export.xml');
|
||||
});
|
||||
|
||||
it('should update uploadsFolderPath', async () => {
|
||||
const created = await engine.createDefinition('Test');
|
||||
|
||||
const updated = await engine.updateDefinition(created.id, { uploadsFolderPath: '/path/to/uploads' });
|
||||
|
||||
expect(updated!.uploadsFolderPath).toBe('/path/to/uploads');
|
||||
});
|
||||
|
||||
it('should update lastAnalysisResult as JSON', async () => {
|
||||
const created = await engine.createDefinition('Test');
|
||||
const report = { posts: { total: 10, new: 5 } };
|
||||
|
||||
const updated = await engine.updateDefinition(created.id, { lastAnalysisResult: JSON.stringify(report) });
|
||||
|
||||
expect(updated).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return null for non-existent definition', async () => {
|
||||
const updated = await engine.updateDefinition('non-existent', { name: 'Test' });
|
||||
|
||||
expect(updated).toBeNull();
|
||||
});
|
||||
|
||||
it('should not update definitions from other projects', async () => {
|
||||
const created = await engine.createDefinition('Test');
|
||||
engine.setProjectContext('other-project');
|
||||
|
||||
const updated = await engine.updateDefinition(created.id, { name: 'Hacked' });
|
||||
|
||||
expect(updated).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteDefinition', () => {
|
||||
it('should delete an existing definition', async () => {
|
||||
const created = await engine.createDefinition('To Delete');
|
||||
|
||||
const result = await engine.deleteDefinition(created.id);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-existent definition', async () => {
|
||||
const result = await engine.deleteDefinition('non-existent');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should not delete definitions from other projects', async () => {
|
||||
const created = await engine.createDefinition('Test');
|
||||
engine.setProjectContext('other-project');
|
||||
|
||||
const result = await engine.deleteDefinition(created.id);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should remove the definition from the database', async () => {
|
||||
const created = await engine.createDefinition('Test');
|
||||
await engine.deleteDefinition(created.id);
|
||||
|
||||
const def = await engine.getDefinition(created.id);
|
||||
expect(def).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -111,6 +111,13 @@ Object.defineProperty(globalThis, 'window', {
|
||||
analyzeFile: vi.fn(),
|
||||
selectUploadsFolder: vi.fn(),
|
||||
},
|
||||
importDefinitions: {
|
||||
create: vi.fn(),
|
||||
get: vi.fn(),
|
||||
getAll: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
on: vi.fn(() => () => {}),
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user