From f52c911959d2a153d76fbcf60a3a739d46d08b81 Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 24 Oct 2026 06:17:19 +0000 Subject: [PATCH] Integration: #927 Header & Footer dialog sends dateFormat only when picked; set_date doc comment back above set_date Without it, Apply rewrote every slide's existing date field to datetime1. --- crates/engine/src/cmd/design.rs | 4 +- crates/ui-egui/src/dialogs.rs | 68 ++++++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/crates/engine/src/cmd/design.rs b/crates/engine/src/cmd/design.rs index 7baa6b58..3130b38b 100644 --- a/crates/engine/src/cmd/design.rs +++ b/crates/engine/src/cmd/design.rs @@ -961,8 +961,6 @@ fn header_footer(s: &mut Session, p: &Value) -> Result { ok() } -/// Point a slide's date placeholder at a fixed date (its own plain text) or, for an empty -/// `fixed`, at an automatic date field (one it already has keeps its format). /// The date field type `format` names, `datetime1` when it is empty or unknown. fn date_kind(format: &str) -> &str { if deckcraft_text::datetime::DATE_KINDS.contains(&format) { format } else { "datetime1" } @@ -974,6 +972,8 @@ fn date_now(format: &str) -> String { deckcraft_text::datetime::DateTime::now_local().and_then(|now| deckcraft_text::datetime::field_text(kind, None, now)).unwrap_or_else(today) } +/// Point a slide's date placeholder at a fixed date (its own plain text) or, for an empty +/// `fixed`, at an automatic date field (one it already has keeps its format). fn set_date(sh: &mut Shape, fixed: &str, format: &str) { let runs = || sh.text.iter().flat_map(|t| &t.paragraphs).flat_map(|p| &p.runs); let field_now = runs().find_map(|r| match &r.kind { diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 4ef2391f..3e7e4c2b 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1126,11 +1126,20 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { if apply || apply_all { let dt = d.get("dateText", ""); let ft = d.get("footerText", ""); - run( - app, - "design.headerFooter", - json!({"date": date, "dateText": dt, "dateFormat": d.get("dateFormat", &fmt_now), "slideNumber": num_on, "footer": foot, "footerText": ft, "hideOnTitle": hide, "all": apply_all}), - ); + let mut params = serde_json::Map::new(); + params.insert("date".into(), json!(date)); + params.insert("dateText".into(), json!(dt)); + // Only a format the user picked: sending the default would rewrite every slide's + // existing date field (an imported datetime4, say) to datetime1. + if d.fields.contains_key("dateFormat") { + params.insert("dateFormat".into(), json!(d.get("dateFormat", &fmt_now))); + } + params.insert("slideNumber".into(), json!(num_on)); + params.insert("footer".into(), json!(foot)); + params.insert("footerText".into(), json!(ft)); + params.insert("hideOnTitle".into(), json!(hide)); + params.insert("all".into(), json!(apply_all)); + run(app, "design.headerFooter", serde_json::Value::Object(params)); } apply || apply_all || d.fields.contains_key("cancel") } @@ -3410,6 +3419,55 @@ mod tests { h } + /// Header & Footer ▸ Apply without picking a format leaves a slide's existing date field in + /// its own format instead of rewriting it to the default one. + #[test] + fn header_footer_apply_keeps_an_existing_date_format() { + use std::sync::Arc; + + use egui::vec2; + use egui_kittest::kittest::Queryable as _; + use serde_json::json; + + use crate::SlideApp; + let mut session = deckcraft_engine::Session::with_new(); + session.execute("design.headerFooter", &json!({"date": true, "dateText": "", "dateFormat": "datetime4", "all": true})).unwrap(); + // A file whose document-level format is unset, as an imported deck's is. + Arc::make_mut(&mut session.doc_mut().unwrap().doc).header_footer.date_format = String::new(); + let date_fields = |app: &SlideApp| -> Vec { + app.session + .doc() + .unwrap() + .doc + .slides + .iter() + .flat_map(|sl| sl.shapes.iter()) + .filter(|sh| sh.ph_type() == Some(deckcraft_model::PhType::Date)) + .flat_map(|sh| sh.text.iter()) + .flat_map(|t| t.paragraphs.iter()) + .flat_map(|p| p.runs.iter()) + .filter_map(|r| match &r.kind { + deckcraft_model::text::RunKind::Field { field } => Some(field.clone()), + _ => None, + }) + .collect() + }; + let mut harness = egui_kittest::Harness::builder().with_size(vec2(1200.0, 900.0)).build_ui_state( + |ui, app: &mut SlideApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + SlideApp::new(session, crate::Services::default()), + ); + harness.run_steps(2); + assert_eq!(date_fields(harness.state()), vec!["datetime4".to_string()]); + harness.state_mut().run("app.dialog", json!({"id": "headerFooter"})).unwrap(); + harness.run_steps(2); + harness.get_by_label("Apply").click_accesskit(); + harness.run_steps(3); + assert_eq!(date_fields(harness.state()), vec!["datetime4".to_string()], "the dialog left the field's format alone"); + } + /// The Export… dialog (Animated GIF) shows the GIF options and exports through `file.export` (#171). #[test] fn export_dialog_writes_an_animated_gif() {