diff --git a/Cargo.lock b/Cargo.lock index a306a08..6a3ed90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1319,6 +1319,7 @@ dependencies = [ "base64 0.20.0", "chrono", "console_error_panic_hook", + "js-sys", "recipes", "reqwasm", "serde_json", diff --git a/web/Cargo.toml b/web/Cargo.toml index 25e669b..5d6cf92 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -23,6 +23,7 @@ tracing = "0.1.35" async-trait = "0.1.57" base64 = "0.20.0" sycamore-router = "0.8" +js-sys = "0.3.60" [dependencies.tracing-subscriber] version = "0.3.16" diff --git a/web/src/app_state.rs b/web/src/app_state.rs index bbbc851..49d4b63 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -59,17 +59,17 @@ pub enum Message { AddExtra(String, String), RemoveExtra(usize), UpdateExtra(usize, String, String), - SaveRecipe(RecipeEntry), + SaveRecipe(RecipeEntry, Option>), SetRecipe(String, Recipe), - RemoveRecipe(String), - UpdateCategory(String, String), + RemoveRecipe(String, Option>), + UpdateCategory(String, String, Option>), ResetInventory, AddFilteredIngredient(IngredientKey), UpdateAmt(IngredientKey, String), SetUserData(UserData), SaveState(Option>), LoadState(Option>), - UpdateStaples(String), + UpdateStaples(String, Option>), } impl Debug for Message { @@ -91,12 +91,12 @@ impl Debug for Message { .field(arg1) .field(arg2) .finish(), - Self::SaveRecipe(arg0) => f.debug_tuple("SaveRecipe").field(arg0).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::RemoveRecipe(arg0) => f.debug_tuple("SetCategoryMap").field(arg0).finish(), - Self::UpdateCategory(i, c) => { + Self::RemoveRecipe(arg0, _) => f.debug_tuple("SetCategoryMap").field(arg0).finish(), + Self::UpdateCategory(i, c, _) => { f.debug_tuple("UpdateCategory").field(i).field(c).finish() } Self::ResetInventory => write!(f, "ResetInventory"), @@ -109,7 +109,7 @@ impl Debug for Message { Self::SetUserData(arg0) => f.debug_tuple("SetUserData").field(arg0).finish(), Self::SaveState(_) => write!(f, "SaveState"), Self::LoadState(_) => write!(f, "LoadState"), - Self::UpdateStaples(arg) => f.debug_tuple("UpdateStaples").field(arg).finish(), + Self::UpdateStaples(arg, _) => f.debug_tuple("UpdateStaples").field(arg).finish(), } } } @@ -312,7 +312,7 @@ impl MessageMapper for StateMachine { Message::SetRecipe(id, recipe) => { original_copy.recipes.insert(id, recipe); } - Message::SaveRecipe(entry) => { + Message::SaveRecipe(entry, callback) => { let recipe = parse::as_recipe(entry.recipe_text()).expect("Failed to parse RecipeEntry"); original_copy @@ -327,9 +327,10 @@ impl MessageMapper for StateMachine { if let Err(e) = store.store_recipes(vec![entry]).await { error!(err=?e, "Unable to save Recipe"); } + callback.map(|f| f()); }); } - Message::RemoveRecipe(recipe) => { + Message::RemoveRecipe(recipe, callback) => { original_copy.recipe_counts.remove(&recipe); original_copy.recipes.remove(&recipe); self.local_store.delete_recipe_entry(&recipe); @@ -338,9 +339,10 @@ impl MessageMapper for StateMachine { if let Err(err) = store.delete_recipe(&recipe).await { error!(?err, "Failed to delete recipe"); } + callback.map(|f| f()); }); } - Message::UpdateCategory(ingredient, category) => { + Message::UpdateCategory(ingredient, category, callback) => { self.local_store .set_categories(Some(&vec![(ingredient.clone(), category.clone())])); original_copy @@ -351,6 +353,7 @@ impl MessageMapper for StateMachine { if let Err(e) = store.store_categories(&vec![(ingredient, category)]).await { error!(?e, "Failed to save categories"); } + callback.map(|f| f()); }); } Message::ResetInventory => { @@ -409,7 +412,7 @@ impl MessageMapper for StateMachine { }); return; } - Message::UpdateStaples(content) => { + Message::UpdateStaples(content, callback) => { let store = self.store.clone(); let local_store = self.local_store.clone(); spawn_local_scoped(cx, async move { @@ -418,6 +421,7 @@ impl MessageMapper for StateMachine { .store_staples(content) .await .expect("Failed to store staples"); + callback.map(|f| f()); }); return; } diff --git a/web/src/components/add_recipe.rs b/web/src/components/add_recipe.rs index f007127..9827678 100644 --- a/web/src/components/add_recipe.rs +++ b/web/src/components/add_recipe.rs @@ -77,9 +77,10 @@ pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View error!(?err) } } - sh.dispatch(cx, Message::SaveRecipe((*entry).clone())); - crate::js_lib::navigate_to_path(&format!("/ui/recipe/edit/{}", entry.recipe_id())) - .expect("Unable to navigate to recipe"); + sh.dispatch(cx, Message::SaveRecipe((*entry).clone(), Some(Box::new({ + let path = format!("/ui/recipe/edit/{}", entry.recipe_id()); + move || sycamore_router::navigate(path.as_str()) + })))); } }); }) { "Create" } diff --git a/web/src/components/categories.rs b/web/src/components/categories.rs index f464752..f792067 100644 --- a/web/src/components/categories.rs +++ b/web/src/components/categories.rs @@ -69,7 +69,7 @@ fn CategoryRow<'ctx, G: Html>(cx: Scope<'ctx>, props: CategoryRowProps<'ctx>) -> td() { input(type="text", list="category_options", bind:value=category, on:change={ let ingredient_clone = ingredient.clone(); move |_| { - sh.dispatch(cx, Message::UpdateCategory(ingredient_clone.clone(), category.get_untracked().as_ref().clone())); + sh.dispatch(cx, Message::UpdateCategory(ingredient_clone.clone(), category.get_untracked().as_ref().clone(), None)); } }) } } diff --git a/web/src/components/recipe.rs b/web/src/components/recipe.rs index 79bc8ba..ca5399b 100644 --- a/web/src/components/recipe.rs +++ b/web/src/components/recipe.rs @@ -14,7 +14,10 @@ use sycamore::{futures::spawn_local_scoped, prelude::*}; use tracing::{debug, error}; -use crate::app_state::{Message, StateHandler}; +use crate::{ + app_state::{Message, StateHandler}, + js_lib, +}; use recipes::{self, RecipeEntry}; fn check_recipe_parses( @@ -68,21 +71,25 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>) let id = create_memo(cx, || recipe.get().recipe_id().to_owned()); let dirty = create_signal(cx, false); + let ts = create_signal(cx, js_lib::get_ms_timestamp()); debug!("creating editor view"); view! {cx, div(class="grid") { textarea(bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| { dirty.set(true); + check_recipe_parses(text.get_untracked().as_str(), error_text, aria_hint); + }, on:input=move |_| { + let current_ts = js_lib::get_ms_timestamp(); + if (current_ts - *ts.get_untracked()) > 100 { + check_recipe_parses(text.get_untracked().as_str(), error_text, aria_hint); + ts.set(current_ts); + } }) div(class="parse") { (error_text.get()) } } span(role="button", on:click=move |_| { - let unparsed = text.get(); - check_recipe_parses(unparsed.as_str(), error_text, aria_hint); - }) { "Check" } " " - span(role="button", on:click=move |_| { - let unparsed = text.get(); + let unparsed = text.get_untracked(); if check_recipe_parses(unparsed.as_str(), error_text, aria_hint) { debug!("triggering a save"); if !*dirty.get_untracked() { @@ -119,9 +126,8 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>) } }) { "Save" } " " span(role="button", on:click=move |_| { - sh.dispatch(cx, Message::RemoveRecipe(id.get_untracked().as_ref().to_owned())); - sycamore_router::navigate("/ui/planning/plan") - }) { "delete" } + sh.dispatch(cx, Message::RemoveRecipe(id.get_untracked().as_ref().to_owned(), Some(Box::new(|| sycamore_router::navigate("/ui/planning/plan"))))); + }) { "delete" } " " } } diff --git a/web/src/components/staples.rs b/web/src/components/staples.rs index ef2100f..3cb420f 100644 --- a/web/src/components/staples.rs +++ b/web/src/components/staples.rs @@ -15,6 +15,7 @@ use sycamore::{futures::spawn_local_scoped, prelude::*}; use tracing::{debug, error}; use crate::app_state::{Message, StateHandler}; +use crate::js_lib; use recipes::{self, parse}; fn check_ingredients_parses( @@ -67,19 +68,22 @@ pub fn IngredientsEditor<'ctx, G: Html>( }); let dirty = create_signal(cx, false); + let ts = create_signal(cx, js_lib::get_ms_timestamp()); debug!("creating editor view"); view! {cx, div(class="grid") { textarea(bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| { dirty.set(true); + }, on:input=move |_| { + let current_ts = js_lib::get_ms_timestamp(); + if (current_ts - *ts.get_untracked()) > 100 { + check_ingredients_parses(text.get_untracked().as_str(), error_text, aria_hint); + ts.set(current_ts); + } }) div(class="parse") { (error_text.get()) } } - span(role="button", on:click=move |_| { - let unparsed = text.get(); - check_ingredients_parses(unparsed.as_str(), error_text, aria_hint); - }) { "Check" } " " span(role="button", on:click=move |_| { let unparsed = text.get(); if !*dirty.get_untracked() { @@ -89,7 +93,7 @@ pub fn IngredientsEditor<'ctx, G: Html>( debug!("triggering a save"); if check_ingredients_parses(unparsed.as_str(), error_text, aria_hint) { debug!("Staples text is changed"); - sh.dispatch(cx, Message::UpdateStaples(unparsed.as_ref().clone())); + sh.dispatch(cx, Message::UpdateStaples(unparsed.as_ref().clone(), None)); } }) { "Save" } } diff --git a/web/src/js_lib.rs b/web/src/js_lib.rs index 8ef0740..eab0eea 100644 --- a/web/src/js_lib.rs +++ b/web/src/js_lib.rs @@ -11,6 +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 js_sys::Date; use wasm_bindgen::JsValue; use web_sys::{window, Storage}; @@ -28,3 +29,7 @@ pub fn get_storage() -> Storage { .expect("Failed to get storage") .expect("No storage available") } + +pub fn get_ms_timestamp() -> u32 { + Date::new_0().get_milliseconds() +}