refactor: deduplicate update handlers and edit templates
Add HasChanges trait with impl_has_changes! macro, validate_update() and update_response() helpers to reduce boilerplate across all 7 entity update handlers. Extract edit form actions (error, spinner, buttons) into a shared Askama macro. Also adds missing no-changes validation to the bag update handler.
This commit is contained in:
parent
8ced58039b
commit
2cd579a574
17 changed files with 170 additions and 305 deletions
|
|
@ -11,7 +11,8 @@ use crate::application::errors::{ApiError, AppError};
|
|||
use crate::application::routes::api::images::save_deferred_image;
|
||||
use crate::application::routes::api::macros::{define_delete_handler, define_enriched_get_handler};
|
||||
use crate::application::routes::support::{
|
||||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request,
|
||||
FlexiblePayload, ListQuery, PayloadSource, impl_has_changes, is_datastar_request,
|
||||
validate_update,
|
||||
};
|
||||
use crate::application::state::AppState;
|
||||
use crate::domain::bags::{BagFilter, BagSortKey, BagWithRoast, NewBag, UpdateBag};
|
||||
|
|
@ -156,6 +157,17 @@ impl UpdateBagSubmission {
|
|||
}
|
||||
}
|
||||
|
||||
impl_has_changes!(
|
||||
UpdateBag,
|
||||
roast_id,
|
||||
roast_date,
|
||||
amount,
|
||||
remaining,
|
||||
closed,
|
||||
finished_at,
|
||||
created_at
|
||||
);
|
||||
|
||||
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
||||
pub(crate) async fn update_bag(
|
||||
State(state): State<AppState>,
|
||||
|
|
@ -180,6 +192,8 @@ pub(crate) async fn update_bag(
|
|||
created_at: body_update.created_at,
|
||||
};
|
||||
|
||||
validate_update(&update, image_data_url.as_ref())?;
|
||||
|
||||
// When the bag amount changes, recompute remaining based on how much has been consumed.
|
||||
if let Some(new_amount) = update.amount
|
||||
&& update.remaining.is_none()
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ use crate::application::errors::{ApiError, AppError};
|
|||
use crate::application::routes::api::images::save_deferred_image;
|
||||
use crate::application::routes::api::macros::{define_delete_handler, define_enriched_get_handler};
|
||||
use crate::application::routes::support::{
|
||||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request,
|
||||
FlexiblePayload, ListQuery, PayloadSource, impl_has_changes, is_datastar_request,
|
||||
validate_update,
|
||||
};
|
||||
use crate::application::state::AppState;
|
||||
use crate::domain::bags::BagFilter;
|
||||
|
|
@ -375,6 +376,21 @@ impl UpdateBrewSubmission {
|
|||
}
|
||||
}
|
||||
|
||||
impl_has_changes!(
|
||||
UpdateBrew,
|
||||
bag_id,
|
||||
coffee_weight,
|
||||
grinder_id,
|
||||
grind_setting,
|
||||
brewer_id,
|
||||
filter_paper_id,
|
||||
water_volume,
|
||||
water_temp,
|
||||
quick_notes,
|
||||
brew_time,
|
||||
created_at
|
||||
);
|
||||
|
||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
||||
pub(crate) async fn update_brew(
|
||||
State(state): State<AppState>,
|
||||
|
|
@ -386,22 +402,7 @@ pub(crate) async fn update_brew(
|
|||
let (submission, source) = payload.into_parts();
|
||||
let (update, image_data_url) = submission.into_parts();
|
||||
|
||||
let has_changes = update.bag_id.is_some()
|
||||
|| update.coffee_weight.is_some()
|
||||
|| update.grinder_id.is_some()
|
||||
|| update.grind_setting.is_some()
|
||||
|| update.brewer_id.is_some()
|
||||
|| update.filter_paper_id.is_some()
|
||||
|| update.water_volume.is_some()
|
||||
|| update.water_temp.is_some()
|
||||
|| update.quick_notes.is_some()
|
||||
|| update.brew_time.is_some()
|
||||
|| update.created_at.is_some()
|
||||
|| image_data_url.is_some();
|
||||
|
||||
if !has_changes {
|
||||
return Err(AppError::validation("no changes provided").into());
|
||||
}
|
||||
validate_update(&update, image_data_url.as_ref())?;
|
||||
|
||||
state
|
||||
.brew_repo
|
||||
|
|
@ -414,12 +415,6 @@ pub(crate) async fn update_brew(
|
|||
|
||||
save_deferred_image(&state, "brew", i64::from(id), image_data_url.as_deref()).await;
|
||||
|
||||
let enriched = state
|
||||
.brew_repo
|
||||
.get_with_details(id)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
let detail_url = format!("/brews/{id}");
|
||||
|
||||
if is_datastar_request(&headers) {
|
||||
|
|
@ -428,6 +423,11 @@ pub(crate) async fn update_brew(
|
|||
} else if matches!(source, PayloadSource::Form) {
|
||||
Ok(Redirect::to(&detail_url).into_response())
|
||||
} else {
|
||||
let enriched = state
|
||||
.brew_repo
|
||||
.get_with_details(id)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(Json(enriched).into_response())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ use crate::application::routes::api::images::save_deferred_image;
|
|||
use crate::application::routes::api::macros::{
|
||||
define_delete_handler, define_get_handler, define_list_fragment_renderer,
|
||||
};
|
||||
use crate::application::routes::support::impl_has_changes;
|
||||
use crate::application::routes::support::{
|
||||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, render_redirect_script,
|
||||
update_response, validate_update,
|
||||
};
|
||||
use crate::application::state::AppState;
|
||||
use crate::domain::cafes::{Cafe, CafeSortKey, NewCafe, UpdateCafe};
|
||||
|
|
@ -175,6 +177,10 @@ impl UpdateCafeSubmission {
|
|||
}
|
||||
}
|
||||
|
||||
impl_has_changes!(
|
||||
UpdateCafe, name, city, country, latitude, longitude, website, created_at
|
||||
);
|
||||
|
||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
||||
pub(crate) async fn update_cafe(
|
||||
State(state): State<AppState>,
|
||||
|
|
@ -186,18 +192,7 @@ pub(crate) async fn update_cafe(
|
|||
let (submission, source) = payload.into_parts();
|
||||
let (update, image_data_url) = submission.into_parts();
|
||||
|
||||
let has_changes = update.name.is_some()
|
||||
|| update.city.is_some()
|
||||
|| update.country.is_some()
|
||||
|| update.latitude.is_some()
|
||||
|| update.longitude.is_some()
|
||||
|| update.website.is_some()
|
||||
|| update.created_at.is_some()
|
||||
|| image_data_url.is_some();
|
||||
|
||||
if !has_changes {
|
||||
return Err(AppError::validation("no changes provided").into());
|
||||
}
|
||||
validate_update(&update, image_data_url.as_ref())?;
|
||||
|
||||
let cafe = state
|
||||
.cafe_repo
|
||||
|
|
@ -216,14 +211,7 @@ pub(crate) async fn update_cafe(
|
|||
.await;
|
||||
|
||||
let detail_url = format!("/cafes/{}", cafe.slug);
|
||||
|
||||
if is_datastar_request(&headers) {
|
||||
render_redirect_script(&detail_url).map_err(ApiError::from)
|
||||
} else if matches!(source, PayloadSource::Form) {
|
||||
Ok(Redirect::to(&detail_url).into_response())
|
||||
} else {
|
||||
Ok(Json(cafe).into_response())
|
||||
}
|
||||
update_response(&headers, source, &detail_url, Json(cafe).into_response())
|
||||
}
|
||||
|
||||
define_delete_handler!(
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ use crate::application::routes::api::images::save_deferred_image;
|
|||
use crate::application::routes::api::macros::{
|
||||
define_delete_handler, define_enriched_get_handler, define_list_fragment_renderer,
|
||||
};
|
||||
use crate::application::routes::support::impl_has_changes;
|
||||
use crate::application::routes::support::{
|
||||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, render_redirect_script,
|
||||
validate_update,
|
||||
};
|
||||
use crate::application::state::AppState;
|
||||
use crate::domain::cups::{CupFilter, CupSortKey, CupWithDetails, NewCup, UpdateCup};
|
||||
|
|
@ -116,6 +118,8 @@ impl UpdateCupSubmission {
|
|||
}
|
||||
}
|
||||
|
||||
impl_has_changes!(UpdateCup, roast_id, cafe_id, created_at);
|
||||
|
||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
||||
pub(crate) async fn update_cup(
|
||||
State(state): State<AppState>,
|
||||
|
|
@ -127,14 +131,7 @@ pub(crate) async fn update_cup(
|
|||
let (submission, source) = payload.into_parts();
|
||||
let (update, image_data_url) = submission.into_parts();
|
||||
|
||||
let has_changes = update.roast_id.is_some()
|
||||
|| update.cafe_id.is_some()
|
||||
|| update.created_at.is_some()
|
||||
|| image_data_url.is_some();
|
||||
|
||||
if !has_changes {
|
||||
return Err(AppError::validation("no changes provided").into());
|
||||
}
|
||||
validate_update(&update, image_data_url.as_ref())?;
|
||||
|
||||
let cup = state
|
||||
.cup_repo
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ use crate::application::routes::api::images::save_deferred_image;
|
|||
use crate::application::routes::api::macros::{
|
||||
define_delete_handler, define_get_handler, define_list_fragment_renderer,
|
||||
};
|
||||
use crate::application::routes::support::impl_has_changes;
|
||||
use crate::application::routes::support::{
|
||||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, render_redirect_script,
|
||||
update_response, validate_update,
|
||||
};
|
||||
use crate::application::state::AppState;
|
||||
use crate::domain::gear::{Gear, GearCategory, GearFilter, GearSortKey, NewGear, UpdateGear};
|
||||
|
|
@ -148,6 +150,8 @@ impl UpdateGearSubmission {
|
|||
}
|
||||
}
|
||||
|
||||
impl_has_changes!(UpdateGear, make, model, created_at);
|
||||
|
||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
||||
pub(crate) async fn update_gear(
|
||||
State(state): State<AppState>,
|
||||
|
|
@ -159,14 +163,7 @@ pub(crate) async fn update_gear(
|
|||
let (submission, source) = payload.into_parts();
|
||||
let (update, image_data_url) = submission.into_parts();
|
||||
|
||||
let has_changes = update.make.is_some()
|
||||
|| update.model.is_some()
|
||||
|| update.created_at.is_some()
|
||||
|| image_data_url.is_some();
|
||||
|
||||
if !has_changes {
|
||||
return Err(AppError::validation("no changes provided").into());
|
||||
}
|
||||
validate_update(&update, image_data_url.as_ref())?;
|
||||
|
||||
let gear = state
|
||||
.gear_repo
|
||||
|
|
@ -186,14 +183,7 @@ pub(crate) async fn update_gear(
|
|||
.await;
|
||||
|
||||
let detail_url = format!("/gear/{}", gear.id);
|
||||
|
||||
if is_datastar_request(&headers) {
|
||||
render_redirect_script(&detail_url).map_err(ApiError::from)
|
||||
} else if matches!(source, PayloadSource::Form) {
|
||||
Ok(Redirect::to(&detail_url).into_response())
|
||||
} else {
|
||||
Ok(Json(gear).into_response())
|
||||
}
|
||||
update_response(&headers, source, &detail_url, Json(gear).into_response())
|
||||
}
|
||||
|
||||
define_delete_handler!(
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ use crate::application::routes::api::images::save_deferred_image;
|
|||
use crate::application::routes::api::macros::{
|
||||
define_delete_handler, define_get_handler, define_list_fragment_renderer,
|
||||
};
|
||||
use crate::application::routes::support::impl_has_changes;
|
||||
use crate::application::routes::support::{
|
||||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, render_redirect_script,
|
||||
update_response, validate_update,
|
||||
};
|
||||
use crate::application::state::AppState;
|
||||
use crate::domain::ids::RoasterId;
|
||||
|
|
@ -168,6 +170,8 @@ impl UpdateRoasterSubmission {
|
|||
}
|
||||
}
|
||||
|
||||
impl_has_changes!(UpdateRoaster, name, country, city, homepage, created_at);
|
||||
|
||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
||||
pub(crate) async fn update_roaster(
|
||||
State(state): State<AppState>,
|
||||
|
|
@ -180,16 +184,7 @@ pub(crate) async fn update_roaster(
|
|||
let (update, image_data_url) = submission.into_parts();
|
||||
let update = update.normalize();
|
||||
|
||||
let has_changes = update.name.is_some()
|
||||
|| update.country.is_some()
|
||||
|| update.city.is_some()
|
||||
|| update.homepage.is_some()
|
||||
|| update.created_at.is_some()
|
||||
|| image_data_url.is_some();
|
||||
|
||||
if !has_changes {
|
||||
return Err(AppError::validation("no changes provided").into());
|
||||
}
|
||||
validate_update(&update, image_data_url.as_ref())?;
|
||||
|
||||
let roaster = state
|
||||
.roaster_repo
|
||||
|
|
@ -208,14 +203,7 @@ pub(crate) async fn update_roaster(
|
|||
.await;
|
||||
|
||||
let detail_url = format!("/roasters/{}", roaster.slug);
|
||||
|
||||
if is_datastar_request(&headers) {
|
||||
render_redirect_script(&detail_url).map_err(ApiError::from)
|
||||
} else if matches!(source, PayloadSource::Form) {
|
||||
Ok(Redirect::to(&detail_url).into_response())
|
||||
} else {
|
||||
Ok(Json(roaster).into_response())
|
||||
}
|
||||
update_response(&headers, source, &detail_url, Json(roaster).into_response())
|
||||
}
|
||||
|
||||
define_delete_handler!(
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ use crate::application::routes::api::macros::{
|
|||
define_delete_handler, define_enriched_get_handler, define_list_fragment_renderer,
|
||||
};
|
||||
use crate::application::routes::support::{
|
||||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, render_redirect_script,
|
||||
FlexiblePayload, ListQuery, PayloadSource, impl_has_changes, is_datastar_request,
|
||||
render_redirect_script, validate_update,
|
||||
};
|
||||
use crate::application::state::AppState;
|
||||
use crate::domain::ids::{RoastId, RoasterId};
|
||||
|
|
@ -214,6 +215,18 @@ impl UpdateRoastSubmission {
|
|||
}
|
||||
}
|
||||
|
||||
impl_has_changes!(
|
||||
UpdateRoast,
|
||||
roaster_id,
|
||||
name,
|
||||
origin,
|
||||
region,
|
||||
producer,
|
||||
tasting_notes,
|
||||
process,
|
||||
created_at
|
||||
);
|
||||
|
||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
||||
pub(crate) async fn update_roast(
|
||||
State(state): State<AppState>,
|
||||
|
|
@ -225,19 +238,7 @@ pub(crate) async fn update_roast(
|
|||
let (submission, source) = payload.into_parts();
|
||||
let (update, image_data_url) = submission.into_parts();
|
||||
|
||||
let has_changes = update.roaster_id.is_some()
|
||||
|| update.name.is_some()
|
||||
|| update.origin.is_some()
|
||||
|| update.region.is_some()
|
||||
|| update.producer.is_some()
|
||||
|| update.tasting_notes.is_some()
|
||||
|| update.process.is_some()
|
||||
|| update.created_at.is_some()
|
||||
|| image_data_url.is_some();
|
||||
|
||||
if !has_changes {
|
||||
return Err(AppError::validation("no changes provided").into());
|
||||
}
|
||||
validate_update(&update, image_data_url.as_ref())?;
|
||||
|
||||
state
|
||||
.roast_repo
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use askama::Template;
|
||||
use axum::extract::{Form, FromRequest, Json as JsonPayload, Request};
|
||||
use axum::http::{HeaderMap, HeaderValue, header::CONTENT_TYPE};
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use axum::response::{Html, IntoResponse, Redirect, Response};
|
||||
use serde::Deserialize;
|
||||
use tracing::warn;
|
||||
|
||||
|
|
@ -32,6 +32,50 @@ impl<T> FlexiblePayload<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Trait for update structs that can report whether any field was set.
|
||||
pub(crate) trait HasChanges {
|
||||
fn has_changes(&self) -> bool;
|
||||
}
|
||||
|
||||
/// Implement `HasChanges` for an update struct by checking `is_some()` on each field.
|
||||
macro_rules! impl_has_changes {
|
||||
($type:ty, $($field:ident),+) => {
|
||||
impl $crate::application::routes::support::HasChanges for $type {
|
||||
fn has_changes(&self) -> bool {
|
||||
$(self.$field.is_some())||+
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(crate) use impl_has_changes;
|
||||
|
||||
/// Return `400 Bad Request` if neither the update struct nor the image has any changes.
|
||||
pub(crate) fn validate_update<T: HasChanges>(
|
||||
update: &T,
|
||||
image: Option<&String>,
|
||||
) -> Result<(), ApiError> {
|
||||
if !update.has_changes() && image.is_none() {
|
||||
return Err(AppError::validation("no changes provided").into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Three-way response for update handlers: Datastar redirect, form redirect, or JSON.
|
||||
pub(crate) fn update_response(
|
||||
headers: &HeaderMap,
|
||||
source: PayloadSource,
|
||||
detail_url: &str,
|
||||
json_body: Response,
|
||||
) -> Result<Response, ApiError> {
|
||||
if is_datastar_request(headers) {
|
||||
render_redirect_script(detail_url).map_err(ApiError::from)
|
||||
} else if matches!(source, PayloadSource::Form) {
|
||||
Ok(Redirect::to(detail_url).into_response())
|
||||
} else {
|
||||
Ok(json_body)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub(crate) struct ListQuery {
|
||||
page: Option<u32>,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "partials/icons.html" as icons %}
|
||||
{% import "partials/detail_cards.html" as detail_cards %}
|
||||
{% block title %}Brewlog · Edit Bag{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
@ -75,35 +76,7 @@
|
|||
/>
|
||||
</label>
|
||||
</div>
|
||||
<p
|
||||
data-show="$_submitError"
|
||||
data-text="$_submitError"
|
||||
style="display:none"
|
||||
class="text-sm text-error"
|
||||
role="alert"
|
||||
></p>
|
||||
<div
|
||||
data-show="$_submitting"
|
||||
style="display:none"
|
||||
class="flex items-center justify-center gap-3 py-2 text-sm text-accent"
|
||||
>
|
||||
{{ icons::spinner("h-5 w-5") }} Saving…
|
||||
</div>
|
||||
<div class="flex flex-col gap-2" data-show="!$_submitting">
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
||||
>
|
||||
{{ icons::check("h-4 w-4") }} Save Changes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick="history.back()"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt"
|
||||
>
|
||||
{{ icons::x_mark("h-4 w-4") }} Cancel
|
||||
</button>
|
||||
</div>
|
||||
{{ detail_cards::edit_form_actions() }}
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "partials/icons.html" as icons %}
|
||||
{% import "partials/image_section.html" as img %}
|
||||
{% import "partials/detail_cards.html" as detail_cards %}
|
||||
{% block title %}Brewlog · Edit Brew{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
@ -361,35 +362,7 @@
|
|||
</div>
|
||||
|
||||
{{ img::deferred_upload_with_preview("edit-brew-image", "Brew Image", "brew", id, image_url) }}
|
||||
<p
|
||||
data-show="$_submitError"
|
||||
data-text="$_submitError"
|
||||
style="display:none"
|
||||
class="text-sm text-error"
|
||||
role="alert"
|
||||
></p>
|
||||
<div
|
||||
data-show="$_submitting"
|
||||
style="display:none"
|
||||
class="flex items-center justify-center gap-3 py-2 text-sm text-accent"
|
||||
>
|
||||
{{ icons::spinner("h-5 w-5") }} Saving…
|
||||
</div>
|
||||
<div class="flex flex-col gap-2" data-show="!$_submitting">
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
||||
>
|
||||
{{ icons::check("h-4 w-4") }} Save Changes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick="history.back()"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt"
|
||||
>
|
||||
{{ icons::x_mark("h-4 w-4") }} Cancel
|
||||
</button>
|
||||
</div>
|
||||
{{ detail_cards::edit_form_actions() }}
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "partials/icons.html" as icons %}
|
||||
{% import "partials/image_section.html" as img %}
|
||||
{% import "partials/detail_cards.html" as detail_cards %}
|
||||
{% block title %}Brewlog · Edit Cafe{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
@ -118,35 +119,7 @@
|
|||
</label>
|
||||
</div>
|
||||
{{ img::deferred_upload_with_preview("edit-cafe-image", "Cafe Image", "cafe", id, image_url) }}
|
||||
<p
|
||||
data-show="$_submitError"
|
||||
data-text="$_submitError"
|
||||
style="display:none"
|
||||
class="text-sm text-error"
|
||||
role="alert"
|
||||
></p>
|
||||
<div
|
||||
data-show="$_submitting"
|
||||
style="display:none"
|
||||
class="flex items-center justify-center gap-3 py-2 text-sm text-accent"
|
||||
>
|
||||
{{ icons::spinner("h-5 w-5") }} Saving…
|
||||
</div>
|
||||
<div class="flex flex-col gap-2" data-show="!$_submitting">
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
||||
>
|
||||
{{ icons::check("h-4 w-4") }} Save Changes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick="history.back()"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt"
|
||||
>
|
||||
{{ icons::x_mark("h-4 w-4") }} Cancel
|
||||
</button>
|
||||
</div>
|
||||
{{ detail_cards::edit_form_actions() }}
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "partials/icons.html" as icons %}
|
||||
{% import "partials/image_section.html" as img %}
|
||||
{% import "partials/detail_cards.html" as detail_cards %}
|
||||
{% block title %}Brewlog · Edit Cup{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
@ -72,35 +73,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{{ img::deferred_upload_with_preview("edit-cup-image", "Cup Image", "cup", id, image_url) }}
|
||||
<p
|
||||
data-show="$_submitError"
|
||||
data-text="$_submitError"
|
||||
style="display:none"
|
||||
class="text-sm text-error"
|
||||
role="alert"
|
||||
></p>
|
||||
<div
|
||||
data-show="$_submitting"
|
||||
style="display:none"
|
||||
class="flex items-center justify-center gap-3 py-2 text-sm text-accent"
|
||||
>
|
||||
{{ icons::spinner("h-5 w-5") }} Saving…
|
||||
</div>
|
||||
<div class="flex flex-col gap-2" data-show="!$_submitting">
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
||||
>
|
||||
{{ icons::check("h-4 w-4") }} Save Changes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick="history.back()"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt"
|
||||
>
|
||||
{{ icons::x_mark("h-4 w-4") }} Cancel
|
||||
</button>
|
||||
</div>
|
||||
{{ detail_cards::edit_form_actions() }}
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "partials/icons.html" as icons %}
|
||||
{% import "partials/image_section.html" as img %}
|
||||
{% import "partials/detail_cards.html" as detail_cards %}
|
||||
{% block title %}Brewlog · Edit Gear{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
@ -64,35 +65,7 @@
|
|||
</label>
|
||||
</div>
|
||||
{{ img::deferred_upload_with_preview("edit-gear-image", "Gear Image", "gear", id, image_url) }}
|
||||
<p
|
||||
data-show="$_submitError"
|
||||
data-text="$_submitError"
|
||||
style="display:none"
|
||||
class="text-sm text-error"
|
||||
role="alert"
|
||||
></p>
|
||||
<div
|
||||
data-show="$_submitting"
|
||||
style="display:none"
|
||||
class="flex items-center justify-center gap-3 py-2 text-sm text-accent"
|
||||
>
|
||||
{{ icons::spinner("h-5 w-5") }} Saving…
|
||||
</div>
|
||||
<div class="flex flex-col gap-2" data-show="!$_submitting">
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
||||
>
|
||||
{{ icons::check("h-4 w-4") }} Save Changes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick="history.back()"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt"
|
||||
>
|
||||
{{ icons::x_mark("h-4 w-4") }} Cancel
|
||||
</button>
|
||||
</div>
|
||||
{{ detail_cards::edit_form_actions() }}
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "partials/icons.html" as icons %}
|
||||
{% import "partials/image_section.html" as img %}
|
||||
{% import "partials/detail_cards.html" as detail_cards %}
|
||||
{% block title %}Brewlog · Edit Roast{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
@ -140,35 +141,7 @@
|
|||
</label>
|
||||
</div>
|
||||
{{ img::deferred_upload_with_preview("edit-roast-image", "Roast Image", "roast", id, image_url) }}
|
||||
<p
|
||||
data-show="$_submitError"
|
||||
data-text="$_submitError"
|
||||
style="display:none"
|
||||
class="text-sm text-error"
|
||||
role="alert"
|
||||
></p>
|
||||
<div
|
||||
data-show="$_submitting"
|
||||
style="display:none"
|
||||
class="flex items-center justify-center gap-3 py-2 text-sm text-accent"
|
||||
>
|
||||
{{ icons::spinner("h-5 w-5") }} Saving…
|
||||
</div>
|
||||
<div class="flex flex-col gap-2" data-show="!$_submitting">
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
||||
>
|
||||
{{ icons::check("h-4 w-4") }} Save Changes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick="history.back()"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt"
|
||||
>
|
||||
{{ icons::x_mark("h-4 w-4") }} Cancel
|
||||
</button>
|
||||
</div>
|
||||
{{ detail_cards::edit_form_actions() }}
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "partials/icons.html" as icons %}
|
||||
{% import "partials/image_section.html" as img %}
|
||||
{% import "partials/detail_cards.html" as detail_cards %}
|
||||
{% block title %}Brewlog · Edit Roaster{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
@ -82,35 +83,7 @@
|
|||
</label>
|
||||
</div>
|
||||
{{ img::deferred_upload_with_preview("edit-roaster-image", "Roaster Image", "roaster", id, image_url) }}
|
||||
<p
|
||||
data-show="$_submitError"
|
||||
data-text="$_submitError"
|
||||
style="display:none"
|
||||
class="text-sm text-error"
|
||||
role="alert"
|
||||
></p>
|
||||
<div
|
||||
data-show="$_submitting"
|
||||
style="display:none"
|
||||
class="flex items-center justify-center gap-3 py-2 text-sm text-accent"
|
||||
>
|
||||
{{ icons::spinner("h-5 w-5") }} Saving…
|
||||
</div>
|
||||
<div class="flex flex-col gap-2" data-show="!$_submitting">
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
||||
>
|
||||
{{ icons::check("h-4 w-4") }} Save Changes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick="history.back()"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt"
|
||||
>
|
||||
{{ icons::x_mark("h-4 w-4") }} Cancel
|
||||
</button>
|
||||
</div>
|
||||
{{ detail_cards::edit_form_actions() }}
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -146,6 +146,38 @@
|
|||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro edit_form_actions() %}
|
||||
<p
|
||||
data-show="$_submitError"
|
||||
data-text="$_submitError"
|
||||
style="display:none"
|
||||
class="text-sm text-error"
|
||||
role="alert"
|
||||
></p>
|
||||
<div
|
||||
data-show="$_submitting"
|
||||
style="display:none"
|
||||
class="flex items-center justify-center gap-3 py-2 text-sm text-accent"
|
||||
>
|
||||
{{ icons::spinner("h-5 w-5") }} Saving…
|
||||
</div>
|
||||
<div class="flex flex-col gap-2" data-show="!$_submitting">
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
||||
>
|
||||
{{ icons::check("h-4 w-4") }} Save Changes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick="history.back()"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt"
|
||||
>
|
||||
{{ icons::x_mark("h-4 w-4") }} Cancel
|
||||
</button>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro edit_delete_buttons(edit_url, entity_label, api_path, id) %}
|
||||
<div class="rounded-lg border bg-surface p-5 flex items-center gap-2">
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -662,7 +662,7 @@ async fn edit_form_cancel_navigates_back() {
|
|||
// Click Cancel
|
||||
let cancel_btn = session
|
||||
.driver
|
||||
.find(By::XPath("//button[contains(text(), 'Cancel')]"))
|
||||
.find(By::Css("button[onclick='history.back()']"))
|
||||
.await
|
||||
.unwrap();
|
||||
cancel_btn.click().await.unwrap();
|
||||
|
|
|
|||
Loading…
Reference in a new issue