mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
New UI for ingredient category management
This commit is contained in:
parent
44e8c0f727
commit
66a558d1e6
@ -112,6 +112,16 @@
|
|||||||
},
|
},
|
||||||
"query": "select plan_date as \"plan_date: NaiveDate\", recipe_id, count\nfrom plan_recipes\nwhere\n user_id = ?\n and date(plan_date) > ?\norder by user_id, plan_date"
|
"query": "select plan_date as \"plan_date: NaiveDate\", recipe_id, count\nfrom plan_recipes\nwhere\n user_id = ?\n and date(plan_date) > ?\norder by user_id, plan_date"
|
||||||
},
|
},
|
||||||
|
"2582522f8ca9f12eccc70a3b339d9030aee0f52e62d6674cfd3862de2a68a177": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "insert into category_mappings\n (user_id, ingredient_name, category_name)\n values (?, ?, ?)\n on conflict (user_id, ingredient_name)\n do update set category_name=excluded.category_name\n"
|
||||||
|
},
|
||||||
"37f382be1b53efd2f79a0d59ae6a8717f88a86908a7a4128d5ed7339147ca59d": {
|
"37f382be1b53efd2f79a0d59ae6a8717f88a86908a7a4128d5ed7339147ca59d": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
@ -342,16 +352,6 @@
|
|||||||
},
|
},
|
||||||
"query": "select category_text from categories where user_id = ?"
|
"query": "select category_text from categories where user_id = ?"
|
||||||
},
|
},
|
||||||
"d73e4bfb1fbee6d2dd35fc787141a1c2909a77cf4b19950671f87e694289c242": {
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"nullable": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"query": "insert into category_mappings\n (user_id, ingredient_name, category_name)\n values (?, ?, ?)"
|
|
||||||
},
|
|
||||||
"d84685a82585c5e4ae72c86ba1fe6e4a7241c4c3c9e948213e5849d956132bad": {
|
"d84685a82585c5e4ae72c86ba1fe6e4a7241c4c3c9e948213e5849d956132bad": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
insert into category_mappings
|
insert into category_mappings
|
||||||
(user_id, ingredient_name, category_name)
|
(user_id, ingredient_name, category_name)
|
||||||
values (?, ?, ?)
|
values (?, ?, ?)
|
||||||
|
on conflict (user_id, ingredient_name)
|
||||||
|
do update set category_name=excluded.category_name
|
||||||
|
@ -231,17 +231,15 @@ pub struct Ingredient {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub form: Option<String>,
|
pub form: Option<String>,
|
||||||
pub amt: Measure,
|
pub amt: Measure,
|
||||||
pub category: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ingredient {
|
impl Ingredient {
|
||||||
pub fn new<S: Into<String>>(name: S, form: Option<String>, amt: Measure, category: S) -> Self {
|
pub fn new<S: Into<String>>(name: S, form: Option<String>, amt: Measure) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: None,
|
id: None,
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
form,
|
form,
|
||||||
amt,
|
amt,
|
||||||
category: category.into(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,14 +248,12 @@ impl Ingredient {
|
|||||||
name: S,
|
name: S,
|
||||||
form: Option<String>,
|
form: Option<String>,
|
||||||
amt: Measure,
|
amt: Measure,
|
||||||
category: S,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: Some(id),
|
id: Some(id),
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
form,
|
form,
|
||||||
amt,
|
amt,
|
||||||
category: category.into(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,7 +447,7 @@ make_fn!(
|
|||||||
name => ingredient_name,
|
name => ingredient_name,
|
||||||
modifier => optional!(ingredient_modifier),
|
modifier => optional!(ingredient_modifier),
|
||||||
_ => optional!(ws),
|
_ => optional!(ws),
|
||||||
(Ingredient::new(name, modifier.map(|s| s.to_owned()), measure, String::new()))
|
(Ingredient::new(name, modifier.map(|s| s.to_owned()), measure))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -89,108 +89,80 @@ fn test_volume_normalize() {
|
|||||||
fn test_ingredient_display() {
|
fn test_ingredient_display() {
|
||||||
let cases = vec![
|
let cases = vec![
|
||||||
(
|
(
|
||||||
Ingredient::new(
|
Ingredient::new("onion", Some("chopped".to_owned()), Measure::cup(1.into())),
|
||||||
"onion",
|
|
||||||
Some("chopped".to_owned()),
|
|
||||||
Measure::cup(1.into()),
|
|
||||||
"Produce",
|
|
||||||
),
|
|
||||||
"1 cup onion (chopped)",
|
"1 cup onion (chopped)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new(
|
Ingredient::new("onion", Some("chopped".to_owned()), Measure::cup(2.into())),
|
||||||
"onion",
|
|
||||||
Some("chopped".to_owned()),
|
|
||||||
Measure::cup(2.into()),
|
|
||||||
"Produce",
|
|
||||||
),
|
|
||||||
"2 cups onion (chopped)",
|
"2 cups onion (chopped)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new(
|
Ingredient::new("onion", Some("chopped".to_owned()), Measure::tbsp(1.into())),
|
||||||
"onion",
|
|
||||||
Some("chopped".to_owned()),
|
|
||||||
Measure::tbsp(1.into()),
|
|
||||||
"Produce",
|
|
||||||
),
|
|
||||||
"1 tbsp onion (chopped)",
|
"1 tbsp onion (chopped)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new(
|
Ingredient::new("onion", Some("chopped".to_owned()), Measure::tbsp(2.into())),
|
||||||
"onion",
|
|
||||||
Some("chopped".to_owned()),
|
|
||||||
Measure::tbsp(2.into()),
|
|
||||||
"Produce",
|
|
||||||
),
|
|
||||||
"2 tbsps onion (chopped)",
|
"2 tbsps onion (chopped)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new("soy sauce", None, Measure::floz(1.into()), "Produce"),
|
Ingredient::new("soy sauce", None, Measure::floz(1.into())),
|
||||||
"1 floz soy sauce",
|
"1 floz soy sauce",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new("soy sauce", None, Measure::floz(2.into()), "Produce"),
|
Ingredient::new("soy sauce", None, Measure::floz(2.into())),
|
||||||
"2 floz soy sauce",
|
"2 floz soy sauce",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new("soy sauce", None, Measure::qrt(1.into()), "Produce"),
|
Ingredient::new("soy sauce", None, Measure::qrt(1.into())),
|
||||||
"1 qrt soy sauce",
|
"1 qrt soy sauce",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new("soy sauce", None, Measure::qrt(2.into()), "Produce"),
|
Ingredient::new("soy sauce", None, Measure::qrt(2.into())),
|
||||||
"2 qrts soy sauce",
|
"2 qrts soy sauce",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new("soy sauce", None, Measure::pint(1.into()), "Produce"),
|
Ingredient::new("soy sauce", None, Measure::pint(1.into())),
|
||||||
"1 pint soy sauce",
|
"1 pint soy sauce",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new("soy sauce", None, Measure::pint(2.into()), "Produce"),
|
Ingredient::new("soy sauce", None, Measure::pint(2.into())),
|
||||||
"2 pints soy sauce",
|
"2 pints soy sauce",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new("soy sauce", None, Measure::gal(1.into()), "Produce"),
|
Ingredient::new("soy sauce", None, Measure::gal(1.into())),
|
||||||
"1 gal soy sauce",
|
"1 gal soy sauce",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new("soy sauce", None, Measure::gal(2.into()), "Produce"),
|
Ingredient::new("soy sauce", None, Measure::gal(2.into())),
|
||||||
"2 gals soy sauce",
|
"2 gals soy sauce",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new("soy sauce", None, Measure::ml(1.into()), "Produce"),
|
Ingredient::new("soy sauce", None, Measure::ml(1.into())),
|
||||||
"1 ml soy sauce",
|
"1 ml soy sauce",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new("soy sauce", None, Measure::ml(2.into()), "Produce"),
|
Ingredient::new("soy sauce", None, Measure::ml(2.into())),
|
||||||
"2 ml soy sauce",
|
"2 ml soy sauce",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new("soy sauce", None, Measure::ltr(1.into()), "Produce"),
|
Ingredient::new("soy sauce", None, Measure::ltr(1.into())),
|
||||||
"1 ltr soy sauce",
|
"1 ltr soy sauce",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new("soy sauce", None, Measure::ltr(2.into()), "Produce"),
|
Ingredient::new("soy sauce", None, Measure::ltr(2.into())),
|
||||||
"2 ltr soy sauce",
|
"2 ltr soy sauce",
|
||||||
),
|
),
|
||||||
|
(Ingredient::new("apple", None, Measure::count(1)), "1 apple"),
|
||||||
(
|
(
|
||||||
Ingredient::new("apple", None, Measure::count(1), "Produce"),
|
Ingredient::new("salt", None, Measure::gram(1.into())),
|
||||||
"1 apple",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Ingredient::new("salt", None, Measure::gram(1.into()), "Produce"),
|
|
||||||
"1 gram salt",
|
"1 gram salt",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new("salt", None, Measure::gram(2.into()), "Produce"),
|
Ingredient::new("salt", None, Measure::gram(2.into())),
|
||||||
"2 grams salt",
|
"2 grams salt",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new(
|
Ingredient::new("onion", Some("minced".to_owned()), Measure::cup(1.into())),
|
||||||
"onion",
|
|
||||||
Some("minced".to_owned()),
|
|
||||||
Measure::cup(1.into()),
|
|
||||||
"Produce",
|
|
||||||
),
|
|
||||||
"1 cup onion (minced)",
|
"1 cup onion (minced)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@ -198,7 +170,6 @@ fn test_ingredient_display() {
|
|||||||
"pepper",
|
"pepper",
|
||||||
Some("ground".to_owned()),
|
Some("ground".to_owned()),
|
||||||
Measure::tsp(Ratio::new(1, 2).into()),
|
Measure::tsp(Ratio::new(1, 2).into()),
|
||||||
"Produce",
|
|
||||||
),
|
),
|
||||||
"1/2 tsp pepper (ground)",
|
"1/2 tsp pepper (ground)",
|
||||||
),
|
),
|
||||||
@ -207,35 +178,19 @@ fn test_ingredient_display() {
|
|||||||
"pepper",
|
"pepper",
|
||||||
Some("ground".to_owned()),
|
Some("ground".to_owned()),
|
||||||
Measure::tsp(Ratio::new(3, 2).into()),
|
Measure::tsp(Ratio::new(3, 2).into()),
|
||||||
"Produce",
|
|
||||||
),
|
),
|
||||||
"1 1/2 tsps pepper (ground)",
|
"1 1/2 tsps pepper (ground)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new(
|
Ingredient::new("apple", Some("sliced".to_owned()), Measure::count(1)),
|
||||||
"apple",
|
|
||||||
Some("sliced".to_owned()),
|
|
||||||
Measure::count(1),
|
|
||||||
"Produce",
|
|
||||||
),
|
|
||||||
"1 apple (sliced)",
|
"1 apple (sliced)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new(
|
Ingredient::new("potato", Some("mashed".to_owned()), Measure::count(1)),
|
||||||
"potato",
|
|
||||||
Some("mashed".to_owned()),
|
|
||||||
Measure::count(1),
|
|
||||||
"Produce",
|
|
||||||
),
|
|
||||||
"1 potato (mashed)",
|
"1 potato (mashed)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Ingredient::new(
|
Ingredient::new("potato", Some("blanched".to_owned()), Measure::count(1)),
|
||||||
"potato",
|
|
||||||
Some("blanched".to_owned()),
|
|
||||||
Measure::count(1),
|
|
||||||
"Produce",
|
|
||||||
),
|
|
||||||
"1 potato (blanched)",
|
"1 potato (blanched)",
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@ -312,7 +267,6 @@ fn test_ingredient_parse() {
|
|||||||
"green bell pepper",
|
"green bell pepper",
|
||||||
Some("chopped".to_owned()),
|
Some("chopped".to_owned()),
|
||||||
Count(Quantity::Whole(1)),
|
Count(Quantity::Whole(1)),
|
||||||
"",
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
@ -332,18 +286,16 @@ fn test_ingredient_list_parse() {
|
|||||||
"flour",
|
"flour",
|
||||||
None,
|
None,
|
||||||
Volume(Cup(Quantity::Whole(1))),
|
Volume(Cup(Quantity::Whole(1))),
|
||||||
"",
|
|
||||||
)],
|
)],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"1 cup flour \n1/2 tsp butter ",
|
"1 cup flour \n1/2 tsp butter ",
|
||||||
vec![
|
vec![
|
||||||
Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""),
|
Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1)))),
|
||||||
Ingredient::new(
|
Ingredient::new(
|
||||||
"butter",
|
"butter",
|
||||||
None,
|
None,
|
||||||
Volume(Tsp(Quantity::Frac(Ratio::new(1, 2)))),
|
Volume(Tsp(Quantity::Frac(Ratio::new(1, 2)))),
|
||||||
"",
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -26,35 +26,6 @@ use web_sys::Storage;
|
|||||||
|
|
||||||
use crate::{app_state::AppState, js_lib};
|
use crate::{app_state::AppState, js_lib};
|
||||||
|
|
||||||
// FIXME(jwall): We should be able to delete this now.
|
|
||||||
#[instrument]
|
|
||||||
fn filter_recipes(
|
|
||||||
recipe_entries: &Option<Vec<RecipeEntry>>,
|
|
||||||
) -> Result<(Option<Recipe>, Option<BTreeMap<String, Recipe>>), 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)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Error(String);
|
pub struct Error(String);
|
||||||
|
|
||||||
@ -104,6 +75,10 @@ fn recipe_key<S: std::fmt::Display>(id: S) -> String {
|
|||||||
format!("recipe:{}", id)
|
format!("recipe:{}", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn category_key<S: std::fmt::Display>(id: S) -> String {
|
||||||
|
format!("category:{}", id)
|
||||||
|
}
|
||||||
|
|
||||||
fn token68(user: String, pass: String) -> String {
|
fn token68(user: String, pass: String) -> String {
|
||||||
base64::encode(format!("{}:{}", user, pass))
|
base64::encode(format!("{}:{}", user, pass))
|
||||||
}
|
}
|
||||||
@ -145,22 +120,39 @@ impl LocalStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gets categories from local storage.
|
/// Gets categories from local storage.
|
||||||
pub fn get_categories(&self) -> Option<String> {
|
pub fn get_categories(&self) -> Option<Vec<(String, String)>> {
|
||||||
self.store
|
let mut mappings = Vec::new();
|
||||||
.get("categories")
|
for k in self.get_category_keys() {
|
||||||
.expect("Failed go get categories")
|
if let Some(mut cat_map) = self
|
||||||
|
.store
|
||||||
|
.get(&k)
|
||||||
|
.expect(&format!("Failed to get category key {}", k))
|
||||||
|
.map(|v| {
|
||||||
|
from_str::<Vec<(String, String)>>(&v)
|
||||||
|
.expect(&format!("Failed to parse category key {}", k))
|
||||||
|
})
|
||||||
|
{
|
||||||
|
mappings.extend(cat_map.drain(0..));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mappings.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(mappings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the categories to the given string.
|
/// Set the categories to the given string.
|
||||||
pub fn set_categories(&self, categories: Option<&String>) {
|
pub fn set_categories(&self, mappings: Option<&Vec<(String, String)>>) {
|
||||||
if let Some(c) = categories {
|
if let Some(mappings) = mappings {
|
||||||
self.store
|
for (i, cat) in mappings.iter() {
|
||||||
.set("categories", c)
|
self.store
|
||||||
.expect("Failed to set categories");
|
.set(
|
||||||
} else {
|
&category_key(i),
|
||||||
self.store
|
&to_string(&(i, cat)).expect("Failed to serialize category mapping"),
|
||||||
.delete("categories")
|
)
|
||||||
.expect("Failed to delete categories")
|
.expect("Failed to store category mapping");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +166,12 @@ impl LocalStore {
|
|||||||
keys
|
keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_category_keys(&self) -> impl Iterator<Item = String> {
|
||||||
|
self.get_storage_keys()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|k| k.starts_with("category:"))
|
||||||
|
}
|
||||||
|
|
||||||
fn get_recipe_keys(&self) -> impl Iterator<Item = String> {
|
fn get_recipe_keys(&self) -> impl Iterator<Item = String> {
|
||||||
self.get_storage_keys()
|
self.get_storage_keys()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -392,10 +390,11 @@ impl HttpStore {
|
|||||||
}
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#[instrument]
|
//#[instrument]
|
||||||
pub async fn fetch_categories(&self) -> Result<Option<String>, Error> {
|
pub async fn fetch_categories(&self) -> Result<Option<Vec<(String, String)>>, Error> {
|
||||||
let mut path = self.v2_path();
|
let mut path = self.v2_path();
|
||||||
path.push_str("/categories");
|
path.push_str("/category_map");
|
||||||
let resp = match reqwasm::http::Request::get(&path).send().await {
|
let resp = match reqwasm::http::Request::get(&path).send().await {
|
||||||
Ok(resp) => resp,
|
Ok(resp) => resp,
|
||||||
Err(reqwasm::Error::JsError(err)) => {
|
Err(reqwasm::Error::JsError(err)) => {
|
||||||
@ -413,7 +412,11 @@ impl HttpStore {
|
|||||||
Err(format!("Status: {}", resp.status()).into())
|
Err(format!("Status: {}", resp.status()).into())
|
||||||
} else {
|
} else {
|
||||||
debug!("We got a valid response back!");
|
debug!("We got a valid response back!");
|
||||||
let resp = resp.json::<CategoryResponse>().await?.as_success().unwrap();
|
let resp = resp
|
||||||
|
.json::<CategoryMappingResponse>()
|
||||||
|
.await?
|
||||||
|
.as_success()
|
||||||
|
.unwrap();
|
||||||
Ok(Some(resp))
|
Ok(Some(resp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -506,9 +509,9 @@ impl HttpStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(categories))]
|
#[instrument(skip(categories))]
|
||||||
pub async fn store_categories(&self, categories: String) -> Result<(), Error> {
|
pub async fn store_categories(&self, categories: &Vec<(String, String)>) -> Result<(), Error> {
|
||||||
let mut path = self.v2_path();
|
let mut path = self.v2_path();
|
||||||
path.push_str("/categories");
|
path.push_str("/category_map");
|
||||||
let resp = reqwasm::http::Request::post(&path)
|
let resp = reqwasm::http::Request::post(&path)
|
||||||
.body(to_string(&categories).expect("Unable to encode categories as json"))
|
.body(to_string(&categories).expect("Unable to encode categories as json"))
|
||||||
.header("content-type", "application/json")
|
.header("content-type", "application/json")
|
||||||
|
@ -32,7 +32,7 @@ pub struct AppState {
|
|||||||
pub extras: Vec<(String, String)>,
|
pub extras: Vec<(String, String)>,
|
||||||
pub staples: Option<Recipe>,
|
pub staples: Option<Recipe>,
|
||||||
pub recipes: BTreeMap<String, Recipe>,
|
pub recipes: BTreeMap<String, Recipe>,
|
||||||
pub category_map: String,
|
pub category_map: BTreeMap<String, String>,
|
||||||
pub filtered_ingredients: BTreeSet<IngredientKey>,
|
pub filtered_ingredients: BTreeSet<IngredientKey>,
|
||||||
pub modified_amts: BTreeMap<IngredientKey, String>,
|
pub modified_amts: BTreeMap<IngredientKey, String>,
|
||||||
pub auth: Option<UserData>,
|
pub auth: Option<UserData>,
|
||||||
@ -45,7 +45,7 @@ impl AppState {
|
|||||||
extras: Vec::new(),
|
extras: Vec::new(),
|
||||||
staples: None,
|
staples: None,
|
||||||
recipes: BTreeMap::new(),
|
recipes: BTreeMap::new(),
|
||||||
category_map: String::new(),
|
category_map: BTreeMap::new(),
|
||||||
filtered_ingredients: BTreeSet::new(),
|
filtered_ingredients: BTreeSet::new(),
|
||||||
modified_amts: BTreeMap::new(),
|
modified_amts: BTreeMap::new(),
|
||||||
auth: None,
|
auth: None,
|
||||||
@ -61,7 +61,8 @@ pub enum Message {
|
|||||||
UpdateExtra(usize, String, String),
|
UpdateExtra(usize, String, String),
|
||||||
SaveRecipe(RecipeEntry),
|
SaveRecipe(RecipeEntry),
|
||||||
SetRecipe(String, Recipe),
|
SetRecipe(String, Recipe),
|
||||||
SetCategoryMap(String),
|
SetCategoryMap(BTreeMap<String, String>),
|
||||||
|
UpdateCategory(String, String),
|
||||||
ResetInventory,
|
ResetInventory,
|
||||||
AddFilteredIngredient(IngredientKey),
|
AddFilteredIngredient(IngredientKey),
|
||||||
UpdateAmt(IngredientKey, String),
|
UpdateAmt(IngredientKey, String),
|
||||||
@ -94,6 +95,9 @@ impl Debug for Message {
|
|||||||
f.debug_tuple("SetRecipe").field(arg0).field(arg1).finish()
|
f.debug_tuple("SetRecipe").field(arg0).field(arg1).finish()
|
||||||
}
|
}
|
||||||
Self::SetCategoryMap(arg0) => f.debug_tuple("SetCategoryMap").field(arg0).finish(),
|
Self::SetCategoryMap(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"),
|
Self::ResetInventory => write!(f, "ResetInventory"),
|
||||||
Self::AddFilteredIngredient(arg0) => {
|
Self::AddFilteredIngredient(arg0) => {
|
||||||
f.debug_tuple("AddFilteredIngredient").field(arg0).finish()
|
f.debug_tuple("AddFilteredIngredient").field(arg0).finish()
|
||||||
@ -197,10 +201,11 @@ impl StateMachine {
|
|||||||
}
|
}
|
||||||
info!("Synchronizing categories");
|
info!("Synchronizing categories");
|
||||||
match store.fetch_categories().await {
|
match store.fetch_categories().await {
|
||||||
Ok(Some(categories_content)) => {
|
Ok(Some(mut categories_content)) => {
|
||||||
debug!(categories=?categories_content);
|
debug!(categories=?categories_content);
|
||||||
local_store.set_categories(Some(&categories_content));
|
local_store.set_categories(Some(&categories_content));
|
||||||
state.category_map = categories_content;
|
let category_map = BTreeMap::from_iter(categories_content.drain(0..));
|
||||||
|
state.category_map = category_map;
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
warn!("There is no category file");
|
warn!("There is no category file");
|
||||||
@ -308,12 +313,26 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Message::SetCategoryMap(category_text) => {
|
Message::SetCategoryMap(map) => {
|
||||||
original_copy.category_map = category_text.clone();
|
original_copy.category_map = map.clone();
|
||||||
self.local_store.set_categories(Some(&category_text));
|
let list = map.iter().map(|(i, c)| (i.clone(), c.clone())).collect();
|
||||||
|
self.local_store.set_categories(Some(&list));
|
||||||
let store = self.store.clone();
|
let store = self.store.clone();
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
if let Err(e) = store.store_categories(category_text).await {
|
if let Err(e) = store.store_categories(&list).await {
|
||||||
|
error!(?e, "Failed to save categories");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Message::UpdateCategory(ingredient, category) => {
|
||||||
|
self.local_store
|
||||||
|
.set_categories(Some(&vec![(ingredient.clone(), category.clone())]));
|
||||||
|
original_copy
|
||||||
|
.category_map
|
||||||
|
.insert(ingredient.clone(), category.clone());
|
||||||
|
let store = self.store.clone();
|
||||||
|
spawn_local_scoped(cx, async move {
|
||||||
|
if let Err(e) = store.store_categories(&vec![(ingredient, category)]).await {
|
||||||
error!(?e, "Failed to save categories");
|
error!(?e, "Failed to save categories");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -11,91 +11,154 @@
|
|||||||
// 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 crate::{
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
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::app_state::{Message, StateHandler};
|
||||||
|
use sycamore::prelude::*;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
fn get_error_dialog() -> HtmlDialogElement {
|
#[derive(Props)]
|
||||||
get_element_by_id::<HtmlDialogElement>("error-dialog")
|
struct CategoryRowProps<'ctx> {
|
||||||
.expect("error-dialog isn't an html dialog element!")
|
sh: StateHandler<'ctx>,
|
||||||
.expect("No error-dialog element present")
|
ingredient: String,
|
||||||
|
category: String,
|
||||||
|
ingredient_recipe_map: &'ctx ReadSignal<BTreeMap<String, BTreeSet<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_category_text_parses(unparsed: &str, error_text: &Signal<String>) -> bool {
|
#[instrument(skip_all)]
|
||||||
let el = get_error_dialog();
|
#[component]
|
||||||
if let Err(e) = parse::as_categories(unparsed) {
|
fn CategoryRow<'ctx, G: Html>(cx: Scope<'ctx>, props: CategoryRowProps<'ctx>) -> View<G> {
|
||||||
error!(?e, "Error parsing categories");
|
let CategoryRowProps {
|
||||||
error_text.set(e);
|
sh,
|
||||||
el.show();
|
ingredient,
|
||||||
false
|
category,
|
||||||
} else {
|
ingredient_recipe_map,
|
||||||
el.close();
|
} = props;
|
||||||
true
|
let category = create_signal(cx, category);
|
||||||
|
let ingredient_clone = ingredient.clone();
|
||||||
|
let ingredient_clone2 = ingredient.clone();
|
||||||
|
let recipes = create_memo(cx, move || {
|
||||||
|
ingredient_recipe_map
|
||||||
|
.get()
|
||||||
|
.get(&ingredient_clone2)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| BTreeSet::new())
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
});
|
||||||
|
view! {cx,
|
||||||
|
tr() {
|
||||||
|
td() {
|
||||||
|
(ingredient_clone) br()
|
||||||
|
Indexed(
|
||||||
|
iterable=recipes,
|
||||||
|
view=|cx, r| {
|
||||||
|
let recipe_name = r.clone();
|
||||||
|
view!{cx,
|
||||||
|
a(href=format!("/ui/recipe/edit/{}", r)) { (recipe_name) } br()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Categories<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> {
|
pub fn Categories<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> {
|
||||||
let error_text = create_signal(cx, String::new());
|
let category_list = sh.get_selector(cx, |state| {
|
||||||
let category_text: &Signal<String> = create_signal(cx, String::new());
|
let mut categories = state
|
||||||
let dirty = create_signal(cx, false);
|
.get()
|
||||||
|
.category_map
|
||||||
spawn_local_scoped(cx, {
|
.iter()
|
||||||
let store = crate::api::HttpStore::get_from_context(cx);
|
.map(|(_, v)| v.clone())
|
||||||
async move {
|
.collect::<Vec<String>>();
|
||||||
if let Some(js) = store
|
categories.sort();
|
||||||
.fetch_categories()
|
categories.dedup();
|
||||||
.await
|
categories
|
||||||
.expect("Failed to get categories.")
|
|
||||||
{
|
|
||||||
category_text.set(js);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let dialog_view = view! {cx,
|
let ingredient_recipe_map = sh.get_selector(cx, |state| {
|
||||||
dialog(id="error-dialog") {
|
let state = state.get();
|
||||||
article{
|
let mut ingredients: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
|
||||||
header {
|
for (recipe_id, r) in state.recipes.iter() {
|
||||||
a(href="#", on:click=|_| {
|
for (_, i) in r.get_ingredients().iter() {
|
||||||
let el = get_error_dialog();
|
let ingredient_name = i.name.clone();
|
||||||
el.close();
|
ingredients
|
||||||
}, class="close")
|
.entry(ingredient_name)
|
||||||
"Invalid Categories"
|
.or_insert(BTreeSet::new())
|
||||||
}
|
.insert(recipe_id.clone());
|
||||||
p {
|
|
||||||
(error_text.get().clone())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
if let Some(staples) = &state.staples {
|
||||||
|
for (_, i) in staples.get_ingredients().iter() {
|
||||||
|
let ingredient_name = i.name.clone();
|
||||||
|
ingredients
|
||||||
|
.entry(ingredient_name)
|
||||||
|
.or_insert(BTreeSet::new())
|
||||||
|
.insert("Staples".to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ingredients
|
||||||
|
});
|
||||||
|
|
||||||
|
let rows = sh.get_selector(cx, |state| {
|
||||||
|
let state = state.get();
|
||||||
|
let category_map = state.category_map.clone();
|
||||||
|
let mut ingredients = BTreeSet::new();
|
||||||
|
for (_, r) in state.recipes.iter() {
|
||||||
|
for (_, i) in r.get_ingredients().iter() {
|
||||||
|
ingredients.insert(i.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(staples) = &state.staples {
|
||||||
|
for (_, i) in staples.get_ingredients().iter() {
|
||||||
|
ingredients.insert(i.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut mapping_list = Vec::new();
|
||||||
|
for i in ingredients.iter() {
|
||||||
|
let cat = category_map
|
||||||
|
.get(i)
|
||||||
|
.map(|v| v.clone())
|
||||||
|
.unwrap_or_else(|| "None".to_owned());
|
||||||
|
mapping_list.push((i.clone(), cat));
|
||||||
|
}
|
||||||
|
mapping_list.sort_by(|tpl1, tpl2| tpl1.1.cmp(&tpl2.1));
|
||||||
|
mapping_list
|
||||||
|
});
|
||||||
view! {cx,
|
view! {cx,
|
||||||
(dialog_view)
|
table() {
|
||||||
textarea(bind:value=category_text, rows=20, on:change=move |_| {
|
tr {
|
||||||
dirty.set(true);
|
th { "Ingredient" }
|
||||||
})
|
th { "Category" }
|
||||||
span(role="button", on:click=move |_| {
|
|
||||||
check_category_text_parses(category_text.get().as_str(), error_text);
|
|
||||||
}) { "Check" } " "
|
|
||||||
span(role="button", on:click=move |_| {
|
|
||||||
if !*dirty.get() {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if check_category_text_parses(category_text.get().as_str(), error_text) {
|
Keyed(
|
||||||
debug!("triggering category save");
|
iterable=rows,
|
||||||
sh.dispatch(
|
view=move |cx, (i, c)| {
|
||||||
cx,
|
view! {cx, CategoryRow(sh=sh, ingredient=i, category=c, ingredient_recipe_map=ingredient_recipe_map)}
|
||||||
Message::SetCategoryMap(category_text.get_untracked().as_ref().clone()),
|
},
|
||||||
);
|
key=|(i, _)| i.clone()
|
||||||
}
|
)
|
||||||
}) { "Save" }
|
}
|
||||||
|
datalist(id="category_options") {
|
||||||
|
Keyed(
|
||||||
|
iterable=category_list,
|
||||||
|
view=move |cx, c| {
|
||||||
|
view!{cx,
|
||||||
|
option(value=c)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key=|c| c.clone(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ fn make_ingredients_rows<'ctx, G: Html>(
|
|||||||
debug!("Making ingredients rows");
|
debug!("Making ingredients rows");
|
||||||
let ingredients = sh.get_selector(cx, move |state| {
|
let ingredients = sh.get_selector(cx, move |state| {
|
||||||
let state = state.get();
|
let state = state.get();
|
||||||
|
let category_map = &state.category_map;
|
||||||
debug!("building ingredient list from state");
|
debug!("building ingredient list from state");
|
||||||
let mut acc = IngredientAccumulator::new();
|
let mut acc = IngredientAccumulator::new();
|
||||||
for (id, count) in state.recipe_counts.iter() {
|
for (id, count) in state.recipe_counts.iter() {
|
||||||
@ -45,19 +46,24 @@ fn make_ingredients_rows<'ctx, G: Html>(
|
|||||||
acc.accumulate_from(staples);
|
acc.accumulate_from(staples);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
acc.ingredients()
|
let mut ingredients = acc
|
||||||
|
.ingredients()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
// First we filter out any filtered ingredients
|
// 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
|
// Then we take into account our modified amts
|
||||||
.map(|(k, (i, rs))| {
|
.map(|(k, (i, rs))| {
|
||||||
|
let category = category_map
|
||||||
|
.get(&i.name)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| String::new());
|
||||||
if state.modified_amts.contains_key(&k) {
|
if state.modified_amts.contains_key(&k) {
|
||||||
(
|
(
|
||||||
k.clone(),
|
k.clone(),
|
||||||
(
|
(
|
||||||
i.name,
|
i.name,
|
||||||
i.form,
|
i.form,
|
||||||
i.category,
|
category,
|
||||||
state.modified_amts.get(&k).unwrap().clone(),
|
state.modified_amts.get(&k).unwrap().clone(),
|
||||||
rs,
|
rs,
|
||||||
),
|
),
|
||||||
@ -68,7 +74,7 @@ fn make_ingredients_rows<'ctx, G: Html>(
|
|||||||
(
|
(
|
||||||
i.name,
|
i.name,
|
||||||
i.form,
|
i.form,
|
||||||
i.category,
|
category,
|
||||||
format!("{}", i.amt.normalize()),
|
format!("{}", i.amt.normalize()),
|
||||||
rs,
|
rs,
|
||||||
),
|
),
|
||||||
@ -78,7 +84,9 @@ fn make_ingredients_rows<'ctx, G: Html>(
|
|||||||
.collect::<Vec<(
|
.collect::<Vec<(
|
||||||
IngredientKey,
|
IngredientKey,
|
||||||
(String, Option<String>, String, String, BTreeSet<String>),
|
(String, Option<String>, String, String, BTreeSet<String>),
|
||||||
)>>()
|
)>>();
|
||||||
|
ingredients.sort_by(|tpl1, tpl2| (&tpl1.1 .2, &tpl1.1 .0).cmp(&(&tpl2.1 .2, &tpl2.1 .0)));
|
||||||
|
ingredients
|
||||||
});
|
});
|
||||||
view!(
|
view!(
|
||||||
cx,
|
cx,
|
||||||
|
@ -11,8 +11,8 @@
|
|||||||
// 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 wasm_bindgen::{JsCast, JsValue};
|
use wasm_bindgen::JsValue;
|
||||||
use web_sys::{window, Element, Storage};
|
use web_sys::{window, Storage};
|
||||||
|
|
||||||
pub fn navigate_to_path(path: &str) -> Result<(), JsValue> {
|
pub fn navigate_to_path(path: &str) -> Result<(), JsValue> {
|
||||||
window()
|
window()
|
||||||
@ -21,21 +21,6 @@ pub fn navigate_to_path(path: &str) -> Result<(), JsValue> {
|
|||||||
.set_pathname(path)
|
.set_pathname(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_element_by_id<E>(id: &str) -> Result<Option<E>, Element>
|
|
||||||
where
|
|
||||||
E: JsCast,
|
|
||||||
{
|
|
||||||
match window()
|
|
||||||
.expect("No window present")
|
|
||||||
.document()
|
|
||||||
.expect("No document in window")
|
|
||||||
.get_element_by_id(id)
|
|
||||||
{
|
|
||||||
Some(e) => e.dyn_into::<E>().map(|e| Some(e)),
|
|
||||||
None => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_storage() -> Storage {
|
pub fn get_storage() -> Storage {
|
||||||
window()
|
window()
|
||||||
.expect("No Window Present")
|
.expect("No Window Present")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user