multirow FK
This commit is contained in:
@@ -157,10 +157,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
".komp_ac.table_definition.ColumnDefinition",
|
".komp_ac.table_definition.ColumnDefinition",
|
||||||
"#[derive(serde::Serialize, serde::Deserialize)]",
|
"#[derive(serde::Serialize, serde::Deserialize)]",
|
||||||
)
|
)
|
||||||
.type_attribute(
|
|
||||||
".komp_ac.table_definition.TableLink",
|
|
||||||
"#[derive(serde::Serialize, serde::Deserialize)]"
|
|
||||||
)
|
|
||||||
.type_attribute(
|
.type_attribute(
|
||||||
".komp_ac.table_definition.PostTableDefinitionRequest",
|
".komp_ac.table_definition.PostTableDefinitionRequest",
|
||||||
"#[derive(serde::Serialize, serde::Deserialize)]",
|
"#[derive(serde::Serialize, serde::Deserialize)]",
|
||||||
|
|||||||
@@ -48,19 +48,6 @@ service TableDefinition {
|
|||||||
rpc DeleteTable(DeleteTableRequest) returns (DeleteTableResponse);
|
rpc DeleteTable(DeleteTableRequest) returns (DeleteTableResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A single link to another table within the same profile (schema).
|
|
||||||
message TableLink {
|
|
||||||
// Name of an existing table within the same profile to link to.
|
|
||||||
// For each link, a "<linked>_id" column is created on the new table.
|
|
||||||
// That column references "<linked>"(id) and adds an index automatically.
|
|
||||||
string linked_table_name = 1;
|
|
||||||
|
|
||||||
// If true, the generated foreign key column is NOT NULL.
|
|
||||||
// Otherwise the column allows NULL.
|
|
||||||
// Duplicate links to the same target table in one request are rejected.
|
|
||||||
bool required = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defines the input for creating a new table definition.
|
// Defines the input for creating a new table definition.
|
||||||
message PostTableDefinitionRequest {
|
message PostTableDefinitionRequest {
|
||||||
// Table name to create inside the target profile.
|
// Table name to create inside the target profile.
|
||||||
@@ -69,10 +56,6 @@ message PostTableDefinitionRequest {
|
|||||||
// Forbidden names: "id", "deleted", "created_at", "row_revision", or ending in "_id".
|
// Forbidden names: "id", "deleted", "created_at", "row_revision", or ending in "_id".
|
||||||
string table_name = 1;
|
string table_name = 1;
|
||||||
|
|
||||||
// List of links (foreign keys) to existing tables in the same profile.
|
|
||||||
// Each will automatically get a "<linked>_id" column and an index.
|
|
||||||
repeated TableLink links = 2;
|
|
||||||
|
|
||||||
// List of user-defined columns (adds to system/id/fk columns).
|
// List of user-defined columns (adds to system/id/fk columns).
|
||||||
repeated ColumnDefinition columns = 3;
|
repeated ColumnDefinition columns = 3;
|
||||||
|
|
||||||
@@ -187,6 +170,9 @@ message ColumnDefinition {
|
|||||||
// DURATION
|
// DURATION
|
||||||
// PERIOD
|
// PERIOD
|
||||||
// DECIMAL(p,s) → NUMERIC(p,s)
|
// DECIMAL(p,s) → NUMERIC(p,s)
|
||||||
|
// LINK(table) → BIGINT referencing that table in the same profile, indexed
|
||||||
|
// automatically. A table may hold several links to the same
|
||||||
|
// target as long as the columns are named differently.
|
||||||
// DECIMAL args must be integers (no sign, no dot, no leading zeros);
|
// DECIMAL args must be integers (no sign, no dot, no leading zeros);
|
||||||
// s ≤ p and p ≥ 1.
|
// s ≤ p and p ≥ 1.
|
||||||
string field_type = 2;
|
string field_type = 2;
|
||||||
@@ -371,6 +357,12 @@ enum ColumnTypeSpelling {
|
|||||||
// at least 1 and scale no greater than precision; neither may carry a sign,
|
// at least 1 and scale no greater than precision; neither may carry a sign,
|
||||||
// a decimal point, or leading zeros.
|
// a decimal point, or leading zeros.
|
||||||
COLUMN_TYPE_SPELLING_DECIMAL = 1;
|
COLUMN_TYPE_SPELLING_DECIMAL = 1;
|
||||||
|
|
||||||
|
// The name takes the name of another table in the same profile:
|
||||||
|
// "link(adresar)". The column holds that table's id, and the server creates
|
||||||
|
// the foreign key and its index. A picker offers the profile's other tables
|
||||||
|
// as the argument.
|
||||||
|
COLUMN_TYPE_SPELLING_LINK = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response describing the whole column-type vocabulary.
|
// Response describing the whole column-type vocabulary.
|
||||||
|
|||||||
Binary file not shown.
@@ -1,19 +1,4 @@
|
|||||||
// This file is @generated by prost-build.
|
// This file is @generated by prost-build.
|
||||||
/// A single link to another table within the same profile (schema).
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
|
||||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
|
||||||
pub struct TableLink {
|
|
||||||
/// Name of an existing table within the same profile to link to.
|
|
||||||
/// For each link, a "<linked>\_id" column is created on the new table.
|
|
||||||
/// That column references "<linked>"(id) and adds an index automatically.
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub linked_table_name: ::prost::alloc::string::String,
|
|
||||||
/// If true, the generated foreign key column is NOT NULL.
|
|
||||||
/// Otherwise the column allows NULL.
|
|
||||||
/// Duplicate links to the same target table in one request are rejected.
|
|
||||||
#[prost(bool, tag = "2")]
|
|
||||||
pub required: bool,
|
|
||||||
}
|
|
||||||
/// Defines the input for creating a new table definition.
|
/// Defines the input for creating a new table definition.
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
@@ -24,10 +9,6 @@ pub struct PostTableDefinitionRequest {
|
|||||||
/// Forbidden names: "id", "deleted", "created_at", "row_revision", or ending in "\_id".
|
/// Forbidden names: "id", "deleted", "created_at", "row_revision", or ending in "\_id".
|
||||||
#[prost(string, tag = "1")]
|
#[prost(string, tag = "1")]
|
||||||
pub table_name: ::prost::alloc::string::String,
|
pub table_name: ::prost::alloc::string::String,
|
||||||
/// List of links (foreign keys) to existing tables in the same profile.
|
|
||||||
/// Each will automatically get a "<linked>\_id" column and an index.
|
|
||||||
#[prost(message, repeated, tag = "2")]
|
|
||||||
pub links: ::prost::alloc::vec::Vec<TableLink>,
|
|
||||||
/// List of user-defined columns (adds to system/id/fk columns).
|
/// List of user-defined columns (adds to system/id/fk columns).
|
||||||
#[prost(message, repeated, tag = "3")]
|
#[prost(message, repeated, tag = "3")]
|
||||||
pub columns: ::prost::alloc::vec::Vec<ColumnDefinition>,
|
pub columns: ::prost::alloc::vec::Vec<ColumnDefinition>,
|
||||||
@@ -153,6 +134,9 @@ pub struct ColumnDefinition {
|
|||||||
/// DURATION
|
/// DURATION
|
||||||
/// PERIOD
|
/// PERIOD
|
||||||
/// DECIMAL(p,s) → NUMERIC(p,s)
|
/// DECIMAL(p,s) → NUMERIC(p,s)
|
||||||
|
/// LINK(table) → BIGINT referencing that table in the same profile, indexed
|
||||||
|
/// automatically. A table may hold several links to the same
|
||||||
|
/// target as long as the columns are named differently.
|
||||||
/// DECIMAL args must be integers (no sign, no dot, no leading zeros);
|
/// DECIMAL args must be integers (no sign, no dot, no leading zeros);
|
||||||
/// s ≤ p and p ≥ 1.
|
/// s ≤ p and p ≥ 1.
|
||||||
#[prost(string, tag = "2")]
|
#[prost(string, tag = "2")]
|
||||||
@@ -486,6 +470,11 @@ pub enum ColumnTypeSpelling {
|
|||||||
/// at least 1 and scale no greater than precision; neither may carry a sign,
|
/// at least 1 and scale no greater than precision; neither may carry a sign,
|
||||||
/// a decimal point, or leading zeros.
|
/// a decimal point, or leading zeros.
|
||||||
Decimal = 1,
|
Decimal = 1,
|
||||||
|
/// The name takes the name of another table in the same profile:
|
||||||
|
/// "link(adresar)". The column holds that table's id, and the server creates
|
||||||
|
/// the foreign key and its index. A picker offers the profile's other tables
|
||||||
|
/// as the argument.
|
||||||
|
Link = 2,
|
||||||
}
|
}
|
||||||
impl ColumnTypeSpelling {
|
impl ColumnTypeSpelling {
|
||||||
/// String value of the enum field names used in the ProtoBuf definition.
|
/// String value of the enum field names used in the ProtoBuf definition.
|
||||||
@@ -496,6 +485,7 @@ impl ColumnTypeSpelling {
|
|||||||
match self {
|
match self {
|
||||||
Self::Bare => "COLUMN_TYPE_SPELLING_BARE",
|
Self::Bare => "COLUMN_TYPE_SPELLING_BARE",
|
||||||
Self::Decimal => "COLUMN_TYPE_SPELLING_DECIMAL",
|
Self::Decimal => "COLUMN_TYPE_SPELLING_DECIMAL",
|
||||||
|
Self::Link => "COLUMN_TYPE_SPELLING_LINK",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||||
@@ -503,6 +493,7 @@ impl ColumnTypeSpelling {
|
|||||||
match value {
|
match value {
|
||||||
"COLUMN_TYPE_SPELLING_BARE" => Some(Self::Bare),
|
"COLUMN_TYPE_SPELLING_BARE" => Some(Self::Bare),
|
||||||
"COLUMN_TYPE_SPELLING_DECIMAL" => Some(Self::Decimal),
|
"COLUMN_TYPE_SPELLING_DECIMAL" => Some(Self::Decimal),
|
||||||
|
"COLUMN_TYPE_SPELLING_LINK" => Some(Self::Link),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
server
2
server
Submodule server updated: 8ed666531b...9b5956ce99
@@ -8,65 +8,16 @@
|
|||||||
//!
|
//!
|
||||||
//! What a *column* may be is not here: that is [`crate::schema`], which the
|
//! What a *column* may be is not here: that is [`crate::schema`], which the
|
||||||
//! append screen in `admin/table_definition` shares. This module is only what
|
//! append screen in `admin/table_definition` shares. This module is only what
|
||||||
//! is true of a table being created — its profile, its name, its links, and
|
//! is true of a table being created — its profile, its name, its columns, and
|
||||||
//! what identifies one of its rows.
|
//! what identifies one of its rows.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
definitions::table_definition::{
|
definitions::table_definition::{
|
||||||
PostTableDefinitionRequest, TableLink as ProtoTableLink,
|
PostTableDefinitionRequest,
|
||||||
},
|
},
|
||||||
schema::{ColumnCatalog, ColumnDraft, proto_columns, validate_identifier},
|
schema::{ColumnCatalog, ColumnDraft, proto_columns, validate_identifier},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
||||||
pub(crate) enum LinkMode {
|
|
||||||
#[default]
|
|
||||||
None,
|
|
||||||
Optional,
|
|
||||||
Required,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LinkMode {
|
|
||||||
pub(crate) fn label(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::None => "none",
|
|
||||||
Self::Optional => "optional",
|
|
||||||
Self::Required => "required",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cycles none → optional → required → none, as `Select` does in the TUI.
|
|
||||||
pub(crate) fn next(self) -> Self {
|
|
||||||
match self {
|
|
||||||
Self::None => Self::Optional,
|
|
||||||
Self::Optional => Self::Required,
|
|
||||||
Self::Required => Self::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn from_label(value: &str) -> Self {
|
|
||||||
match value.trim() {
|
|
||||||
"optional" => Self::Optional,
|
|
||||||
"required" => Self::Required,
|
|
||||||
_ => Self::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_active(self) -> bool {
|
|
||||||
!matches!(self, Self::None)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_required(self) -> bool {
|
|
||||||
matches!(self, Self::Required)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub(crate) struct LinkDefinition {
|
|
||||||
pub linked_table_name: String,
|
|
||||||
pub mode: LinkMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One row of the "Table definition preview" — the schema as it will exist.
|
/// One row of the "Table definition preview" — the schema as it will exist.
|
||||||
pub(crate) struct PreviewRow {
|
pub(crate) struct PreviewRow {
|
||||||
pub mark: String,
|
pub mark: String,
|
||||||
@@ -91,7 +42,9 @@ pub(crate) struct TableDraft {
|
|||||||
/// The column panel: the pending column and the ones already described.
|
/// The column panel: the pending column and the ones already described.
|
||||||
pub columns: ColumnDraft,
|
pub columns: ColumnDraft,
|
||||||
|
|
||||||
pub links: Vec<LinkDefinition>,
|
/// Tables in the target profile, offered as `link(...)` targets by the
|
||||||
|
/// column picker.
|
||||||
|
pub relation_tables: Vec<String>,
|
||||||
/// Columns identifying a row to users, in the order they are shown.
|
/// Columns identifying a row to users, in the order they are shown.
|
||||||
/// Empty means rows are identified by their id alone.
|
/// Empty means rows are identified by their id alone.
|
||||||
pub row_display_columns: Vec<String>,
|
pub row_display_columns: Vec<String>,
|
||||||
@@ -133,12 +86,6 @@ impl TableDraft {
|
|||||||
Ok(format!("Column `{}` removed.", removed.name))
|
Ok(format!("Column `{}` removed.", removed.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn cycle_link_mode(&mut self, index: usize) {
|
|
||||||
if let Some(link) = self.links.get_mut(index) {
|
|
||||||
link.mode = link.mode.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds or removes one display-column candidate.
|
/// Adds or removes one display-column candidate.
|
||||||
///
|
///
|
||||||
/// Index 0 is `id`, which is not a display column of its own: choosing it
|
/// Index 0 is `id`, which is not a display column of its own: choosing it
|
||||||
@@ -175,25 +122,12 @@ impl TableDraft {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rebuilds the link list from the tables available in the target profile,
|
/// Records the tables the target profile offers as link targets. A table
|
||||||
/// keeping whatever mode each surviving link already had.
|
/// cannot link to itself, so its own name is never among them.
|
||||||
pub(crate) fn set_available_relation_tables(&mut self, table_names: Vec<String>) {
|
pub(crate) fn set_available_relation_tables(&mut self, table_names: Vec<String>) {
|
||||||
let previous_modes = self
|
self.relation_tables = table_names
|
||||||
.links
|
|
||||||
.iter()
|
|
||||||
.map(|link| (link.linked_table_name.clone(), link.mode))
|
|
||||||
.collect::<std::collections::HashMap<_, _>>();
|
|
||||||
|
|
||||||
self.links = table_names
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|table_name| table_name != &self.table_name)
|
.filter(|table_name| table_name != &self.table_name)
|
||||||
.map(|linked_table_name| LinkDefinition {
|
|
||||||
mode: previous_modes
|
|
||||||
.get(&linked_table_name)
|
|
||||||
.copied()
|
|
||||||
.unwrap_or(LinkMode::None),
|
|
||||||
linked_table_name,
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,20 +183,6 @@ impl TableDraft {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for link in self.links.iter().filter(|link| link.mode.is_active()) {
|
|
||||||
rows.push(PreviewRow {
|
|
||||||
mark: String::new(),
|
|
||||||
column: format!("{}_id", link.linked_table_name),
|
|
||||||
data_type: "BIGINT".to_string(),
|
|
||||||
option: if link.mode.is_required() {
|
|
||||||
"required".to_string()
|
|
||||||
} else {
|
|
||||||
"optional".to_string()
|
|
||||||
},
|
|
||||||
source: "relation".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for column in &self.columns.added {
|
for column in &self.columns.added {
|
||||||
rows.push(PreviewRow {
|
rows.push(PreviewRow {
|
||||||
mark: self
|
mark: self
|
||||||
@@ -324,16 +244,6 @@ impl TableDraft {
|
|||||||
profile_name: self.effective_profile_name(),
|
profile_name: self.effective_profile_name(),
|
||||||
columns: proto_columns(&self.columns.added),
|
columns: proto_columns(&self.columns.added),
|
||||||
indexes: self.columns.selected_index_names(),
|
indexes: self.columns.selected_index_names(),
|
||||||
links: self
|
|
||||||
.links
|
|
||||||
.iter()
|
|
||||||
.filter(|link| link.mode.is_active())
|
|
||||||
.map(|link| ProtoTableLink {
|
|
||||||
linked_table_name: link.linked_table_name.clone(),
|
|
||||||
required: link.mode.is_required(),
|
|
||||||
name: link.linked_table_name.clone(),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
accounting_currency: if self.creating_new_profile {
|
accounting_currency: if self.creating_new_profile {
|
||||||
self.accounting_currency.trim().to_ascii_uppercase()
|
self.accounting_currency.trim().to_ascii_uppercase()
|
||||||
} else {
|
} else {
|
||||||
@@ -430,34 +340,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn links_keep_their_mode_when_the_table_list_is_reloaded() {
|
fn a_table_never_offers_itself_as_a_link_target() {
|
||||||
let mut draft = draft_with_column("total", "int");
|
let mut draft = TableDraft::new();
|
||||||
draft.set_available_relation_tables(vec!["customer".into(), "project".into()]);
|
draft.table_name = "invoice".into();
|
||||||
draft.cycle_link_mode(0); // none -> optional
|
|
||||||
draft.cycle_link_mode(0); // optional -> required
|
|
||||||
|
|
||||||
draft.set_available_relation_tables(vec![
|
|
||||||
"customer".into(),
|
|
||||||
"project".into(),
|
|
||||||
"address".into(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert_eq!(draft.links[0].mode, LinkMode::Required);
|
|
||||||
assert_eq!(draft.links[2].mode, LinkMode::None);
|
|
||||||
|
|
||||||
let request = draft.into_request().unwrap();
|
|
||||||
assert_eq!(request.links.len(), 1);
|
|
||||||
assert_eq!(request.links[0].linked_table_name, "customer");
|
|
||||||
assert!(request.links[0].required);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_table_never_links_to_itself() {
|
|
||||||
let mut draft = draft_with_column("total", "int");
|
|
||||||
draft.set_available_relation_tables(vec!["invoice".into(), "customer".into()]);
|
draft.set_available_relation_tables(vec!["invoice".into(), "customer".into()]);
|
||||||
|
|
||||||
assert_eq!(draft.links.len(), 1);
|
assert_eq!(draft.relation_tables, vec!["customer".to_string()]);
|
||||||
assert_eq!(draft.links[0].linked_table_name, "customer");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -513,10 +401,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_preview_shows_system_relation_and_user_columns() {
|
fn the_preview_shows_system_and_user_columns() {
|
||||||
let mut draft = draft_with_column("number", "text");
|
let draft = draft_with_column("number", "text");
|
||||||
draft.set_available_relation_tables(vec!["customer".into()]);
|
|
||||||
draft.cycle_link_mode(0);
|
|
||||||
|
|
||||||
let rows = draft.preview_rows();
|
let rows = draft.preview_rows();
|
||||||
let columns = rows
|
let columns = rows
|
||||||
@@ -524,11 +410,7 @@ mod tests {
|
|||||||
.map(|row| row.column.as_str())
|
.map(|row| row.column.as_str())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(columns, vec!["id", "deleted", "number", "created_at"]);
|
||||||
columns,
|
|
||||||
vec!["id", "deleted", "customer_id", "number", "created_at"]
|
|
||||||
);
|
|
||||||
assert_eq!(rows[2].option, "optional");
|
|
||||||
// No display column chosen, so `id` identifies the row.
|
// No display column chosen, so `id` identifies the row.
|
||||||
assert_eq!(rows[0].mark, "[x]");
|
assert_eq!(rows[0].mark, "[x]");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ pub(crate) async fn load_page(
|
|||||||
// A brand-new (or not-yet-named) profile has nothing to link to.
|
// A brand-new (or not-yet-named) profile has nothing to link to.
|
||||||
None => {
|
None => {
|
||||||
draft.existing_profile_tables.clear();
|
draft.existing_profile_tables.clear();
|
||||||
draft.links.clear();
|
draft.relation_tables.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,6 @@ fn apply_action(page: &mut AddTablePageState, form: &BuilderForm) {
|
|||||||
Err(message) => page.error = Some(message),
|
Err(message) => page.error = Some(message),
|
||||||
},
|
},
|
||||||
"toggle-index" => page.draft.columns.toggle_indexed(index),
|
"toggle-index" => page.draft.columns.toggle_indexed(index),
|
||||||
"cycle-link" => page.draft.cycle_link_mode(index),
|
|
||||||
"toggle-display" => page.draft.toggle_row_display_candidate(index),
|
"toggle-display" => page.draft.toggle_row_display_candidate(index),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Wire format for the builder form, and the page state the templates read.
|
//! 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
|
//! HTTP is stateless, so the whole draft travels with every interaction: each
|
||||||
//! already-added column, link and display column is posted back as a set of
|
//! already-added column and display column is posted back as a set of
|
||||||
//! parallel repeated fields. `serde_html_form` (via `axum_extra::extract::Form`)
|
//! parallel repeated fields. `serde_html_form` (via `axum_extra::extract::Form`)
|
||||||
//! decodes the repeats into `Vec`s, which `to_draft` zips back into a
|
//! decodes the repeats into `Vec`s, which `to_draft` zips back into a
|
||||||
//! [`TableDraft`].
|
//! [`TableDraft`].
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
use crate::schema::{ColumnCatalog, ColumnDraft, columns_from_rows};
|
use crate::schema::{ColumnCatalog, ColumnDraft, columns_from_rows};
|
||||||
|
|
||||||
use super::draft::{LinkDefinition, LinkMode, TableDraft};
|
use super::draft::TableDraft;
|
||||||
|
|
||||||
/// The `profile_name` option meaning "create a new profile too".
|
/// The `profile_name` option meaning "create a new profile too".
|
||||||
pub(crate) const NEW_PROFILE: &str = "__new__";
|
pub(crate) const NEW_PROFILE: &str = "__new__";
|
||||||
@@ -72,11 +72,9 @@ pub(crate) struct BuilderForm {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub column_currencies: Vec<String>,
|
pub column_currencies: Vec<String>,
|
||||||
|
|
||||||
// One entry per link target offered by the profile, in order.
|
// Tables the profile offers as link targets, in order.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub link_tables: Vec<String>,
|
pub relation_tables: Vec<String>,
|
||||||
#[serde(default)]
|
|
||||||
pub link_modes: Vec<String>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub row_display_columns: Vec<String>,
|
pub row_display_columns: Vec<String>,
|
||||||
@@ -118,14 +116,6 @@ impl BuilderForm {
|
|||||||
creating_table: true,
|
creating_table: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let link_count = self.link_tables.len().min(self.link_modes.len());
|
|
||||||
let links = (0..link_count)
|
|
||||||
.map(|index| LinkDefinition {
|
|
||||||
linked_table_name: self.link_tables[index].clone(),
|
|
||||||
mode: LinkMode::from_label(&self.link_modes[index]),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Drop display columns whose column is gone, so a stale post cannot
|
// Drop display columns whose column is gone, so a stale post cannot
|
||||||
// send a display column that no longer exists.
|
// send a display column that no longer exists.
|
||||||
let row_display_columns = self
|
let row_display_columns = self
|
||||||
@@ -146,7 +136,7 @@ impl BuilderForm {
|
|||||||
accounting_currency: self.accounting_currency.clone(),
|
accounting_currency: self.accounting_currency.clone(),
|
||||||
table_name: self.table_name.clone(),
|
table_name: self.table_name.clone(),
|
||||||
columns,
|
columns,
|
||||||
links,
|
relation_tables: self.relation_tables.clone(),
|
||||||
row_display_columns,
|
row_display_columns,
|
||||||
// Filled in by the loader from the live profile tree, never by the
|
// Filled in by the loader from the live profile tree, never by the
|
||||||
// client: it is what duplicate table names are checked against.
|
// client: it is what duplicate table names are checked against.
|
||||||
@@ -240,22 +230,20 @@ mod tests {
|
|||||||
column_quantity_ledger: vec!["no".into(), "no".into()],
|
column_quantity_ledger: vec!["no".into(), "no".into()],
|
||||||
column_rounding: vec!["exact".into(), "half-up".into()],
|
column_rounding: vec!["exact".into(), "half-up".into()],
|
||||||
column_currencies: vec![String::new(), "EUR".into()],
|
column_currencies: vec![String::new(), "EUR".into()],
|
||||||
link_tables: vec!["customer".into(), "project".into()],
|
relation_tables: vec!["customer".into(), "project".into()],
|
||||||
link_modes: vec!["required".into(), "none".into()],
|
|
||||||
row_display_columns: vec!["number".into()],
|
row_display_columns: vec!["number".into()],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn round_trips_columns_links_and_display_columns() {
|
fn round_trips_columns_relation_tables_and_display_columns() {
|
||||||
let draft = posted_form().to_draft();
|
let draft = posted_form().to_draft();
|
||||||
|
|
||||||
assert_eq!(draft.columns.added.len(), 2);
|
assert_eq!(draft.columns.added.len(), 2);
|
||||||
assert!(draft.columns.added[0].indexed);
|
assert!(draft.columns.added[0].indexed);
|
||||||
assert_eq!(draft.columns.added[1].money_mode, MoneyMode::Rounded);
|
assert_eq!(draft.columns.added[1].money_mode, MoneyMode::Rounded);
|
||||||
assert_eq!(draft.links[0].mode, LinkMode::Required);
|
assert_eq!(draft.relation_tables, vec!["customer", "project"]);
|
||||||
assert_eq!(draft.links[1].mode, LinkMode::None);
|
|
||||||
assert_eq!(draft.row_display_columns, vec!["number"]);
|
assert_eq!(draft.row_display_columns, vec!["number"]);
|
||||||
assert!(!draft.creating_new_profile);
|
assert!(!draft.creating_new_profile);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,26 +195,8 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="builder-section">
|
<section class="builder-section">
|
||||||
<h2>Relations</h2>
|
{% for table in page.draft.relation_tables %}
|
||||||
{% if page.draft.links.is_empty() %}
|
<input type="hidden" name="relation_tables" value="{{ table }}">
|
||||||
<p class="hint">No other tables in this profile to link to.</p>
|
|
||||||
{% else %}
|
|
||||||
<p class="hint">Each active relation adds a <code><table>_id</code> column.</p>
|
|
||||||
<ul class="builder-list">
|
|
||||||
{% for link in page.draft.links %}
|
|
||||||
<li>
|
|
||||||
<button type="button" class="toggle mode-{{ link.mode.label() }}" hx-post="/admin/tables/builder"
|
|
||||||
hx-include="#table-form" hx-target="#builder" hx-swap="innerHTML"
|
|
||||||
hx-vals='{"action": "cycle-link", "index": "{{ loop.index0 }}"}'>{{ link.mode.label() }}</button>
|
|
||||||
<code>{{ link.linked_table_name }}</code>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% for link in page.draft.links %}
|
|
||||||
<input type="hidden" name="link_tables" value="{{ link.linked_table_name }}">
|
|
||||||
<input type="hidden" name="link_modes" value="{{ link.mode.label() }}">
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user