diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index 25b767cf..10dc0ea5 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -375,6 +375,8 @@ message TableDetail { map column_behaviors = 7; string table_kind = 8; bool global = 9; + // Revision of this table definition for optimistic concurrency control. + int64 row_version = 10; } // Server-owned behavior for one logical column returned in table details. @@ -417,12 +419,16 @@ message SetColumnPresentationRequest { string profile_name = 1; string table_name = 2; repeated ColumnPresentation columns = 3; + // Row version returned by GetProfileDetails when this presentation was read. + int64 expected_row_version = 4; } message SetColumnPresentationResponse { bool success = 1; string message = 2; repeated ColumnPresentation columns = 3; + // New row version after the update. + int64 row_version = 4; } // Request to delete one table definition entirely. diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 8b5f2276..2180e340 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 c264b1bd..b798fc69 100644 --- a/common/src/proto/komp_ac.table_definition.rs +++ b/common/src/proto/komp_ac.table_definition.rs @@ -385,6 +385,9 @@ pub struct TableDetail { pub table_kind: ::prost::alloc::string::String, #[prost(bool, tag = "9")] pub global: bool, + /// Revision of this table definition for optimistic concurrency control. + #[prost(int64, tag = "10")] + pub row_version: i64, } /// Server-owned behavior for one logical column returned in table details. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] @@ -440,6 +443,9 @@ pub struct SetColumnPresentationRequest { pub table_name: ::prost::alloc::string::String, #[prost(message, repeated, tag = "3")] pub columns: ::prost::alloc::vec::Vec, + /// Row version returned by GetProfileDetails when this presentation was read. + #[prost(int64, tag = "4")] + pub expected_row_version: i64, } #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -450,6 +456,9 @@ pub struct SetColumnPresentationResponse { pub message: ::prost::alloc::string::String, #[prost(message, repeated, tag = "3")] pub columns: ::prost::alloc::vec::Vec, + /// New row version after the update. + #[prost(int64, tag = "4")] + pub row_version: i64, } /// Request to delete one table definition entirely. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] diff --git a/server b/server index 481719e3..08319bdc 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 481719e3cb0d6960528f47c93f03975bb4a15ad3 +Subproject commit 08319bdc33f6952ed360ef02b3cc7af105360456 diff --git a/web/src/pages/admin/table_definition/loader.rs b/web/src/pages/admin/table_definition/loader.rs index 769793b6..1797b88c 100644 --- a/web/src/pages/admin/table_definition/loader.rs +++ b/web/src/pages/admin/table_definition/loader.rs @@ -194,6 +194,7 @@ pub(crate) async fn load_page( .find(|table| table.name == inputs.selection.table) .map(|table| TableDetailView { id: table.id, + row_version: table.row_version, columns: table .columns .iter() diff --git a/web/src/pages/admin/table_definition/logic.rs b/web/src/pages/admin/table_definition/logic.rs index d1d6174a..0c740c43 100644 --- a/web/src/pages/admin/table_definition/logic.rs +++ b/web/src/pages/admin/table_definition/logic.rs @@ -363,6 +363,7 @@ pub(crate) async fn set_column_presentation( profile_name: form.profile.clone(), table_name: form.table.clone(), columns, + expected_row_version: form.expected_row_version, }; let Ok(request) = authenticated_request(&headers, request) else { return Redirect::to("/login").into_response(); diff --git a/web/src/pages/admin/table_definition/state.rs b/web/src/pages/admin/table_definition/state.rs index 3a13e7d5..05c52aea 100644 --- a/web/src/pages/admin/table_definition/state.rs +++ b/web/src/pages/admin/table_definition/state.rs @@ -80,6 +80,7 @@ impl TableSummary { #[derive(Clone, Debug)] pub(crate) struct TableDetailView { pub id: i64, + pub row_version: i64, pub row_display_columns: Vec, pub columns: Vec, pub scripts: Vec, @@ -181,6 +182,8 @@ pub(crate) struct PresentationForm { #[serde(default)] pub table: String, #[serde(default)] + pub expected_row_version: i64, + #[serde(default)] pub column_ids: Vec, #[serde(default)] pub aliases: Vec, @@ -387,6 +390,7 @@ mod tests { fn provenance_and_renameability_are_independent() { let detail = TableDetailView { id: 1, + row_version: 1, row_display_columns: Vec::new(), scripts: Vec::new(), columns: vec![ diff --git a/web/src/pages/admin/table_definition/ui.rs b/web/src/pages/admin/table_definition/ui.rs index b2543216..574d77f5 100644 --- a/web/src/pages/admin/table_definition/ui.rs +++ b/web/src/pages/admin/table_definition/ui.rs @@ -237,6 +237,7 @@ mod tests { link_targets: vec![table("invoice", "dynamic"), table("accounts", "system")], detail: Some(TableDetailView { id: 7, + row_version: 1, row_display_columns: vec!["number".to_string()], scripts: Vec::new(), columns: vec![DetailColumn { @@ -377,6 +378,7 @@ mod tests { assert!(html.contains(r#"hx-post="/admin/tables/presentation""#), "{html}"); assert!(html.contains(r#"name="column_ids" value="1""#), "{html}"); + assert!(html.contains(r#"name="expected_row_version" value="1""#), "{html}"); assert!(html.contains(r#"name="aliases" value="number""#), "{html}"); assert!(html.contains(r#"name="action" value="save""#), "{html}"); } diff --git a/web/templates/pages/admin/table_definition/columns_panel.html b/web/templates/pages/admin/table_definition/columns_panel.html index 8c751955..48e64839 100644 --- a/web/templates/pages/admin/table_definition/columns_panel.html +++ b/web/templates/pages/admin/table_definition/columns_panel.html @@ -32,6 +32,7 @@ hx-target="#table-panel" hx-swap="innerHTML"> +
{% for column in detail.columns %}