fix single-option dropdown presentation form bug

This commit is contained in:
Brent Schroeter 2025-12-11 08:30:43 +00:00
parent a341a317fc
commit 92d2a963f5
3 changed files with 68 additions and 18 deletions

View file

@ -8,7 +8,12 @@ use serde::Deserialize;
use crate::errors::AppError;
/// A subset of an HTTP form that represents a [`Presentation`] in its component
/// parts. It may be merged into a larger HTTP form using `serde(flatten)`.
/// parts.
///
/// WARNING: Though this was originally designed to be combined with
/// `#[serde(flatten)]`, Serde's buffering strategy causes `flatten` to break
/// deserialization of `Vec<_>` fields when provided with a sequence of length
/// == 1. `#[serde(flatten)]` should be avoided.
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct PresentationForm {
pub(crate) presentation_tag: String,

View file

@ -34,13 +34,37 @@ pub(super) struct PathParams {
}
// FIXME: validate name, prevent leading underscore
#[derive(Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize)]
pub(super) struct FormBody {
name: String,
table_label: String,
#[serde(flatten)]
presentation_form: PresentationForm,
presentation_tag: String,
#[serde(default)]
dropdown_option_colors: Vec<String>,
#[serde(default)]
dropdown_option_values: Vec<String>,
#[serde(default)]
text_input_mode: String,
#[serde(default)]
timestamp_format: String,
}
impl From<FormBody> for PresentationForm {
fn from(value: FormBody) -> Self {
Self {
presentation_tag: value.presentation_tag,
dropdown_option_colors: value.dropdown_option_colors,
dropdown_option_values: value.dropdown_option_values,
text_input_mode: value.text_input_mode,
timestamp_format: value.timestamp_format,
}
}
}
/// HTTP POST handler for adding a [`Field`] to a [`Portal`]. If the field name
@ -84,7 +108,7 @@ pub(super) async fn post(
.fetch_one()
.await?;
let presentation = Presentation::try_from(form.presentation_form)?;
let presentation = Presentation::try_from(Into::<PresentationForm>::into(form.clone()))?;
query(&format!(
"alter table {ident} add column if not exists {col} {typ}",

View file

@ -21,13 +21,36 @@ pub(super) struct PathParams {
workspace_id: Uuid,
}
#[derive(Debug, Deserialize, Validate)]
#[derive(Clone, Debug, Deserialize, Validate)]
pub(super) struct FormBody {
field_id: Uuid,
table_label: String,
#[serde(flatten)]
presentation_form: PresentationForm,
presentation_tag: String,
#[serde(default)]
dropdown_option_colors: Vec<String>,
#[serde(default)]
dropdown_option_values: Vec<String>,
#[serde(default)]
text_input_mode: String,
#[serde(default)]
timestamp_format: String,
}
impl From<FormBody> for PresentationForm {
fn from(value: FormBody) -> Self {
Self {
presentation_tag: value.presentation_tag,
dropdown_option_colors: value.dropdown_option_colors,
dropdown_option_values: value.dropdown_option_values,
text_input_mode: value.text_input_mode,
timestamp_format: value.timestamp_format,
}
}
}
/// HTTP POST handler for updating an existing [`Field`].
@ -44,11 +67,7 @@ pub(super) async fn post(
rel_oid,
workspace_id,
}): Path<PathParams>,
ValidatedForm(FormBody {
field_id,
table_label,
presentation_form,
}): ValidatedForm<FormBody>,
ValidatedForm(form): ValidatedForm<FormBody>,
) -> Result<Response, AppError> {
// FIXME CSRF
@ -57,18 +76,20 @@ pub(super) async fn post(
// Ensure field exists and belongs to portal.
Field::belonging_to_portal(portal_id)
.with_id(field_id)
.with_id(form.field_id)
.fetch_one(&mut app_db)
.await?;
Field::update()
.id(field_id)
.table_label(if table_label.is_empty() {
.id(form.field_id)
.table_label(if form.table_label.is_empty() {
None
} else {
Some(table_label)
Some(form.table_label.clone())
})
.presentation(Presentation::try_from(presentation_form)?)
.presentation(Presentation::try_from(Into::<PresentationForm>::into(
form,
))?)
.build()?
.execute(&mut app_db)
.await?;