Fix infinite signal loop in shopping list

This commit is contained in:
Jeremy Wall 2022-12-31 15:57:09 -06:00
parent 0167e6070d
commit 77cae25c74
2 changed files with 66 additions and 58 deletions

View File

@ -20,6 +20,7 @@ use sycamore::futures::spawn_local_scoped;
use sycamore::prelude::*; use sycamore::prelude::*;
use sycamore_state::{Handler, MessageMapper}; use sycamore_state::{Handler, MessageMapper};
use tracing::{debug, error, info, instrument, warn}; use tracing::{debug, error, info, instrument, warn};
use wasm_bindgen::throw_str;
use crate::api::HttpStore; use crate::api::HttpStore;
use crate::js_lib; use crate::js_lib;
@ -27,7 +28,7 @@ use crate::js_lib;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct AppState { pub struct AppState {
pub recipe_counts: BTreeMap<String, usize>, pub recipe_counts: BTreeMap<String, usize>,
pub extras: BTreeSet<(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: String,
@ -40,7 +41,7 @@ impl AppState {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
recipe_counts: BTreeMap::new(), recipe_counts: BTreeMap::new(),
extras: BTreeSet::new(), extras: Vec::new(),
staples: None, staples: None,
recipes: BTreeMap::new(), recipes: BTreeMap::new(),
category_map: String::new(), category_map: String::new(),
@ -56,7 +57,8 @@ pub enum Message {
ResetRecipeCounts, ResetRecipeCounts,
UpdateRecipeCount(String, usize), UpdateRecipeCount(String, usize),
AddExtra(String, String), AddExtra(String, String),
RemoveExtra(String, String), RemoveExtra(usize),
UpdateExtra(usize, String, String),
SaveRecipe(RecipeEntry), SaveRecipe(RecipeEntry),
SetRecipe(String, Recipe), SetRecipe(String, Recipe),
// TODO(jwall): Remove this annotation when safe to do so. // TODO(jwall): Remove this annotation when safe to do so.
@ -171,7 +173,7 @@ impl StateMachine {
Ok((filtered_ingredients, modified_amts, extra_items)) => { Ok((filtered_ingredients, modified_amts, extra_items)) => {
state.modified_amts = modified_amts; state.modified_amts = modified_amts;
state.filtered_ingredients = filtered_ingredients; state.filtered_ingredients = filtered_ingredients;
state.extras = BTreeSet::from_iter(extra_items); state.extras = extra_items;
} }
Err(e) => { Err(e) => {
error!("{:?}", e); error!("{:?}", e);
@ -198,11 +200,20 @@ impl MessageMapper<Message, AppState> for StateMachine {
original_copy.recipe_counts.insert(id, count); original_copy.recipe_counts.insert(id, count);
} }
Message::AddExtra(amt, name) => { Message::AddExtra(amt, name) => {
original_copy.extras.insert((amt, name)); original_copy.extras.push((amt, name));
} }
Message::RemoveExtra(amt, name) => { Message::RemoveExtra(idx) => {
original_copy.extras.remove(&(amt, name)); original_copy.extras.remove(idx);
} }
Message::UpdateExtra(idx, amt, name) => match original_copy.extras.get_mut(idx) {
Some(extra) => {
extra.0 = amt;
extra.1 = name;
}
None => {
throw_str("Attempted to remove extra that didn't exist");
}
},
Message::SetStaples(staples) => { Message::SetStaples(staples) => {
original_copy.staples = staples; original_copy.staples = staples;
} }
@ -240,7 +251,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
Message::ResetInventory => { Message::ResetInventory => {
original_copy.filtered_ingredients = BTreeSet::new(); original_copy.filtered_ingredients = BTreeSet::new();
original_copy.modified_amts = BTreeMap::new(); original_copy.modified_amts = BTreeMap::new();
original_copy.extras = BTreeSet::new(); original_copy.extras = Vec::new();
} }
Message::AddFilteredIngredient(key) => { Message::AddFilteredIngredient(key) => {
original_copy.filtered_ingredients.insert(key); original_copy.filtered_ingredients.insert(key);

View File

@ -15,17 +15,20 @@ use std::collections::BTreeSet;
use recipes::{IngredientAccumulator, IngredientKey}; use recipes::{IngredientAccumulator, IngredientKey};
use sycamore::prelude::*; use sycamore::prelude::*;
use tracing::{info, instrument}; use tracing::{debug, info, instrument};
use crate::app_state::{Message, StateHandler}; use crate::app_state::{Message, StateHandler};
#[instrument(skip_all)]
fn make_ingredients_rows<'ctx, G: Html>( fn make_ingredients_rows<'ctx, G: Html>(
cx: Scope<'ctx>, cx: Scope<'ctx>,
sh: StateHandler<'ctx>, sh: StateHandler<'ctx>,
show_staples: &'ctx ReadSignal<bool>, show_staples: &'ctx ReadSignal<bool>,
) -> View<G> { ) -> View<G> {
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();
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() {
for _ in 0..(*count) { for _ in 0..(*count) {
@ -45,7 +48,7 @@ fn make_ingredients_rows<'ctx, G: Html>(
acc.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))| {
if state.modified_amts.contains_key(&k) { if state.modified_amts.contains_key(&k) {
@ -89,9 +92,6 @@ fn make_ingredients_rows<'ctx, G: Html>(
}; };
let amt_signal = create_signal(cx, amt); let amt_signal = create_signal(cx, amt);
let k_clone = k.clone(); 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 form = form.map(|form| format!("({})", form)).unwrap_or_default();
let recipes = rs let recipes = rs
.iter() .iter()
@ -101,7 +101,9 @@ fn make_ingredients_rows<'ctx, G: Html>(
view! {cx, view! {cx,
tr { tr {
td { td {
input(bind:value=amt_signal, type="text") input(bind:value=amt_signal, type="text", on:change=move |_| {
sh.dispatch(cx, Message::UpdateAmt(k_clone.clone(), amt_signal.get_untracked().as_ref().clone()));
})
} }
td { td {
input(type="button", class="no-print destructive", value="X", on:click={ input(type="button", class="no-print destructive", value="X", on:click={
@ -118,50 +120,44 @@ fn make_ingredients_rows<'ctx, G: Html>(
) )
} }
#[instrument(skip_all)]
fn make_extras_rows<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> { fn make_extras_rows<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> {
debug!("Making extras rows");
let extras_read_signal = sh.get_selector(cx, |state| { let extras_read_signal = sh.get_selector(cx, |state| {
state state.get().extras.iter().cloned().enumerate().collect()
.get()
.extras
.iter()
.cloned()
.collect::<Vec<(String, String)>>()
}); });
view! {cx, view! {cx,
Indexed( Indexed(
iterable=extras_read_signal, iterable=extras_read_signal,
view= move |cx, (amt, name)| { view= move |cx, (idx, (amt, name))| {
let amt_signal = create_signal(cx, amt.clone()); let amt_signal = create_signal(cx, amt.clone());
let name_signal = create_signal(cx, name.clone()); let name_signal = create_signal(cx, name.clone());
create_effect(cx, { view! {cx,
let amt_clone = amt.clone(); tr {
let name_clone = name.clone(); td {
move || { input(bind:value=amt_signal, type="text", on:change=move |_| {
let new_amt = amt_signal.get(); sh.dispatch(cx, Message::UpdateExtra(idx,
let new_name = name_signal.get(); amt_signal.get_untracked().as_ref().clone(),
sh.dispatch(cx, Message::RemoveExtra(amt_clone.clone(), name_clone.clone())); name_signal.get_untracked().as_ref().clone()));
sh.dispatch(cx, Message::AddExtra(new_amt.as_ref().clone(), new_name.as_ref().clone())); })
}
});
view! {cx,
tr {
td {
input(bind:value=amt_signal, type="text")
}
td {
input(type="button", class="no-print destructive", value="X", on:click={
move |_| {
sh.dispatch(cx, Message::RemoveExtra(amt.clone(), name.clone()));
}})
}
td {
input(bind:value=name_signal, type="text")
}
td { "Misc" }
}
} }
td {
input(type="button", class="no-print destructive", value="X", on:click=move |_| {
sh.dispatch(cx, Message::RemoveExtra(idx));
})
}
td {
input(bind:value=name_signal, type="text", on:change=move |_| {
sh.dispatch(cx, Message::UpdateExtra(idx,
amt_signal.get_untracked().as_ref().clone(),
name_signal.get_untracked().as_ref().clone()));
})
}
td { "Misc" }
} }
) }
}
)
} }
} }
@ -170,8 +166,7 @@ fn make_shopping_table<'ctx, G: Html>(
sh: StateHandler<'ctx>, sh: StateHandler<'ctx>,
show_staples: &'ctx ReadSignal<bool>, show_staples: &'ctx ReadSignal<bool>,
) -> View<G> { ) -> View<G> {
let extra_rows_view = make_extras_rows(cx, sh); debug!("Making shopping table");
let ingredient_rows = make_ingredients_rows(cx, sh, show_staples);
view! {cx, view! {cx,
table(class="pad-top shopping-list page-breaker container-fluid", role="grid") { table(class="pad-top shopping-list page-breaker container-fluid", role="grid") {
tr { tr {
@ -181,8 +176,8 @@ fn make_shopping_table<'ctx, G: Html>(
th { " Recipes " } th { " Recipes " }
} }
tbody { tbody {
(ingredient_rows) (make_ingredients_rows(cx, sh, show_staples))
(extra_rows_view) (make_extras_rows(cx, sh))
} }
} }
} }
@ -198,13 +193,15 @@ pub fn ShoppingList<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> V
input(id="show_staples_cb", type="checkbox", bind:checked=show_staples) input(id="show_staples_cb", type="checkbox", bind:checked=show_staples)
(make_shopping_table(cx, sh, show_staples)) (make_shopping_table(cx, sh, show_staples))
input(type="button", value="Add Item", class="no-print", on:click=move |_| { input(type="button", value="Add Item", class="no-print", on:click=move |_| {
info!("Registering add item request for inventory");
sh.dispatch(cx, Message::AddExtra(String::new(), String::new())); sh.dispatch(cx, Message::AddExtra(String::new(), String::new()));
}) })
input(type="button", value="Reset", class="no-print", on:click=move |_| { input(type="button", value="Reset", class="no-print", on:click=move |_| {
sh.dispatch(cx, Message::ResetInventory); info!("Registering reset request for inventory");
sh.dispatch(cx, Message::ResetInventory);
}) })
input(type="button", value="Save", class="no-print", on:click=move |_| { input(type="button", value="Save", class="no-print", on:click=move |_| {
info!("Registering save request for inventory"); info!("Registering save request for inventory");
sh.dispatch(cx, Message::SaveState); sh.dispatch(cx, Message::SaveState);
}) })
} }