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 }>;
|
||||
|
||||
Reference in New Issue
Block a user