Compare commits
7 Commits
486a9f8019
...
9d3a092359
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d3a092359 | ||
|
|
1ecd7a32d2 | ||
|
|
0b4c303c89 | ||
|
|
18d96a10b8 | ||
|
|
184d6e4845 | ||
|
|
d50e538fc3 | ||
|
|
1e6c2a4e2c |
@@ -915,7 +915,7 @@ fn terminate_child(child: &mut std::process::Child) {
|
|||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepend_tool_paths(command: &mut Command) {
|
fn prepend_tool_paths(_command: &mut Command) {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
let mut paths = vec![
|
let mut paths = vec![
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ pub fn add_category(data_dir: &Path, category: &str) -> EngineResult<()> {
|
|||||||
meta.insert(
|
meta.insert(
|
||||||
category.to_string(),
|
category.to_string(),
|
||||||
CategorySettings {
|
CategorySettings {
|
||||||
|
title: None,
|
||||||
render_in_lists: true,
|
render_in_lists: true,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
@@ -318,6 +319,7 @@ mod tests {
|
|||||||
meta.insert(
|
meta.insert(
|
||||||
"article".to_string(),
|
"article".to_string(),
|
||||||
CategorySettings {
|
CategorySettings {
|
||||||
|
title: None,
|
||||||
render_in_lists: true,
|
render_in_lists: true,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
@@ -330,6 +332,33 @@ mod tests {
|
|||||||
assert!(read["article"].render_in_lists);
|
assert!(read["article"].render_in_lists);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn category_meta_json_preserves_bds2_title() {
|
||||||
|
let dir = setup();
|
||||||
|
let mut meta = HashMap::new();
|
||||||
|
meta.insert(
|
||||||
|
"news".to_string(),
|
||||||
|
CategorySettings {
|
||||||
|
title: Some("News Archive".to_string()),
|
||||||
|
render_in_lists: false,
|
||||||
|
show_title: true,
|
||||||
|
post_template_slug: Some("article".to_string()),
|
||||||
|
list_template_slug: Some("listing".to_string()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
write_category_meta_json(dir.path(), &meta).unwrap();
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(dir.path().join("meta/category-meta.json")).unwrap();
|
||||||
|
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
|
||||||
|
assert_eq!(json["news"]["title"], "News Archive");
|
||||||
|
assert_eq!(json["news"]["renderInLists"], false);
|
||||||
|
assert_eq!(json["news"]["showTitle"], true);
|
||||||
|
|
||||||
|
let read = read_category_meta_json(dir.path()).unwrap();
|
||||||
|
assert_eq!(read["news"].title.as_deref(), Some("News Archive"));
|
||||||
|
}
|
||||||
|
|
||||||
// ── publishing.json ─────────────────────────────────────────────
|
// ── publishing.json ─────────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -407,6 +436,7 @@ mod tests {
|
|||||||
meta.insert(
|
meta.insert(
|
||||||
"article".to_string(),
|
"article".to_string(),
|
||||||
CategorySettings {
|
CategorySettings {
|
||||||
|
title: None,
|
||||||
render_in_lists: true,
|
render_in_lists: true,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ pub fn create_post(
|
|||||||
qp::insert_post(conn, &post)?;
|
qp::insert_post(conn, &post)?;
|
||||||
|
|
||||||
// Index for FTS
|
// Index for FTS
|
||||||
fts_index_post(conn, &post)?;
|
fts_index_post(conn, data_dir, &post)?;
|
||||||
|
|
||||||
emit_post(&post, NotificationAction::Created);
|
emit_post(&post, NotificationAction::Created);
|
||||||
crate::engine::embedding::sync_post_best_effort(conn, data_dir, &post);
|
crate::engine::embedding::sync_post_best_effort(conn, data_dir, &post);
|
||||||
@@ -185,7 +185,7 @@ pub fn update_post(
|
|||||||
qp::update_post(conn, &post)?;
|
qp::update_post(conn, &post)?;
|
||||||
|
|
||||||
// Re-index FTS
|
// Re-index FTS
|
||||||
fts_index_post(conn, &post)?;
|
fts_index_post(conn, data_dir, &post)?;
|
||||||
|
|
||||||
emit_post(&post, NotificationAction::Updated);
|
emit_post(&post, NotificationAction::Updated);
|
||||||
crate::engine::embedding::sync_post_best_effort(conn, data_dir, &post);
|
crate::engine::embedding::sync_post_best_effort(conn, data_dir, &post);
|
||||||
@@ -319,7 +319,7 @@ fn publish_post_in_savepoint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re-index FTS
|
// Re-index FTS
|
||||||
fts_index_post(conn, &post)?;
|
fts_index_post(conn, data_dir, &post)?;
|
||||||
|
|
||||||
Ok(post)
|
Ok(post)
|
||||||
}
|
}
|
||||||
@@ -454,7 +454,7 @@ pub fn discard_post_draft(conn: &Connection, data_dir: &Path, post_id: &str) ->
|
|||||||
conn.begin_savepoint()?;
|
conn.begin_savepoint()?;
|
||||||
match (|| {
|
match (|| {
|
||||||
qp::update_post(conn, &post)?;
|
qp::update_post(conn, &post)?;
|
||||||
fts_index_post(conn, &post)?;
|
fts_index_post(conn, data_dir, &post)?;
|
||||||
Ok(post)
|
Ok(post)
|
||||||
})() {
|
})() {
|
||||||
Ok(post) => {
|
Ok(post) => {
|
||||||
@@ -571,7 +571,7 @@ pub fn upsert_translation(
|
|||||||
qt::update_post_translation(conn, &t)?;
|
qt::update_post_translation(conn, &t)?;
|
||||||
|
|
||||||
// Re-index FTS for parent post
|
// Re-index FTS for parent post
|
||||||
fts_index_post(conn, &post)?;
|
fts_index_post(conn, data_dir, &post)?;
|
||||||
|
|
||||||
Ok(t)
|
Ok(t)
|
||||||
}
|
}
|
||||||
@@ -596,7 +596,7 @@ pub fn upsert_translation(
|
|||||||
qt::insert_post_translation(conn, &t)?;
|
qt::insert_post_translation(conn, &t)?;
|
||||||
|
|
||||||
// Re-index FTS for parent post
|
// Re-index FTS for parent post
|
||||||
fts_index_post(conn, &post)?;
|
fts_index_post(conn, data_dir, &post)?;
|
||||||
|
|
||||||
Ok(t)
|
Ok(t)
|
||||||
}
|
}
|
||||||
@@ -623,7 +623,7 @@ pub fn delete_translation(
|
|||||||
|
|
||||||
// Re-index FTS for parent post
|
// Re-index FTS for parent post
|
||||||
if let Ok(post) = qp::get_post_by_id(conn, &t.translation_for) {
|
if let Ok(post) = qp::get_post_by_id(conn, &t.translation_for) {
|
||||||
fts_index_post(conn, &post)?;
|
fts_index_post(conn, data_dir, &post)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -643,6 +643,7 @@ pub fn publish_post_translation(
|
|||||||
conn.begin_savepoint()?;
|
conn.begin_savepoint()?;
|
||||||
match publish_translation(conn, data_dir, &mut translation, &post) {
|
match publish_translation(conn, data_dir, &mut translation, &post) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
|
fts_index_post(conn, data_dir, &post)?;
|
||||||
conn.release_savepoint()?;
|
conn.release_savepoint()?;
|
||||||
Ok(translation)
|
Ok(translation)
|
||||||
}
|
}
|
||||||
@@ -747,7 +748,7 @@ pub fn rebuild_posts_from_filesystem_with_progress(
|
|||||||
// Re-index FTS for all posts in this project
|
// Re-index FTS for all posts in this project
|
||||||
let posts = qp::list_posts_by_project(conn, project_id)?;
|
let posts = qp::list_posts_by_project(conn, project_id)?;
|
||||||
for post in &posts {
|
for post in &posts {
|
||||||
fts_index_post(conn, post)?;
|
fts_index_post(conn, data_dir, post)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(report)
|
Ok(report)
|
||||||
@@ -895,25 +896,32 @@ fn publish_translation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Index a post in FTS, gathering translation texts.
|
/// Index a post in FTS, gathering translation texts.
|
||||||
fn fts_index_post(conn: &Connection, post: &Post) -> EngineResult<()> {
|
fn fts_index_post(conn: &Connection, data_dir: &Path, post: &Post) -> EngineResult<()> {
|
||||||
let translations = qt::list_post_translations_by_post(conn, &post.id).unwrap_or_default();
|
let translations = qt::list_post_translations_by_post(conn, &post.id).unwrap_or_default();
|
||||||
let translation_data: Vec<fts::PostTranslationFts> = translations
|
let translation_data: Vec<fts::PostTranslationFts> = translations
|
||||||
.iter()
|
.iter()
|
||||||
.map(|t| fts::PostTranslationFts {
|
.map(|t| {
|
||||||
title: t.title.clone(),
|
Ok(fts::PostTranslationFts {
|
||||||
excerpt: t.excerpt.clone(),
|
title: t.title.clone(),
|
||||||
content: t.content.clone(),
|
excerpt: t.excerpt.clone(),
|
||||||
language: t.language.clone(),
|
content: resolve_translation_fts_content(data_dir, t)?,
|
||||||
|
language: t.language.clone(),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect::<EngineResult<_>>()?;
|
||||||
|
|
||||||
let lang = post.language.as_deref().unwrap_or("en");
|
let main_language = crate::engine::meta::read_project_json(data_dir)
|
||||||
|
.ok()
|
||||||
|
.and_then(|metadata| metadata.main_language)
|
||||||
|
.unwrap_or_else(|| "en".to_string());
|
||||||
|
let content = resolve_post_fts_content(data_dir, post)?;
|
||||||
|
let lang = post.language.as_deref().unwrap_or(&main_language);
|
||||||
fts::index_post(
|
fts::index_post(
|
||||||
conn,
|
conn,
|
||||||
&post.id,
|
&post.id,
|
||||||
&post.title,
|
&post.title,
|
||||||
post.excerpt.as_deref(),
|
post.excerpt.as_deref(),
|
||||||
post.content.as_deref(),
|
content.as_deref(),
|
||||||
&post.tags,
|
&post.tags,
|
||||||
&post.categories,
|
&post.categories,
|
||||||
&translation_data,
|
&translation_data,
|
||||||
@@ -922,6 +930,33 @@ fn fts_index_post(conn: &Connection, post: &Post) -> EngineResult<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_post_fts_content(data_dir: &Path, post: &Post) -> EngineResult<Option<String>> {
|
||||||
|
if post.content.is_some() {
|
||||||
|
return Ok(post.content.clone());
|
||||||
|
}
|
||||||
|
if post.file_path.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let raw = fs::read_to_string(data_dir.join(&post.file_path))?;
|
||||||
|
let (_fm, body) = read_post_file(&raw).map_err(EngineError::Parse)?;
|
||||||
|
Ok(Some(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_translation_fts_content(
|
||||||
|
data_dir: &Path,
|
||||||
|
translation: &PostTranslation,
|
||||||
|
) -> EngineResult<Option<String>> {
|
||||||
|
if translation.content.is_some() {
|
||||||
|
return Ok(translation.content.clone());
|
||||||
|
}
|
||||||
|
if translation.file_path.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let raw = fs::read_to_string(data_dir.join(&translation.file_path))?;
|
||||||
|
let (_fm, body) = read_translation_file(&raw).map_err(EngineError::Parse)?;
|
||||||
|
Ok(Some(body))
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if a file stem looks like a translation filename: `{slug}.{lang}`
|
/// Check if a file stem looks like a translation filename: `{slug}.{lang}`
|
||||||
/// where lang is a 2-letter code. We look for a dot followed by exactly 2 lowercase letters.
|
/// where lang is a 2-letter code. We look for a dot followed by exactly 2 lowercase letters.
|
||||||
pub(crate) fn is_translation_filename(stem: &str) -> bool {
|
pub(crate) fn is_translation_filename(stem: &str) -> bool {
|
||||||
@@ -1190,6 +1225,64 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn publish_indexes_post_body_from_file_when_db_content_is_cleared() {
|
||||||
|
let (db, dir) = setup();
|
||||||
|
let post = create_post(
|
||||||
|
db.conn(),
|
||||||
|
dir.path(),
|
||||||
|
"p1",
|
||||||
|
"Published Search",
|
||||||
|
Some("distinctive quokkafire body"),
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
None,
|
||||||
|
Some("en"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
publish_post(db.conn(), dir.path(), &post.id).unwrap();
|
||||||
|
let stored = qp::get_post_by_id(db.conn(), &post.id).unwrap();
|
||||||
|
assert!(stored.content.is_none());
|
||||||
|
assert_eq!(
|
||||||
|
crate::db::fts::search_posts(db.conn(), "quokkafire", "en").unwrap(),
|
||||||
|
vec![post.id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn publish_indexes_translation_body_from_file_when_db_content_is_cleared() {
|
||||||
|
let (db, dir) = setup();
|
||||||
|
let post = create_post(
|
||||||
|
db.conn(),
|
||||||
|
dir.path(),
|
||||||
|
"p1",
|
||||||
|
"Translated Search",
|
||||||
|
Some("canonical body"),
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
None,
|
||||||
|
Some("en"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
upsert_translation(
|
||||||
|
db.conn(),
|
||||||
|
dir.path(),
|
||||||
|
&post.id,
|
||||||
|
"de",
|
||||||
|
"Übersetzte Suche",
|
||||||
|
None,
|
||||||
|
Some("markantes drachenfalter wort"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
publish_post(db.conn(), dir.path(), &post.id).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
crate::db::fts::search_posts(db.conn(), "drachenfalter", "de").unwrap(),
|
||||||
|
vec![post.id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create_post_empty_title_uses_untitled() {
|
fn create_post_empty_title_uses_untitled() {
|
||||||
let (db, dir) = setup();
|
let (db, dir) = setup();
|
||||||
|
|||||||
@@ -1530,6 +1530,7 @@ mod tests {
|
|||||||
(
|
(
|
||||||
"hidden".to_string(),
|
"hidden".to_string(),
|
||||||
crate::model::CategorySettings {
|
crate::model::CategorySettings {
|
||||||
|
title: None,
|
||||||
render_in_lists: false,
|
render_in_lists: false,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
@@ -1539,6 +1540,7 @@ mod tests {
|
|||||||
(
|
(
|
||||||
"featured".to_string(),
|
"featured".to_string(),
|
||||||
crate::model::CategorySettings {
|
crate::model::CategorySettings {
|
||||||
|
title: None,
|
||||||
render_in_lists: true,
|
render_in_lists: true,
|
||||||
show_title: false,
|
show_title: false,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! Full-text search reindexing engine functions.
|
//! Full-text search reindexing engine functions.
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::db::DbConnection as Connection;
|
use crate::db::DbConnection as Connection;
|
||||||
@@ -9,7 +10,8 @@ use crate::db::queries::{
|
|||||||
media as media_q, media_translation, post as post_q, post_translation, project as project_q,
|
media as media_q, media_translation, post as post_q, post_translation, project as project_q,
|
||||||
setting,
|
setting,
|
||||||
};
|
};
|
||||||
use crate::engine::{EngineResult, domain_events};
|
use crate::engine::{EngineError, EngineResult, domain_events};
|
||||||
|
use crate::util::frontmatter::{read_post_file, read_translation_file};
|
||||||
use crate::util::now_unix_ms;
|
use crate::util::now_unix_ms;
|
||||||
|
|
||||||
const REBUILD_REQUIRED_SETTING: &str = "app.search-index-rebuild-required";
|
const REBUILD_REQUIRED_SETTING: &str = "app.search-index-rebuild-required";
|
||||||
@@ -201,8 +203,9 @@ fn index_project(
|
|||||||
current: &mut usize,
|
current: &mut usize,
|
||||||
on_item: Option<&ItemProgressFn>,
|
on_item: Option<&ItemProgressFn>,
|
||||||
) -> EngineResult<ReindexReport> {
|
) -> EngineResult<ReindexReport> {
|
||||||
let main_language = data_path
|
let data_dir = data_path.map(Path::new);
|
||||||
.and_then(|path| crate::engine::meta::read_project_json(Path::new(path)).ok())
|
let main_language = data_dir
|
||||||
|
.and_then(|path| crate::engine::meta::read_project_json(path).ok())
|
||||||
.and_then(|metadata| metadata.main_language)
|
.and_then(|metadata| metadata.main_language)
|
||||||
.unwrap_or_else(|| "en".to_string());
|
.unwrap_or_else(|| "en".to_string());
|
||||||
let mut report = ReindexReport {
|
let mut report = ReindexReport {
|
||||||
@@ -218,19 +221,30 @@ fn index_project(
|
|||||||
let translations = post_translation::list_post_translations_by_post(conn, &post.id)?;
|
let translations = post_translation::list_post_translations_by_post(conn, &post.id)?;
|
||||||
let translation_data = translations
|
let translation_data = translations
|
||||||
.iter()
|
.iter()
|
||||||
.map(|translation| fts::PostTranslationFts {
|
.map(|translation| {
|
||||||
title: translation.title.clone(),
|
Ok(fts::PostTranslationFts {
|
||||||
excerpt: translation.excerpt.clone(),
|
title: translation.title.clone(),
|
||||||
content: translation.content.clone(),
|
excerpt: translation.excerpt.clone(),
|
||||||
language: translation.language.clone(),
|
content: data_dir
|
||||||
|
.map(|dir| resolve_translation_fts_content(dir, translation))
|
||||||
|
.transpose()?
|
||||||
|
.flatten()
|
||||||
|
.or_else(|| translation.content.clone()),
|
||||||
|
language: translation.language.clone(),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<EngineResult<Vec<_>>>()?;
|
||||||
|
let content = data_dir
|
||||||
|
.map(|dir| resolve_post_fts_content(dir, &post))
|
||||||
|
.transpose()?
|
||||||
|
.flatten()
|
||||||
|
.or_else(|| post.content.clone());
|
||||||
fts::index_post(
|
fts::index_post(
|
||||||
conn,
|
conn,
|
||||||
&post.id,
|
&post.id,
|
||||||
&post.title,
|
&post.title,
|
||||||
post.excerpt.as_deref(),
|
post.excerpt.as_deref(),
|
||||||
post.content.as_deref(),
|
content.as_deref(),
|
||||||
&post.tags,
|
&post.tags,
|
||||||
&post.categories,
|
&post.categories,
|
||||||
&translation_data,
|
&translation_data,
|
||||||
@@ -271,11 +285,41 @@ fn index_project(
|
|||||||
Ok(report)
|
Ok(report)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_post_fts_content(
|
||||||
|
data_dir: &Path,
|
||||||
|
post: &crate::model::Post,
|
||||||
|
) -> EngineResult<Option<String>> {
|
||||||
|
if post.content.is_some() {
|
||||||
|
return Ok(post.content.clone());
|
||||||
|
}
|
||||||
|
if post.file_path.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let raw = fs::read_to_string(data_dir.join(&post.file_path))?;
|
||||||
|
let (_fm, body) = read_post_file(&raw).map_err(EngineError::Parse)?;
|
||||||
|
Ok(Some(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_translation_fts_content(
|
||||||
|
data_dir: &Path,
|
||||||
|
translation: &crate::model::PostTranslation,
|
||||||
|
) -> EngineResult<Option<String>> {
|
||||||
|
if translation.content.is_some() {
|
||||||
|
return Ok(translation.content.clone());
|
||||||
|
}
|
||||||
|
if translation.file_path.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let raw = fs::read_to_string(data_dir.join(&translation.file_path))?;
|
||||||
|
let (_fm, body) = read_translation_file(&raw).map_err(EngineError::Parse)?;
|
||||||
|
Ok(Some(body))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::db::Database;
|
use crate::db::Database;
|
||||||
use crate::db::fts::ensure_fts_tables;
|
use crate::db::fts::{self, ensure_fts_tables};
|
||||||
use crate::engine;
|
use crate::engine;
|
||||||
|
|
||||||
fn setup() -> (Database, String) {
|
fn setup() -> (Database, String) {
|
||||||
@@ -330,6 +374,57 @@ mod tests {
|
|||||||
assert_eq!(results.len(), 1);
|
assert_eq!(results.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rebuild_indexes_published_post_and_translation_bodies_from_files() {
|
||||||
|
let db = Database::open_in_memory().unwrap();
|
||||||
|
let _ = db.migrate();
|
||||||
|
ensure_fts_tables(db.conn()).unwrap();
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let project = engine::project::create_project(
|
||||||
|
db.conn(),
|
||||||
|
"Published Project",
|
||||||
|
Some(dir.path().to_str().unwrap()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let post = engine::post::create_post(
|
||||||
|
db.conn(),
|
||||||
|
dir.path(),
|
||||||
|
&project.id,
|
||||||
|
"Published Rebuild",
|
||||||
|
Some("distinctive platypusnova body"),
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
None,
|
||||||
|
Some("en"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
engine::post::upsert_translation(
|
||||||
|
db.conn(),
|
||||||
|
dir.path(),
|
||||||
|
&post.id,
|
||||||
|
"de",
|
||||||
|
"Veröffentlichter Neuaufbau",
|
||||||
|
None,
|
||||||
|
Some("markantes schmetterlingskomet wort"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
engine::post::publish_post(db.conn(), dir.path(), &post.id).unwrap();
|
||||||
|
fts::remove_post_from_index(db.conn(), &post.id).unwrap();
|
||||||
|
|
||||||
|
let report = reindex_project(db.conn(), &project.id, None).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(report.posts_indexed, 1);
|
||||||
|
assert_eq!(
|
||||||
|
fts::search_posts(db.conn(), "platypusnova", "en").unwrap(),
|
||||||
|
vec![post.id.clone()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
fts::search_posts(db.conn(), "schmetterlingskomet", "de").unwrap(),
|
||||||
|
vec![post.id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shared_rebuild_indexes_every_project() {
|
fn shared_rebuild_indexes_every_project() {
|
||||||
let (db, first_project_id) = setup();
|
let (db, first_project_id) = setup();
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ impl ProjectMetadata {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CategorySettings {
|
pub struct CategorySettings {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub title: Option<String>,
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub render_in_lists: bool,
|
pub render_in_lists: bool,
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
@@ -167,12 +169,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn category_settings_camel_case() {
|
fn category_settings_camel_case() {
|
||||||
let settings = CategorySettings {
|
let settings = CategorySettings {
|
||||||
|
title: Some("Article".into()),
|
||||||
render_in_lists: false,
|
render_in_lists: false,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
post_template_slug: Some("article-tpl".into()),
|
post_template_slug: Some("article-tpl".into()),
|
||||||
list_template_slug: None,
|
list_template_slug: None,
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string(&settings).unwrap();
|
let json = serde_json::to_string(&settings).unwrap();
|
||||||
|
assert!(json.contains("title"));
|
||||||
assert!(json.contains("renderInLists"));
|
assert!(json.contains("renderInLists"));
|
||||||
assert!(json.contains("showTitle"));
|
assert!(json.contains("showTitle"));
|
||||||
assert!(json.contains("postTemplateSlug"));
|
assert!(json.contains("postTemplateSlug"));
|
||||||
|
|||||||
@@ -619,6 +619,7 @@ fn generation_respects_category_list_settings_and_writes_bundled_images() {
|
|||||||
(
|
(
|
||||||
"hidden".to_string(),
|
"hidden".to_string(),
|
||||||
CategorySettings {
|
CategorySettings {
|
||||||
|
title: None,
|
||||||
render_in_lists: false,
|
render_in_lists: false,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
@@ -628,6 +629,7 @@ fn generation_respects_category_list_settings_and_writes_bundled_images() {
|
|||||||
(
|
(
|
||||||
"featured".to_string(),
|
"featured".to_string(),
|
||||||
CategorySettings {
|
CategorySettings {
|
||||||
|
title: None,
|
||||||
render_in_lists: true,
|
render_in_lists: true,
|
||||||
show_title: false,
|
show_title: false,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn sign_app_for_dmg(workspace: &Path) -> Result<(), Box<dyn Error>> {
|
fn sign_app_for_dmg(workspace: &Path) -> Result<(), Box<dyn Error>> {
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let _ = workspace;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
if env::var_os("CARGO_PACKAGER_FORMAT").as_deref() == Some(OsStr::new("dmg")) {
|
if env::var_os("CARGO_PACKAGER_FORMAT").as_deref() == Some(OsStr::new("dmg")) {
|
||||||
let target = env::var_os("CARGO_TARGET_DIR")
|
let target = env::var_os("CARGO_TARGET_DIR")
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ pub enum OneShotAiAction {
|
|||||||
MediaTranslation { target_language: String },
|
MediaTranslation { target_language: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
// Menu
|
// Menu
|
||||||
MenuEvent(muda::MenuId),
|
MenuEvent(muda::MenuId),
|
||||||
@@ -7110,7 +7111,9 @@ impl BdsApp {
|
|||||||
.map(|name| {
|
.map(|name| {
|
||||||
let meta = category_meta.get(&name);
|
let meta = category_meta.get(&name);
|
||||||
SettingsCategoryRow {
|
SettingsCategoryRow {
|
||||||
title: name.clone(),
|
title: meta
|
||||||
|
.and_then(|value| value.title.clone())
|
||||||
|
.unwrap_or_else(|| name.clone()),
|
||||||
render_in_lists: meta.map(|value| value.render_in_lists).unwrap_or(true),
|
render_in_lists: meta.map(|value| value.render_in_lists).unwrap_or(true),
|
||||||
show_title: meta.map(|value| value.show_title).unwrap_or(true),
|
show_title: meta.map(|value| value.show_title).unwrap_or(true),
|
||||||
post_template_slug: meta
|
post_template_slug: meta
|
||||||
@@ -7781,6 +7784,7 @@ impl BdsApp {
|
|||||||
category_meta.insert(
|
category_meta.insert(
|
||||||
row.name.clone(),
|
row.name.clone(),
|
||||||
bds_core::model::metadata::CategorySettings {
|
bds_core::model::metadata::CategorySettings {
|
||||||
|
title: Some(row.title.clone()).filter(|title| !title.trim().is_empty()),
|
||||||
render_in_lists: row.render_in_lists,
|
render_in_lists: row.render_in_lists,
|
||||||
show_title: row.show_title,
|
show_title: row.show_title,
|
||||||
post_template_slug: (!row.post_template_slug.is_empty())
|
post_template_slug: (!row.post_template_slug.is_empty())
|
||||||
@@ -7822,6 +7826,8 @@ impl BdsApp {
|
|||||||
(
|
(
|
||||||
row.name,
|
row.name,
|
||||||
bds_core::model::metadata::CategorySettings {
|
bds_core::model::metadata::CategorySettings {
|
||||||
|
title: Some(row.title.clone())
|
||||||
|
.filter(|title| !title.trim().is_empty()),
|
||||||
render_in_lists: row.render_in_lists,
|
render_in_lists: row.render_in_lists,
|
||||||
show_title: row.show_title,
|
show_title: row.show_title,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
@@ -9179,7 +9185,7 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_media_ai_analysis(&mut self, media_id: &str) -> Task<Message> {
|
fn run_media_ai_analysis(&mut self, media_id: &str) -> Task<Message> {
|
||||||
let Some(db) = &self.db else {
|
let Some(_db) = &self.db else {
|
||||||
self.notify(
|
self.notify(
|
||||||
ToastLevel::Error,
|
ToastLevel::Error,
|
||||||
&t(self.ui_locale, "common.databaseUnavailable"),
|
&t(self.ui_locale, "common.databaseUnavailable"),
|
||||||
@@ -9230,7 +9236,7 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn detect_media_language(&mut self, media_id: &str) -> Task<Message> {
|
fn detect_media_language(&mut self, media_id: &str) -> Task<Message> {
|
||||||
let Some(db) = &self.db else {
|
let Some(_db) = &self.db else {
|
||||||
self.notify(
|
self.notify(
|
||||||
ToastLevel::Error,
|
ToastLevel::Error,
|
||||||
&t(self.ui_locale, "common.databaseUnavailable"),
|
&t(self.ui_locale, "common.databaseUnavailable"),
|
||||||
@@ -9287,7 +9293,7 @@ impl BdsApp {
|
|||||||
|
|
||||||
fn translate_media_to(&mut self, media_id: &str, target_language: &str) -> Task<Message> {
|
fn translate_media_to(&mut self, media_id: &str, target_language: &str) -> Task<Message> {
|
||||||
self.active_modal = None;
|
self.active_modal = None;
|
||||||
let Some(db) = &self.db else {
|
let Some(_db) = &self.db else {
|
||||||
self.notify(
|
self.notify(
|
||||||
ToastLevel::Error,
|
ToastLevel::Error,
|
||||||
&t(self.ui_locale, "common.databaseUnavailable"),
|
&t(self.ui_locale, "common.databaseUnavailable"),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::process::{Command, Stdio};
|
|||||||
use std::sync::{Arc, Mutex, OnceLock};
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
|
||||||
use bds_core::scripting::AppHostHandler;
|
use bds_core::scripting::AppHostHandler;
|
||||||
use serde_json::{Value, json};
|
use serde_json::Value;
|
||||||
|
|
||||||
use super::menu::MenuAction;
|
use super::menu::MenuAction;
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn desktop_handler_queues_supported_menu_actions() {
|
fn desktop_handler_queues_supported_menu_actions() {
|
||||||
let queued = Arc::new(Mutex::new(Vec::new()));
|
let queued = Arc::new(Mutex::new(Vec::new()));
|
||||||
handler(Arc::clone(&queued), String::new())("trigger_menu_action", &[json!("new_post")])
|
handler(Arc::clone(&queued), String::new())("trigger_menu_action", &[serde_json::json!("new_post")])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(queued.lock().unwrap().as_slice(), &[MenuAction::NewPost]);
|
assert_eq!(queued.lock().unwrap().as_slice(), &[MenuAction::NewPost]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user