use askama::Template; use crate::{ schema::CURRENCY_CODES, ui::{Alert, Nav, render}, }; use super::state::AddTablePageState; /// GET /admin/tables/new — the page shell around the builder. #[derive(Template)] #[template(path = "pages/add_table/add_table.html")] struct AddTablePage<'a> { nav: Nav, page: &'a AddTablePageState, column_types: Vec, temporal_types: Vec, gtin_types: Vec, currency_codes: &'static [&'static str], } /// POST /admin/tables/builder — the `#builder` swap, which is the same markup /// the page embeds, so one template serves both. #[derive(Template)] #[template(path = "pages/add_table/builder.html")] struct BuilderFragment<'a> { page: &'a AddTablePageState, column_types: Vec, temporal_types: Vec, gtin_types: Vec, } pub(crate) fn render_page(page: &AddTablePageState) -> String { render(&AddTablePage { nav: page.nav.clone(), page, // Every picker is the backend's own vocabulary, asked through the // draft so it can only offer what the draft would accept. column_types: page.draft.columns.offered_types(), temporal_types: page.draft.columns.temporal_types(), gtin_types: page.draft.columns.gtin_types(), currency_codes: CURRENCY_CODES, }) } pub(crate) fn render_builder(page: &AddTablePageState) -> String { render(&BuilderFragment { page, column_types: page.draft.columns.offered_types(), temporal_types: page.draft.columns.temporal_types(), gtin_types: page.draft.columns.gtin_types(), }) } /// 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)) } #[cfg(test)] mod tests { use super::*; use crate::{ pages::add_table::draft::{LinkMode, TableDraft}, schema::{ColumnDefinition, MoneyMode}, }; fn page() -> AddTablePageState { let mut draft = TableDraft::new(); draft.columns.catalog = crate::schema::tests::catalog(); draft.profile_name = "billing".to_string(); draft.table_name = "invoice".to_string(); draft.columns.added.push(ColumnDefinition { name: "number".to_string(), data_type: "text".to_string(), indexed: true, quantity_ledger: false, money_mode: MoneyMode::Exact, currency: String::new(), }); draft.set_available_relation_tables(vec!["customer".to_string()]); draft.cycle_link_mode(0); draft.toggle_row_display_candidate(1); AddTablePageState { nav: Nav::default(), profiles: vec!["billing".to_string()], draft, status: None, error: None, } } /// `render` turns a template failure into an error string rather than /// panicking, so the markup has to be asserted on. #[test] fn the_builder_carries_the_whole_draft_and_the_preview() { let html = render_builder(&page()); assert!(!html.contains("Template error"), "{html}"); // Every part of the draft travels with the next request. assert!(html.contains(r#"name="column_names" value="number""#)); assert!(html.contains(r#"name="column_indexed" value="yes""#)); assert!(html.contains(r#"name="link_tables" value="customer""#)); assert!(html.contains(r#"name="link_modes" value="optional""#)); assert!(html.contains(r#"name="row_display_columns" value="number""#)); // The preview shows the schema as it will exist. assert!(html.contains("customer_id")); assert!(html.contains("BIGSERIAL")); assert!(html.contains("TIMESTAMPTZ")); } #[test] fn the_page_offers_the_new_profile_option_and_the_currency_list() { let html = render_page(&page()); assert!(!html.contains("Template error"), "{html}"); assert!(html.contains(r#"value="__new__""#)); assert!(html.contains(r#""#)); assert!(html.contains(r#"