This commit is contained in:
Priec
2026-08-15 18:31:51 +02:00
parent 56baa35cf2
commit 10d9784b9b
14 changed files with 404 additions and 111 deletions

View File

@@ -33,8 +33,8 @@ use crate::{
use super::{
loader::{self, load_page},
state::{
CopyForm, DeleteForm, GeneratedTableView, InvoiceTemplateForm, LoadError, PageInputs,
PresentationForm, Selection, TableDefinitionPageState,
AliasForm, CopyForm, DeleteForm, DetailColumn, GeneratedTableView, InvoiceTemplateForm,
LoadError, OrderForm, PageInputs, Selection, TableDefinitionPageState,
},
ui,
};
@@ -316,67 +316,164 @@ pub(crate) async fn add_columns(
}
}
/// POST /admin/tables/presentation — SetColumnPresentation.
pub(crate) async fn set_column_presentation(
/// POST /admin/tables/presentation/alias — SetColumnPresentation, renaming one
/// column.
///
/// The request the backend wants is the whole table, so the columns this write
/// is not about are filled in from a fresh read rather than from the browser.
/// That is the point of the split: the only thing the form contributes is the
/// alias and the id of the column it was typed into.
pub(crate) async fn set_column_alias(
State(state): State<AppState>,
headers: HeaderMap,
Form(form): Form<PresentationForm>,
Form(form): Form<AliasForm>,
) -> Response {
if let Some(rejection) = reject_cross_site(&headers) {
return rejection;
}
let mut inputs = PageInputs::for_selection(Selection {
let inputs = PageInputs::for_selection(Selection {
profile: form.profile.clone(),
table: form.table.clone(),
});
inputs.presentation = form.clone();
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"
);
return refuse(
state,
headers,
inputs,
Page::Presentation,
message,
)
.await;
let alias = form.alias.trim().to_string();
if alias.is_empty() {
let message = tr!(Locale::from_headers(&headers), "td-err-choose-rename");
return refuse(state, headers, inputs, Page::Presentation, message).await;
}
let mut columns = form
.column_ids
let columns = match current_columns(&state, &headers, &inputs).await {
Ok(columns) => columns,
Err(response) => return response,
};
if !columns.iter().any(|column| column.column_id == form.column_id) {
let message = tr!(Locale::from_headers(&headers), "td-err-unknown-column");
return refuse(state, headers, inputs, Page::Presentation, message).await;
}
let presentation = columns
.iter()
.copied()
.zip(form.aliases.iter())
.map(|(column_id, alias)| ColumnPresentation {
column_id,
alias: alias.trim().to_string(),
.map(|column| ColumnPresentation {
column_id: column.column_id,
alias: if column.column_id == form.column_id {
alias.clone()
} else {
column.name.clone()
},
})
.collect();
apply_presentation(
state,
headers,
inputs,
form.profile,
form.table,
form.expected_row_version,
presentation,
)
.await
}
/// POST /admin/tables/presentation/order — SetColumnPresentation, moving one
/// column past its neighbour.
///
/// Every alias in the request is the name the backend just reported, so this
/// write cannot rename a column even when the browser's copy of the table is
/// stale. Only the order it sends comes from the form.
pub(crate) async fn set_column_order(
State(state): State<AppState>,
headers: HeaderMap,
Form(form): Form<OrderForm>,
) -> 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 columns = match current_columns(&state, &headers, &inputs).await {
Ok(columns) => columns,
Err(response) => return response,
};
let Some(index) = columns
.iter()
.position(|column| column.column_id == form.column_id)
else {
let message = tr!(Locale::from_headers(&headers), "td-err-unknown-column");
return refuse(state, headers, inputs, Page::Presentation, message).await;
};
// A column at the end of the table has nowhere further to go, and the
// button that says so is disabled; a request that asks anyway is answered
// with the table as it is.
let swap_with = match form.direction.as_str() {
"up" => index.checked_sub(1),
"down" if index + 1 < columns.len() => Some(index + 1),
_ => None,
};
let Some(swap_with) = swap_with else {
return respond(state, headers, inputs, Page::Presentation, StatusCode::OK).await;
};
let mut presentation = columns
.iter()
.map(|column| ColumnPresentation {
column_id: column.column_id,
alias: column.name.clone(),
})
.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);
}
}
presentation.swap(index, swap_with);
apply_presentation(
state,
headers,
inputs,
form.profile,
form.table,
form.expected_row_version,
presentation,
)
.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
/// not about must carry the name the server holds this moment -- not the name
/// the browser was showing when the page was drawn.
async fn current_columns(
state: &AppState,
headers: &HeaderMap,
inputs: &PageInputs,
) -> Result<Vec<DetailColumn>, Response> {
match load_page(state.clone(), headers, inputs.clone()).await {
Ok(page) => Ok(page.detail.map(|detail| detail.columns).unwrap_or_default()),
Err(error) => Err(load_error_response(headers, error)),
}
}
/// The half both writes share: send the presentation, answer with the page.
///
/// `expected_row_version` is the browser's, not the one the read above saw, so
/// a definition that changed under the user is still refused by the backend
/// rather than silently written over.
async fn apply_presentation(
state: AppState,
headers: HeaderMap,
inputs: PageInputs,
profile: String,
table: String,
expected_row_version: i64,
columns: Vec<ColumnPresentation>,
) -> Response {
let request = SetColumnPresentationRequest {
profile_name: form.profile.clone(),
table_name: form.table.clone(),
profile_name: profile,
table_name: table,
columns,
expected_row_version: form.expected_row_version,
expected_row_version,
};
let Ok(request) = authenticated_request(&headers, request) else {
return Redirect::to("/login").into_response();
@@ -385,28 +482,28 @@ pub(crate) async fn set_column_presentation(
let mut definitions = state.definitions.clone();
match definitions.set_column_presentation(request).await {
Ok(response) if response.get_ref().success => {
let mut inputs = inputs;
inputs.status = Some(response.into_inner().message);
inputs.presentation = PresentationForm {
profile: form.profile,
table: form.table,
..Default::default()
};
respond(state, headers, inputs, Page::Presentation, StatusCode::OK).await
}
Ok(response) => {
let message = response.into_inner().message;
let message = if message.is_empty() {
tr!(
Locale::from_headers(&headers),
"td-err-backend-no-rename"
)
tr!(Locale::from_headers(&headers), "td-err-backend-no-rename")
} else {
message
};
refuse(state, headers, inputs, Page::Presentation, message).await
}
Err(error) => {
refuse(state, headers, inputs, Page::Presentation, error.message().to_string()).await
refuse(
state,
headers,
inputs,
Page::Presentation,
error.message().to_string(),
)
.await
}
}
}