Run cargo fmt on the unit code and drop a quantity box's invalid mark when its line is removed
The new CI runs cargo fmt --check. QuantityInput also reported its validity only when it changed, so removing a line whose quantity box held bad text left that box counted as invalid and blocked issuing; it now reports valid when it unmounts, and no longer writes a ref during render. Claude-Session: https://claude.ai/code/session_01PZypiWDfMkDTeEPeXjRhW5
This commit is contained in:
@@ -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();
|
||||
|
||||
+42
-5
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, String> {
|
||||
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.
|
||||
|
||||
+13
-4
@@ -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<String> {
|
||||
@@ -40,11 +43,14 @@ fn custom_problem(text: &str) -> Option<String> {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
useEffect(() => {
|
||||
validity.current = onValidity;
|
||||
useEffect(() => validity.current?.(parsed.ok), [parsed.ok]);
|
||||
});
|
||||
// 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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user