diff --git a/client b/client index 7eaf34d0..2e4b1589 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit 7eaf34d09e49bb0f30225368e20c63fb18c049bf +Subproject commit 2e4b158935ea31e1cf0c8504b6293d36ec703edb diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index 1e7aa49b..67946424 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -58,6 +58,11 @@ service TableDefinition { // Physical column names and identities remain unchanged. rpc SetColumnPresentation(SetColumnPresentationRequest) returns (SetColumnPresentationResponse); + // Shows or hides one column in bundled data-entry forms. This is presentation + // metadata only: hidden columns remain available to data APIs, scripts and + // backend calculations. + rpc SetColumnFormVisibility(SetColumnFormVisibilityRequest) returns (SetColumnFormVisibilityResponse); + // Drops a table and its metadata, then deletes the profile if it becomes empty. rpc DeleteTable(DeleteTableRequest) returns (DeleteTableResponse); } @@ -471,6 +476,10 @@ message ColumnBehavior { // Stable public identity used when updating this column's presentation. int64 column_id = 5; + + // True when bundled data-entry clients should not render this column. + // This is not an authorization boundary; data APIs still expose the column. + bool hidden_from_forms = 6; } // A script that targets a specific column in a table. @@ -507,6 +516,24 @@ message SetColumnPresentationResponse { int64 row_version = 4; } +message SetColumnFormVisibilityRequest { + string profile_name = 1; + string table_name = 2; + int64 column_id = 3; + bool hidden_from_forms = 4; + // Row version returned by GetProfileDetails when the column was read. + int64 expected_row_version = 5; +} + +message SetColumnFormVisibilityResponse { + bool success = 1; + string message = 2; + int64 column_id = 3; + bool hidden_from_forms = 4; + // New row version after the update. + int64 row_version = 5; +} + // Request to delete one table definition entirely. message DeleteTableRequest { // Profile (schema) name owning the table (must exist). diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index c073db77..8e339ff0 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 3ab28d34..27cd74db 100644 --- a/common/src/proto/komp_ac.table_definition.rs +++ b/common/src/proto/komp_ac.table_definition.rs @@ -495,6 +495,10 @@ pub struct ColumnBehavior { /// Stable public identity used when updating this column's presentation. #[prost(int64, tag = "5")] pub column_id: i64, + /// True when bundled data-entry clients should not render this column. + /// This is not an authorization boundary; data APIs still expose the column. + #[prost(bool, tag = "6")] + pub hidden_from_forms: bool, } /// A script that targets a specific column in a table. #[derive(serde::Serialize, serde::Deserialize)] @@ -549,6 +553,36 @@ pub struct SetColumnPresentationResponse { #[prost(int64, tag = "4")] pub row_version: i64, } +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct SetColumnFormVisibilityRequest { + #[prost(string, tag = "1")] + pub profile_name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub table_name: ::prost::alloc::string::String, + #[prost(int64, tag = "3")] + pub column_id: i64, + #[prost(bool, tag = "4")] + pub hidden_from_forms: bool, + /// Row version returned by GetProfileDetails when the column was read. + #[prost(int64, tag = "5")] + pub expected_row_version: i64, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct SetColumnFormVisibilityResponse { + #[prost(bool, tag = "1")] + pub success: bool, + #[prost(string, tag = "2")] + pub message: ::prost::alloc::string::String, + #[prost(int64, tag = "3")] + pub column_id: i64, + #[prost(bool, tag = "4")] + pub hidden_from_forms: bool, + /// New row version after the update. + #[prost(int64, tag = "5")] + pub row_version: i64, +} /// Request to delete one table definition entirely. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] @@ -1227,6 +1261,38 @@ pub mod table_definition_client { ); self.inner.unary(req, path, codec).await } + /// Shows or hides one column in bundled data-entry forms. This is presentation + /// metadata only: hidden columns remain available to data APIs, scripts and + /// backend calculations. + pub async fn set_column_form_visibility( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.table_definition.TableDefinition/SetColumnFormVisibility", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.table_definition.TableDefinition", + "SetColumnFormVisibility", + ), + ); + self.inner.unary(req, path, codec).await + } /// Drops a table and its metadata, then deletes the profile if it becomes empty. pub async fn delete_table( &mut self, @@ -1381,6 +1447,16 @@ pub mod table_definition_server { tonic::Response, tonic::Status, >; + /// Shows or hides one column in bundled data-entry forms. This is presentation + /// metadata only: hidden columns remain available to data APIs, scripts and + /// backend calculations. + async fn set_column_form_visibility( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; /// Drops a table and its metadata, then deletes the profile if it becomes empty. async fn delete_table( &self, @@ -2052,6 +2128,57 @@ pub mod table_definition_server { }; Box::pin(fut) } + "/komp_ac.table_definition.TableDefinition/SetColumnFormVisibility" => { + #[allow(non_camel_case_types)] + struct SetColumnFormVisibilitySvc(pub Arc); + impl< + T: TableDefinition, + > tonic::server::UnaryService + for SetColumnFormVisibilitySvc { + type Response = super::SetColumnFormVisibilityResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::SetColumnFormVisibilityRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::set_column_form_visibility( + &inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = SetColumnFormVisibilitySvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } "/komp_ac.table_definition.TableDefinition/DeleteTable" => { #[allow(non_camel_case_types)] struct DeleteTableSvc(pub Arc); diff --git a/server b/server index 2e9522fe..9d960ff7 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 2e9522fe257344df11a9968d572e57258a2a355f +Subproject commit 9d960ff719c29b92a4fb43a1a19747d883d0659d diff --git a/web/locales/cs/main.ftl b/web/locales/cs/main.ftl index bb99cd90..47554207 100644 --- a/web/locales/cs/main.ftl +++ b/web/locales/cs/main.ftl @@ -59,6 +59,7 @@ column-flag-half-up = zaokrouhleno nahoru column-flag-read-only = pouze pro čtení column-flag-generated = generovaný column-flag-generated-from = generováno z { $source } +column-flag-hidden-from-forms = skrytý ve formulářích # --- Přihlášení / registrace / změna hesla ----------------------------------- login-title = Přihlášení @@ -189,6 +190,9 @@ td-columns-now = Současné sloupce td-col-column = Sloupec td-col-type = Typ td-col-notes = Poznámky +td-form-visibility = Viditelnost ve formuláři +td-hide-from-forms = Skrýt +td-show-in-forms = Zobrazit td-scripts = Skripty td-col-target-column = Cílový sloupec td-col-description = Popis diff --git a/web/locales/en/main.ftl b/web/locales/en/main.ftl index 8757ba69..b7f90f32 100644 --- a/web/locales/en/main.ftl +++ b/web/locales/en/main.ftl @@ -64,6 +64,7 @@ column-flag-half-up = half-up column-flag-read-only = read only column-flag-generated = generated column-flag-generated-from = generated from { $source } +column-flag-hidden-from-forms = hidden from forms # --- Login / register / change password ------------------------------------ login-title = Sign in @@ -193,6 +194,9 @@ td-columns-now = Columns it has now td-col-column = Column td-col-type = Type td-col-notes = Notes +td-form-visibility = Form visibility +td-hide-from-forms = Hide +td-show-in-forms = Show td-scripts = Scripts td-col-target-column = Target column td-col-description = Description diff --git a/web/locales/sk/main.ftl b/web/locales/sk/main.ftl index 261162a3..eec10768 100644 --- a/web/locales/sk/main.ftl +++ b/web/locales/sk/main.ftl @@ -59,6 +59,7 @@ column-flag-half-up = zaokrúhlené nahor column-flag-read-only = len na čítanie column-flag-generated = generovaný column-flag-generated-from = generovaný z { $source } +column-flag-hidden-from-forms = skrytý vo formulároch # --- Prihlásenie / registrácia / zmena hesla ------------------------------- login-title = Prihlásenie @@ -189,6 +190,9 @@ td-columns-now = Súčasné stĺpce td-col-column = Stĺpec td-col-type = Typ td-col-notes = Poznámky +td-form-visibility = Viditeľnosť vo formulári +td-hide-from-forms = Skryť +td-show-in-forms = Zobraziť td-scripts = Skripty td-col-target-column = Cieľový stĺpec td-col-description = Popis diff --git a/web/src/lib.rs b/web/src/lib.rs index 7f97f68b..7687e797 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -344,6 +344,7 @@ mod tests { "/admin/tables/columns/add/builder", "/admin/tables/presentation/alias", "/admin/tables/presentation/order", + "/admin/tables/presentation/visibility", "/admin/tables/delete", "/admin/profiles/copy", "/admin/tables/from-template", @@ -481,6 +482,10 @@ mod tests { "/admin/tables/presentation/order", "profile=billing&table=invoice&column_ids=1&column_ids=2", ), + ( + "/admin/tables/presentation/visibility", + "profile=billing&table=invoice&column_id=1&hidden_from_forms=true", + ), ("/admin/tables/columns/add", "profile=billing&table=invoice"), ("/admin/tables/builder", ""), ("/admin/tables", ""), diff --git a/web/src/pages/admin/table_definition/loader.rs b/web/src/pages/admin/table_definition/loader.rs index 81debd98..33ebacc4 100644 --- a/web/src/pages/admin/table_definition/loader.rs +++ b/web/src/pages/admin/table_definition/loader.rs @@ -225,6 +225,8 @@ pub(crate) async fn load_page( // incomplete server capability response does // not grant permission by omission. renameable: behavior.is_some_and(|behavior| behavior.renameable), + hidden_from_forms: behavior + .is_some_and(|behavior| behavior.hidden_from_forms), } }) .collect(), diff --git a/web/src/pages/admin/table_definition/logic.rs b/web/src/pages/admin/table_definition/logic.rs index 888b4e1b..4c5746ef 100644 --- a/web/src/pages/admin/table_definition/logic.rs +++ b/web/src/pages/admin/table_definition/logic.rs @@ -23,8 +23,8 @@ use crate::{ AppState, definitions::table_definition::{ AddTableColumnsRequest, CopyProfileRequest, CreateInvoiceTemplateTableRequest, - ColumnPresentation, DeleteTableRequest, SetColumnPresentationRequest, - PutTableDefinitionRequest, + ColumnPresentation, DeleteTableRequest, PutTableDefinitionRequest, + SetColumnFormVisibilityRequest, SetColumnPresentationRequest, }, {i18n::Locale, tr}, schema::{ColumnForm, proto_columns, validate_column_alias, validate_table_name}, @@ -35,7 +35,7 @@ use super::{ loader::{self, load_page}, state::{ AliasForm, CopyForm, DeleteForm, DetailColumn, GeneratedTableView, InvoiceTemplateForm, - LoadError, OrderForm, PageInputs, Selection, TableDefinitionPageState, + LoadError, OrderForm, PageInputs, Selection, TableDefinitionPageState, VisibilityForm, }, ui, }; @@ -551,6 +551,49 @@ pub(crate) async fn set_column_order( .await } +/// POST /admin/tables/presentation/visibility — show or hide one column in +/// bundled data-entry forms without changing its data-plane behavior. +pub(crate) async fn set_column_visibility( + State(state): State, + headers: HeaderMap, + Form(form): Form, +) -> Response { + if let Some(rejection) = reject_cross_site(&headers) { + return rejection; + } + + let inputs = PageInputs::for_selection(Selection { + profile: form.profile.clone(), + table: form.table.clone(), + }); + let request = SetColumnFormVisibilityRequest { + profile_name: form.profile, + table_name: form.table, + column_id: form.column_id, + hidden_from_forms: form.hidden_from_forms, + expected_row_version: form.expected_row_version, + }; + let Ok(request) = authenticated_request(&headers, request) else { + return Redirect::to("/login").into_response(); + }; + + let mut definitions = state.definitions.clone(); + match definitions.set_column_form_visibility(request).await { + Ok(response) if response.get_ref().success => { + let mut inputs = inputs; + inputs.status = Some(response.into_inner().message); + respond(state, headers, inputs, Page::Presentation, StatusCode::OK).await + } + Ok(response) => { + let message = response.into_inner().message; + refuse(state, headers, inputs, Page::Presentation, message).await + } + Err(error) => { + refuse(state, headers, inputs, Page::Presentation, error.message().to_string()).await + } + } +} + /// The table's columns as the backend has them now, in their current order. /// /// Both presentation writes have to send every column, and the ones they are diff --git a/web/src/pages/admin/table_definition/mod.rs b/web/src/pages/admin/table_definition/mod.rs index 118320e4..0e1fd2b8 100644 --- a/web/src/pages/admin/table_definition/mod.rs +++ b/web/src/pages/admin/table_definition/mod.rs @@ -50,6 +50,10 @@ pub(crate) fn router() -> Router { "/admin/tables/presentation/order", post(logic::set_column_order), ) + .route( + "/admin/tables/presentation/visibility", + post(logic::set_column_visibility), + ) .route("/admin/tables/delete", get(logic::delete_page)) .route("/admin/tables/delete", post(logic::delete_table)) // Profile-scoped. diff --git a/web/src/pages/admin/table_definition/state.rs b/web/src/pages/admin/table_definition/state.rs index 2734597f..d6de9cca 100644 --- a/web/src/pages/admin/table_definition/state.rs +++ b/web/src/pages/admin/table_definition/state.rs @@ -159,6 +159,7 @@ pub(crate) struct DetailColumn { pub read_only: bool, pub generated_from: String, pub renameable: bool, + pub hidden_from_forms: bool, } impl DetailColumn { @@ -186,6 +187,9 @@ impl DetailColumn { if self.read_only { flags.push(crate::tr!(*locale, "column-flag-read-only")); } + if self.hidden_from_forms { + flags.push(crate::tr!(*locale, "column-flag-hidden-from-forms")); + } if !self.generated_from.is_empty() { flags.push(crate::tr!( *locale, @@ -266,6 +270,20 @@ pub(crate) struct OrderForm { pub column_ids: Vec, } +#[derive(Clone, Debug, Default, serde::Deserialize)] +pub(crate) struct VisibilityForm { + #[serde(default)] + pub profile: String, + #[serde(default)] + pub table: String, + #[serde(default)] + pub expected_row_version: i64, + #[serde(default)] + pub column_id: i64, + #[serde(default)] + pub hidden_from_forms: bool, +} + /// The copy-profile panel. An empty `table_names` copies the whole profile, /// which is what the backend takes an empty list to mean. #[derive(Clone, Debug, Default, serde::Deserialize)] @@ -515,6 +533,7 @@ mod tests { read_only: false, generated_from: String::new(), renameable: true, + hidden_from_forms: false, }, DetailColumn { column_id: 2, @@ -528,6 +547,7 @@ mod tests { read_only: true, generated_from: "work_phone".to_string(), renameable: false, + hidden_from_forms: false, }, DetailColumn { column_id: 3, @@ -541,6 +561,7 @@ mod tests { read_only: false, generated_from: "accounting".to_string(), renameable: true, + hidden_from_forms: false, }, ], }; diff --git a/web/src/pages/admin/table_definition/ui.rs b/web/src/pages/admin/table_definition/ui.rs index 274ebd53..b970a29d 100644 --- a/web/src/pages/admin/table_definition/ui.rs +++ b/web/src/pages/admin/table_definition/ui.rs @@ -288,6 +288,7 @@ mod tests { read_only: false, generated_from: String::new(), renameable: true, + hidden_from_forms: false, }], }), history: Vec::new(), diff --git a/web/templates/pages/admin/table_definition/presentation_panel.html b/web/templates/pages/admin/table_definition/presentation_panel.html index f69e48fc..a2b82a69 100644 --- a/web/templates/pages/admin/table_definition/presentation_panel.html +++ b/web/templates/pages/admin/table_definition/presentation_panel.html @@ -82,7 +82,7 @@

{{ nav.tr("td-columns-now") }} {{ detail.columns.len() }}

- + {% for column in detail.columns %} @@ -91,6 +91,21 @@ + {% endfor %}
{{ nav.tr("td-col-column") }}{{ nav.tr("td-col-type") }}{{ nav.tr("td-col-notes") }}
{{ nav.tr("td-col-column") }}{{ nav.tr("td-col-type") }}{{ nav.tr("td-col-notes") }}{{ nav.tr("td-form-visibility") }}
{%- for flag in column.flags(nav.locale) %}{{ flag }}{% endfor -%} + {% if page.table_is_writable() %} +
+ + + + + + +
+ {% endif %} +