app,server: Allow to edit recipe ingredients
This commit is contained in:
parent
47b547caf4
commit
6004520fb9
6 changed files with 307 additions and 35 deletions
|
|
@ -113,3 +113,8 @@ pub struct RecipeInfo {
|
||||||
pub struct RecipeRenameRequest {
|
pub struct RecipeRenameRequest {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct RecipeIngredientEditRequest {
|
||||||
|
pub amount: f64,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,14 @@ pub mod bs {
|
||||||
#[wasm_bindgen(static_method_of = Modal, js_name = "getInstance")]
|
#[wasm_bindgen(static_method_of = Modal, js_name = "getInstance")]
|
||||||
pub fn get_instance(selector: &str) -> Modal;
|
pub fn get_instance(selector: &str) -> Modal;
|
||||||
|
|
||||||
|
#[wasm_bindgen(static_method_of = Modal, js_name = "getOrCreateInstance")]
|
||||||
|
pub fn get_or_create_instance(selector: &str) -> Modal;
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn hide(this: &Modal);
|
pub fn hide(this: &Modal);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn show(this: &Modal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use api::{RecipeInfo, RecipeRenameRequest};
|
use api::{RecipeInfo, RecipeIngredientEditRequest, RecipeRenameRequest};
|
||||||
|
use itertools::Itertools;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::HtmlInputElement;
|
use web_sys::HtmlInputElement;
|
||||||
|
|
@ -10,7 +11,7 @@ use yew_router::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api,
|
api,
|
||||||
bootstrap::{FormModal, ModalToggleButton, bs},
|
bootstrap::{bs, ConfirmDangerModal, FormModal, ModalToggleButton},
|
||||||
RegaladeGlobalState, Route,
|
RegaladeGlobalState, Route,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -45,7 +46,7 @@ fn RecipeListInner(props: &RecipeListProps) -> HtmlResult {
|
||||||
Ok(l) => html! {
|
Ok(l) => html! {
|
||||||
<div class="container text-center">
|
<div class="container text-center">
|
||||||
<div class="row row-cols-2 row-cols-sm-2 row-cols-md-4 g-2 mb-2">
|
<div class="row row-cols-2 row-cols-sm-2 row-cols-md-4 g-2 mb-2">
|
||||||
{for l.recipes.iter().map(|(id, name)| html!{
|
{for l.recipes.iter().sorted_by_key(|(_, name)| name).map(|(id, name)| html!{
|
||||||
<div class="col" key={*id}>
|
<div class="col" key={*id}>
|
||||||
<div class="p-3 border rounded border-light-subtle h-100">
|
<div class="p-3 border rounded border-light-subtle h-100">
|
||||||
<Link<Route>
|
<Link<Route>
|
||||||
|
|
@ -191,7 +192,7 @@ fn EditName(props: &EditNameProps) -> Html {
|
||||||
|
|
||||||
err.set(None);
|
err.set(None);
|
||||||
update.emit(());
|
update.emit(());
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
err.set(Some(format!("Could not edit name: {e}")));
|
err.set(Some(format!("Could not edit name: {e}")));
|
||||||
}
|
}
|
||||||
|
|
@ -230,6 +231,146 @@ fn EditName(props: &EditNameProps) -> Html {
|
||||||
</>}
|
</>}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Properties)]
|
||||||
|
struct EditIngredientProps {
|
||||||
|
token: String,
|
||||||
|
household: Uuid,
|
||||||
|
recipe: i64,
|
||||||
|
ingredient: i64,
|
||||||
|
amount: f64,
|
||||||
|
update: Callback<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_edit_ingredient_recipe(
|
||||||
|
token: String,
|
||||||
|
household: Uuid,
|
||||||
|
recipe: i64,
|
||||||
|
ingredient: i64,
|
||||||
|
amount: f64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let rsp = gloo_net::http::Request::patch(api!(
|
||||||
|
"household/{household}/recipe/{recipe}/ingredients/{ingredient}"
|
||||||
|
))
|
||||||
|
.json(&RecipeIngredientEditRequest { amount })?
|
||||||
|
.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 EditIngredient(props: &EditIngredientProps) -> Html {
|
||||||
|
let amount = use_state(|| props.amount);
|
||||||
|
|
||||||
|
let am = amount.clone();
|
||||||
|
let onchange = Callback::from(move |e: Event| {
|
||||||
|
let Some(target) = e.target() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(target) = target.dyn_into::<HtmlInputElement>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
am.set(target.value().parse().unwrap());
|
||||||
|
});
|
||||||
|
|
||||||
|
let error = use_state(|| None::<String>);
|
||||||
|
|
||||||
|
let modal_id = format!("rcpEditIg{}", props.ingredient);
|
||||||
|
|
||||||
|
let am = amount.clone();
|
||||||
|
let err = error.clone();
|
||||||
|
let token = props.token.clone();
|
||||||
|
let household = props.household;
|
||||||
|
let recipe = props.recipe;
|
||||||
|
let ingredient = props.ingredient;
|
||||||
|
let update = props.update.clone();
|
||||||
|
let mid = modal_id.clone();
|
||||||
|
let on_submit = Callback::from(move |_| {
|
||||||
|
let future = do_edit_ingredient_recipe(token.clone(), household, recipe, ingredient, *am);
|
||||||
|
|
||||||
|
let err = err.clone();
|
||||||
|
let update = update.clone();
|
||||||
|
let modal_selector = format!("#{mid}");
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
match future.await {
|
||||||
|
Ok(_) => {
|
||||||
|
let modal = bs::Modal::get_instance(&modal_selector);
|
||||||
|
modal.hide();
|
||||||
|
|
||||||
|
err.set(None);
|
||||||
|
update.emit(());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
err.set(Some(format!("Could not edit ingredient: {e}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let input_id = format!("{modal_id}Inp");
|
||||||
|
|
||||||
|
html! {<>
|
||||||
|
<FormModal
|
||||||
|
id={modal_id.clone()}
|
||||||
|
fade=true
|
||||||
|
centered=true
|
||||||
|
submit_label="Edit"
|
||||||
|
title="Edit Ingredient"
|
||||||
|
{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={input_id.clone()}
|
||||||
|
placeholder="Ingredient Amount"
|
||||||
|
value={amount.to_string()}
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
{onchange}
|
||||||
|
/>
|
||||||
|
<label for={input_id}>{"Ingredient Amount"}</label>
|
||||||
|
</div>
|
||||||
|
</FormModal>
|
||||||
|
<ModalToggleButton {modal_id}>
|
||||||
|
<i class="bi-pencil-fill" />
|
||||||
|
</ModalToggleButton>
|
||||||
|
</>}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_delete_ig(
|
||||||
|
token: String,
|
||||||
|
household: Uuid,
|
||||||
|
recipe: i64,
|
||||||
|
ingredient: i64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let rsp = gloo_net::http::Request::delete(api!(
|
||||||
|
"household/{household}/recipe/{recipe}/ingredients/{ingredient}"
|
||||||
|
))
|
||||||
|
.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]
|
#[function_component]
|
||||||
fn RecipeViewerInner(props: &RecipeViewerInnerProps) -> HtmlResult {
|
fn RecipeViewerInner(props: &RecipeViewerInnerProps) -> HtmlResult {
|
||||||
let recipe_render = use_state(|| 0u64);
|
let recipe_render = use_state(|| 0u64);
|
||||||
|
|
@ -242,25 +383,85 @@ fn RecipeViewerInner(props: &RecipeViewerInnerProps) -> HtmlResult {
|
||||||
recipe_render.set((*recipe_render).wrapping_add(1));
|
recipe_render.set((*recipe_render).wrapping_add(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let error = use_state(|| None::<String>);
|
||||||
|
|
||||||
|
let mk_del_ig = |&id| {
|
||||||
|
let update = update.clone();
|
||||||
|
let token = props.token.clone();
|
||||||
|
let household = props.household;
|
||||||
|
let recipe = props.id;
|
||||||
|
let err = error.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
let update = update.clone();
|
||||||
|
let future = do_delete_ig(token.clone(), household, recipe, id);
|
||||||
|
let err = err.clone();
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
match future.await {
|
||||||
|
Err(e) => {
|
||||||
|
err.set(Some(format!("Could not delete ingredient: {e}")));
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
update.emit(());
|
||||||
|
err.set(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
Ok(match &*recipe {
|
Ok(match &*recipe {
|
||||||
Ok(r) => html! {<>
|
Ok(r) => html! {<>
|
||||||
<h1>{&r.name}</h1>
|
<h1>{&r.name}</h1>
|
||||||
|
if let Some(e) = &*error {
|
||||||
|
<div class={classes!("alert", "alert-danger")} role="alert">
|
||||||
|
{e}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<EditName
|
<EditName
|
||||||
token={props.token.clone()}
|
token={props.token.clone()}
|
||||||
id={props.id}
|
id={props.id}
|
||||||
household={props.household}
|
household={props.household}
|
||||||
name={r.name.clone()}
|
name={r.name.clone()}
|
||||||
{update}
|
update={update.clone()}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="text-start">
|
<div class="text-start">
|
||||||
<h2>{"Ingredients"}</h2>
|
<h2>{"Ingredients"}</h2>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
{for r.ingredients.iter().map(|(id, info, amount)| html!{
|
{for r.ingredients.iter().map(|(id, info, amount)| {
|
||||||
<li key={*id} class="list-group-item">
|
let delete_modal_id = format!("rcpRmIg{id}");
|
||||||
|
html!{
|
||||||
|
<li
|
||||||
|
key={*id}
|
||||||
|
class="list-group-item d-flex justify-content-between align-items-center"
|
||||||
|
>
|
||||||
{format!("{amount}{} {}", info.unit.as_deref().unwrap_or(""), info.name)}
|
{format!("{amount}{} {}", info.unit.as_deref().unwrap_or(""), info.name)}
|
||||||
|
<div>
|
||||||
|
<ConfirmDangerModal
|
||||||
|
id={delete_modal_id.clone()}
|
||||||
|
title="Remove ingredient"
|
||||||
|
centered=true
|
||||||
|
on_confirm={mk_del_ig(id)}
|
||||||
|
>
|
||||||
|
{format!("Are you sure you to delete '{}'", info.name)}
|
||||||
|
</ConfirmDangerModal>
|
||||||
|
<ModalToggleButton
|
||||||
|
modal_id={delete_modal_id}
|
||||||
|
classes={classes!("btn", "btn-danger", "me-1")}
|
||||||
|
>
|
||||||
|
<i class="bi-trash3" />
|
||||||
|
</ModalToggleButton>
|
||||||
|
<EditIngredient
|
||||||
|
token={props.token.clone()}
|
||||||
|
recipe={props.id}
|
||||||
|
ingredient={*id}
|
||||||
|
household={props.household}
|
||||||
|
amount={*amount}
|
||||||
|
update={update.clone()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
})}
|
}})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
async_trait,
|
||||||
|
extract::{FromRef, FromRequestParts, Path, State},
|
||||||
|
http::request::Parts,
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -10,7 +12,7 @@ use api::{
|
||||||
use sea_orm::{prelude::*, ActiveValue};
|
use sea_orm::{prelude::*, ActiveValue};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::{household::AuthorizedHousehold, AppState, JsonResult};
|
use super::{household::AuthorizedHousehold, AppState, JsonResult, RouteError};
|
||||||
use crate::entity::{ingredient, prelude::*};
|
use crate::entity::{ingredient, prelude::*};
|
||||||
|
|
||||||
pub(super) async fn create_ingredient(
|
pub(super) async fn create_ingredient(
|
||||||
|
|
@ -52,37 +54,57 @@ pub(super) async fn list_ingredients(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
pub(super) struct IngredientExtractor(pub(super) ingredient::Model);
|
||||||
pub struct IngredientId {
|
|
||||||
iid: i64,
|
#[async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for IngredientExtractor
|
||||||
|
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(Deserialize)]
|
||||||
|
pub struct IngredientId {
|
||||||
|
iid: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
let household = AuthorizedHousehold::from_request_parts(parts, state).await?;
|
||||||
|
let Path(ingredient_id): Path<IngredientId> =
|
||||||
|
Path::from_request_parts(parts, state).await?;
|
||||||
|
|
||||||
|
match household
|
||||||
|
.0
|
||||||
|
.find_related(Ingredient)
|
||||||
|
.filter(ingredient::Column::Id.eq(ingredient_id.iid))
|
||||||
|
.one(&app_state.db)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
None => Err(RouteError::RessourceNotFound),
|
||||||
|
Some(r) => Ok(Self(r)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn remove_ingredient(
|
pub(super) async fn remove_ingredient(
|
||||||
AuthorizedHousehold(household): AuthorizedHousehold,
|
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(IngredientId { iid }): Path<IngredientId>,
|
IngredientExtractor(ingredient): IngredientExtractor,
|
||||||
) -> JsonResult<EmptyResponse> {
|
) -> JsonResult<EmptyResponse> {
|
||||||
Ingredient::delete_by_id(iid)
|
ingredient.delete(&state.db).await?;
|
||||||
.filter(ingredient::Column::Household.eq(household.id))
|
|
||||||
.exec(&state.db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(EmptyResponse {}))
|
Ok(Json(EmptyResponse {}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn edit_ingredient(
|
pub(super) async fn edit_ingredient(
|
||||||
AuthorizedHousehold(household): AuthorizedHousehold,
|
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(IngredientId { iid }): Path<IngredientId>,
|
IngredientExtractor(ingredient): IngredientExtractor,
|
||||||
Json(request): Json<EditIngredientRequest>,
|
Json(request): Json<EditIngredientRequest>,
|
||||||
) -> JsonResult<EmptyResponse> {
|
) -> JsonResult<EmptyResponse> {
|
||||||
let Some(ingredient) = Ingredient::find_by_id(iid)
|
|
||||||
.filter(ingredient::Column::Household.eq(household.id))
|
|
||||||
.one(&state.db)
|
|
||||||
.await? else {
|
|
||||||
return Err(super::RouteError::RessourceNotFound);
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut ingredient: ingredient::ActiveModel = ingredient.into();
|
let mut ingredient: ingredient::ActiveModel = ingredient.into();
|
||||||
|
|
||||||
if let Some(name) = request.name {
|
if let Some(name) = request.name {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use axum::{
|
||||||
HeaderValue, Method, StatusCode,
|
HeaderValue, Method, StatusCode,
|
||||||
},
|
},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::{delete, get, post, put},
|
routing::{delete, get, patch, post, put},
|
||||||
Json, Router, TypedHeader,
|
Json, Router, TypedHeader,
|
||||||
};
|
};
|
||||||
use jwt_simple::prelude::*;
|
use jwt_simple::prelude::*;
|
||||||
|
|
@ -228,4 +228,10 @@ pub(crate) fn router(api_allowed: Option<HeaderValue>) -> Router<AppState> {
|
||||||
.patch(recipe::edit_name)
|
.patch(recipe::edit_name)
|
||||||
.layer(mk_service(vec![Method::GET, Method::PATCH])),
|
.layer(mk_service(vec![Method::GET, Method::PATCH])),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/household/:house_id/recipe/:recipe_id/ingredients/:iid",
|
||||||
|
patch(recipe::edit_ig_amount)
|
||||||
|
.delete(recipe::delete_ig)
|
||||||
|
.layer(mk_service(vec![Method::PATCH, Method::DELETE])),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use api::{
|
use api::{
|
||||||
CreateRecipeRequest, CreateRecipeResponse, EmptyResponse, IngredientInfo, ListRecipesResponse,
|
CreateRecipeRequest, CreateRecipeResponse, EmptyResponse, IngredientInfo, ListRecipesResponse,
|
||||||
RecipeInfo, RecipeRenameRequest,
|
RecipeInfo, RecipeIngredientEditRequest, RecipeRenameRequest,
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
async_trait,
|
async_trait,
|
||||||
|
|
@ -12,7 +12,10 @@ use sea_orm::{prelude::*, ActiveValue, TransactionTrait};
|
||||||
|
|
||||||
use crate::entity::{ingredient, prelude::*, recipe, recipe_ingerdients, recipe_steps};
|
use crate::entity::{ingredient, prelude::*, recipe, recipe_ingerdients, recipe_steps};
|
||||||
|
|
||||||
use super::{household::AuthorizedHousehold, AppState, JsonResult, RouteError};
|
use super::{
|
||||||
|
household::AuthorizedHousehold, ingredients::IngredientExtractor, AppState, JsonResult,
|
||||||
|
RouteError,
|
||||||
|
};
|
||||||
|
|
||||||
pub(super) async fn create_recipe(
|
pub(super) async fn create_recipe(
|
||||||
AuthorizedHousehold(household): AuthorizedHousehold,
|
AuthorizedHousehold(household): AuthorizedHousehold,
|
||||||
|
|
@ -176,3 +179,32 @@ pub(super) async fn edit_name(
|
||||||
|
|
||||||
Ok(EmptyResponse {}.into())
|
Ok(EmptyResponse {}.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) async fn edit_ig_amount(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
RecipeExtractor(recipe): RecipeExtractor,
|
||||||
|
IngredientExtractor(ingredient): IngredientExtractor,
|
||||||
|
Json(req): Json<RecipeIngredientEditRequest>,
|
||||||
|
) -> JsonResult<EmptyResponse> {
|
||||||
|
let active_model = recipe_ingerdients::ActiveModel {
|
||||||
|
recipe_id: ActiveValue::Set(recipe.id),
|
||||||
|
ingredient_id: ActiveValue::Set(ingredient.id),
|
||||||
|
amount: ActiveValue::Set(req.amount),
|
||||||
|
};
|
||||||
|
|
||||||
|
active_model.update(&state.db).await?;
|
||||||
|
|
||||||
|
Ok(EmptyResponse {}.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn delete_ig(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
RecipeExtractor(recipe): RecipeExtractor,
|
||||||
|
IngredientExtractor(ingredient): IngredientExtractor,
|
||||||
|
) -> JsonResult<EmptyResponse> {
|
||||||
|
RecipeIngerdients::delete_by_id((recipe.id, ingredient.id))
|
||||||
|
.exec(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(EmptyResponse {}.into())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue