diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index a0d9d328..c73de16a 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -353,6 +353,9 @@ message ColumnBehavior { // Logical source column name, empty for ordinary user-defined columns. string generated_from = 3; + + // True when clients may offer this column in an alias rename picker. + bool renameable = 4; } // A script that targets a specific column in a table. diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 8a1045f9..358dde94 100644 Binary files a/common/src/proto/descriptor.bin and b/common/src/proto/descriptor.bin differ diff --git a/common/src/proto/komp_ac.table_definition.rs b/common/src/proto/komp_ac.table_definition.rs index d870c9c1..ffb8f478 100644 --- a/common/src/proto/komp_ac.table_definition.rs +++ b/common/src/proto/komp_ac.table_definition.rs @@ -363,6 +363,9 @@ pub struct ColumnBehavior { /// Logical source column name, empty for ordinary user-defined columns. #[prost(string, tag = "3")] pub generated_from: ::prost::alloc::string::String, + /// True when clients may offer this column in an alias rename picker. + #[prost(bool, tag = "4")] + pub renameable: bool, } /// A script that targets a specific column in a table. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] diff --git a/server b/server index 5b6b8603..da8681a1 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 5b6b860372459553eced168717d4dd4ecc639a26 +Subproject commit da8681a1c2df5c254ee9f6f1dc2c1f944e171185 diff --git a/web/src/pages/admin/table_definition/loader.rs b/web/src/pages/admin/table_definition/loader.rs index 542cd8bc..61cacc26 100644 --- a/web/src/pages/admin/table_definition/loader.rs +++ b/web/src/pages/admin/table_definition/loader.rs @@ -177,6 +177,7 @@ pub(crate) async fn load_page( generated_from: behavior .map(|behavior| behavior.generated_from.clone()) .unwrap_or_default(), + renameable: behavior.is_some_and(|behavior| behavior.renameable), } }) .collect(), diff --git a/web/src/pages/admin/table_definition/state.rs b/web/src/pages/admin/table_definition/state.rs index 1145bce2..a61ee357 100644 --- a/web/src/pages/admin/table_definition/state.rs +++ b/web/src/pages/admin/table_definition/state.rs @@ -74,13 +74,13 @@ impl TableDetailView { self.table_kind == "system" } - /// Columns a rename may target. A generated companion (`phone_country`, - /// the accounting fields) belongs to the column it was derived from, and - /// the server refuses to rename one. + /// Columns a rename may target. Provenance and renameability are separate: + /// accounting companions remain renameable while protected generated + /// columns do not. pub(crate) fn renameable_columns(&self) -> Vec<&DetailColumn> { self.columns .iter() - .filter(|column| !column.generated) + .filter(|column| column.renameable) .collect() } } @@ -98,6 +98,7 @@ pub(crate) struct DetailColumn { pub generated: bool, pub read_only: bool, pub generated_from: String, + pub renameable: bool, } impl DetailColumn { @@ -116,12 +117,10 @@ impl DetailColumn { if self.read_only { flags.push("read only".to_string()); } - if self.generated { - flags.push(if self.generated_from.is_empty() { - "generated".to_string() - } else { - format!("generated from {}", self.generated_from) - }); + if !self.generated_from.is_empty() { + flags.push(format!("generated from {}", self.generated_from)); + } else if self.generated { + flags.push("generated".to_string()); } flags } @@ -361,7 +360,7 @@ mod tests { } #[test] - fn a_generated_column_says_what_it_came_from_and_cannot_be_renamed() { + fn provenance_and_renameability_are_independent() { let detail = TableDetailView { id: 1, name: "contact".to_string(), @@ -379,6 +378,7 @@ mod tests { generated: false, read_only: false, generated_from: String::new(), + renameable: true, }, DetailColumn { name: "work_phone_country".to_string(), @@ -390,16 +390,34 @@ mod tests { generated: true, read_only: true, generated_from: "work_phone".to_string(), + renameable: false, + }, + DetailColumn { + name: "charge".to_string(), + field_type: "money".to_string(), + sql_type: "NUMERIC".to_string(), + currency: "EUR".to_string(), + quantity_ledger: false, + rounded: false, + generated: true, + read_only: false, + generated_from: "accounting".to_string(), + renameable: true, }, ], }; let renameable = detail.renameable_columns(); - assert_eq!(renameable.len(), 1); + assert_eq!(renameable.len(), 2); assert_eq!(renameable[0].name, "work_phone"); + assert_eq!(renameable[1].name, "charge"); assert_eq!( detail.columns[1].flags(), vec!["read only", "generated from work_phone"] ); + assert_eq!( + detail.columns[2].flags(), + vec!["EUR", "generated from accounting"] + ); } } diff --git a/web/src/pages/admin/table_definition/ui.rs b/web/src/pages/admin/table_definition/ui.rs index 7e15b0be..ba7d6bfe 100644 --- a/web/src/pages/admin/table_definition/ui.rs +++ b/web/src/pages/admin/table_definition/ui.rs @@ -153,6 +153,7 @@ mod tests { generated: false, read_only: false, generated_from: String::new(), + renameable: true, }], }), history: Vec::new(),