diff --git a/README.md b/README.md index 3cdebd6..29736d6 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ The project is under active development. Core blogging workflows are broadly ava ## Available Features - Native Iced desktop workspace with localized menus, tabs, anchored editor popovers, automatically paged post/media sidebars, dialogs, tasks, embedded Wry previews, and a live Pico CSS theme editor. -- Post and translation authoring with draft/published lifecycle, metadata, tags, categories, links, media, and batch gallery-image import. +- Post and translation authoring with change-aware draft/published lifecycle, metadata, tags, categories, links, media, and batch gallery-image import. - Media import, thumbnails, metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors. - WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping. - Post, Liquid template, and Lua script editing with dedicated syntax highlighting and explicit syntax-check feedback, including publish-time enforcement of the bDS2 Liquid tag/filter/operator subset, using a custom Ropey/Syntect/Cosmic Text editor and the documented, bDS2-signature-compatible project-scoped [`bds` host API](docs/scripting/API_REFERENCE.md) across utilities, rendered macros, and Blogmark transforms, including airplane-gated Git sync. diff --git a/crates/bds-core/src/engine/post.rs b/crates/bds-core/src/engine/post.rs index e3b887a..51235dd 100644 --- a/crates/bds-core/src/engine/post.rs +++ b/crates/bds-core/src/engine/post.rs @@ -134,6 +134,29 @@ pub fn update_post( ))); } + let published_metadata_changed = post.status == PostStatus::Published + && (title.is_some_and(|value| post.title != value) + || excerpt.is_some_and(|value| post.excerpt.as_deref() != value) + || tags + .as_ref() + .is_some_and(|value| post.tags.as_slice() != value.as_slice()) + || categories + .as_ref() + .is_some_and(|value| post.categories.as_slice() != value.as_slice()) + || author.is_some_and(|value| post.author.as_deref() != value) + || language.is_some_and(|value| post.language.as_deref() != value) + || do_not_translate.is_some_and(|value| post.do_not_translate != value)); + let published_body = if post.status == PostStatus::Published + && (published_metadata_changed || content.is_some()) + { + resolve_post_fts_content(data_dir, &post)? + } else { + None + }; + let reopen_published = published_metadata_changed + || (post.status == PostStatus::Published + && content.is_some_and(|value| published_body.as_deref() != Some(value))); + if let Some(t) = title { post.title = t.to_string(); } @@ -143,7 +166,9 @@ pub fn update_post( if let Some(exc) = excerpt { post.excerpt = exc.map(|s| s.to_string()); } - if let Some(c) = content { + if let Some(c) = content + && (post.status != PostStatus::Published || reopen_published) + { post.content = Some(c.to_string()); } if let Some(t) = tags { @@ -165,9 +190,12 @@ pub fn update_post( post.do_not_translate = dnt; } - // Auto-transition published or archived post back to draft on content/metadata change - if post.status == PostStatus::Published || post.status == PostStatus::Archived { - // Reload content from filesystem if content field is NULL (published state) + if reopen_published { + if post.content.is_none() { + post.content = published_body; + } + post.status = PostStatus::Draft; + } else if post.status == PostStatus::Archived { if post.content.is_none() && !post.file_path.is_empty() { let abs_path = data_dir.join(&post.file_path); if abs_path.exists() @@ -1171,6 +1199,23 @@ mod tests { (db, dir) } + fn create_published_post(db: &Database, dir: &TempDir, title: &str, body: &str) -> Post { + let post = create_post( + db.conn(), + dir.path(), + "p1", + title, + Some(body), + vec!["tag".into()], + vec!["category".into()], + Some("Alice"), + Some("en"), + None, + ) + .unwrap(); + publish_post(db.conn(), dir.path(), &post.id).unwrap() + } + #[test] fn create_post_generates_slug_and_draft() { let (db, dir) = setup(); @@ -1372,6 +1417,111 @@ mod tests { assert!(updated.do_not_translate); } + #[test] + fn identical_published_update_stays_published() { + let (db, dir) = setup(); + let post = create_published_post(&db, &dir, "Published", "body"); + + let updated = update_post( + db.conn(), + dir.path(), + &post.id, + Some("Published"), + None, + Some(None), + Some("body"), + Some(vec!["tag".into()]), + Some(vec!["category".into()]), + Some(Some("Alice")), + Some(Some("en")), + Some(None), + Some(false), + ) + .unwrap(); + + assert_eq!(updated.status, PostStatus::Published); + assert_eq!(updated.content, None); + } + + #[test] + fn published_title_change_reopens_draft_with_file_body() { + let (db, dir) = setup(); + let post = create_published_post(&db, &dir, "Published", "body"); + + let updated = update_post( + db.conn(), + dir.path(), + &post.id, + Some("Changed"), + None, + None, + None, + None, + None, + None, + None, + None, + None, + ) + .unwrap(); + + assert_eq!(updated.status, PostStatus::Draft); + assert_eq!(updated.content.as_deref(), Some("body")); + } + + #[test] + fn published_template_slug_only_change_stays_published() { + let (db, dir) = setup(); + let post = create_published_post(&db, &dir, "Published", "body"); + + let updated = update_post( + db.conn(), + dir.path(), + &post.id, + None, + None, + None, + None, + None, + None, + None, + None, + Some(Some("page")), + None, + ) + .unwrap(); + + assert_eq!(updated.status, PostStatus::Published); + assert_eq!(updated.template_slug.as_deref(), Some("page")); + assert_eq!(updated.content, None); + } + + #[test] + fn published_content_change_reopens_draft_with_new_body() { + let (db, dir) = setup(); + let post = create_published_post(&db, &dir, "Published", "body"); + + let updated = update_post( + db.conn(), + dir.path(), + &post.id, + None, + None, + None, + Some("changed body"), + None, + None, + None, + None, + None, + None, + ) + .unwrap(); + + assert_eq!(updated.status, PostStatus::Draft); + assert_eq!(updated.content.as_deref(), Some("changed body")); + } + #[test] fn update_post_slug_frozen_after_publish() { let (db, dir) = setup();