add table is better now
This commit is contained in:
@@ -1,54 +1,85 @@
|
||||
//! The builder's request handlers.
|
||||
//!
|
||||
//! Every interaction posts the whole draft and swaps the whole builder back,
|
||||
//! so the server stays the single owner of the draft's rules — the same ones
|
||||
//! the TUI client applies in-process between keystrokes.
|
||||
|
||||
use axum::{
|
||||
Form,
|
||||
extract::State,
|
||||
http::{HeaderMap, HeaderValue, StatusCode, header},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::Form;
|
||||
|
||||
use crate::{AppState, services::authenticated_request};
|
||||
|
||||
use super::{
|
||||
draft::TableDraft,
|
||||
loader::{LoadError, load_page},
|
||||
state::CreateTableForm,
|
||||
state::{AddTablePageState, BuilderForm},
|
||||
ui,
|
||||
};
|
||||
|
||||
pub(crate) async fn new_table_page(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
) -> Response {
|
||||
render_loaded(load_page(state, &headers, CreateTableForm::default(), None).await)
|
||||
/// GET /admin/tables/new
|
||||
pub(crate) async fn new_table_page(State(state): State<AppState>, headers: HeaderMap) -> Response {
|
||||
match load_page(state, &headers, TableDraft::new(), None, None).await {
|
||||
Ok(page) => Html(ui::render_page(&page)).into_response(),
|
||||
Err(error) => load_error_response(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// POST /admin/tables/builder — every button and every `change` in the builder.
|
||||
pub(crate) async fn update_builder(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Form(form): Form<BuilderForm>,
|
||||
) -> Response {
|
||||
if let Some(rejection) = reject_cross_site(&headers) {
|
||||
return rejection;
|
||||
}
|
||||
|
||||
let mut page = match load_page(state, &headers, form.to_draft(), None, None).await {
|
||||
Ok(page) => page,
|
||||
Err(error) => return load_error_response(error),
|
||||
};
|
||||
|
||||
apply_action(&mut page, &form);
|
||||
Html(ui::render_builder(&page)).into_response()
|
||||
}
|
||||
|
||||
/// POST /admin/tables — create the table.
|
||||
pub(crate) async fn create_table(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Form(form): Form<CreateTableForm>,
|
||||
Form(form): Form<BuilderForm>,
|
||||
) -> Response {
|
||||
let submitted_form = form.clone();
|
||||
if let Err(error) = load_page(state.clone(), &headers, form.clone(), None).await {
|
||||
return render_loaded(Err(error));
|
||||
if let Some(rejection) = reject_cross_site(&headers) {
|
||||
return rejection;
|
||||
}
|
||||
if headers
|
||||
.get("sec-fetch-site")
|
||||
.is_some_and(|value| value == "cross-site")
|
||||
{
|
||||
return (StatusCode::FORBIDDEN, "Cross-site form submission rejected").into_response();
|
||||
}
|
||||
let request = match form.into_request() {
|
||||
|
||||
let mut page = match load_page(state.clone(), &headers, form.to_draft(), None, None).await {
|
||||
Ok(page) => page,
|
||||
Err(error) => return load_error_response(error),
|
||||
};
|
||||
|
||||
// The draft is validated here with exactly the checks the client runs
|
||||
// before it will save; the server re-validates authoritatively.
|
||||
let request = match page.draft.clone().into_request() {
|
||||
Ok(request) => request,
|
||||
Err(message) => {
|
||||
return render_loaded(
|
||||
load_page(state, &headers, submitted_form, Some(message)).await,
|
||||
);
|
||||
page.error = Some(message);
|
||||
return (StatusCode::UNPROCESSABLE_ENTITY, Html(ui::render_builder(&page)))
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let profile_name = request.profile_name.clone();
|
||||
let mut definitions = state.definitions;
|
||||
let request = match authenticated_request(&headers, request) {
|
||||
Ok(request) => request,
|
||||
Err(_) => return Redirect::to("/login").into_response(),
|
||||
};
|
||||
|
||||
let mut definitions = state.definitions;
|
||||
match definitions.post_table_definition(request).await {
|
||||
Ok(response) if response.get_ref().success => {
|
||||
let location = format!("/admin?profile={profile_name}");
|
||||
@@ -56,40 +87,67 @@ pub(crate) async fn create_table(
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid redirect").into_response();
|
||||
};
|
||||
let mut response = StatusCode::SEE_OTHER.into_response();
|
||||
response.headers_mut().insert(header::LOCATION, location.clone());
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(header::LOCATION, location.clone());
|
||||
response.headers_mut().insert("hx-redirect", location);
|
||||
response
|
||||
}
|
||||
Ok(response) => {
|
||||
let detail = if response.get_ref().sql.is_empty() {
|
||||
page.error = Some(if response.get_ref().sql.is_empty() {
|
||||
"The backend did not create the table.".to_string()
|
||||
} else {
|
||||
response.get_ref().sql.clone()
|
||||
};
|
||||
(
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
Html(ui::render_submission_error(&detail)),
|
||||
)
|
||||
.into_response()
|
||||
});
|
||||
(StatusCode::UNPROCESSABLE_ENTITY, Html(ui::render_builder(&page))).into_response()
|
||||
}
|
||||
Err(error) => {
|
||||
page.error = Some(error.message().to_string());
|
||||
(StatusCode::UNPROCESSABLE_ENTITY, Html(ui::render_builder(&page))).into_response()
|
||||
}
|
||||
Err(error) => (
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
Html(ui::render_submission_error(error.message())),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_loaded(result: Result<super::state::AddTablePageState, LoadError>) -> Response {
|
||||
match result {
|
||||
Ok(page) => Html(ui::render_page(&page)).into_response(),
|
||||
Err(LoadError::Unauthenticated) => Redirect::to("/login").into_response(),
|
||||
Err(LoadError::Forbidden) => (
|
||||
/// Runs the pressed button against the draft.
|
||||
///
|
||||
/// `refresh` covers the plain re-renders — picking a profile or a column type
|
||||
/// changes which fields apply, which is the client's field-visibility rule.
|
||||
fn apply_action(page: &mut AddTablePageState, form: &BuilderForm) {
|
||||
let index = form.index.unwrap_or(0);
|
||||
match form.action.as_str() {
|
||||
"add-column" => match page.draft.add_column_from_inputs() {
|
||||
Ok(status) => page.status = Some(status),
|
||||
Err(message) => page.error = Some(message),
|
||||
},
|
||||
"remove-column" => match page.draft.remove_column(index) {
|
||||
Ok(status) => page.status = Some(status),
|
||||
Err(message) => page.error = Some(message),
|
||||
},
|
||||
"toggle-index" => page.draft.toggle_column_indexed(index),
|
||||
"cycle-link" => page.draft.cycle_link_mode(index),
|
||||
"toggle-display" => page.draft.toggle_row_display_candidate(index),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn reject_cross_site(headers: &HeaderMap) -> Option<Response> {
|
||||
headers
|
||||
.get("sec-fetch-site")
|
||||
.is_some_and(|value| value == "cross-site")
|
||||
.then(|| (StatusCode::FORBIDDEN, "Cross-site form submission rejected").into_response())
|
||||
}
|
||||
|
||||
fn load_error_response(error: LoadError) -> Response {
|
||||
match error {
|
||||
LoadError::Unauthenticated => Redirect::to("/login").into_response(),
|
||||
LoadError::Forbidden => (
|
||||
StatusCode::FORBIDDEN,
|
||||
Html(ui::render_submission_error("Administrator access is required.")),
|
||||
Html(ui::render_submission_error(
|
||||
"Administrator access is required.",
|
||||
)),
|
||||
)
|
||||
.into_response(),
|
||||
Err(LoadError::Backend(message)) => (
|
||||
LoadError::Backend(message) => (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Html(ui::render_submission_error(&message)),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user