diff --git a/client-gui b/client-gui index a1476ba1..cd5824fa 160000 --- a/client-gui +++ b/client-gui @@ -1 +1 @@ -Subproject commit a1476ba1579489210abd68873392a7f38013c694 +Subproject commit cd5824fae37a220a5a1c2dad725e77667c324301 diff --git a/web/src/pages/add_table/draft.rs b/web/src/pages/add_table/draft.rs index b9f47ac5..74ff66b7 100644 --- a/web/src/pages/add_table/draft.rs +++ b/web/src/pages/add_table/draft.rs @@ -298,7 +298,7 @@ impl TableDraft { self.generated_aliases .iter() .find(|entry| entry.source == source) - .map(|entry| entry.alias.trim()) + .map(|entry| entry.alias.as_str()) .unwrap_or_default() } @@ -530,7 +530,7 @@ impl TableDraft { if let Some(error) = validate_accounting_currency(locale, self) { return Err(error); } - if let Some(error) = validate_table_name(locale, self.table_name.trim()) { + if let Some(error) = validate_table_name(locale, &self.table_name) { return Err(error); } if self.table_name_conflicts() { @@ -565,10 +565,9 @@ impl TableDraft { } pub(crate) fn into_request( - mut self, + self, locale: Locale, ) -> Result { - self.table_name = self.table_name.trim().to_string(); self.validate(locale)?; Ok(PostTableDefinitionRequest { @@ -770,6 +769,12 @@ mod tests { }]; assert!(draft.validate(crate::i18n::Locale::default()).is_ok()); + draft.generated_aliases[0].alias = " Md ".to_string(); + assert!( + draft.validate(crate::i18n::Locale::default()).is_err(), + "boundary whitespace must not be silently removed" + ); + draft.generated_aliases[0].alias = "NOTEπŸ™‚".to_string(); assert!( draft.validate(crate::i18n::Locale::default()).is_err(), @@ -1105,8 +1110,18 @@ mod tests { draft.table_name = "general_ledger".to_string(); assert!(draft.validate(crate::i18n::Locale::default()).is_err()); - draft.table_name = "invoice".to_string(); + draft.table_name = " Invoice ".to_string(); + assert!(draft.validate(crate::i18n::Locale::default()).is_err()); + + draft.table_name = "2026-Sales_Q4".to_string(); assert!(draft.validate(crate::i18n::Locale::default()).is_ok()); + assert_eq!( + draft + .into_request(crate::i18n::Locale::default()) + .unwrap() + .table_name, + "2026-Sales_Q4" + ); } /// A shared table has no books of its own, so the two definition rows that diff --git a/web/src/pages/add_table/loader.rs b/web/src/pages/add_table/loader.rs index ab8d8bed..54e6f88e 100644 --- a/web/src/pages/add_table/loader.rs +++ b/web/src/pages/add_table/loader.rs @@ -71,7 +71,7 @@ pub(crate) async fn load_page( // for, so it is told what that table is on every render rather than left to // read a copy that a `refresh` could have moved on from. draft.columns.global = draft.global; - draft.columns.table_name = draft.table_name.trim().to_string(); + draft.columns.table_name = draft.table_name.clone(); let tree = definitions .get_profile_tree( diff --git a/web/src/pages/add_table/state.rs b/web/src/pages/add_table/state.rs index a0845f92..5482bb1e 100644 --- a/web/src/pages/add_table/state.rs +++ b/web/src/pages/add_table/state.rs @@ -140,7 +140,7 @@ impl BuilderForm { // takes no column that posts to one profile's books, and no link // may point at the table being created. global: self.global, - table_name: self.table_name.trim().to_string(), + table_name: self.table_name.clone(), }; // Drop display columns whose column is gone, so a stale post cannot @@ -181,7 +181,7 @@ impl BuilderForm { .zip(self.generated_alias_names.iter()) .map(|(source, alias)| GeneratedAlias { source: source.trim().to_string(), - alias: alias.trim().to_string(), + alias: alias.clone(), }) .collect(), } @@ -576,16 +576,16 @@ mod tests { form.column_rounding.push("exact".into()); form.column_currencies.push("EUR".into()); form.generated_alias_sources = vec!["debit".into(), "account".into()]; - form.generated_alias_names = vec![" md ".into(), "ucet".into()]; + form.generated_alias_names = vec!["Md πŸ™‚".into(), "ucet".into()]; let mut draft = form.to_draft(); draft.columns.catalog = crate::schema::tests::catalog(); - assert_eq!(draft.alias_for("debit"), "md"); + assert_eq!(draft.alias_for("debit"), "Md πŸ™‚"); assert_eq!( draft.aliased_generated_columns(), vec![ - ("debit".to_string(), "md".to_string()), + ("debit".to_string(), "Md πŸ™‚".to_string()), ("account".to_string(), "ucet".to_string()), ] ); @@ -601,8 +601,8 @@ mod tests { }; let rows = page.column_rows(); let row = |name: &str| rows.iter().find(|row| row.name == name).unwrap(); - assert_eq!(row("debit").alias_source.as_deref(), Some("debit")); - assert_eq!(row("debit").alias, "md"); + assert_eq!(row("Md πŸ™‚").alias_source.as_deref(), Some("debit")); + assert_eq!(row("Md πŸ™‚").alias, "Md πŸ™‚"); // `account` is reported by the catalog, so it is named there and the // physical column it lands in is not offered a second field. assert_eq!(row("account").alias_source.as_deref(), Some("account")); diff --git a/web/src/schema/mod.rs b/web/src/schema/mod.rs index 4c8281b3..7483d342 100644 --- a/web/src/schema/mod.rs +++ b/web/src/schema/mod.rs @@ -629,7 +629,7 @@ impl ColumnDraft { let column_name = if compound { column_type.clone() } else { - self.name_input.trim().to_string() + self.name_input.clone() }; if column_name.is_empty() { @@ -820,10 +820,13 @@ impl ColumnDraft { column_type: &str, ) -> Option { let target = link_target(column_type)?; - if target == LEDGER_ACCOUNTS_TABLE { + if common::alias::canonical_table_name(target) == LEDGER_ACCOUNTS_TABLE { return Some(crate::tr!(locale, "schema-err-account-link")); } - (!self.table_name.is_empty() && target == self.table_name).then(|| { + (!self.table_name.is_empty() + && common::alias::canonical_table_name(target) + == common::alias::canonical_table_name(&self.table_name)) + .then(|| { crate::tr!( locale, "schema-err-link-self", @@ -1781,6 +1784,8 @@ pub(crate) mod tests { draft.name_input = "Total".to_string(); assert!(draft.add_from_inputs(crate::i18n::Locale::default()).is_err()); + draft.name_input = " total".to_string(); + assert!(draft.add_from_inputs(crate::i18n::Locale::default()).is_err()); draft.name_input = "created_at".to_string(); assert!(draft.add_from_inputs(crate::i18n::Locale::default()).is_err()); @@ -2220,7 +2225,7 @@ pub(crate) mod tests { draft.table_name = "invoice".to_string(); draft.name_input = "parent".to_string(); draft.type_input = "link".to_string(); - draft.link_table_input = "invoice".to_string(); + draft.link_table_input = "INVOICE".to_string(); let error = draft.add_from_inputs(crate::i18n::Locale::default()).unwrap_err(); assert!(error.contains("cannot point at the table"), "{error}"); @@ -2245,6 +2250,10 @@ pub(crate) mod tests { let error = draft.add_from_inputs(crate::i18n::Locale::default()).unwrap_err(); assert!(error.contains("built into ACCOUNTING"), "{error}"); + + draft.link_table_input = "LEDGER_ACCOUNTS".to_string(); + let error = draft.add_from_inputs(crate::i18n::Locale::default()).unwrap_err(); + assert!(error.contains("built into ACCOUNTING"), "{error}"); } /// The names a definition row generates are the table's columns too, so a