Implement save categories functionality

This commit is contained in:
Jeremy Wall 2022-09-19 17:19:29 -04:00
parent 3094dee9f7
commit 481e44911f
15 changed files with 186 additions and 72 deletions

68
Cargo.lock generated
View File

@ -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",

View File

@ -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"]

View File

@ -70,7 +70,7 @@ async fn ui_static_assets(Path(path): Path<String>) -> 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"

View File

@ -18,6 +18,7 @@ pub enum AppRoutes {
Inventory,
Cook,
Recipe(String),
Categories,
Login,
Error(String),
NotFound,

View File

@ -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::<HtmlDialogElement>("error-dialog")
.expect("error-dialog isn't an html dialog element!")
.unwrap()
}
fn check_category_text_parses(unparsed: &str, error_text: Signal<String>) -> 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<G>)]
pub fn categories() -> View<G> {
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::<String>(&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" }
})
}

View File

@ -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::*;

View File

@ -50,6 +50,7 @@ fn editor(recipe: RecipeEntry) -> View<G> {
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 {

View File

@ -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<G>)]
pub fn recipe_selector() -> View<G> {
let app_service = get_appservice_from_context();
pub fn recipe_selector(app_service: AppService) -> View<G> {
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::<Vec<(String, Signal<Recipe>)>>().chunks(4) {

View File

@ -29,6 +29,8 @@ pub fn tabbed_view(state: TabState<G>) -> View<G> {
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 {

View File

@ -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<G>)]
pub fn category_page() -> View<G> {
view! {
TabbedView(TabState {
inner: view! {
Categories()
}
})
}
}

View File

@ -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,
}

View File

@ -15,12 +15,14 @@ use crate::components::{recipe_selector::*, tabs::*};
use sycamore::prelude::*;
use super::PageProps;
#[component(PlanPage<G>)]
pub fn plan_page() -> View<G> {
pub fn plan_page(props: PageProps) -> View<G> {
view! {
TabbedView(TabState {
inner: view! {
RecipeSelector()
RecipeSelector(props.service.clone())
},
})
}

View File

@ -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();

View File

@ -86,15 +86,18 @@ impl AppService {
Ok(())
}
pub fn get_category_text(&self) -> Result<Option<String>, 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<Option<BTreeMap<String, String>>, 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::<String>(&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")

View File

@ -47,6 +47,9 @@ fn route_switch<G: Html>(route: ReadSignal<AppRoutes>) -> View<G> {
AppRoutes::Recipe(idx) => view! {
RecipePage(RecipePageProps { recipe: Signal::new(idx.clone()) })
},
AppRoutes::Categories => view ! {
CategoryPage()
},
AppRoutes::NotFound => view! {
// TODO(Create a real one)
PlanPage()