feat: author support and UI support for multi-category

This commit is contained in:
2026-02-15 16:51:29 +01:00
parent 21ed992727
commit 14be7aa7af
12 changed files with 936 additions and 7 deletions

View File

@@ -0,0 +1 @@
ALTER TABLE `media` ADD `author` text;

View File

@@ -0,0 +1,766 @@
{
"version": "6",
"dialect": "sqlite",
"id": "602674b9-0b1e-4d1b-aed3-125bac4d1dda",
"prevId": "26aa5345-b6d8-4426-a144-0199140a896a",
"tables": {
"chat_conversations": {
"name": "chat_conversations",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"model": {
"name": "model",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"copilot_session_id": {
"name": "copilot_session_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"chat_messages": {
"name": "chat_messages",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"conversation_id": {
"name": "conversation_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"tool_call_id": {
"name": "tool_call_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"tool_calls": {
"name": "tool_calls",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"import_definitions": {
"name": "import_definitions",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"project_id": {
"name": "project_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"wxr_file_path": {
"name": "wxr_file_path",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"uploads_folder_path": {
"name": "uploads_folder_path",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"last_analysis_result": {
"name": "last_analysis_result",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"media": {
"name": "media",
"columns": {
"project_id": {
"name": "project_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"filename": {
"name": "filename",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"original_name": {
"name": "original_name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"mime_type": {
"name": "mime_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"size": {
"name": "size",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"width": {
"name": "width",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"height": {
"name": "height",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"alt": {
"name": "alt",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"caption": {
"name": "caption",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"author": {
"name": "author",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"file_path": {
"name": "file_path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"sidecar_path": {
"name": "sidecar_path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"checksum": {
"name": "checksum",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"tags": {
"name": "tags",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"post_links": {
"name": "post_links",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"source_post_id": {
"name": "source_post_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"target_post_id": {
"name": "target_post_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"link_text": {
"name": "link_text",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"post_media": {
"name": "post_media",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"project_id": {
"name": "project_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"post_id": {
"name": "post_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"media_id": {
"name": "media_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"sort_order": {
"name": "sort_order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"post_media_post_media_idx": {
"name": "post_media_post_media_idx",
"columns": [
"post_id",
"media_id"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"posts": {
"name": "posts",
"columns": {
"project_id": {
"name": "project_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"excerpt": {
"name": "excerpt",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'draft'"
},
"author": {
"name": "author",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"published_at": {
"name": "published_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"file_path": {
"name": "file_path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"checksum": {
"name": "checksum",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"tags": {
"name": "tags",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"categories": {
"name": "categories",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"published_title": {
"name": "published_title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"published_content": {
"name": "published_content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"published_tags": {
"name": "published_tags",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"published_categories": {
"name": "published_categories",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"published_excerpt": {
"name": "published_excerpt",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"posts_project_slug_idx": {
"name": "posts_project_slug_idx",
"columns": [
"project_id",
"slug"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"projects": {
"name": "projects",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"data_path": {
"name": "data_path",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_active": {
"name": "is_active",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
}
},
"indexes": {
"projects_slug_unique": {
"name": "projects_slug_unique",
"columns": [
"slug"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"settings": {
"name": "settings",
"columns": {
"key": {
"name": "key",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"tags": {
"name": "tags",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"project_id": {
"name": "project_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"color": {
"name": "color",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"tags_project_name_idx": {
"name": "tags_project_name_idx",
"columns": [
"project_id",
"name"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -22,6 +22,13 @@
"when": 1771141922712, "when": 1771141922712,
"tag": "0002_rainy_luckman", "tag": "0002_rainy_luckman",
"breakpoints": true "breakpoints": true
},
{
"idx": 3,
"version": "6",
"when": 1771168311850,
"tag": "0003_foamy_whiplash",
"breakpoints": true
} }
] ]
} }

View File

@@ -57,6 +57,7 @@ export const media = sqliteTable('media', {
title: text('title'), title: text('title'),
alt: text('alt'), alt: text('alt'),
caption: text('caption'), caption: text('caption'),
author: text('author'),
filePath: text('file_path').notNull(), filePath: text('file_path').notNull(),
sidecarPath: text('sidecar_path').notNull(), sidecarPath: text('sidecar_path').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),

View File

@@ -36,6 +36,8 @@ import type { WxrPost, WxrMedia } from './WxrParser';
export interface ImportExecutionOptions { export interface ImportExecutionOptions {
/** Path to the WordPress uploads folder for media files */ /** Path to the WordPress uploads folder for media files */
uploadsFolder?: string; uploadsFolder?: string;
/** Default author to use when WXR post/media has no author */
defaultAuthor?: string;
/** Progress callback */ /** Progress callback */
onProgress?: (phase: string, current: number, total: number, detail?: string) => void; onProgress?: (phase: string, current: number, total: number, detail?: string) => void;
} }
@@ -461,7 +463,7 @@ export class ImportExecutionEngine extends EventEmitter {
excerpt: wxrPost.excerpt || undefined, excerpt: wxrPost.excerpt || undefined,
content: transformedContent, content: transformedContent,
status, status,
author: wxrPost.creator || undefined, author: wxrPost.creator || options.defaultAuthor || undefined,
createdAt, createdAt,
updatedAt, updatedAt,
publishedAt, publishedAt,
@@ -634,6 +636,7 @@ export class ImportExecutionEngine extends EventEmitter {
title: wxrMedia.title || undefined, title: wxrMedia.title || undefined,
alt: wxrMedia.description || undefined, alt: wxrMedia.description || undefined,
mimeType: wxrMedia.mimeType, mimeType: wxrMedia.mimeType,
author: options.defaultAuthor,
tags: [], tags: [],
linkedPostIds, linkedPostIds,
createdAt, createdAt,

View File

@@ -30,6 +30,7 @@ export interface MediaData {
title?: string; title?: string;
alt?: string; alt?: string;
caption?: string; caption?: string;
author?: string;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
tags: string[]; tags: string[];
@@ -46,6 +47,7 @@ export interface MediaMetadata {
title?: string; title?: string;
alt?: string; alt?: string;
caption?: string; caption?: string;
author?: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
tags: string[]; tags: string[];
@@ -317,6 +319,7 @@ export class MediaEngine extends EventEmitter {
title: mediaData.title, title: mediaData.title,
alt: mediaData.alt, alt: mediaData.alt,
caption: mediaData.caption, caption: mediaData.caption,
author: mediaData.author,
createdAt: mediaData.createdAt.toISOString(), createdAt: mediaData.createdAt.toISOString(),
updatedAt: mediaData.updatedAt.toISOString(), updatedAt: mediaData.updatedAt.toISOString(),
tags: mediaData.tags, tags: mediaData.tags,
@@ -337,6 +340,7 @@ export class MediaEngine extends EventEmitter {
if (metadata.title) lines.push(`title: "${metadata.title}"`); if (metadata.title) lines.push(`title: "${metadata.title}"`);
if (metadata.alt) lines.push(`alt: "${metadata.alt}"`); if (metadata.alt) lines.push(`alt: "${metadata.alt}"`);
if (metadata.caption) lines.push(`caption: "${metadata.caption}"`); if (metadata.caption) lines.push(`caption: "${metadata.caption}"`);
if (metadata.author) lines.push(`author: "${metadata.author}"`);
lines.push(`createdAt: ${metadata.createdAt}`); lines.push(`createdAt: ${metadata.createdAt}`);
lines.push(`updatedAt: ${metadata.updatedAt}`); lines.push(`updatedAt: ${metadata.updatedAt}`);
@@ -410,6 +414,9 @@ export class MediaEngine extends EventEmitter {
case 'caption': case 'caption':
metadata.caption = value; metadata.caption = value;
break; break;
case 'author':
metadata.author = value;
break;
case 'createdAt': case 'createdAt':
metadata.createdAt = value; metadata.createdAt = value;
break; break;
@@ -514,6 +521,7 @@ export class MediaEngine extends EventEmitter {
title: metadata?.title, title: metadata?.title,
alt: metadata?.alt, alt: metadata?.alt,
caption: metadata?.caption, caption: metadata?.caption,
author: metadata?.author,
createdAt, createdAt,
updatedAt, updatedAt,
tags: metadata?.tags || [], tags: metadata?.tags || [],
@@ -541,6 +549,7 @@ export class MediaEngine extends EventEmitter {
title: mediaData.title, title: mediaData.title,
alt: mediaData.alt, alt: mediaData.alt,
caption: mediaData.caption, caption: mediaData.caption,
author: mediaData.author,
filePath: destPath, filePath: destPath,
sidecarPath, sidecarPath,
createdAt: mediaData.createdAt, createdAt: mediaData.createdAt,
@@ -591,6 +600,7 @@ export class MediaEngine extends EventEmitter {
title: updated.title, title: updated.title,
alt: updated.alt, alt: updated.alt,
caption: updated.caption, caption: updated.caption,
author: updated.author,
updatedAt: updated.updatedAt, updatedAt: updated.updatedAt,
tags: JSON.stringify(updated.tags), tags: JSON.stringify(updated.tags),
}) })
@@ -740,6 +750,7 @@ export class MediaEngine extends EventEmitter {
title: dbMedia.title || undefined, title: dbMedia.title || undefined,
alt: dbMedia.alt || undefined, alt: dbMedia.alt || undefined,
caption: dbMedia.caption || undefined, caption: dbMedia.caption || undefined,
author: dbMedia.author || undefined,
createdAt: dbMedia.createdAt, createdAt: dbMedia.createdAt,
updatedAt: dbMedia.updatedAt, updatedAt: dbMedia.updatedAt,
tags: JSON.parse(dbMedia.tags || '[]'), tags: JSON.parse(dbMedia.tags || '[]'),
@@ -766,6 +777,7 @@ export class MediaEngine extends EventEmitter {
title: dbMedia.title || undefined, title: dbMedia.title || undefined,
alt: dbMedia.alt || undefined, alt: dbMedia.alt || undefined,
caption: dbMedia.caption || undefined, caption: dbMedia.caption || undefined,
author: dbMedia.author || undefined,
createdAt: dbMedia.createdAt, createdAt: dbMedia.createdAt,
updatedAt: dbMedia.updatedAt, updatedAt: dbMedia.updatedAt,
tags: JSON.parse(dbMedia.tags || '[]'), tags: JSON.parse(dbMedia.tags || '[]'),
@@ -827,6 +839,7 @@ export class MediaEngine extends EventEmitter {
title: dbMedia.title || undefined, title: dbMedia.title || undefined,
alt: dbMedia.alt || undefined, alt: dbMedia.alt || undefined,
caption: dbMedia.caption || undefined, caption: dbMedia.caption || undefined,
author: dbMedia.author || undefined,
createdAt: dbMedia.createdAt, createdAt: dbMedia.createdAt,
updatedAt: dbMedia.updatedAt, updatedAt: dbMedia.updatedAt,
tags: JSON.parse(dbMedia.tags || '[]'), tags: JSON.parse(dbMedia.tags || '[]'),

View File

@@ -14,6 +14,7 @@ export interface ProjectMetadata {
description?: string; description?: string;
dataPath?: string; // Custom path for project data dataPath?: string; // Custom path for project data
mainLanguage?: string; // Main language for AI-generated content (ISO code, e.g., 'en', 'de', 'es') mainLanguage?: string; // Main language for AI-generated content (ISO code, e.g., 'en', 'de', 'es')
defaultAuthor?: string; // Default author for new posts and media
} }
/** /**

View File

@@ -138,6 +138,16 @@ export function registerIpcHandlers(): void {
safeHandle('posts:create', async (_, data: Partial<PostData>) => { safeHandle('posts:create', async (_, data: Partial<PostData>) => {
const engine = getPostEngine(); const engine = getPostEngine();
// If no author provided, use default author from project settings
if (!data.author) {
const metaEngine = getMetaEngine();
const metadata = await metaEngine.getProjectMetadata();
if (metadata?.defaultAuthor) {
data.author = metadata.defaultAuthor;
}
}
return engine.createPost(data); return engine.createPost(data);
}); });
@@ -279,6 +289,17 @@ export function registerIpcHandlers(): void {
safeHandle('media:import', async (_, sourcePath: string, metadata?: Partial<MediaData>) => { safeHandle('media:import', async (_, sourcePath: string, metadata?: Partial<MediaData>) => {
const engine = getMediaEngine(); const engine = getMediaEngine();
// If no author provided, use default author from project settings
if (!metadata?.author) {
const metaEngine = getMetaEngine();
const projectMetadata = await metaEngine.getProjectMetadata();
if (projectMetadata?.defaultAuthor) {
metadata = metadata || {};
metadata.author = projectMetadata.defaultAuthor;
}
}
return engine.importMedia(sourcePath, metadata); return engine.importMedia(sourcePath, metadata);
}); });
@@ -309,9 +330,14 @@ export function registerIpcHandlers(): void {
const imported: MediaData[] = []; const imported: MediaData[] = [];
// Get default author from project settings
const metaEngine = getMetaEngine();
const projectMetadata = await metaEngine.getProjectMetadata();
const defaultAuthor = projectMetadata?.defaultAuthor;
for (const filePath of result.filePaths) { for (const filePath of result.filePaths) {
try { try {
const media = await engine.importMedia(filePath); const media = await engine.importMedia(filePath, defaultAuthor ? { author: defaultAuthor } : undefined);
imported.push(media); imported.push(media);
} catch (error) { } catch (error) {
console.error(`Failed to import ${filePath}:`, error); console.error(`Failed to import ${filePath}:`, error);
@@ -881,8 +907,14 @@ export function registerIpcHandlers(): void {
executionEngine.setProjectContext(activeProject.id, activeProject.dataPath); executionEngine.setProjectContext(activeProject.id, activeProject.dataPath);
} }
// Get default author from project settings
const metaEngine = getMetaEngine();
const projectMetadata = await metaEngine.getProjectMetadata();
const defaultAuthor = projectMetadata?.defaultAuthor;
const result = await executionEngine.executeImport(report, { const result = await executionEngine.executeImport(report, {
uploadsFolder, uploadsFolder,
defaultAuthor,
onProgress: (phase, current, total, detail) => { onProgress: (phase, current, total, detail) => {
// Update processed items count based on phase progress // Update processed items count based on phase progress
processedItems++; processedItems++;

View File

@@ -884,7 +884,7 @@ const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
title, title,
content, content,
tags, tags,
categories: category ? [category] : ['article'], categories: selectedCategories.length > 0 ? selectedCategories : ['article'],
}); });
if (updated) { if (updated) {
@@ -903,7 +903,7 @@ const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
}, [postId, title, content, tags, category, isDirty, isSaving, updatePost, markClean, showErrorModal]); }, [postId, title, content, tags, selectedCategories, isDirty, isSaving, updatePost, markClean, showErrorModal]);
const handlePublish = async () => { const handlePublish = async () => {
await handleSave(); await handleSave();

View File

@@ -102,6 +102,7 @@ export const SettingsView: React.FC = () => {
const [projectDataPath, setProjectDataPath] = useState(''); const [projectDataPath, setProjectDataPath] = useState('');
const [defaultProjectPath, setDefaultProjectPath] = useState(''); const [defaultProjectPath, setDefaultProjectPath] = useState('');
const [projectMainLanguage, setProjectMainLanguage] = useState('en'); const [projectMainLanguage, setProjectMainLanguage] = useState('en');
const [projectDefaultAuthor, setProjectDefaultAuthor] = useState('');
// Post categories management // Post categories management
const [postCategories, setPostCategories] = useState<string[]>(DEFAULT_POST_CATEGORIES); const [postCategories, setPostCategories] = useState<string[]>(DEFAULT_POST_CATEGORIES);
@@ -136,11 +137,16 @@ export const SettingsView: React.FC = () => {
setDefaultProjectPath(path); setDefaultProjectPath(path);
}); });
// Load project metadata (includes mainLanguage) // Load project metadata (includes mainLanguage and defaultAuthor)
window.electronAPI?.meta.getProjectMetadata().then(metadata => { window.electronAPI?.meta.getProjectMetadata().then(metadata => {
if (metadata?.mainLanguage) { if (metadata?.mainLanguage) {
setProjectMainLanguage(metadata.mainLanguage); setProjectMainLanguage(metadata.mainLanguage);
} }
if (metadata?.defaultAuthor) {
setProjectDefaultAuthor(metadata.defaultAuthor);
} else {
setProjectDefaultAuthor('');
}
}); });
} }
}, [activeProject]); }, [activeProject]);
@@ -233,12 +239,13 @@ export const SettingsView: React.FC = () => {
setActiveProject(updated as any); setActiveProject(updated as any);
useAppStore.getState().updateProject(activeProject.id, updated as any); useAppStore.getState().updateProject(activeProject.id, updated as any);
// Also update project.json to keep dataPath and mainLanguage in sync // Also update project.json to keep dataPath, mainLanguage, and defaultAuthor in sync
await window.electronAPI?.meta.updateProjectMetadata({ await window.electronAPI?.meta.updateProjectMetadata({
name: projectName.trim() || activeProject.name, name: projectName.trim() || activeProject.name,
description: projectDescription.trim(), description: projectDescription.trim(),
dataPath: projectDataPath.trim() || undefined, dataPath: projectDataPath.trim() || undefined,
mainLanguage: projectMainLanguage, mainLanguage: projectMainLanguage,
defaultAuthor: projectDefaultAuthor.trim() || undefined,
}); });
} }
showToast.success('Project settings saved'); showToast.success('Project settings saved');
@@ -260,7 +267,7 @@ export const SettingsView: React.FC = () => {
}; };
// Keywords for each section for search filtering // Keywords for each section for search filtering
const projectKeywords = ['project', 'name', 'description', 'blog', 'site', 'path', 'folder', 'location', 'data', 'language']; const projectKeywords = ['project', 'name', 'description', 'blog', 'site', 'path', 'folder', 'location', 'data', 'language', 'author', 'default'];
const editorKeywords = ['editor', 'mode', 'wysiwyg', 'markdown', 'preview', 'visual']; const editorKeywords = ['editor', 'mode', 'wysiwyg', 'markdown', 'preview', 'visual'];
const contentKeywords = ['content', 'categories', 'post', 'article', 'picture', 'aside', 'page']; const contentKeywords = ['content', 'categories', 'post', 'article', 'picture', 'aside', 'page'];
const aiKeywords = ['ai', 'assistant', 'chat', 'model', 'prompt', 'system', 'api', 'key', 'claude', 'gpt', 'opencode']; const aiKeywords = ['ai', 'assistant', 'chat', 'model', 'prompt', 'system', 'api', 'key', 'claude', 'gpt', 'opencode'];
@@ -359,6 +366,20 @@ export const SettingsView: React.FC = () => {
</select> </select>
</SettingRow> </SettingRow>
<SettingRow
id="project-author"
label="Default Author"
description="The default author name for new posts and media. Can be overridden per item."
>
<input
id="project-author"
type="text"
placeholder="Author Name"
value={projectDefaultAuthor}
onChange={(e) => setProjectDefaultAuthor(e.target.value)}
/>
</SettingRow>
<div className="setting-actions"> <div className="setting-actions">
<button className="primary" onClick={handleSaveProject}> <button className="primary" onClick={handleSaveProject}>
Save Project Settings Save Project Settings

View File

@@ -582,6 +582,38 @@ describe('MediaEngine', () => {
}); });
}); });
describe('Author Field', () => {
beforeEach(() => {
mockFiles.set('/source/author-test.jpg', Buffer.from('image-data'));
});
it('should store author when provided during import', async () => {
const media = await mediaEngine.importMedia('/source/author-test.jpg', {
author: 'John Doe',
});
expect(media.author).toBe('John Doe');
});
it('should handle media without author', async () => {
const media = await mediaEngine.importMedia('/source/author-test.jpg');
expect(media.author).toBeUndefined();
});
it('should include author in database insert', async () => {
await mediaEngine.importMedia('/source/author-test.jpg', {
author: 'Jane Smith',
});
// Check that the database insert was called with author
expect(mockLocalDb.insert).toHaveBeenCalled();
// The mock captures the inserted data in mockMedia
const insertedMedia = Array.from(mockMedia.values()).pop();
expect(insertedMedia?.author).toBe('Jane Smith');
});
});
describe('Date-based folder structure', () => { describe('Date-based folder structure', () => {
beforeEach(() => { beforeEach(() => {
mockFiles.set('/source/dated-image.jpg', Buffer.from('image-data')); mockFiles.set('/source/dated-image.jpg', Buffer.from('image-data'));

View File

@@ -498,6 +498,58 @@ describe('MetaEngine', () => {
consoleErrorSpy.mockRestore(); consoleErrorSpy.mockRestore();
}); });
it('should set and get defaultAuthor in project metadata', async () => {
await metaEngine.setProjectMetadata({
name: 'My Blog',
description: 'A blog',
defaultAuthor: 'John Doe',
});
const metadata = await metaEngine.getProjectMetadata();
expect(metadata?.defaultAuthor).toBe('John Doe');
});
it('should update defaultAuthor only', async () => {
await metaEngine.setProjectMetadata({
name: 'My Blog',
description: 'A blog',
});
await metaEngine.updateProjectMetadata({ defaultAuthor: 'Jane Smith' });
const metadata = await metaEngine.getProjectMetadata();
expect(metadata?.name).toBe('My Blog');
expect(metadata?.defaultAuthor).toBe('Jane Smith');
});
it('should persist defaultAuthor to filesystem', async () => {
await metaEngine.setProjectMetadata({
name: 'Test Project',
defaultAuthor: 'Author Name',
});
const metaDir = metaEngine.getMetaDir();
const projectPath = normalizePath(`${metaDir}/project.json`);
const content = mockFiles.get(projectPath);
const parsed = JSON.parse(content!);
expect(parsed.defaultAuthor).toBe('Author Name');
});
it('should load defaultAuthor from filesystem', async () => {
const metaDir = metaEngine.getMetaDir();
const projectPath = normalizePath(`${metaDir}/project.json`);
mockFiles.set(projectPath, JSON.stringify({
name: 'Loaded Project',
defaultAuthor: 'Loaded Author',
}));
await metaEngine.loadProjectMetadata();
const metadata = await metaEngine.getProjectMetadata();
expect(metadata?.defaultAuthor).toBe('Loaded Author');
});
it('should handle ENOENT error when loading categories (no file)', async () => { it('should handle ENOENT error when loading categories (no file)', async () => {
// No file exists, should not throw // No file exists, should not throw
await metaEngine.loadCategories(); await metaEngine.loadCategories();