fixes
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user