mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
Use StateHandler in the shopping list
This commit is contained in:
parent
4b2563509b
commit
73c298661a
@ -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!(
|
||||
|
@ -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};
|
||||
|
@ -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=|_| {
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user