Reopen published posts only for changes.

This commit is contained in:
2026-07-21 21:25:16 +02:00
parent b3bf134980
commit 80f291b8ea
2 changed files with 155 additions and 5 deletions

View File

@@ -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.

View File

@@ -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();