fix: project reload on rebuild and syntax check now gives toast

This commit is contained in:
2026-07-19 12:07:10 +02:00
parent 422f71c8ad
commit 9dab0ca57e
11 changed files with 119 additions and 13 deletions

View File

@@ -46,6 +46,7 @@ Invariants and behaviours in the allium spec should be covered by unit tests of
- there are no "pre-existing" problems - you own every problem, you fix every problem - there are no "pre-existing" problems - you own every problem, you fix every problem
- don't leave unused code in the codebase, remove it instead - don't leave unused code in the codebase, remove it instead
- after implementing / changing things, run the build and run tests to verify all works - after implementing / changing things, run the build and run tests to verify all works
- run `cargo test --workspace` with permission for its loopback test servers on the first attempt; AI mock-server and preview-server tests bind localhost and otherwise fail under sandboxing, causing a pointless rerun
- do not reference external JavaScript or CSS on CDNs, always bring it into the project - do not reference external JavaScript or CSS on CDNs, always bring it into the project
- do not embedd CSS/JavaScript into HTML, always reference .css and .js files in the project assets - do not embedd CSS/JavaScript into HTML, always reference .css and .js files in the project assets
- always make sure you follow proper i18n best practices. no untranslated string constants. - always make sure you follow proper i18n best practices. no untranslated string constants.

View File

@@ -9,7 +9,7 @@ The project is under active development. Core blogging workflows are broadly ava
- Native Iced desktop workspace with localized menus, tabs, sidebars, dialogs, tasks, and embedded Wry previews. - Native Iced desktop workspace with localized menus, tabs, sidebars, dialogs, tasks, and embedded Wry previews.
- Post and translation authoring with draft/published lifecycle, metadata, tags, categories, links, media, and batch gallery-image import. - Post and translation authoring with draft/published lifecycle, metadata, tags, categories, links, media, and batch gallery-image import.
- Media import, thumbnails, metadata translations, filters, validation, and post assignment. - Media import, thumbnails, metadata translations, filters, validation, and post assignment.
- Template and Lua script management 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. - Template and Lua script management with explicit syntax-check feedback, 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.
- SQLite and filesystem persistence with frontmatter, sidecars, rebuild, metadata diff/repair, and FTS5 search. - SQLite and filesystem persistence with frontmatter, sidecars, rebuild, metadata diff/repair, and FTS5 search.
- Markdown/Liquid rendering with native macros, multilingual routes, feeds, sitemap, Pagefind, and incremental site generation through cancellable section task groups. - Markdown/Liquid rendering with native macros, multilingual routes, feeds, sitemap, Pagefind, and incremental site generation through cancellable section task groups.
- Local preview in the app or system browser. - Local preview in the app or system browser.

View File

@@ -200,6 +200,7 @@ pub enum Message {
ApplySiteValidation, ApplySiteValidation,
EngineTaskDone { EngineTaskDone {
task_id: TaskId, task_id: TaskId,
operation: &'static str,
label: String, label: String,
result: Result<String, String>, result: Result<String, String>,
}, },
@@ -6299,11 +6300,12 @@ mod tests {
save_template_editor_state_impl, save_template_editor_state_impl,
}; };
use crate::i18n::t; use crate::i18n::t;
use crate::state::ToastLevel;
use crate::state::sidebar_filter::{MediaFilter, PostFilter}; use crate::state::sidebar_filter::{MediaFilter, PostFilter};
use crate::views::media_editor::{MediaEditorMsg, MediaEditorState}; use crate::views::media_editor::{MediaEditorMsg, MediaEditorState};
use crate::views::modal; use crate::views::modal;
use crate::views::post_editor::{PostEditorMsg, PostEditorState}; use crate::views::post_editor::{PostEditorMsg, PostEditorState};
use crate::views::script_editor::ScriptEditorState; use crate::views::script_editor::{ScriptEditorMsg, ScriptEditorState};
use crate::views::settings_view::SettingsViewState; use crate::views::settings_view::SettingsViewState;
use crate::views::template_editor::TemplateEditorState; use crate::views::template_editor::TemplateEditorState;
use bds_core::db::Database; use bds_core::db::Database;
@@ -7704,6 +7706,66 @@ mod tests {
assert!(editor.content.contains("new script")); assert!(editor.content.contains("new script"));
} }
#[test]
fn script_syntax_check_reports_success_and_failure() {
let (db, project, tmp) = setup();
let mut app = make_app(db, project, &tmp);
let _ = app.update(Message::CreateScript);
let _ = app.update(Message::ScriptEditor(ScriptEditorMsg::CheckSyntax));
assert_eq!(
app.toasts.last().map(|toast| toast.message.as_str()),
Some(t(UiLocale::En, "editor.syntaxValid").as_str())
);
let _ = app.update(Message::ScriptEditor(ScriptEditorMsg::ContentChanged(
"function render(".to_string(),
)));
let _ = app.update(Message::ScriptEditor(ScriptEditorMsg::CheckSyntax));
assert_eq!(
app.toasts.last().map(|toast| toast.level),
Some(ToastLevel::Error)
);
}
#[test]
fn rebuild_completion_refreshes_project_metadata_in_settings() {
let (db, project, tmp) = setup();
let mut app = make_app(db, project, &tmp);
app.settings_state = Some(app.hydrate_settings_state());
let mut rebuilt = bds_core::db::queries::project::get_project_by_id(
app.db.as_ref().unwrap().conn(),
&app.active_project.as_ref().unwrap().id,
)
.unwrap();
rebuilt.description = Some("Description from project.json".to_string());
bds_core::db::queries::project::update_project(app.db.as_ref().unwrap().conn(), &rebuilt)
.unwrap();
let label = t(UiLocale::En, "engine.rebuildStarted");
let task_id = app.task_manager.submit(&label);
let _ = app.update(Message::EngineTaskDone {
task_id,
operation: "engine.rebuildStarted",
label,
result: Ok("done".to_string()),
});
assert_eq!(
app.active_project
.as_ref()
.and_then(|project| project.description.as_deref()),
Some("Description from project.json")
);
assert_eq!(
app.settings_state
.as_ref()
.map(|state| state.project_description.text()),
Some("Description from project.json\n".to_string())
);
}
#[test] #[test]
fn create_template_opens_editor_and_refreshes_sidebar() { fn create_template_opens_editor_and_refreshes_sidebar() {
let (db, project, tmp) = setup(); let (db, project, tmp) = setup();

View File

@@ -647,6 +647,7 @@ impl BdsApp {
pub(super) fn handle_script_editor_msg(&mut self, msg: ScriptEditorMsg) -> Task<Message> { pub(super) fn handle_script_editor_msg(&mut self, msg: ScriptEditorMsg) -> Task<Message> {
let mut run_request = None; let mut run_request = None;
let mut syntax_notification = None;
if let Some(tab_id) = self.active_tab.clone() if let Some(tab_id) = self.active_tab.clone()
&& let Some(state) = self.script_editors.get_mut(&tab_id) && let Some(state) = self.script_editors.get_mut(&tab_id)
{ {
@@ -680,14 +681,20 @@ impl BdsApp {
return self.save_script_editor(&tab_id); return self.save_script_editor(&tab_id);
} }
ScriptEditorMsg::CheckSyntax => { ScriptEditorMsg::CheckSyntax => {
if let Some(st) = self.script_editors.get_mut(&tab_id) { match engine::script::validate_script_syntax(&state.content) {
match engine::script::validate_script_syntax(&st.content) { Ok(()) => {
Ok(()) => { state.validation_error = None;
st.validation_error = None; syntax_notification = Some((
} ToastLevel::Success,
Err(e) => { t(self.ui_locale, "editor.syntaxValid"),
st.validation_error = Some(e); ));
} }
Err(error) => {
state.validation_error = Some(error.clone());
syntax_notification = Some((
ToastLevel::Error,
tw(self.ui_locale, "editor.syntaxInvalid", &[("error", &error)]),
));
} }
} }
} }
@@ -713,6 +720,9 @@ impl BdsApp {
tab.is_dirty = st.is_dirty; tab.is_dirty = st.is_dirty;
} }
} }
if let Some((level, message)) = syntax_notification {
self.notify(level, &message);
}
if let Some((source, entrypoint, kind)) = run_request { if let Some((source, entrypoint, kind)) = run_request {
let offline_mode = self.offline_mode; let offline_mode = self.offline_mode;
let app_handler = crate::platform::script_host::handler( let app_handler = crate::platform::script_host::handler(

View File

@@ -213,6 +213,7 @@ impl BdsApp {
Message::ApplySiteValidation => self.apply_site_validation(), Message::ApplySiteValidation => self.apply_site_validation(),
Message::EngineTaskDone { Message::EngineTaskDone {
task_id, task_id,
operation,
label, label,
result, result,
} => { } => {
@@ -222,6 +223,26 @@ impl BdsApp {
_ if cancelled => {} _ if cancelled => {}
Ok(detail) => { Ok(detail) => {
self.task_manager.complete(task_id); self.task_manager.complete(task_id);
if operation == "engine.rebuildStarted" {
let refreshed = self.db.as_ref().and_then(|db| {
let id = &self.active_project.as_ref()?.id;
bds_core::db::queries::project::get_project_by_id(db.conn(), id)
.ok()
});
if let Some(project) = refreshed {
if let Some(cached) = self
.projects
.iter_mut()
.find(|cached| cached.id == project.id)
{
*cached = project.clone();
}
self.active_project = Some(project);
if self.settings_state.is_some() {
self.settings_state = Some(self.hydrate_settings_state());
}
}
}
self.notify(ToastLevel::Success, &format!("{label}: {detail}")); self.notify(ToastLevel::Success, &format!("{label}: {detail}"));
} }
Err(err) => { Err(err) => {

View File

@@ -314,6 +314,7 @@ impl BdsApp {
}, },
move |result| Message::EngineTaskDone { move |result| Message::EngineTaskDone {
task_id, task_id,
operation: "engine.reindexStarted",
label: label_for_message.clone(), label: label_for_message.clone(),
result, result,
}, },
@@ -321,7 +322,7 @@ impl BdsApp {
} }
/// Spawn a blocking engine operation on a background thread via TaskManager. /// Spawn a blocking engine operation on a background thread via TaskManager.
pub(super) fn spawn_engine_task<F>(&mut self, label_key: &str, work: F) -> Task<Message> pub(super) fn spawn_engine_task<F>(&mut self, label_key: &'static str, work: F) -> Task<Message>
where where
F: FnOnce(PathBuf, String, PathBuf, Arc<TaskManager>, TaskId) -> Result<String, String> F: FnOnce(PathBuf, String, PathBuf, Arc<TaskManager>, TaskId) -> Result<String, String>
+ Send + Send
@@ -332,7 +333,7 @@ impl BdsApp {
pub(super) fn spawn_grouped_engine_task<F>( pub(super) fn spawn_grouped_engine_task<F>(
&mut self, &mut self,
label_key: &str, label_key: &'static str,
group_name: &str, group_name: &str,
work: F, work: F,
) -> Task<Message> ) -> Task<Message>
@@ -346,7 +347,7 @@ impl BdsApp {
pub(super) fn spawn_engine_task_in_group<F>( pub(super) fn spawn_engine_task_in_group<F>(
&mut self, &mut self,
label_key: &str, label_key: &'static str,
group_name: Option<&str>, group_name: Option<&str>,
work: F, work: F,
) -> Task<Message> ) -> Task<Message>
@@ -391,6 +392,7 @@ impl BdsApp {
}, },
move |result| Message::EngineTaskDone { move |result| Message::EngineTaskDone {
task_id, task_id,
operation: label_key,
label: label_for_msg.clone(), label: label_for_msg.clone(),
result, result,
}, },

View File

@@ -274,6 +274,8 @@ editor-discard = Verwerfen
editor-validate = Validieren editor-validate = Validieren
editor-run = Ausführen editor-run = Ausführen
editor-checkSyntax = Syntax prüfen editor-checkSyntax = Syntax prüfen
editor-syntaxValid = Syntax ist gültig.
editor-syntaxInvalid = Syntaxfehler: { $error }
editor-kind = Art editor-kind = Art
editor-enabled = Aktiviert editor-enabled = Aktiviert
editor-entrypoint = Einstiegspunkt editor-entrypoint = Einstiegspunkt

View File

@@ -265,6 +265,8 @@ editor-discard = Discard
editor-validate = Validate editor-validate = Validate
editor-run = Run editor-run = Run
editor-checkSyntax = Check Syntax editor-checkSyntax = Check Syntax
editor-syntaxValid = Syntax is valid.
editor-syntaxInvalid = Syntax error: { $error }
editor-kind = Kind editor-kind = Kind
editor-enabled = Enabled editor-enabled = Enabled
editor-entrypoint = Entrypoint editor-entrypoint = Entrypoint

View File

@@ -274,6 +274,8 @@ editor-discard = Descartar
editor-validate = Validar editor-validate = Validar
editor-run = Ejecutar editor-run = Ejecutar
editor-checkSyntax = Verificar sintaxis editor-checkSyntax = Verificar sintaxis
editor-syntaxValid = La sintaxis es válida.
editor-syntaxInvalid = Error de sintaxis: { $error }
editor-kind = Tipo editor-kind = Tipo
editor-enabled = Habilitado editor-enabled = Habilitado
editor-entrypoint = Punto de entrada editor-entrypoint = Punto de entrada

View File

@@ -274,6 +274,8 @@ editor-discard = Annuler les modifications
editor-validate = Valider editor-validate = Valider
editor-run = Exécuter editor-run = Exécuter
editor-checkSyntax = Vérifier la syntaxe editor-checkSyntax = Vérifier la syntaxe
editor-syntaxValid = La syntaxe est valide.
editor-syntaxInvalid = Erreur de syntaxe : { $error }
editor-kind = Type editor-kind = Type
editor-enabled = Activé editor-enabled = Activé
editor-entrypoint = Point d'entrée editor-entrypoint = Point d'entrée

View File

@@ -274,6 +274,8 @@ editor-discard = Scarta
editor-validate = Valida editor-validate = Valida
editor-run = Esegui editor-run = Esegui
editor-checkSyntax = Controlla sintassi editor-checkSyntax = Controlla sintassi
editor-syntaxValid = La sintassi è valida.
editor-syntaxInvalid = Errore di sintassi: { $error }
editor-kind = Tipo editor-kind = Tipo
editor-enabled = Abilitato editor-enabled = Abilitato
editor-entrypoint = Punto di ingresso editor-entrypoint = Punto di ingresso