Use StateHandler in the shopping list

This commit is contained in:
Jeremy Wall 2022-12-28 15:46:19 -06:00
parent 4b2563509b
commit 73c298661a
4 changed files with 121 additions and 77 deletions

View File

@ -51,6 +51,14 @@ pub fn as_categories(i: &str) -> std::result::Result<BTreeMap<String, String>, S
}
}
pub fn as_measure(i: &str) -> std::result::Result<Measure, String> {
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<StrIter, BTreeMap<String, String>>,
do_each!(

View File

@ -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};

View File

@ -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<Vec<(IngredientKey, (Ingredient, BTreeSet<String>))>>,
modified_amts: RcSignal<BTreeMap<IngredientKey, RcSignal<String>>>,
filtered_keys: RcSignal<BTreeSet<IngredientKey>>,
sh: StateHandler<'ctx>,
show_staples: &'ctx ReadSignal<bool>,
) -> View<G> {
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::<Vec<(
IngredientKey,
(String, Option<String>, String, String, BTreeSet<String>),
)>>()
});
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<Vec<(usize, (RcSignal<String>, RcSignal<String>))>>,
) -> View<G> {
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<G> {
let extras_read_signal = sh.get_selector(cx, |state| {
state
.get()
.extras
.iter()
.cloned()
.collect::<Vec<(String, String)>>()
});
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<Vec<(IngredientKey, (Ingredient, BTreeSet<String>))>>,
modified_amts: RcSignal<BTreeMap<IngredientKey, RcSignal<String>>>,
extras: RcSignal<Vec<(usize, (RcSignal<String>, RcSignal<String>))>>,
filtered_keys: RcSignal<BTreeSet<IngredientKey>>,
sh: StateHandler<'ctx>,
show_staples: &'ctx ReadSignal<bool>,
) -> View<G> {
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<G: Html>(cx: Scope) -> View<G> {
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<BTreeSet<IngredientKey>> = state.filtered_ingredients.clone();
pub fn ShoppingList<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> {
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<G: Html>(cx: Scope) -> View<G> {
});
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<G: Html>(cx: Scope) -> View<G> {
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<G: Html>(cx: Scope) -> View<G> {
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=|_| {

View File

@ -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) }
}
}