On the fly recipe parse checks

This commit is contained in:
Jeremy Wall 2023-01-07 16:34:15 -05:00
parent 829b81d3e2
commit 7559aae9d0
8 changed files with 52 additions and 30 deletions

1
Cargo.lock generated
View File

@ -1319,6 +1319,7 @@ dependencies = [
"base64 0.20.0", "base64 0.20.0",
"chrono", "chrono",
"console_error_panic_hook", "console_error_panic_hook",
"js-sys",
"recipes", "recipes",
"reqwasm", "reqwasm",
"serde_json", "serde_json",

View File

@ -23,6 +23,7 @@ tracing = "0.1.35"
async-trait = "0.1.57" async-trait = "0.1.57"
base64 = "0.20.0" base64 = "0.20.0"
sycamore-router = "0.8" sycamore-router = "0.8"
js-sys = "0.3.60"
[dependencies.tracing-subscriber] [dependencies.tracing-subscriber]
version = "0.3.16" version = "0.3.16"

View File

@ -59,17 +59,17 @@ pub enum Message {
AddExtra(String, String), AddExtra(String, String),
RemoveExtra(usize), RemoveExtra(usize),
UpdateExtra(usize, String, String), UpdateExtra(usize, String, String),
SaveRecipe(RecipeEntry), SaveRecipe(RecipeEntry, Option<Box<dyn FnOnce()>>),
SetRecipe(String, Recipe), SetRecipe(String, Recipe),
RemoveRecipe(String), RemoveRecipe(String, Option<Box<dyn FnOnce()>>),
UpdateCategory(String, String), UpdateCategory(String, String, Option<Box<dyn FnOnce()>>),
ResetInventory, ResetInventory,
AddFilteredIngredient(IngredientKey), AddFilteredIngredient(IngredientKey),
UpdateAmt(IngredientKey, String), UpdateAmt(IngredientKey, String),
SetUserData(UserData), SetUserData(UserData),
SaveState(Option<Box<dyn FnOnce()>>), SaveState(Option<Box<dyn FnOnce()>>),
LoadState(Option<Box<dyn FnOnce()>>), LoadState(Option<Box<dyn FnOnce()>>),
UpdateStaples(String), UpdateStaples(String, Option<Box<dyn FnOnce()>>),
} }
impl Debug for Message { impl Debug for Message {
@ -91,12 +91,12 @@ impl Debug for Message {
.field(arg1) .field(arg1)
.field(arg2) .field(arg2)
.finish(), .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) => { Self::SetRecipe(arg0, arg1) => {
f.debug_tuple("SetRecipe").field(arg0).field(arg1).finish() f.debug_tuple("SetRecipe").field(arg0).field(arg1).finish()
} }
Self::RemoveRecipe(arg0) => f.debug_tuple("SetCategoryMap").field(arg0).finish(), Self::RemoveRecipe(arg0, _) => f.debug_tuple("SetCategoryMap").field(arg0).finish(),
Self::UpdateCategory(i, c) => { Self::UpdateCategory(i, c, _) => {
f.debug_tuple("UpdateCategory").field(i).field(c).finish() f.debug_tuple("UpdateCategory").field(i).field(c).finish()
} }
Self::ResetInventory => write!(f, "ResetInventory"), Self::ResetInventory => write!(f, "ResetInventory"),
@ -109,7 +109,7 @@ impl Debug for Message {
Self::SetUserData(arg0) => f.debug_tuple("SetUserData").field(arg0).finish(), Self::SetUserData(arg0) => f.debug_tuple("SetUserData").field(arg0).finish(),
Self::SaveState(_) => write!(f, "SaveState"), Self::SaveState(_) => write!(f, "SaveState"),
Self::LoadState(_) => write!(f, "LoadState"), 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<Message, AppState> for StateMachine {
Message::SetRecipe(id, recipe) => { Message::SetRecipe(id, recipe) => {
original_copy.recipes.insert(id, recipe); original_copy.recipes.insert(id, recipe);
} }
Message::SaveRecipe(entry) => { Message::SaveRecipe(entry, callback) => {
let recipe = let recipe =
parse::as_recipe(entry.recipe_text()).expect("Failed to parse RecipeEntry"); parse::as_recipe(entry.recipe_text()).expect("Failed to parse RecipeEntry");
original_copy original_copy
@ -327,9 +327,10 @@ impl MessageMapper<Message, AppState> for StateMachine {
if let Err(e) = store.store_recipes(vec![entry]).await { if let Err(e) = store.store_recipes(vec![entry]).await {
error!(err=?e, "Unable to save Recipe"); 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.recipe_counts.remove(&recipe);
original_copy.recipes.remove(&recipe); original_copy.recipes.remove(&recipe);
self.local_store.delete_recipe_entry(&recipe); self.local_store.delete_recipe_entry(&recipe);
@ -338,9 +339,10 @@ impl MessageMapper<Message, AppState> for StateMachine {
if let Err(err) = store.delete_recipe(&recipe).await { if let Err(err) = store.delete_recipe(&recipe).await {
error!(?err, "Failed to delete recipe"); error!(?err, "Failed to delete recipe");
} }
callback.map(|f| f());
}); });
} }
Message::UpdateCategory(ingredient, category) => { Message::UpdateCategory(ingredient, category, callback) => {
self.local_store self.local_store
.set_categories(Some(&vec![(ingredient.clone(), category.clone())])); .set_categories(Some(&vec![(ingredient.clone(), category.clone())]));
original_copy original_copy
@ -351,6 +353,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
if let Err(e) = store.store_categories(&vec![(ingredient, category)]).await { if let Err(e) = store.store_categories(&vec![(ingredient, category)]).await {
error!(?e, "Failed to save categories"); error!(?e, "Failed to save categories");
} }
callback.map(|f| f());
}); });
} }
Message::ResetInventory => { Message::ResetInventory => {
@ -409,7 +412,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
}); });
return; return;
} }
Message::UpdateStaples(content) => { Message::UpdateStaples(content, callback) => {
let store = self.store.clone(); let store = self.store.clone();
let local_store = self.local_store.clone(); let local_store = self.local_store.clone();
spawn_local_scoped(cx, async move { spawn_local_scoped(cx, async move {
@ -418,6 +421,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
.store_staples(content) .store_staples(content)
.await .await
.expect("Failed to store staples"); .expect("Failed to store staples");
callback.map(|f| f());
}); });
return; return;
} }

View File

@ -77,9 +77,10 @@ pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View
error!(?err) error!(?err)
} }
} }
sh.dispatch(cx, Message::SaveRecipe((*entry).clone())); sh.dispatch(cx, Message::SaveRecipe((*entry).clone(), Some(Box::new({
crate::js_lib::navigate_to_path(&format!("/ui/recipe/edit/{}", entry.recipe_id())) let path = format!("/ui/recipe/edit/{}", entry.recipe_id());
.expect("Unable to navigate to recipe"); move || sycamore_router::navigate(path.as_str())
}))));
} }
}); });
}) { "Create" } }) { "Create" }

View File

@ -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={ td() { input(type="text", list="category_options", bind:value=category, on:change={
let ingredient_clone = ingredient.clone(); let ingredient_clone = ingredient.clone();
move |_| { 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));
} }
}) } }) }
} }

View File

@ -14,7 +14,10 @@
use sycamore::{futures::spawn_local_scoped, prelude::*}; use sycamore::{futures::spawn_local_scoped, prelude::*};
use tracing::{debug, error}; use tracing::{debug, error};
use crate::app_state::{Message, StateHandler}; use crate::{
app_state::{Message, StateHandler},
js_lib,
};
use recipes::{self, RecipeEntry}; use recipes::{self, RecipeEntry};
fn check_recipe_parses( 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 id = create_memo(cx, || recipe.get().recipe_id().to_owned());
let dirty = create_signal(cx, false); let dirty = create_signal(cx, false);
let ts = create_signal(cx, js_lib::get_ms_timestamp());
debug!("creating editor view"); debug!("creating editor view");
view! {cx, view! {cx,
div(class="grid") { div(class="grid") {
textarea(bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| { textarea(bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| {
dirty.set(true); 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()) } div(class="parse") { (error_text.get()) }
} }
span(role="button", on:click=move |_| { span(role="button", on:click=move |_| {
let unparsed = text.get(); let unparsed = text.get_untracked();
check_recipe_parses(unparsed.as_str(), error_text, aria_hint);
}) { "Check" } " "
span(role="button", on:click=move |_| {
let unparsed = text.get();
if check_recipe_parses(unparsed.as_str(), error_text, aria_hint) { if check_recipe_parses(unparsed.as_str(), error_text, aria_hint) {
debug!("triggering a save"); debug!("triggering a save");
if !*dirty.get_untracked() { if !*dirty.get_untracked() {
@ -119,9 +126,8 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>)
} }
}) { "Save" } " " }) { "Save" } " "
span(role="button", on:click=move |_| { span(role="button", on:click=move |_| {
sh.dispatch(cx, Message::RemoveRecipe(id.get_untracked().as_ref().to_owned())); sh.dispatch(cx, Message::RemoveRecipe(id.get_untracked().as_ref().to_owned(), Some(Box::new(|| sycamore_router::navigate("/ui/planning/plan")))));
sycamore_router::navigate("/ui/planning/plan") }) { "delete" } " "
}) { "delete" }
} }
} }

View File

@ -15,6 +15,7 @@ use sycamore::{futures::spawn_local_scoped, prelude::*};
use tracing::{debug, error}; use tracing::{debug, error};
use crate::app_state::{Message, StateHandler}; use crate::app_state::{Message, StateHandler};
use crate::js_lib;
use recipes::{self, parse}; use recipes::{self, parse};
fn check_ingredients_parses( fn check_ingredients_parses(
@ -67,19 +68,22 @@ pub fn IngredientsEditor<'ctx, G: Html>(
}); });
let dirty = create_signal(cx, false); let dirty = create_signal(cx, false);
let ts = create_signal(cx, js_lib::get_ms_timestamp());
debug!("creating editor view"); debug!("creating editor view");
view! {cx, view! {cx,
div(class="grid") { div(class="grid") {
textarea(bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| { textarea(bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| {
dirty.set(true); 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()) } 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 |_| { span(role="button", on:click=move |_| {
let unparsed = text.get(); let unparsed = text.get();
if !*dirty.get_untracked() { if !*dirty.get_untracked() {
@ -89,7 +93,7 @@ pub fn IngredientsEditor<'ctx, G: Html>(
debug!("triggering a save"); debug!("triggering a save");
if check_ingredients_parses(unparsed.as_str(), error_text, aria_hint) { if check_ingredients_parses(unparsed.as_str(), error_text, aria_hint) {
debug!("Staples text is changed"); 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" } }) { "Save" }
} }

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use js_sys::Date;
use wasm_bindgen::JsValue; use wasm_bindgen::JsValue;
use web_sys::{window, Storage}; use web_sys::{window, Storage};
@ -28,3 +29,7 @@ pub fn get_storage() -> Storage {
.expect("Failed to get storage") .expect("Failed to get storage")
.expect("No storage available") .expect("No storage available")
} }
pub fn get_ms_timestamp() -> u32 {
Date::new_0().get_milliseconds()
}