web aliasing of accounting and others

This commit is contained in:
Priec
2026-08-13 19:05:41 +02:00
parent 6c15fcedc8
commit 8662b00ccf
7 changed files with 161 additions and 33 deletions

View File

@@ -17,8 +17,8 @@ use crate::{
};
/// The compound type that also brings a system column with it. Which columns
/// it generates is the catalog's answer; the account foreign key is not one of
/// them, and [`TableDraft::preview_rows`] is where that is explained.
/// it generates is the catalog's answer; the physical `account_id` is not one
/// of them, and [`TableDraft::preview_rows`] is where that is explained.
pub(crate) const ACCOUNTING_FIELD_TYPE: &str = "accounting";
/// The compound type whose connectors remain tied to their backend names.
@@ -237,13 +237,19 @@ impl TableDraft {
if column.data_type == ACCOUNTING_TRANSFER_FIELD_TYPE {
continue;
}
names.extend(
self.columns
.generated_columns_of(index)
.iter()
.map(|generated| generated.name.clone()),
);
if column.data_type == ACCOUNTING_FIELD_TYPE {
let generated: Vec<String> = self
.columns
.generated_columns_of(index)
.iter()
.map(|generated| generated.name.clone())
.collect();
// A backend that reports `account` with the rest of ACCOUNTING's
// companions has already named it here; offering it a second time
// for the foreign key would put two fields on one column, and only
// the first of the two would be read.
let account_reported = generated.iter().any(|name| name == ACCOUNT_API_COLUMN);
names.extend(generated);
if column.data_type == ACCOUNTING_FIELD_TYPE && !account_reported {
names.push(ACCOUNT_API_COLUMN.to_string());
}
}
@@ -831,8 +837,10 @@ mod tests {
"tax_point_date",
"debit",
"credit",
// The account foreign key, which the catalog does not report
// because it is a system column rather than a generated one.
// The foreign key to the profile's accounts, once as the
// column the backend reports and once as the physical column
// it is stored in.
"account",
"account_id",
"created_at",
]

View File

@@ -231,7 +231,15 @@ impl AddTablePageState {
// backend names and remain the exception.
let aliasable = column.data_type != ACCOUNTING_TRANSFER_FIELD_TYPE;
for generated in columns.generated_columns_of(index) {
let generated_columns = columns.generated_columns_of(index);
// Whether the backend already reports `account` as one of them, in
// which case the foreign-key row below is a second view of a column
// that is named once, not a column of its own to name again.
let account_reported = generated_columns
.iter()
.any(|generated| generated.name == ACCOUNT_API_COLUMN);
for generated in generated_columns {
let mut tags = vec![format!("generated by {}", column.data_type)];
if generated.inherits_currency {
tags.push(format!("{}, {}", column.currency, column.money_mode.label()));
@@ -250,9 +258,10 @@ impl AddTablePageState {
});
}
// The account foreign key is a system column rather than a
// generated user column, so the catalog does not report it; the
// same explanation as in `TableDraft::preview_rows`.
// The physical column the account foreign key is stored in. It is
// a system column, so it is listed whether or not the catalog
// reports `account` among the generated ones — but it is named
// there when it does, never twice.
if column.data_type == ACCOUNTING_FIELD_TYPE {
rows.push(ColumnRow {
index: None,
@@ -269,7 +278,7 @@ impl AddTablePageState {
self.draft.generated_display_name(ACCOUNT_API_COLUMN)
),
],
alias_source: Some(ACCOUNT_API_COLUMN.to_string()),
alias_source: (!account_reported).then(|| ACCOUNT_API_COLUMN.to_string()),
alias: self.draft.alias_for(ACCOUNT_API_COLUMN).to_string(),
});
}
@@ -277,6 +286,33 @@ impl AddTablePageState {
rows
}
/// The generated columns that may be renamed, in the order they are
/// listed.
///
/// Renaming lives in a section of its own rather than in the column list:
/// the list is where the table is read, and a text box in every other row
/// of it turns reading into scanning past inputs.
/// One field per generated column and no more: a source named twice would
/// be two fields over one column, and the draft reads the first of them,
/// so whatever was typed in the second would be dropped without a word.
pub(crate) fn alias_rows(&self) -> Vec<AliasRow> {
let mut rows: Vec<AliasRow> = Vec::new();
for row in self.column_rows() {
let Some(source) = row.alias_source else {
continue;
};
if rows.iter().any(|existing| existing.source == source) {
continue;
}
rows.push(AliasRow {
source,
alias: row.alias,
data_type: row.data_type,
});
}
rows
}
/// Row-display candidates: `id` first, then every column, matching the
/// client's candidate list.
pub(crate) fn row_display_candidates(&self) -> Vec<RowDisplayCandidate> {
@@ -331,6 +367,16 @@ pub(crate) struct ColumnRow {
pub alias: String,
}
/// One line of the "Rename generated columns" section: a generated column the
/// request may name, and the name asked for it so far.
pub(crate) struct AliasRow {
/// The generated column's own name, which is what the rename asks for.
pub source: String,
/// The alias typed for it, empty when the backend's name is being kept.
pub alias: String,
pub data_type: String,
}
pub(crate) struct RowDisplayCandidate {
pub index: usize,
pub name: String,
@@ -449,6 +495,7 @@ mod tests {
"tax_point_date",
"debit",
"credit",
"account",
"account_id",
]
);
@@ -504,7 +551,11 @@ mod tests {
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("account_id").alias_source.as_deref(), Some("account"));
// `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"));
assert_eq!(row("account").alias, "ucet");
assert!(row("account_id").alias_source.is_none());
assert!(row("account_id").tags.contains(&"written as ucet".to_string()));
assert!(
row("accounting").alias_source.is_none(),

View File

@@ -126,7 +126,8 @@ mod tests {
}
/// The columns an ACCOUNTING row generates get a name field of their own,
/// which is what makes them aliasable at creation time.
/// which is what makes them aliasable at creation time — in a section of
/// their own, not in the column list, which is there to be read.
#[test]
fn generated_accounting_columns_get_an_alias_field() {
let mut page = page();
@@ -151,6 +152,38 @@ mod tests {
);
}
assert!(html.contains(r#"name="generated_alias_names""#));
// The column list itself carries no input at all.
let list = html
.split("<h2>Columns")
.nth(1)
.and_then(|rest| rest.split("</table>").next())
.expect("the column list should be rendered");
assert!(!list.contains("<input"), "{list}");
assert!(html.contains("<summary>Rename generated columns</summary>"));
}
/// A rename is reported where the column is listed, so the list still says
/// what the table will look like without the section being opened.
#[test]
fn a_renamed_generated_column_says_so_in_the_column_list() {
let mut page = page();
page.draft.columns.added.push(ColumnDefinition {
name: "accounting".to_string(),
data_type: "accounting".to_string(),
indexed: false,
quantity_ledger: false,
money_mode: MoneyMode::Exact,
currency: "EUR".to_string(),
});
page.draft.generated_aliases = vec![crate::pages::add_table::draft::GeneratedAlias {
source: "debit".to_string(),
alias: "md".to_string(),
}];
let html = render_builder(&page);
assert!(html.contains(r#"<span class="tag">renamed to md</span>"#), "{html}");
assert!(html.contains(r#"name="generated_alias_names" value="md""#));
}
#[test]

View File

@@ -1095,6 +1095,9 @@ pub(crate) mod tests {
generated_column("tax_point_date", "date", false),
generated_column("debit", "money", true),
generated_column("credit", "money", true),
// The foreign key to the profile's accounts, which the
// backend reports with the rest of them.
generated_column("account", "link(accounts)", false),
],
..compound("accounting")
},
@@ -1586,11 +1589,11 @@ pub(crate) mod tests {
.iter()
.map(|generated| generated.name.as_str())
.collect::<Vec<_>>(),
["name", "tax_point_date", "debit", "credit"]
["name", "tax_point_date", "debit", "credit", "account"]
);
draft.add_from_inputs().unwrap();
assert_eq!(draft.generated_columns_of(0).len(), 4);
assert_eq!(draft.generated_columns_of(0).len(), 5);
// DEBIT and CREDIT are the two kept in the declared currency.
assert_eq!(
draft