diff --git a/AGENTS.md b/AGENTS.md index 75654a4..3e7d5cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 - 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 +- 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 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. diff --git a/README.md b/README.md index 40f7d99..4ac8e33 100644 --- a/README.md +++ b/README.md @@ -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. - 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. -- 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. - 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. diff --git a/crates/bds-ui/src/app.rs b/crates/bds-ui/src/app.rs index 8a2026a..9d7ece6 100644 --- a/crates/bds-ui/src/app.rs +++ b/crates/bds-ui/src/app.rs @@ -200,6 +200,7 @@ pub enum Message { ApplySiteValidation, EngineTaskDone { task_id: TaskId, + operation: &'static str, label: String, result: Result, }, @@ -6299,11 +6300,12 @@ mod tests { save_template_editor_state_impl, }; use crate::i18n::t; + use crate::state::ToastLevel; use crate::state::sidebar_filter::{MediaFilter, PostFilter}; use crate::views::media_editor::{MediaEditorMsg, MediaEditorState}; use crate::views::modal; 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::template_editor::TemplateEditorState; use bds_core::db::Database; @@ -7704,6 +7706,66 @@ mod tests { 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] fn create_template_opens_editor_and_refreshes_sidebar() { let (db, project, tmp) = setup(); diff --git a/crates/bds-ui/src/app/editor_handlers.rs b/crates/bds-ui/src/app/editor_handlers.rs index 2b04bdb..3764adf 100644 --- a/crates/bds-ui/src/app/editor_handlers.rs +++ b/crates/bds-ui/src/app/editor_handlers.rs @@ -647,6 +647,7 @@ impl BdsApp { pub(super) fn handle_script_editor_msg(&mut self, msg: ScriptEditorMsg) -> Task { let mut run_request = None; + let mut syntax_notification = None; if let Some(tab_id) = self.active_tab.clone() && let Some(state) = self.script_editors.get_mut(&tab_id) { @@ -680,14 +681,20 @@ impl BdsApp { return self.save_script_editor(&tab_id); } ScriptEditorMsg::CheckSyntax => { - if let Some(st) = self.script_editors.get_mut(&tab_id) { - match engine::script::validate_script_syntax(&st.content) { - Ok(()) => { - st.validation_error = None; - } - Err(e) => { - st.validation_error = Some(e); - } + match engine::script::validate_script_syntax(&state.content) { + Ok(()) => { + state.validation_error = None; + syntax_notification = Some(( + ToastLevel::Success, + t(self.ui_locale, "editor.syntaxValid"), + )); + } + 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; } } + if let Some((level, message)) = syntax_notification { + self.notify(level, &message); + } if let Some((source, entrypoint, kind)) = run_request { let offline_mode = self.offline_mode; let app_handler = crate::platform::script_host::handler( diff --git a/crates/bds-ui/src/app/engine_handlers.rs b/crates/bds-ui/src/app/engine_handlers.rs index c0428c9..935238f 100644 --- a/crates/bds-ui/src/app/engine_handlers.rs +++ b/crates/bds-ui/src/app/engine_handlers.rs @@ -213,6 +213,7 @@ impl BdsApp { Message::ApplySiteValidation => self.apply_site_validation(), Message::EngineTaskDone { task_id, + operation, label, result, } => { @@ -222,6 +223,26 @@ impl BdsApp { _ if cancelled => {} Ok(detail) => { 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}")); } Err(err) => { diff --git a/crates/bds-ui/src/app/tasks.rs b/crates/bds-ui/src/app/tasks.rs index 2497073..65efdac 100644 --- a/crates/bds-ui/src/app/tasks.rs +++ b/crates/bds-ui/src/app/tasks.rs @@ -314,6 +314,7 @@ impl BdsApp { }, move |result| Message::EngineTaskDone { task_id, + operation: "engine.reindexStarted", label: label_for_message.clone(), result, }, @@ -321,7 +322,7 @@ impl BdsApp { } /// Spawn a blocking engine operation on a background thread via TaskManager. - pub(super) fn spawn_engine_task(&mut self, label_key: &str, work: F) -> Task + pub(super) fn spawn_engine_task(&mut self, label_key: &'static str, work: F) -> Task where F: FnOnce(PathBuf, String, PathBuf, Arc, TaskId) -> Result + Send @@ -332,7 +333,7 @@ impl BdsApp { pub(super) fn spawn_grouped_engine_task( &mut self, - label_key: &str, + label_key: &'static str, group_name: &str, work: F, ) -> Task @@ -346,7 +347,7 @@ impl BdsApp { pub(super) fn spawn_engine_task_in_group( &mut self, - label_key: &str, + label_key: &'static str, group_name: Option<&str>, work: F, ) -> Task @@ -391,6 +392,7 @@ impl BdsApp { }, move |result| Message::EngineTaskDone { task_id, + operation: label_key, label: label_for_msg.clone(), result, }, diff --git a/locales/ui/de.ftl b/locales/ui/de.ftl index 2ac6299..e26b43f 100644 --- a/locales/ui/de.ftl +++ b/locales/ui/de.ftl @@ -274,6 +274,8 @@ editor-discard = Verwerfen editor-validate = Validieren editor-run = Ausführen editor-checkSyntax = Syntax prüfen +editor-syntaxValid = Syntax ist gültig. +editor-syntaxInvalid = Syntaxfehler: { $error } editor-kind = Art editor-enabled = Aktiviert editor-entrypoint = Einstiegspunkt diff --git a/locales/ui/en.ftl b/locales/ui/en.ftl index 026d895..0051744 100644 --- a/locales/ui/en.ftl +++ b/locales/ui/en.ftl @@ -265,6 +265,8 @@ editor-discard = Discard editor-validate = Validate editor-run = Run editor-checkSyntax = Check Syntax +editor-syntaxValid = Syntax is valid. +editor-syntaxInvalid = Syntax error: { $error } editor-kind = Kind editor-enabled = Enabled editor-entrypoint = Entrypoint diff --git a/locales/ui/es.ftl b/locales/ui/es.ftl index f9b45ff..0980948 100644 --- a/locales/ui/es.ftl +++ b/locales/ui/es.ftl @@ -274,6 +274,8 @@ editor-discard = Descartar editor-validate = Validar editor-run = Ejecutar editor-checkSyntax = Verificar sintaxis +editor-syntaxValid = La sintaxis es válida. +editor-syntaxInvalid = Error de sintaxis: { $error } editor-kind = Tipo editor-enabled = Habilitado editor-entrypoint = Punto de entrada diff --git a/locales/ui/fr.ftl b/locales/ui/fr.ftl index e3702ed..1622ec8 100644 --- a/locales/ui/fr.ftl +++ b/locales/ui/fr.ftl @@ -274,6 +274,8 @@ editor-discard = Annuler les modifications editor-validate = Valider editor-run = Exécuter editor-checkSyntax = Vérifier la syntaxe +editor-syntaxValid = La syntaxe est valide. +editor-syntaxInvalid = Erreur de syntaxe : { $error } editor-kind = Type editor-enabled = Activé editor-entrypoint = Point d'entrée diff --git a/locales/ui/it.ftl b/locales/ui/it.ftl index fb1998e..8e63edd 100644 --- a/locales/ui/it.ftl +++ b/locales/ui/it.ftl @@ -274,6 +274,8 @@ editor-discard = Scarta editor-validate = Valida editor-run = Esegui editor-checkSyntax = Controlla sintassi +editor-syntaxValid = La sintassi è valida. +editor-syntaxInvalid = Errore di sintassi: { $error } editor-kind = Tipo editor-enabled = Abilitato editor-entrypoint = Punto di ingresso