From 73c298661a0b6316b27f9efd6bcc24630d64a3a9 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 28 Dec 2022 15:46:19 -0600 Subject: [PATCH] Use StateHandler in the shopping list --- recipes/src/parse.rs | 8 ++ web/src/api.rs | 2 +- web/src/components/shopping_list.rs | 186 +++++++++++++++++----------- web/src/pages/planning/inventory.rs | 2 +- 4 files changed, 121 insertions(+), 77 deletions(-) diff --git a/recipes/src/parse.rs b/recipes/src/parse.rs index b8253fd..d4a934e 100644 --- a/recipes/src/parse.rs +++ b/recipes/src/parse.rs @@ -51,6 +51,14 @@ pub fn as_categories(i: &str) -> std::result::Result, S } } +pub fn as_measure(i: &str) -> std::result::Result { + match measure(StrIter::new(i)) { + Result::Abort(e) | Result::Fail(e) => Err(format_err(e)), + Result::Incomplete(_) => Err(format!("Incomplete categories list can not parse")), + Result::Complete(_, m) => Ok(m), + } +} + make_fn!( pub categories>, do_each!( diff --git a/web/src/api.rs b/web/src/api.rs index 2ee8f17..750751e 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -17,7 +17,7 @@ use base64; use reqwasm; use serde_json::{from_str, to_string}; use sycamore::prelude::*; -use tracing::{debug, error, info, instrument, warn}; +use tracing::{debug, error, instrument, warn}; use client_api::*; use recipes::{parse, IngredientKey, Recipe, RecipeEntry}; diff --git a/web/src/components/shopping_list.rs b/web/src/components/shopping_list.rs index 36bd680..b56eba4 100644 --- a/web/src/components/shopping_list.rs +++ b/web/src/components/shopping_list.rs @@ -13,34 +13,86 @@ // limitations under the License. use std::collections::{BTreeMap, BTreeSet}; -use recipes::{Ingredient, IngredientKey}; -use sycamore::{futures::spawn_local_scoped, prelude::*}; +use recipes::{IngredientAccumulator, IngredientKey}; +use sycamore::prelude::*; use tracing::{debug, info, instrument}; +use crate::app_state::{Message, StateHandler}; + fn make_ingredients_rows<'ctx, G: Html>( cx: Scope<'ctx>, - ingredients: &'ctx ReadSignal))>>, - modified_amts: RcSignal>>, - filtered_keys: RcSignal>, + sh: StateHandler<'ctx>, + show_staples: &'ctx ReadSignal, ) -> View { + let ingredients = sh.get_selector(cx, move |state| { + let state = state.get(); + let mut acc = IngredientAccumulator::new(); + for (id, count) in state.recipe_counts.iter() { + for _ in 0..(*count) { + acc.accumulate_from( + state + .recipes + .get(id) + .expect(&format!("No such recipe id exists: {}", id)), + ); + } + } + if *show_staples.get() { + if let Some(staples) = &state.staples { + acc.accumulate_from(staples); + } + } + 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))| { + if state.modified_amts.contains_key(&k) { + ( + k.clone(), + ( + i.name, + i.form, + i.category, + state.modified_amts.get(&k).unwrap().clone(), + rs, + ), + ) + } else { + ( + k.clone(), + ( + i.name, + i.form, + i.category, + format!("{}", i.amt.normalize()), + rs, + ), + ) + } + }) + .collect::, String, String, BTreeSet), + )>>() + }); view!( cx, Indexed( iterable = ingredients, - view = move |cx, (k, (i, rs))| { - let mut modified_amt_set = modified_amts.get().as_ref().clone(); - let amt = modified_amt_set - .entry(k.clone()) - .or_insert(create_rc_signal(format!("{}", i.amt.normalize()))) - .clone(); - modified_amts.set(modified_amt_set); - let name = i.name; - let category = if i.category == "" { + view = move |cx, (k, (name, form, category, amt, rs))| { + let category = if category == "" { "other".to_owned() } else { - i.category + category }; - let form = i.form.map(|form| format!("({})", form)).unwrap_or_default(); + let amt_signal = create_signal(cx, amt); + let k_clone = k.clone(); + sh.bind_trigger(cx, &amt_signal, move |val| { + Message::UpdateAmt(k_clone.clone(), val.as_ref().clone()) + }); + let form = form.map(|form| format!("({})", form)).unwrap_or_default(); let recipes = rs .iter() .fold(String::new(), |acc, s| format!("{}{},", acc, s)) @@ -49,15 +101,12 @@ fn make_ingredients_rows<'ctx, G: Html>( view! {cx, tr { td { - input(bind:value=amt, type="text") + input(bind:value=amt_signal, type="text") } td { input(type="button", class="no-print destructive", value="X", on:click={ - let filtered_keys = filtered_keys.clone(); move |_| { - let mut keyset = filtered_keys.get().as_ref().clone(); - keyset.insert(k.clone()); - filtered_keys.set(keyset); + sh.dispatch(cx, Message::AddFilteredIngredient(k.clone())); }}) } td { (name) " " (form) "" br {} "" (category) "" } @@ -69,36 +118,44 @@ fn make_ingredients_rows<'ctx, G: Html>( ) } -fn make_extras_rows<'ctx, G: Html>( - cx: Scope<'ctx>, - extras: RcSignal, RcSignal))>>, -) -> View { - let extras_read_signal = create_memo(cx, { - let extras = extras.clone(); - move || extras.get().as_ref().clone() +fn make_extras_rows<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { + let extras_read_signal = sh.get_selector(cx, |state| { + state + .get() + .extras + .iter() + .cloned() + .collect::>() }); view! {cx, Indexed( iterable=extras_read_signal, - view= move |cx, (idx, (amt, name))| { + view= move |cx, (amt, name)| { + let amt_signal = create_signal(cx, amt.clone()); + let name_signal = create_signal(cx, name.clone()); + create_effect(cx, { + let amt_clone = amt.clone(); + let name_clone = name.clone(); + move || { + let new_amt = amt_signal.get(); + let new_name = name_signal.get(); + sh.dispatch(cx, Message::RemoveExtra(amt_clone.clone(), name_clone.clone())); + sh.dispatch(cx, Message::AddExtra(new_amt.as_ref().clone(), new_name.as_ref().clone())); + } + }); view! {cx, tr { td { - input(bind:value=amt, type="text") + input(bind:value=amt_signal, type="text") } td { input(type="button", class="no-print destructive", value="X", on:click={ - let extras = extras.clone(); move |_| { - extras.set(extras.get().iter() - .filter(|(i, _)| *i != idx) - .map(|(_, v)| v.clone()) - .enumerate() - .collect()) + sh.dispatch(cx, Message::RemoveExtra(amt.clone(), name.clone())); }}) } td { - input(bind:value=name, type="text") + input(bind:value=name_signal, type="text") } td { "Misc" } } @@ -110,14 +167,11 @@ fn make_extras_rows<'ctx, G: Html>( fn make_shopping_table<'ctx, G: Html>( cx: Scope<'ctx>, - ingredients: &'ctx ReadSignal))>>, - modified_amts: RcSignal>>, - extras: RcSignal, RcSignal))>>, - filtered_keys: RcSignal>, + sh: StateHandler<'ctx>, + show_staples: &'ctx ReadSignal, ) -> View { - let extra_rows_view = make_extras_rows(cx, extras); - let ingredient_rows = - make_ingredients_rows(cx, ingredients, modified_amts, filtered_keys.clone()); + let extra_rows_view = make_extras_rows(cx, sh); + let ingredient_rows = make_ingredients_rows(cx, sh, show_staples); view! {cx, table(class="pad-top shopping-list page-breaker container-fluid", role="grid") { tr { @@ -134,12 +188,10 @@ fn make_shopping_table<'ctx, G: Html>( } } -#[instrument] +#[instrument(skip_all)] #[component] -pub fn ShoppingList(cx: Scope) -> View { - let state = crate::app_state::State::get_from_context(cx); - // FIXME(jwall): We need to init this state for the page at some point. - let filtered_keys: RcSignal> = state.filtered_ingredients.clone(); +pub fn ShoppingList<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { + let filtered_keys = sh.get_selector(cx, |state| state.get().filtered_ingredients.clone()); let ingredients_map = create_rc_signal(BTreeMap::new()); let show_staples = create_signal(cx, true); let save_click = create_signal(cx, ()); @@ -169,17 +221,10 @@ pub fn ShoppingList(cx: Scope) -> View { }); let table_view = create_signal(cx, View::empty()); create_effect(cx, { - let filtered_keys = filtered_keys.clone(); let state = crate::app_state::State::get_from_context(cx); move || { if (ingredients.get().len() > 0) || (state.extras.get().len() > 0) { - table_view.set(make_shopping_table( - cx, - ingredients, - state.modified_amts.clone(), - state.extras.clone(), - filtered_keys.clone(), - )); + table_view.set(make_shopping_table(cx, sh, show_staples)); } else { table_view.set(View::empty()); } @@ -188,17 +233,7 @@ pub fn ShoppingList(cx: Scope) -> View { create_effect(cx, move || { save_click.track(); info!("Registering save request for inventory"); - spawn_local_scoped(cx, { - let state = crate::app_state::State::get_from_context(cx); - let store = crate::api::HttpStore::get_from_context(cx); - async move { - debug!(?state, "Attempting save for inventory"); - store - .save_state(state) - .await - .expect("Unable to save inventory data"); - } - }) + sh.dispatch(cx, Message::SaveState); }); let state = crate::app_state::State::get_from_context(cx); view! {cx, @@ -212,14 +247,15 @@ pub fn ShoppingList(cx: Scope) -> View { state.extras.set(cloned_extras.drain(0..).enumerate().collect()); }) input(type="button", value="Reset", class="no-print", on:click={ - let state = crate::app_state::State::get_from_context(cx); + //let state = crate::app_state::State::get_from_context(cx); move |_| { - // TODO(jwall): We should actually pop up a modal here or use a different set of items. - ingredients_map.set(state.get_shopping_list(*show_staples.get())); - // clear the filter_signal - filtered_keys.set(BTreeSet::new()); - state.modified_amts.set(BTreeMap::new()); - state.extras.set(Vec::new()); + // FIXME(jwall): This should be an event. + // // TODO(jwall): We should actually pop up a modal here or use a different set of items. + // ingredients_map.set(state.get_shopping_list(*show_staples.get())); + // // clear the filter_signal + // filtered_keys.set(BTreeSet::new()); + // state.modified_amts.set(BTreeMap::new()); + // state.extras.set(Vec::new()); } }) input(type="button", value="Save", class="no-print", on:click=|_| { diff --git a/web/src/pages/planning/inventory.rs b/web/src/pages/planning/inventory.rs index e6ea29f..39a84ea 100644 --- a/web/src/pages/planning/inventory.rs +++ b/web/src/pages/planning/inventory.rs @@ -21,6 +21,6 @@ pub fn InventoryPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> view! {cx, PlanningPage( selected=Some("Inventory".to_owned()), - ) { ShoppingList() } + ) { ShoppingList(sh) } } }