i18n on the web - deepseek translations
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
|
||||
use crate::{
|
||||
definitions::table_definition::{GeneratedColumnAlias, PostTableDefinitionRequest},
|
||||
{i18n::Locale, tr},
|
||||
schema::{
|
||||
ColumnCatalog, ColumnDraft, proto_columns, validate_identifier, validate_table_name,
|
||||
},
|
||||
@@ -115,11 +116,19 @@ impl TableDraft {
|
||||
// ---- mutations -------------------------------------------------------
|
||||
|
||||
/// Removes one column, and drops it from the display columns with it.
|
||||
pub(crate) fn remove_column(&mut self, index: usize) -> Result<String, String> {
|
||||
let removed = self.columns.remove(index)?;
|
||||
pub(crate) fn remove_column(
|
||||
&mut self,
|
||||
locale: Locale,
|
||||
index: usize,
|
||||
) -> Result<String, String> {
|
||||
let removed = self.columns.remove(locale, index)?;
|
||||
self.row_display_columns
|
||||
.retain(|display| display != &removed.name);
|
||||
Ok(format!("Column `{}` removed.", removed.name))
|
||||
Ok(tr!(
|
||||
locale,
|
||||
"schema-status-column-removed",
|
||||
"name" => removed.name,
|
||||
))
|
||||
}
|
||||
|
||||
/// Moves one column one place up or down the list.
|
||||
@@ -127,8 +136,13 @@ impl TableDraft {
|
||||
/// The display columns keep their own order, which is the order they were
|
||||
/// chosen in rather than the order the columns are declared in, so nothing
|
||||
/// here touches them.
|
||||
pub(crate) fn move_column(&mut self, index: usize, offset: isize) -> Option<String> {
|
||||
self.columns.move_column(index, offset)
|
||||
pub(crate) fn move_column(
|
||||
&mut self,
|
||||
locale: Locale,
|
||||
index: usize,
|
||||
offset: isize,
|
||||
) -> Option<String> {
|
||||
self.columns.move_column(locale, index, offset)
|
||||
}
|
||||
|
||||
/// Adds or removes one display-column candidate.
|
||||
@@ -331,7 +345,7 @@ impl TableDraft {
|
||||
/// Every alias has to be a legal column name, and has to be free: the table
|
||||
/// is about to hold the declared columns, the generated ones and the system
|
||||
/// ones, and two columns cannot share a name.
|
||||
pub(crate) fn validate_generated_aliases(&self) -> Result<(), String> {
|
||||
pub(crate) fn validate_generated_aliases(&self, locale: Locale) -> Result<(), String> {
|
||||
let generated = self.aliasable_generated_columns();
|
||||
let renames = self.aliased_generated_columns();
|
||||
|
||||
@@ -350,12 +364,17 @@ impl TableDraft {
|
||||
);
|
||||
|
||||
for (source, alias) in &renames {
|
||||
if let Some(error) = validate_identifier(alias, "Column alias", true) {
|
||||
if let Some(error) =
|
||||
validate_identifier(locale, alias, "label-column-alias", true)
|
||||
{
|
||||
return Err(error);
|
||||
}
|
||||
if taken.iter().filter(|name| *name == alias).count() > 1 {
|
||||
return Err(format!(
|
||||
"Alias `{alias}` for generated column `{source}` is already taken by another column."
|
||||
return Err(tr!(
|
||||
locale,
|
||||
"draft-err-alias-taken",
|
||||
"alias" => alias.clone(),
|
||||
"source" => source.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -392,7 +411,7 @@ impl TableDraft {
|
||||
|
||||
/// The schema as it will exist: system columns, relation columns, then the
|
||||
/// user's own. Mirrors the client's preview pane.
|
||||
pub(crate) fn preview_rows(&self) -> Vec<PreviewRow> {
|
||||
pub(crate) fn preview_rows(&self, locale: &Locale) -> Vec<PreviewRow> {
|
||||
let mut rows = vec![
|
||||
PreviewRow {
|
||||
mark: if self.row_display_columns.is_empty() {
|
||||
@@ -402,14 +421,14 @@ impl TableDraft {
|
||||
},
|
||||
column: "id".to_string(),
|
||||
data_type: "BIGSERIAL".to_string(),
|
||||
option: "primary key".to_string(),
|
||||
option: tr!(*locale, "builder-preview-primary-key"),
|
||||
source: "system".to_string(),
|
||||
},
|
||||
PreviewRow {
|
||||
mark: String::new(),
|
||||
column: "deleted".to_string(),
|
||||
data_type: "BOOLEAN".to_string(),
|
||||
option: "default false".to_string(),
|
||||
option: tr!(*locale, "builder-preview-default-false"),
|
||||
source: "system".to_string(),
|
||||
},
|
||||
// Every managed table carries one, and the preview claims to be the
|
||||
@@ -418,7 +437,7 @@ impl TableDraft {
|
||||
mark: String::new(),
|
||||
column: "row_revision".to_string(),
|
||||
data_type: "BIGINT".to_string(),
|
||||
option: "not null, default 1".to_string(),
|
||||
option: tr!(*locale, "builder-preview-not-null-1"),
|
||||
source: "system".to_string(),
|
||||
},
|
||||
];
|
||||
@@ -431,7 +450,7 @@ impl TableDraft {
|
||||
.unwrap_or_else(|| "[ ]".to_string()),
|
||||
column: column.name.clone(),
|
||||
data_type: column.data_type.clone(),
|
||||
option: column.option_label(),
|
||||
option: column.option_label(locale),
|
||||
source: "user".to_string(),
|
||||
});
|
||||
|
||||
@@ -445,7 +464,15 @@ impl TableDraft {
|
||||
column: self.generated_display_name(&generated.name),
|
||||
data_type: generated.data_type.clone(),
|
||||
option: if generated.inherits_currency {
|
||||
format!("{}, {}", column.currency, column.money_mode.label())
|
||||
let mode = match column.money_mode {
|
||||
crate::schema::MoneyMode::Exact => "exact",
|
||||
crate::schema::MoneyMode::Rounded => "half-up",
|
||||
};
|
||||
format!(
|
||||
"{}, {}",
|
||||
column.currency,
|
||||
tr!(*locale, &format!("td-money-{mode}")),
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
@@ -462,9 +489,10 @@ impl TableDraft {
|
||||
mark: String::new(),
|
||||
column: "account_id".to_string(),
|
||||
data_type: "BIGINT".to_string(),
|
||||
option: format!(
|
||||
"not null, → accounts, written as {}",
|
||||
self.generated_display_name(ACCOUNT_API_COLUMN)
|
||||
option: tr!(
|
||||
*locale,
|
||||
"builder-preview-account-option",
|
||||
"field" => self.generated_display_name(ACCOUNT_API_COLUMN),
|
||||
),
|
||||
source: "system".to_string(),
|
||||
});
|
||||
@@ -475,7 +503,7 @@ impl TableDraft {
|
||||
mark: String::new(),
|
||||
column: "created_at".to_string(),
|
||||
data_type: "TIMESTAMPTZ".to_string(),
|
||||
option: "current time".to_string(),
|
||||
option: tr!(*locale, "builder-preview-current-time"),
|
||||
source: "system".to_string(),
|
||||
});
|
||||
rows
|
||||
@@ -484,18 +512,22 @@ impl TableDraft {
|
||||
// ---- validation and submission ---------------------------------------
|
||||
|
||||
/// Every check the client runs before it will save.
|
||||
pub(crate) fn validate(&self) -> Result<(), String> {
|
||||
pub(crate) fn validate(&self, locale: Locale) -> Result<(), String> {
|
||||
let profile_name = self.effective_profile_name();
|
||||
if !self.global && self.creating_new_profile && profile_name.is_empty() {
|
||||
return Err("Enter a name for the new profile.".to_string());
|
||||
return Err(tr!(locale, "draft-err-profile-name"));
|
||||
}
|
||||
if !self.global && let Some(error) = validate_identifier(&profile_name, "Profile name", false) {
|
||||
if !self.global {
|
||||
if let Some(error) =
|
||||
validate_identifier(locale, &profile_name, "label-profile-name", false)
|
||||
{
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
if let Some(error) = validate_accounting_currency(locale, self) {
|
||||
return Err(error);
|
||||
}
|
||||
if let Some(error) = validate_accounting_currency(self) {
|
||||
return Err(error);
|
||||
}
|
||||
if let Some(error) = validate_table_name(self.table_name.trim()) {
|
||||
if let Some(error) = validate_table_name(locale, self.table_name.trim()) {
|
||||
return Err(error);
|
||||
}
|
||||
if self.table_name_conflicts() {
|
||||
@@ -503,27 +535,33 @@ impl TableDraft {
|
||||
// be free in all of them — and naming a profile in that message
|
||||
// would name the wrong thing, there being none.
|
||||
return Err(if self.global {
|
||||
format!(
|
||||
"A table named `{}` already exists. A shared table's name has to be free in every profile.",
|
||||
self.table_name
|
||||
tr!(
|
||||
locale,
|
||||
"draft-err-table-conflict-global",
|
||||
"name" => self.table_name.clone(),
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"A table named `{}` already exists in profile `{profile_name}`, or is shared by every profile.",
|
||||
self.table_name
|
||||
tr!(
|
||||
locale,
|
||||
"draft-err-table-conflict",
|
||||
"name" => self.table_name.clone(),
|
||||
"profile" => profile_name.clone(),
|
||||
)
|
||||
});
|
||||
}
|
||||
if self.columns.is_empty() {
|
||||
return Err("Add at least one column before saving.".to_string());
|
||||
return Err(tr!(locale, "draft-err-no-columns"));
|
||||
}
|
||||
self.validate_generated_aliases()?;
|
||||
self.columns.validate()
|
||||
self.validate_generated_aliases(locale)?;
|
||||
self.columns.validate(locale)
|
||||
}
|
||||
|
||||
pub(crate) fn into_request(mut self) -> Result<PostTableDefinitionRequest, String> {
|
||||
pub(crate) fn into_request(
|
||||
mut self,
|
||||
locale: Locale,
|
||||
) -> Result<PostTableDefinitionRequest, String> {
|
||||
self.table_name = self.table_name.trim().to_string();
|
||||
self.validate()?;
|
||||
self.validate(locale)?;
|
||||
|
||||
Ok(PostTableDefinitionRequest {
|
||||
table_name: self.table_name.clone(),
|
||||
@@ -545,13 +583,13 @@ impl TableDraft {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_accounting_currency(draft: &TableDraft) -> Option<String> {
|
||||
pub(crate) fn validate_accounting_currency(locale: Locale, draft: &TableDraft) -> Option<String> {
|
||||
if !draft.creating_new_profile || draft.global {
|
||||
return None;
|
||||
}
|
||||
let currency = draft.accounting_currency.to_ascii_uppercase();
|
||||
if rusty_money::iso::find(¤cy).is_none() {
|
||||
return Some("Accounting currency must be a three-letter ISO-4217 code".to_string());
|
||||
return Some(tr!(locale, "draft-err-accounting-currency"));
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -591,11 +629,11 @@ mod tests {
|
||||
draft.profile_name_input = "billing".to_string();
|
||||
|
||||
draft.accounting_currency = "AAA".to_string();
|
||||
assert!(draft.validate().is_err());
|
||||
assert!(draft.validate(crate::i18n::Locale::default()).is_err());
|
||||
|
||||
draft.accounting_currency = "eur".to_string();
|
||||
assert!(draft.validate().is_ok());
|
||||
assert_eq!(draft.into_request().unwrap().accounting_currency, "EUR");
|
||||
assert!(draft.validate(crate::i18n::Locale::default()).is_ok());
|
||||
assert_eq!(draft.into_request(crate::i18n::Locale::default()).unwrap().accounting_currency, "EUR");
|
||||
}
|
||||
|
||||
/// Every column an ACCOUNTING row generates may be aliased, including the
|
||||
@@ -669,16 +707,16 @@ mod tests {
|
||||
);
|
||||
assert_eq!(draft.generated_display_name("debit"), "md");
|
||||
assert_eq!(draft.generated_display_name("credit"), "credit");
|
||||
assert!(draft.validate().is_ok());
|
||||
assert!(draft.validate(crate::i18n::Locale::default()).is_ok());
|
||||
assert!(
|
||||
draft
|
||||
.preview_rows()
|
||||
.preview_rows(&crate::i18n::Locale::default())
|
||||
.iter()
|
||||
.any(|row| row.column == "md" && row.source == "generated")
|
||||
);
|
||||
|
||||
// The request that creates the columns is the request that names them.
|
||||
let request = draft.into_request().unwrap();
|
||||
let request = draft.into_request(crate::i18n::Locale::default()).unwrap();
|
||||
assert_eq!(request.generated_aliases.len(), 1);
|
||||
assert_eq!(request.generated_aliases[0].generated_name, "debit");
|
||||
assert_eq!(request.generated_aliases[0].alias, "md");
|
||||
@@ -701,25 +739,25 @@ mod tests {
|
||||
source: "debit".to_string(),
|
||||
alias: "Md".to_string(),
|
||||
}];
|
||||
assert!(draft.validate().is_err(), "an alias is a column name");
|
||||
assert!(draft.validate(crate::i18n::Locale::default()).is_err(), "an alias is a column name");
|
||||
|
||||
draft.generated_aliases[0].alias = "note".to_string();
|
||||
assert!(draft.validate().is_err(), "a declared column holds the name");
|
||||
assert!(draft.validate(crate::i18n::Locale::default()).is_err(), "a declared column holds the name");
|
||||
|
||||
draft.generated_aliases[0].alias = "credit".to_string();
|
||||
assert!(draft.validate().is_err(), "another generated column does");
|
||||
assert!(draft.validate(crate::i18n::Locale::default()).is_err(), "another generated column does");
|
||||
|
||||
draft.generated_aliases[0].alias = "id".to_string();
|
||||
assert!(draft.validate().is_err(), "a system column does");
|
||||
assert!(draft.validate(crate::i18n::Locale::default()).is_err(), "a system column does");
|
||||
|
||||
draft.generated_aliases[0].alias = "md".to_string();
|
||||
assert!(draft.validate().is_ok());
|
||||
assert!(draft.validate(crate::i18n::Locale::default()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_profile_sends_no_accounting_currency() {
|
||||
let draft = draft_with_column("total", "int");
|
||||
let request = draft.into_request().unwrap();
|
||||
let request = draft.into_request(crate::i18n::Locale::default()).unwrap();
|
||||
|
||||
assert_eq!(request.accounting_currency, "");
|
||||
assert_eq!(request.profile_name, "billing");
|
||||
@@ -730,13 +768,13 @@ mod tests {
|
||||
let mut draft = draft_with_column("total", "int");
|
||||
draft.existing_profile_tables = vec!["invoice".to_string()];
|
||||
|
||||
let error = draft.validate().unwrap_err();
|
||||
let error = draft.validate(crate::i18n::Locale::default()).unwrap_err();
|
||||
assert!(error.contains("profile `billing`"), "{error}");
|
||||
|
||||
// A shared table has no profile to name, and its name has to be free
|
||||
// everywhere rather than in one place.
|
||||
draft.global = true;
|
||||
let error = draft.validate().unwrap_err();
|
||||
let error = draft.validate(crate::i18n::Locale::default()).unwrap_err();
|
||||
assert!(!error.contains("profile ``"), "{error}");
|
||||
assert!(error.contains("every profile"), "{error}");
|
||||
}
|
||||
@@ -746,15 +784,15 @@ mod tests {
|
||||
let mut draft = draft_with_column("total", "int");
|
||||
draft.profile_name = "pg_catalog".to_string();
|
||||
|
||||
assert!(draft.validate().is_err());
|
||||
assert!(draft.validate(crate::i18n::Locale::default()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_table_with_no_columns_is_refused() {
|
||||
let mut draft = draft_with_column("total", "int");
|
||||
draft.remove_column(0).unwrap();
|
||||
draft.remove_column(crate::i18n::Locale::default(), 0).unwrap();
|
||||
|
||||
assert!(draft.validate().is_err());
|
||||
assert!(draft.validate(crate::i18n::Locale::default()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -840,7 +878,7 @@ mod tests {
|
||||
draft.toggle_row_display_candidate(1);
|
||||
assert_eq!(draft.row_display_columns, vec!["number"]);
|
||||
|
||||
draft.remove_column(0).unwrap();
|
||||
draft.remove_column(crate::i18n::Locale::default(), 0).unwrap();
|
||||
assert!(draft.row_display_columns.is_empty());
|
||||
}
|
||||
|
||||
@@ -848,7 +886,7 @@ mod tests {
|
||||
fn the_preview_shows_system_and_user_columns() {
|
||||
let draft = draft_with_column("number", "text");
|
||||
|
||||
let rows = draft.preview_rows();
|
||||
let rows = draft.preview_rows(&crate::i18n::Locale::default());
|
||||
let columns = rows
|
||||
.iter()
|
||||
.map(|row| row.column.as_str())
|
||||
@@ -880,7 +918,7 @@ mod tests {
|
||||
draft.toggle_row_display_candidate(1); // number
|
||||
draft.toggle_row_display_candidate(2); // issued_on
|
||||
|
||||
draft.move_column(0, 1).unwrap();
|
||||
draft.move_column(crate::i18n::Locale::default(), 0, 1).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
draft
|
||||
@@ -909,7 +947,7 @@ mod tests {
|
||||
currency: "CZK".to_string(),
|
||||
});
|
||||
|
||||
let rows = draft.preview_rows();
|
||||
let rows = draft.preview_rows(&crate::i18n::Locale::default());
|
||||
let columns = rows
|
||||
.iter()
|
||||
.map(|row| row.column.as_str())
|
||||
@@ -954,7 +992,7 @@ mod tests {
|
||||
let mut draft = draft_with_column("number", "text");
|
||||
draft.columns.toggle_indexed(0);
|
||||
|
||||
assert_eq!(draft.into_request().unwrap().indexes, vec!["number"]);
|
||||
assert_eq!(draft.into_request(crate::i18n::Locale::default()).unwrap().indexes, vec!["number"]);
|
||||
}
|
||||
|
||||
/// The request the builder sends is one the server will take: a link
|
||||
@@ -976,12 +1014,15 @@ mod tests {
|
||||
});
|
||||
|
||||
// Refused rather than quietly repaired, because the server refuses it.
|
||||
let error = draft.clone().into_request().unwrap_err();
|
||||
let error = draft
|
||||
.clone()
|
||||
.into_request(crate::i18n::Locale::default())
|
||||
.unwrap_err();
|
||||
assert!(error.contains("indexed automatically"), "{error}");
|
||||
|
||||
draft.columns.added[1].indexed = false;
|
||||
draft.columns.toggle_indexed(0);
|
||||
let request = draft.into_request().unwrap();
|
||||
let request = draft.into_request(crate::i18n::Locale::default()).unwrap();
|
||||
|
||||
assert_eq!(request.indexes, vec!["number"]);
|
||||
assert!(request.columns[0].required);
|
||||
@@ -995,16 +1036,16 @@ mod tests {
|
||||
let mut draft = draft_with_column("total", "int");
|
||||
|
||||
draft.table_name = "t".repeat(39);
|
||||
let error = draft.validate().unwrap_err();
|
||||
let error = draft.validate(crate::i18n::Locale::default()).unwrap_err();
|
||||
assert!(error.contains("38 characters"), "{error}");
|
||||
|
||||
draft.table_name = "accounts".to_string();
|
||||
assert!(draft.validate().is_err());
|
||||
assert!(draft.validate(crate::i18n::Locale::default()).is_err());
|
||||
draft.table_name = "general_ledger".to_string();
|
||||
assert!(draft.validate().is_err());
|
||||
assert!(draft.validate(crate::i18n::Locale::default()).is_err());
|
||||
|
||||
draft.table_name = "invoice".to_string();
|
||||
assert!(draft.validate().is_ok());
|
||||
assert!(draft.validate(crate::i18n::Locale::default()).is_ok());
|
||||
}
|
||||
|
||||
/// A shared table has no books of its own, so the two definition rows that
|
||||
@@ -1015,7 +1056,7 @@ mod tests {
|
||||
draft.global = true;
|
||||
draft.columns.global = true;
|
||||
|
||||
assert!(draft.validate().is_err());
|
||||
assert!(draft.validate(crate::i18n::Locale::default()).is_err());
|
||||
|
||||
draft.columns.added.clear();
|
||||
draft.columns.added.push(ColumnDefinition {
|
||||
@@ -1027,7 +1068,7 @@ mod tests {
|
||||
money_mode: MoneyMode::Exact,
|
||||
currency: String::new(),
|
||||
});
|
||||
assert!(draft.validate().is_err());
|
||||
assert!(draft.validate(crate::i18n::Locale::default()).is_err());
|
||||
}
|
||||
|
||||
/// The preview is the schema as it will exist, so it carries every system
|
||||
@@ -1038,7 +1079,7 @@ mod tests {
|
||||
let draft = draft_with_column("number", "text");
|
||||
assert!(
|
||||
draft
|
||||
.preview_rows()
|
||||
.preview_rows(&crate::i18n::Locale::default())
|
||||
.iter()
|
||||
.any(|row| row.column == "row_revision" && row.source == "system")
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ use axum_extra::extract::Form;
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
{i18n::Locale, tr},
|
||||
services::{authenticated_request, reject_cross_site},
|
||||
};
|
||||
|
||||
@@ -45,7 +46,7 @@ pub(crate) async fn new_table_page(
|
||||
|
||||
match load_page(state, &headers, draft, None, None).await {
|
||||
Ok(page) => Html(ui::render_page(&page)).into_response(),
|
||||
Err(error) => load_error_response(error),
|
||||
Err(error) => load_error_response(&headers, error),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +62,7 @@ pub(crate) async fn update_builder(
|
||||
|
||||
let mut page = match load_page(state, &headers, form.to_draft(), None, None).await {
|
||||
Ok(page) => page,
|
||||
Err(error) => return load_error_response(error),
|
||||
Err(error) => return load_error_response(&headers, error),
|
||||
};
|
||||
|
||||
apply_action(&mut page, &form);
|
||||
@@ -80,12 +81,12 @@ pub(crate) async fn create_table(
|
||||
|
||||
let mut page = match load_page(state.clone(), &headers, form.to_draft(), None, None).await {
|
||||
Ok(page) => page,
|
||||
Err(error) => return load_error_response(error),
|
||||
Err(error) => return load_error_response(&headers, error),
|
||||
};
|
||||
|
||||
// The draft is validated here with exactly the checks the client runs
|
||||
// before it will save; the server re-validates authoritatively.
|
||||
let request = match page.draft.clone().into_request() {
|
||||
let request = match page.draft.clone().into_request(Locale::from_headers(&headers)) {
|
||||
Ok(request) => request,
|
||||
Err(message) => {
|
||||
page.error = Some(message);
|
||||
@@ -127,7 +128,10 @@ pub(crate) async fn create_table(
|
||||
}
|
||||
Ok(response) => {
|
||||
page.error = Some(if response.get_ref().sql.is_empty() {
|
||||
"The backend did not create the table.".to_string()
|
||||
tr!(
|
||||
Locale::from_headers(&headers),
|
||||
"builder-err-backend-no-create"
|
||||
)
|
||||
} else {
|
||||
response.get_ref().sql.clone()
|
||||
});
|
||||
@@ -146,30 +150,35 @@ pub(crate) async fn create_table(
|
||||
/// changes which fields apply, which is the client's field-visibility rule.
|
||||
fn apply_action(page: &mut AddTablePageState, form: &BuilderForm) {
|
||||
let index = form.index.unwrap_or(0);
|
||||
let locale = page.nav.locale;
|
||||
match form.action.as_str() {
|
||||
"add-column" => match page.draft.columns.add_from_inputs() {
|
||||
"add-column" => match page.draft.columns.add_from_inputs(locale) {
|
||||
Ok(status) => page.status = Some(status),
|
||||
Err(message) => page.error = Some(message),
|
||||
},
|
||||
"remove-column" => match page.draft.remove_column(index) {
|
||||
"remove-column" => match page.draft.remove_column(locale, index) {
|
||||
Ok(status) => page.status = Some(status),
|
||||
Err(message) => page.error = Some(message),
|
||||
},
|
||||
// The order columns are declared in is the order the table gets them
|
||||
// in, so moving one is a change to the draft like any other.
|
||||
"move-column-up" => page.status = page.draft.move_column(index, -1),
|
||||
"move-column-down" => page.status = page.draft.move_column(index, 1),
|
||||
"move-column-up" => page.status = page.draft.move_column(locale, index, -1),
|
||||
"move-column-down" => page.status = page.draft.move_column(locale, index, 1),
|
||||
// The alias fields carry no `change` trigger of their own — a text
|
||||
// input fires `change` on blur, which would swap the builder out from
|
||||
// under the click that is still in flight. This button is what applies
|
||||
// what was typed, so the column list and the preview say what the
|
||||
// table will really be called.
|
||||
"apply-generated-names" => match page.draft.validate_generated_aliases() {
|
||||
"apply-generated-names" => match page.draft.validate_generated_aliases(locale) {
|
||||
Ok(()) => {
|
||||
page.status = Some(match page.draft.aliased_generated_columns().len() {
|
||||
0 => "The generated columns keep their own names.".to_string(),
|
||||
1 => "1 generated column renamed.".to_string(),
|
||||
count => format!("{count} generated columns renamed."),
|
||||
0 => tr!(locale, "builder-status-generated-keep"),
|
||||
1 => tr!(locale, "builder-status-generated-one"),
|
||||
count => tr!(
|
||||
locale,
|
||||
"builder-status-generated-many",
|
||||
"count" => count as i64,
|
||||
),
|
||||
})
|
||||
}
|
||||
Err(message) => page.error = Some(message),
|
||||
@@ -182,7 +191,7 @@ fn apply_action(page: &mut AddTablePageState, form: &BuilderForm) {
|
||||
// one that does: the picker stops offering the columns that post to a
|
||||
// profile's books, and one added before the switch is still there.
|
||||
"refresh" => {
|
||||
if let Err(message) = page.draft.columns.validate() {
|
||||
if let Err(message) = page.draft.columns.validate(locale) {
|
||||
page.error = Some(message);
|
||||
}
|
||||
}
|
||||
@@ -190,19 +199,26 @@ fn apply_action(page: &mut AddTablePageState, form: &BuilderForm) {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_error_response(error: LoadError) -> Response {
|
||||
fn load_error_response(headers: &HeaderMap, error: LoadError) -> Response {
|
||||
match error {
|
||||
LoadError::Unauthenticated => Redirect::to("/login").into_response(),
|
||||
LoadError::Forbidden => (
|
||||
StatusCode::FORBIDDEN,
|
||||
Html(ui::render_submission_error(
|
||||
"Table-management permission is required.",
|
||||
Locale::from_headers(headers),
|
||||
&tr!(
|
||||
Locale::from_headers(headers),
|
||||
"builder-err-permission"
|
||||
),
|
||||
)),
|
||||
)
|
||||
.into_response(),
|
||||
LoadError::Backend(message) => (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Html(ui::render_submission_error(&message)),
|
||||
Html(ui::render_submission_error(
|
||||
Locale::from_headers(headers),
|
||||
&message,
|
||||
)),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use askama::Template;
|
||||
|
||||
use crate::{
|
||||
{i18n::Locale, tr},
|
||||
schema::CURRENCY_CODES,
|
||||
ui::{Alert, Nav, render},
|
||||
};
|
||||
@@ -24,6 +25,7 @@ struct AddTablePage<'a> {
|
||||
#[derive(Template)]
|
||||
#[template(path = "pages/add_table/builder.html")]
|
||||
struct BuilderFragment<'a> {
|
||||
nav: Nav,
|
||||
page: &'a AddTablePageState,
|
||||
column_types: Vec<String>,
|
||||
temporal_types: Vec<String>,
|
||||
@@ -45,6 +47,7 @@ pub(crate) fn render_page(page: &AddTablePageState) -> String {
|
||||
|
||||
pub(crate) fn render_builder(page: &AddTablePageState) -> String {
|
||||
render(&BuilderFragment {
|
||||
nav: page.nav.clone(),
|
||||
page,
|
||||
column_types: page.draft.columns.offered_types(),
|
||||
temporal_types: page.draft.columns.temporal_types(),
|
||||
@@ -55,8 +58,12 @@ pub(crate) fn render_builder(page: &AddTablePageState) -> String {
|
||||
/// Used when the page itself cannot be loaded (auth or backend failure).
|
||||
/// There is no draft left to render, so this replaces the builder — the dialog
|
||||
/// is what tells the user why the form just emptied.
|
||||
pub(crate) fn render_submission_error(message: &str) -> String {
|
||||
render(&Alert::error("Could not create the table", message))
|
||||
pub(crate) fn render_submission_error(locale: Locale, message: &str) -> String {
|
||||
render(&Alert::error(
|
||||
locale,
|
||||
&tr!(locale, "builder-err-title"),
|
||||
message,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user