aliasing now move column position also
This commit is contained in:
2
client
2
client
Submodule client updated: be0c552638...429600a181
@@ -45,8 +45,8 @@ Commit 29ccd8d added `ColumnDefinition.required` end-to-end (definition → vali
|
||||
**10. `account` field is bare.**
|
||||
The server exposes physical `account_id` as API column `account` — TEXT, slash-delimited, required on ACCOUNTING tables, and sending `account_id` directly is rejected (`server/src/tables_data/account_binding.rs`, `table_structure/query.rs:258`). The client removed its old `account_id → accounts` special case (client 827e6a4) and added no `account` handling: the form shows it as a plain TEXT field with no account picker or format validation. The ledger display part was fixed (`ledger.rs` renders `line.account`).
|
||||
|
||||
**11. No alias rename UI.**
|
||||
`rename_column_alias` is defined in the client's gRPC client (`grpc_client.rs:441`) but never called; the server's renameable-alias feature (8881041) is unreachable from the client.
|
||||
**11. Column presentation is managed by the table-definition UI.**
|
||||
`SetColumnPresentation` atomically updates aliases and presentation order from the admin table workspace.
|
||||
|
||||
**12. `CreateInvoiceTemplateTable` not used.**
|
||||
The server added table bundles generated from Typst invoice templates (a5c8e08); the client ships the `typst-template` feature but has no call or screen for it.
|
||||
|
||||
@@ -208,11 +208,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
"#[derive(serde::Serialize, serde::Deserialize)]"
|
||||
)
|
||||
.type_attribute(
|
||||
".komp_ac.table_definition.RenameColumnAliasRequest",
|
||||
".komp_ac.table_definition.ColumnPresentation",
|
||||
"#[derive(serde::Serialize, serde::Deserialize)]"
|
||||
)
|
||||
.type_attribute(
|
||||
".komp_ac.table_definition.RenameColumnAliasResponse",
|
||||
".komp_ac.table_definition.SetColumnPresentationRequest",
|
||||
"#[derive(serde::Serialize, serde::Deserialize)]"
|
||||
)
|
||||
.type_attribute(
|
||||
".komp_ac.table_definition.SetColumnPresentationResponse",
|
||||
"#[derive(serde::Serialize, serde::Deserialize)]"
|
||||
)
|
||||
.type_attribute(
|
||||
|
||||
@@ -45,8 +45,9 @@ service TableDefinition {
|
||||
// Returns the stored rename history for column aliases in one profile.
|
||||
rpc GetColumnAliasRenameHistory(GetColumnAliasRenameHistoryRequest) returns (GetColumnAliasRenameHistoryResponse);
|
||||
|
||||
// Renames a user-visible column alias while keeping the physical column unchanged.
|
||||
rpc RenameColumnAlias(RenameColumnAliasRequest) returns (RenameColumnAliasResponse);
|
||||
// Atomically replaces the aliases and presentation order of a table's columns.
|
||||
// Physical column names and identities remain unchanged.
|
||||
rpc SetColumnPresentation(SetColumnPresentationRequest) returns (SetColumnPresentationResponse);
|
||||
|
||||
// Drops a table and its metadata, then deletes the profile if it becomes empty.
|
||||
rpc DeleteTable(DeleteTableRequest) returns (DeleteTableResponse);
|
||||
@@ -106,12 +107,12 @@ message PostTableDefinitionRequest {
|
||||
// is required to be named after its own type, and what it expands into is the
|
||||
// backend's to decide. The name is only ever a display name over a physical
|
||||
// column, though, so it is free to be anything: this is where that choice is
|
||||
// made, instead of a RenameColumnAlias call afterwards.
|
||||
// made, instead of a SetColumnPresentation call afterwards.
|
||||
//
|
||||
// ACCOUNTING, PHONE and IBAN generated columns may be renamed: their
|
||||
// relationships are recorded by physical column, so the rest of the system can
|
||||
// find them without knowing their display names. ACCOUNTING_TRANSFER connectors
|
||||
// are the exception and are refused here exactly as RenameColumnAlias refuses
|
||||
// are the exception and are refused here exactly as SetColumnPresentation refuses
|
||||
// them.
|
||||
message GeneratedColumnAlias {
|
||||
// The name the backend would otherwise give the column: one of ACCOUNTING's
|
||||
@@ -389,6 +390,9 @@ message ColumnBehavior {
|
||||
|
||||
// True when clients may offer this column in an alias rename picker.
|
||||
bool renameable = 4;
|
||||
|
||||
// Stable public identity used when updating this column's presentation.
|
||||
int64 column_id = 5;
|
||||
}
|
||||
|
||||
// A script that targets a specific column in a table.
|
||||
@@ -400,18 +404,25 @@ message ScriptInfo {
|
||||
string description = 5;
|
||||
}
|
||||
|
||||
// Request to rename one user-visible column alias in a table.
|
||||
message RenameColumnAliasRequest {
|
||||
string profile_name = 1;
|
||||
string table_name = 2;
|
||||
string old_column_name = 3;
|
||||
string new_column_name = 4;
|
||||
// One column's desired public presentation. Its position in the containing
|
||||
// repeated field is its presentation order.
|
||||
message ColumnPresentation {
|
||||
// Stable public identity of the table_definition_columns row.
|
||||
int64 column_id = 1;
|
||||
string alias = 2;
|
||||
}
|
||||
|
||||
// Response after renaming one column alias.
|
||||
message RenameColumnAliasResponse {
|
||||
// Atomically replaces every user-visible column alias and their order.
|
||||
message SetColumnPresentationRequest {
|
||||
string profile_name = 1;
|
||||
string table_name = 2;
|
||||
repeated ColumnPresentation columns = 3;
|
||||
}
|
||||
|
||||
message SetColumnPresentationResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
repeated ColumnPresentation columns = 3;
|
||||
}
|
||||
|
||||
// Request to delete one table definition entirely.
|
||||
|
||||
@@ -87,4 +87,7 @@ message TableColumn {
|
||||
|
||||
// True when clients may offer this column in an alias rename picker.
|
||||
bool renameable = 9;
|
||||
|
||||
// Stable public identity of a managed user column. Zero for system columns.
|
||||
int64 column_id = 10;
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -54,12 +54,12 @@ pub struct PostTableDefinitionRequest {
|
||||
/// is required to be named after its own type, and what it expands into is the
|
||||
/// backend's to decide. The name is only ever a display name over a physical
|
||||
/// column, though, so it is free to be anything: this is where that choice is
|
||||
/// made, instead of a RenameColumnAlias call afterwards.
|
||||
/// made, instead of a SetColumnPresentation call afterwards.
|
||||
///
|
||||
/// ACCOUNTING, PHONE and IBAN generated columns may be renamed: their
|
||||
/// relationships are recorded by physical column, so the rest of the system can
|
||||
/// find them without knowing their display names. ACCOUNTING_TRANSFER connectors
|
||||
/// are the exception and are refused here exactly as RenameColumnAlias refuses
|
||||
/// are the exception and are refused here exactly as SetColumnPresentation refuses
|
||||
/// them.
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
@@ -401,6 +401,9 @@ pub struct ColumnBehavior {
|
||||
/// True when clients may offer this column in an alias rename picker.
|
||||
#[prost(bool, tag = "4")]
|
||||
pub renameable: bool,
|
||||
/// Stable public identity used when updating this column's presentation.
|
||||
#[prost(int64, tag = "5")]
|
||||
pub column_id: i64,
|
||||
}
|
||||
/// A script that targets a specific column in a table.
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
@@ -416,27 +419,37 @@ pub struct ScriptInfo {
|
||||
#[prost(string, tag = "5")]
|
||||
pub description: ::prost::alloc::string::String,
|
||||
}
|
||||
/// Request to rename one user-visible column alias in a table.
|
||||
/// One column's desired public presentation. Its position in the containing
|
||||
/// repeated field is its presentation order.
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct RenameColumnAliasRequest {
|
||||
pub struct ColumnPresentation {
|
||||
/// Stable public identity of the table_definition_columns row.
|
||||
#[prost(int64, tag = "1")]
|
||||
pub column_id: i64,
|
||||
#[prost(string, tag = "2")]
|
||||
pub alias: ::prost::alloc::string::String,
|
||||
}
|
||||
/// Atomically replaces every user-visible column alias and their order.
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct SetColumnPresentationRequest {
|
||||
#[prost(string, tag = "1")]
|
||||
pub profile_name: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "2")]
|
||||
pub table_name: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "3")]
|
||||
pub old_column_name: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "4")]
|
||||
pub new_column_name: ::prost::alloc::string::String,
|
||||
#[prost(message, repeated, tag = "3")]
|
||||
pub columns: ::prost::alloc::vec::Vec<ColumnPresentation>,
|
||||
}
|
||||
/// Response after renaming one column alias.
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct RenameColumnAliasResponse {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct SetColumnPresentationResponse {
|
||||
#[prost(bool, tag = "1")]
|
||||
pub success: bool,
|
||||
#[prost(string, tag = "2")]
|
||||
pub message: ::prost::alloc::string::String,
|
||||
#[prost(message, repeated, tag = "3")]
|
||||
pub columns: ::prost::alloc::vec::Vec<ColumnPresentation>,
|
||||
}
|
||||
/// Request to delete one table definition entirely.
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
@@ -987,12 +1000,13 @@ pub mod table_definition_client {
|
||||
);
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
/// Renames a user-visible column alias while keeping the physical column unchanged.
|
||||
pub async fn rename_column_alias(
|
||||
/// Atomically replaces the aliases and presentation order of a table's columns.
|
||||
/// Physical column names and identities remain unchanged.
|
||||
pub async fn set_column_presentation(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::RenameColumnAliasRequest>,
|
||||
request: impl tonic::IntoRequest<super::SetColumnPresentationRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::RenameColumnAliasResponse>,
|
||||
tonic::Response<super::SetColumnPresentationResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
@@ -1005,14 +1019,14 @@ pub mod table_definition_client {
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/komp_ac.table_definition.TableDefinition/RenameColumnAlias",
|
||||
"/komp_ac.table_definition.TableDefinition/SetColumnPresentation",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"komp_ac.table_definition.TableDefinition",
|
||||
"RenameColumnAlias",
|
||||
"SetColumnPresentation",
|
||||
),
|
||||
);
|
||||
self.inner.unary(req, path, codec).await
|
||||
@@ -1143,12 +1157,13 @@ pub mod table_definition_server {
|
||||
tonic::Response<super::GetColumnAliasRenameHistoryResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
/// Renames a user-visible column alias while keeping the physical column unchanged.
|
||||
async fn rename_column_alias(
|
||||
/// Atomically replaces the aliases and presentation order of a table's columns.
|
||||
/// Physical column names and identities remain unchanged.
|
||||
async fn set_column_presentation(
|
||||
&self,
|
||||
request: tonic::Request<super::RenameColumnAliasRequest>,
|
||||
request: tonic::Request<super::SetColumnPresentationRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::RenameColumnAliasResponse>,
|
||||
tonic::Response<super::SetColumnPresentationResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
/// Drops a table and its metadata, then deletes the profile if it becomes empty.
|
||||
@@ -1670,25 +1685,28 @@ pub mod table_definition_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/komp_ac.table_definition.TableDefinition/RenameColumnAlias" => {
|
||||
"/komp_ac.table_definition.TableDefinition/SetColumnPresentation" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct RenameColumnAliasSvc<T: TableDefinition>(pub Arc<T>);
|
||||
struct SetColumnPresentationSvc<T: TableDefinition>(pub Arc<T>);
|
||||
impl<
|
||||
T: TableDefinition,
|
||||
> tonic::server::UnaryService<super::RenameColumnAliasRequest>
|
||||
for RenameColumnAliasSvc<T> {
|
||||
type Response = super::RenameColumnAliasResponse;
|
||||
> tonic::server::UnaryService<super::SetColumnPresentationRequest>
|
||||
for SetColumnPresentationSvc<T> {
|
||||
type Response = super::SetColumnPresentationResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::RenameColumnAliasRequest>,
|
||||
request: tonic::Request<super::SetColumnPresentationRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as TableDefinition>::rename_column_alias(&inner, request)
|
||||
<T as TableDefinition>::set_column_presentation(
|
||||
&inner,
|
||||
request,
|
||||
)
|
||||
.await
|
||||
};
|
||||
Box::pin(fut)
|
||||
@@ -1700,7 +1718,7 @@ pub mod table_definition_server {
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = RenameColumnAliasSvc(inner);
|
||||
let method = SetColumnPresentationSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
|
||||
@@ -70,6 +70,9 @@ pub struct TableColumn {
|
||||
/// True when clients may offer this column in an alias rename picker.
|
||||
#[prost(bool, tag = "9")]
|
||||
pub renameable: bool,
|
||||
/// Stable public identity of a managed user column. Zero for system columns.
|
||||
#[prost(int64, tag = "10")]
|
||||
pub column_id: i64,
|
||||
}
|
||||
/// Generated client implementations.
|
||||
pub mod table_structure_service_client {
|
||||
|
||||
2
server
2
server
Submodule server updated: 0d96c9e380...481719e3cb
@@ -160,8 +160,8 @@ response is consumed.
|
||||
(table, old/new column names, timestamp); only those four fields are rendered.
|
||||
- **`TableDefinition.AddTableColumns`** — called by
|
||||
`POST /admin/table-definition/columns`. Adds columns to an existing table.
|
||||
- **`TableDefinition.RenameColumnAlias`** — called by
|
||||
`POST /admin/table-definition/rename`. Renames a table column alias.
|
||||
- **`TableDefinition.SetColumnPresentation`** — called by
|
||||
`POST /admin/tables/presentation`. Atomically changes column aliases and order.
|
||||
- **`TableDefinition.DeleteTable`** — called by
|
||||
`POST /admin/table-definition/delete`. Deletes a table definition.
|
||||
- **`TableDefinition.CopyProfile`** — called by
|
||||
|
||||
@@ -323,7 +323,7 @@ mod tests {
|
||||
for path in [
|
||||
"/admin/tables/columns",
|
||||
"/admin/tables/columns/builder",
|
||||
"/admin/tables/rename",
|
||||
"/admin/tables/presentation",
|
||||
"/admin/tables/delete",
|
||||
"/admin/profiles/copy",
|
||||
"/admin/tables/from-template",
|
||||
|
||||
@@ -200,6 +200,7 @@ pub(crate) async fn load_page(
|
||||
.map(|column| {
|
||||
let behavior = table.column_behaviors.get(&column.name);
|
||||
DetailColumn {
|
||||
column_id: behavior.map(|behavior| behavior.column_id).unwrap_or_default(),
|
||||
name: column.name.clone(),
|
||||
sql_type: catalog.sql_type(&column.field_type),
|
||||
field_type: column.field_type.clone(),
|
||||
@@ -279,7 +280,6 @@ pub(crate) async fn load_page(
|
||||
history,
|
||||
selection: inputs.selection,
|
||||
columns: inputs.columns,
|
||||
rename: inputs.rename,
|
||||
copy: inputs.copy,
|
||||
invoice: inputs.invoice,
|
||||
status: inputs.status,
|
||||
|
||||
@@ -23,7 +23,7 @@ use crate::{
|
||||
AppState,
|
||||
definitions::table_definition::{
|
||||
AddTableColumnsRequest, CopyProfileRequest, CreateInvoiceTemplateTableRequest,
|
||||
DeleteTableRequest, RenameColumnAliasRequest,
|
||||
ColumnPresentation, DeleteTableRequest, SetColumnPresentationRequest,
|
||||
},
|
||||
{i18n::Locale, tr},
|
||||
schema::{ColumnForm, proto_columns},
|
||||
@@ -34,7 +34,7 @@ use super::{
|
||||
loader::{self, load_page},
|
||||
state::{
|
||||
CopyForm, DeleteForm, GeneratedTableView, InvoiceTemplateForm, LoadError, PageInputs,
|
||||
RenameForm, Selection, TableDefinitionPageState,
|
||||
PresentationForm, Selection, TableDefinitionPageState,
|
||||
},
|
||||
ui,
|
||||
};
|
||||
@@ -303,11 +303,11 @@ pub(crate) async fn add_columns(
|
||||
}
|
||||
}
|
||||
|
||||
/// POST /admin/tables/rename — RenameColumnAlias.
|
||||
pub(crate) async fn rename_column(
|
||||
/// POST /admin/tables/presentation — SetColumnPresentation.
|
||||
pub(crate) async fn set_column_presentation(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Form(form): Form<RenameForm>,
|
||||
Form(form): Form<PresentationForm>,
|
||||
) -> Response {
|
||||
if let Some(rejection) = reject_cross_site(&headers) {
|
||||
return rejection;
|
||||
@@ -317,9 +317,12 @@ pub(crate) async fn rename_column(
|
||||
profile: form.profile.clone(),
|
||||
table: form.table.clone(),
|
||||
});
|
||||
inputs.rename = form.clone();
|
||||
inputs.presentation = form.clone();
|
||||
|
||||
if form.old_column_name.is_empty() || form.new_column_name.trim().is_empty() {
|
||||
if form.column_ids.is_empty()
|
||||
|| form.column_ids.len() != form.aliases.len()
|
||||
|| form.aliases.iter().any(|alias| alias.trim().is_empty())
|
||||
{
|
||||
let message = tr!(
|
||||
Locale::from_headers(&headers),
|
||||
"td-err-choose-rename"
|
||||
@@ -334,21 +337,42 @@ pub(crate) async fn rename_column(
|
||||
.await;
|
||||
}
|
||||
|
||||
let request = RenameColumnAliasRequest {
|
||||
let mut columns = form
|
||||
.column_ids
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(form.aliases.iter())
|
||||
.map(|(column_id, alias)| ColumnPresentation {
|
||||
column_id,
|
||||
alias: alias.trim().to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if let Some((direction, index)) = form.action.split_once(':') {
|
||||
if let Ok(index) = index.parse::<usize>() {
|
||||
let other = match direction {
|
||||
"up" => index.checked_sub(1),
|
||||
"down" if index + 1 < columns.len() => Some(index + 1),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(other) = other {
|
||||
columns.swap(index, other);
|
||||
}
|
||||
}
|
||||
}
|
||||
let request = SetColumnPresentationRequest {
|
||||
profile_name: form.profile.clone(),
|
||||
table_name: form.table.clone(),
|
||||
old_column_name: form.old_column_name.clone(),
|
||||
new_column_name: form.new_column_name.trim().to_string(),
|
||||
columns,
|
||||
};
|
||||
let Ok(request) = authenticated_request(&headers, request) else {
|
||||
return Redirect::to("/login").into_response();
|
||||
};
|
||||
|
||||
let mut definitions = state.definitions.clone();
|
||||
match definitions.rename_column_alias(request).await {
|
||||
match definitions.set_column_presentation(request).await {
|
||||
Ok(response) if response.get_ref().success => {
|
||||
inputs.status = Some(response.into_inner().message);
|
||||
inputs.rename = RenameForm {
|
||||
inputs.presentation = PresentationForm {
|
||||
profile: form.profile,
|
||||
table: form.table,
|
||||
..Default::default()
|
||||
|
||||
@@ -36,7 +36,7 @@ pub(crate) fn router() -> Router<AppState> {
|
||||
"/admin/tables/columns/builder",
|
||||
post(logic::update_columns),
|
||||
)
|
||||
.route("/admin/tables/rename", post(logic::rename_column))
|
||||
.route("/admin/tables/presentation", post(logic::set_column_presentation))
|
||||
.route("/admin/tables/delete", get(logic::delete_page))
|
||||
.route("/admin/tables/delete", post(logic::delete_table))
|
||||
// Profile-scoped.
|
||||
|
||||
@@ -89,6 +89,7 @@ impl TableDetailView {
|
||||
/// Columns a rename may target. Provenance and renameability are separate:
|
||||
/// accounting companions remain renameable while protected generated
|
||||
/// columns do not.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn renameable_columns(&self) -> Vec<&DetailColumn> {
|
||||
self.columns
|
||||
.iter()
|
||||
@@ -99,6 +100,7 @@ impl TableDetailView {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct DetailColumn {
|
||||
pub column_id: i64,
|
||||
pub name: String,
|
||||
pub field_type: String,
|
||||
/// The PostgreSQL type the column is stored as, from the column-type
|
||||
@@ -173,15 +175,17 @@ pub(crate) struct GeneratedTableView {
|
||||
/// The rename panel's inputs, kept across a failed submit so the user does not
|
||||
/// retype them.
|
||||
#[derive(Clone, Debug, Default, serde::Deserialize)]
|
||||
pub(crate) struct RenameForm {
|
||||
pub(crate) struct PresentationForm {
|
||||
#[serde(default)]
|
||||
pub profile: String,
|
||||
#[serde(default)]
|
||||
pub table: String,
|
||||
#[serde(default)]
|
||||
pub old_column_name: String,
|
||||
pub column_ids: Vec<i64>,
|
||||
#[serde(default)]
|
||||
pub new_column_name: String,
|
||||
pub aliases: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub action: String,
|
||||
}
|
||||
|
||||
/// The copy-profile panel. An empty `table_names` copies the whole profile,
|
||||
@@ -241,7 +245,7 @@ pub(crate) struct PageInputs {
|
||||
pub selection: Selection,
|
||||
/// The columns staged for `AddTableColumns`.
|
||||
pub columns: ColumnDraft,
|
||||
pub rename: RenameForm,
|
||||
pub presentation: PresentationForm,
|
||||
pub copy: CopyForm,
|
||||
pub invoice: InvoiceTemplateForm,
|
||||
pub status: Option<String>,
|
||||
@@ -278,7 +282,6 @@ pub(crate) struct TableDefinitionPageState {
|
||||
pub detail: Option<TableDetailView>,
|
||||
pub history: Vec<RenameEntry>,
|
||||
pub columns: ColumnDraft,
|
||||
pub rename: RenameForm,
|
||||
pub copy: CopyForm,
|
||||
pub invoice: InvoiceTemplateForm,
|
||||
pub status: Option<String>,
|
||||
@@ -388,6 +391,7 @@ mod tests {
|
||||
scripts: Vec::new(),
|
||||
columns: vec![
|
||||
DetailColumn {
|
||||
column_id: 1,
|
||||
name: "work_phone".to_string(),
|
||||
field_type: "phone".to_string(),
|
||||
sql_type: "TEXT".to_string(),
|
||||
@@ -400,6 +404,7 @@ mod tests {
|
||||
renameable: true,
|
||||
},
|
||||
DetailColumn {
|
||||
column_id: 2,
|
||||
name: "work_phone_country".to_string(),
|
||||
field_type: "phone_country".to_string(),
|
||||
sql_type: "TEXT".to_string(),
|
||||
@@ -412,6 +417,7 @@ mod tests {
|
||||
renameable: false,
|
||||
},
|
||||
DetailColumn {
|
||||
column_id: 3,
|
||||
name: "charge".to_string(),
|
||||
field_type: "money".to_string(),
|
||||
sql_type: "NUMERIC".to_string(),
|
||||
|
||||
@@ -211,7 +211,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
pages::admin::table_definition::state::{
|
||||
CopyForm, DetailColumn, InvoiceTemplateForm, RenameForm, Selection, TableDetailView,
|
||||
CopyForm, DetailColumn, InvoiceTemplateForm, Selection, TableDetailView,
|
||||
TableSummary,
|
||||
},
|
||||
schema::ColumnDraft,
|
||||
@@ -240,6 +240,7 @@ mod tests {
|
||||
row_display_columns: vec!["number".to_string()],
|
||||
scripts: Vec::new(),
|
||||
columns: vec![DetailColumn {
|
||||
column_id: 1,
|
||||
name: "number".to_string(),
|
||||
field_type: "text".to_string(),
|
||||
sql_type: "TEXT".to_string(),
|
||||
@@ -254,7 +255,6 @@ mod tests {
|
||||
}),
|
||||
history: Vec::new(),
|
||||
columns: ColumnDraft::for_append(crate::schema::tests::catalog()),
|
||||
rename: RenameForm::default(),
|
||||
copy: CopyForm::default(),
|
||||
invoice: InvoiceTemplateForm::default(),
|
||||
status: None,
|
||||
@@ -307,7 +307,7 @@ mod tests {
|
||||
assert!(html.contains("/admin/tables/delete"));
|
||||
assert!(html.contains("Type <code>invoice</code> to confirm"));
|
||||
// The other writes are links in the switcher, not forms on the page.
|
||||
assert!(!html.contains("/admin/tables/rename"));
|
||||
assert!(!html.contains("/admin/tables/presentation"));
|
||||
assert!(!html.contains("/admin/profiles/copy?profile=billing\" method"));
|
||||
}
|
||||
|
||||
@@ -368,7 +368,17 @@ mod tests {
|
||||
assert!(!html.contains(r#"name="confirm_table_name""#));
|
||||
|
||||
let html = render_columns_page(&state);
|
||||
assert!(!html.contains("/admin/tables/rename"));
|
||||
assert!(!html.contains("/admin/tables/presentation"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_presentation_posts_stable_ids_aliases_and_order_controls() {
|
||||
let html = render_columns_page(&page());
|
||||
|
||||
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="aliases" value="number""#), "{html}");
|
||||
assert!(html.contains(r#"name="action" value="save""#), "{html}");
|
||||
}
|
||||
|
||||
/// The profile-wide pages need only a profile, and say so by still
|
||||
|
||||
@@ -28,25 +28,24 @@
|
||||
<section class="panel">
|
||||
<h2>{{ nav.tr("td-rename-column") }}</h2>
|
||||
<p class="hint">{{ nav.tr("td-rename-hint") }}</p>
|
||||
<form hx-post="/admin/tables/rename"
|
||||
<form hx-post="/admin/tables/presentation"
|
||||
hx-target="#table-panel" hx-swap="innerHTML">
|
||||
<input type="hidden" name="profile" value="{{ page.selection.profile }}">
|
||||
<input type="hidden" name="table" value="{{ page.selection.table }}">
|
||||
<div class="form-grid">
|
||||
{% for column in detail.columns %}
|
||||
<input type="hidden" name="column_ids" value="{{ column.column_id }}">
|
||||
<label>{{ nav.tr("td-column-label") }}
|
||||
<select name="old_column_name">
|
||||
<option value="">{{ nav.tr("td-choose-column") }}</option>
|
||||
{% for column in detail.renameable_columns() %}
|
||||
<option value="{{ column.name }}" {% if page.rename.old_column_name == column.name %}selected{% endif %}>{{ column.name }}</option>
|
||||
<input name="aliases" value="{{ column.name }}" {% if !column.renameable %}readonly{% endif %}>
|
||||
</label>
|
||||
<div class="form-actions">
|
||||
<button type="submit" name="action" value="up:{{ loop.index0 }}" {% if loop.first %}disabled{% endif %}>↑</button>
|
||||
<button type="submit" name="action" value="down:{{ loop.index0 }}" {% if loop.last %}disabled{% endif %}>↓</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>{{ nav.tr("td-new-name") }}
|
||||
<input name="new_column_name" value="{{ page.rename.new_column_name }}" placeholder="invoice_number">
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit">{{ nav.tr("td-rename-button") }}</button>
|
||||
<button type="submit" name="action" value="save">{{ nav.tr("td-rename-button") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user