From e7bf248a6271a31698efc8a4f9548bf8d00b6cd0 Mon Sep 17 00:00:00 2001 From: Sam Waechter <138798589+swektr@users.noreply.github.com> Date: Mon, 18 Sep 2023 00:33:56 -0400 Subject: [PATCH] Fix unable to save field dialog if certain fields are deleted (#2663) * Fix unable to save field dialog if certain fields are deleted Implemented solution suggested in issue #2556 * Fix unable to save field dialog if certain fields are deleted fixed code formating * Fix unable to save field dialog if certain fields are deleted Made new functions to check referencelessness. Added unit test. --- CONTRIBUTORS | 1 + rslib/src/notetype/mod.rs | 78 +++++++++++++++++++++++++++++++++++++++ rslib/src/template.rs | 24 ++++++++++++ 3 files changed, 103 insertions(+) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index a4c476a12..c053b30fc 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -139,6 +139,7 @@ Nil Admirari Michael Winkworth Mateusz Wojewoda Jarrett Ye +Sam Waechter ******************** diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs index 1e0151897..492b05b1c 100644 --- a/rslib/src/notetype/mod.rs +++ b/rslib/src/notetype/mod.rs @@ -551,13 +551,21 @@ impl Notetype { fields: HashMap>, parsed: &mut [(Option, Option)], ) { + let first_remaining_field_name = &self.fields.get(0).unwrap().name; + let is_cloze = self.is_cloze(); for (idx, (q_opt, a_opt)) in parsed.iter_mut().enumerate() { if let Some(q) = q_opt { q.rename_and_remove_fields(&fields); + if !q.contains_field_replacement() || is_cloze && !q.contains_cloze_replacement() { + q.add_missing_field_replacement(first_remaining_field_name, is_cloze); + } self.templates[idx].config.q_format = q.template_to_string(); } if let Some(a) = a_opt { a.rename_and_remove_fields(&fields); + if is_cloze && !a.contains_cloze_replacement() { + a.add_missing_field_replacement(first_remaining_field_name, is_cloze); + } self.templates[idx].config.a_format = a.template_to_string(); } } @@ -745,3 +753,73 @@ impl Collection { } } } + +// Tests +//--------------------------------------- + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn update_templates_after_removing_crucial_fields() { + // Normal Test (all front fields removed) + let mut nt_norm = Notetype::default(); + nt_norm.add_field("baz"); // Fields "foo" and "bar" were removed + nt_norm.fields[0].ord = Some(2); + + nt_norm.add_template("Card 1", "front {{foo}}", "back {{bar}}"); + nt_norm.templates[0].ord = Some(0); + let mut parsed = nt_norm.parsed_templates(); + + let mut field_map: HashMap> = HashMap::new(); + field_map.insert("foo".to_owned(), None); + field_map.insert("bar".to_owned(), None); + + nt_norm.update_templates_for_renamed_and_removed_fields(field_map, &mut parsed); + assert_eq!(nt_norm.templates[0].config.q_format, "front {{baz}}"); + assert_eq!(nt_norm.templates[0].config.a_format, "back "); + + // Cloze Test 1/2 (front and back cloze fields removed) + let mut nt_cloze = Notetype { + config: Notetype::new_cloze_config(), + ..Default::default() + }; + nt_cloze.add_field("baz"); // Fields "foo" and "bar" were removed + nt_cloze.fields[0].ord = Some(2); + + nt_cloze.add_template("Card 1", "front {{cloze:foo}}", "back {{cloze:bar}}"); + nt_cloze.templates[0].ord = Some(0); + let mut parsed = nt_cloze.parsed_templates(); + + let mut field_map: HashMap> = HashMap::new(); + field_map.insert("foo".to_owned(), None); + field_map.insert("bar".to_owned(), None); + + nt_cloze.update_templates_for_renamed_and_removed_fields(field_map, &mut parsed); + assert_eq!(nt_cloze.templates[0].config.q_format, "front {{cloze:baz}}"); + assert_eq!(nt_cloze.templates[0].config.a_format, "back {{cloze:baz}}"); + + // Cloze Test 2/2 (only back cloze field is removed) + let mut nt_cloze = Notetype { + config: Notetype::new_cloze_config(), + ..Default::default() + }; + nt_cloze.add_field("foo"); + nt_cloze.fields[0].ord = Some(0); + nt_cloze.add_field("baz"); + nt_cloze.fields[1].ord = Some(2); + // ^ only field "bar" was removed + + nt_cloze.add_template("Card 1", "front {{cloze:foo}}", "back {{cloze:bar}}"); + nt_cloze.templates[0].ord = Some(0); + let mut parsed = nt_cloze.parsed_templates(); + + let mut field_map: HashMap> = HashMap::new(); + field_map.insert("bar".to_owned(), None); + + nt_cloze.update_templates_for_renamed_and_removed_fields(field_map, &mut parsed); + assert_eq!(nt_cloze.templates[0].config.q_format, "front {{cloze:foo}}"); + assert_eq!(nt_cloze.templates[0].config.a_format, "back {{cloze:foo}}"); + } +} diff --git a/rslib/src/template.rs b/rslib/src/template.rs index 733c960bf..5b92f04a6 100644 --- a/rslib/src/template.rs +++ b/rslib/src/template.rs @@ -730,6 +730,30 @@ impl ParsedTemplate { let old_nodes = std::mem::take(&mut self.0); self.0 = rename_and_remove_fields(old_nodes, fields); } + + pub(crate) fn contains_cloze_replacement(&self) -> bool { + self.0.iter().any(|node| { + matches!( + node, + ParsedNode::Replacement {key:_, filters} if filters.iter().any(|f| f=="cloze") + ) + }) + } + + pub(crate) fn contains_field_replacement(&self) -> bool { + self.0 + .iter() + .any(|node| matches!(node, ParsedNode::Replacement { key: _, filters: _ })) + } + + pub(crate) fn add_missing_field_replacement(&mut self, field_name: &str, is_cloze: bool) { + let key = String::from(field_name); + let filters = match is_cloze { + true => vec![String::from("cloze")], + false => Vec::new(), + }; + self.0.push(ParsedNode::Replacement { key, filters }); + } } fn rename_and_remove_fields(