web aliasing of accounting and others
This commit is contained in:
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -180,6 +180,17 @@
|
||||
.compound-note li { padding: 1px 0; }
|
||||
.compound-note .hint { font-size: 12px; }
|
||||
|
||||
/* Renaming what a compound row generates: closed by default, because the
|
||||
backend's names are the answer most tables keep. */
|
||||
.rename-generated > summary { font-size: 15px; color: #33415c; cursor: pointer; }
|
||||
.rename-generated > summary::marker { color: #8a94a6; }
|
||||
.rename-generated > .hint { margin: 8px 0 0; font-size: 13px; }
|
||||
.rename-list { margin: 12px 0 0; padding: 0; list-style: none; display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 8px; }
|
||||
.rename-list li { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border: 1px solid #e1e6ee; border-radius: 7px; background: white; }
|
||||
.rename-list li > code { flex: none; }
|
||||
.rename-list li > .hint { flex: none; font-size: 12px; }
|
||||
.rename-list input { flex: 1; min-width: 90px; }
|
||||
|
||||
.builder-list { margin: 12px 0 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.builder-list li { display: flex; align-items: center; gap: 8px; padding: 5px 10px; border: 1px solid #e1e6ee; border-radius: 7px; background: white; }
|
||||
|
||||
|
||||
@@ -238,7 +238,7 @@
|
||||
<li><code>account_id</code> <span class="hint">system column — the account each row is posted to, written as <code>account</code></span></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<p class="hint">Each of these can be given a name of your own in the column list once the row is added; leave a field empty to keep the name shown here.</p>
|
||||
<p class="hint">Each of these can be given a name of your own under <em>Rename generated columns</em> once the row is added; leave a field empty to keep the name shown here.</p>
|
||||
<p class="hint">These names are reserved: no column of your own may use them, and the table may hold only one <code>{{ page.draft.columns.pending_compound_name() }}</code>. They land where this row sits in the column list, and can be moved with it.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -279,17 +279,9 @@
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ row.name }}</code>
|
||||
{#
|
||||
A generated column is named by the backend, so it cannot be
|
||||
named in the request that creates the table — but the name is a
|
||||
display name, and an alias here is applied as a rename as soon
|
||||
as the table exists. Leaving it empty keeps the backend's name.
|
||||
#}
|
||||
{% if let Some(source) = row.alias_source %}
|
||||
<input type="hidden" name="generated_alias_sources" value="{{ source }}">
|
||||
<input name="generated_alias_names" value="{{ row.alias }}"
|
||||
aria-label="Alias for {{ source }}" placeholder="alias for {{ source }}">
|
||||
{% endif %}
|
||||
{# The list only reports a rename; it is asked for below, so that
|
||||
this column stays something to read. #}
|
||||
{% if !row.alias.is_empty() %}<span class="tag">renamed to {{ row.alias }}</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ row.data_type }}</td>
|
||||
<td>
|
||||
@@ -334,6 +326,36 @@
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
{#
|
||||
Renaming what a definition row generates. A generated column is named by the
|
||||
backend, so it cannot be named in the request that creates the table — but the
|
||||
name is a display name, and an alias here is applied as a rename as soon as
|
||||
the table exists. Leaving a field empty keeps the backend's name.
|
||||
|
||||
It is a closed `<details>`, so the default — the backend's names — costs
|
||||
nothing to read past. Fields inside a closed `<details>` are still part of the
|
||||
form, so the aliases travel with every builder post either way.
|
||||
#}
|
||||
{% if !page.alias_rows().is_empty() %}
|
||||
<section class="builder-section">
|
||||
<details class="rename-generated">
|
||||
<summary>Rename generated columns</summary>
|
||||
<p class="hint">These columns come from the definition rows above. Give one a name of your own, or leave it empty to keep the name shown.</p>
|
||||
<ul class="rename-list">
|
||||
{% for row in page.alias_rows() %}
|
||||
<li>
|
||||
<code>{{ row.source }}</code>
|
||||
<span class="hint">{{ row.data_type }}</span>
|
||||
<input type="hidden" name="generated_alias_sources" value="{{ row.source }}">
|
||||
<input name="generated_alias_names" value="{{ row.alias }}"
|
||||
aria-label="Rename {{ row.source }}" placeholder="{{ row.source }}">
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="builder-section">
|
||||
{% for table in page.draft.relation_tables %}
|
||||
<input type="hidden" name="relation_tables" value="{{ table }}">
|
||||
|
||||
Reference in New Issue
Block a user