//! Wire format for the builder form, and the page state the templates read. //! //! HTTP is stateless, so the whole draft travels with every interaction: each //! already-added column and display column is posted back as a set of //! parallel repeated fields. `serde_html_form` (via `axum_extra::extract::Form`) //! decodes the repeats into `Vec`s, which `to_draft` zips back into a //! [`TableDraft`]. //! //! The column half of that form is the shared one — the field names here are //! the same ones [`crate::schema::ColumnForm`] declares, and both are rebuilt //! by [`crate::schema::columns_from_rows`], so the two screens that describe //! columns cannot drift apart. use crate::schema::{ColumnCatalog, ColumnDraft, columns_from_rows}; use super::draft::{ACCOUNTING_FIELD_TYPE, TableDraft}; /// The `profile_name` option meaning "create a new profile too". pub(crate) const NEW_PROFILE: &str = "__new__"; #[derive(Clone, Debug, Default, serde::Deserialize)] pub(crate) struct BuilderForm { /// Which builder button was pressed. Empty on the initial page load. #[serde(default)] pub action: String, /// Row the action applies to, for the per-row buttons. #[serde(default)] pub index: Option, #[serde(default)] pub profile_name: String, #[serde(default)] pub profile_name_input: String, #[serde(default)] pub accounting_currency: String, #[serde(default)] pub global: bool, #[serde(default)] pub table_name: String, // The pending column being described in the input panel. #[serde(default)] pub column_name_input: String, #[serde(default)] pub column_type_input: String, #[serde(default)] pub temporal_type_input: String, #[serde(default)] pub gtin_type_input: String, #[serde(default)] pub link_table_input: String, #[serde(default)] pub decimal_precision_input: String, #[serde(default)] pub decimal_scale_input: String, #[serde(default)] pub column_indexing_input: String, #[serde(default)] pub column_quantity_ledger_input: String, #[serde(default)] pub column_rounding_input: String, #[serde(default)] pub column_currency_input: String, // One entry per already-added column, in order. #[serde(default)] pub column_names: Vec, #[serde(default)] pub column_types: Vec, #[serde(default)] pub column_indexed: Vec, #[serde(default)] pub column_quantity_ledger: Vec, #[serde(default)] pub column_rounding: Vec, #[serde(default)] pub column_currencies: Vec, // Tables the profile offers as link targets, in order. #[serde(default)] pub relation_tables: Vec, #[serde(default)] pub row_display_columns: Vec, } impl BuilderForm { pub(crate) fn creating_new_profile(&self) -> bool { self.profile_name == NEW_PROFILE } /// Rebuilds the draft this form was rendered from. pub(crate) fn to_draft(&self) -> TableDraft { let creating_new_profile = self.creating_new_profile(); let columns = ColumnDraft { name_input: self.column_name_input.clone(), type_input: self.column_type_input.clone(), temporal_type_input: self.temporal_type_input.clone(), gtin_type_input: self.gtin_type_input.clone(), link_table_input: self.link_table_input.clone(), decimal_precision_input: self.decimal_precision_input.clone(), decimal_scale_input: self.decimal_scale_input.clone(), indexing_input: self.column_indexing_input.clone(), quantity_ledger_input: self.column_quantity_ledger_input.clone(), rounding_input: self.column_rounding_input.clone(), currency_input: self.column_currency_input.clone(), added: columns_from_rows( &self.column_names, &self.column_types, &self.column_indexed, &self.column_quantity_ledger, &self.column_rounding, &self.column_currencies, ), // Filled in by the loader from `ListColumnTypes`, never by the // form: it is the vocabulary the draft is validated against. catalog: ColumnCatalog::default(), // The table is being created here, so the creation-only types are // on the table. creating_table: true, }; // Drop display columns whose column is gone, so a stale post cannot // send a display column that no longer exists. let row_display_columns = self .row_display_columns .iter() .filter(|display| columns.added.iter().any(|column| &&column.name == display)) .cloned() .collect(); TableDraft { profile_name: if creating_new_profile { String::new() } else { self.profile_name.trim().to_string() }, profile_name_input: self.profile_name_input.clone(), creating_new_profile, accounting_currency: self.accounting_currency.clone(), global: self.global, table_name: self.table_name.clone(), columns, relation_tables: self.relation_tables.clone(), relation_table_options: Vec::new(), row_display_columns, // Filled in by the loader from the live profile tree, never by the // client: it is what duplicate table names are checked against. existing_profile_tables: Vec::new(), } } } /// What the page and the builder fragment render. pub(crate) struct AddTablePageState { pub nav: crate::ui::Nav, pub profiles: Vec, pub draft: TableDraft, /// The outcome of the last builder action, if any. pub status: Option, pub error: Option, } impl AddTablePageState { /// The value the profile `