From 481e44911fa7fb7df9b0db5724af0f4951901cf2 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Mon, 19 Sep 2022 17:19:29 -0400 Subject: [PATCH] Implement save categories functionality --- Cargo.lock | 68 +++------------- kitchen/Cargo.toml | 7 +- kitchen/src/web/mod.rs | 2 +- web/src/app_state.rs | 1 + web/src/components/categories.rs | 107 ++++++++++++++++++++++++++ web/src/components/mod.rs | 2 + web/src/components/recipe.rs | 1 + web/src/components/recipe_selector.rs | 5 +- web/src/components/tabs.rs | 2 + web/src/pages/categories.rs | 30 ++++++++ web/src/pages/mod.rs | 6 ++ web/src/pages/plan.rs | 6 +- web/src/router_integration.rs | 1 + web/src/service.rs | 17 ++-- web/src/web.rs | 3 + 15 files changed, 186 insertions(+), 72 deletions(-) create mode 100644 web/src/components/categories.rs create mode 100644 web/src/pages/categories.rs diff --git a/Cargo.lock b/Cargo.lock index 9badc59..d40288a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,24 +146,6 @@ dependencies = [ "event-listener", ] -[[package]] -name = "async-process" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02111fd8655a613c25069ea89fc8d9bb89331fa77486eb3bc059ee757cfa481c" -dependencies = [ - "async-io", - "autocfg", - "blocking", - "cfg-if 1.0.0", - "event-listener", - "futures-lite", - "libc", - "once_cell", - "signal-hook", - "winapi", -] - [[package]] name = "async-session" version = "3.0.0" @@ -195,7 +177,6 @@ dependencies = [ "async-global-executor", "async-io", "async-lock", - "async-process", "crossbeam-utils", "futures-channel", "futures-core", @@ -263,9 +244,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de18bc5f2e9df8f52da03856bf40e29b747de5a84e43aefff90e3dc4a21529b" +checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043" dependencies = [ "async-trait", "axum-core", @@ -307,9 +288,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4f44a0e6200e9d11a1cdc989e4b358f6e3d354fbf48478f345a17f4e43f8635" +checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b" dependencies = [ "async-trait", "bytes", @@ -317,6 +298,8 @@ dependencies = [ "http", "http-body", "mime", + "tower-layer", + "tower-service", ] [[package]] @@ -1894,25 +1877,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "signal-hook" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" -dependencies = [ - "libc", -] - [[package]] name = "slab" version = "0.4.7" @@ -1955,9 +1919,9 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.1.8" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b7922be017ee70900be125523f38bdd644f4f06a1b16e8fa5a8ee8c34bffd4" +checksum = "f87e292b4291f154971a43c3774364e2cbcaec599d3f5bf6fa9d122885dbc38a" dependencies = [ "itertools", "nom", @@ -1966,9 +1930,7 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788841def501aabde58d3666fcea11351ec3962e6ea75dbcd05c84a71d68bcd1" +version = "0.6.2" dependencies = [ "sqlx-core", "sqlx-macros", @@ -1976,9 +1938,7 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c21d3b5e7cadfe9ba7cdc1295f72cc556c750b4419c27c219c0693198901f8e" +version = "0.6.2" dependencies = [ "ahash", "atoi", @@ -2022,9 +1982,7 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4adfd2df3557bddd3b91377fc7893e8fa899e9b4061737cbade4e1bb85f1b45c" +version = "0.6.2" dependencies = [ "dotenvy", "either", @@ -2044,9 +2002,7 @@ dependencies = [ [[package]] name = "sqlx-rt" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be52fc7c96c136cedea840ed54f7d446ff31ad670c9dea95ebcb998530971a3" +version = "0.6.2" dependencies = [ "async-std", "futures-rustls", diff --git a/kitchen/Cargo.toml b/kitchen/Cargo.toml index 7d23b88..4fa88b0 100644 --- a/kitchen/Cargo.toml +++ b/kitchen/Cargo.toml @@ -41,7 +41,7 @@ version = "1.0.144" features = ["serde", "v4"] [dependencies.axum] -version = "0.5.15" +version = "0.5.16" features = ["headers", "http2"] [dependencies.clap] @@ -49,9 +49,10 @@ version = "3.2.16" features = [ "cargo" ] [dependencies.async-std] -version = "1.10.0" +version = "1.12.0" features = ["tokio1"] [dependencies.sqlx] -version = "0.6.1" +path = "../../sqlx" +#version = "0.6.2" features = ["sqlite", "runtime-async-std-rustls", "offline"] \ No newline at end of file diff --git a/kitchen/src/web/mod.rs b/kitchen/src/web/mod.rs index 30447b6..0c1392c 100644 --- a/kitchen/src/web/mod.rs +++ b/kitchen/src/web/mod.rs @@ -70,7 +70,7 @@ async fn ui_static_assets(Path(path): Path) -> impl IntoResponse { let mut path = path.trim_start_matches("/"); path = match path { - "" | "inventory" | "plan" | "cook" | "login" => "index.html", + "" | "inventory" | "plan" | "cook" | "categories" | "login" => "index.html", _ => { if path.starts_with("recipe") { "index.html" diff --git a/web/src/app_state.rs b/web/src/app_state.rs index e413519..458e165 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -18,6 +18,7 @@ pub enum AppRoutes { Inventory, Cook, Recipe(String), + Categories, Login, Error(String), NotFound, diff --git a/web/src/components/categories.rs b/web/src/components/categories.rs new file mode 100644 index 0000000..23587c7 --- /dev/null +++ b/web/src/components/categories.rs @@ -0,0 +1,107 @@ +// 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 serde_json::{from_str, to_string}; +use sycamore::{futures::spawn_local_in_scope, prelude::*}; +use tracing::{debug, error, instrument}; +use web_sys::HtmlDialogElement; + +use recipes::parse; + +use crate::{js_lib::get_element_by_id, service::get_appservice_from_context}; + +fn get_error_dialog() -> HtmlDialogElement { + get_element_by_id::("error-dialog") + .expect("error-dialog isn't an html dialog element!") + .unwrap() +} + +fn check_category_text_parses(unparsed: &str, error_text: Signal) -> bool { + let el = get_error_dialog(); + if let Err(e) = parse::as_categories(unparsed) { + error!(?e, "Error parsing categories"); + error_text.set(e); + el.show(); + false + } else { + el.close(); + true + } +} + +#[instrument] +#[component(Categories)] +pub fn categories() -> View { + let app_service = get_appservice_from_context(); + let save_signal = Signal::new(()); + let error_text = Signal::new(String::new()); + let category_text = Signal::new( + match app_service + .get_category_text() + .expect("Failed to get categories.") + { + Some(js) => from_str::(&js) + .map_err(|e| format!("{}", e)) + .expect("Failed to parse categories as json"), + None => String::new(), + }, //.unwrap_or_else(|| String::new()), + ); + + create_effect( + cloned!((app_service, category_text, save_signal, error_text) => move || { + // TODO(jwall): This is triggering on load which is not desired. + save_signal.get(); + spawn_local_in_scope({ + cloned!((app_service, category_text, error_text) => async move { + // TODO(jwall): Save the categories. + if let Err(e) = app_service.save_categories(category_text.get_untracked().as_ref().clone()).await { + error!(?e, "Failed to save categories"); + error_text.set(format!("{:?}", e)); + } + }) + }); + }), + ); + + let dialog_view = cloned!((error_text) => view! { + dialog(id="error-dialog") { + article{ + header { + a(href="#", on:click=|_| { + let el = get_error_dialog(); + el.close(); + }, class="close") + "Invalid Categories" + } + p { + (error_text.get().clone()) + } + } + } + }); + + cloned!((category_text, error_text) => view! { + (dialog_view) + textarea(bind:value=category_text.clone(), rows=20) + a(role="button", href="#", on:click=cloned!((category_text, error_text) => move |_| { + check_category_text_parses(category_text.get().as_str(), error_text.clone()); + })) { "Check" } " " + a(role="button", href="#", on:click=cloned!((category_text, error_text) => move |_| { + // TODO(jwall): check and then save the categories. + if check_category_text_parses(category_text.get().as_str(), error_text.clone()) { + debug!("triggering category save"); + save_signal.trigger_subscribers(); + } + })) { "Save" } + }) +} diff --git a/web/src/components/mod.rs b/web/src/components/mod.rs index a69eb0e..dd4b51b 100644 --- a/web/src/components/mod.rs +++ b/web/src/components/mod.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. +pub mod categories; pub mod header; pub mod recipe; pub mod recipe_list; @@ -19,6 +20,7 @@ pub mod recipe_selector; pub mod shopping_list; pub mod tabs; +pub use categories::*; pub use header::*; pub use recipe::*; pub use recipe_list::*; diff --git a/web/src/components/recipe.rs b/web/src/components/recipe.rs index 8453108..a112cbc 100644 --- a/web/src/components/recipe.rs +++ b/web/src/components/recipe.rs @@ -50,6 +50,7 @@ fn editor(recipe: RecipeEntry) -> View { create_effect( cloned!((id, app_service, text, save_signal, error_text) => move || { + // TODO(jwall): This is triggering on load which is not desired. save_signal.get(); spawn_local_in_scope({ cloned!((id, app_service, text, error_text) => async move { diff --git a/web/src/components/recipe_selector.rs b/web/src/components/recipe_selector.rs index 3242203..4148901 100644 --- a/web/src/components/recipe_selector.rs +++ b/web/src/components/recipe_selector.rs @@ -16,12 +16,11 @@ use sycamore::{futures::spawn_local_in_scope, prelude::*}; use tracing::{error, instrument}; use crate::components::recipe_selection::*; -use crate::service::get_appservice_from_context; +use crate::service::AppService; #[instrument] #[component(RecipeSelector)] -pub fn recipe_selector() -> View { - let app_service = get_appservice_from_context(); +pub fn recipe_selector(app_service: AppService) -> View { let rows = create_memo(cloned!(app_service => move || { let mut rows = Vec::new(); for row in app_service.get_recipes().get().iter().map(|(k, v)| (k.clone(), v.clone())).collect::)>>().chunks(4) { diff --git a/web/src/components/tabs.rs b/web/src/components/tabs.rs index 77b63c5..80aecf2 100644 --- a/web/src/components/tabs.rs +++ b/web/src/components/tabs.rs @@ -29,6 +29,8 @@ pub fn tabbed_view(state: TabState) -> View { li { a(href="/ui/inventory", class="no-print") { "Inventory" } " > " } li { a(href="/ui/cook", class="no-print") { "Cook" } + } " | " + li { a(href="/ui/categories", class="no-print") { "Categories" } } } ul { diff --git a/web/src/pages/categories.rs b/web/src/pages/categories.rs new file mode 100644 index 0000000..28084b9 --- /dev/null +++ b/web/src/pages/categories.rs @@ -0,0 +1,30 @@ +// Copyright 2022 Jeremy Wall (jeremy@marzhillstudios.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 crate::components::categories::*; +use crate::components::tabs::*; + +use sycamore::prelude::*; +use tracing::instrument; + +#[instrument] +#[component(CategoryPage)] +pub fn category_page() -> View { + view! { + TabbedView(TabState { + inner: view! { + Categories() + } + }) + } +} diff --git a/web/src/pages/mod.rs b/web/src/pages/mod.rs index 25cb012..5734b76 100644 --- a/web/src/pages/mod.rs +++ b/web/src/pages/mod.rs @@ -11,14 +11,20 @@ // 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. +mod categories; mod cook; mod inventory; mod login; mod plan; mod recipe; +pub use categories::*; pub use cook::*; pub use inventory::*; pub use login::*; pub use plan::*; pub use recipe::*; + +pub struct PageProps { + service: crate::service::AppService, +} diff --git a/web/src/pages/plan.rs b/web/src/pages/plan.rs index b4637cb..0780fe9 100644 --- a/web/src/pages/plan.rs +++ b/web/src/pages/plan.rs @@ -15,12 +15,14 @@ use crate::components::{recipe_selector::*, tabs::*}; use sycamore::prelude::*; +use super::PageProps; + #[component(PlanPage)] -pub fn plan_page() -> View { +pub fn plan_page(props: PageProps) -> View { view! { TabbedView(TabState { inner: view! { - RecipeSelector() + RecipeSelector(props.service.clone()) }, }) } diff --git a/web/src/router_integration.rs b/web/src/router_integration.rs index 6a5e54f..1f952b8 100644 --- a/web/src/router_integration.rs +++ b/web/src/router_integration.rs @@ -197,6 +197,7 @@ impl DeriveRoute for AppRoutes { "/ui/plan" => AppRoutes::Plan, "/ui/cook" => AppRoutes::Cook, "/ui/inventory" => AppRoutes::Inventory, + "/ui/categories" => AppRoutes::Categories, h => { if h.starts_with("/ui/recipe/") { let parts: Vec<&str> = h.split("/").collect(); diff --git a/web/src/service.rs b/web/src/service.rs index aff8d5e..7ac67fe 100644 --- a/web/src/service.rs +++ b/web/src/service.rs @@ -86,15 +86,18 @@ impl AppService { Ok(()) } + pub fn get_category_text(&self) -> Result, String> { + let storage = self.get_storage()?.unwrap(); + storage + .get_item("categories") + .map_err(|e| format!("{:?}", e)) + } + #[instrument(skip(self))] pub fn fetch_categories_from_storage( &self, ) -> Result>, String> { - let storage = self.get_storage()?.unwrap(); - match storage - .get_item("categories") - .map_err(|e| format!("{:?}", e))? - { + match self.get_category_text()? { Some(s) => { let parsed = from_str::(&s).map_err(|e| format!("{}", e))?; if parsed.is_empty() { @@ -224,7 +227,7 @@ impl AppService { .push((i.clone(), recipes.clone())); } debug!(?self.category_map); - // FIXM(jwall): Sort by categories and names. + // FIXME(jwall): Sort by categories and names. groups } @@ -369,7 +372,7 @@ impl HttpStore { #[instrument(skip(categories))] async fn save_categories(&self, categories: String) -> Result<(), Error> { let mut path = self.root.clone(); - path.push_str("/recipes"); + path.push_str("/categories"); let resp = reqwasm::http::Request::post(&path) .body(to_string(&categories).expect("Unable to encode categories as json")) .header("content-type", "application/json") diff --git a/web/src/web.rs b/web/src/web.rs index b128c6e..24306c9 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -47,6 +47,9 @@ fn route_switch(route: ReadSignal) -> View { AppRoutes::Recipe(idx) => view! { RecipePage(RecipePageProps { recipe: Signal::new(idx.clone()) }) }, + AppRoutes::Categories => view ! { + CategoryPage() + }, AppRoutes::NotFound => view! { // TODO(Create a real one) PlanPage()