diff --git a/src-tauri/src/commands/presets.rs b/src-tauri/src/commands/presets.rs index 35cd952..a1ffec3 100644 --- a/src-tauri/src/commands/presets.rs +++ b/src-tauri/src/commands/presets.rs @@ -161,7 +161,12 @@ mod tests { #[test] fn units_beyond_the_original_five_are_stored_as_given() { let conn = open_in_memory().unwrap(); - for (typed, stored) in [("km", "km"), ("SQFT", "sqft"), (" bundle ", "bundle"), ("sq.ft/day", "sq.ft/day")] { + for (typed, stored) in [ + ("km", "km"), + ("SQFT", "sqft"), + (" bundle ", "bundle"), + ("sq.ft/day", "sq.ft/day"), + ] { let mut p = preset(typed); p.mode = "rate".into(); p.unit = typed.into(); diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index d3523bb..0042521 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -1108,7 +1108,17 @@ CREATE INDEX IF NOT EXISTS idx_invoices_created ON invoices(created_at DESC); fn m12_rebuilds_item_presets_keeping_rows_and_allowing_new_units() { let mut conn = Connection::open_in_memory().unwrap(); let v11 = vec![ - M::up(SCHEMA), M::up(M2), M::up(M3), M::up(M4), M::up(M5), M::up(M6), M::up(M7), M::up(M8), M::up(M9), M::up(M10), M::up(M11), + M::up(SCHEMA), + M::up(M2), + M::up(M3), + M::up(M4), + M::up(M5), + M::up(M6), + M::up(M7), + M::up(M8), + M::up(M9), + M::up(M10), + M::up(M11), ]; Migrations::new(v11).to_latest(&mut conn).unwrap(); assert_eq!(user_version(&conn), 11); @@ -1134,16 +1144,43 @@ CREATE INDEX IF NOT EXISTS idx_invoices_created ON invoices(created_at DESC); assert_eq!( rows, vec![ - (1, "Studio".into(), "998397".into(), "rate".into(), "hour".into(), 150050, 3, "then".into(), Some("ITEM-1".into())), - (2, "Fee".into(), "".into(), "fixed".into(), "unit".into(), 500000, 0, "then".into(), None), + ( + 1, + "Studio".into(), + "998397".into(), + "rate".into(), + "hour".into(), + 150050, + 3, + "then".into(), + Some("ITEM-1".into()) + ), + ( + 2, + "Fee".into(), + "".into(), + "fixed".into(), + "unit".into(), + 500000, + 0, + "then".into(), + None + ), ] ); // New units and custom text are allowed; an empty or over-long unit is not; ids keep counting up. conn.execute("INSERT INTO item_presets (description, unit, created_at) VALUES ('Travel', 'km', 'now')", []).unwrap(); conn.execute("INSERT INTO item_presets (description, unit, created_at) VALUES ('Parcel', 'bundle', 'now')", []).unwrap(); - assert!(conn.execute("INSERT INTO item_presets (description, unit, created_at) VALUES ('X', '', 'now')", []).is_err()); + assert!(conn + .execute( + "INSERT INTO item_presets (description, unit, created_at) VALUES ('X', '', 'now')", + [] + ) + .is_err()); assert!(conn.execute("INSERT INTO item_presets (description, unit, created_at) VALUES ('X', 'thirteen char', 'now')", []).is_err()); - let last: i64 = conn.query_row("SELECT MAX(id) FROM item_presets", [], |r| r.get(0)).unwrap(); + let last: i64 = conn + .query_row("SELECT MAX(id) FROM item_presets", [], |r| r.get(0)) + .unwrap(); assert_eq!(last, 4); } diff --git a/src-tauri/src/integrations/erpnext/config.rs b/src-tauri/src/integrations/erpnext/config.rs index d01cc8c..4e0bc81 100644 --- a/src-tauri/src/integrations/erpnext/config.rs +++ b/src-tauri/src/integrations/erpnext/config.rs @@ -67,7 +67,10 @@ impl NamingMode { /// Voiced line unit to ERPNext UOM. Whole-number UOMs such as "Nos" reject fractional quantities, /// which is why hours, minutes, kilometres and the like map to their own UOMs (see `units::KNOWN`). pub fn default_uom_map() -> BTreeMap { - crate::units::KNOWN.iter().map(|(id, uom)| (id.to_string(), uom.to_string())).collect() + crate::units::KNOWN + .iter() + .map(|(id, uom)| (id.to_string(), uom.to_string())) + .collect() } /// Everything stored for the integration, secret included. Backend-only. diff --git a/src-tauri/src/units.rs b/src-tauri/src/units.rs index f19e942..0ebc2e2 100644 --- a/src-tauri/src/units.rs +++ b/src-tauri/src/units.rs @@ -31,7 +31,10 @@ pub fn is_known(unit: &str) -> bool { /// The ERPNext UOM that suits a built-in unit. pub fn default_uom(unit: &str) -> Option<&'static str> { - KNOWN.iter().find(|(id, _)| *id == unit).map(|(_, uom)| *uom) + KNOWN + .iter() + .find(|(id, _)| *id == unit) + .map(|(_, uom)| *uom) } fn custom_problem(text: &str) -> Option { @@ -40,11 +43,14 @@ fn custom_problem(text: &str) -> Option { return Some("a unit is required".into()); } if t.chars().count() > MAX_CUSTOM_LEN { - return Some(format!("a custom unit has at most {MAX_CUSTOM_LEN} characters")); + return Some(format!( + "a custom unit has at most {MAX_CUSTOM_LEN} characters" + )); } let mut chars = t.chars(); let first_ok = chars.next().is_some_and(|c| c.is_ascii_alphanumeric()); - if !first_ok || !chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, ' ' | '.' | '/' | '-')) { + if !first_ok || !chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, ' ' | '.' | '/' | '-')) + { return Some("a unit may use letters, digits, spaces, . / and - only".into()); } None @@ -113,7 +119,10 @@ mod tests { #[test] fn fractional_units_do_not_map_to_whole_number_uoms() { - for id in ["second", "minute", "hour", "day", "week", "month", "km", "m", "sqft", "sqm", "kg", "litre"] { + for id in [ + "second", "minute", "hour", "day", "week", "month", "km", "m", "sqft", "sqm", "kg", + "litre", + ] { assert_ne!(default_uom(id), Some("Nos"), "{id}"); } } diff --git a/src/components/QuantityInput.tsx b/src/components/QuantityInput.tsx index 24951a5..071ac97 100644 --- a/src/components/QuantityInput.tsx +++ b/src/components/QuantityInput.tsx @@ -39,8 +39,15 @@ export default function QuantityInput({ id, value, unit, onChange, disabled, onV const parsed = parseQuantity(text, unit); const clock = knownUnit(unit)?.clock; const validity = useRef(onValidity); - validity.current = onValidity; - useEffect(() => validity.current?.(parsed.ok), [parsed.ok]); + useEffect(() => { + validity.current = onValidity; + }); + // Reported while this box exists; a box that goes away (its line was removed) stops counting as invalid. + useEffect(() => { + const report = validity.current; + report?.(parsed.ok); + return () => report?.(true); + }, [parsed.ok]); const helper = parsed.ok && text.includes(":") ? `= ${formatQuantity(parsed.value, "unit")} ${knownUnit(unit)?.short ?? ""}`.trim() : undefined;