From 66a558d1e6dee2faaef24cefeaab6d6c48b805ca Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Thu, 5 Jan 2023 17:56:15 -0500 Subject: [PATCH] New UI for ingredient category management --- kitchen/sqlx-data.json | 20 +- .../save_category_mappings_for_user.sql | 4 +- recipes/src/lib.rs | 6 +- recipes/src/parse.rs | 2 +- recipes/src/test.rs | 96 +++------ web/src/api.rs | 97 +++++---- web/src/app_state.rs | 37 +++- web/src/components/categories.rs | 201 ++++++++++++------ web/src/components/shopping_list.rs | 16 +- web/src/js_lib.rs | 19 +- 10 files changed, 263 insertions(+), 235 deletions(-) diff --git a/kitchen/sqlx-data.json b/kitchen/sqlx-data.json index c481d79..4cc79fd 100644 --- a/kitchen/sqlx-data.json +++ b/kitchen/sqlx-data.json @@ -112,6 +112,16 @@ }, "query": "select plan_date as \"plan_date: NaiveDate\", recipe_id, count\nfrom plan_recipes\nwhere\n user_id = ?\n and date(plan_date) > ?\norder by user_id, plan_date" }, + "2582522f8ca9f12eccc70a3b339d9030aee0f52e62d6674cfd3862de2a68a177": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 3 + } + }, + "query": "insert into category_mappings\n (user_id, ingredient_name, category_name)\n values (?, ?, ?)\n on conflict (user_id, ingredient_name)\n do update set category_name=excluded.category_name\n" + }, "37f382be1b53efd2f79a0d59ae6a8717f88a86908a7a4128d5ed7339147ca59d": { "describe": { "columns": [ @@ -342,16 +352,6 @@ }, "query": "select category_text from categories where user_id = ?" }, - "d73e4bfb1fbee6d2dd35fc787141a1c2909a77cf4b19950671f87e694289c242": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 3 - } - }, - "query": "insert into category_mappings\n (user_id, ingredient_name, category_name)\n values (?, ?, ?)" - }, "d84685a82585c5e4ae72c86ba1fe6e4a7241c4c3c9e948213e5849d956132bad": { "describe": { "columns": [], diff --git a/kitchen/src/web/storage/save_category_mappings_for_user.sql b/kitchen/src/web/storage/save_category_mappings_for_user.sql index 870ba75..3b5c19d 100644 --- a/kitchen/src/web/storage/save_category_mappings_for_user.sql +++ b/kitchen/src/web/storage/save_category_mappings_for_user.sql @@ -1,3 +1,5 @@ insert into category_mappings (user_id, ingredient_name, category_name) - values (?, ?, ?) \ No newline at end of file + values (?, ?, ?) + on conflict (user_id, ingredient_name) + do update set category_name=excluded.category_name diff --git a/recipes/src/lib.rs b/recipes/src/lib.rs index 286fd83..4d5cdcf 100644 --- a/recipes/src/lib.rs +++ b/recipes/src/lib.rs @@ -231,17 +231,15 @@ pub struct Ingredient { pub name: String, pub form: Option, pub amt: Measure, - pub category: String, } impl Ingredient { - pub fn new>(name: S, form: Option, amt: Measure, category: S) -> Self { + pub fn new>(name: S, form: Option, amt: Measure) -> Self { Self { id: None, name: name.into(), form, amt, - category: category.into(), } } @@ -250,14 +248,12 @@ impl Ingredient { name: S, form: Option, amt: Measure, - category: S, ) -> Self { Self { id: Some(id), name: name.into(), form, amt, - category: category.into(), } } diff --git a/recipes/src/parse.rs b/recipes/src/parse.rs index d4a934e..a14103c 100644 --- a/recipes/src/parse.rs +++ b/recipes/src/parse.rs @@ -447,7 +447,7 @@ make_fn!( name => ingredient_name, modifier => optional!(ingredient_modifier), _ => optional!(ws), - (Ingredient::new(name, modifier.map(|s| s.to_owned()), measure, String::new())) + (Ingredient::new(name, modifier.map(|s| s.to_owned()), measure)) ) ); diff --git a/recipes/src/test.rs b/recipes/src/test.rs index 29cfb33..ff5ebd6 100644 --- a/recipes/src/test.rs +++ b/recipes/src/test.rs @@ -89,108 +89,80 @@ fn test_volume_normalize() { fn test_ingredient_display() { let cases = vec![ ( - Ingredient::new( - "onion", - Some("chopped".to_owned()), - Measure::cup(1.into()), - "Produce", - ), + Ingredient::new("onion", Some("chopped".to_owned()), Measure::cup(1.into())), "1 cup onion (chopped)", ), ( - Ingredient::new( - "onion", - Some("chopped".to_owned()), - Measure::cup(2.into()), - "Produce", - ), + Ingredient::new("onion", Some("chopped".to_owned()), Measure::cup(2.into())), "2 cups onion (chopped)", ), ( - Ingredient::new( - "onion", - Some("chopped".to_owned()), - Measure::tbsp(1.into()), - "Produce", - ), + Ingredient::new("onion", Some("chopped".to_owned()), Measure::tbsp(1.into())), "1 tbsp onion (chopped)", ), ( - Ingredient::new( - "onion", - Some("chopped".to_owned()), - Measure::tbsp(2.into()), - "Produce", - ), + Ingredient::new("onion", Some("chopped".to_owned()), Measure::tbsp(2.into())), "2 tbsps onion (chopped)", ), ( - Ingredient::new("soy sauce", None, Measure::floz(1.into()), "Produce"), + Ingredient::new("soy sauce", None, Measure::floz(1.into())), "1 floz soy sauce", ), ( - Ingredient::new("soy sauce", None, Measure::floz(2.into()), "Produce"), + Ingredient::new("soy sauce", None, Measure::floz(2.into())), "2 floz soy sauce", ), ( - Ingredient::new("soy sauce", None, Measure::qrt(1.into()), "Produce"), + Ingredient::new("soy sauce", None, Measure::qrt(1.into())), "1 qrt soy sauce", ), ( - Ingredient::new("soy sauce", None, Measure::qrt(2.into()), "Produce"), + Ingredient::new("soy sauce", None, Measure::qrt(2.into())), "2 qrts soy sauce", ), ( - Ingredient::new("soy sauce", None, Measure::pint(1.into()), "Produce"), + Ingredient::new("soy sauce", None, Measure::pint(1.into())), "1 pint soy sauce", ), ( - Ingredient::new("soy sauce", None, Measure::pint(2.into()), "Produce"), + Ingredient::new("soy sauce", None, Measure::pint(2.into())), "2 pints soy sauce", ), ( - Ingredient::new("soy sauce", None, Measure::gal(1.into()), "Produce"), + Ingredient::new("soy sauce", None, Measure::gal(1.into())), "1 gal soy sauce", ), ( - Ingredient::new("soy sauce", None, Measure::gal(2.into()), "Produce"), + Ingredient::new("soy sauce", None, Measure::gal(2.into())), "2 gals soy sauce", ), ( - Ingredient::new("soy sauce", None, Measure::ml(1.into()), "Produce"), + Ingredient::new("soy sauce", None, Measure::ml(1.into())), "1 ml soy sauce", ), ( - Ingredient::new("soy sauce", None, Measure::ml(2.into()), "Produce"), + Ingredient::new("soy sauce", None, Measure::ml(2.into())), "2 ml soy sauce", ), ( - Ingredient::new("soy sauce", None, Measure::ltr(1.into()), "Produce"), + Ingredient::new("soy sauce", None, Measure::ltr(1.into())), "1 ltr soy sauce", ), ( - Ingredient::new("soy sauce", None, Measure::ltr(2.into()), "Produce"), + Ingredient::new("soy sauce", None, Measure::ltr(2.into())), "2 ltr soy sauce", ), + (Ingredient::new("apple", None, Measure::count(1)), "1 apple"), ( - Ingredient::new("apple", None, Measure::count(1), "Produce"), - "1 apple", - ), - ( - Ingredient::new("salt", None, Measure::gram(1.into()), "Produce"), + Ingredient::new("salt", None, Measure::gram(1.into())), "1 gram salt", ), ( - Ingredient::new("salt", None, Measure::gram(2.into()), "Produce"), + Ingredient::new("salt", None, Measure::gram(2.into())), "2 grams salt", ), ( - Ingredient::new( - "onion", - Some("minced".to_owned()), - Measure::cup(1.into()), - "Produce", - ), + Ingredient::new("onion", Some("minced".to_owned()), Measure::cup(1.into())), "1 cup onion (minced)", ), ( @@ -198,7 +170,6 @@ fn test_ingredient_display() { "pepper", Some("ground".to_owned()), Measure::tsp(Ratio::new(1, 2).into()), - "Produce", ), "1/2 tsp pepper (ground)", ), @@ -207,35 +178,19 @@ fn test_ingredient_display() { "pepper", Some("ground".to_owned()), Measure::tsp(Ratio::new(3, 2).into()), - "Produce", ), "1 1/2 tsps pepper (ground)", ), ( - Ingredient::new( - "apple", - Some("sliced".to_owned()), - Measure::count(1), - "Produce", - ), + Ingredient::new("apple", Some("sliced".to_owned()), Measure::count(1)), "1 apple (sliced)", ), ( - Ingredient::new( - "potato", - Some("mashed".to_owned()), - Measure::count(1), - "Produce", - ), + Ingredient::new("potato", Some("mashed".to_owned()), Measure::count(1)), "1 potato (mashed)", ), ( - Ingredient::new( - "potato", - Some("blanched".to_owned()), - Measure::count(1), - "Produce", - ), + Ingredient::new("potato", Some("blanched".to_owned()), Measure::count(1)), "1 potato (blanched)", ), ]; @@ -312,7 +267,6 @@ fn test_ingredient_parse() { "green bell pepper", Some("chopped".to_owned()), Count(Quantity::Whole(1)), - "", ), ), ] { @@ -332,18 +286,16 @@ fn test_ingredient_list_parse() { "flour", None, Volume(Cup(Quantity::Whole(1))), - "", )], ), ( "1 cup flour \n1/2 tsp butter ", vec![ - Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""), + Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1)))), Ingredient::new( "butter", None, Volume(Tsp(Quantity::Frac(Ratio::new(1, 2)))), - "", ), ], ), diff --git a/web/src/api.rs b/web/src/api.rs index 3145067..7728354 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -26,35 +26,6 @@ use web_sys::Storage; use crate::{app_state::AppState, js_lib}; -// FIXME(jwall): We should be able to delete this now. -#[instrument] -fn filter_recipes( - recipe_entries: &Option>, -) -> Result<(Option, Option>), String> { - match recipe_entries { - Some(parsed) => { - let mut staples = None; - let mut parsed_map = BTreeMap::new(); - for r in parsed { - let recipe = match parse::as_recipe(&r.recipe_text()) { - Ok(r) => r, - Err(e) => { - error!("Error parsing recipe {}", e); - continue; - } - }; - if recipe.title == "Staples" { - staples = Some(recipe); - } else { - parsed_map.insert(r.recipe_id().to_owned(), recipe); - } - } - Ok((staples, Some(parsed_map))) - } - None => Ok((None, None)), - } -} - #[derive(Debug)] pub struct Error(String); @@ -104,6 +75,10 @@ fn recipe_key(id: S) -> String { format!("recipe:{}", id) } +fn category_key(id: S) -> String { + format!("category:{}", id) +} + fn token68(user: String, pass: String) -> String { base64::encode(format!("{}:{}", user, pass)) } @@ -145,22 +120,39 @@ impl LocalStore { } /// Gets categories from local storage. - pub fn get_categories(&self) -> Option { - self.store - .get("categories") - .expect("Failed go get categories") + pub fn get_categories(&self) -> Option> { + let mut mappings = Vec::new(); + for k in self.get_category_keys() { + if let Some(mut cat_map) = self + .store + .get(&k) + .expect(&format!("Failed to get category key {}", k)) + .map(|v| { + from_str::>(&v) + .expect(&format!("Failed to parse category key {}", k)) + }) + { + mappings.extend(cat_map.drain(0..)); + } + } + if mappings.is_empty() { + None + } else { + Some(mappings) + } } /// Set the categories to the given string. - pub fn set_categories(&self, categories: Option<&String>) { - if let Some(c) = categories { - self.store - .set("categories", c) - .expect("Failed to set categories"); - } else { - self.store - .delete("categories") - .expect("Failed to delete categories") + pub fn set_categories(&self, mappings: Option<&Vec<(String, String)>>) { + if let Some(mappings) = mappings { + for (i, cat) in mappings.iter() { + self.store + .set( + &category_key(i), + &to_string(&(i, cat)).expect("Failed to serialize category mapping"), + ) + .expect("Failed to store category mapping"); + } } } @@ -174,6 +166,12 @@ impl LocalStore { keys } + fn get_category_keys(&self) -> impl Iterator { + self.get_storage_keys() + .into_iter() + .filter(|k| k.starts_with("category:")) + } + fn get_recipe_keys(&self) -> impl Iterator { self.get_storage_keys() .into_iter() @@ -392,10 +390,11 @@ impl HttpStore { } return None; } + //#[instrument] - pub async fn fetch_categories(&self) -> Result, Error> { + pub async fn fetch_categories(&self) -> Result>, Error> { let mut path = self.v2_path(); - path.push_str("/categories"); + path.push_str("/category_map"); let resp = match reqwasm::http::Request::get(&path).send().await { Ok(resp) => resp, Err(reqwasm::Error::JsError(err)) => { @@ -413,7 +412,11 @@ impl HttpStore { Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back!"); - let resp = resp.json::().await?.as_success().unwrap(); + let resp = resp + .json::() + .await? + .as_success() + .unwrap(); Ok(Some(resp)) } } @@ -506,9 +509,9 @@ impl HttpStore { } #[instrument(skip(categories))] - pub async fn store_categories(&self, categories: String) -> Result<(), Error> { + pub async fn store_categories(&self, categories: &Vec<(String, String)>) -> Result<(), Error> { let mut path = self.v2_path(); - path.push_str("/categories"); + path.push_str("/category_map"); let resp = reqwasm::http::Request::post(&path) .body(to_string(&categories).expect("Unable to encode categories as json")) .header("content-type", "application/json") diff --git a/web/src/app_state.rs b/web/src/app_state.rs index 586ac13..8a3a7dc 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -32,7 +32,7 @@ pub struct AppState { pub extras: Vec<(String, String)>, pub staples: Option, pub recipes: BTreeMap, - pub category_map: String, + pub category_map: BTreeMap, pub filtered_ingredients: BTreeSet, pub modified_amts: BTreeMap, pub auth: Option, @@ -45,7 +45,7 @@ impl AppState { extras: Vec::new(), staples: None, recipes: BTreeMap::new(), - category_map: String::new(), + category_map: BTreeMap::new(), filtered_ingredients: BTreeSet::new(), modified_amts: BTreeMap::new(), auth: None, @@ -61,7 +61,8 @@ pub enum Message { UpdateExtra(usize, String, String), SaveRecipe(RecipeEntry), SetRecipe(String, Recipe), - SetCategoryMap(String), + SetCategoryMap(BTreeMap), + UpdateCategory(String, String), ResetInventory, AddFilteredIngredient(IngredientKey), UpdateAmt(IngredientKey, String), @@ -94,6 +95,9 @@ impl Debug for Message { f.debug_tuple("SetRecipe").field(arg0).field(arg1).finish() } Self::SetCategoryMap(arg0) => f.debug_tuple("SetCategoryMap").field(arg0).finish(), + Self::UpdateCategory(i, c) => { + f.debug_tuple("UpdateCategory").field(i).field(c).finish() + } Self::ResetInventory => write!(f, "ResetInventory"), Self::AddFilteredIngredient(arg0) => { f.debug_tuple("AddFilteredIngredient").field(arg0).finish() @@ -197,10 +201,11 @@ impl StateMachine { } info!("Synchronizing categories"); match store.fetch_categories().await { - Ok(Some(categories_content)) => { + Ok(Some(mut categories_content)) => { debug!(categories=?categories_content); local_store.set_categories(Some(&categories_content)); - state.category_map = categories_content; + let category_map = BTreeMap::from_iter(categories_content.drain(0..)); + state.category_map = category_map; } Ok(None) => { warn!("There is no category file"); @@ -308,12 +313,26 @@ impl MessageMapper for StateMachine { } }); } - Message::SetCategoryMap(category_text) => { - original_copy.category_map = category_text.clone(); - self.local_store.set_categories(Some(&category_text)); + Message::SetCategoryMap(map) => { + original_copy.category_map = map.clone(); + let list = map.iter().map(|(i, c)| (i.clone(), c.clone())).collect(); + self.local_store.set_categories(Some(&list)); let store = self.store.clone(); spawn_local_scoped(cx, async move { - if let Err(e) = store.store_categories(category_text).await { + if let Err(e) = store.store_categories(&list).await { + error!(?e, "Failed to save categories"); + } + }); + } + Message::UpdateCategory(ingredient, category) => { + self.local_store + .set_categories(Some(&vec![(ingredient.clone(), category.clone())])); + original_copy + .category_map + .insert(ingredient.clone(), category.clone()); + let store = self.store.clone(); + spawn_local_scoped(cx, async move { + if let Err(e) = store.store_categories(&vec![(ingredient, category)]).await { error!(?e, "Failed to save categories"); } }); diff --git a/web/src/components/categories.rs b/web/src/components/categories.rs index b2345c9..0a1bee2 100644 --- a/web/src/components/categories.rs +++ b/web/src/components/categories.rs @@ -11,91 +11,154 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use crate::{ - app_state::{Message, StateHandler}, - js_lib::get_element_by_id, -}; -use sycamore::{futures::spawn_local_scoped, prelude::*}; -use tracing::{debug, error, instrument}; -use web_sys::HtmlDialogElement; +use std::collections::{BTreeMap, BTreeSet}; -use recipes::parse; +use crate::app_state::{Message, StateHandler}; +use sycamore::prelude::*; +use tracing::instrument; -fn get_error_dialog() -> HtmlDialogElement { - get_element_by_id::("error-dialog") - .expect("error-dialog isn't an html dialog element!") - .expect("No error-dialog element present") +#[derive(Props)] +struct CategoryRowProps<'ctx> { + sh: StateHandler<'ctx>, + ingredient: String, + category: String, + ingredient_recipe_map: &'ctx ReadSignal>>, } -fn check_category_text_parses(unparsed: &str, error_text: &Signal) -> bool { - let el = get_error_dialog(); - if let Err(e) = parse::as_categories(unparsed) { - error!(?e, "Error parsing categories"); - error_text.set(e); - el.show(); - false - } else { - el.close(); - true +#[instrument(skip_all)] +#[component] +fn CategoryRow<'ctx, G: Html>(cx: Scope<'ctx>, props: CategoryRowProps<'ctx>) -> View { + let CategoryRowProps { + sh, + ingredient, + category, + ingredient_recipe_map, + } = props; + let category = create_signal(cx, category); + let ingredient_clone = ingredient.clone(); + let ingredient_clone2 = ingredient.clone(); + let recipes = create_memo(cx, move || { + ingredient_recipe_map + .get() + .get(&ingredient_clone2) + .cloned() + .unwrap_or_else(|| BTreeSet::new()) + .iter() + .cloned() + .collect::>() + }); + view! {cx, + tr() { + td() { + (ingredient_clone) br() + Indexed( + iterable=recipes, + view=|cx, r| { + let recipe_name = r.clone(); + view!{cx, + a(href=format!("/ui/recipe/edit/{}", r)) { (recipe_name) } br() + } + } + ) + } + td() { input(type="text", list="category_options", bind:value=category, on:change={ + let ingredient_clone = ingredient.clone(); + move |_| { + sh.dispatch(cx, Message::UpdateCategory(ingredient_clone.clone(), category.get_untracked().as_ref().clone())); + } + }) } + } } } #[instrument(skip_all)] #[component] pub fn Categories<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { - let error_text = create_signal(cx, String::new()); - let category_text: &Signal = create_signal(cx, String::new()); - let dirty = create_signal(cx, false); - - spawn_local_scoped(cx, { - let store = crate::api::HttpStore::get_from_context(cx); - async move { - if let Some(js) = store - .fetch_categories() - .await - .expect("Failed to get categories.") - { - category_text.set(js); - }; - } + let category_list = sh.get_selector(cx, |state| { + let mut categories = state + .get() + .category_map + .iter() + .map(|(_, v)| v.clone()) + .collect::>(); + categories.sort(); + categories.dedup(); + categories }); - let dialog_view = view! {cx, - dialog(id="error-dialog") { - article{ - header { - a(href="#", on:click=|_| { - let el = get_error_dialog(); - el.close(); - }, class="close") - "Invalid Categories" - } - p { - (error_text.get().clone()) - } + let ingredient_recipe_map = sh.get_selector(cx, |state| { + let state = state.get(); + let mut ingredients: BTreeMap> = BTreeMap::new(); + for (recipe_id, r) in state.recipes.iter() { + for (_, i) in r.get_ingredients().iter() { + let ingredient_name = i.name.clone(); + ingredients + .entry(ingredient_name) + .or_insert(BTreeSet::new()) + .insert(recipe_id.clone()); } } - }; + if let Some(staples) = &state.staples { + for (_, i) in staples.get_ingredients().iter() { + let ingredient_name = i.name.clone(); + ingredients + .entry(ingredient_name) + .or_insert(BTreeSet::new()) + .insert("Staples".to_owned()); + } + } + ingredients + }); + let rows = sh.get_selector(cx, |state| { + let state = state.get(); + let category_map = state.category_map.clone(); + let mut ingredients = BTreeSet::new(); + for (_, r) in state.recipes.iter() { + for (_, i) in r.get_ingredients().iter() { + ingredients.insert(i.name.clone()); + } + } + if let Some(staples) = &state.staples { + for (_, i) in staples.get_ingredients().iter() { + ingredients.insert(i.name.clone()); + } + } + let mut mapping_list = Vec::new(); + for i in ingredients.iter() { + let cat = category_map + .get(i) + .map(|v| v.clone()) + .unwrap_or_else(|| "None".to_owned()); + mapping_list.push((i.clone(), cat)); + } + mapping_list.sort_by(|tpl1, tpl2| tpl1.1.cmp(&tpl2.1)); + mapping_list + }); view! {cx, - (dialog_view) - textarea(bind:value=category_text, rows=20, on:change=move |_| { - dirty.set(true); - }) - span(role="button", on:click=move |_| { - check_category_text_parses(category_text.get().as_str(), error_text); - }) { "Check" } " " - span(role="button", on:click=move |_| { - if !*dirty.get() { - return; + table() { + tr { + th { "Ingredient" } + th { "Category" } } - if check_category_text_parses(category_text.get().as_str(), error_text) { - debug!("triggering category save"); - sh.dispatch( - cx, - Message::SetCategoryMap(category_text.get_untracked().as_ref().clone()), - ); - } - }) { "Save" } + Keyed( + iterable=rows, + view=move |cx, (i, c)| { + view! {cx, CategoryRow(sh=sh, ingredient=i, category=c, ingredient_recipe_map=ingredient_recipe_map)} + }, + key=|(i, _)| i.clone() + ) + } + datalist(id="category_options") { + Keyed( + iterable=category_list, + view=move |cx, c| { + view!{cx, + option(value=c) + } + }, + key=|c| c.clone(), + ) + } } } diff --git a/web/src/components/shopping_list.rs b/web/src/components/shopping_list.rs index 1a219e8..5e01a45 100644 --- a/web/src/components/shopping_list.rs +++ b/web/src/components/shopping_list.rs @@ -28,6 +28,7 @@ fn make_ingredients_rows<'ctx, G: Html>( debug!("Making ingredients rows"); let ingredients = sh.get_selector(cx, move |state| { let state = state.get(); + let category_map = &state.category_map; debug!("building ingredient list from state"); let mut acc = IngredientAccumulator::new(); for (id, count) in state.recipe_counts.iter() { @@ -45,19 +46,24 @@ fn make_ingredients_rows<'ctx, G: Html>( acc.accumulate_from(staples); } } - acc.ingredients() + let mut ingredients = acc + .ingredients() .into_iter() // First we filter out any filtered ingredients .filter(|(i, _)| !state.filtered_ingredients.contains(i)) // Then we take into account our modified amts .map(|(k, (i, rs))| { + let category = category_map + .get(&i.name) + .cloned() + .unwrap_or_else(|| String::new()); if state.modified_amts.contains_key(&k) { ( k.clone(), ( i.name, i.form, - i.category, + category, state.modified_amts.get(&k).unwrap().clone(), rs, ), @@ -68,7 +74,7 @@ fn make_ingredients_rows<'ctx, G: Html>( ( i.name, i.form, - i.category, + category, format!("{}", i.amt.normalize()), rs, ), @@ -78,7 +84,9 @@ fn make_ingredients_rows<'ctx, G: Html>( .collect::, String, String, BTreeSet), - )>>() + )>>(); + ingredients.sort_by(|tpl1, tpl2| (&tpl1.1 .2, &tpl1.1 .0).cmp(&(&tpl2.1 .2, &tpl2.1 .0))); + ingredients }); view!( cx, diff --git a/web/src/js_lib.rs b/web/src/js_lib.rs index ef99c4d..8ef0740 100644 --- a/web/src/js_lib.rs +++ b/web/src/js_lib.rs @@ -11,8 +11,8 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use wasm_bindgen::{JsCast, JsValue}; -use web_sys::{window, Element, Storage}; +use wasm_bindgen::JsValue; +use web_sys::{window, Storage}; pub fn navigate_to_path(path: &str) -> Result<(), JsValue> { window() @@ -21,21 +21,6 @@ pub fn navigate_to_path(path: &str) -> Result<(), JsValue> { .set_pathname(path) } -pub fn get_element_by_id(id: &str) -> Result, Element> -where - E: JsCast, -{ - match window() - .expect("No window present") - .document() - .expect("No document in window") - .get_element_by_id(id) - { - Some(e) => e.dyn_into::().map(|e| Some(e)), - None => Ok(None), - } -} - pub fn get_storage() -> Storage { window() .expect("No Window Present")