From 02536d63d8c3222d0b83e78a414d86d5d207df54 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Fri, 23 Dec 2022 13:34:40 -0500 Subject: [PATCH 01/29] Implement a sycamore-state Handler for Kitchen --- Cargo.lock | 8 +++ api/src/lib.rs | 2 +- web/Cargo.toml | 1 + web/src/api.rs | 87 +++++++++++++++++++++- web/src/app_state.rs | 135 ++++++++++++++++++++++++++++++----- web/src/components/header.rs | 18 ++--- web/src/web.rs | 14 ++-- 7 files changed, 228 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c670a8..8b213cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1344,6 +1344,7 @@ dependencies = [ "serde_json", "sycamore", "sycamore-router", + "sycamore-state", "tracing", "tracing-subscriber", "tracing-web", @@ -2249,6 +2250,13 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "sycamore-state" +version = "0.0.1" +dependencies = [ + "sycamore", +] + [[package]] name = "sycamore-web" version = "0.8.2" diff --git a/api/src/lib.rs b/api/src/lib.rs index 294ae2b..7f0dd1c 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -103,7 +103,7 @@ pub type CategoryResponse = Response; pub type EmptyResponse = Response<()>; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct UserData { pub user_id: String, } diff --git a/web/Cargo.toml b/web/Cargo.toml index cf4fa30..990d670 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -15,6 +15,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] recipes = { path = "../recipes" } client-api = { path = "../api", package="api", features = ["browser"] } +sycamore-state = { path = "../../sycamore-state"} # This makes debugging panics more tractable. console_error_panic_hook = "0.1.7" serde_json = "1.0.79" diff --git a/web/src/api.rs b/web/src/api.rs index 754970d..6863e2d 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -17,13 +17,17 @@ use base64; use reqwasm; use serde_json::{from_str, to_string}; use sycamore::prelude::*; +use sycamore_state::Handler; use tracing::{debug, error, info, instrument, warn}; use client_api::*; use recipes::{parse, IngredientKey, Recipe, RecipeEntry}; use wasm_bindgen::JsValue; -use crate::{app_state, js_lib}; +use crate::{ + app_state::{self, AppState, Message, StateMachine}, + js_lib, +}; #[instrument] fn filter_recipes( @@ -53,6 +57,87 @@ fn filter_recipes( } } +pub async fn init_app_state<'ctx>( + cx: Scope<'ctx>, + h: &'ctx Handler<'ctx, StateMachine, AppState, Message>, +) { + info!("Synchronizing Recipes"); + let store = HttpStore::get_from_context(cx); + // TODO(jwall): Make our caching logic using storage more robust. + let recipe_entries = match store.get_recipes().await { + Ok(recipe_entries) => { + if let Ok((staples, recipes)) = filter_recipes(&recipe_entries) { + h.dispatch(Message::SetStaples(staples)); + if let Some(recipes) = recipes { + h.dispatch(Message::InitRecipes(recipes)); + } + } + recipe_entries + } + Err(err) => { + error!(?err); + None + } + }; + + if let Ok(Some(plan)) = store.get_plan().await { + // set the counts. + let mut plan_map = BTreeMap::new(); + for (id, count) in plan { + plan_map.insert(id, count as usize); + } + h.dispatch(Message::InitRecipeCounts(plan_map)); + } else { + // Initialize things to zero + if let Some(rs) = recipe_entries { + for r in rs { + h.dispatch(Message::UpdateRecipeCount(r.recipe_id().to_owned(), 0)) + } + } + } + info!("Checking for user_data in local storage"); + let storage = js_lib::get_storage(); + let user_data = storage + .get("user_data") + .expect("Couldn't read from storage"); + if let Some(data) = user_data { + if let Ok(user_data) = from_str(&data) { + h.dispatch(Message::SetUserData(user_data)); + } + } + info!("Synchronizing categories"); + match store.get_categories().await { + Ok(Some(categories_content)) => { + debug!(categories=?categories_content); + match recipes::parse::as_categories(&categories_content) { + Ok(category_map) => { + h.dispatch(Message::SetCategoryMap(category_map)); + } + Err(err) => { + error!(?err) + } + }; + } + Ok(None) => { + warn!("There is no category file"); + } + Err(e) => { + error!("{:?}", e); + } + } + info!("Synchronizing inventory data"); + match store.get_inventory_data().await { + Ok((filtered_ingredients, modified_amts, extra_items)) => { + h.dispatch(Message::InitAmts(modified_amts)); + h.dispatch(Message::InitFilteredIngredient(filtered_ingredients)); + h.dispatch(Message::InitExtras(BTreeSet::from_iter(extra_items))); + } + Err(e) => { + error!("{:?}", e); + } + } +} + #[instrument(skip(state))] pub async fn init_page_state(store: &HttpStore, state: &app_state::State) -> Result<(), String> { info!("Synchronizing Recipes"); diff --git a/web/src/app_state.rs b/web/src/app_state.rs index 7d77fe5..81e921e 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -19,6 +19,124 @@ use tracing::{debug, instrument, warn}; use client_api::UserData; use recipes::{Ingredient, IngredientAccumulator, IngredientKey, Recipe}; +use sycamore_state::{Handler, MessageMapper}; + +#[derive(Debug, Clone, PartialEq)] +pub struct AppState { + pub recipe_counts: BTreeMap, + pub extras: BTreeSet<(String, String)>, + pub staples: Option, + pub recipes: BTreeMap, + pub category_map: BTreeMap, + pub filtered_ingredients: BTreeSet, + pub modified_amts: BTreeMap, + pub auth: Option, +} + +impl AppState { + pub fn new() -> Self { + Self { + recipe_counts: BTreeMap::new(), + extras: BTreeSet::new(), + staples: None, + recipes: BTreeMap::new(), + category_map: BTreeMap::new(), + filtered_ingredients: BTreeSet::new(), + modified_amts: BTreeMap::new(), + auth: None, + } + } +} + +pub enum Message { + InitRecipeCounts(BTreeMap), + UpdateRecipeCount(String, usize), + InitExtras(BTreeSet<(String, String)>), + AddExtra(String, String), + RemoveExtra(String, String), + InitRecipes(BTreeMap), + SetRecipe(String, Recipe), + RemoveRecipe(String), + SetStaples(Option), + SetCategoryMap(BTreeMap), + InitFilteredIngredient(BTreeSet), + AddFilteredIngredient(IngredientKey), + RemoveFilteredIngredient(IngredientKey), + InitAmts(BTreeMap), + UpdateAmt(IngredientKey, String), + SetUserData(UserData), + UnsetUserData, +} + +// TODO(jwall): Add HttpStore here and do the proper things for each event. +pub struct StateMachine(); + +impl MessageMapper for StateMachine { + fn map(&self, msg: Message, original: &ReadSignal) -> AppState { + let mut original_copy = original.get().as_ref().clone(); + match msg { + Message::InitRecipeCounts(map) => { + original_copy.recipe_counts = map; + } + Message::UpdateRecipeCount(id, count) => { + original_copy.recipe_counts.insert(id, count); + } + Message::InitExtras(set) => { + original_copy.extras = set; + } + Message::AddExtra(amt, name) => { + original_copy.extras.insert((amt, name)); + } + Message::RemoveExtra(amt, name) => { + original_copy.extras.remove(&(amt, name)); + } + Message::SetStaples(staples) => { + original_copy.staples = staples; + } + Message::InitRecipes(recipes) => { + original_copy.recipes = recipes; + } + Message::SetRecipe(id, recipe) => { + original_copy.recipes.insert(id, recipe); + } + Message::RemoveRecipe(id) => { + original_copy.recipes.remove(&id); + } + Message::SetCategoryMap(map) => { + original_copy.category_map = map; + } + Message::InitFilteredIngredient(set) => { + original_copy.filtered_ingredients = set; + } + Message::AddFilteredIngredient(key) => { + original_copy.filtered_ingredients.insert(key); + } + Message::RemoveFilteredIngredient(key) => { + original_copy.filtered_ingredients.remove(&key); + } + Message::InitAmts(map) => { + original_copy.modified_amts = map; + } + Message::UpdateAmt(key, amt) => { + original_copy.modified_amts.insert(key, amt); + } + Message::SetUserData(user_data) => { + original_copy.auth = Some(user_data); + } + Message::UnsetUserData => { + original_copy.auth = None; + } + } + original_copy + } +} + +pub fn get_state_handler<'ctx>( + cx: Scope<'ctx>, + initial: AppState, +) -> &'ctx Handler<'ctx, StateMachine, AppState, Message> { + Handler::new(cx, initial, StateMachine()) +} #[derive(Debug)] pub struct State { pub recipe_counts: RcSignal>>, @@ -32,23 +150,6 @@ pub struct State { } impl State { - pub fn new() -> Self { - Self { - recipe_counts: create_rc_signal(BTreeMap::new()), - extras: create_rc_signal(Vec::new()), - staples: create_rc_signal(None), - recipes: create_rc_signal(BTreeMap::new()), - category_map: create_rc_signal(BTreeMap::new()), - filtered_ingredients: create_rc_signal(BTreeSet::new()), - modified_amts: create_rc_signal(BTreeMap::new()), - auth: create_rc_signal(None), - } - } - - pub fn provide_context(cx: Scope) { - provide_context(cx, std::rc::Rc::new(Self::new())); - } - pub fn get_from_context(cx: Scope) -> std::rc::Rc { use_context::>(cx).clone() } diff --git a/web/src/components/header.rs b/web/src/components/header.rs index b3d45fc..80cf0a5 100644 --- a/web/src/components/header.rs +++ b/web/src/components/header.rs @@ -14,17 +14,17 @@ use sycamore::prelude::*; -use crate::app_state; +use crate::app_state::{AppState, Message, StateMachine}; +use sycamore_state::Handler; #[component] -pub fn Header(cx: Scope) -> View { - let state = app_state::State::get_from_context(cx); - let login = create_memo(cx, move || { - let user_id = state.auth.get(); - match user_id.as_ref() { - Some(user_data) => format!("{}", user_data.user_id), - None => "Login".to_owned(), - } +pub fn Header<'ctx, G: Html>( + cx: Scope<'ctx>, + h: &'ctx Handler<'ctx, StateMachine, AppState, Message>, +) -> View { + let login = h.get_selector(cx, |sig| match &sig.get().auth { + Some(id) => id.user_id.clone(), + None => "Login".to_owned(), }); view! {cx, nav(class="no-print") { diff --git a/web/src/web.rs b/web/src/web.rs index 6f3625b..efedbf3 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. use sycamore::{futures::spawn_local_scoped, prelude::*}; -use tracing::{error, info, instrument}; +use tracing::{info, instrument}; use crate::components::{Footer, Header}; use crate::{api, routing::Handler as RouteHandler}; @@ -20,24 +20,20 @@ use crate::{api, routing::Handler as RouteHandler}; #[instrument] #[component] pub fn UI(cx: Scope) -> View { - crate::app_state::State::provide_context(cx); api::HttpStore::provide_context(cx, "/api".to_owned()); info!("Starting UI"); - + let app_state = crate::app_state::AppState::new(); + let handler = crate::app_state::get_state_handler(cx, app_state); let view = create_signal(cx, View::empty()); // FIXME(jwall): We need a way to trigger refreshes when required. Turn this // into a create_effect with a refresh signal stored as a context. spawn_local_scoped(cx, { - let store = api::HttpStore::get_from_context(cx); - let state = crate::app_state::State::get_from_context(cx); async move { - if let Err(err) = api::init_page_state(store.as_ref(), state.as_ref()).await { - error!(?err); - }; + api::init_app_state(cx, handler).await; // TODO(jwall): This needs to be moved into the RouteHandler view.set(view! { cx, div(class="app") { - Header { } + Header(handler) RouteHandler() Footer { } } From e21353eebaf84c98256b31a0f5aaa9b4f6d0b8f9 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Mon, 26 Dec 2022 21:29:09 -0500 Subject: [PATCH 02/29] use new state handler in RouteHandler --- web/src/app_state.rs | 7 +++---- web/src/components/header.rs | 8 ++------ web/src/routing/mod.rs | 26 ++++++++++++++++++++------ web/src/web.rs | 2 +- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/web/src/app_state.rs b/web/src/app_state.rs index 81e921e..63f2322 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -131,10 +131,9 @@ impl MessageMapper for StateMachine { } } -pub fn get_state_handler<'ctx>( - cx: Scope<'ctx>, - initial: AppState, -) -> &'ctx Handler<'ctx, StateMachine, AppState, Message> { +pub type StateHandler<'ctx> = &'ctx Handler<'ctx, StateMachine, AppState, Message>; + +pub fn get_state_handler<'ctx>(cx: Scope<'ctx>, initial: AppState) -> StateHandler<'ctx> { Handler::new(cx, initial, StateMachine()) } #[derive(Debug)] diff --git a/web/src/components/header.rs b/web/src/components/header.rs index 80cf0a5..438c866 100644 --- a/web/src/components/header.rs +++ b/web/src/components/header.rs @@ -14,14 +14,10 @@ use sycamore::prelude::*; -use crate::app_state::{AppState, Message, StateMachine}; -use sycamore_state::Handler; +use crate::app_state::StateHandler; #[component] -pub fn Header<'ctx, G: Html>( - cx: Scope<'ctx>, - h: &'ctx Handler<'ctx, StateMachine, AppState, Message>, -) -> View { +pub fn Header<'ctx, G: Html>(cx: Scope<'ctx>, h: StateHandler<'ctx>) -> View { let login = h.get_selector(cx, |sig| match &sig.get().auth { Some(id) => id.user_id.clone(), None => "Login".to_owned(), diff --git a/web/src/routing/mod.rs b/web/src/routing/mod.rs index 8e87c20..c8c3024 100644 --- a/web/src/routing/mod.rs +++ b/web/src/routing/mod.rs @@ -12,19 +12,25 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::app_state::StateHandler; use sycamore::prelude::*; use sycamore_router::{HistoryIntegration, Route, Router}; + use tracing::{debug, instrument}; use crate::pages::*; -#[instrument] -fn route_switch<'a, G: Html>(cx: Scope<'a>, route: &'a ReadSignal) -> View { +#[instrument(skip_all, fields(?route))] +fn route_switch<'ctx, G: Html>( + cx: Scope<'ctx>, + sh: StateHandler<'ctx>, + route: &'ctx ReadSignal, +) -> View { // NOTE(jwall): This needs to not be a dynamic node. The rules around // this are somewhat unclear and underdocumented for Sycamore. But basically // avoid conditionals in the `view!` macro calls here. - let switcher = |cx: Scope, route: &Routes| { + let switcher = |cx: Scope, sh: StateHandler, route: &Routes| { debug!(?route, "Dispatching for route"); match route { Routes::Planning(Plan) => view! {cx, @@ -65,7 +71,7 @@ fn route_switch<'a, G: Html>(cx: Scope<'a>, route: &'a ReadSignal) -> Vi }; use PlanningRoutes::*; view! {cx, - (switcher(cx, route.get().as_ref())) + (switcher(cx, sh, route.get().as_ref())) } } @@ -117,12 +123,20 @@ pub enum PlanningRoutes { NotFound, } +#[derive(Props)] +pub struct HandlerProps<'ctx> { + sh: StateHandler<'ctx>, +} + #[component] -pub fn Handler(cx: Scope) -> View { +pub fn Handler<'ctx, G: Html>(cx: Scope<'ctx>, props: HandlerProps<'ctx>) -> View { + let HandlerProps { sh } = props; view! {cx, Router( integration=HistoryIntegration::new(), - view=route_switch, + view=|cx, route| { + route_switch(cx, sh, route) + }, ) } } diff --git a/web/src/web.rs b/web/src/web.rs index efedbf3..a16932d 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -34,7 +34,7 @@ pub fn UI(cx: Scope) -> View { view.set(view! { cx, div(class="app") { Header(handler) - RouteHandler() + RouteHandler(sh=handler) Footer { } } }); From 90346d55eb2cdb60d52f7eec231433e6c67736dd Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Mon, 26 Dec 2022 21:56:28 -0500 Subject: [PATCH 03/29] Thread statehandler through router to pages --- web/src/pages/login.rs | 4 +- web/src/pages/manage/add_recipe.rs | 4 +- web/src/pages/manage/categories.rs | 4 +- web/src/pages/manage/staples.rs | 6 +- web/src/pages/planning/cook.rs | 4 +- web/src/pages/planning/inventory.rs | 4 +- web/src/pages/planning/plan.rs | 4 +- web/src/pages/recipe/edit.rs | 7 +- web/src/pages/recipe/mod.rs | 7 +- web/src/pages/recipe/view.rs | 7 +- web/src/routing/mod.rs | 99 ++++++++++++----------------- 11 files changed, 67 insertions(+), 83 deletions(-) diff --git a/web/src/pages/login.rs b/web/src/pages/login.rs index bd0a8c3..0f4a243 100644 --- a/web/src/pages/login.rs +++ b/web/src/pages/login.rs @@ -14,7 +14,7 @@ use sycamore::{futures::spawn_local_scoped, prelude::*}; use tracing::{debug, info}; -use crate::app_state; +use crate::app_state::{self, StateHandler}; #[component] pub fn LoginForm(cx: Scope) -> View { @@ -50,7 +50,7 @@ pub fn LoginForm(cx: Scope) -> View { } #[component] -pub fn LoginPage(cx: Scope) -> View { +pub fn LoginPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { view! {cx, LoginForm() } diff --git a/web/src/pages/manage/add_recipe.rs b/web/src/pages/manage/add_recipe.rs index 7f68088..68830b7 100644 --- a/web/src/pages/manage/add_recipe.rs +++ b/web/src/pages/manage/add_recipe.rs @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. use super::ManagePage; -use crate::components::add_recipe::AddRecipe; +use crate::{app_state::StateHandler, components::add_recipe::AddRecipe}; use sycamore::prelude::*; #[component] -pub fn AddRecipePage(cx: Scope) -> View { +pub fn AddRecipePage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { view! {cx, ManagePage( selected=Some("New Recipe".to_owned()), diff --git a/web/src/pages/manage/categories.rs b/web/src/pages/manage/categories.rs index 1ebbc83..ac78bf6 100644 --- a/web/src/pages/manage/categories.rs +++ b/web/src/pages/manage/categories.rs @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. use super::ManagePage; -use crate::components::categories::*; +use crate::{app_state::StateHandler, components::categories::*}; use sycamore::prelude::*; #[component()] -pub fn CategoryPage(cx: Scope) -> View { +pub fn CategoryPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { view! {cx, ManagePage( selected=Some("Categories".to_owned()), diff --git a/web/src/pages/manage/staples.rs b/web/src/pages/manage/staples.rs index 59361bf..d3aa9af 100644 --- a/web/src/pages/manage/staples.rs +++ b/web/src/pages/manage/staples.rs @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. use super::ManagePage; -use crate::components::recipe::Editor; +use crate::{app_state::StateHandler, components::recipe::Editor}; use sycamore::prelude::*; use tracing::instrument; -#[instrument] +#[instrument(skip_all)] #[component()] -pub fn StaplesPage(cx: Scope) -> View { +pub fn StaplesPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { view! {cx, ManagePage( selected=Some("Staples".to_owned()), diff --git a/web/src/pages/planning/cook.rs b/web/src/pages/planning/cook.rs index 60100b5..7779912 100644 --- a/web/src/pages/planning/cook.rs +++ b/web/src/pages/planning/cook.rs @@ -14,10 +14,10 @@ use sycamore::prelude::*; use super::PlanningPage; -use crate::components::recipe_list::*; +use crate::{app_state::StateHandler, components::recipe_list::*}; #[component] -pub fn CookPage(cx: Scope) -> View { +pub fn CookPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { view! {cx, PlanningPage( selected=Some("Cook".to_owned()), diff --git a/web/src/pages/planning/inventory.rs b/web/src/pages/planning/inventory.rs index 953503d..e6ea29f 100644 --- a/web/src/pages/planning/inventory.rs +++ b/web/src/pages/planning/inventory.rs @@ -14,10 +14,10 @@ use sycamore::prelude::*; use super::PlanningPage; -use crate::components::shopping_list::*; +use crate::{app_state::StateHandler, components::shopping_list::*}; #[component] -pub fn InventoryPage(cx: Scope) -> View { +pub fn InventoryPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { view! {cx, PlanningPage( selected=Some("Inventory".to_owned()), diff --git a/web/src/pages/planning/plan.rs b/web/src/pages/planning/plan.rs index ef6a0cf..838eb76 100644 --- a/web/src/pages/planning/plan.rs +++ b/web/src/pages/planning/plan.rs @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. use super::PlanningPage; -use crate::components::recipe_plan::*; +use crate::{app_state::StateHandler, components::recipe_plan::*}; use sycamore::prelude::*; #[component] -pub fn PlanPage(cx: Scope) -> View { +pub fn PlanPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { view! {cx, PlanningPage( selected=Some("Plan".to_owned()), diff --git a/web/src/pages/recipe/edit.rs b/web/src/pages/recipe/edit.rs index 9e29ae1..fa6e9b5 100644 --- a/web/src/pages/recipe/edit.rs +++ b/web/src/pages/recipe/edit.rs @@ -17,13 +17,14 @@ use crate::components::recipe::Editor; use sycamore::prelude::*; use tracing::instrument; -#[instrument] +#[instrument(skip_all, fields(recipe=props.recipe))] #[component()] pub fn RecipeEditPage(cx: Scope, props: RecipePageProps) -> View { + let RecipePageProps { recipe, sh } = props; view! {cx, RecipePage( selected=Some("Edit".to_owned()), - recipe=props.recipe.clone(), - ) { Editor(props.recipe) } + recipe=recipe.clone(), + ) { Editor(recipe) } } } diff --git a/web/src/pages/recipe/mod.rs b/web/src/pages/recipe/mod.rs index f137c8c..9e4a5c1 100644 --- a/web/src/pages/recipe/mod.rs +++ b/web/src/pages/recipe/mod.rs @@ -13,16 +13,17 @@ // limitations under the License. use sycamore::prelude::*; -use crate::components::tabs::*; +use crate::{app_state::StateHandler, components::tabs::*}; mod edit; mod view; pub use edit::*; pub use view::*; -#[derive(Debug, Props)] -pub struct RecipePageProps { +#[derive(Props)] +pub struct RecipePageProps<'ctx> { pub recipe: String, + pub sh: StateHandler<'ctx>, } #[derive(Props)] diff --git a/web/src/pages/recipe/view.rs b/web/src/pages/recipe/view.rs index 8685608..b98331c 100644 --- a/web/src/pages/recipe/view.rs +++ b/web/src/pages/recipe/view.rs @@ -18,13 +18,14 @@ use tracing::instrument; use super::{RecipePage, RecipePageProps}; -#[instrument] +#[instrument(skip_all, fields(recipe=props.recipe))] #[component()] pub fn RecipeViewPage(cx: Scope, props: RecipePageProps) -> View { + let RecipePageProps { recipe, sh } = props; view! {cx, RecipePage( selected=Some("View".to_owned()), - recipe=props.recipe.clone(), - ) { Viewer(props.recipe) } + recipe=recipe.clone(), + ) { Viewer(recipe) } } } diff --git a/web/src/routing/mod.rs b/web/src/routing/mod.rs index c8c3024..1bba2b9 100644 --- a/web/src/routing/mod.rs +++ b/web/src/routing/mod.rs @@ -16,65 +16,8 @@ use crate::app_state::StateHandler; use sycamore::prelude::*; use sycamore_router::{HistoryIntegration, Route, Router}; -use tracing::{debug, instrument}; - use crate::pages::*; -#[instrument(skip_all, fields(?route))] -fn route_switch<'ctx, G: Html>( - cx: Scope<'ctx>, - sh: StateHandler<'ctx>, - route: &'ctx ReadSignal, -) -> View { - // NOTE(jwall): This needs to not be a dynamic node. The rules around - // this are somewhat unclear and underdocumented for Sycamore. But basically - // avoid conditionals in the `view!` macro calls here. - - let switcher = |cx: Scope, sh: StateHandler, route: &Routes| { - debug!(?route, "Dispatching for route"); - match route { - Routes::Planning(Plan) => view! {cx, - PlanPage() - }, - Routes::Planning(Inventory) => view! {cx, - InventoryPage() - }, - Routes::Planning(Cook) => view! {cx, - CookPage() - }, - Routes::Login => view! {cx, - LoginPage() - }, - Routes::Recipe(RecipeRoutes::View(id)) => view! {cx, - RecipeViewPage(recipe=id.clone()) - }, - Routes::Recipe(RecipeRoutes::Edit(id)) => view! {cx, - RecipeEditPage(recipe=id.clone()) - }, - Routes::Manage(ManageRoutes::Categories) => view! {cx, - CategoryPage() - }, - Routes::Manage(ManageRoutes::NewRecipe) => view! {cx, - AddRecipePage() - }, - Routes::Manage(ManageRoutes::Staples) => view! {cx, - StaplesPage() - }, - Routes::NotFound - | Routes::Manage(ManageRoutes::NotFound) - | Routes::Planning(PlanningRoutes::NotFound) - | Routes::Recipe(RecipeRoutes::NotFound) => view! {cx, - // TODO(Create a real one) - PlanPage() - }, - } - }; - use PlanningRoutes::*; - view! {cx, - (switcher(cx, sh, route.get().as_ref())) - } -} - #[derive(Route, Debug)] pub enum Routes { #[to("/ui/planning/<_..>")] @@ -131,11 +74,49 @@ pub struct HandlerProps<'ctx> { #[component] pub fn Handler<'ctx, G: Html>(cx: Scope<'ctx>, props: HandlerProps<'ctx>) -> View { let HandlerProps { sh } = props; + use ManageRoutes::*; + use PlanningRoutes::*; view! {cx, Router( integration=HistoryIntegration::new(), - view=|cx, route| { - route_switch(cx, sh, route) + view=move |cx: Scope, route: &ReadSignal| { + match route.get().as_ref() { + Routes::Planning(Plan) => view! {cx, + PlanPage(sh) + }, + Routes::Planning(Inventory) => view! {cx, + InventoryPage(sh) + }, + Routes::Planning(Cook) => view! {cx, + CookPage(sh) + }, + Routes::Login => view! {cx, + LoginPage(sh) + }, + Routes::Recipe(RecipeRoutes::View(id)) => view! {cx, + RecipeViewPage(recipe=id.clone(), sh=sh) + }, + Routes::Recipe(RecipeRoutes::Edit(id)) => view! {cx, + RecipeEditPage(recipe=id.clone(), sh=sh) + }, + Routes::Manage(Categories) => view! {cx, + CategoryPage(sh) + }, + Routes::Manage(NewRecipe) => view! {cx, + AddRecipePage(sh) + }, + Routes::Manage(Staples) => view! {cx, + StaplesPage(sh) + }, + Routes::NotFound + | Routes::Manage(ManageRoutes::NotFound) + | Routes::Planning(PlanningRoutes::NotFound) + | Routes::Recipe(RecipeRoutes::NotFound) => view! {cx, + // TODO(Create a real one) + PlanPage(sh) + }, + } + }, ) } From 9881d2972bb81f9f19b379b7d40425ab1fdcece1 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Mon, 26 Dec 2022 22:22:05 -0500 Subject: [PATCH 04/29] Use state handler in the recipe pages --- web/src/components/recipe.rs | 38 ++++++++++++++++++++++----------- web/src/pages/manage/staples.rs | 2 +- web/src/pages/recipe/edit.rs | 2 +- web/src/pages/recipe/view.rs | 2 +- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/web/src/components/recipe.rs b/web/src/components/recipe.rs index f4159a8..55f98a8 100644 --- a/web/src/components/recipe.rs +++ b/web/src/components/recipe.rs @@ -14,7 +14,7 @@ use sycamore::{futures::spawn_local_scoped, prelude::*}; use tracing::{debug, error}; -use crate::app_state; +use crate::app_state::{self, Message, StateHandler}; use recipes::{self, RecipeEntry}; fn check_recipe_parses( @@ -34,8 +34,15 @@ fn check_recipe_parses( } } +#[derive(Props)] +pub struct RecipeComponentProps<'ctx> { + recipe_id: String, + sh: StateHandler<'ctx>, +} + #[component] -pub fn Editor(cx: Scope, recipe_id: String) -> View { +pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>) -> View { + let RecipeComponentProps { recipe_id, sh } = props; let store = crate::api::HttpStore::get_from_context(cx); let recipe: &Signal = create_signal(cx, RecipeEntry::new(&recipe_id, String::new())); @@ -73,7 +80,6 @@ pub fn Editor(cx: Scope, recipe_id: String) -> View { debug!("Recipe text is changed"); spawn_local_scoped(cx, { let store = crate::api::HttpStore::get_from_context(cx); - let state = app_state::State::get_from_context(cx); async move { debug!("Attempting to save recipe"); if let Err(e) = store @@ -89,10 +95,10 @@ pub fn Editor(cx: Scope, recipe_id: String) -> View { // We also need to set recipe in our state dirty.set(false); if let Ok(recipe) = recipes::parse::as_recipe(text.get_untracked().as_ref()) { - state - .recipes - .modify() - .insert(id.get_untracked().as_ref().to_owned(), recipe); + sh.dispatch(Message::SetRecipe( + id.get_untracked().as_ref().to_owned(), + recipe, + )); } }; } @@ -154,13 +160,21 @@ fn Steps(cx: Scope, steps: Vec) -> View { } #[component] -pub fn Viewer(cx: Scope, recipe_id: String) -> View { +pub fn Viewer<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>) -> View { + let RecipeComponentProps { recipe_id, sh } = props; let state = app_state::State::get_from_context(cx); let view = create_signal(cx, View::empty()); - if let Some(recipe) = state.recipes.get_untracked().get(&recipe_id) { - let title = recipe.title.clone(); - let desc = recipe.desc.clone().unwrap_or_else(|| String::new()); - let steps = recipe.steps.clone(); + let recipe_signal = sh.get_selector(cx, |state| { + if let Some(recipe) = state.get().recipes.get(&recipe_id) { + let title = recipe.title.clone(); + let desc = recipe.desc.clone().unwrap_or_else(|| String::new()); + let steps = recipe.steps.clone(); + Some((title, desc, steps)) + } else { + None + } + }); + if let Some((title, desc, steps)) = recipe_signal.get().as_ref().clone() { debug!("Viewing recipe."); view.set(view! {cx, div(class="recipe") { diff --git a/web/src/pages/manage/staples.rs b/web/src/pages/manage/staples.rs index d3aa9af..318b4a2 100644 --- a/web/src/pages/manage/staples.rs +++ b/web/src/pages/manage/staples.rs @@ -23,6 +23,6 @@ pub fn StaplesPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vi view! {cx, ManagePage( selected=Some("Staples".to_owned()), - ) { Editor("staples.txt".to_owned()) } + ) { Editor(recipe_id="staples.txt".to_owned(), sh=sh) } } } diff --git a/web/src/pages/recipe/edit.rs b/web/src/pages/recipe/edit.rs index fa6e9b5..d3ca8e3 100644 --- a/web/src/pages/recipe/edit.rs +++ b/web/src/pages/recipe/edit.rs @@ -25,6 +25,6 @@ pub fn RecipeEditPage(cx: Scope, props: RecipePageProps) -> View { RecipePage( selected=Some("Edit".to_owned()), recipe=recipe.clone(), - ) { Editor(recipe) } + ) { Editor(recipe_id=recipe, sh=sh) } } } diff --git a/web/src/pages/recipe/view.rs b/web/src/pages/recipe/view.rs index b98331c..2ff892b 100644 --- a/web/src/pages/recipe/view.rs +++ b/web/src/pages/recipe/view.rs @@ -26,6 +26,6 @@ pub fn RecipeViewPage(cx: Scope, props: RecipePageProps) -> View { RecipePage( selected=Some("View".to_owned()), recipe=recipe.clone(), - ) { Viewer(recipe) } + ) { Viewer(recipe_id=recipe, sh=sh) } } } From 69dc4b99cfb41357377828296a0db8dca3e281ad Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Tue, 27 Dec 2022 23:00:48 -0500 Subject: [PATCH 05/29] Use handler in login form --- web/src/pages/login.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/web/src/pages/login.rs b/web/src/pages/login.rs index 0f4a243..99cf486 100644 --- a/web/src/pages/login.rs +++ b/web/src/pages/login.rs @@ -14,10 +14,10 @@ use sycamore::{futures::spawn_local_scoped, prelude::*}; use tracing::{debug, info}; -use crate::app_state::{self, StateHandler}; +use crate::app_state::{Message, StateHandler}; #[component] -pub fn LoginForm(cx: Scope) -> View { +pub fn LoginForm<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { let username = create_signal(cx, "".to_owned()); let password = create_signal(cx, "".to_owned()); let clicked = create_signal(cx, ("".to_owned(), "".to_owned())); @@ -25,11 +25,12 @@ pub fn LoginForm(cx: Scope) -> View { let (username, password) = (*clicked.get()).clone(); if username != "" && password != "" { spawn_local_scoped(cx, async move { - let state = app_state::State::get_from_context(cx); let store = crate::api::HttpStore::get_from_context(cx); debug!("authenticating against ui"); // TODO(jwall): Navigate to plan if the below is successful. - state.auth.set(store.authenticate(username, password).await); + if let Some(user_data) = store.authenticate(username, password).await { + sh.dispatch(Message::SetUserData(user_data)); + } }); } }); @@ -52,6 +53,6 @@ pub fn LoginForm(cx: Scope) -> View { #[component] pub fn LoginPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { view! {cx, - LoginForm() + LoginForm(sh) } } From fbb4e4ceeb8954ed5005734a2e0e06d32d1e59b5 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Tue, 27 Dec 2022 22:08:50 -0600 Subject: [PATCH 06/29] Fix some lifetime annotation issues --- web/src/components/recipe.rs | 2 +- web/src/components/recipe_list.rs | 13 ++++++++----- web/src/pages/planning/cook.rs | 2 +- web/src/pages/recipe/edit.rs | 2 +- web/src/pages/recipe/view.rs | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/web/src/components/recipe.rs b/web/src/components/recipe.rs index 55f98a8..0fdea98 100644 --- a/web/src/components/recipe.rs +++ b/web/src/components/recipe.rs @@ -164,7 +164,7 @@ pub fn Viewer<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>) let RecipeComponentProps { recipe_id, sh } = props; let state = app_state::State::get_from_context(cx); let view = create_signal(cx, View::empty()); - let recipe_signal = sh.get_selector(cx, |state| { + let recipe_signal = sh.get_selector(cx, move |state| { if let Some(recipe) = state.get().recipes.get(&recipe_id) { let title = recipe.title.clone(); let desc = recipe.desc.clone().unwrap_or_else(|| String::new()); diff --git a/web/src/components/recipe_list.rs b/web/src/components/recipe_list.rs index 3cc3ffe..062c828 100644 --- a/web/src/components/recipe_list.rs +++ b/web/src/components/recipe_list.rs @@ -11,14 +11,17 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use crate::{app_state, components::recipe::Viewer}; +use crate::{ + app_state::{self, StateHandler}, + components::recipe::Viewer, +}; use sycamore::prelude::*; use tracing::{debug, instrument}; -#[instrument] +#[instrument(skip_all)] #[component] -pub fn RecipeList(cx: Scope) -> View { +pub fn RecipeList<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { let state = app_state::State::get_from_context(cx); let menu_list = create_memo(cx, move || state.get_menu_list()); view! {cx, @@ -26,10 +29,10 @@ pub fn RecipeList(cx: Scope) -> View { div() { Indexed( iterable=menu_list, - view= |cx, (id, _count)| { + view= move |cx, (id, _count)| { debug!(id=%id, "Rendering recipe"); view ! {cx, - Viewer(id) + Viewer(recipe_id=id, sh=sh) hr() } } diff --git a/web/src/pages/planning/cook.rs b/web/src/pages/planning/cook.rs index 7779912..2ae3cf9 100644 --- a/web/src/pages/planning/cook.rs +++ b/web/src/pages/planning/cook.rs @@ -21,6 +21,6 @@ pub fn CookPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View< view! {cx, PlanningPage( selected=Some("Cook".to_owned()), - ) { RecipeList() } + ) { RecipeList(sh) } } } diff --git a/web/src/pages/recipe/edit.rs b/web/src/pages/recipe/edit.rs index d3ca8e3..be2e8bf 100644 --- a/web/src/pages/recipe/edit.rs +++ b/web/src/pages/recipe/edit.rs @@ -19,7 +19,7 @@ use tracing::instrument; #[instrument(skip_all, fields(recipe=props.recipe))] #[component()] -pub fn RecipeEditPage(cx: Scope, props: RecipePageProps) -> View { +pub fn RecipeEditPage<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipePageProps<'ctx>) -> View { let RecipePageProps { recipe, sh } = props; view! {cx, RecipePage( diff --git a/web/src/pages/recipe/view.rs b/web/src/pages/recipe/view.rs index 2ff892b..54ef784 100644 --- a/web/src/pages/recipe/view.rs +++ b/web/src/pages/recipe/view.rs @@ -20,7 +20,7 @@ use super::{RecipePage, RecipePageProps}; #[instrument(skip_all, fields(recipe=props.recipe))] #[component()] -pub fn RecipeViewPage(cx: Scope, props: RecipePageProps) -> View { +pub fn RecipeViewPage<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipePageProps<'ctx>) -> View { let RecipePageProps { recipe, sh } = props; view! {cx, RecipePage( From e77fe40d752506690eaa0bf31ce845c8d40fc914 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 28 Dec 2022 11:54:24 -0600 Subject: [PATCH 07/29] Introduce a save state message --- web/src/api.rs | 26 +++++++++++++++++++++++--- web/src/app_state.rs | 30 ++++++++++++++++++++++++------ web/src/components/recipe_plan.rs | 31 +++++++++---------------------- web/src/pages/planning/plan.rs | 2 +- web/src/web.rs | 7 +++++-- 5 files changed, 62 insertions(+), 34 deletions(-) diff --git a/web/src/api.rs b/web/src/api.rs index 6863e2d..e73c88a 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -58,11 +58,10 @@ fn filter_recipes( } pub async fn init_app_state<'ctx>( - cx: Scope<'ctx>, + store: &HttpStore, h: &'ctx Handler<'ctx, StateMachine, AppState, Message>, ) { info!("Synchronizing Recipes"); - let store = HttpStore::get_from_context(cx); // TODO(jwall): Make our caching logic using storage more robust. let recipe_entries = match store.get_recipes().await { Ok(recipe_entries) => { @@ -270,7 +269,7 @@ pub struct HttpStore { } impl HttpStore { - fn new(root: String) -> Self { + pub fn new(root: String) -> Self { Self { root } } @@ -493,6 +492,27 @@ impl HttpStore { } } + #[instrument(skip_all)] + pub async fn save_app_state(&self, state: AppState) -> Result<(), Error> { + let mut plan = Vec::new(); + for (key, count) in state.recipe_counts.iter() { + plan.push((key.clone(), *count as i32)); + } + debug!("Saving plan data"); + self.save_plan(plan).await?; + debug!("Saving inventory data"); + self.save_inventory_data( + state.filtered_ingredients, + state.modified_amts, + state + .extras + .iter() + .cloned() + .collect::>(), + ) + .await + } + #[instrument] pub async fn save_state(&self, state: std::rc::Rc) -> Result<(), Error> { let mut plan = Vec::new(); diff --git a/web/src/app_state.rs b/web/src/app_state.rs index 63f2322..057bbc7 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -13,14 +13,16 @@ // limitations under the License. use std::collections::{BTreeMap, BTreeSet}; -use sycamore::prelude::*; -use tracing::{debug, instrument, warn}; +use sycamore::{futures::spawn_local, prelude::*}; +use tracing::{debug, error, instrument, warn}; use client_api::UserData; use recipes::{Ingredient, IngredientAccumulator, IngredientKey, Recipe}; use sycamore_state::{Handler, MessageMapper}; +use crate::api::HttpStore; + #[derive(Debug, Clone, PartialEq)] pub struct AppState { pub recipe_counts: BTreeMap, @@ -48,6 +50,7 @@ impl AppState { } } +#[derive(Debug)] pub enum Message { InitRecipeCounts(BTreeMap), UpdateRecipeCount(String, usize), @@ -66,12 +69,13 @@ pub enum Message { UpdateAmt(IngredientKey, String), SetUserData(UserData), UnsetUserData, + SaveState, } -// TODO(jwall): Add HttpStore here and do the proper things for each event. -pub struct StateMachine(); +pub struct StateMachine(HttpStore); impl MessageMapper for StateMachine { + #[instrument(skip_all, fields(?msg))] fn map(&self, msg: Message, original: &ReadSignal) -> AppState { let mut original_copy = original.get().as_ref().clone(); match msg { @@ -126,6 +130,15 @@ impl MessageMapper for StateMachine { Message::UnsetUserData => { original_copy.auth = None; } + Message::SaveState => { + let store = self.0.clone(); + let original_copy = original_copy.clone(); + spawn_local(async move { + if let Err(e) = store.save_app_state(original_copy).await { + error!(err=?e, "Error saving app state") + }; + }); + } } original_copy } @@ -133,9 +146,14 @@ impl MessageMapper for StateMachine { pub type StateHandler<'ctx> = &'ctx Handler<'ctx, StateMachine, AppState, Message>; -pub fn get_state_handler<'ctx>(cx: Scope<'ctx>, initial: AppState) -> StateHandler<'ctx> { - Handler::new(cx, initial, StateMachine()) +pub fn get_state_handler<'ctx>( + cx: Scope<'ctx>, + initial: AppState, + store: HttpStore, +) -> StateHandler<'ctx> { + Handler::new(cx, initial, StateMachine(store)) } + #[derive(Debug)] pub struct State { pub recipe_counts: RcSignal>>, diff --git a/web/src/components/recipe_plan.rs b/web/src/components/recipe_plan.rs index ab409fb..3633c0d 100644 --- a/web/src/components/recipe_plan.rs +++ b/web/src/components/recipe_plan.rs @@ -13,21 +13,20 @@ // limitations under the License. use recipes::Recipe; use sycamore::{futures::spawn_local_scoped, prelude::*}; -use tracing::{error, instrument}; +use tracing::instrument; +use crate::app_state::{Message, StateHandler}; use crate::components::recipe_selection::*; use crate::{api::*, app_state}; #[allow(non_snake_case)] -#[instrument] -pub fn RecipePlan(cx: Scope) -> View { - let rows = create_memo(cx, move || { - let state = app_state::State::get_from_context(cx); +#[instrument(skip_all)] +pub fn RecipePlan<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { + let rows = sh.get_selector(cx, move |state| { let mut rows = Vec::new(); for row in state - .recipes .get() - .as_ref() + .recipes .iter() .map(|(k, v)| create_signal(cx, (k.clone(), v.clone()))) .collect::>>() @@ -39,27 +38,15 @@ pub fn RecipePlan(cx: Scope) -> View { }); let refresh_click = create_signal(cx, false); let save_click = create_signal(cx, false); + // FIXME(jwall): We should probably make this a dispatch method instead. create_effect(cx, move || { refresh_click.track(); let store = HttpStore::get_from_context(cx); - let state = app_state::State::get_from_context(cx); - spawn_local_scoped(cx, { - async move { - if let Err(err) = init_page_state(store.as_ref(), state.as_ref()).await { - error!(?err); - }; - } - }); + spawn_local_scoped(cx, async move { init_app_state(store.as_ref(), sh).await }); }); create_effect(cx, move || { save_click.track(); - let store = HttpStore::get_from_context(cx); - let state = app_state::State::get_from_context(cx); - spawn_local_scoped(cx, { - async move { - store.save_state(state).await.expect("Failed to save plan"); - } - }) + sh.dispatch(Message::SaveState); }); view! {cx, table(class="recipe_selector no-print") { diff --git a/web/src/pages/planning/plan.rs b/web/src/pages/planning/plan.rs index 838eb76..f3f4975 100644 --- a/web/src/pages/planning/plan.rs +++ b/web/src/pages/planning/plan.rs @@ -21,6 +21,6 @@ pub fn PlanPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View< view! {cx, PlanningPage( selected=Some("Plan".to_owned()), - ) { RecipePlan() } + ) { RecipePlan(sh) } } } diff --git a/web/src/web.rs b/web/src/web.rs index a16932d..58b0e8a 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -21,15 +21,18 @@ use crate::{api, routing::Handler as RouteHandler}; #[component] pub fn UI(cx: Scope) -> View { api::HttpStore::provide_context(cx, "/api".to_owned()); + // FIXME(jwall): We shouldn't need to get the store from a context anymore. + let store = api::HttpStore::get_from_context(cx).as_ref().clone(); info!("Starting UI"); let app_state = crate::app_state::AppState::new(); - let handler = crate::app_state::get_state_handler(cx, app_state); + let handler = crate::app_state::get_state_handler(cx, app_state, store); let view = create_signal(cx, View::empty()); // FIXME(jwall): We need a way to trigger refreshes when required. Turn this // into a create_effect with a refresh signal stored as a context. spawn_local_scoped(cx, { + let store = api::HttpStore::get_from_context(cx); async move { - api::init_app_state(cx, handler).await; + api::init_app_state(store.as_ref(), handler).await; // TODO(jwall): This needs to be moved into the RouteHandler view.set(view! { cx, div(class="app") { From 8bcafc385d0bea89feabd32115c74d17d662f51c Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 28 Dec 2022 12:59:11 -0600 Subject: [PATCH 08/29] Allow async using spawn_local_scoped --- Cargo.lock | 674 +++++++++++++++--------------- web/src/api.rs | 84 +--- web/src/app_state.rs | 133 +++++- web/src/components/recipe.rs | 8 +- web/src/components/recipe_plan.rs | 9 +- web/src/pages/login.rs | 2 +- web/src/web.rs | 9 +- 7 files changed, 482 insertions(+), 437 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b213cf..8ac5230 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,9 +31,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] @@ -49,9 +49,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.62" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1485d4d2cc45e7b201ee3767015c96faa5904387c9d87c6efdd0fb511f12d305" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" [[package]] name = "api" @@ -94,9 +94,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "async-channel" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" dependencies = [ "concurrent-queue", "event-listener", @@ -105,23 +105,23 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" dependencies = [ + "async-lock", "async-task", "concurrent-queue", "fastrand", "futures-lite", - "once_cell", "slab", ] [[package]] name = "async-global-executor" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5262ed948da60dd8956c6c5aca4d4163593dddb7b32d73267c93dab7b2e98940" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" dependencies = [ "async-channel", "async-executor", @@ -129,37 +129,38 @@ dependencies = [ "async-lock", "blocking", "futures-lite", - "num_cpus", "once_cell", "tokio", ] [[package]] name = "async-io" -version = "1.7.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" dependencies = [ + "async-lock", + "autocfg", "concurrent-queue", "futures-lite", "libc", "log", - "once_cell", "parking", "polling", "slab", "socket2", "waker-fn", - "winapi", + "windows-sys", ] [[package]] name = "async-lock" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" dependencies = [ "event-listener", + "futures-lite", ] [[package]] @@ -171,7 +172,7 @@ dependencies = [ "anyhow", "async-lock", "async-trait", - "base64 0.13.0", + "base64 0.13.1", "bincode", "blake3", "chrono", @@ -217,9 +218,9 @@ checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" [[package]] name = "async-trait" -version = "0.1.57" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" +checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3" dependencies = [ "proc-macro2", "quote", @@ -247,7 +248,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -260,9 +261,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043" +checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43" dependencies = [ "async-trait", "axum-core", @@ -273,7 +274,7 @@ dependencies = [ "http", "http-body", "hyper", - "itoa 1.0.2", + "itoa 1.0.5", "matchit", "memchr", "mime", @@ -298,15 +299,15 @@ checksum = "f9770f9a9147b2324066609acb5495538cb25f973129663fba2658ba7ed69407" dependencies = [ "async-trait", "axum-core", - "base64 0.13.0", + "base64 0.13.1", "http", ] [[package]] name = "axum-core" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b" +checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc" dependencies = [ "async-trait", "bytes", @@ -340,9 +341,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" @@ -352,9 +353,9 @@ checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" [[package]] name = "base64ct" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" [[package]] name = "bincode" @@ -373,11 +374,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "blake2" -version = "0.10.4" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest 0.10.3", + "digest 0.10.6", ] [[package]] @@ -406,25 +407,25 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ "generic-array", ] [[package]] name = "blocking" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" dependencies = [ "async-channel", + "async-lock", "async-task", "atomic-waker", "fastrand", "futures-lite", - "once_cell", ] [[package]] @@ -441,9 +442,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.10.0" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "byteorder" @@ -453,21 +454,15 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e" - -[[package]] -name = "cache-padded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" [[package]] name = "cfg-if" @@ -483,16 +478,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ "iana-time-zone", "js-sys", "num-integer", "num-traits", "serde", - "time 0.1.44", + "time 0.1.45", "wasm-bindgen", "winapi", ] @@ -526,9 +521,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.16" +version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "atty", "bitflags", @@ -561,11 +556,11 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "1.2.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" dependencies = [ - "cache-padded", + "crossbeam-utils", ] [[package]] @@ -586,11 +581,11 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "cookie" -version = "0.16.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ - "time 0.3.14", + "time 0.3.17", "version_check", ] @@ -602,9 +597,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cpufeatures" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" dependencies = [ "libc", ] @@ -626,9 +621,9 @@ checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" [[package]] name = "crossbeam-queue" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd42583b04998a5363558e5f9291ee5a5ff6b49944332103f251e7479a82aa7" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -636,12 +631,11 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.10" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" dependencies = [ "cfg-if 1.0.0", - "once_cell", ] [[package]] @@ -698,9 +692,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.1.22" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" dependencies = [ "quote", "syn", @@ -708,9 +702,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.81" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97abf9f0eca9e52b7f81b945524e76710e6cb2366aead23b7d4fbf72e281f888" +checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd" dependencies = [ "cc", "cxxbridge-flags", @@ -720,9 +714,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.81" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc32cc5fea1d894b77d269ddb9f192110069a8a9c1f1d441195fba90553dea3" +checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0" dependencies = [ "cc", "codespan-reporting", @@ -735,15 +729,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.81" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca220e4794c934dc6b1207c3b42856ad4c302f2df1712e9f8d2eec5afaacf1f" +checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59" [[package]] name = "cxxbridge-macro" -version = "1.0.81" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704" +checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6" dependencies = [ "proc-macro2", "quote", @@ -761,43 +755,20 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.3" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ - "block-buffer 0.10.2", + "block-buffer 0.10.3", "crypto-common", "subtle", ] -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "dotenvy" -version = "0.15.3" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3db6fcad7c1fc4abdd99bf5276a4db30d6a819127903a709ed41e5ff016e84" -dependencies = [ - "dirs", -] +checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" [[package]] name = "either" @@ -810,15 +781,15 @@ dependencies = [ [[package]] name = "event-listener" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "fastrand" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" dependencies = [ "instant", ] @@ -843,19 +814,18 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] [[package]] name = "futures" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" dependencies = [ "futures-channel", "futures-core", @@ -868,9 +838,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" dependencies = [ "futures-core", "futures-sink", @@ -878,15 +848,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" [[package]] name = "futures-executor" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" dependencies = [ "futures-core", "futures-task", @@ -895,9 +865,9 @@ dependencies = [ [[package]] name = "futures-intrusive" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62007592ac46aa7c2b6416f7deb9a8a8f63a01e0f1d6e1787d5630170db2b63e" +checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" dependencies = [ "futures-core", "lock_api", @@ -906,9 +876,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" [[package]] name = "futures-lite" @@ -927,9 +897,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" +checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ "proc-macro2", "quote", @@ -949,21 +919,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" [[package]] name = "futures-task" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" [[package]] name = "futures-util" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" dependencies = [ "futures-channel", "futures-core", @@ -979,9 +949,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", @@ -989,9 +959,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if 1.0.0", "libc", @@ -1020,9 +990,9 @@ dependencies = [ [[package]] name = "gloo-timers" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9" +checksum = "98c4a8d6391675c6b2ee1a6c8d06e8e2d03605c44cec1270675985a4c2a5500b" dependencies = [ "futures-channel", "futures-core", @@ -1032,20 +1002,22 @@ dependencies = [ [[package]] name = "gloo-utils" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "929c53c913bb7a88d75d9dc3e9705f963d8c2b9001510b25ddaf671b9fb7049d" +checksum = "a8e8fc851e9c7b9852508bc6e3f690f452f474417e8545ec9857b7f7377036b5" dependencies = [ "js-sys", + "serde", + "serde_json", "wasm-bindgen", "web-sys", ] [[package]] name = "h2" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" dependencies = [ "bytes", "fnv", @@ -1077,27 +1049,27 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452c155cb93fecdfb02a73dd57b5d8e442c2063bd7aac72f1bc5e4263a43086" +checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" dependencies = [ "hashbrown", ] [[package]] name = "headers" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "bitflags", "bytes", "headers-core", "http", "httpdate", "mime", - "sha-1", + "sha1", ] [[package]] @@ -1127,6 +1099,15 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "hex" version = "0.4.3" @@ -1145,9 +1126,9 @@ dependencies = [ [[package]] name = "html-escape" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e7479fa1ef38eb49fb6a42c426be515df2d063f06cb8efd3e50af073dbc26c" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" dependencies = [ "utf8-width", ] @@ -1160,7 +1141,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa 1.0.2", + "itoa 1.0.5", ] [[package]] @@ -1207,7 +1188,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.2", + "itoa 1.0.5", "pin-project-lite", "socket2", "tokio", @@ -1242,20 +1223,19 @@ dependencies = [ [[package]] name = "idna" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", "hashbrown", @@ -1272,9 +1252,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] @@ -1287,9 +1267,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" [[package]] name = "js-sys" @@ -1369,9 +1349,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.126" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libsqlite3-sys" @@ -1386,18 +1366,18 @@ dependencies = [ [[package]] name = "link-cplusplus" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" dependencies = [ "cc", ] [[package]] name = "lock_api" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg", "scopeguard", @@ -1413,12 +1393,6 @@ dependencies = [ "value-bag", ] -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - [[package]] name = "matchit" version = "0.5.0" @@ -1455,9 +1429,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", @@ -1467,9 +1441,9 @@ dependencies = [ [[package]] name = "nom" -version = "7.1.1" +version = "7.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "e5507769c4919c998e69e49c839d9dc6e693ede4cc4290d6ad8b41d4f09c548c" dependencies = [ "memchr", "minimal-lexical", @@ -1529,28 +1503,19 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ + "hermit-abi 0.2.6", "libc", ] [[package]] name = "once_cell" -version = "1.13.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "opaque-debug" @@ -1560,9 +1525,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "os_str_bytes" -version = "6.2.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] name = "overload" @@ -1589,9 +1554,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" dependencies = [ "cfg-if 1.0.0", "instant", @@ -1614,30 +1579,30 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.7" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pin-project" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", @@ -1658,43 +1623,44 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "polling" -version = "2.2.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +checksum = "22122d5ec4f9fe1b3916419b76be1e80bcb93f618d071d2edf841b137b2a2bd6" dependencies = [ + "autocfg", "cfg-if 1.0.0", "libc", "log", "wepoll-ffi", - "winapi", + "windows-sys", ] [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] @@ -1722,9 +1688,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] @@ -1749,22 +1715,11 @@ dependencies = [ "bitflags", ] -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom", - "redox_syscall", - "thiserror", -] - [[package]] name = "regex" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ "aho-corasick", "memchr", @@ -1779,9 +1734,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "reqwasm" @@ -1809,9 +1764,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "6.4.0" +version = "6.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a17e5ac65b318f397182ae94e532da0ba56b88dd1200b774715d36c4943b1c3" +checksum = "283ffe2f866869428c92e0d61c2f35dfb4355293cdfdc48f49e895c15f1333d1" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -1820,9 +1775,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "6.2.0" +version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e763e24ba2bf0c72bc6be883f967f794a019fafd1b86ba1daff9c91a7edd30" +checksum = "31ab23d42d71fb9be1b643fe6765d292c5e14d46912d13f3ae2815ca048ea04d" dependencies = [ "proc-macro2", "quote", @@ -1833,19 +1788,19 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "7.2.0" +version = "7.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756feca3afcbb1487a1d01f4ecd94cf8ec98ea074c55a69e7136d29fb6166029" +checksum = "c1669d81dfabd1b5f8e2856b8bbe146c6192b0ba22162edc738ac0a5de18f054" dependencies = [ - "sha2 0.9.9", + "sha2 0.10.6", "walkdir", ] [[package]] name = "rustls" -version = "0.20.6" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" dependencies = [ "log", "ring", @@ -1859,14 +1814,14 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", ] [[package]] name = "ryu" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" [[package]] name = "same-file" @@ -1877,6 +1832,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.1.0" @@ -1885,9 +1846,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "scratch" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" [[package]] name = "sct" @@ -1911,18 +1872,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.144" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.144" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", @@ -1931,11 +1892,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.82" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" dependencies = [ - "itoa 1.0.2", + "itoa 1.0.5", "ryu", "serde", ] @@ -1947,20 +1908,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.2", + "itoa 1.0.5", "ryu", "serde", ] [[package]] -name = "sha-1" -version = "0.10.0" +name = "sha1" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.3", + "digest 0.10.6", ] [[package]] @@ -1978,13 +1939,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.3" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899bf02746a2c92bf1053d9327dadb252b01af1f81f90cdb902411f518bc7215" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.3", + "digest 0.10.6", ] [[package]] @@ -2016,9 +1977,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "socket2" @@ -2090,7 +2051,7 @@ dependencies = [ "hashlink", "hex", "indexmap", - "itoa 1.0.2", + "itoa 1.0.5", "libc", "libsqlite3-sys", "log", @@ -2101,7 +2062,7 @@ dependencies = [ "rustls", "rustls-pemfile", "serde", - "sha2 0.10.3", + "sha2 0.10.6", "smallvec", "sqlformat", "sqlx-rt", @@ -2125,7 +2086,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2 0.10.3", + "sha2 0.10.6", "sqlx-core", "sqlx-rt", "syn", @@ -2252,9 +2213,11 @@ dependencies = [ [[package]] name = "sycamore-state" -version = "0.0.1" +version = "0.1.0" dependencies = [ "sycamore", + "wasm-bindgen", + "wasm-bindgen-test", ] [[package]] @@ -2274,9 +2237,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.98" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", @@ -2300,24 +2263,24 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", @@ -2335,9 +2298,9 @@ dependencies = [ [[package]] name = "time" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", @@ -2346,21 +2309,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.14" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ - "itoa 1.0.2", - "libc", - "num_threads", + "itoa 1.0.5", + "serde", + "time-core", "time-macros", ] [[package]] -name = "time-macros" -version = "0.2.4" +name = "time-core" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", +] [[package]] name = "tinyvec" @@ -2379,9 +2351,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.20.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57aec3cfa4c296db7255446efb4928a6be304b431a806216105542a67b6ca82e" +checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" dependencies = [ "autocfg", "bytes", @@ -2389,11 +2361,10 @@ dependencies = [ "memchr", "mio", "num_cpus", - "once_cell", "pin-project-lite", "socket2", "tokio-macros", - "winapi", + "windows-sys", ] [[package]] @@ -2420,9 +2391,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" dependencies = [ "bytes", "futures-core", @@ -2450,11 +2421,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba" +checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "bitflags", "bytes", "futures-core", @@ -2471,9 +2442,9 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-service" @@ -2483,9 +2454,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.35" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", "log", @@ -2496,9 +2467,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", @@ -2536,7 +2507,7 @@ dependencies = [ "sharded-slab", "smallvec", "thread_local", - "time 0.3.14", + "time 0.3.17", "tracing-core", "tracing-log", ] @@ -2562,9 +2533,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicase" @@ -2583,24 +2554,24 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.2" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] name = "unicode-normalization" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" [[package]] name = "unicode-width" @@ -2628,13 +2599,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", ] @@ -2646,9 +2616,9 @@ checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" [[package]] name = "uuid" -version = "1.1.2" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" dependencies = [ "getrandom", "serde", @@ -2750,9 +2720,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.31" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -2789,6 +2759,30 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +[[package]] +name = "wasm-bindgen-test" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d2fff962180c3fadf677438054b1db62bee4aa32af26a45388af07d1287e1d" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4683da3dfc016f704c9f82cf401520c4f1cb3ee440f7f52b3d6ac29506a49ca7" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "web-sys" version = "0.3.60" @@ -2811,9 +2805,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.4" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" dependencies = [ "webpki", ] @@ -2860,46 +2854,60 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.36.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ + "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", + "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" +name = "windows_aarch64_gnullvm" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" [[package]] name = "windows_i686_gnu" -version = "0.36.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" [[package]] name = "windows_i686_msvc" -version = "0.36.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" [[package]] name = "windows_x86_64_gnu" -version = "0.36.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" [[package]] name = "windows_x86_64_msvc" -version = "0.36.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" [[package]] name = "zeroize" diff --git a/web/src/api.rs b/web/src/api.rs index e73c88a..3048e3f 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -17,7 +17,6 @@ use base64; use reqwasm; use serde_json::{from_str, to_string}; use sycamore::prelude::*; -use sycamore_state::Handler; use tracing::{debug, error, info, instrument, warn}; use client_api::*; @@ -25,10 +24,11 @@ use recipes::{parse, IngredientKey, Recipe, RecipeEntry}; use wasm_bindgen::JsValue; use crate::{ - app_state::{self, AppState, Message, StateMachine}, + app_state::{self, AppState}, js_lib, }; +// FIXME(jwall): We should be able to delete this now. #[instrument] fn filter_recipes( recipe_entries: &Option>, @@ -57,86 +57,6 @@ fn filter_recipes( } } -pub async fn init_app_state<'ctx>( - store: &HttpStore, - h: &'ctx Handler<'ctx, StateMachine, AppState, Message>, -) { - info!("Synchronizing Recipes"); - // TODO(jwall): Make our caching logic using storage more robust. - let recipe_entries = match store.get_recipes().await { - Ok(recipe_entries) => { - if let Ok((staples, recipes)) = filter_recipes(&recipe_entries) { - h.dispatch(Message::SetStaples(staples)); - if let Some(recipes) = recipes { - h.dispatch(Message::InitRecipes(recipes)); - } - } - recipe_entries - } - Err(err) => { - error!(?err); - None - } - }; - - if let Ok(Some(plan)) = store.get_plan().await { - // set the counts. - let mut plan_map = BTreeMap::new(); - for (id, count) in plan { - plan_map.insert(id, count as usize); - } - h.dispatch(Message::InitRecipeCounts(plan_map)); - } else { - // Initialize things to zero - if let Some(rs) = recipe_entries { - for r in rs { - h.dispatch(Message::UpdateRecipeCount(r.recipe_id().to_owned(), 0)) - } - } - } - info!("Checking for user_data in local storage"); - let storage = js_lib::get_storage(); - let user_data = storage - .get("user_data") - .expect("Couldn't read from storage"); - if let Some(data) = user_data { - if let Ok(user_data) = from_str(&data) { - h.dispatch(Message::SetUserData(user_data)); - } - } - info!("Synchronizing categories"); - match store.get_categories().await { - Ok(Some(categories_content)) => { - debug!(categories=?categories_content); - match recipes::parse::as_categories(&categories_content) { - Ok(category_map) => { - h.dispatch(Message::SetCategoryMap(category_map)); - } - Err(err) => { - error!(?err) - } - }; - } - Ok(None) => { - warn!("There is no category file"); - } - Err(e) => { - error!("{:?}", e); - } - } - info!("Synchronizing inventory data"); - match store.get_inventory_data().await { - Ok((filtered_ingredients, modified_amts, extra_items)) => { - h.dispatch(Message::InitAmts(modified_amts)); - h.dispatch(Message::InitFilteredIngredient(filtered_ingredients)); - h.dispatch(Message::InitExtras(BTreeSet::from_iter(extra_items))); - } - Err(e) => { - error!("{:?}", e); - } - } -} - #[instrument(skip(state))] pub async fn init_page_state(store: &HttpStore, state: &app_state::State) -> Result<(), String> { info!("Synchronizing Recipes"); diff --git a/web/src/app_state.rs b/web/src/app_state.rs index 057bbc7..4f31053 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -13,15 +13,16 @@ // limitations under the License. use std::collections::{BTreeMap, BTreeSet}; -use sycamore::{futures::spawn_local, prelude::*}; -use tracing::{debug, error, instrument, warn}; - use client_api::UserData; -use recipes::{Ingredient, IngredientAccumulator, IngredientKey, Recipe}; - +use recipes::{parse, Ingredient, IngredientAccumulator, IngredientKey, Recipe, RecipeEntry}; +use serde_json::from_str; +use sycamore::futures::spawn_local_scoped; +use sycamore::{futures::spawn_local, prelude::*}; use sycamore_state::{Handler, MessageMapper}; +use tracing::{debug, error, info, instrument, warn}; use crate::api::HttpStore; +use crate::js_lib; #[derive(Debug, Clone, PartialEq)] pub struct AppState { @@ -70,13 +71,122 @@ pub enum Message { SetUserData(UserData), UnsetUserData, SaveState, + LoadState, } pub struct StateMachine(HttpStore); +#[instrument] +fn filter_recipes( + recipe_entries: &Option>, +) -> Result<(Option, Option>), 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)), + } +} +impl StateMachine { + async fn load_state(store: HttpStore, original: &Signal) { + let mut state = original.get().as_ref().clone(); + info!("Synchronizing Recipes"); + // TODO(jwall): Make our caching logic using storage more robust. + let recipe_entries = match store.get_recipes().await { + Ok(recipe_entries) => { + if let Ok((staples, recipes)) = filter_recipes(&recipe_entries) { + state.staples = staples; + if let Some(recipes) = recipes { + state.recipes = recipes; + } + } + recipe_entries + } + Err(err) => { + error!(?err); + None + } + }; + + if let Ok(Some(plan)) = store.get_plan().await { + // set the counts. + let mut plan_map = BTreeMap::new(); + for (id, count) in plan { + plan_map.insert(id, count as usize); + } + state.recipe_counts = plan_map; + } else { + // Initialize things to zero + if let Some(rs) = recipe_entries { + for r in rs { + state.recipe_counts.insert(r.recipe_id().to_owned(), 0); + } + } + } + info!("Checking for user_data in local storage"); + let storage = js_lib::get_storage(); + let user_data = storage + .get("user_data") + .expect("Couldn't read from storage"); + if let Some(data) = user_data { + if let Ok(user_data) = from_str(&data) { + state.auth = Some(user_data); + } + } + info!("Synchronizing categories"); + match store.get_categories().await { + Ok(Some(categories_content)) => { + debug!(categories=?categories_content); + match recipes::parse::as_categories(&categories_content) { + Ok(category_map) => { + state.category_map = category_map; + } + Err(err) => { + error!(?err) + } + }; + } + Ok(None) => { + warn!("There is no category file"); + } + Err(e) => { + error!("{:?}", e); + } + } + info!("Synchronizing inventory data"); + match store.get_inventory_data().await { + Ok((filtered_ingredients, modified_amts, extra_items)) => { + state.modified_amts = modified_amts; + state.filtered_ingredients = filtered_ingredients; + state.extras = BTreeSet::from_iter(extra_items); + } + Err(e) => { + error!("{:?}", e); + } + } + original.set(state); + } +} + impl MessageMapper for StateMachine { #[instrument(skip_all, fields(?msg))] - fn map(&self, msg: Message, original: &ReadSignal) -> AppState { + fn map<'ctx>(&self, cx: Scope<'ctx>, msg: Message, original: &'ctx Signal) { let mut original_copy = original.get().as_ref().clone(); match msg { Message::InitRecipeCounts(map) => { @@ -133,14 +243,21 @@ impl MessageMapper for StateMachine { Message::SaveState => { let store = self.0.clone(); let original_copy = original_copy.clone(); - spawn_local(async move { + spawn_local_scoped(cx, async move { if let Err(e) = store.save_app_state(original_copy).await { error!(err=?e, "Error saving app state") }; }); } + Message::LoadState => { + let store = self.0.clone(); + spawn_local_scoped(cx, async move { + Self::load_state(store, original).await; + }); + return; + } } - original_copy + original.set(original_copy); } } diff --git a/web/src/components/recipe.rs b/web/src/components/recipe.rs index 0fdea98..eefc862 100644 --- a/web/src/components/recipe.rs +++ b/web/src/components/recipe.rs @@ -95,10 +95,10 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>) // We also need to set recipe in our state dirty.set(false); if let Ok(recipe) = recipes::parse::as_recipe(text.get_untracked().as_ref()) { - sh.dispatch(Message::SetRecipe( - id.get_untracked().as_ref().to_owned(), - recipe, - )); + sh.dispatch( + cx, + Message::SetRecipe(id.get_untracked().as_ref().to_owned(), recipe), + ); } }; } diff --git a/web/src/components/recipe_plan.rs b/web/src/components/recipe_plan.rs index 3633c0d..15aa500 100644 --- a/web/src/components/recipe_plan.rs +++ b/web/src/components/recipe_plan.rs @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. use recipes::Recipe; -use sycamore::{futures::spawn_local_scoped, prelude::*}; +use sycamore::prelude::*; use tracing::instrument; +use crate::app_state; use crate::app_state::{Message, StateHandler}; use crate::components::recipe_selection::*; -use crate::{api::*, app_state}; #[allow(non_snake_case)] #[instrument(skip_all)] @@ -41,12 +41,11 @@ pub fn RecipePlan<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie // FIXME(jwall): We should probably make this a dispatch method instead. create_effect(cx, move || { refresh_click.track(); - let store = HttpStore::get_from_context(cx); - spawn_local_scoped(cx, async move { init_app_state(store.as_ref(), sh).await }); + sh.dispatch(cx, Message::LoadState); }); create_effect(cx, move || { save_click.track(); - sh.dispatch(Message::SaveState); + sh.dispatch(cx, Message::SaveState); }); view! {cx, table(class="recipe_selector no-print") { diff --git a/web/src/pages/login.rs b/web/src/pages/login.rs index 99cf486..560a162 100644 --- a/web/src/pages/login.rs +++ b/web/src/pages/login.rs @@ -29,7 +29,7 @@ pub fn LoginForm<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View debug!("authenticating against ui"); // TODO(jwall): Navigate to plan if the below is successful. if let Some(user_data) = store.authenticate(username, password).await { - sh.dispatch(Message::SetUserData(user_data)); + sh.dispatch(cx, Message::SetUserData(user_data)); } }); } diff --git a/web/src/web.rs b/web/src/web.rs index 58b0e8a..733c1cb 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -14,6 +14,7 @@ use sycamore::{futures::spawn_local_scoped, prelude::*}; use tracing::{info, instrument}; +use crate::app_state::Message; use crate::components::{Footer, Header}; use crate::{api, routing::Handler as RouteHandler}; @@ -25,19 +26,19 @@ pub fn UI(cx: Scope) -> View { let store = api::HttpStore::get_from_context(cx).as_ref().clone(); info!("Starting UI"); let app_state = crate::app_state::AppState::new(); - let handler = crate::app_state::get_state_handler(cx, app_state, store); + let sh = crate::app_state::get_state_handler(cx, app_state, store); let view = create_signal(cx, View::empty()); // FIXME(jwall): We need a way to trigger refreshes when required. Turn this // into a create_effect with a refresh signal stored as a context. spawn_local_scoped(cx, { let store = api::HttpStore::get_from_context(cx); async move { - api::init_app_state(store.as_ref(), handler).await; + sh.dispatch(cx, Message::LoadState); // TODO(jwall): This needs to be moved into the RouteHandler view.set(view! { cx, div(class="app") { - Header(handler) - RouteHandler(sh=handler) + Header(sh) + RouteHandler(sh=sh) Footer { } } }); From f3425dedeb01c2cca1226f6e1284f539930bfd11 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 28 Dec 2022 14:27:45 -0600 Subject: [PATCH 09/29] Cleanup Unused code after state management refactors --- web/src/api.rs | 73 ------------------------ web/src/app_state.rs | 2 +- web/src/components/add_recipe.rs | 92 ------------------------------ web/src/components/recipe.rs | 3 +- web/src/pages/manage/add_recipe.rs | 26 --------- web/src/web.rs | 1 - 6 files changed, 2 insertions(+), 195 deletions(-) delete mode 100644 web/src/components/add_recipe.rs delete mode 100644 web/src/pages/manage/add_recipe.rs diff --git a/web/src/api.rs b/web/src/api.rs index 3048e3f..2ee8f17 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -57,79 +57,6 @@ fn filter_recipes( } } -#[instrument(skip(state))] -pub async fn init_page_state(store: &HttpStore, state: &app_state::State) -> Result<(), String> { - info!("Synchronizing Recipes"); - // TODO(jwall): Make our caching logic using storage more robust. - let recipes = store.get_recipes().await.map_err(|e| format!("{:?}", e))?; - if let Ok((staples, recipes)) = filter_recipes(&recipes) { - state.staples.set(staples); - if let Some(recipes) = recipes { - state.recipes.set(recipes); - } - } - - if let Ok(Some(plan)) = store.get_plan().await { - // set the counts. - for (id, count) in plan { - state.set_recipe_count_by_index(&id, count as usize); - } - } else { - // Initialize things to zero - if let Some(rs) = recipes { - for r in rs { - if !state.recipe_counts.get().contains_key(r.recipe_id()) { - state.set_recipe_count_by_index(&r.recipe_id().to_owned(), 0); - } - } - } - } - info!("Checking for user_data in local storage"); - let storage = js_lib::get_storage(); - let user_data = storage - .get("user_data") - .expect("Couldn't read from storage"); - if let Some(data) = user_data { - if let Ok(user_data) = from_str(&data) { - state.auth.set(user_data) - } - } - info!("Synchronizing categories"); - match store.get_categories().await { - Ok(Some(categories_content)) => { - debug!(categories=?categories_content); - let category_map = recipes::parse::as_categories(&categories_content)?; - state.category_map.set(category_map); - } - Ok(None) => { - warn!("There is no category file"); - } - Err(e) => { - error!("{:?}", e); - } - } - info!("Synchronizing inventory data"); - match store.get_inventory_data().await { - Ok((filtered_ingredients, modified_amts, mut extra_items)) => { - state.reset_modified_amts(modified_amts); - state.filtered_ingredients.set(filtered_ingredients); - state.extras.set( - extra_items - .drain(0..) - .enumerate() - .map(|(idx, (amt, name))| { - (idx, (create_rc_signal(amt.clone()), create_rc_signal(name))) - }) - .collect(), - ) - } - Err(e) => { - error!("{:?}", e); - } - } - Ok(()) -} - #[derive(Debug)] pub struct Error(String); diff --git a/web/src/app_state.rs b/web/src/app_state.rs index 4f31053..d91399b 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -17,7 +17,7 @@ use client_api::UserData; use recipes::{parse, Ingredient, IngredientAccumulator, IngredientKey, Recipe, RecipeEntry}; use serde_json::from_str; use sycamore::futures::spawn_local_scoped; -use sycamore::{futures::spawn_local, prelude::*}; +use sycamore::prelude::*; use sycamore_state::{Handler, MessageMapper}; use tracing::{debug, error, info, instrument, warn}; diff --git a/web/src/components/add_recipe.rs b/web/src/components/add_recipe.rs deleted file mode 100644 index fc49fb7..0000000 --- a/web/src/components/add_recipe.rs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2022 Jeremy Wall (Jeremy@marzhilsltudios.com) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -use sycamore::{futures::spawn_local_scoped, prelude::*}; -use tracing::{error, info}; - -use recipes::RecipeEntry; - -const STARTER_RECIPE: &'static str = "title: TITLE_PLACEHOLDER - -Description here. - -step: - -1 ingredient - -Instructions here -"; - -#[component] -pub fn AddRecipe(cx: Scope) -> View { - let recipe_title = create_signal(cx, String::new()); - let create_recipe_signal = create_signal(cx, ()); - let dirty = create_signal(cx, false); - - let entry = create_memo(cx, || { - RecipeEntry( - recipe_title - .get() - .as_ref() - .to_lowercase() - .replace(" ", "_") - .replace("\n", ""), - STARTER_RECIPE - .replace("TITLE_PLACEHOLDER", recipe_title.get().as_str()) - .replace("\r", ""), - ) - }); - - create_effect(cx, move || { - create_recipe_signal.track(); - if !*dirty.get_untracked() { - return; - } - spawn_local_scoped(cx, { - let store = crate::api::HttpStore::get_from_context(cx); - async move { - let entry = entry.get_untracked(); - // TODO(jwall): Better error reporting here. - match store.get_recipe_text(entry.recipe_id()).await { - Ok(Some(_)) => { - // TODO(jwall): We should tell the user that this id already exists - info!(recipe_id = entry.recipe_id(), "Recipe already exists"); - return; - } - Ok(None) => { - // noop - } - Err(err) => { - // TODO(jwall): We should tell the user that this is failing - error!(?err) - } - } - store - .save_recipes(vec![entry.as_ref().clone()]) - .await - .expect("Unable to save New Recipe"); - crate::js_lib::navigate_to_path(&format!("/ui/recipe/{}", entry.recipe_id())) - .expect("Unable to navigate to recipe"); - } - }); - }); - view! {cx, - label(for="recipe_title") { "Recipe Title" } - input(bind:value=recipe_title, type="text", name="recipe_title", id="recipe_title", on:change=move |_| { - dirty.set(true); - }) - button(on:click=move |_| { - create_recipe_signal.trigger_subscribers(); - }) { "Create" } - } -} diff --git a/web/src/components/recipe.rs b/web/src/components/recipe.rs index eefc862..cb1c951 100644 --- a/web/src/components/recipe.rs +++ b/web/src/components/recipe.rs @@ -14,7 +14,7 @@ use sycamore::{futures::spawn_local_scoped, prelude::*}; use tracing::{debug, error}; -use crate::app_state::{self, Message, StateHandler}; +use crate::app_state::{Message, StateHandler}; use recipes::{self, RecipeEntry}; fn check_recipe_parses( @@ -162,7 +162,6 @@ fn Steps(cx: Scope, steps: Vec) -> View { #[component] pub fn Viewer<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>) -> View { let RecipeComponentProps { recipe_id, sh } = props; - let state = app_state::State::get_from_context(cx); let view = create_signal(cx, View::empty()); let recipe_signal = sh.get_selector(cx, move |state| { if let Some(recipe) = state.get().recipes.get(&recipe_id) { diff --git a/web/src/pages/manage/add_recipe.rs b/web/src/pages/manage/add_recipe.rs deleted file mode 100644 index 68830b7..0000000 --- a/web/src/pages/manage/add_recipe.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2022 Jeremy Wall (Jeremy@marzhilsltudios.com) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -use super::ManagePage; -use crate::{app_state::StateHandler, components::add_recipe::AddRecipe}; - -use sycamore::prelude::*; - -#[component] -pub fn AddRecipePage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { - view! {cx, - ManagePage( - selected=Some("New Recipe".to_owned()), - ) { AddRecipe() } - } -} diff --git a/web/src/web.rs b/web/src/web.rs index 733c1cb..a50ea74 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -31,7 +31,6 @@ pub fn UI(cx: Scope) -> View { // FIXME(jwall): We need a way to trigger refreshes when required. Turn this // into a create_effect with a refresh signal stored as a context. spawn_local_scoped(cx, { - let store = api::HttpStore::get_from_context(cx); async move { sh.dispatch(cx, Message::LoadState); // TODO(jwall): This needs to be moved into the RouteHandler From 1888e5328f93f891fc683082da215e9089755a35 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 28 Dec 2022 14:27:45 -0600 Subject: [PATCH 10/29] Use StateHandler in AddRecipe --- web/src/app_state.rs | 17 ++++++ web/src/components/add_recipe.rs | 90 ++++++++++++++++++++++++++++++ web/src/pages/manage/add_recipe.rs | 26 +++++++++ 3 files changed, 133 insertions(+) create mode 100644 web/src/components/add_recipe.rs create mode 100644 web/src/pages/manage/add_recipe.rs diff --git a/web/src/app_state.rs b/web/src/app_state.rs index d91399b..0e0479f 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -59,6 +59,7 @@ pub enum Message { AddExtra(String, String), RemoveExtra(String, String), InitRecipes(BTreeMap), + SaveRecipe(RecipeEntry), SetRecipe(String, Recipe), RemoveRecipe(String), SetStaples(Option), @@ -213,6 +214,22 @@ impl MessageMapper for StateMachine { Message::SetRecipe(id, recipe) => { original_copy.recipes.insert(id, recipe); } + Message::SaveRecipe(entry) => { + let recipe = + parse::as_recipe(entry.recipe_text()).expect("Failed to parse RecipeEntry"); + original_copy + .recipes + .insert(entry.recipe_id().to_owned(), recipe); + let store = self.0.clone(); + original_copy + .recipe_counts + .insert(entry.recipe_id().to_owned(), 0); + spawn_local_scoped(cx, async move { + if let Err(e) = store.save_recipes(vec![entry]).await { + error!(err=?e, "Unable to save Recipe"); + } + }); + } Message::RemoveRecipe(id) => { original_copy.recipes.remove(&id); } diff --git a/web/src/components/add_recipe.rs b/web/src/components/add_recipe.rs new file mode 100644 index 0000000..e47551f --- /dev/null +++ b/web/src/components/add_recipe.rs @@ -0,0 +1,90 @@ +// Copyright 2022 Jeremy Wall (Jeremy@marzhilsltudios.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use sycamore::{futures::spawn_local_scoped, prelude::*}; +use tracing::{error, info}; + +use crate::app_state::{Message, StateHandler}; +use recipes::RecipeEntry; + +const STARTER_RECIPE: &'static str = "title: TITLE_PLACEHOLDER + +Description here. + +step: + +1 ingredient + +Instructions here +"; + +#[component] +pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { + let recipe_title = create_signal(cx, String::new()); + let create_recipe_signal = create_signal(cx, ()); + let dirty = create_signal(cx, false); + + let entry = create_memo(cx, || { + RecipeEntry( + recipe_title + .get() + .as_ref() + .to_lowercase() + .replace(" ", "_") + .replace("\n", ""), + STARTER_RECIPE + .replace("TITLE_PLACEHOLDER", recipe_title.get().as_str()) + .replace("\r", ""), + ) + }); + + create_effect(cx, move || { + create_recipe_signal.track(); + if !*dirty.get_untracked() { + return; + } + spawn_local_scoped(cx, { + let store = crate::api::HttpStore::get_from_context(cx); + async move { + let entry = entry.get_untracked(); + // TODO(jwall): Better error reporting here. + match store.get_recipe_text(entry.recipe_id()).await { + Ok(Some(_)) => { + // TODO(jwall): We should tell the user that this id already exists + info!(recipe_id = entry.recipe_id(), "Recipe already exists"); + return; + } + Ok(None) => { + // noop + } + Err(err) => { + // TODO(jwall): We should tell the user that this is failing + error!(?err) + } + } + sh.dispatch(cx, Message::SaveRecipe((*entry).clone())); + crate::js_lib::navigate_to_path(&format!("/ui/recipe/{}", entry.recipe_id())) + .expect("Unable to navigate to recipe"); + } + }); + }); + view! {cx, + label(for="recipe_title") { "Recipe Title" } + input(bind:value=recipe_title, type="text", name="recipe_title", id="recipe_title", on:change=move |_| { + dirty.set(true); + }) + button(on:click=move |_| { + create_recipe_signal.trigger_subscribers(); + }) { "Create" } + } +} diff --git a/web/src/pages/manage/add_recipe.rs b/web/src/pages/manage/add_recipe.rs new file mode 100644 index 0000000..a84dc9f --- /dev/null +++ b/web/src/pages/manage/add_recipe.rs @@ -0,0 +1,26 @@ +// Copyright 2022 Jeremy Wall (Jeremy@marzhilsltudios.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use super::ManagePage; +use crate::{app_state::StateHandler, components::add_recipe::AddRecipe}; + +use sycamore::prelude::*; + +#[component] +pub fn AddRecipePage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { + view! {cx, + ManagePage( + selected=Some("New Recipe".to_owned()), + ) { AddRecipe(sh) } + } +} From 4b2563509b7f5f6bbd4402863bf8c9a8dc692a6c Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 28 Dec 2022 14:55:15 -0600 Subject: [PATCH 11/29] Use StateHandler in Categories --- web/src/app_state.rs | 41 ++++++++++++++++++++---------- web/src/components/categories.rs | 25 ++++++++---------- web/src/pages/manage/categories.rs | 2 +- web/src/web.rs | 2 +- 4 files changed, 40 insertions(+), 30 deletions(-) diff --git a/web/src/app_state.rs b/web/src/app_state.rs index 0e0479f..d2a1b4a 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -30,7 +30,7 @@ pub struct AppState { pub extras: BTreeSet<(String, String)>, pub staples: Option, pub recipes: BTreeMap, - pub category_map: BTreeMap, + pub category_map: String, pub filtered_ingredients: BTreeSet, pub modified_amts: BTreeMap, pub auth: Option, @@ -43,7 +43,7 @@ impl AppState { extras: BTreeSet::new(), staples: None, recipes: BTreeMap::new(), - category_map: BTreeMap::new(), + category_map: String::new(), filtered_ingredients: BTreeSet::new(), modified_amts: BTreeMap::new(), auth: None, @@ -63,7 +63,8 @@ pub enum Message { SetRecipe(String, Recipe), RemoveRecipe(String), SetStaples(Option), - SetCategoryMap(BTreeMap), + SetCategoryMap(String), + UpdateCategories, InitFilteredIngredient(BTreeSet), AddFilteredIngredient(IngredientKey), RemoveFilteredIngredient(IngredientKey), @@ -154,14 +155,7 @@ impl StateMachine { match store.get_categories().await { Ok(Some(categories_content)) => { debug!(categories=?categories_content); - match recipes::parse::as_categories(&categories_content) { - Ok(category_map) => { - state.category_map = category_map; - } - Err(err) => { - error!(?err) - } - }; + state.category_map = categories_content; } Ok(None) => { warn!("There is no category file"); @@ -233,8 +227,29 @@ impl MessageMapper for StateMachine { Message::RemoveRecipe(id) => { original_copy.recipes.remove(&id); } - Message::SetCategoryMap(map) => { - original_copy.category_map = map; + Message::SetCategoryMap(category_text) => { + let store = self.0.clone(); + original_copy.category_map = category_text.clone(); + spawn_local_scoped(cx, async move { + if let Err(e) = store.save_categories(category_text).await { + error!(?e, "Failed to save categories"); + } + }); + } + Message::UpdateCategories => { + let store = self.0.clone(); + let mut original_copy = original_copy.clone(); + spawn_local_scoped(cx, async move { + if let Some(categories) = match store.get_categories().await { + Ok(js) => js, + Err(e) => { + error!(err=?e, "Failed to get categories."); + return; + } + } { + original_copy.category_map = categories; + }; + }); } Message::InitFilteredIngredient(set) => { original_copy.filtered_ingredients = set; diff --git a/web/src/components/categories.rs b/web/src/components/categories.rs index 68338d6..935f468 100644 --- a/web/src/components/categories.rs +++ b/web/src/components/categories.rs @@ -11,14 +11,16 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +use crate::{ + 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::js_lib::get_element_by_id; - fn get_error_dialog() -> HtmlDialogElement { get_element_by_id::("error-dialog") .expect("error-dialog isn't an html dialog element!") @@ -38,9 +40,9 @@ fn check_category_text_parses(unparsed: &str, error_text: &Signal) -> bo } } -#[instrument] +#[instrument(skip_all)] #[component] -pub fn Categories(cx: Scope) -> View { +pub fn Categories<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { let save_signal = create_signal(cx, ()); let error_text = create_signal(cx, String::new()); let category_text: &Signal = create_signal(cx, String::new()); @@ -65,18 +67,11 @@ pub fn Categories(cx: Scope) -> View { return; } spawn_local_scoped(cx, { - let store = crate::api::HttpStore::get_from_context(cx); async move { - // TODO(jwall): Save the categories. - if let Err(e) = store - .save_categories(category_text.get_untracked().as_ref().clone()) - .await - { - error!(?e, "Failed to save categories"); - error_text.set(format!("{:?}", e)); - } else { - dirty.set(false); - } + sh.dispatch( + cx, + Message::SetCategoryMap(category_text.get_untracked().as_ref().clone()), + ); } }); }); diff --git a/web/src/pages/manage/categories.rs b/web/src/pages/manage/categories.rs index ac78bf6..fb46e92 100644 --- a/web/src/pages/manage/categories.rs +++ b/web/src/pages/manage/categories.rs @@ -21,6 +21,6 @@ pub fn CategoryPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> V view! {cx, ManagePage( selected=Some("Categories".to_owned()), - ) { Categories() } + ) { Categories(sh) } } } diff --git a/web/src/web.rs b/web/src/web.rs index a50ea74..d217ba9 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -21,8 +21,8 @@ use crate::{api, routing::Handler as RouteHandler}; #[instrument] #[component] pub fn UI(cx: Scope) -> View { - api::HttpStore::provide_context(cx, "/api".to_owned()); // FIXME(jwall): We shouldn't need to get the store from a context anymore. + api::HttpStore::provide_context(cx, "/api".to_owned()); let store = api::HttpStore::get_from_context(cx).as_ref().clone(); info!("Starting UI"); let app_state = crate::app_state::AppState::new(); From 73c298661a0b6316b27f9efd6bcc24630d64a3a9 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 28 Dec 2022 15:46:19 -0600 Subject: [PATCH 12/29] 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) } } } From fdaf5ad70dd79d65e42f604e5ce7ce43121c9e8d Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 28 Dec 2022 19:27:14 -0600 Subject: [PATCH 13/29] Use a state handler selector for RecipeList --- web/src/components/recipe_list.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/web/src/components/recipe_list.rs b/web/src/components/recipe_list.rs index 062c828..787bd01 100644 --- a/web/src/components/recipe_list.rs +++ b/web/src/components/recipe_list.rs @@ -11,10 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use crate::{ - app_state::{self, StateHandler}, - components::recipe::Viewer, -}; +use crate::{app_state::StateHandler, components::recipe::Viewer}; use sycamore::prelude::*; use tracing::{debug, instrument}; @@ -22,8 +19,15 @@ use tracing::{debug, instrument}; #[instrument(skip_all)] #[component] pub fn RecipeList<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { - let state = app_state::State::get_from_context(cx); - let menu_list = create_memo(cx, move || state.get_menu_list()); + let menu_list = sh.get_selector(cx, |state| { + state + .get() + .recipe_counts + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .filter(|(_, v)| *(v) != 0) + .collect() + }); view! {cx, h1 { "Recipe List" } div() { From 0b7ff32d42493f676a8ec241a8e9dcea797b90e2 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 28 Dec 2022 19:33:19 -0600 Subject: [PATCH 14/29] Cleanup more unused after refactors --- web/src/app_state.rs | 15 ++++++- web/src/components/recipe_plan.rs | 8 ++-- web/src/components/recipe_selection.rs | 18 +++++--- web/src/components/shopping_list.rs | 61 +++----------------------- 4 files changed, 33 insertions(+), 69 deletions(-) diff --git a/web/src/app_state.rs b/web/src/app_state.rs index d2a1b4a..b4e900d 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -53,7 +53,7 @@ impl AppState { #[derive(Debug)] pub enum Message { - InitRecipeCounts(BTreeMap), + ResetRecipeCounts, UpdateRecipeCount(String, usize), InitExtras(BTreeSet<(String, String)>), AddExtra(String, String), @@ -64,6 +64,7 @@ pub enum Message { RemoveRecipe(String), SetStaples(Option), SetCategoryMap(String), + ResetInventory, UpdateCategories, InitFilteredIngredient(BTreeSet), AddFilteredIngredient(IngredientKey), @@ -105,6 +106,7 @@ fn filter_recipes( None => Ok((None, None)), } } + impl StateMachine { async fn load_state(store: HttpStore, original: &Signal) { let mut state = original.get().as_ref().clone(); @@ -184,7 +186,11 @@ impl MessageMapper for StateMachine { fn map<'ctx>(&self, cx: Scope<'ctx>, msg: Message, original: &'ctx Signal) { let mut original_copy = original.get().as_ref().clone(); match msg { - Message::InitRecipeCounts(map) => { + Message::ResetRecipeCounts => { + let mut map = BTreeMap::new(); + for (id, _) in original_copy.recipes.iter() { + map.insert(id.clone(), 0); + } original_copy.recipe_counts = map; } Message::UpdateRecipeCount(id, count) => { @@ -251,6 +257,11 @@ impl MessageMapper for StateMachine { }; }); } + Message::ResetInventory => { + original_copy.filtered_ingredients = BTreeSet::new(); + original_copy.modified_amts = BTreeMap::new(); + original_copy.extras = BTreeSet::new(); + } Message::InitFilteredIngredient(set) => { original_copy.filtered_ingredients = set; } diff --git a/web/src/components/recipe_plan.rs b/web/src/components/recipe_plan.rs index 15aa500..2d8c4b6 100644 --- a/web/src/components/recipe_plan.rs +++ b/web/src/components/recipe_plan.rs @@ -15,7 +15,6 @@ use recipes::Recipe; use sycamore::prelude::*; use tracing::instrument; -use crate::app_state; use crate::app_state::{Message, StateHandler}; use crate::components::recipe_selection::*; @@ -54,10 +53,10 @@ pub fn RecipePlan<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie view ! {cx, tr { Keyed( iterable=r, - view=|cx, sig| { + view=move |cx, sig| { let title = create_memo(cx, move || sig.get().1.title.clone()); view! {cx, - td { RecipeSelection(i=sig.get().0.to_owned(), title=title) } + td { RecipeSelection(i=sig.get().0.to_owned(), title=title, sh=sh) } } }, key=|sig| sig.get().0.to_owned(), @@ -72,8 +71,7 @@ pub fn RecipePlan<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie refresh_click.set(toggle); }) input(type="button", value="Clear All", on:click=move |_| { - let state = app_state::State::get_from_context(cx); - state.reset_recipe_counts(); + sh.dispatch(cx, Message::ResetRecipeCounts); }) input(type="button", value="Save Plan", on:click=move |_| { // Poor man's click event signaling. diff --git a/web/src/components/recipe_selection.rs b/web/src/components/recipe_selection.rs index 7a2e0ad..8375027 100644 --- a/web/src/components/recipe_selection.rs +++ b/web/src/components/recipe_selection.rs @@ -16,24 +16,29 @@ use std::rc::Rc; use sycamore::prelude::*; use tracing::{debug, instrument}; -use crate::app_state; +use crate::app_state::{self, Message, StateHandler}; #[derive(Props)] pub struct RecipeCheckBoxProps<'ctx> { pub i: String, pub title: &'ctx ReadSignal, + pub sh: StateHandler<'ctx>, } #[instrument(skip(props, cx), fields( - idx=%props.i, + id=%props.i, title=%props.title.get() ))] #[component] -pub fn RecipeSelection(cx: Scope, props: RecipeCheckBoxProps) -> View { +pub fn RecipeSelection<'ctx, G: Html>( + cx: Scope<'ctx>, + props: RecipeCheckBoxProps<'ctx>, +) -> View { + let RecipeCheckBoxProps { i, title, sh } = props; let state = app_state::State::get_from_context(cx); // This is total hack but it works around the borrow issues with // the `view!` macro. - let id = Rc::new(props.i); + let id = Rc::new(i); let count = create_signal( cx, format!( @@ -52,7 +57,7 @@ pub fn RecipeSelection(cx: Scope, props: RecipeCheckBoxProps) -> View(cx: Scope, props: RecipeCheckBoxProps) -> View( #[instrument(skip_all)] #[component] 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, ()); - create_effect(cx, { - let state = crate::app_state::State::get_from_context(cx); - let ingredients_map = ingredients_map.clone(); - move || { - ingredients_map.set(state.get_shopping_list(*show_staples.get())); - } - }); - debug!(ingredients_map=?ingredients_map.get_untracked()); - let ingredients = create_memo(cx, { - let filtered_keys = filtered_keys.clone(); - let ingredients_map = ingredients_map.clone(); - move || { - let mut ingredients = Vec::new(); - // This has the effect of sorting the ingredients by category - for (_, ingredients_list) in ingredients_map.get().iter() { - for (i, recipes) in ingredients_list.iter() { - if !filtered_keys.get().contains(&i.key()) { - ingredients.push((i.key(), (i.clone(), recipes.clone()))); - } - } - } - ingredients - } - }); - let table_view = create_signal(cx, View::empty()); - create_effect(cx, { - 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, sh, show_staples)); - } else { - table_view.set(View::empty()); - } - } - }); create_effect(cx, move || { save_click.track(); info!("Registering save request for inventory"); sh.dispatch(cx, Message::SaveState); }); - let state = crate::app_state::State::get_from_context(cx); view! {cx, h1 { "Shopping List " } label(for="show_staples_cb") { "Show staples" } input(id="show_staples_cb", type="checkbox", bind:checked=show_staples) - (table_view.get().as_ref().clone()) + (make_shopping_table(cx, sh, show_staples)) input(type="button", value="Add Item", class="no-print", on:click=move |_| { - let mut cloned_extras: Vec<(RcSignal, RcSignal)> = (*state.extras.get()).iter().map(|(_, tpl)| tpl.clone()).collect(); - cloned_extras.push((create_rc_signal("".to_owned()), create_rc_signal("".to_owned()))); - state.extras.set(cloned_extras.drain(0..).enumerate().collect()); + sh.dispatch(cx, Message::AddExtra(String::new(), String::new())); }) - input(type="button", value="Reset", class="no-print", on:click={ - //let state = crate::app_state::State::get_from_context(cx); - move |_| { - // 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="Reset", class="no-print", on:click=move |_| { + sh.dispatch(cx, Message::ResetInventory); }) input(type="button", value="Save", class="no-print", on:click=|_| { save_click.trigger_subscribers(); From a1fa17da68feebd257b4ecca4681f65a76d2e67a Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Thu, 29 Dec 2022 11:37:10 -0600 Subject: [PATCH 15/29] use state handler in recipe selection --- web/src/components/recipe_selection.rs | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/web/src/components/recipe_selection.rs b/web/src/components/recipe_selection.rs index 8375027..e8ff8a6 100644 --- a/web/src/components/recipe_selection.rs +++ b/web/src/components/recipe_selection.rs @@ -35,28 +35,18 @@ pub fn RecipeSelection<'ctx, G: Html>( props: RecipeCheckBoxProps<'ctx>, ) -> View { let RecipeCheckBoxProps { i, title, sh } = props; - let state = app_state::State::get_from_context(cx); - // This is total hack but it works around the borrow issues with - // the `view!` macro. let id = Rc::new(i); + let id_clone = id.clone(); let count = create_signal( cx, - format!( - "{}", - state - .get_recipe_count_by_index(id.as_ref()) - .unwrap_or_else(|| state.set_recipe_count_by_index(id.as_ref(), 0)) + sh.get_value( + |state| match state.get_untracked().recipe_counts.get(id_clone.as_ref()) { + Some(count) => format!("{}", count), + None => "0".to_owned(), + }, ), ); - create_effect(cx, { - let id = id.clone(); - let state = app_state::State::get_from_context(cx); - move || { - if let Some(usize_count) = state.get_recipe_count_by_index(id.as_ref()) { - count.set(format!("{}", *usize_count.get())); - } - } - }); + let id_clone = id.clone(); let title = title.get().clone(); let for_id = id.clone(); let href = format!("/ui/recipe/view/{}", id); @@ -64,7 +54,7 @@ pub fn RecipeSelection<'ctx, G: Html>( view! {cx, div() { label(for=for_id) { a(href=href) { (*title) } } - input(type="number", class="item-count-sel", min="0", bind:value=count, name=name, on:change=move |_| { + input(type="number", class="item-count-sel", min="0", value=count, name=name, on:change=move |_| { debug!(idx=%id, count=%(*count.get()), "setting recipe count"); sh.dispatch(cx, Message::UpdateRecipeCount(id.as_ref().clone(), count.get().parse().expect("Count is not a valid usize"))); }) From f3f27a03504d3e5a8786d93c4b7dc4670a81aed5 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Thu, 29 Dec 2022 11:58:37 -0600 Subject: [PATCH 16/29] Remove the old State --- web/src/api.rs | 28 +---- web/src/app_state.rs | 156 ++----------------------- web/src/components/recipe_selection.rs | 3 +- 3 files changed, 9 insertions(+), 178 deletions(-) diff --git a/web/src/api.rs b/web/src/api.rs index 750751e..20e5a41 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -23,10 +23,7 @@ use client_api::*; use recipes::{parse, IngredientKey, Recipe, RecipeEntry}; use wasm_bindgen::JsValue; -use crate::{ - app_state::{self, AppState}, - js_lib, -}; +use crate::{app_state::AppState, js_lib}; // FIXME(jwall): We should be able to delete this now. #[instrument] @@ -360,29 +357,6 @@ impl HttpStore { .await } - #[instrument] - pub async fn save_state(&self, state: std::rc::Rc) -> Result<(), Error> { - let mut plan = Vec::new(); - for (key, count) in state.recipe_counts.get_untracked().iter() { - plan.push((key.clone(), *count.get_untracked() as i32)); - } - debug!("Saving plan data"); - self.save_plan(plan).await?; - debug!("Saving inventory data"); - self.save_inventory_data( - state.filtered_ingredients.get_untracked().as_ref().clone(), - state.get_current_modified_amts(), - state - .extras - .get() - .as_ref() - .iter() - .map(|t| (t.1 .0.get().as_ref().clone(), t.1 .1.get().as_ref().clone())) - .collect(), - ) - .await - } - pub async fn save_plan(&self, plan: Vec<(String, i32)>) -> Result<(), Error> { let mut path = self.v1_path(); path.push_str("/plan"); diff --git a/web/src/app_state.rs b/web/src/app_state.rs index b4e900d..eed2541 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -14,7 +14,7 @@ use std::collections::{BTreeMap, BTreeSet}; use client_api::UserData; -use recipes::{parse, Ingredient, IngredientAccumulator, IngredientKey, Recipe, RecipeEntry}; +use recipes::{parse, IngredientKey, Recipe, RecipeEntry}; use serde_json::from_str; use sycamore::futures::spawn_local_scoped; use sycamore::prelude::*; @@ -55,23 +55,23 @@ impl AppState { pub enum Message { ResetRecipeCounts, UpdateRecipeCount(String, usize), - InitExtras(BTreeSet<(String, String)>), AddExtra(String, String), RemoveExtra(String, String), - InitRecipes(BTreeMap), SaveRecipe(RecipeEntry), SetRecipe(String, Recipe), + // TODO(jwall): Remove this annotation when safe to do so. + #[allow(dead_code)] RemoveRecipe(String), + // TODO(jwall): Remove this annotation when safe to do so. + #[allow(dead_code)] SetStaples(Option), SetCategoryMap(String), ResetInventory, - UpdateCategories, - InitFilteredIngredient(BTreeSet), AddFilteredIngredient(IngredientKey), - RemoveFilteredIngredient(IngredientKey), - InitAmts(BTreeMap), UpdateAmt(IngredientKey, String), SetUserData(UserData), + // TODO(jwall): Remove this annotation when safe to do so. + #[allow(dead_code)] UnsetUserData, SaveState, LoadState, @@ -196,9 +196,6 @@ impl MessageMapper for StateMachine { Message::UpdateRecipeCount(id, count) => { original_copy.recipe_counts.insert(id, count); } - Message::InitExtras(set) => { - original_copy.extras = set; - } Message::AddExtra(amt, name) => { original_copy.extras.insert((amt, name)); } @@ -208,9 +205,6 @@ impl MessageMapper for StateMachine { Message::SetStaples(staples) => { original_copy.staples = staples; } - Message::InitRecipes(recipes) => { - original_copy.recipes = recipes; - } Message::SetRecipe(id, recipe) => { original_copy.recipes.insert(id, recipe); } @@ -242,38 +236,14 @@ impl MessageMapper for StateMachine { } }); } - Message::UpdateCategories => { - let store = self.0.clone(); - let mut original_copy = original_copy.clone(); - spawn_local_scoped(cx, async move { - if let Some(categories) = match store.get_categories().await { - Ok(js) => js, - Err(e) => { - error!(err=?e, "Failed to get categories."); - return; - } - } { - original_copy.category_map = categories; - }; - }); - } Message::ResetInventory => { original_copy.filtered_ingredients = BTreeSet::new(); original_copy.modified_amts = BTreeMap::new(); original_copy.extras = BTreeSet::new(); } - Message::InitFilteredIngredient(set) => { - original_copy.filtered_ingredients = set; - } Message::AddFilteredIngredient(key) => { original_copy.filtered_ingredients.insert(key); } - Message::RemoveFilteredIngredient(key) => { - original_copy.filtered_ingredients.remove(&key); - } - Message::InitAmts(map) => { - original_copy.modified_amts = map; - } Message::UpdateAmt(key, amt) => { original_copy.modified_amts.insert(key, amt); } @@ -313,115 +283,3 @@ pub fn get_state_handler<'ctx>( ) -> StateHandler<'ctx> { Handler::new(cx, initial, StateMachine(store)) } - -#[derive(Debug)] -pub struct State { - pub recipe_counts: RcSignal>>, - pub extras: RcSignal, RcSignal))>>, - pub staples: RcSignal>, - pub recipes: RcSignal>, - pub category_map: RcSignal>, - pub filtered_ingredients: RcSignal>, - pub modified_amts: RcSignal>>, - pub auth: RcSignal>, -} - -impl State { - pub fn get_from_context(cx: Scope) -> std::rc::Rc { - use_context::>(cx).clone() - } - - pub fn get_menu_list(&self) -> Vec<(String, RcSignal)> { - self.recipe_counts - .get() - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .filter(|(_, v)| *(v.get_untracked()) != 0) - .collect() - } - - #[instrument(skip(self))] - pub fn get_shopping_list( - &self, - show_staples: bool, - ) -> BTreeMap)>> { - let mut acc = IngredientAccumulator::new(); - let recipe_counts = self.get_menu_list(); - for (idx, count) in recipe_counts.iter() { - for _ in 0..*count.get_untracked() { - acc.accumulate_from( - self.recipes - .get() - .get(idx) - .expect(&format!("No such recipe id exists: {}", idx)), - ); - } - } - if show_staples { - if let Some(staples) = self.staples.get().as_ref() { - acc.accumulate_from(staples); - } - } - let mut ingredients = acc.ingredients(); - let mut groups = BTreeMap::new(); - let cat_map = self.category_map.get().clone(); - for (_, (i, recipes)) in ingredients.iter_mut() { - let category = if let Some(cat) = cat_map.get(&i.name) { - cat.clone() - } else { - "other".to_owned() - }; - i.category = category.clone(); - groups - .entry(category) - .or_insert(vec![]) - .push((i.clone(), recipes.clone())); - } - debug!(?self.category_map); - // FIXME(jwall): Sort by categories and names. - groups - } - - /// Retrieves the count for a recipe without triggering subscribers to the entire - /// recipe count set. - pub fn get_recipe_count_by_index(&self, key: &String) -> Option> { - self.recipe_counts.get_untracked().get(key).cloned() - } - - pub fn reset_recipe_counts(&self) { - for (_, count) in self.recipe_counts.get_untracked().iter() { - count.set(0); - } - } - - /// Set the recipe_count by index. Does not trigger subscribers to the entire set of recipe_counts. - /// This does trigger subscribers of the specific recipe you are updating though. - pub fn set_recipe_count_by_index(&self, key: &String, count: usize) -> RcSignal { - let mut counts = self.recipe_counts.get_untracked().as_ref().clone(); - counts - .entry(key.clone()) - .and_modify(|e| e.set(count)) - .or_insert_with(|| create_rc_signal(count)); - self.recipe_counts.set(counts); - self.recipe_counts.get_untracked().get(key).unwrap().clone() - } - - pub fn get_current_modified_amts(&self) -> BTreeMap { - let mut modified_amts = BTreeMap::new(); - for (key, amt) in self.modified_amts.get_untracked().iter() { - modified_amts.insert(key.clone(), amt.get_untracked().as_ref().clone()); - } - modified_amts - } - - pub fn reset_modified_amts(&self, modified_amts: BTreeMap) { - let mut modified_amts_copy = self.modified_amts.get().as_ref().clone(); - for (key, amt) in modified_amts { - modified_amts_copy - .entry(key) - .and_modify(|amt_signal| amt_signal.set(amt.clone())) - .or_insert_with(|| create_rc_signal(amt)); - } - self.modified_amts.set(modified_amts_copy); - } -} diff --git a/web/src/components/recipe_selection.rs b/web/src/components/recipe_selection.rs index e8ff8a6..b758161 100644 --- a/web/src/components/recipe_selection.rs +++ b/web/src/components/recipe_selection.rs @@ -16,7 +16,7 @@ use std::rc::Rc; use sycamore::prelude::*; use tracing::{debug, instrument}; -use crate::app_state::{self, Message, StateHandler}; +use crate::app_state::{Message, StateHandler}; #[derive(Props)] pub struct RecipeCheckBoxProps<'ctx> { @@ -46,7 +46,6 @@ pub fn RecipeSelection<'ctx, G: Html>( }, ), ); - let id_clone = id.clone(); let title = title.get().clone(); let for_id = id.clone(); let href = format!("/ui/recipe/view/{}", id); From 47fab335615d95694856401e49c519d42dc46903 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Thu, 29 Dec 2022 12:09:36 -0600 Subject: [PATCH 17/29] Use sycamore-state from github --- Cargo.lock | 3 ++- nix/kitchenWasm/default.nix | 1 + web/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ac5230..9c06522 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2213,7 +2213,8 @@ dependencies = [ [[package]] name = "sycamore-state" -version = "0.1.0" +version = "0.0.1" +source = "git+https://github.com/zaphar/sycamore-state?rev=v0.0.1#f74a051bf85330aef926a457d31e33f983966781" dependencies = [ "sycamore", "wasm-bindgen", diff --git a/nix/kitchenWasm/default.nix b/nix/kitchenWasm/default.nix index bef1696..eb10832 100644 --- a/nix/kitchenWasm/default.nix +++ b/nix/kitchenWasm/default.nix @@ -14,6 +14,7 @@ let # incorrect. We override those here. "sycamore-0.8.2" = "sha256-D968+8C5EelGGmot9/LkAlULZOf/Cr+1WYXRCMwb1nQ="; "sqlx-0.6.2" = "sha256-X/LFvtzRfiOIEZJiVzmFvvULPpjhqvI99pSwH7a//GM="; + "sycamore-state-0.0.1" = "sha256-RatNr1b6r7eP3fOVatHA44D9xhDAljqSIWtFpMeBA9Y="; }; }); in diff --git a/web/Cargo.toml b/web/Cargo.toml index 990d670..1250f8d 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -15,7 +15,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] recipes = { path = "../recipes" } client-api = { path = "../api", package="api", features = ["browser"] } -sycamore-state = { path = "../../sycamore-state"} +sycamore-state = { git="https://github.com/zaphar/sycamore-state", rev="v0.0.1" } # This makes debugging panics more tractable. console_error_panic_hook = "0.1.7" serde_json = "1.0.79" From a39ed5589fc41ebb32cc98a912d3d2370d0e122f Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Thu, 29 Dec 2022 12:42:05 -0600 Subject: [PATCH 18/29] Get rid of unnecessary create_effect calls --- web/src/app_state.rs | 1 + web/src/components/add_recipe.rs | 1 + web/src/components/categories.rs | 25 +++------- web/src/components/recipe.rs | 67 ++++++++++++-------------- web/src/components/recipe_plan.rs | 18 +------ web/src/components/recipe_selection.rs | 2 +- web/src/components/shopping_list.rs | 11 ++--- web/src/pages/login.rs | 30 +++++------- web/src/web.rs | 2 - 9 files changed, 58 insertions(+), 99 deletions(-) diff --git a/web/src/app_state.rs b/web/src/app_state.rs index eed2541..6fed7d5 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -185,6 +185,7 @@ impl MessageMapper for StateMachine { #[instrument(skip_all, fields(?msg))] fn map<'ctx>(&self, cx: Scope<'ctx>, msg: Message, original: &'ctx Signal) { let mut original_copy = original.get().as_ref().clone(); + debug!("handling state message"); match msg { Message::ResetRecipeCounts => { let mut map = BTreeMap::new(); diff --git a/web/src/components/add_recipe.rs b/web/src/components/add_recipe.rs index e47551f..a93ff50 100644 --- a/web/src/components/add_recipe.rs +++ b/web/src/components/add_recipe.rs @@ -48,6 +48,7 @@ pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View ) }); + // TODO(jwall): This create effect should no longer be necessary; create_effect(cx, move || { create_recipe_signal.track(); if !*dirty.get_untracked() { diff --git a/web/src/components/categories.rs b/web/src/components/categories.rs index 935f468..3e4c527 100644 --- a/web/src/components/categories.rs +++ b/web/src/components/categories.rs @@ -43,7 +43,6 @@ fn check_category_text_parses(unparsed: &str, error_text: &Signal) -> bo #[instrument(skip_all)] #[component] pub fn Categories<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { - let save_signal = create_signal(cx, ()); let error_text = create_signal(cx, String::new()); let category_text: &Signal = create_signal(cx, String::new()); let dirty = create_signal(cx, false); @@ -61,21 +60,6 @@ pub fn Categories<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie } }); - create_effect(cx, move || { - save_signal.track(); - if !*dirty.get() { - return; - } - spawn_local_scoped(cx, { - async move { - sh.dispatch( - cx, - Message::SetCategoryMap(category_text.get_untracked().as_ref().clone()), - ); - } - }); - }); - let dialog_view = view! {cx, dialog(id="error-dialog") { article{ @@ -102,10 +86,15 @@ pub fn Categories<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie check_category_text_parses(category_text.get().as_str(), error_text); }) { "Check" } " " span(role="button", on:click=move |_| { - // TODO(jwall): check and then save the categories. + if !*dirty.get() { + return; + } if check_category_text_parses(category_text.get().as_str(), error_text) { debug!("triggering category save"); - save_signal.trigger_subscribers(); + sh.dispatch( + cx, + Message::SetCategoryMap(category_text.get_untracked().as_ref().clone()), + ); } }) { "Save" } } diff --git a/web/src/components/recipe.rs b/web/src/components/recipe.rs index cb1c951..0677ef0 100644 --- a/web/src/components/recipe.rs +++ b/web/src/components/recipe.rs @@ -67,44 +67,8 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>) }); let id = create_memo(cx, || recipe.get().recipe_id().to_owned()); - let save_signal = create_signal(cx, ()); let dirty = create_signal(cx, false); - debug!("Creating effect"); - create_effect(cx, move || { - save_signal.track(); - if !*dirty.get_untracked() { - debug!("Recipe text is unchanged"); - return; - } - debug!("Recipe text is changed"); - spawn_local_scoped(cx, { - let store = crate::api::HttpStore::get_from_context(cx); - async move { - debug!("Attempting to save recipe"); - if let Err(e) = store - .save_recipes(vec![RecipeEntry( - id.get_untracked().as_ref().clone(), - text.get_untracked().as_ref().clone(), - )]) - .await - { - error!(?e, "Failed to save recipe"); - error_text.set(format!("{:?}", e)); - } else { - // We also need to set recipe in our state - dirty.set(false); - if let Ok(recipe) = recipes::parse::as_recipe(text.get_untracked().as_ref()) { - sh.dispatch( - cx, - Message::SetRecipe(id.get_untracked().as_ref().to_owned(), recipe), - ); - } - }; - } - }); - }); - debug!("creating editor view"); view! {cx, div(class="grid") { @@ -121,7 +85,36 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>) let unparsed = text.get(); if check_recipe_parses(unparsed.as_str(), error_text, aria_hint) { debug!("triggering a save"); - save_signal.trigger_subscribers(); + if !*dirty.get_untracked() { + debug!("Recipe text is unchanged"); + return; + } + debug!("Recipe text is changed"); + spawn_local_scoped(cx, { + let store = crate::api::HttpStore::get_from_context(cx); + async move { + debug!("Attempting to save recipe"); + if let Err(e) = store + .save_recipes(vec![RecipeEntry( + id.get_untracked().as_ref().clone(), + text.get_untracked().as_ref().clone(), + )]) + .await + { + error!(?e, "Failed to save recipe"); + error_text.set(format!("{:?}", e)); + } else { + // We also need to set recipe in our state + dirty.set(false); + if let Ok(recipe) = recipes::parse::as_recipe(text.get_untracked().as_ref()) { + sh.dispatch( + cx, + Message::SetRecipe(id.get_untracked().as_ref().to_owned(), recipe), + ); + } + }; + } + }); } else { } }) { "Save" } diff --git a/web/src/components/recipe_plan.rs b/web/src/components/recipe_plan.rs index 2d8c4b6..6c0c4ae 100644 --- a/web/src/components/recipe_plan.rs +++ b/web/src/components/recipe_plan.rs @@ -35,17 +35,6 @@ pub fn RecipePlan<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie } rows }); - let refresh_click = create_signal(cx, false); - let save_click = create_signal(cx, false); - // FIXME(jwall): We should probably make this a dispatch method instead. - create_effect(cx, move || { - refresh_click.track(); - sh.dispatch(cx, Message::LoadState); - }); - create_effect(cx, move || { - save_click.track(); - sh.dispatch(cx, Message::SaveState); - }); view! {cx, table(class="recipe_selector no-print") { (View::new_fragment( @@ -66,17 +55,14 @@ pub fn RecipePlan<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie )) } input(type="button", value="Reset", on:click=move |_| { - // Poor man's click event signaling. - let toggle = !*refresh_click.get(); - refresh_click.set(toggle); + sh.dispatch(cx, Message::LoadState); }) input(type="button", value="Clear All", on:click=move |_| { sh.dispatch(cx, Message::ResetRecipeCounts); }) input(type="button", value="Save Plan", on:click=move |_| { // Poor man's click event signaling. - let toggle = !*save_click.get(); - save_click.set(toggle); + sh.dispatch(cx, Message::SaveState); }) } } diff --git a/web/src/components/recipe_selection.rs b/web/src/components/recipe_selection.rs index b758161..e3a09f8 100644 --- a/web/src/components/recipe_selection.rs +++ b/web/src/components/recipe_selection.rs @@ -53,7 +53,7 @@ pub fn RecipeSelection<'ctx, G: Html>( view! {cx, div() { label(for=for_id) { a(href=href) { (*title) } } - input(type="number", class="item-count-sel", min="0", value=count, name=name, on:change=move |_| { + input(type="number", class="item-count-sel", min="0", bind:value=count, name=name, on:change=move |_| { debug!(idx=%id, count=%(*count.get()), "setting recipe count"); sh.dispatch(cx, Message::UpdateRecipeCount(id.as_ref().clone(), count.get().parse().expect("Count is not a valid usize"))); }) diff --git a/web/src/components/shopping_list.rs b/web/src/components/shopping_list.rs index b2289f5..c695e06 100644 --- a/web/src/components/shopping_list.rs +++ b/web/src/components/shopping_list.rs @@ -192,12 +192,6 @@ fn make_shopping_table<'ctx, G: Html>( #[component] pub fn ShoppingList<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { let show_staples = create_signal(cx, true); - let save_click = create_signal(cx, ()); - create_effect(cx, move || { - save_click.track(); - info!("Registering save request for inventory"); - sh.dispatch(cx, Message::SaveState); - }); view! {cx, h1 { "Shopping List " } label(for="show_staples_cb") { "Show staples" } @@ -209,8 +203,9 @@ pub fn ShoppingList<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> V input(type="button", value="Reset", class="no-print", on:click=move |_| { sh.dispatch(cx, Message::ResetInventory); }) - input(type="button", value="Save", class="no-print", on:click=|_| { - save_click.trigger_subscribers(); + input(type="button", value="Save", class="no-print", on:click=move |_| { + info!("Registering save request for inventory"); + sh.dispatch(cx, Message::SaveState); }) } } diff --git a/web/src/pages/login.rs b/web/src/pages/login.rs index 560a162..90ec765 100644 --- a/web/src/pages/login.rs +++ b/web/src/pages/login.rs @@ -11,7 +11,8 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use sycamore::{futures::spawn_local_scoped, prelude::*}; +use sycamore::futures::spawn_local_scoped; +use sycamore::prelude::*; use tracing::{debug, info}; use crate::app_state::{Message, StateHandler}; @@ -20,20 +21,6 @@ use crate::app_state::{Message, StateHandler}; pub fn LoginForm<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { let username = create_signal(cx, "".to_owned()); let password = create_signal(cx, "".to_owned()); - let clicked = create_signal(cx, ("".to_owned(), "".to_owned())); - create_effect(cx, move || { - let (username, password) = (*clicked.get()).clone(); - if username != "" && password != "" { - spawn_local_scoped(cx, async move { - let store = crate::api::HttpStore::get_from_context(cx); - debug!("authenticating against ui"); - // TODO(jwall): Navigate to plan if the below is successful. - if let Some(user_data) = store.authenticate(username, password).await { - sh.dispatch(cx, Message::SetUserData(user_data)); - } - }); - } - }); view! {cx, form() { label(for="username") { "Username" } @@ -42,9 +29,18 @@ pub fn LoginForm<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View input(type="password", bind:value=password) input(type="button", value="Login", on:click=move |_| { info!("Attempting login request"); - clicked.set(((*username.get_untracked()).clone(), (*password.get_untracked()).clone())); + let (username, password) = ((*username.get_untracked()).clone(), (*password.get_untracked()).clone()); + if username != "" && password != "" { + spawn_local_scoped(cx, async move { + let store = crate::api::HttpStore::get_from_context(cx); + debug!("authenticating against ui"); + // TODO(jwall): Navigate to plan if the below is successful. + if let Some(user_data) = store.authenticate(username, password).await { + sh.dispatch(cx, Message::SetUserData(user_data)); + } + }); + } debug!("triggering login click subscribers"); - clicked.trigger_subscribers(); }) { } } } diff --git a/web/src/web.rs b/web/src/web.rs index d217ba9..9a6fc1e 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -28,8 +28,6 @@ pub fn UI(cx: Scope) -> View { let app_state = crate::app_state::AppState::new(); let sh = crate::app_state::get_state_handler(cx, app_state, store); let view = create_signal(cx, View::empty()); - // FIXME(jwall): We need a way to trigger refreshes when required. Turn this - // into a create_effect with a refresh signal stored as a context. spawn_local_scoped(cx, { async move { sh.dispatch(cx, Message::LoadState); From 0167e6070d63688b4fd46775a56b128b3e2e3249 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Fri, 30 Dec 2022 16:43:56 -0600 Subject: [PATCH 19/29] Fix non dynamic routing :-( --- .gitignore | 3 +- web/src/components/tabs.rs | 1 - web/src/routing/mod.rs | 96 ++++++++++++++++++++++---------------- web/src/web.rs | 7 +-- 4 files changed, 58 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index ed4d94c..68539d4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ webdist/ nix/*/result result .vscode/ -.session_store/ \ No newline at end of file +.session_store/ +.gitignore/ \ No newline at end of file diff --git a/web/src/components/tabs.rs b/web/src/components/tabs.rs index 9771c41..2e876ef 100644 --- a/web/src/components/tabs.rs +++ b/web/src/components/tabs.rs @@ -43,7 +43,6 @@ pub fn TabbedView<'a, G: Html>(cx: Scope<'a>, state: TabState<'a, G>) -> View view! {cx, li(class=class) { a(href=href) { (show) } } } - // TODO }) .collect(), ); diff --git a/web/src/routing/mod.rs b/web/src/routing/mod.rs index 1bba2b9..fbc0169 100644 --- a/web/src/routing/mod.rs +++ b/web/src/routing/mod.rs @@ -12,11 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::app_state::StateHandler; +use crate::{ + app_state::StateHandler, + components::{Footer, Header}, + pages::*, +}; use sycamore::prelude::*; use sycamore_router::{HistoryIntegration, Route, Router}; - -use crate::pages::*; +use tracing::{debug, instrument}; #[derive(Route, Debug)] pub enum Routes { @@ -71,52 +74,63 @@ pub struct HandlerProps<'ctx> { sh: StateHandler<'ctx>, } +#[instrument(skip_all, fields(?route))] +fn route_switch<'ctx, G: Html>(route: &Routes, cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { + debug!("Handling route change"); + use ManageRoutes::*; + use PlanningRoutes::*; + match route { + Routes::Planning(Plan) => view! {cx, + PlanPage(sh) + }, + Routes::Planning(Inventory) => view! {cx, + InventoryPage(sh) + }, + Routes::Planning(Cook) => view! {cx, + CookPage(sh) + }, + Routes::Login => view! {cx, + LoginPage(sh) + }, + Routes::Recipe(RecipeRoutes::View(id)) => view! {cx, + RecipeViewPage(recipe=id.clone(), sh=sh) + }, + Routes::Recipe(RecipeRoutes::Edit(id)) => view! {cx, + RecipeEditPage(recipe=id.clone(), sh=sh) + }, + Routes::Manage(Categories) => view! {cx, + CategoryPage(sh) + }, + Routes::Manage(NewRecipe) => view! {cx, + AddRecipePage(sh) + }, + Routes::Manage(Staples) => view! {cx, + StaplesPage(sh) + }, + Routes::NotFound + | Routes::Manage(ManageRoutes::NotFound) + | Routes::Planning(PlanningRoutes::NotFound) + | Routes::Recipe(RecipeRoutes::NotFound) => view! {cx, + // TODO(Create a real one) + PlanPage(sh) + }, + } +} + #[component] pub fn Handler<'ctx, G: Html>(cx: Scope<'ctx>, props: HandlerProps<'ctx>) -> View { let HandlerProps { sh } = props; - use ManageRoutes::*; - use PlanningRoutes::*; view! {cx, Router( integration=HistoryIntegration::new(), view=move |cx: Scope, route: &ReadSignal| { - match route.get().as_ref() { - Routes::Planning(Plan) => view! {cx, - PlanPage(sh) - }, - Routes::Planning(Inventory) => view! {cx, - InventoryPage(sh) - }, - Routes::Planning(Cook) => view! {cx, - CookPage(sh) - }, - Routes::Login => view! {cx, - LoginPage(sh) - }, - Routes::Recipe(RecipeRoutes::View(id)) => view! {cx, - RecipeViewPage(recipe=id.clone(), sh=sh) - }, - Routes::Recipe(RecipeRoutes::Edit(id)) => view! {cx, - RecipeEditPage(recipe=id.clone(), sh=sh) - }, - Routes::Manage(Categories) => view! {cx, - CategoryPage(sh) - }, - Routes::Manage(NewRecipe) => view! {cx, - AddRecipePage(sh) - }, - Routes::Manage(Staples) => view! {cx, - StaplesPage(sh) - }, - Routes::NotFound - | Routes::Manage(ManageRoutes::NotFound) - | Routes::Planning(PlanningRoutes::NotFound) - | Routes::Recipe(RecipeRoutes::NotFound) => view! {cx, - // TODO(Create a real one) - PlanPage(sh) - }, + view!{cx, + div(class="app") { + Header(sh) + (route_switch(route.get().as_ref(), cx, sh)) + Footer { } + } } - }, ) } diff --git a/web/src/web.rs b/web/src/web.rs index 9a6fc1e..8791351 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -15,7 +15,6 @@ use sycamore::{futures::spawn_local_scoped, prelude::*}; use tracing::{info, instrument}; use crate::app_state::Message; -use crate::components::{Footer, Header}; use crate::{api, routing::Handler as RouteHandler}; #[instrument] @@ -33,11 +32,7 @@ pub fn UI(cx: Scope) -> View { sh.dispatch(cx, Message::LoadState); // TODO(jwall): This needs to be moved into the RouteHandler view.set(view! { cx, - div(class="app") { - Header(sh) - RouteHandler(sh=sh) - Footer { } - } + RouteHandler(sh=sh) }); } }); From 77cae25c74a96b3b62d40e26d599d67c6e45666c Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sat, 31 Dec 2022 15:57:09 -0600 Subject: [PATCH 20/29] Fix infinite signal loop in shopping list --- web/src/app_state.rs | 27 +++++--- web/src/components/shopping_list.rs | 97 ++++++++++++++--------------- 2 files changed, 66 insertions(+), 58 deletions(-) diff --git a/web/src/app_state.rs b/web/src/app_state.rs index 6fed7d5..50d21d9 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -20,6 +20,7 @@ use sycamore::futures::spawn_local_scoped; use sycamore::prelude::*; use sycamore_state::{Handler, MessageMapper}; use tracing::{debug, error, info, instrument, warn}; +use wasm_bindgen::throw_str; use crate::api::HttpStore; use crate::js_lib; @@ -27,7 +28,7 @@ use crate::js_lib; #[derive(Debug, Clone, PartialEq)] pub struct AppState { pub recipe_counts: BTreeMap, - pub extras: BTreeSet<(String, String)>, + pub extras: Vec<(String, String)>, pub staples: Option, pub recipes: BTreeMap, pub category_map: String, @@ -40,7 +41,7 @@ impl AppState { pub fn new() -> Self { Self { recipe_counts: BTreeMap::new(), - extras: BTreeSet::new(), + extras: Vec::new(), staples: None, recipes: BTreeMap::new(), category_map: String::new(), @@ -56,7 +57,8 @@ pub enum Message { ResetRecipeCounts, UpdateRecipeCount(String, usize), AddExtra(String, String), - RemoveExtra(String, String), + RemoveExtra(usize), + UpdateExtra(usize, String, String), SaveRecipe(RecipeEntry), SetRecipe(String, Recipe), // TODO(jwall): Remove this annotation when safe to do so. @@ -171,7 +173,7 @@ impl StateMachine { Ok((filtered_ingredients, modified_amts, extra_items)) => { state.modified_amts = modified_amts; state.filtered_ingredients = filtered_ingredients; - state.extras = BTreeSet::from_iter(extra_items); + state.extras = extra_items; } Err(e) => { error!("{:?}", e); @@ -198,11 +200,20 @@ impl MessageMapper for StateMachine { original_copy.recipe_counts.insert(id, count); } Message::AddExtra(amt, name) => { - original_copy.extras.insert((amt, name)); + original_copy.extras.push((amt, name)); } - Message::RemoveExtra(amt, name) => { - original_copy.extras.remove(&(amt, name)); + Message::RemoveExtra(idx) => { + 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) => { original_copy.staples = staples; } @@ -240,7 +251,7 @@ impl MessageMapper for StateMachine { Message::ResetInventory => { original_copy.filtered_ingredients = BTreeSet::new(); original_copy.modified_amts = BTreeMap::new(); - original_copy.extras = BTreeSet::new(); + original_copy.extras = Vec::new(); } Message::AddFilteredIngredient(key) => { original_copy.filtered_ingredients.insert(key); diff --git a/web/src/components/shopping_list.rs b/web/src/components/shopping_list.rs index c695e06..2d023d5 100644 --- a/web/src/components/shopping_list.rs +++ b/web/src/components/shopping_list.rs @@ -15,17 +15,20 @@ use std::collections::BTreeSet; use recipes::{IngredientAccumulator, IngredientKey}; use sycamore::prelude::*; -use tracing::{info, instrument}; +use tracing::{debug, info, instrument}; use crate::app_state::{Message, StateHandler}; +#[instrument(skip_all)] fn make_ingredients_rows<'ctx, G: Html>( cx: Scope<'ctx>, sh: StateHandler<'ctx>, show_staples: &'ctx ReadSignal, ) -> View { + debug!("Making ingredients rows"); let ingredients = sh.get_selector(cx, move |state| { let state = state.get(); + debug!("building ingredient list from state"); let mut acc = IngredientAccumulator::new(); for (id, count) in state.recipe_counts.iter() { for _ in 0..(*count) { @@ -45,7 +48,7 @@ fn make_ingredients_rows<'ctx, G: Html>( acc.ingredients() .into_iter() // 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 .map(|(k, (i, rs))| { 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 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() @@ -101,7 +101,9 @@ fn make_ingredients_rows<'ctx, G: Html>( view! {cx, tr { 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 { 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 { + debug!("Making extras rows"); let extras_read_signal = sh.get_selector(cx, |state| { - state - .get() - .extras - .iter() - .cloned() - .collect::>() + state.get().extras.iter().cloned().enumerate().collect() }); view! {cx, - Indexed( - iterable=extras_read_signal, - 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_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" } - } + Indexed( + iterable=extras_read_signal, + view= move |cx, (idx, (amt, name))| { + let amt_signal = create_signal(cx, amt.clone()); + let name_signal = create_signal(cx, name.clone()); + view! {cx, + tr { + td { + input(bind:value=amt_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 { + 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>, show_staples: &'ctx ReadSignal, ) -> View { - let extra_rows_view = make_extras_rows(cx, sh); - let ingredient_rows = make_ingredients_rows(cx, sh, show_staples); + debug!("Making shopping table"); view! {cx, table(class="pad-top shopping-list page-breaker container-fluid", role="grid") { tr { @@ -181,8 +176,8 @@ fn make_shopping_table<'ctx, G: Html>( th { " Recipes " } } tbody { - (ingredient_rows) - (extra_rows_view) + (make_ingredients_rows(cx, sh, show_staples)) + (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) (make_shopping_table(cx, sh, show_staples)) 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())); }) 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 |_| { - info!("Registering save request for inventory"); + info!("Registering save request for inventory"); sh.dispatch(cx, Message::SaveState); }) } From b8b11e07a48904309884864846e16e2b57d37a07 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sat, 31 Dec 2022 17:42:45 -0600 Subject: [PATCH 21/29] Update sycamore-state to v0.1.0 --- Cargo.lock | 4 ++-- web/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c06522..e8d4bb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2213,8 +2213,8 @@ dependencies = [ [[package]] name = "sycamore-state" -version = "0.0.1" -source = "git+https://github.com/zaphar/sycamore-state?rev=v0.0.1#f74a051bf85330aef926a457d31e33f983966781" +version = "0.1.0" +source = "git+https://github.com/zaphar/sycamore-state?rev=v0.1.0#bc8854b4dde1294915b2aa7f3fcd710515f3dfd8" dependencies = [ "sycamore", "wasm-bindgen", diff --git a/web/Cargo.toml b/web/Cargo.toml index 1250f8d..7619a16 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -15,7 +15,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] recipes = { path = "../recipes" } client-api = { path = "../api", package="api", features = ["browser"] } -sycamore-state = { git="https://github.com/zaphar/sycamore-state", rev="v0.0.1" } +sycamore-state = { git="https://github.com/zaphar/sycamore-state", rev="v0.1.0" } # This makes debugging panics more tractable. console_error_panic_hook = "0.1.7" serde_json = "1.0.79" From cdf95c5206ae22ff2ae0457231af5ea329f72876 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sat, 31 Dec 2022 17:45:39 -0600 Subject: [PATCH 22/29] get rid of the last create_effect call --- web/src/components/add_recipe.rs | 58 +++++++++++++++----------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/web/src/components/add_recipe.rs b/web/src/components/add_recipe.rs index a93ff50..8739143 100644 --- a/web/src/components/add_recipe.rs +++ b/web/src/components/add_recipe.rs @@ -48,37 +48,6 @@ pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View ) }); - // TODO(jwall): This create effect should no longer be necessary; - create_effect(cx, move || { - create_recipe_signal.track(); - if !*dirty.get_untracked() { - return; - } - spawn_local_scoped(cx, { - let store = crate::api::HttpStore::get_from_context(cx); - async move { - let entry = entry.get_untracked(); - // TODO(jwall): Better error reporting here. - match store.get_recipe_text(entry.recipe_id()).await { - Ok(Some(_)) => { - // TODO(jwall): We should tell the user that this id already exists - info!(recipe_id = entry.recipe_id(), "Recipe already exists"); - return; - } - Ok(None) => { - // noop - } - Err(err) => { - // TODO(jwall): We should tell the user that this is failing - error!(?err) - } - } - sh.dispatch(cx, Message::SaveRecipe((*entry).clone())); - crate::js_lib::navigate_to_path(&format!("/ui/recipe/{}", entry.recipe_id())) - .expect("Unable to navigate to recipe"); - } - }); - }); view! {cx, label(for="recipe_title") { "Recipe Title" } input(bind:value=recipe_title, type="text", name="recipe_title", id="recipe_title", on:change=move |_| { @@ -86,6 +55,33 @@ pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View }) button(on:click=move |_| { create_recipe_signal.trigger_subscribers(); + if !*dirty.get_untracked() { + return; + } + spawn_local_scoped(cx, { + let store = crate::api::HttpStore::get_from_context(cx); + async move { + let entry = entry.get_untracked(); + // TODO(jwall): Better error reporting here. + match store.get_recipe_text(entry.recipe_id()).await { + Ok(Some(_)) => { + // TODO(jwall): We should tell the user that this id already exists + info!(recipe_id = entry.recipe_id(), "Recipe already exists"); + return; + } + Ok(None) => { + // noop + } + Err(err) => { + // TODO(jwall): We should tell the user that this is failing + error!(?err) + } + } + sh.dispatch(cx, Message::SaveRecipe((*entry).clone())); + crate::js_lib::navigate_to_path(&format!("/ui/recipe/{}", entry.recipe_id())) + .expect("Unable to navigate to recipe"); + } + }); }) { "Create" } } } From fe3f2e896b458635fc8b0649b2e255fa909c9624 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sun, 1 Jan 2023 13:23:17 -0600 Subject: [PATCH 23/29] Fix new recipe redirect url --- web/src/components/add_recipe.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/add_recipe.rs b/web/src/components/add_recipe.rs index 8739143..f740189 100644 --- a/web/src/components/add_recipe.rs +++ b/web/src/components/add_recipe.rs @@ -78,7 +78,7 @@ pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View } } sh.dispatch(cx, Message::SaveRecipe((*entry).clone())); - crate::js_lib::navigate_to_path(&format!("/ui/recipe/{}", entry.recipe_id())) + crate::js_lib::navigate_to_path(&format!("/ui/recipe/edit/{}", entry.recipe_id())) .expect("Unable to navigate to recipe"); } }); From c424432defd4c6769cebe2f9290f8bffc36855a0 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sun, 1 Jan 2023 13:36:25 -0600 Subject: [PATCH 24/29] Give localstorage a full implemenation --- web/src/api.rs | 256 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 183 insertions(+), 73 deletions(-) diff --git a/web/src/api.rs b/web/src/api.rs index 20e5a41..40e1519 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -21,7 +21,8 @@ use tracing::{debug, error, instrument, warn}; use client_api::*; use recipes::{parse, IngredientKey, Recipe, RecipeEntry}; -use wasm_bindgen::JsValue; +use wasm_bindgen::{JsValue, UnwrapThrowExt}; +use web_sys::Storage; use crate::{app_state::AppState, js_lib}; @@ -107,14 +108,170 @@ fn token68(user: String, pass: String) -> String { base64::encode(format!("{}:{}", user, pass)) } +#[derive(Clone, Debug)] +pub struct LocalStore { + store: Storage, +} + +impl LocalStore { + pub fn new() -> Self { + Self { + store: js_lib::get_storage(), + } + } + + /// Gets user data from local storage. + pub fn get_user_data(&self) -> Option { + self.store + .get("user_data") + .map_or(None, |val| val.map(|val| from_str(&val).unwrap_or(None))) + .flatten() + } + + // Set's user data to local storage. + pub fn set_user_data(&self, data: Option<&UserData>) { + if let Some(data) = data { + self.store + .set("user_data", &to_string(data).unwrap_throw()) + .unwrap_throw(); + } else { + self.store.delete("user_data").unwrap_throw(); + } + } + + /// Gets categories from local storage. + pub fn get_categories(&self) -> Option { + self.store.get("categories").unwrap_throw() + } + + /// Set the categories to the given string. + pub fn set_categories(&self, categories: Option<&String>) { + if let Some(c) = categories { + self.store.set("categories", c).unwrap_throw(); + } else { + self.store.delete("categories").unwrap_throw() + } + } + + fn get_storage_keys(&self) -> Vec { + let mut keys = Vec::new(); + for idx in 0..self.store.length().unwrap() { + keys.push(self.store.key(idx).unwrap_throw().unwrap_throw()) + } + keys + } + + fn get_recipe_keys(&self) -> impl Iterator { + self.get_storage_keys() + .into_iter() + .filter(|k| k.starts_with("recipe:")) + } + + /// Gets all the recipes from local storage. + pub fn get_recipes(&self) -> Option> { + let mut recipe_list = Vec::new(); + for recipe_key in self.get_recipe_keys() { + if let Some(entry) = self.store.get(&recipe_key).unwrap_throw() { + match from_str(&entry) { + Ok(entry) => { + recipe_list.push(entry); + } + Err(e) => { + error!(recipe_key, err = ?e, "Failed to parse recipe entry"); + } + } + } + } + if recipe_list.is_empty() { + return None; + } + Some(recipe_list) + } + + /// Sets the set of recipes to the entries passed in. Deletes any recipes not + /// in the list. + pub fn set_all_recipes(&self, entries: &Vec) { + for recipe_key in self.get_recipe_keys() { + self.store.delete(&recipe_key).unwrap_throw(); + } + for entry in entries { + self.set_recipe_entry(entry); + } + } + + /// Set recipe entry in local storage. + pub fn set_recipe_entry(&self, entry: &RecipeEntry) { + self.store + .set( + &recipe_key(entry.recipe_id()), + &to_string(&entry).unwrap_throw(), + ) + .unwrap_throw() + } + + /// Delete recipe entry from local storage. + pub fn delete_recipe_entry(&self, recipe_key: &str) { + self.store.delete(recipe_key).unwrap_throw() + } + + /// Save working plan to local storage. + pub fn save_plan(&self, plan: &Vec<(String, i32)>) { + self.store + .set("plan", &to_string(&plan).unwrap_throw()) + .unwrap_throw(); + } + + pub fn get_plan(&self) -> Option> { + if let Some(plan) = self.store.get("plan").unwrap_throw() { + Some(from_str(&plan).unwrap_throw()) + } else { + None + } + } + + pub fn get_inventory_data( + &self, + ) -> Option<( + BTreeSet, + BTreeMap, + Vec<(String, String)>, + )> { + if let Some(inventory) = self.store.get("inventory").unwrap_throw() { + return Some(from_str(&inventory).unwrap_throw()); + } + return None; + } + + pub fn delete_inventory_data(&self) { + self.store.delete("inventory").unwrap_throw(); + } + + pub fn set_inventory_data( + &self, + inventory: ( + &BTreeSet, + &BTreeMap, + &Vec<(String, String)>, + ), + ) { + self.store + .set("inventory", &to_string(&inventory).unwrap_throw()) + .unwrap_throw(); + } +} + #[derive(Clone, Debug)] pub struct HttpStore { root: String, + local_store: LocalStore, } impl HttpStore { pub fn new(root: String) -> Self { - Self { root } + Self { + root, + local_store: LocalStore::new(), + } } pub fn v1_path(&self) -> String { @@ -143,7 +300,6 @@ impl HttpStore { debug!("attempting login request against api."); let mut path = self.v1_path(); path.push_str("/auth"); - let storage = js_lib::get_storage(); let result = reqwasm::http::Request::get(&path) .header( "Authorization", @@ -158,12 +314,7 @@ impl HttpStore { .await .expect("Unparseable authentication response") .as_success(); - storage - .set( - "user_data", - &to_string(&user_data).expect("Unable to serialize user_data"), - ) - .unwrap(); + self.local_store.set_user_data(user_data.as_ref()); return user_data; } error!(status = resp.status(), "Login was unsuccessful") @@ -177,12 +328,11 @@ impl HttpStore { pub async fn get_categories(&self) -> Result, Error> { let mut path = self.v1_path(); path.push_str("/categories"); - let storage = js_lib::get_storage(); let resp = match reqwasm::http::Request::get(&path).send().await { Ok(resp) => resp, Err(reqwasm::Error::JsError(err)) => { error!(path, ?err, "Error hitting api"); - return Ok(storage.get("categories")?); + return Ok(self.local_store.get_categories()); } Err(err) => { return Err(err)?; @@ -190,14 +340,14 @@ impl HttpStore { }; if resp.status() == 404 { debug!("Categories returned 404"); - storage.remove_item("categories")?; + self.local_store.set_categories(None); Ok(None) } else if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back!"); let resp = resp.json::().await?.as_success().unwrap(); - storage.set("categories", &resp)?; + self.local_store.set_categories(Some(&resp)); Ok(Some(resp)) } } @@ -206,26 +356,16 @@ impl HttpStore { pub async fn get_recipes(&self) -> Result>, Error> { let mut path = self.v1_path(); path.push_str("/recipes"); - let storage = js_lib::get_storage(); let resp = match reqwasm::http::Request::get(&path).send().await { Ok(resp) => resp, Err(reqwasm::Error::JsError(err)) => { error!(path, ?err, "Error hitting api"); - let mut entries = Vec::new(); - for key in js_lib::get_storage_keys() { - if key.starts_with("recipe:") { - let entry = from_str(&storage.get_item(&key)?.unwrap()) - .map_err(|e| format!("{}", e))?; - entries.push(entry); - } - } - return Ok(Some(entries)); + return Ok(self.local_store.get_recipes()); } Err(err) => { return Err(err)?; } }; - let storage = js_lib::get_storage(); if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { @@ -236,12 +376,7 @@ impl HttpStore { .map_err(|e| format!("{}", e))? .as_success(); if let Some(ref entries) = entries { - for r in entries.iter() { - storage.set( - &recipe_key(r.recipe_id()), - &to_string(&r).expect("Unable to serialize recipe entries"), - )?; - } + self.local_store.set_all_recipes(&entries); } Ok(entries) } @@ -293,15 +428,11 @@ impl HttpStore { pub async fn save_recipes(&self, recipes: Vec) -> Result<(), Error> { let mut path = self.v1_path(); path.push_str("/recipes"); - let storage = js_lib::get_storage(); for r in recipes.iter() { if r.recipe_id().is_empty() { return Err("Recipe Ids can not be empty".into()); } - storage.set( - &recipe_key(r.recipe_id()), - &to_string(&r).expect("Unable to serialize recipe entries"), - )?; + self.local_store.set_recipe_entry(&r); } let serialized = to_string(&recipes).expect("Unable to serialize recipe entries"); let resp = reqwasm::http::Request::post(&path) @@ -321,8 +452,7 @@ impl HttpStore { pub async fn save_categories(&self, categories: String) -> Result<(), Error> { let mut path = self.v1_path(); path.push_str("/categories"); - let storage = js_lib::get_storage(); - storage.set("categories", &categories)?; + self.local_store.set_categories(Some(&categories)); let resp = reqwasm::http::Request::post(&path) .body(to_string(&categories).expect("Unable to encode categories as json")) .header("content-type", "application/json") @@ -360,9 +490,7 @@ impl HttpStore { pub async fn save_plan(&self, plan: Vec<(String, i32)>) -> Result<(), Error> { let mut path = self.v1_path(); path.push_str("/plan"); - let storage = js_lib::get_storage(); - let serialized_plan = to_string(&plan).expect("Unable to encode plan as json"); - storage.set("plan", &serialized_plan)?; + self.local_store.save_plan(&plan); let resp = reqwasm::http::Request::post(&path) .body(to_string(&plan).expect("Unable to encode plan as json")) .header("content-type", "application/json") @@ -380,7 +508,6 @@ impl HttpStore { let mut path = self.v1_path(); path.push_str("/plan"); let resp = reqwasm::http::Request::get(&path).send().await?; - let storage = js_lib::get_storage(); if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { @@ -391,8 +518,7 @@ impl HttpStore { .map_err(|e| format!("{}", e))? .as_success(); if let Some(ref entry) = plan { - let serialized: String = to_string(entry).map_err(|e| format!("{}", e))?; - storage.set("plan", &serialized)? + self.local_store.save_plan(&entry); } Ok(plan) } @@ -414,26 +540,9 @@ impl HttpStore { let resp = reqwasm::http::Request::get(&path).send().await?; if resp.status() != 200 { let err = Err(format!("Status: {}", resp.status()).into()); - Ok(match storage.get("inventory") { - Ok(Some(val)) => match from_str(&val) { - // TODO(jwall): Once we remove the v1 endpoint this is no longer needed. - Ok((filtered_ingredients, modified_amts)) => { - (filtered_ingredients, modified_amts, Vec::new()) - } - Err(_) => match from_str(&val) { - Ok((filtered_ingredients, modified_amts, extra_items)) => { - (filtered_ingredients, modified_amts, extra_items) - } - Err(_) => { - // Whatever is in storage is corrupted or invalid so we should delete it. - storage - .delete("inventory") - .expect("Unable to delete corrupt data in inventory cache"); - return err; - } - }, - }, - Ok(None) | Err(_) => return err, + Ok(match self.local_store.get_inventory_data() { + Some(val) => val, + None => return err, }) } else { debug!("We got a valid response back"); @@ -447,11 +556,11 @@ impl HttpStore { .map_err(|e| format!("{}", e))? .as_success() .unwrap(); - let _ = storage.set( - "inventory", - &to_string(&(&filtered_ingredients, &modified_amts)) - .expect("Failed to serialize inventory data"), - ); + self.local_store.set_inventory_data(( + &(filtered_ingredients.iter().cloned().collect()), + &(modified_amts.iter().cloned().collect()), + &extra_items, + )); Ok(( filtered_ingredients.into_iter().collect(), modified_amts.into_iter().collect(), @@ -471,13 +580,14 @@ impl HttpStore { path.push_str("/inventory"); let filtered_ingredients: Vec = filtered_ingredients.into_iter().collect(); let modified_amts: Vec<(IngredientKey, String)> = modified_amts.into_iter().collect(); + debug!("Storing inventory data in cache"); + self.local_store.set_inventory_data(( + &(filtered_ingredients.iter().cloned().collect()), + &(modified_amts.iter().cloned().collect()), + &extra_items, + )); let serialized_inventory = to_string(&(filtered_ingredients, modified_amts, extra_items)) .expect("Unable to encode plan as json"); - let storage = js_lib::get_storage(); - debug!("Storing inventory data in cache"); - storage - .set("inventory", &serialized_inventory) - .expect("Failed to cache inventory data"); debug!("Storing inventory data via API"); let resp = reqwasm::http::Request::post(&path) .body(&serialized_inventory) From 0f28b758fa5345f35ea6fec09d3a7b3ba4618bb4 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Mon, 2 Jan 2023 12:54:10 -0600 Subject: [PATCH 25/29] Update our local cache in the StateMachine --- web/src/api.rs | 39 +++-------- web/src/app_state.rs | 151 +++++++++++++++++++++++++------------------ web/src/js_lib.rs | 9 --- 3 files changed, 98 insertions(+), 101 deletions(-) diff --git a/web/src/api.rs b/web/src/api.rs index 40e1519..72d35b1 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -188,6 +188,13 @@ impl LocalStore { Some(recipe_list) } + pub fn get_recipe_entry(&self, id: &str) -> Option { + self.store + .get(&recipe_key(id)) + .unwrap_throw() + .map(|entry| from_str(&entry).unwrap_throw()) + } + /// Sets the set of recipes to the entries passed in. Deletes any recipes not /// in the list. pub fn set_all_recipes(&self, entries: &Vec) { @@ -314,7 +321,6 @@ impl HttpStore { .await .expect("Unparseable authentication response") .as_success(); - self.local_store.set_user_data(user_data.as_ref()); return user_data; } error!(status = resp.status(), "Login was unsuccessful") @@ -340,14 +346,12 @@ impl HttpStore { }; if resp.status() == 404 { debug!("Categories returned 404"); - self.local_store.set_categories(None); Ok(None) } else if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back!"); let resp = resp.json::().await?.as_success().unwrap(); - self.local_store.set_categories(Some(&resp)); Ok(Some(resp)) } } @@ -375,9 +379,6 @@ impl HttpStore { .await .map_err(|e| format!("{}", e))? .as_success(); - if let Some(ref entries) = entries { - self.local_store.set_all_recipes(&entries); - } Ok(entries) } } @@ -389,15 +390,11 @@ impl HttpStore { let mut path = self.v1_path(); path.push_str("/recipe/"); path.push_str(id.as_ref()); - let storage = js_lib::get_storage(); let resp = match reqwasm::http::Request::get(&path).send().await { Ok(resp) => resp, Err(reqwasm::Error::JsError(err)) => { error!(path, ?err, "Error hitting api"); - return match storage.get(&recipe_key(&id))? { - Some(s) => Ok(Some(from_str(&s).map_err(|e| format!("{}", e))?)), - None => Ok(None), - }; + return Ok(self.local_store.get_recipe_entry(id.as_ref())); } Err(err) => { return Err(err)?; @@ -417,8 +414,7 @@ impl HttpStore { .as_success() .unwrap(); if let Some(ref entry) = entry { - let serialized: String = to_string(entry).map_err(|e| format!("{}", e))?; - storage.set(&recipe_key(entry.recipe_id()), &serialized)? + self.local_store.set_recipe_entry(entry); } Ok(entry) } @@ -432,7 +428,6 @@ impl HttpStore { if r.recipe_id().is_empty() { return Err("Recipe Ids can not be empty".into()); } - self.local_store.set_recipe_entry(&r); } let serialized = to_string(&recipes).expect("Unable to serialize recipe entries"); let resp = reqwasm::http::Request::post(&path) @@ -452,7 +447,6 @@ impl HttpStore { pub async fn save_categories(&self, categories: String) -> Result<(), Error> { let mut path = self.v1_path(); path.push_str("/categories"); - self.local_store.set_categories(Some(&categories)); let resp = reqwasm::http::Request::post(&path) .body(to_string(&categories).expect("Unable to encode categories as json")) .header("content-type", "application/json") @@ -490,7 +484,6 @@ impl HttpStore { pub async fn save_plan(&self, plan: Vec<(String, i32)>) -> Result<(), Error> { let mut path = self.v1_path(); path.push_str("/plan"); - self.local_store.save_plan(&plan); let resp = reqwasm::http::Request::post(&path) .body(to_string(&plan).expect("Unable to encode plan as json")) .header("content-type", "application/json") @@ -517,9 +510,6 @@ impl HttpStore { .await .map_err(|e| format!("{}", e))? .as_success(); - if let Some(ref entry) = plan { - self.local_store.save_plan(&entry); - } Ok(plan) } } @@ -536,7 +526,6 @@ impl HttpStore { > { let mut path = self.v2_path(); path.push_str("/inventory"); - let storage = js_lib::get_storage(); let resp = reqwasm::http::Request::get(&path).send().await?; if resp.status() != 200 { let err = Err(format!("Status: {}", resp.status()).into()); @@ -556,11 +545,6 @@ impl HttpStore { .map_err(|e| format!("{}", e))? .as_success() .unwrap(); - self.local_store.set_inventory_data(( - &(filtered_ingredients.iter().cloned().collect()), - &(modified_amts.iter().cloned().collect()), - &extra_items, - )); Ok(( filtered_ingredients.into_iter().collect(), modified_amts.into_iter().collect(), @@ -581,11 +565,6 @@ impl HttpStore { let filtered_ingredients: Vec = filtered_ingredients.into_iter().collect(); let modified_amts: Vec<(IngredientKey, String)> = modified_amts.into_iter().collect(); debug!("Storing inventory data in cache"); - self.local_store.set_inventory_data(( - &(filtered_ingredients.iter().cloned().collect()), - &(modified_amts.iter().cloned().collect()), - &extra_items, - )); let serialized_inventory = to_string(&(filtered_ingredients, modified_amts, extra_items)) .expect("Unable to encode plan as json"); debug!("Storing inventory data via API"); diff --git a/web/src/app_state.rs b/web/src/app_state.rs index 50d21d9..4c49da2 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -15,15 +15,13 @@ use std::collections::{BTreeMap, BTreeSet}; use client_api::UserData; use recipes::{parse, IngredientKey, Recipe, RecipeEntry}; -use serde_json::from_str; use sycamore::futures::spawn_local_scoped; use sycamore::prelude::*; use sycamore_state::{Handler, MessageMapper}; use tracing::{debug, error, info, instrument, warn}; -use wasm_bindgen::throw_str; +use wasm_bindgen::{throw_str, UnwrapThrowExt}; -use crate::api::HttpStore; -use crate::js_lib; +use crate::api::{HttpStore, LocalStore}; #[derive(Debug, Clone, PartialEq)] pub struct AppState { @@ -61,25 +59,19 @@ pub enum Message { UpdateExtra(usize, String, String), SaveRecipe(RecipeEntry), SetRecipe(String, Recipe), - // TODO(jwall): Remove this annotation when safe to do so. - #[allow(dead_code)] - RemoveRecipe(String), - // TODO(jwall): Remove this annotation when safe to do so. - #[allow(dead_code)] - SetStaples(Option), SetCategoryMap(String), ResetInventory, AddFilteredIngredient(IngredientKey), UpdateAmt(IngredientKey, String), SetUserData(UserData), - // TODO(jwall): Remove this annotation when safe to do so. - #[allow(dead_code)] - UnsetUserData, SaveState, LoadState, } -pub struct StateMachine(HttpStore); +pub struct StateMachine { + store: HttpStore, + local_store: LocalStore, +} #[instrument] fn filter_recipes( @@ -110,27 +102,26 @@ fn filter_recipes( } impl StateMachine { - async fn load_state(store: HttpStore, original: &Signal) { + pub fn new(store: HttpStore, local_store: LocalStore) -> Self { + Self { store, local_store } + } + + async fn load_state( + store: &HttpStore, + local_store: &LocalStore, + original: &Signal, + ) -> Result<(), crate::api::Error> { let mut state = original.get().as_ref().clone(); info!("Synchronizing Recipes"); - // TODO(jwall): Make our caching logic using storage more robust. - let recipe_entries = match store.get_recipes().await { - Ok(recipe_entries) => { - if let Ok((staples, recipes)) = filter_recipes(&recipe_entries) { - state.staples = staples; - if let Some(recipes) = recipes { - state.recipes = recipes; - } - } - recipe_entries - } - Err(err) => { - error!(?err); - None - } + let recipe_entries = &store.get_recipes().await?; + let (staples, recipes) = filter_recipes(&recipe_entries)?; + if let Some(recipes) = recipes { + state.staples = staples; + state.recipes = recipes; }; - if let Ok(Some(plan)) = store.get_plan().await { + let plan = store.get_plan().await?; + if let Some(plan) = plan { // set the counts. let mut plan_map = BTreeMap::new(); for (id, count) in plan { @@ -146,15 +137,8 @@ impl StateMachine { } } info!("Checking for user_data in local storage"); - let storage = js_lib::get_storage(); - let user_data = storage - .get("user_data") - .expect("Couldn't read from storage"); - if let Some(data) = user_data { - if let Ok(user_data) = from_str(&data) { - state.auth = Some(user_data); - } - } + let user_data = local_store.get_user_data(); + state.auth = user_data; info!("Synchronizing categories"); match store.get_categories().await { Ok(Some(categories_content)) => { @@ -180,6 +164,7 @@ impl StateMachine { } } original.set(state); + Ok(()) } } @@ -194,28 +179,51 @@ impl MessageMapper for StateMachine { for (id, _) in original_copy.recipes.iter() { map.insert(id.clone(), 0); } + let plan: Vec<(String, i32)> = + map.iter().map(|(s, i)| (s.clone(), *i as i32)).collect(); + self.local_store.save_plan(&plan); original_copy.recipe_counts = map; } Message::UpdateRecipeCount(id, count) => { original_copy.recipe_counts.insert(id, count); + let plan: Vec<(String, i32)> = original_copy + .recipe_counts + .iter() + .map(|(s, i)| (s.clone(), *i as i32)) + .collect(); + self.local_store.save_plan(&plan); } Message::AddExtra(amt, name) => { original_copy.extras.push((amt, name)); + self.local_store.set_inventory_data(( + &original_copy.filtered_ingredients, + &original_copy.modified_amts, + &original_copy.extras, + )) } Message::RemoveExtra(idx) => { original_copy.extras.remove(idx); + self.local_store.set_inventory_data(( + &original_copy.filtered_ingredients, + &original_copy.modified_amts, + &original_copy.extras, + )) } - Message::UpdateExtra(idx, amt, name) => match original_copy.extras.get_mut(idx) { - Some(extra) => { - extra.0 = amt; - extra.1 = name; + 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"); + } } - None => { - throw_str("Attempted to remove extra that didn't exist"); - } - }, - Message::SetStaples(staples) => { - original_copy.staples = staples; + self.local_store.set_inventory_data(( + &original_copy.filtered_ingredients, + &original_copy.modified_amts, + &original_copy.extras, + )) } Message::SetRecipe(id, recipe) => { original_copy.recipes.insert(id, recipe); @@ -226,22 +234,21 @@ impl MessageMapper for StateMachine { original_copy .recipes .insert(entry.recipe_id().to_owned(), recipe); - let store = self.0.clone(); original_copy .recipe_counts .insert(entry.recipe_id().to_owned(), 0); + let store = self.store.clone(); + self.local_store.set_recipe_entry(&entry); spawn_local_scoped(cx, async move { if let Err(e) = store.save_recipes(vec![entry]).await { error!(err=?e, "Unable to save Recipe"); } }); } - Message::RemoveRecipe(id) => { - original_copy.recipes.remove(&id); - } Message::SetCategoryMap(category_text) => { - let store = self.0.clone(); original_copy.category_map = category_text.clone(); + self.local_store.set_categories(Some(&category_text)); + let store = self.store.clone(); spawn_local_scoped(cx, async move { if let Err(e) = store.save_categories(category_text).await { error!(?e, "Failed to save categories"); @@ -252,22 +259,34 @@ impl MessageMapper for StateMachine { original_copy.filtered_ingredients = BTreeSet::new(); original_copy.modified_amts = BTreeMap::new(); original_copy.extras = Vec::new(); + self.local_store.set_inventory_data(( + &original_copy.filtered_ingredients, + &original_copy.modified_amts, + &original_copy.extras, + )); } Message::AddFilteredIngredient(key) => { original_copy.filtered_ingredients.insert(key); + self.local_store.set_inventory_data(( + &original_copy.filtered_ingredients, + &original_copy.modified_amts, + &original_copy.extras, + )); } Message::UpdateAmt(key, amt) => { original_copy.modified_amts.insert(key, amt); + self.local_store.set_inventory_data(( + &original_copy.filtered_ingredients, + &original_copy.modified_amts, + &original_copy.extras, + )); } Message::SetUserData(user_data) => { original_copy.auth = Some(user_data); } - Message::UnsetUserData => { - original_copy.auth = None; - } Message::SaveState => { - let store = self.0.clone(); let original_copy = original_copy.clone(); + let store = self.store.clone(); spawn_local_scoped(cx, async move { if let Err(e) = store.save_app_state(original_copy).await { error!(err=?e, "Error saving app state") @@ -275,9 +294,17 @@ impl MessageMapper for StateMachine { }); } Message::LoadState => { - let store = self.0.clone(); + let store = self.store.clone(); + let local_store = self.local_store.clone(); spawn_local_scoped(cx, async move { - Self::load_state(store, original).await; + Self::load_state(&store, &local_store, original) + .await + .unwrap_throw(); + local_store.set_inventory_data(( + &original.get().filtered_ingredients, + &original.get().modified_amts, + &original.get().extras, + )); }); return; } @@ -293,5 +320,5 @@ pub fn get_state_handler<'ctx>( initial: AppState, store: HttpStore, ) -> StateHandler<'ctx> { - Handler::new(cx, initial, StateMachine(store)) + Handler::new(cx, initial, StateMachine::new(store, LocalStore::new())) } diff --git a/web/src/js_lib.rs b/web/src/js_lib.rs index c4f2b73..ef99c4d 100644 --- a/web/src/js_lib.rs +++ b/web/src/js_lib.rs @@ -43,12 +43,3 @@ pub fn get_storage() -> Storage { .expect("Failed to get storage") .expect("No storage available") } - -pub fn get_storage_keys() -> Vec { - let storage = get_storage(); - let mut keys = Vec::new(); - for idx in 0..storage.length().unwrap() { - keys.push(get_storage().key(idx).unwrap().unwrap()) - } - keys -} From 165719520a07131f4c7db366d96695258a9b065f Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Mon, 2 Jan 2023 14:18:40 -0600 Subject: [PATCH 26/29] Fix various errors --- web/src/api.rs | 99 ++++++++++++++++++++++++++++++++------------ web/src/app_state.rs | 4 +- 2 files changed, 75 insertions(+), 28 deletions(-) diff --git a/web/src/api.rs b/web/src/api.rs index 72d35b1..05f6d9d 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -21,7 +21,7 @@ use tracing::{debug, error, instrument, warn}; use client_api::*; use recipes::{parse, IngredientKey, Recipe, RecipeEntry}; -use wasm_bindgen::{JsValue, UnwrapThrowExt}; +use wasm_bindgen::JsValue; use web_sys::Storage; use crate::{app_state::AppState, js_lib}; @@ -132,31 +132,44 @@ impl LocalStore { pub fn set_user_data(&self, data: Option<&UserData>) { if let Some(data) = data { self.store - .set("user_data", &to_string(data).unwrap_throw()) - .unwrap_throw(); + .set( + "user_data", + &to_string(data).expect("Failed to desrialize user_data"), + ) + .expect("Failed to set user_data"); } else { - self.store.delete("user_data").unwrap_throw(); + self.store + .delete("user_data") + .expect("Failed to delete user_data"); } } /// Gets categories from local storage. pub fn get_categories(&self) -> Option { - self.store.get("categories").unwrap_throw() + self.store + .get("categories") + .expect("Failed go get categories") } /// Set the categories to the given string. pub fn set_categories(&self, categories: Option<&String>) { if let Some(c) = categories { - self.store.set("categories", c).unwrap_throw(); + self.store + .set("categories", c) + .expect("Failed to set categories"); } else { - self.store.delete("categories").unwrap_throw() + self.store + .delete("categories") + .expect("Failed to delete categories") } } fn get_storage_keys(&self) -> Vec { let mut keys = Vec::new(); for idx in 0..self.store.length().unwrap() { - keys.push(self.store.key(idx).unwrap_throw().unwrap_throw()) + if let Some(k) = self.store.key(idx).expect("Failed to get storage key") { + keys.push(k) + } } keys } @@ -171,7 +184,11 @@ impl LocalStore { pub fn get_recipes(&self) -> Option> { let mut recipe_list = Vec::new(); for recipe_key in self.get_recipe_keys() { - if let Some(entry) = self.store.get(&recipe_key).unwrap_throw() { + if let Some(entry) = self + .store + .get(&recipe_key) + .expect(&format!("Failed to get recipe: {}", recipe_key)) + { match from_str(&entry) { Ok(entry) => { recipe_list.push(entry); @@ -189,17 +206,20 @@ impl LocalStore { } pub fn get_recipe_entry(&self, id: &str) -> Option { + let key = recipe_key(id); self.store - .get(&recipe_key(id)) - .unwrap_throw() - .map(|entry| from_str(&entry).unwrap_throw()) + .get(&key) + .expect(&format!("Failed to get recipe {}", key)) + .map(|entry| from_str(&entry).expect(&format!("Failed to get recipe {}", key))) } /// Sets the set of recipes to the entries passed in. Deletes any recipes not /// in the list. pub fn set_all_recipes(&self, entries: &Vec) { for recipe_key in self.get_recipe_keys() { - self.store.delete(&recipe_key).unwrap_throw(); + self.store + .delete(&recipe_key) + .expect(&format!("Failed to get recipe {}", recipe_key)); } for entry in entries { self.set_recipe_entry(entry); @@ -211,26 +231,28 @@ impl LocalStore { self.store .set( &recipe_key(entry.recipe_id()), - &to_string(&entry).unwrap_throw(), + &to_string(&entry).expect(&format!("Failed to get recipe {}", entry.recipe_id())), ) - .unwrap_throw() + .expect(&format!("Failed to store recipe {}", entry.recipe_id())) } /// Delete recipe entry from local storage. - pub fn delete_recipe_entry(&self, recipe_key: &str) { - self.store.delete(recipe_key).unwrap_throw() + pub fn delete_recipe_entry(&self, recipe_id: &str) { + self.store + .delete(&recipe_key(recipe_id)) + .expect(&format!("Failed to delete recipe {}", recipe_id)) } /// Save working plan to local storage. pub fn save_plan(&self, plan: &Vec<(String, i32)>) { self.store - .set("plan", &to_string(&plan).unwrap_throw()) - .unwrap_throw(); + .set("plan", &to_string(&plan).expect("Failed to serialize plan")) + .expect("Failed to store plan'"); } pub fn get_plan(&self) -> Option> { - if let Some(plan) = self.store.get("plan").unwrap_throw() { - Some(from_str(&plan).unwrap_throw()) + if let Some(plan) = self.store.get("plan").expect("Failed to store plan") { + Some(from_str(&plan).expect("Failed to deserialize plan")) } else { None } @@ -243,14 +265,25 @@ impl LocalStore { BTreeMap, Vec<(String, String)>, )> { - if let Some(inventory) = self.store.get("inventory").unwrap_throw() { - return Some(from_str(&inventory).unwrap_throw()); + if let Some(inventory) = self + .store + .get("inventory") + .expect("Failed to retrieve inventory data") + { + let (filtered, modified, extras): ( + BTreeSet, + Vec<(IngredientKey, String)>, + Vec<(String, String)>, + ) = from_str(&inventory).expect("Failed to deserialize inventory"); + return Some((filtered, BTreeMap::from_iter(modified), extras)); } return None; } pub fn delete_inventory_data(&self) { - self.store.delete("inventory").unwrap_throw(); + self.store + .delete("inventory") + .expect("Failed to delete inventory data"); } pub fn set_inventory_data( @@ -261,9 +294,23 @@ impl LocalStore { &Vec<(String, String)>, ), ) { + let filtered = inventory.0; + let modified_amts = inventory + .1 + .iter() + .map(|(k, amt)| (k.clone(), amt.clone())) + .collect::>(); + let extras = inventory.2; + let inventory_data = (filtered, &modified_amts, extras); self.store - .set("inventory", &to_string(&inventory).unwrap_throw()) - .unwrap_throw(); + .set( + "inventory", + &to_string(&inventory_data).expect(&format!( + "Failed to serialize inventory {:?}", + inventory_data + )), + ) + .expect("Failed to set inventory"); } } diff --git a/web/src/app_state.rs b/web/src/app_state.rs index 4c49da2..88bbdca 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -19,7 +19,7 @@ use sycamore::futures::spawn_local_scoped; use sycamore::prelude::*; use sycamore_state::{Handler, MessageMapper}; use tracing::{debug, error, info, instrument, warn}; -use wasm_bindgen::{throw_str, UnwrapThrowExt}; +use wasm_bindgen::throw_str; use crate::api::{HttpStore, LocalStore}; @@ -299,7 +299,7 @@ impl MessageMapper for StateMachine { spawn_local_scoped(cx, async move { Self::load_state(&store, &local_store, original) .await - .unwrap_throw(); + .expect("Failed to load_state."); local_store.set_inventory_data(( &original.get().filtered_ingredients, &original.get().modified_amts, From 6d216265210cc4e4417cca10b97252bfce7540dc Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Mon, 2 Jan 2023 18:06:10 -0600 Subject: [PATCH 27/29] Load State after logging in --- web/src/pages/login.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/pages/login.rs b/web/src/pages/login.rs index 90ec765..614191f 100644 --- a/web/src/pages/login.rs +++ b/web/src/pages/login.rs @@ -34,9 +34,10 @@ pub fn LoginForm<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View spawn_local_scoped(cx, async move { let store = crate::api::HttpStore::get_from_context(cx); debug!("authenticating against ui"); - // TODO(jwall): Navigate to plan if the below is successful. if let Some(user_data) = store.authenticate(username, password).await { sh.dispatch(cx, Message::SetUserData(user_data)); + sh.dispatch(cx, Message::LoadState); + sycamore_router::navigate("/ui/planning/plan"); } }); } From dd9ce0fca2998e371ce8a71fc5715ad83ecce272 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Mon, 2 Jan 2023 20:48:31 -0600 Subject: [PATCH 28/29] Properly delay the navigation until state has loaded --- web/src/app_state.rs | 54 +++++++++++++++++++++++++---- web/src/components/recipe_plan.rs | 4 +-- web/src/components/shopping_list.rs | 2 +- web/src/pages/login.rs | 3 +- web/src/web.rs | 4 +-- 5 files changed, 53 insertions(+), 14 deletions(-) diff --git a/web/src/app_state.rs b/web/src/app_state.rs index 88bbdca..81bbbd8 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -11,7 +11,10 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::{BTreeMap, BTreeSet}; +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt::Debug, +}; use client_api::UserData; use recipes::{parse, IngredientKey, Recipe, RecipeEntry}; @@ -50,7 +53,6 @@ impl AppState { } } -#[derive(Debug)] pub enum Message { ResetRecipeCounts, UpdateRecipeCount(String, usize), @@ -64,8 +66,46 @@ pub enum Message { AddFilteredIngredient(IngredientKey), UpdateAmt(IngredientKey, String), SetUserData(UserData), - SaveState, - LoadState, + SaveState(Option>), + LoadState(Option>), +} + +impl Debug for Message { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ResetRecipeCounts => write!(f, "ResetRecipeCounts"), + Self::UpdateRecipeCount(arg0, arg1) => f + .debug_tuple("UpdateRecipeCount") + .field(arg0) + .field(arg1) + .finish(), + Self::AddExtra(arg0, arg1) => { + f.debug_tuple("AddExtra").field(arg0).field(arg1).finish() + } + Self::RemoveExtra(arg0) => f.debug_tuple("RemoveExtra").field(arg0).finish(), + Self::UpdateExtra(arg0, arg1, arg2) => f + .debug_tuple("UpdateExtra") + .field(arg0) + .field(arg1) + .field(arg2) + .finish(), + Self::SaveRecipe(arg0) => f.debug_tuple("SaveRecipe").field(arg0).finish(), + Self::SetRecipe(arg0, arg1) => { + f.debug_tuple("SetRecipe").field(arg0).field(arg1).finish() + } + Self::SetCategoryMap(arg0) => f.debug_tuple("SetCategoryMap").field(arg0).finish(), + Self::ResetInventory => write!(f, "ResetInventory"), + Self::AddFilteredIngredient(arg0) => { + f.debug_tuple("AddFilteredIngredient").field(arg0).finish() + } + Self::UpdateAmt(arg0, arg1) => { + f.debug_tuple("UpdateAmt").field(arg0).field(arg1).finish() + } + Self::SetUserData(arg0) => f.debug_tuple("SetUserData").field(arg0).finish(), + Self::SaveState(_) => write!(f, "SaveState"), + Self::LoadState(_) => write!(f, "LoadState"), + } + } } pub struct StateMachine { @@ -284,16 +324,17 @@ impl MessageMapper for StateMachine { Message::SetUserData(user_data) => { original_copy.auth = Some(user_data); } - Message::SaveState => { + Message::SaveState(f) => { let original_copy = original_copy.clone(); let store = self.store.clone(); spawn_local_scoped(cx, async move { if let Err(e) = store.save_app_state(original_copy).await { error!(err=?e, "Error saving app state") }; + f.map(|f| f()); }); } - Message::LoadState => { + Message::LoadState(f) => { let store = self.store.clone(); let local_store = self.local_store.clone(); spawn_local_scoped(cx, async move { @@ -305,6 +346,7 @@ impl MessageMapper for StateMachine { &original.get().modified_amts, &original.get().extras, )); + f.map(|f| f()); }); return; } diff --git a/web/src/components/recipe_plan.rs b/web/src/components/recipe_plan.rs index 6c0c4ae..697c2ee 100644 --- a/web/src/components/recipe_plan.rs +++ b/web/src/components/recipe_plan.rs @@ -55,14 +55,14 @@ pub fn RecipePlan<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie )) } input(type="button", value="Reset", on:click=move |_| { - sh.dispatch(cx, Message::LoadState); + sh.dispatch(cx, Message::LoadState(None)); }) input(type="button", value="Clear All", on:click=move |_| { sh.dispatch(cx, Message::ResetRecipeCounts); }) input(type="button", value="Save Plan", on:click=move |_| { // Poor man's click event signaling. - sh.dispatch(cx, Message::SaveState); + sh.dispatch(cx, Message::SaveState(None)); }) } } diff --git a/web/src/components/shopping_list.rs b/web/src/components/shopping_list.rs index 2d023d5..1a219e8 100644 --- a/web/src/components/shopping_list.rs +++ b/web/src/components/shopping_list.rs @@ -202,7 +202,7 @@ pub fn ShoppingList<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> V }) input(type="button", value="Save", class="no-print", on:click=move |_| { info!("Registering save request for inventory"); - sh.dispatch(cx, Message::SaveState); + sh.dispatch(cx, Message::SaveState(None)); }) } } diff --git a/web/src/pages/login.rs b/web/src/pages/login.rs index 614191f..d16c495 100644 --- a/web/src/pages/login.rs +++ b/web/src/pages/login.rs @@ -36,8 +36,7 @@ pub fn LoginForm<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View debug!("authenticating against ui"); if let Some(user_data) = store.authenticate(username, password).await { sh.dispatch(cx, Message::SetUserData(user_data)); - sh.dispatch(cx, Message::LoadState); - sycamore_router::navigate("/ui/planning/plan"); + sh.dispatch(cx, Message::LoadState(Some(Box::new(|| sycamore_router::navigate("/ui/planning/plan"))))); } }); } diff --git a/web/src/web.rs b/web/src/web.rs index 8791351..bff2228 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -20,7 +20,6 @@ use crate::{api, routing::Handler as RouteHandler}; #[instrument] #[component] pub fn UI(cx: Scope) -> View { - // FIXME(jwall): We shouldn't need to get the store from a context anymore. api::HttpStore::provide_context(cx, "/api".to_owned()); let store = api::HttpStore::get_from_context(cx).as_ref().clone(); info!("Starting UI"); @@ -29,8 +28,7 @@ pub fn UI(cx: Scope) -> View { let view = create_signal(cx, View::empty()); spawn_local_scoped(cx, { async move { - sh.dispatch(cx, Message::LoadState); - // TODO(jwall): This needs to be moved into the RouteHandler + sh.dispatch(cx, Message::LoadState(None)); view.set(view! { cx, RouteHandler(sh=sh) }); From e859455db0e4392115af08a9895ba7112ee8ff3d Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 4 Jan 2023 18:11:35 -0500 Subject: [PATCH 29/29] Properly cache items during load state --- web/src/app_state.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/web/src/app_state.rs b/web/src/app_state.rs index 81bbbd8..a90de0f 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -159,6 +159,9 @@ impl StateMachine { state.staples = staples; state.recipes = recipes; }; + if let Some(recipe_entries) = recipe_entries { + local_store.set_all_recipes(recipe_entries); + } let plan = store.get_plan().await?; if let Some(plan) = plan { @@ -176,6 +179,12 @@ impl StateMachine { } } } + let plan = state + .recipe_counts + .iter() + .map(|(k, v)| (k.clone(), *v as i32)) + .collect::>(); + local_store.save_plan(&plan); info!("Checking for user_data in local storage"); let user_data = local_store.get_user_data(); state.auth = user_data; @@ -183,10 +192,12 @@ impl StateMachine { match store.get_categories().await { Ok(Some(categories_content)) => { debug!(categories=?categories_content); + local_store.set_categories(Some(&categories_content)); state.category_map = categories_content; } Ok(None) => { warn!("There is no category file"); + local_store.set_categories(None); } Err(e) => { error!("{:?}", e); @@ -195,6 +206,11 @@ impl StateMachine { info!("Synchronizing inventory data"); match store.get_inventory_data().await { Ok((filtered_ingredients, modified_amts, extra_items)) => { + local_store.set_inventory_data(( + &filtered_ingredients, + &modified_amts, + &extra_items, + )); state.modified_amts = modified_amts; state.filtered_ingredients = filtered_ingredients; state.extras = extra_items; @@ -322,6 +338,7 @@ impl MessageMapper for StateMachine { )); } Message::SetUserData(user_data) => { + self.local_store.set_user_data(Some(&user_data)); original_copy.auth = Some(user_data); } Message::SaveState(f) => {