New UI for ingredient category management

This commit is contained in:
Jeremy Wall 2023-01-05 17:56:15 -05:00
parent 44e8c0f727
commit 66a558d1e6
10 changed files with 263 additions and 235 deletions

View File

@ -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" "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": { "37f382be1b53efd2f79a0d59ae6a8717f88a86908a7a4128d5ed7339147ca59d": {
"describe": { "describe": {
"columns": [ "columns": [
@ -342,16 +352,6 @@
}, },
"query": "select category_text from categories where user_id = ?" "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": { "d84685a82585c5e4ae72c86ba1fe6e4a7241c4c3c9e948213e5849d956132bad": {
"describe": { "describe": {
"columns": [], "columns": [],

View File

@ -1,3 +1,5 @@
insert into category_mappings insert into category_mappings
(user_id, ingredient_name, category_name) (user_id, ingredient_name, category_name)
values (?, ?, ?) values (?, ?, ?)
on conflict (user_id, ingredient_name)
do update set category_name=excluded.category_name

View File

@ -231,17 +231,15 @@ pub struct Ingredient {
pub name: String, pub name: String,
pub form: Option<String>, pub form: Option<String>,
pub amt: Measure, pub amt: Measure,
pub category: String,
} }
impl Ingredient { impl Ingredient {
pub fn new<S: Into<String>>(name: S, form: Option<String>, amt: Measure, category: S) -> Self { pub fn new<S: Into<String>>(name: S, form: Option<String>, amt: Measure) -> Self {
Self { Self {
id: None, id: None,
name: name.into(), name: name.into(),
form, form,
amt, amt,
category: category.into(),
} }
} }
@ -250,14 +248,12 @@ impl Ingredient {
name: S, name: S,
form: Option<String>, form: Option<String>,
amt: Measure, amt: Measure,
category: S,
) -> Self { ) -> Self {
Self { Self {
id: Some(id), id: Some(id),
name: name.into(), name: name.into(),
form, form,
amt, amt,
category: category.into(),
} }
} }

View File

@ -447,7 +447,7 @@ make_fn!(
name => ingredient_name, name => ingredient_name,
modifier => optional!(ingredient_modifier), modifier => optional!(ingredient_modifier),
_ => optional!(ws), _ => optional!(ws),
(Ingredient::new(name, modifier.map(|s| s.to_owned()), measure, String::new())) (Ingredient::new(name, modifier.map(|s| s.to_owned()), measure))
) )
); );

View File

@ -89,108 +89,80 @@ fn test_volume_normalize() {
fn test_ingredient_display() { fn test_ingredient_display() {
let cases = vec![ let cases = vec![
( (
Ingredient::new( Ingredient::new("onion", Some("chopped".to_owned()), Measure::cup(1.into())),
"onion",
Some("chopped".to_owned()),
Measure::cup(1.into()),
"Produce",
),
"1 cup onion (chopped)", "1 cup onion (chopped)",
), ),
( (
Ingredient::new( Ingredient::new("onion", Some("chopped".to_owned()), Measure::cup(2.into())),
"onion",
Some("chopped".to_owned()),
Measure::cup(2.into()),
"Produce",
),
"2 cups onion (chopped)", "2 cups onion (chopped)",
), ),
( (
Ingredient::new( Ingredient::new("onion", Some("chopped".to_owned()), Measure::tbsp(1.into())),
"onion",
Some("chopped".to_owned()),
Measure::tbsp(1.into()),
"Produce",
),
"1 tbsp onion (chopped)", "1 tbsp onion (chopped)",
), ),
( (
Ingredient::new( Ingredient::new("onion", Some("chopped".to_owned()), Measure::tbsp(2.into())),
"onion",
Some("chopped".to_owned()),
Measure::tbsp(2.into()),
"Produce",
),
"2 tbsps onion (chopped)", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "2 ltr soy sauce",
), ),
(Ingredient::new("apple", None, Measure::count(1)), "1 apple"),
( (
Ingredient::new("apple", None, Measure::count(1), "Produce"), Ingredient::new("salt", None, Measure::gram(1.into())),
"1 apple",
),
(
Ingredient::new("salt", None, Measure::gram(1.into()), "Produce"),
"1 gram salt", "1 gram salt",
), ),
( (
Ingredient::new("salt", None, Measure::gram(2.into()), "Produce"), Ingredient::new("salt", None, Measure::gram(2.into())),
"2 grams salt", "2 grams salt",
), ),
( (
Ingredient::new( Ingredient::new("onion", Some("minced".to_owned()), Measure::cup(1.into())),
"onion",
Some("minced".to_owned()),
Measure::cup(1.into()),
"Produce",
),
"1 cup onion (minced)", "1 cup onion (minced)",
), ),
( (
@ -198,7 +170,6 @@ fn test_ingredient_display() {
"pepper", "pepper",
Some("ground".to_owned()), Some("ground".to_owned()),
Measure::tsp(Ratio::new(1, 2).into()), Measure::tsp(Ratio::new(1, 2).into()),
"Produce",
), ),
"1/2 tsp pepper (ground)", "1/2 tsp pepper (ground)",
), ),
@ -207,35 +178,19 @@ fn test_ingredient_display() {
"pepper", "pepper",
Some("ground".to_owned()), Some("ground".to_owned()),
Measure::tsp(Ratio::new(3, 2).into()), Measure::tsp(Ratio::new(3, 2).into()),
"Produce",
), ),
"1 1/2 tsps pepper (ground)", "1 1/2 tsps pepper (ground)",
), ),
( (
Ingredient::new( Ingredient::new("apple", Some("sliced".to_owned()), Measure::count(1)),
"apple",
Some("sliced".to_owned()),
Measure::count(1),
"Produce",
),
"1 apple (sliced)", "1 apple (sliced)",
), ),
( (
Ingredient::new( Ingredient::new("potato", Some("mashed".to_owned()), Measure::count(1)),
"potato",
Some("mashed".to_owned()),
Measure::count(1),
"Produce",
),
"1 potato (mashed)", "1 potato (mashed)",
), ),
( (
Ingredient::new( Ingredient::new("potato", Some("blanched".to_owned()), Measure::count(1)),
"potato",
Some("blanched".to_owned()),
Measure::count(1),
"Produce",
),
"1 potato (blanched)", "1 potato (blanched)",
), ),
]; ];
@ -312,7 +267,6 @@ fn test_ingredient_parse() {
"green bell pepper", "green bell pepper",
Some("chopped".to_owned()), Some("chopped".to_owned()),
Count(Quantity::Whole(1)), Count(Quantity::Whole(1)),
"",
), ),
), ),
] { ] {
@ -332,18 +286,16 @@ fn test_ingredient_list_parse() {
"flour", "flour",
None, None,
Volume(Cup(Quantity::Whole(1))), Volume(Cup(Quantity::Whole(1))),
"",
)], )],
), ),
( (
"1 cup flour \n1/2 tsp butter ", "1 cup flour \n1/2 tsp butter ",
vec![ vec![
Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""), Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1)))),
Ingredient::new( Ingredient::new(
"butter", "butter",
None, None,
Volume(Tsp(Quantity::Frac(Ratio::new(1, 2)))), Volume(Tsp(Quantity::Frac(Ratio::new(1, 2)))),
"",
), ),
], ],
), ),

View File

@ -26,35 +26,6 @@ use web_sys::Storage;
use crate::{app_state::AppState, js_lib}; use crate::{app_state::AppState, js_lib};
// FIXME(jwall): We should be able to delete this now.
#[instrument]
fn filter_recipes(
recipe_entries: &Option<Vec<RecipeEntry>>,
) -> Result<(Option<Recipe>, Option<BTreeMap<String, Recipe>>), 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)] #[derive(Debug)]
pub struct Error(String); pub struct Error(String);
@ -104,6 +75,10 @@ fn recipe_key<S: std::fmt::Display>(id: S) -> String {
format!("recipe:{}", id) format!("recipe:{}", id)
} }
fn category_key<S: std::fmt::Display>(id: S) -> String {
format!("category:{}", id)
}
fn token68(user: String, pass: String) -> String { fn token68(user: String, pass: String) -> String {
base64::encode(format!("{}:{}", user, pass)) base64::encode(format!("{}:{}", user, pass))
} }
@ -145,22 +120,39 @@ impl LocalStore {
} }
/// Gets categories from local storage. /// Gets categories from local storage.
pub fn get_categories(&self) -> Option<String> { pub fn get_categories(&self) -> Option<Vec<(String, String)>> {
self.store let mut mappings = Vec::new();
.get("categories") for k in self.get_category_keys() {
.expect("Failed go get categories") if let Some(mut cat_map) = self
.store
.get(&k)
.expect(&format!("Failed to get category key {}", k))
.map(|v| {
from_str::<Vec<(String, String)>>(&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. /// Set the categories to the given string.
pub fn set_categories(&self, categories: Option<&String>) { pub fn set_categories(&self, mappings: Option<&Vec<(String, String)>>) {
if let Some(c) = categories { if let Some(mappings) = mappings {
self.store for (i, cat) in mappings.iter() {
.set("categories", c) self.store
.expect("Failed to set categories"); .set(
} else { &category_key(i),
self.store &to_string(&(i, cat)).expect("Failed to serialize category mapping"),
.delete("categories") )
.expect("Failed to delete categories") .expect("Failed to store category mapping");
}
} }
} }
@ -174,6 +166,12 @@ impl LocalStore {
keys keys
} }
fn get_category_keys(&self) -> impl Iterator<Item = String> {
self.get_storage_keys()
.into_iter()
.filter(|k| k.starts_with("category:"))
}
fn get_recipe_keys(&self) -> impl Iterator<Item = String> { fn get_recipe_keys(&self) -> impl Iterator<Item = String> {
self.get_storage_keys() self.get_storage_keys()
.into_iter() .into_iter()
@ -392,10 +390,11 @@ impl HttpStore {
} }
return None; return None;
} }
//#[instrument] //#[instrument]
pub async fn fetch_categories(&self) -> Result<Option<String>, Error> { pub async fn fetch_categories(&self) -> Result<Option<Vec<(String, String)>>, Error> {
let mut path = self.v2_path(); 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 { let resp = match reqwasm::http::Request::get(&path).send().await {
Ok(resp) => resp, Ok(resp) => resp,
Err(reqwasm::Error::JsError(err)) => { Err(reqwasm::Error::JsError(err)) => {
@ -413,7 +412,11 @@ impl HttpStore {
Err(format!("Status: {}", resp.status()).into()) Err(format!("Status: {}", resp.status()).into())
} else { } else {
debug!("We got a valid response back!"); debug!("We got a valid response back!");
let resp = resp.json::<CategoryResponse>().await?.as_success().unwrap(); let resp = resp
.json::<CategoryMappingResponse>()
.await?
.as_success()
.unwrap();
Ok(Some(resp)) Ok(Some(resp))
} }
} }
@ -506,9 +509,9 @@ impl HttpStore {
} }
#[instrument(skip(categories))] #[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(); let mut path = self.v2_path();
path.push_str("/categories"); path.push_str("/category_map");
let resp = reqwasm::http::Request::post(&path) let resp = reqwasm::http::Request::post(&path)
.body(to_string(&categories).expect("Unable to encode categories as json")) .body(to_string(&categories).expect("Unable to encode categories as json"))
.header("content-type", "application/json") .header("content-type", "application/json")

View File

@ -32,7 +32,7 @@ pub struct AppState {
pub extras: Vec<(String, String)>, pub extras: Vec<(String, String)>,
pub staples: Option<Recipe>, pub staples: Option<Recipe>,
pub recipes: BTreeMap<String, Recipe>, pub recipes: BTreeMap<String, Recipe>,
pub category_map: String, pub category_map: BTreeMap<String, String>,
pub filtered_ingredients: BTreeSet<IngredientKey>, pub filtered_ingredients: BTreeSet<IngredientKey>,
pub modified_amts: BTreeMap<IngredientKey, String>, pub modified_amts: BTreeMap<IngredientKey, String>,
pub auth: Option<UserData>, pub auth: Option<UserData>,
@ -45,7 +45,7 @@ impl AppState {
extras: Vec::new(), extras: Vec::new(),
staples: None, staples: None,
recipes: BTreeMap::new(), recipes: BTreeMap::new(),
category_map: String::new(), category_map: BTreeMap::new(),
filtered_ingredients: BTreeSet::new(), filtered_ingredients: BTreeSet::new(),
modified_amts: BTreeMap::new(), modified_amts: BTreeMap::new(),
auth: None, auth: None,
@ -61,7 +61,8 @@ pub enum Message {
UpdateExtra(usize, String, String), UpdateExtra(usize, String, String),
SaveRecipe(RecipeEntry), SaveRecipe(RecipeEntry),
SetRecipe(String, Recipe), SetRecipe(String, Recipe),
SetCategoryMap(String), SetCategoryMap(BTreeMap<String, String>),
UpdateCategory(String, String),
ResetInventory, ResetInventory,
AddFilteredIngredient(IngredientKey), AddFilteredIngredient(IngredientKey),
UpdateAmt(IngredientKey, String), UpdateAmt(IngredientKey, String),
@ -94,6 +95,9 @@ impl Debug for Message {
f.debug_tuple("SetRecipe").field(arg0).field(arg1).finish() f.debug_tuple("SetRecipe").field(arg0).field(arg1).finish()
} }
Self::SetCategoryMap(arg0) => f.debug_tuple("SetCategoryMap").field(arg0).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::ResetInventory => write!(f, "ResetInventory"),
Self::AddFilteredIngredient(arg0) => { Self::AddFilteredIngredient(arg0) => {
f.debug_tuple("AddFilteredIngredient").field(arg0).finish() f.debug_tuple("AddFilteredIngredient").field(arg0).finish()
@ -197,10 +201,11 @@ impl StateMachine {
} }
info!("Synchronizing categories"); info!("Synchronizing categories");
match store.fetch_categories().await { match store.fetch_categories().await {
Ok(Some(categories_content)) => { Ok(Some(mut categories_content)) => {
debug!(categories=?categories_content); debug!(categories=?categories_content);
local_store.set_categories(Some(&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) => { Ok(None) => {
warn!("There is no category file"); warn!("There is no category file");
@ -308,12 +313,26 @@ impl MessageMapper<Message, AppState> for StateMachine {
} }
}); });
} }
Message::SetCategoryMap(category_text) => { Message::SetCategoryMap(map) => {
original_copy.category_map = category_text.clone(); original_copy.category_map = map.clone();
self.local_store.set_categories(Some(&category_text)); let list = map.iter().map(|(i, c)| (i.clone(), c.clone())).collect();
self.local_store.set_categories(Some(&list));
let store = self.store.clone(); let store = self.store.clone();
spawn_local_scoped(cx, async move { 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"); error!(?e, "Failed to save categories");
} }
}); });

View File

@ -11,91 +11,154 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use crate::{ use std::collections::{BTreeMap, BTreeSet};
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 recipes::parse; use crate::app_state::{Message, StateHandler};
use sycamore::prelude::*;
use tracing::instrument;
fn get_error_dialog() -> HtmlDialogElement { #[derive(Props)]
get_element_by_id::<HtmlDialogElement>("error-dialog") struct CategoryRowProps<'ctx> {
.expect("error-dialog isn't an html dialog element!") sh: StateHandler<'ctx>,
.expect("No error-dialog element present") ingredient: String,
category: String,
ingredient_recipe_map: &'ctx ReadSignal<BTreeMap<String, BTreeSet<String>>>,
} }
fn check_category_text_parses(unparsed: &str, error_text: &Signal<String>) -> bool { #[instrument(skip_all)]
let el = get_error_dialog(); #[component]
if let Err(e) = parse::as_categories(unparsed) { fn CategoryRow<'ctx, G: Html>(cx: Scope<'ctx>, props: CategoryRowProps<'ctx>) -> View<G> {
error!(?e, "Error parsing categories"); let CategoryRowProps {
error_text.set(e); sh,
el.show(); ingredient,
false category,
} else { ingredient_recipe_map,
el.close(); } = props;
true 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::<Vec<String>>()
});
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)] #[instrument(skip_all)]
#[component] #[component]
pub fn Categories<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> { pub fn Categories<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> {
let error_text = create_signal(cx, String::new()); let category_list = sh.get_selector(cx, |state| {
let category_text: &Signal<String> = create_signal(cx, String::new()); let mut categories = state
let dirty = create_signal(cx, false); .get()
.category_map
spawn_local_scoped(cx, { .iter()
let store = crate::api::HttpStore::get_from_context(cx); .map(|(_, v)| v.clone())
async move { .collect::<Vec<String>>();
if let Some(js) = store categories.sort();
.fetch_categories() categories.dedup();
.await categories
.expect("Failed to get categories.")
{
category_text.set(js);
};
}
}); });
let dialog_view = view! {cx, let ingredient_recipe_map = sh.get_selector(cx, |state| {
dialog(id="error-dialog") { let state = state.get();
article{ let mut ingredients: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
header { for (recipe_id, r) in state.recipes.iter() {
a(href="#", on:click=|_| { for (_, i) in r.get_ingredients().iter() {
let el = get_error_dialog(); let ingredient_name = i.name.clone();
el.close(); ingredients
}, class="close") .entry(ingredient_name)
"Invalid Categories" .or_insert(BTreeSet::new())
} .insert(recipe_id.clone());
p {
(error_text.get().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, view! {cx,
(dialog_view) table() {
textarea(bind:value=category_text, rows=20, on:change=move |_| { tr {
dirty.set(true); th { "Ingredient" }
}) th { "Category" }
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;
} }
if check_category_text_parses(category_text.get().as_str(), error_text) { Keyed(
debug!("triggering category save"); iterable=rows,
sh.dispatch( view=move |cx, (i, c)| {
cx, view! {cx, CategoryRow(sh=sh, ingredient=i, category=c, ingredient_recipe_map=ingredient_recipe_map)}
Message::SetCategoryMap(category_text.get_untracked().as_ref().clone()), },
); key=|(i, _)| i.clone()
} )
}) { "Save" } }
datalist(id="category_options") {
Keyed(
iterable=category_list,
view=move |cx, c| {
view!{cx,
option(value=c)
}
},
key=|c| c.clone(),
)
}
} }
} }

View File

@ -28,6 +28,7 @@ fn make_ingredients_rows<'ctx, G: Html>(
debug!("Making ingredients rows"); debug!("Making ingredients rows");
let ingredients = sh.get_selector(cx, move |state| { let ingredients = sh.get_selector(cx, move |state| {
let state = state.get(); let state = state.get();
let category_map = &state.category_map;
debug!("building ingredient list from state"); debug!("building ingredient list from state");
let mut acc = IngredientAccumulator::new(); let mut acc = IngredientAccumulator::new();
for (id, count) in state.recipe_counts.iter() { for (id, count) in state.recipe_counts.iter() {
@ -45,19 +46,24 @@ fn make_ingredients_rows<'ctx, G: Html>(
acc.accumulate_from(staples); acc.accumulate_from(staples);
} }
} }
acc.ingredients() let mut ingredients = acc
.ingredients()
.into_iter() .into_iter()
// First we filter out any filtered ingredients // First we filter out any filtered ingredients
.filter(|(i, _)| !state.filtered_ingredients.contains(i)) .filter(|(i, _)| !state.filtered_ingredients.contains(i))
// Then we take into account our modified amts // Then we take into account our modified amts
.map(|(k, (i, rs))| { .map(|(k, (i, rs))| {
let category = category_map
.get(&i.name)
.cloned()
.unwrap_or_else(|| String::new());
if state.modified_amts.contains_key(&k) { if state.modified_amts.contains_key(&k) {
( (
k.clone(), k.clone(),
( (
i.name, i.name,
i.form, i.form,
i.category, category,
state.modified_amts.get(&k).unwrap().clone(), state.modified_amts.get(&k).unwrap().clone(),
rs, rs,
), ),
@ -68,7 +74,7 @@ fn make_ingredients_rows<'ctx, G: Html>(
( (
i.name, i.name,
i.form, i.form,
i.category, category,
format!("{}", i.amt.normalize()), format!("{}", i.amt.normalize()),
rs, rs,
), ),
@ -78,7 +84,9 @@ fn make_ingredients_rows<'ctx, G: Html>(
.collect::<Vec<( .collect::<Vec<(
IngredientKey, IngredientKey,
(String, Option<String>, String, String, BTreeSet<String>), (String, Option<String>, String, String, BTreeSet<String>),
)>>() )>>();
ingredients.sort_by(|tpl1, tpl2| (&tpl1.1 .2, &tpl1.1 .0).cmp(&(&tpl2.1 .2, &tpl2.1 .0)));
ingredients
}); });
view!( view!(
cx, cx,

View File

@ -11,8 +11,8 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen::JsValue;
use web_sys::{window, Element, Storage}; use web_sys::{window, Storage};
pub fn navigate_to_path(path: &str) -> Result<(), JsValue> { pub fn navigate_to_path(path: &str) -> Result<(), JsValue> {
window() window()
@ -21,21 +21,6 @@ pub fn navigate_to_path(path: &str) -> Result<(), JsValue> {
.set_pathname(path) .set_pathname(path)
} }
pub fn get_element_by_id<E>(id: &str) -> Result<Option<E>, 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::<E>().map(|e| Some(e)),
None => Ok(None),
}
}
pub fn get_storage() -> Storage { pub fn get_storage() -> Storage {
window() window()
.expect("No Window Present") .expect("No Window Present")