talbe definition web
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
//! Reads everything the workspace shows.
|
||||
//!
|
||||
//! Three calls, in this order: the profile tree names the profiles and their
|
||||
//! tables, the profile details describe the selected table's columns and
|
||||
//! scripts, and the rename history explains how those columns got their names.
|
||||
//! Four calls, in this order: the column-type catalog is the vocabulary the
|
||||
//! append panel offers, the profile tree names the profiles and their tables,
|
||||
//! the profile details describe the selected table's columns and scripts, and
|
||||
//! the rename history explains how those columns got their names.
|
||||
//! Nothing here trusts the posted selection — a profile or table that is gone
|
||||
//! is dropped from the selection rather than reported as an error, because the
|
||||
//! commonest way to get here with a stale one is having just deleted it.
|
||||
|
||||
use axum::http::HeaderMap;
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
@@ -16,8 +18,10 @@ use crate::{
|
||||
common::Empty,
|
||||
table_definition::{
|
||||
GetColumnAliasRenameHistoryRequest, GetProfileDetailsRequest, MoneyRounding,
|
||||
table_definition_client::TableDefinitionClient,
|
||||
},
|
||||
},
|
||||
schema::{ColumnCatalog, column_catalog},
|
||||
services::authenticated_request,
|
||||
};
|
||||
|
||||
@@ -26,6 +30,27 @@ use super::state::{
|
||||
TableDetailView, TableSummary,
|
||||
};
|
||||
|
||||
/// Reads the column-type vocabulary on its own.
|
||||
///
|
||||
/// The panel's handlers stage and validate a column before the rest of the
|
||||
/// workspace is read, and they cannot do either without the vocabulary, so
|
||||
/// they fetch it with this and hand it to [`load_page`] on the draft.
|
||||
pub(crate) async fn load_column_catalog(
|
||||
definitions: &mut TableDefinitionClient<Channel>,
|
||||
headers: &HeaderMap,
|
||||
) -> Result<ColumnCatalog, LoadError> {
|
||||
let request =
|
||||
authenticated_request(headers, Empty {}).map_err(|_| LoadError::Unauthenticated)?;
|
||||
let response = definitions
|
||||
.list_column_types(request)
|
||||
.await
|
||||
.map_err(|error| match error.code() {
|
||||
tonic::Code::Unauthenticated => LoadError::Unauthenticated,
|
||||
_ => LoadError::Backend(error.message().to_string()),
|
||||
})?;
|
||||
Ok(column_catalog(response.into_inner().column_types))
|
||||
}
|
||||
|
||||
pub(crate) async fn load_page(
|
||||
state: AppState,
|
||||
headers: &HeaderMap,
|
||||
@@ -47,6 +72,15 @@ pub(crate) async fn load_page(
|
||||
}
|
||||
|
||||
let mut definitions = state.definitions;
|
||||
// A handler that had to stage or validate a column before it got here has
|
||||
// already read the vocabulary; anything else reads it now.
|
||||
if !inputs.columns.catalog.is_loaded() {
|
||||
inputs.columns.catalog = load_column_catalog(&mut definitions, headers).await?;
|
||||
}
|
||||
// Also what the definition below is read through: the catalog describes
|
||||
// the server-generated companion types as well as the declarable ones.
|
||||
let catalog = inputs.columns.catalog.clone();
|
||||
|
||||
let tree = definitions
|
||||
.get_profile_tree(
|
||||
authenticated_request(headers, Empty {}).map_err(|_| LoadError::Unauthenticated)?,
|
||||
@@ -119,6 +153,7 @@ pub(crate) async fn load_page(
|
||||
let behavior = table.column_behaviors.get(&column.name);
|
||||
DetailColumn {
|
||||
name: column.name.clone(),
|
||||
sql_type: catalog.sql_type(&column.field_type),
|
||||
field_type: column.field_type.clone(),
|
||||
currency: column.currency.clone(),
|
||||
quantity_ledger: column.quantity_ledger,
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
loader::load_page,
|
||||
loader::{self, load_page},
|
||||
state::{
|
||||
CopyForm, DeleteForm, GeneratedTableView, InvoiceTemplateForm, LoadError, PageInputs,
|
||||
RenameForm, Selection,
|
||||
@@ -73,8 +73,16 @@ pub(crate) async fn update_columns(
|
||||
return rejection;
|
||||
}
|
||||
|
||||
// Staging a column applies the vocabulary's rules, so it is read before
|
||||
// the panel is rebuilt; `load_page` reuses what is read here.
|
||||
let mut definitions = state.definitions.clone();
|
||||
let catalog = match loader::load_column_catalog(&mut definitions, &headers).await {
|
||||
Ok(catalog) => catalog,
|
||||
Err(error) => return load_error_response(error),
|
||||
};
|
||||
|
||||
let mut inputs = PageInputs::for_selection(selection);
|
||||
inputs.columns = form.to_draft(false);
|
||||
inputs.columns = form.to_draft(catalog, false);
|
||||
|
||||
let index = form.index.unwrap_or(0);
|
||||
match form.action.as_str() {
|
||||
@@ -108,8 +116,16 @@ pub(crate) async fn add_columns(
|
||||
return rejection;
|
||||
}
|
||||
|
||||
// The draft is validated before the write, so the vocabulary it is held to
|
||||
// is read first; `load_page` reuses what is read here.
|
||||
let mut definitions = state.definitions.clone();
|
||||
let catalog = match loader::load_column_catalog(&mut definitions, &headers).await {
|
||||
Ok(catalog) => catalog,
|
||||
Err(error) => return load_error_response(error),
|
||||
};
|
||||
|
||||
let mut inputs = PageInputs::for_selection(selection);
|
||||
inputs.columns = form.to_draft(false);
|
||||
inputs.columns = form.to_draft(catalog.clone(), false);
|
||||
|
||||
if !inputs.selection.has_table() {
|
||||
return refuse(state, headers, inputs, "Select a table first.".to_string()).await;
|
||||
@@ -139,7 +155,6 @@ pub(crate) async fn add_columns(
|
||||
return Redirect::to("/login").into_response();
|
||||
};
|
||||
|
||||
let mut definitions = state.definitions.clone();
|
||||
match definitions.add_table_columns(request).await {
|
||||
Ok(response) if response.get_ref().success => {
|
||||
let added = inputs.columns.added.len();
|
||||
@@ -150,7 +165,7 @@ pub(crate) async fn add_columns(
|
||||
inputs.selection.table
|
||||
));
|
||||
// The columns are the table's now, so the panel starts empty.
|
||||
inputs.columns = crate::schema::ColumnDraft::for_append();
|
||||
inputs.columns = crate::schema::ColumnDraft::for_append(catalog);
|
||||
respond(state, headers, inputs, StatusCode::OK).await
|
||||
}
|
||||
Ok(response) => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! write: the response is rebuilt from the live profile tree rather than from
|
||||
//! whatever the browser still had on screen.
|
||||
|
||||
use crate::schema::ColumnDraft;
|
||||
use crate::schema::{ColumnCatalog, ColumnDraft};
|
||||
|
||||
/// The profile and table the workspace is pointed at. Arrives as a query
|
||||
/// string on the selector and on the column-panel endpoints, and as hidden
|
||||
@@ -82,6 +82,9 @@ impl TableDetailView {
|
||||
pub(crate) struct DetailColumn {
|
||||
pub name: String,
|
||||
pub field_type: String,
|
||||
/// The PostgreSQL type the column is stored as, from the column-type
|
||||
/// catalog. Empty for a type the catalog does not describe.
|
||||
pub sql_type: String,
|
||||
pub currency: String,
|
||||
pub quantity_ledger: bool,
|
||||
pub rounded: bool,
|
||||
@@ -225,10 +228,12 @@ pub(crate) struct PageInputs {
|
||||
}
|
||||
|
||||
impl PageInputs {
|
||||
/// A page with an empty append panel. The panel's vocabulary is filled in
|
||||
/// by the loader, which is the only thing that knows it.
|
||||
pub(crate) fn for_selection(selection: Selection) -> Self {
|
||||
Self {
|
||||
selection,
|
||||
columns: ColumnDraft::for_append(),
|
||||
columns: ColumnDraft::for_append(ColumnCatalog::default()),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
@@ -315,6 +320,7 @@ mod tests {
|
||||
DetailColumn {
|
||||
name: "work_phone".to_string(),
|
||||
field_type: "phone".to_string(),
|
||||
sql_type: "TEXT".to_string(),
|
||||
currency: String::new(),
|
||||
quantity_ledger: false,
|
||||
rounded: false,
|
||||
@@ -325,6 +331,7 @@ mod tests {
|
||||
DetailColumn {
|
||||
name: "work_phone_country".to_string(),
|
||||
field_type: "phone_country".to_string(),
|
||||
sql_type: "TEXT".to_string(),
|
||||
currency: String::new(),
|
||||
quantity_ledger: false,
|
||||
rounded: false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use askama::Template;
|
||||
|
||||
use crate::{
|
||||
schema::{CURRENCY_CODES, GTIN_TYPES, TEMPORAL_TYPES},
|
||||
schema::CURRENCY_CODES,
|
||||
ui::{Alert, Nav, render},
|
||||
};
|
||||
|
||||
@@ -13,9 +13,9 @@ use super::state::TableDefinitionPageState;
|
||||
struct TableDefinitionPage<'a> {
|
||||
nav: Nav,
|
||||
page: &'a TableDefinitionPageState,
|
||||
column_types: &'static [&'static str],
|
||||
temporal_types: &'static [&'static str],
|
||||
gtin_types: &'static [&'static str],
|
||||
column_types: Vec<String>,
|
||||
temporal_types: Vec<String>,
|
||||
gtin_types: Vec<String>,
|
||||
currency_codes: &'static [&'static str],
|
||||
/// False, as on the workspace fragment: the page embeds both, and the
|
||||
/// outcome is reported once, at the top.
|
||||
@@ -28,9 +28,9 @@ struct TableDefinitionPage<'a> {
|
||||
#[template(path = "pages/admin/table_definition/workspace.html")]
|
||||
struct WorkspaceFragment<'a> {
|
||||
page: &'a TableDefinitionPageState,
|
||||
column_types: &'static [&'static str],
|
||||
temporal_types: &'static [&'static str],
|
||||
gtin_types: &'static [&'static str],
|
||||
column_types: Vec<String>,
|
||||
temporal_types: Vec<String>,
|
||||
gtin_types: Vec<String>,
|
||||
/// False: the workspace shows the outcome of the last action itself, at
|
||||
/// the top, so the panel it embeds must not repeat it.
|
||||
standalone_column_panel: bool,
|
||||
@@ -41,9 +41,9 @@ struct WorkspaceFragment<'a> {
|
||||
#[template(path = "pages/admin/table_definition/column_panel.html")]
|
||||
struct ColumnPanelFragment<'a> {
|
||||
page: &'a TableDefinitionPageState,
|
||||
column_types: &'static [&'static str],
|
||||
temporal_types: &'static [&'static str],
|
||||
gtin_types: &'static [&'static str],
|
||||
column_types: Vec<String>,
|
||||
temporal_types: Vec<String>,
|
||||
gtin_types: Vec<String>,
|
||||
/// True: this is the whole response, so a refused column has nowhere else
|
||||
/// to be reported.
|
||||
standalone_column_panel: bool,
|
||||
@@ -56,8 +56,8 @@ pub(crate) fn render_page(page: &TableDefinitionPageState) -> String {
|
||||
// Asking the draft, so the picker can only ever offer what the draft
|
||||
// would accept — the accounting rule is stated once.
|
||||
column_types: page.columns.offered_types(),
|
||||
temporal_types: TEMPORAL_TYPES,
|
||||
gtin_types: GTIN_TYPES,
|
||||
temporal_types: page.columns.temporal_types(),
|
||||
gtin_types: page.columns.gtin_types(),
|
||||
currency_codes: CURRENCY_CODES,
|
||||
standalone_column_panel: false,
|
||||
})
|
||||
@@ -69,8 +69,8 @@ pub(crate) fn render_workspace(page: &TableDefinitionPageState) -> String {
|
||||
// Asking the draft, so the picker can only ever offer what the draft
|
||||
// would accept — the accounting rule is stated once.
|
||||
column_types: page.columns.offered_types(),
|
||||
temporal_types: TEMPORAL_TYPES,
|
||||
gtin_types: GTIN_TYPES,
|
||||
temporal_types: page.columns.temporal_types(),
|
||||
gtin_types: page.columns.gtin_types(),
|
||||
standalone_column_panel: false,
|
||||
})
|
||||
}
|
||||
@@ -81,8 +81,8 @@ pub(crate) fn render_column_panel(page: &TableDefinitionPageState) -> String {
|
||||
// Asking the draft, so the picker can only ever offer what the draft
|
||||
// would accept — the accounting rule is stated once.
|
||||
column_types: page.columns.offered_types(),
|
||||
temporal_types: TEMPORAL_TYPES,
|
||||
gtin_types: GTIN_TYPES,
|
||||
temporal_types: page.columns.temporal_types(),
|
||||
gtin_types: page.columns.gtin_types(),
|
||||
standalone_column_panel: true,
|
||||
})
|
||||
}
|
||||
@@ -131,6 +131,7 @@ mod tests {
|
||||
columns: vec![DetailColumn {
|
||||
name: "number".to_string(),
|
||||
field_type: "text".to_string(),
|
||||
sql_type: "TEXT".to_string(),
|
||||
currency: String::new(),
|
||||
quantity_ledger: false,
|
||||
rounded: false,
|
||||
@@ -140,7 +141,7 @@ mod tests {
|
||||
}],
|
||||
}),
|
||||
history: Vec::new(),
|
||||
columns: ColumnDraft::for_append(),
|
||||
columns: ColumnDraft::for_append(crate::schema::tests::catalog()),
|
||||
rename: RenameForm::default(),
|
||||
copy: CopyForm::default(),
|
||||
invoice: InvoiceTemplateForm::default(),
|
||||
|
||||
Reference in New Issue
Block a user