app,server: Allow to edit recipe name

This commit is contained in:
traxys 2023-06-25 16:11:26 +02:00
parent 1a0ffb2d89
commit 47b547caf4
4 changed files with 203 additions and 19 deletions

View file

@ -108,3 +108,8 @@ pub struct RecipeInfo {
pub steps: Vec<String>,
pub ingredients: Vec<(i64, IngredientInfo, f64)>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RecipeRenameRequest {
pub name: String,
}

View file

@ -1,9 +1,18 @@
use api::RecipeInfo;
use api::{RecipeInfo, RecipeRenameRequest};
use uuid::Uuid;
use yew::{prelude::*, suspense::use_future};
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
use yew::{
prelude::*,
suspense::{use_future, use_future_with_deps},
};
use yew_router::prelude::*;
use crate::{api, RegaladeGlobalState, Route};
use crate::{
api,
bootstrap::{FormModal, ModalToggleButton, bs},
RegaladeGlobalState, Route,
};
async fn get_all_recipes(
token: String,
@ -110,13 +119,139 @@ async fn fetch_recipe(token: String, household: Uuid, id: i64) -> anyhow::Result
Ok(rsp.json().await?)
}
#[derive(Clone, PartialEq, Properties)]
struct EditNameProps {
token: String,
household: Uuid,
id: i64,
name: String,
update: Callback<()>,
}
async fn do_rename_recipe(
token: String,
household: Uuid,
recipe: i64,
name: String,
) -> anyhow::Result<()> {
let rsp = gloo_net::http::Request::patch(api!("household/{household}/recipe/{recipe}"))
.json(&RecipeRenameRequest { name })?
.header("Authorization", &format!("Bearer {token}"))
.send()
.await?;
if !rsp.ok() {
let body = rsp.text().await.unwrap_or_default();
anyhow::bail!("Could not get recipes (code={}): {body}", rsp.status());
}
Ok(())
}
#[function_component]
fn EditName(props: &EditNameProps) -> Html {
let name = use_state(|| props.name.clone());
let nm = name.clone();
let onchange = Callback::from(move |e: Event| {
let Some(target) = e.target() else {
return;
};
let Ok(target) = target.dyn_into::<HtmlInputElement>() else {
return;
};
nm.set(target.value());
});
let error = use_state(|| None::<String>);
let nm = name.clone();
let err = error.clone();
let token = props.token.clone();
let household = props.household;
let recipe = props.id;
let update = props.update.clone();
let on_submit = Callback::from(move |_| {
if nm.is_empty() {
err.set(Some("Name can't be empty".into()));
return;
}
let future = do_rename_recipe(token.clone(), household, recipe, (*nm).clone());
let err = err.clone();
let update = update.clone();
wasm_bindgen_futures::spawn_local(async move {
match future.await {
Ok(_) => {
let modal = bs::Modal::get_instance("#rcpEditName");
modal.hide();
err.set(None);
update.emit(());
},
Err(e) => {
err.set(Some(format!("Could not edit name: {e}")));
}
}
})
});
html! {<>
<ModalToggleButton classes={classes!("btn", "btn-secondary")} modal_id="rcpEditName">
{"Edit name"}
</ModalToggleButton>
<FormModal
id="rcpEditName"
fade=true
centered=true
submit_label="Edit"
title="Edit Name"
{on_submit}
>
if let Some(e) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{e}
</div>
}
<div class="form-floating">
<input
class="form-control"
id="rcpEditNameInp"
placeholder={(*name).clone()}
value={(*name).clone()}
{onchange}
/>
<label for="rcpEditNameInp">{"Recipe Name"}</label>
</div>
</FormModal>
</>}
}
#[function_component]
fn RecipeViewerInner(props: &RecipeViewerInnerProps) -> HtmlResult {
let recipe = use_future(|| fetch_recipe(props.token.clone(), props.household, props.id))?;
let recipe_render = use_state(|| 0u64);
let recipe = use_future_with_deps(
|_| fetch_recipe(props.token.clone(), props.household, props.id),
*recipe_render,
)?;
let update = Callback::from(move |_| {
recipe_render.set((*recipe_render).wrapping_add(1));
});
Ok(match &*recipe {
Ok(r) => html! {<>
<h1>{&r.name}</h1>
<EditName
token={props.token.clone()}
id={props.id}
household={props.household}
name={r.name.clone()}
{update}
/>
<hr />
<div class="text-start">
<h2>{"Ingredients"}</h2>

View file

@ -224,6 +224,8 @@ pub(crate) fn router(api_allowed: Option<HeaderValue>) -> Router<AppState> {
)
.route(
"/household/:house_id/recipe/:recipe_id",
get(recipe::fetch_recipe).layer(mk_service(vec![Method::GET])),
get(recipe::fetch_recipe)
.patch(recipe::edit_name)
.layer(mk_service(vec![Method::GET, Method::PATCH])),
)
}

View file

@ -1,8 +1,11 @@
use api::{
CreateRecipeRequest, CreateRecipeResponse, IngredientInfo, ListRecipesResponse, RecipeInfo,
CreateRecipeRequest, CreateRecipeResponse, EmptyResponse, IngredientInfo, ListRecipesResponse,
RecipeInfo, RecipeRenameRequest,
};
use axum::{
extract::{Path, State},
async_trait,
extract::{FromRef, FromRequestParts, Path, State},
http::request::Parts,
Json,
};
use sea_orm::{prelude::*, ActiveValue, TransactionTrait};
@ -83,23 +86,46 @@ pub(super) async fn list_recipes(
.into())
}
#[derive(serde::Deserialize)]
pub(super) struct RecipeId {
recipe_id: i64,
pub(super) struct RecipeExtractor(recipe::Model);
#[async_trait]
impl<S> FromRequestParts<S> for RecipeExtractor
where
S: Send + Sync,
AppState: FromRef<S>,
{
type Rejection = RouteError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let State(app_state): State<AppState> = State::from_request_parts(parts, state)
.await
.expect("No state");
#[derive(serde::Deserialize)]
pub(super) struct RecipeId {
recipe_id: i64,
}
let household = AuthorizedHousehold::from_request_parts(parts, state).await?;
let Path(recipe): Path<RecipeId> = Path::from_request_parts(parts, state).await?;
match household
.0
.find_related(Recipe)
.filter(recipe::Column::Id.eq(recipe.recipe_id))
.one(&app_state.db)
.await?
{
None => Err(RouteError::RessourceNotFound),
Some(r) => Ok(Self(r)),
}
}
}
pub(super) async fn fetch_recipe(
AuthorizedHousehold(household): AuthorizedHousehold,
State(state): State<AppState>,
Path(RecipeId { recipe_id }): Path<RecipeId>,
RecipeExtractor(recipe): RecipeExtractor,
) -> JsonResult<RecipeInfo> {
let Some(recipe) = household
.find_related(Recipe)
.filter(recipe::Column::Id.eq(recipe_id))
.one(&state.db).await? else {
return Err(RouteError::RessourceNotFound);
};
let steps = recipe
.find_related(RecipeSteps)
.all(&state.db)
@ -134,3 +160,19 @@ pub(super) async fn fetch_recipe(
}
.into())
}
pub(super) async fn edit_name(
State(state): State<AppState>,
RecipeExtractor(recipe): RecipeExtractor,
Json(req): Json<RecipeRenameRequest>,
) -> JsonResult<EmptyResponse> {
let active_model = recipe::ActiveModel {
name: ActiveValue::Set(req.name),
id: ActiveValue::Set(recipe.id),
..Default::default()
};
active_model.update(&state.db).await?;
Ok(EmptyResponse {}.into())
}