diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index 31a9fd16..73247c71 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -224,6 +224,10 @@ message PutTableDefinitionRequest { // Revision returned by GetProfileDetails. Prevents a stale editor from // overwriting a newer structural or presentation change. int64 expected_row_version = 7; + + // New physical/catalog name. Empty, or equal to table_name, keeps the + // current name for backwards-compatible callers. + string new_table_name = 8; } message PutTableDefinitionResponse { diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index a71ec3ae..3f754625 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 7a498dba..e8163110 100644 --- a/common/src/proto/komp_ac.table_definition.rs +++ b/common/src/proto/komp_ac.table_definition.rs @@ -188,6 +188,10 @@ pub struct PutTableDefinitionRequest { /// overwriting a newer structural or presentation change. #[prost(int64, tag = "7")] pub expected_row_version: i64, + /// New physical/catalog name. Empty, or equal to table_name, keeps the + /// current name for backwards-compatible callers. + #[prost(string, tag = "8")] + pub new_table_name: ::prost::alloc::string::String, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct PutTableDefinitionResponse { diff --git a/server b/server index e4540055..0fd5a16b 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit e454005545d3722d72b50f3a160ae345c44777b9 +Subproject commit 0fd5a16ba6ac0671daabfa5fcbf34bd24c12d80c diff --git a/web/CHANGELOG.md b/web/CHANGELOG.md index b47a30c2..5bb4dd2e 100644 --- a/web/CHANGELOG.md +++ b/web/CHANGELOG.md @@ -22,7 +22,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). the same ISO-4217 autocomplete options as the accounting currency. - **Editable empty-table definitions** — the table-definition column page now calls `PutTableDefinition` to atomically remove selected columns and append - replacements, with stable column identities and optimistic revision checks. + replacements or rename the table, with stable column identities and + optimistic revision checks. It is clearly labelled “Adjust table” only while the table is empty; after data is posted, the page switches to append-only “Add columns” mode. - **The shared profile is named by the backend** — `ProfileTreeResponse` diff --git a/web/locales/en/main.ftl b/web/locales/en/main.ftl index 17288d01..cf387873 100644 --- a/web/locales/en/main.ftl +++ b/web/locales/en/main.ftl @@ -169,6 +169,8 @@ td-add-columns-to = Add columns to td-append-hint = Columns are appended. Nothing that already exists is changed, and the new columns can be indexed as they are added. td-adjust-table-of = Adjust table td-adjust-table-hint = This table has no data, so you can add new columns or select existing columns to remove. The change is saved atomically. +td-table-name = Table name +td-table-name-hint = Rename the table here. Links and its definition continue to point to the same table. td-remove-column = Remove td-remove-column-aria = Remove { $column } td-remove-column-confirm = Remove column "{ $column }"? This can't be undone once you save. @@ -330,7 +332,7 @@ td-err-select-table-first = Select a table first. td-err-profile-gone = Profile `{ $missing }` no longer exists. td-err-not-a-table = `{ $missing }` is not a table of { $scope }. td-err-describe-column = Describe at least one column before adding. -td-err-describe-change = Select a column to remove or stage at least one column to add. +td-err-describe-change = Change the table name, select a column to remove, or stage at least one column to add. td-err-backend-no-columns = The backend did not add the columns. td-err-backend-no-adjustment = The backend did not adjust the table definition. td-err-populated-append-only = This table contains data. Existing columns cannot be removed; only new columns can be added. diff --git a/web/src/pages/admin/table_definition/loader.rs b/web/src/pages/admin/table_definition/loader.rs index d501d205..81debd98 100644 --- a/web/src/pages/admin/table_definition/loader.rs +++ b/web/src/pages/admin/table_definition/loader.rs @@ -279,7 +279,9 @@ pub(crate) async fn load_page( || tables .iter() .any(|table| table.name == inputs.selection.table && table.global); - inputs.columns.table_name = inputs.selection.table.clone(); + if inputs.columns.table_name.is_empty() { + inputs.columns.table_name = inputs.selection.table.clone(); + } Ok(TableDefinitionPageState { nav: crate::ui::Nav::from_authorization(headers, "admin", &authorization), diff --git a/web/src/pages/admin/table_definition/logic.rs b/web/src/pages/admin/table_definition/logic.rs index b8c8eb2f..ef3a33c3 100644 --- a/web/src/pages/admin/table_definition/logic.rs +++ b/web/src/pages/admin/table_definition/logic.rs @@ -27,7 +27,7 @@ use crate::{ PutTableDefinitionRequest, }, {i18n::Locale, tr}, - schema::{ColumnForm, proto_columns}, + schema::{ColumnForm, proto_columns, validate_table_name}, services::{authenticated_request, reject_cross_site}, }; @@ -189,6 +189,7 @@ pub(crate) async fn update_columns( let mut inputs = PageInputs::for_selection(selection); inputs.columns = form.to_draft(catalog, false); + inputs.columns.table_name = form.table_name_input.trim().to_string(); let index = form.index.unwrap_or(0); let locale = Locale::from_headers(&headers); @@ -239,6 +240,7 @@ pub(crate) async fn add_columns( let mut inputs = PageInputs::for_selection(selection); inputs.columns = form.to_draft(catalog.clone(), false); + inputs.columns.table_name = form.table_name_input.trim().to_string(); inputs.remove_column_ids = form.remove_column_ids.clone(); if !inputs.selection.has_table() { @@ -255,7 +257,21 @@ pub(crate) async fn add_columns( ) .await; } - if inputs.columns.is_empty() && inputs.remove_column_ids.is_empty() { + let requested_table_name = if inputs.columns.table_name.is_empty() { + inputs.selection.table.clone() + } else { + inputs.columns.table_name.clone() + }; + let renaming = requested_table_name != inputs.selection.table; + if renaming + && let Some(message) = validate_table_name( + Locale::from_headers(&headers), + &requested_table_name, + ) + { + return refuse(state, headers, inputs, Page::AddColumns, message).await; + } + if inputs.columns.is_empty() && inputs.remove_column_ids.is_empty() && !renaming { let message = tr!( Locale::from_headers(&headers), "td-err-describe-change" @@ -314,6 +330,7 @@ pub(crate) async fn add_columns( add_indexes: inputs.columns.selected_index_names(), generated_aliases: Vec::new(), expected_row_version: form.expected_row_version, + new_table_name: requested_table_name.clone(), }; let Ok(request) = authenticated_request(&headers, request) else { return Redirect::to("/login").into_response(); @@ -330,9 +347,11 @@ pub(crate) async fn add_columns( "td-definition-adjusted", "added" => added as i64, "removed" => removed as i64, - "table" => inputs.selection.table.clone(), + "table" => requested_table_name.clone(), )); + inputs.selection.table = requested_table_name.clone(); inputs.columns = crate::schema::ColumnDraft::for_append(catalog); + inputs.columns.table_name = requested_table_name; inputs.remove_column_ids.clear(); respond(state, headers, inputs, Page::AddColumns, StatusCode::OK).await } diff --git a/web/src/pages/admin/table_definition/ui.rs b/web/src/pages/admin/table_definition/ui.rs index 9706321a..274ebd53 100644 --- a/web/src/pages/admin/table_definition/ui.rs +++ b/web/src/pages/admin/table_definition/ui.rs @@ -456,6 +456,7 @@ mod tests { let add_html = render_add_columns_page(&page()); assert!(add_html.contains(r#"id="column-form""#), "{add_html}"); assert!(add_html.contains(r#"name="expected_row_version" value="1""#), "{add_html}"); + assert!(add_html.contains(r#"name="table_name_input" value="invoice""#), "{add_html}"); assert!(add_html.contains(r#"name="remove_column_ids" value="1""#), "{add_html}"); assert!(add_html.contains("Adjust table"), "{add_html}"); assert!(!add_html.contains(r#"name="alias""#), "{add_html}"); diff --git a/web/src/schema/mod.rs b/web/src/schema/mod.rs index b9a1966d..decde2cd 100644 --- a/web/src/schema/mod.rs +++ b/web/src/schema/mod.rs @@ -1422,6 +1422,10 @@ pub(crate) struct ColumnForm { pub remove_column_ids: Vec, #[serde(default)] pub expected_row_version: i64, + /// The requested name of an existing empty table. The append-only form + /// does not render it, so populated tables decode this as empty. + #[serde(default)] + pub table_name_input: String, #[serde(default)] pub column_name_input: String, diff --git a/web/templates/pages/admin/table_definition/add_columns_panel.html b/web/templates/pages/admin/table_definition/add_columns_panel.html index 6ae70741..2c4a7fe9 100644 --- a/web/templates/pages/admin/table_definition/add_columns_panel.html +++ b/web/templates/pages/admin/table_definition/add_columns_panel.html @@ -10,6 +10,13 @@ {% if page.can_adjust_definition() %}

{{ nav.tr("td-adjust-table-of") }} {{ page.selection.table }}

{{ nav.tr("td-adjust-table-hint") }}

+
+ +
{% else %}

{{ nav.tr("td-add-columns-to") }} {{ page.selection.table }}

{{ nav.tr("td-append-hint") }}