Update to dioxus 0.4

This commit is contained in:
traxys 2023-08-05 12:54:49 +02:00
parent c36ce14b3b
commit 183f8a75d2
10 changed files with 328 additions and 586 deletions

View file

@ -10,10 +10,9 @@ ammonia = "3.3.0"
anyhow = "1.0.71"
api = { version = "0.1.0", path = "../api" }
console_log = { version = "1.0.0", features = ["color"] }
dioxus = "0.3.2"
dioxus-class = "0.3.0"
dioxus-router = { version = "0.3.0", features = ["web"] }
dioxus-web = "0.3.2"
dioxus = "0.4.0"
dioxus-router = { version = "0.4.1", features = ["web"] }
dioxus-web = "0.4.0"
gloo-net = { version = "0.3.0", features = ["json"] }
gloo-storage = "0.2.2"
gloo-utils = "0.1.7"

View file

@ -1,5 +1,4 @@
use dioxus::prelude::*;
use dioxus_class::prelude::*;
pub mod bs {
use wasm_bindgen::prelude::*;
@ -65,21 +64,30 @@ pub struct ModalProps<'a> {
}
pub fn Modal<'a>(cx: Scope<'a, ModalProps<'a>>) -> Element<'a> {
let mut classes = Class::from(vec!["modal"]);
let mut classes = vec!["modal"];
if cx.props.fade {
classes.append("fade");
classes.push("fade");
}
let mut dialog_class = Class::from(vec!["modal-dialog"]);
let classes = classes.join(" ");
let mut dialog_class = vec!["modal-dialog"];
if cx.props.centered {
dialog_class.append("modal-dialog-centered");
dialog_class.push("modal-dialog-centered");
}
let dialog_class = dialog_class.join(" ");
cx.render(rsx! {
div { class: classes, id: cx.props.id.as_str(), tabindex: "-1", "aria-labelledby": cx.props.labeled_by.as_deref(), "aria-hidden": "true",
div { class: dialog_class,
div {
class: "{classes}",
id: cx.props.id.as_str(),
tabindex: "-1",
"aria-labelledby": cx.props.labeled_by.as_deref(),
"aria-hidden": "true",
div { class: "{dialog_class}",
div { class: "modal-content", &cx.props.children }
}
}

View file

@ -6,11 +6,16 @@ use std::{
};
use dioxus::prelude::*;
use dioxus_router::use_router;
use dioxus_router::prelude::*;
use gloo_storage::{errors::StorageError, LocalStorage, Storage};
use uuid::Uuid;
use crate::{HouseholdInfo, LoginInfo, RedirectorProps};
use crate::{HouseholdInfo, LoginInfo, Route};
#[derive(Props)]
pub struct RedirectorProps<'a> {
children: Element<'a>,
}
pub struct RefreshHandle {
run: Box<dyn FnOnce()>,
@ -159,12 +164,12 @@ fn FullContextRedirectInner<'a>(cx: Scope<'a, RedirectorProps<'a>>) -> Element {
}
pub fn FullContextRedirect<'a>(cx: Scope<'a, RedirectorProps<'a>>) -> Element {
let router = use_router(cx);
let navigator = use_navigator(cx);
let check_token = match LocalStorage::get::<LoginInfo>("token") {
Ok(_) => true,
Err(StorageError::KeyNotFound(_)) => {
router.navigate_to("/login");
navigator.push(Route::Login);
false
}
Err(e) => unreachable!("Could not get token: {e:?}"),
@ -173,7 +178,7 @@ pub fn FullContextRedirect<'a>(cx: Scope<'a, RedirectorProps<'a>>) -> Element {
let check_household = match LocalStorage::get::<HouseholdInfo>("household") {
Ok(_) => true,
Err(StorageError::KeyNotFound(_)) => {
router.navigate_to("/household_selection");
navigator.push(Route::HouseholdSelection);
false
}
Err(e) => unreachable!("Could not get household: {e:?}"),

View file

@ -240,8 +240,8 @@ pub fn Ingredients(cx: Scope) -> Element {
let error = use_error(cx);
let add_ingredient = move |ev: FormEvent| {
let name = ev.values["newIgName"].to_string();
let unit = ev.values["newIgUnit"].to_string();
let name = ev.values["newIgName"][0].to_string();
let unit = ev.values["newIgUnit"][0].to_string();
if name.is_empty() && unit.is_empty() {
return;

View file

@ -6,16 +6,13 @@ use api::{
LoginResponse, UserInfo,
};
use dioxus::prelude::*;
use dioxus_router::{use_route, use_router, Redirect, Route, Router};
use dioxus_router::prelude::*;
use gloo_storage::{errors::StorageError, LocalStorage, Storage};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
bootstrap::{bs, FormModal, ModalToggleButton, Spinner},
sidebar::Page,
};
use crate::bootstrap::{bs, FormModal, ModalToggleButton, Spinner};
mod bootstrap;
mod ingredients;
@ -46,33 +43,6 @@ macro_rules! api {
}};
}
#[macro_export]
macro_rules! to_owned_props {
// Rule matching simple symbols without a path
($es:ident $(, $($rest:tt)*)?) => {
#[allow(unused_mut)]
let mut $es = $es.to_owned();
$( to_owned_props![$($rest)*] )?
};
// We need to find the last element in a path, for this we need to unstack the path part by
// part using, separating what we have with a '@'
($($deref:ident).+ $(, $($rest:tt)*)?) => {
to_owned_props![@ $($deref).+ $(, $($rest)*)?]
};
// Take the head of the path and add it to the list of $deref
($($deref:ident)* @ $head:ident $( . $tail:ident)+ $(, $($rest:tt)*)?) => {
to_owned_props![$($deref)* $head @ $($tail).+ $(, $($rest)*)?]
};
// We have exhausted the path, use the last as a name
($($deref:ident)* @ $last:ident $(, $($rest:tt)*)? ) => {
#[allow(unused_mut)]
let mut $last = $($deref .)* $last .to_owned();
$(to_owned_props![$($rest)*])?
};
}
#[derive(Props)]
pub struct ErrorProps<'a> {
error: &'a Option<String>,
@ -158,26 +128,28 @@ pub struct HouseholdInfo {
name: String,
}
#[derive(Props)]
pub struct RedirectorProps<'a> {
children: Element<'a>,
}
pub fn LoginRedirect<'a>(cx: Scope<'a, RedirectorProps<'a>>) -> Element {
let router = use_router(cx);
pub fn LoginRedirect(cx: Scope) -> Element {
let navigator = use_navigator(cx);
let token = match LocalStorage::get::<LoginInfo>("token") {
Ok(v) => v,
Ok(v) => Some(v),
Err(StorageError::KeyNotFound(_)) => {
router.navigate_to("/login");
return None;
}
Err(e) => unreachable!("Could not get token: {e:?}"),
};
use_shared_state_provider(cx, || token);
use_shared_state_provider(cx, || token.clone());
cx.render(rsx! {&cx.props.children})
cx.render(match token {
Some(_) => rsx! {
Outlet::<Route> {}
},
None => {
navigator.push(Route::Login);
rsx! {{}}
}
})
}
async fn do_login(username: String, password: String) -> anyhow::Result<()> {
@ -242,20 +214,20 @@ fn Openid(cx: Scope) -> Element {
fn Login(cx: Scope) -> Element {
let error = use_state(cx, || None::<String>);
let router = use_router(cx);
let navigator = use_navigator(cx);
let on_submit = move |e: Event<FormData>| {
to_owned![error, router];
to_owned![error, navigator];
cx.spawn(async move {
match do_login(
e.values["username"].to_string(),
e.values["password"].to_string(),
e.values["username"][0].to_string(),
e.values["password"][0].to_string(),
)
.await
{
Ok(_) => {
error.set(None);
router.navigate_to("/");
navigator.push(Route::Index);
}
Err(e) => {
error.set(Some(format!("Could not log in: {e}")));
@ -357,11 +329,11 @@ fn CreateHousehold(cx: Scope) -> Element {
let members = use_ref(cx, Vec::<(Uuid, String)>::new);
let router = use_router(cx);
let navigator = use_navigator(cx);
let token = login.read().token.clone();
let on_submit = move |_| {
to_owned![members, name, error, token, router];
to_owned![members, name, error, token, navigator];
cx.spawn(async move {
match do_new_household(token.clone(), name.to_string()).await {
@ -387,7 +359,7 @@ fn CreateHousehold(cx: Scope) -> Element {
let modal = bs::Modal::get_instance("#newHsModal");
modal.hide();
router.navigate_to("/");
navigator.push(Route::Index);
error.set(None);
}
Err(e) => {
@ -490,7 +462,7 @@ async fn fetch_households(token: String) -> anyhow::Result<api::Households> {
fn HouseholdListSelect(cx: Scope) -> Element {
let login = use_login(cx);
let households = use_future(cx, (), |_| fetch_households(login.read().token.clone()));
let router = use_router(cx);
let navigator = use_navigator(cx);
cx.render(match households.value() {
Some(Ok(response)) => {
@ -511,7 +483,7 @@ fn HouseholdListSelect(cx: Scope) -> Element {
return;
}
router.navigate_to("/");
navigator.push(Route::Index);
};
rsx! {button { key: "{id}", class: "btn btn-secondary m-1", onclick: onclick, "{info.name}" }}
});
@ -542,60 +514,69 @@ fn Index(cx: Scope) -> Element {
cx.render(rsx! {"INDEX"})
}
#[derive(Deserialize)]
#[derive(Deserialize, PartialEq, Clone)]
struct OidcQuery {
token: String,
username: String,
}
fn OidcRedirect(cx: Scope) -> Element {
let auth = use_route(cx).query::<OidcQuery>();
#[derive(PartialEq, Props)]
struct OidcProps {
token: String,
username: String,
}
cx.render(match auth {
None => rsx! {"No authentication query, internal error."},
Some(v) => {
match LocalStorage::set(
"token",
LoginInfo {
token: v.token,
name: v.username,
},
) {
Ok(_) => {
gloo_utils::window().location().replace("/").unwrap();
rsx! {{}}
}
Err(_) => rsx! {"Could not store authentication, try again."},
fn OidcRedirect(cx: Scope<OidcProps>) -> Element {
cx.render({
match LocalStorage::set(
"token",
LoginInfo {
token: cx.props.token.clone(),
name: cx.props.username.clone(),
},
) {
Ok(_) => {
gloo_utils::window().location().replace("/").unwrap();
rsx! {{}}
}
Err(_) => rsx! {"Could not store authentication, try again."},
}
})
}
use ingredients::Ingredients;
use recipe::{RecipeCreator, RecipeList, RecipeView};
#[rustfmt::skip]
#[derive(Clone, Routable)]
enum Route {
#[route("/login")]
Login,
#[route("/login/oidc?:token?:username")]
OidcRedirect { token: String, username: String },
#[layout(LoginRedirect)]
#[route("/household_selection")]
HouseholdSelection,
#[end_layout]
#[layout(RegaladeSidebar)]
#[route("/")]
Index,
#[route("/ingredients")]
Ingredients,
#[route("/recipe_creator")]
RecipeCreator,
#[nest("/recipe")]
#[route("/")]
RecipeList,
#[route("/:id")]
RecipeView {id: i64}
}
fn App(cx: Scope) -> Element {
cx.render(rsx! {
Router {
Route { to: Page::Home.to(),
RegaladeSidebar { current: Page::Home, Index {} }
}
Route { to: Page::Ingredients.to(),
RegaladeSidebar { current: Page::Ingredients, ingredients::Ingredients {} }
}
Route { to: Page::RecipeCreator.to(),
RegaladeSidebar { current: Page::RecipeCreator, recipe::RecipeCreator {} }
}
Route { to: Page::RecipeList.to(),
RegaladeSidebar { current: Page::RecipeList, recipe::RecipeList {} }
}
Route { to: "/recipe/:recipe_id",
RegaladeSidebar { current: Page::RecipeList, recipe::RecipeView {} }
}
Route { to: "/login", Login {} }
Route { to: "/login/oidc", OidcRedirect {} }
Route { to: "/household_selection",
LoginRedirect { HouseholdSelection {} }
}
Route { to: "", "Not found" }
}
Router::<Route> {}
})
}

View file

@ -2,7 +2,7 @@ use std::{marker::PhantomData, rc::Rc};
use api::{CreateRecipeRequest, CreateRecipeResponse, IngredientInfo};
use dioxus::prelude::*;
use dioxus_router::use_router;
use dioxus_router::prelude::*;
use uuid::Uuid;
use crate::{
@ -10,7 +10,7 @@ use crate::{
bootstrap::{bs, FormModal, ModalBody, ModalFooter, ModalToggleButton, TitledModal},
ingredients::do_add_ingredient,
recipe::IngredientSelect,
use_error, use_trimmed_context, ErrorView,
use_error, use_trimmed_context, ErrorView, Route,
};
use super::RecipeIngredient;
@ -211,7 +211,7 @@ pub fn RecipeCreator(cx: Scope) -> Element {
let steps = use_state(cx, String::new);
let router = use_router(cx);
let navigator = use_navigator(cx);
let ingredient_list: Vec<_> =
ingredients.with(|ig| {
@ -254,8 +254,9 @@ pub fn RecipeCreator(cx: Scope) -> Element {
person_count,
steps,
error,
router
navigator
];
cx.spawn(async move {
match do_create_recipe(
token,
@ -281,7 +282,7 @@ pub fn RecipeCreator(cx: Scope) -> Element {
name.set(Default::default());
error.set(Default::default());
router.navigate_to(&format!("/recipe/{id}"));
navigator.push(Route::RecipeView{id});
}
Err(e) => {
error.set(Some(format!("Error creating recipe: {e:?}")));

View file

@ -1,9 +1,11 @@
use dioxus::prelude::*;
use dioxus_router::Link;
use dioxus_router::prelude::*;
use itertools::Itertools;
use uuid::Uuid;
use crate::{api, bootstrap::Spinner, recipe::RecipeRating, use_trimmed_context, ErrorAlert};
use crate::{
api, bootstrap::Spinner, recipe::RecipeRating, use_trimmed_context, ErrorAlert, Route,
};
async fn get_all_recipes(
token: String,
@ -37,7 +39,7 @@ pub fn RecipeList(cx: Scope) -> Element {
div { key: "{id}", class: "col",
div { class: "p-3 border rounded border-light-subtle h-100",
Link {
to: "/recipe/{id}",
to: Route::RecipeView {id: *id},
class: "link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover",
"{name}"
RecipeRating { rating: *rating }

View file

@ -5,15 +5,14 @@ use api::{
RecipeInfo, RecipeIngredientEditRequest, RecipeRenameRequest,
};
use dioxus::prelude::*;
use dioxus_router::{use_route, use_router};
use pulldown_cmark::{Parser, html};
use pulldown_cmark::{html, Parser};
use uuid::Uuid;
use crate::{
api,
bootstrap::{bs, ConfirmDangerModal, FormModal, ModalToggleButton, Spinner},
recipe::{IngredientSelect, RecipeRating},
to_owned_props, use_error, use_refresh, use_trimmed_context, Callback, ErrorAlert, ErrorView,
use_error, use_refresh, use_trimmed_context, Callback, ErrorAlert, ErrorView,
};
async fn do_rename_recipe(
@ -53,7 +52,7 @@ fn EditName(cx: Scope<EditNameProps>) -> Element {
error.set(Some("Name can't be empty".into()));
}
to_owned_props![name, error, token, cx.props.refresh, cx.props.recipe];
to_owned![name, error, token, cx.props.refresh, cx.props.recipe];
cx.spawn(async move {
match do_rename_recipe(token, household, recipe, name.to_string()).await {
@ -136,7 +135,7 @@ fn EditRating(cx: Scope<EditRatingProps>) -> Element {
}
};
to_owned_props![error, token, cx.props.refresh, cx.props.recipe];
to_owned![error, token, cx.props.refresh, cx.props.recipe];
cx.spawn(async move {
match do_edit_rating(token, household, recipe, rating - 1).await {
@ -226,7 +225,7 @@ fn EditPersonCount(cx: Scope<EditPersonCountProps>) -> Element {
}
};
to_owned_props![error, token, cx.props.refresh, cx.props.recipe];
to_owned![error, token, cx.props.refresh, cx.props.recipe];
cx.spawn(async move {
match do_edit_person_count(token, household, recipe, person_count).await {
@ -325,7 +324,7 @@ fn EditIngredient(cx: Scope<EditIngredientProps>) -> Element {
}
};
to_owned_props![
to_owned![
token,
cx.props.recipe,
cx.props.refresh,
@ -439,7 +438,7 @@ fn AddIngredientToRecipe(cx: Scope<AddIngredientToRecipeProps>) -> Element {
return;
};
to_owned_props![
to_owned![
cx.props.refresh,
cx.props.recipe,
token,
@ -538,13 +537,7 @@ fn EditSteps(cx: Scope<EditStepsProps>) -> Element {
let (token, household) = use_trimmed_context(cx);
let on_submit = move |_| {
to_owned_props![
error,
token,
steps,
cx.props.recipe,
cx.props.refresh
];
to_owned![error, token, steps, cx.props.recipe, cx.props.refresh];
cx.spawn(async move {
match do_edit_steps(token, household, recipe, steps.to_string()).await {
@ -600,9 +593,9 @@ fn RecipeViewer(cx: Scope<RecipeViewerProps>) -> Element {
};
let mk_del_ig = |&ingredient_id| {
to_owned_props![token];
to_owned![token];
move |_| {
to_owned_props![cx.props.refresh, token, cx.props.id, error];
to_owned![cx.props.refresh, token, cx.props.id, error];
cx.spawn(async move {
match do_delete_ig(token, household, id, ingredient_id).await {
Ok(_) => {
@ -711,7 +704,7 @@ fn RecipeViewer(cx: Scope<RecipeViewerProps>) -> Element {
}
#[derive(Props, PartialEq)]
struct RecipeFetchProps {
pub struct RecipeViewProps {
id: i64,
}
@ -729,7 +722,7 @@ async fn fetch_recipe(token: String, household: Uuid, id: i64) -> anyhow::Result
Ok(Rc::new(rsp.json().await?))
}
fn RecipeFetch(cx: Scope<RecipeFetchProps>) -> Element {
pub fn RecipeView(cx: Scope<RecipeViewProps>) -> Element {
let (token, household) = use_trimmed_context(cx);
let id = cx.props.id;
let (refresh_dep, do_refresh) = use_refresh(cx);
@ -749,18 +742,3 @@ fn RecipeFetch(cx: Scope<RecipeFetchProps>) -> Element {
None => rsx! { Spinner {} },
})
}
pub fn RecipeView(cx: Scope) -> Element {
let id = use_route(cx).parse_segment_or_404("recipe_id");
let router = use_router(cx);
let id = match id {
Some(id) => id,
None => {
router.navigate_to("/404");
return None;
}
};
cx.render(rsx! { RecipeFetch { id: id } })
}

View file

@ -1,6 +1,6 @@
use api::RenameHouseholdRequest;
use dioxus::prelude::*;
use dioxus_router::{use_router, Link};
use dioxus_router::prelude::*;
use gloo_storage::{LocalStorage, Storage};
use uuid::Uuid;
@ -9,7 +9,7 @@ use crate::{
bootstrap::{bs, ConfirmDangerModal, FormModal},
do_add_user_to_household, do_resolve_user,
full_context::FullContextRedirect,
use_error, use_full_context, use_trimmed_context, ErrorView, HouseholdInfo,
use_error, use_full_context, use_trimmed_context, ErrorView, HouseholdInfo, Route,
};
#[derive(Clone, Copy, PartialEq)]
@ -31,6 +31,22 @@ impl Page {
}
}
impl From<Route> for Option<Page> {
fn from(value: Route) -> Self {
match value {
Route::Index => Some(Page::Home),
Route::Login => None,
Route::OidcRedirect { .. } => None,
Route::HouseholdSelection => None,
Route::Ingredients => Some(Page::Ingredients),
Route::RecipeCreator => Some(Page::RecipeCreator),
Route::RecipeList => Some(Page::RecipeList),
Route::RecipeView { .. } => Some(Page::RecipeList),
}
}
}
#[derive(PartialEq)]
struct MenuEntry {
icon: &'static str,
label: &'static str,
@ -166,11 +182,10 @@ fn RenameHousehold<'a>(cx: Scope<'a>, name: &'a str) -> Element {
})
}
#[derive(Props)]
struct SidebarProps<'a> {
#[derive(Props, PartialEq)]
struct SidebarProps {
entries: Vec<MenuEntry>,
current: Page,
children: Element<'a>,
}
async fn do_leave(token: String, household: Uuid) -> anyhow::Result<()> {
@ -194,12 +209,12 @@ async fn do_leave(token: String, household: Uuid) -> anyhow::Result<()> {
fn SidebarDropdown(cx: Scope) -> Element {
let ctx = use_full_context(cx);
let router = use_router(cx);
let navigator = use_navigator(cx);
let leave = move || {
let token = ctx.read().login.token.clone();
let household = ctx.read().household.id;
to_owned![router];
to_owned![navigator];
cx.spawn(async move {
match do_leave(token, household).await {
@ -207,7 +222,7 @@ fn SidebarDropdown(cx: Scope) -> Element {
log::error!("Could not leave household: {e:?}");
}
Ok(_) => {
router.navigate_to("/household_selection");
navigator.push(Route::HouseholdSelection);
}
}
});
@ -217,7 +232,7 @@ fn SidebarDropdown(cx: Scope) -> Element {
LocalStorage::delete("token");
LocalStorage::delete("household");
router.navigate_to("/login");
navigator.push(Route::Login);
};
cx.render(rsx! {
@ -280,7 +295,7 @@ fn SidebarDropdown(cx: Scope) -> Element {
})
}
fn Sidebar<'a>(cx: Scope<'a, SidebarProps<'a>>) -> Element {
fn Sidebar(cx: Scope<SidebarProps>) -> Element {
let entries = cx.props.entries.iter().map(|e| {
let active = if e.page == cx.props.current {
"active"
@ -318,19 +333,14 @@ fn Sidebar<'a>(cx: Scope<'a, SidebarProps<'a>>) -> Element {
SidebarDropdown {}
}
}
div { class: "col py-3 overflow-scroll vh-100", &cx.props.children }
div { class: "col py-3 overflow-scroll vh-100", Outlet::<Route> {} }
}
}
})
}
#[derive(Props)]
pub struct RegaladeSidebarProps<'a> {
current: Page,
children: Element<'a>,
}
pub fn RegaladeSidebar<'a>(cx: Scope<'a, RegaladeSidebarProps<'a>>) -> Element {
pub fn RegaladeSidebar(cx: Scope) -> Element {
let current: Route = use_route(cx).unwrap();
let entries = vec![
MenuEntry {
label: "Home",
@ -355,8 +365,8 @@ pub fn RegaladeSidebar<'a>(cx: Scope<'a, RegaladeSidebarProps<'a>>) -> Element {
];
cx.render(rsx! {
FullContextRedirect {
Sidebar { current: cx.props.current, entries: entries, &cx.props.children }
FullContextRedirect {
Sidebar { current: Option::from(current).unwrap(), entries: entries }
}
})
}