Added settings for company info. Added code for entity validation.

main
Josef Rokos
parent 44b8b21426
commit 9dd0993204

49
Cargo.lock generated

@ -1504,6 +1504,12 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "if_chain"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
[[package]]
name = "indexmap"
version = "1.9.3"
@ -2503,6 +2509,7 @@ dependencies = [
"serde",
"sqlx",
"uuid",
"validator",
"wasm-bindgen",
"web-sys",
]
@ -3558,6 +3565,48 @@ dependencies = [
"getrandom",
]
[[package]]
name = "validator"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd"
dependencies = [
"idna",
"lazy_static",
"regex",
"serde",
"serde_derive",
"serde_json",
"url",
"validator_derive",
]
[[package]]
name = "validator_derive"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af"
dependencies = [
"if_chain",
"lazy_static",
"proc-macro-error",
"proc-macro2",
"quote",
"regex",
"syn 1.0.109",
"validator_types",
]
[[package]]
name = "validator_types"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3"
dependencies = [
"proc-macro2",
"syn 1.0.109",
]
[[package]]
name = "vcpkg"
version = "0.2.15"

@ -24,6 +24,7 @@ chrono = "0.4.26"
sqlx = { version = "0.7.1", optional = true, features = ["runtime-tokio-rustls", "postgres", "chrono", "rust_decimal"] }
rust_decimal = "1.31.0"
uuid = {version = "1.4.1", features = ["v4"]}
validator = {version = "0.16.1", features = ["derive"]}
[features]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]

@ -3,6 +3,7 @@ use leptos_meta::*;
use leptos_router::*;
use crate::locales::trl;
use crate::pages::home_page::HomePage;
use crate::pages::settings::Settings;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
@ -98,21 +99,15 @@ pub fn App(cx: Scope) -> impl IntoView {
<div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse">
//<!-- Search -->
<div class="navbar-nav align-items-center">
<div class="nav-item d-flex align-items-center">
<i class="bx bx-search fs-4 lh-0"></i>
<input
type="text"
class="form-control border-0 shadow-none"
placeholder={trl(cx, "Search...")}
aria-label={trl(cx, "Search...")}
/>
<div class="navbar-nav align-items-center ms-auto mt-auto">
<div class="nav-item d-flex align-items-center mt-auto">
<h4 class="mt-3"><i class="bx bx-desktop fs-4 lh-0"></i>" Admin portal"</h4>
</div>
</div>
//<!-- /Search -->
<ul class="navbar-nav flex-row align-items-center ms-auto">
<li class="nav-item navbar-dropdown dropdown-user dropdown">
<a class="nav-link dropdown-toggle hide-arrow" href="#" data-bs-toggle="dropdown">
<a class="nav-link dropdown-toggle hide-arrow" href="/settings" data-bs-toggle="dropdown">
<i class="bx bx-cog fs-3 lh-0"></i>
</a>
</li>
@ -179,6 +174,7 @@ pub fn App(cx: Scope) -> impl IntoView {
<main>
<Routes>
<Route path="" view=|cx| view! { cx, <HomePage/> }/>
<Route path="settings" view=|cx| view! { cx, <Settings/> }/>
</Routes>
</main>
</Router>

@ -0,0 +1,48 @@
use crate::backend::data::Company;
use leptos::*;
#[server(GetCompany, "/api", "Url", "get_company")]
pub async fn get_company(cx: Scope) -> Result<Company, ServerFnError> {
use crate::backend::AppData;
use actix_web::web::Data;
use leptos_actix::extract;
let pool = extract(
cx,
|data: Data<AppData>| async move { data.db_pool().clone() },
)
.await?;
let cmp = sqlx::query_as::<_, Company>("SELECT * FROM company")
.fetch_one(&pool)
.await?;
Ok(cmp)
}
#[server(UpdateCompany, "/api", "Url", "update_company")]
pub async fn update_company(cx: Scope, company: Company) -> Result<(), ServerFnError> {
use crate::backend::AppData;
use actix_web::web::Data;
use leptos_actix::extract;
let pool = extract(
cx,
|data: Data<AppData>| async move { data.db_pool().clone() },
)
.await?;
sqlx::query(
"UPDATE company SET name = $1, street = $2, house_number = $3, zip_code = $4, city = $5 \
WHERE id = $6")
.bind(company.name.clone())
.bind(company.street.clone())
.bind(company.house_number.clone())
.bind(company.zip_code.clone())
.bind(company.city.clone())
.bind(company.id())
.execute(&pool)
.await?;
Ok(())
}

@ -1,14 +1,27 @@
use chrono::{NaiveDate, NaiveTime, Weekday};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use validator::Validate;
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct Company {
id: u16,
name: String,
street: String,
house_number: String,
zip_code: String,
city: String,
id: i32,
#[validate(length(min = 1,message = "Name cannot be empty"))]
pub name: String,
#[validate(length(min = 1,message = "Street cannot be empty"))]
pub street: String,
#[validate(length(min = 1,message = "House number cannot be empty"))]
pub house_number: String,
pub zip_code: String,
pub city: String,
}
impl Company {
pub fn id(&self) -> i32 {
self.id
}
}
pub struct User {

@ -1,6 +1,7 @@
use cfg_if::cfg_if;
pub mod data;
pub mod company;
cfg_if!{
if #[cfg(feature = "ssr")] {

@ -1,2 +1,4 @@
pub mod modal_box;
pub mod server_err;
pub mod validation_err;

@ -75,3 +75,19 @@ pub fn ModalFooter(cx: Scope, children: Children) -> impl IntoView {
</div>
}
}
#[component]
pub fn DlgNotLoaded(cx: Scope, opener: DialogOpener, title: &'static str) -> impl IntoView {
view! {cx,
<ModalDialog opener=opener title=title>
<ModalBody>
<div>{trl(cx, "Entity not loaded")}</div>
</ModalBody>
<ModalFooter>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal" on:click=move |_| opener.hide()>
{trl(cx, "Close")}
</button>
</ModalFooter>
</ModalDialog>
}
}

@ -0,0 +1,27 @@
use crate::components::modal_box::DialogOpener;
use leptos::*;
#[component]
pub fn ServerErr(
cx: Scope,
result: RwSignal<Option<Result<(), ServerFnError>>>,
opener: DialogOpener,
) -> impl IntoView {
view! {cx, {move || {
if let Some(val) = result.get() {
if let Err(e) = val {
view! {cx,
<div class="alert alert-danger">
"Server error: " {e.to_string()}
</div>
}
} else {
opener.hide();
view! {cx, <div></div>}
}
} else {
view! {cx, <div></div>}
}
}}
}
}

@ -0,0 +1,37 @@
use leptos::*;
use crate::locales::trl;
use crate::validator::Validator;
#[component]
pub fn ValidationErr(
cx: Scope,
validator: Validator,
) -> impl IntoView {
view! {cx, {move || {
if !validator.is_valid() {
if let Some(msgs) = validator.messages() {
let out_msgs = msgs.into_iter().map(move |e| {
view! {cx,
<div class="alert alert-danger">
{trl(cx, &e)}
</div>
}
}).collect_view(cx);
view! {cx,
<div>
{out_msgs}
</div>
}
} else {
view! {cx,
<div class="alert alert-danger">
"Validation error"
</div>
}
}
} else {
view! {cx, <div></div>}
}
}}
}
}

@ -4,6 +4,7 @@ pub mod locales;
pub mod backend;
mod pages;
mod components;
mod validator;
use cfg_if::cfg_if;

@ -10,7 +10,9 @@ lazy_static! {
("Settings", "Nastavení"),
("Search...", "Najít..."),
("Close", "Zavřít"),
("Save changes", "Uložit změny")
("Save changes", "Uložit změny"),
("Company info", "Organizace"),
("Name cannot be empty", "Jméno nesmí být prázdné"),
])),
("sk", HashMap::from( [
("Dashboard", "Prehlad"),

@ -1,15 +1,19 @@
use std::borrow::Cow;
use std::ops::Deref;
use leptos::*;
use crate::locales::catalogues::get_dictionary;
mod catalogues;
pub fn trl(cx: Scope, phrase: &'static str) -> impl Fn() -> &'static str {
pub fn trl(cx: Scope, phrase: &str) -> impl Fn() -> String {
let mut translated = phrase;
if let Some(dict) = get_dictionary(cx) {
if let Some(p) = dict.get(phrase) {
if let Some(p) = dict.get(phrase.to_string().as_str()) {
translated = *p;
}
}
|| { translated }
let out = translated.to_string();
move || { out.clone() }
}

@ -0,0 +1,119 @@
use crate::backend::data::Company;
use crate::backend::company::UpdateCompany;
use crate::components::modal_box::{
DialogOpener, DlgNotLoaded, ModalBody, ModalDialog, ModalFooter,
};
use crate::components::server_err::ServerErr;
use crate::locales::trl;
use crate::validator::Validator;
use leptos::*;
use leptos_router::*;
use crate::components::validation_err::ValidationErr;
#[component]
pub fn CompanyEdit(
cx: Scope,
company: ReadSignal<Option<Company>>,
opener: DialogOpener,
) -> impl IntoView {
view! {cx,
{move ||
if let Some(c) = company.get() {
let update_company = create_server_action::<UpdateCompany>(cx);
let upd_val = update_company.value();
let validator = Validator::new(cx);
view! {cx,
<ActionForm
on:submit=move |ev| {
let act = UpdateCompany::from_event(&ev);
if !act.is_err() {
validator.check(&act.unwrap().company, &ev);
}
}
action=update_company>
<ModalDialog opener=opener title="Edit company">
<ModalBody>
<ServerErr result={upd_val} opener=opener/>
<ValidationErr validator=validator />
<input type="hidden" value=c.id() name="company[id]"/>
<div class="row">
<div class="col mb-3">
<label for="nameWithTitle" class="form-label">"Name"</label>
<input
type="text"
id="nameWithTitle"
class="form-control"
placeholder="Enter Name"
value=c.name
name="company[name]"
/>
</div>
</div>
<div class="row">
<div class="col mb-3">
<label for="street" class="form-label">"Street"</label>
<input
type="text"
id="street"
class="form-control"
placeholder="Enter Street"
value=c.street
name="company[street]"
/>
</div>
<div class="col-4 mb-3">
<label for="houseNumber" class="form-label">"House number"</label>
<input
type="text"
id="houseNumber"
class="form-control"
placeholder="Enter House number"
value=c.house_number
name="company[house_number]"
/>
</div>
</div>
<div class="row">
<div class="col-4 mb-3">
<label for="zip" class="form-label">"ZIP code"</label>
<input
type="text"
id="zip"
class="form-control"
placeholder="Enter ZIP code"
value=c.zip_code
name="company[zip_code]"
/>
</div>
<div class="col mb-3">
<label for="city" class="form-label">"City"</label>
<input
type="text"
id="city"
class="form-control"
placeholder="Enter City"
value=c.city
name="company[city]"
/>
</div>
</div>
</ModalBody>
<ModalFooter>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal" on:click=move |_| opener.hide()>
{trl(cx, "Close")}
</button>
<button type="submit" class="btn btn-primary">
{trl(cx, "Save changes")}
</button>
</ModalFooter>
</ModalDialog>
</ActionForm>
}
} else {
view! {cx,
<DlgNotLoaded opener=opener title="Edit company" />
}
}
}
}
}

@ -0,0 +1,43 @@
use leptos::*;
use serde::de::Unexpected::Option;
use crate::backend::company::get_company;
use crate::components::modal_box::DialogOpener;
use crate::locales::trl;
use crate::pages::company_edit::CompanyEdit;
#[component]
pub fn CompanyInfo(cx: Scope) -> impl IntoView {
let editor = DialogOpener::new(cx);
let company = create_resource(cx, move|| editor.visible(), move |_| { get_company(cx) });
let (cmp, set_cmp) = create_signal(cx, None);
view! {cx,
<CompanyEdit company={cmp} opener=editor/>
<div class="card">
<div class="card-body">
<h5 class="card-title"><i class="bx bx-buildings"></i>" "{trl(cx, "Company info")}</h5>
<p class="card-text">
<Transition fallback=move || view! {cx, <p>{trl(cx, "Loading...")}</p> }>
{move || {
company.read(cx).map(|c| match c {
Err(e) => {view! {cx, <p>{trl(cx, "Error loading data")}</p>
<p>{e.to_string()}</p>
}}
Ok(c) => {
set_cmp.update(|cmp| *cmp = Some(c.clone()));
view! {
cx, <p><b>{c.name}</b></p>
<p>{c.street}" "{c.house_number}<br/>
{c.zip_code}" "{c.city}
</p>
}}
})
}}
</Transition>
</p>
<a href="javascript:void(0)" class="card-link" on:click = move |_| editor.show()>
<i class="bx bx-pencil fs-4 lh-0"></i></a>
</div>
</div>
}
}

@ -1 +1,4 @@
pub mod home_page;
pub mod settings;
pub mod company_info;
mod company_edit;

@ -0,0 +1,45 @@
use leptos::*;
use crate::locales::trl;
use crate::pages::company_info::CompanyInfo;
#[component]
pub fn Settings(cx: Scope) -> impl IntoView {
view! {cx,
<h1>{trl(cx, "Settings")}</h1>
<div class="row mb-5">
<div class="col-md-6 col-lg-4 mb-3">
<CompanyInfo/>
</div>
<div class="col-md-6 col-lg-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">"Card title"</h5>
<h6 class="card-subtitle text-muted">"Support card subtitle"</h6>
</div>
<img class="img-fluid" src="../assets/img/elements/13.jpg" alt="Card image cap" />
<div class="card-body">
<p class="card-text">"Bear claw sesame snaps gummies chocolate."</p>
<a href="javascript:void(0);" class="card-link">"Card link"</a>
<a href="javascript:void(0);" class="card-link">"Another link"</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">"Card title"</h5>
<h6 class="card-subtitle text-muted">"Support card subtitle"</h6>
<img
class="img-fluid d-flex mx-auto my-4"
src="../assets/img/elements/4.jpg"
alt="Card image cap"
/>
<p class="card-text">"Bear claw sesame snaps gummies chocolate."</p>
<a href="javascript:void(0);" class="card-link">Card link</a>
<a href="javascript:void(0);" class="card-link">Another link</a>
</div>
</div>
</div>
</div>
}
}

@ -0,0 +1,61 @@
use leptos::*;
use validator::Validate;
#[derive(Copy, Clone)]
pub struct Validator {
message: ReadSignal<Option<String>>,
set_message: WriteSignal<Option<String>>,
valid: ReadSignal<bool>,
set_valid: WriteSignal<bool>,
messages: ReadSignal<Option<Vec<String>>>,
set_messages: WriteSignal<Option<Vec<String>>>
}
impl Validator {
pub fn new(cx: Scope) -> Self {
let (valid, set_valid) = create_signal(cx, true);
let (message, set_message) = create_signal(cx, None);
let (messages, set_messages) = create_signal(cx, None);
Self {
message,
set_message,
valid,
set_valid,
messages,
set_messages
}
}
pub fn check(&self, entity: &impl Validate, ev: &web_sys::Event) {
if let Err(val_err) = entity.validate() {
ev.prevent_default();
self.set_message.update(|m| *m = Some(val_err.to_string().clone()));
self.set_messages.update(|m| *m = {
let mut out: Vec<String> = vec![];
val_err.field_errors().drain().for_each(|e| {
e.1.to_vec().into_iter().for_each(|err| {
if let Some(msg) = err.message {
out.push(msg.to_string());
}
});
});
Some(out)
});
self.set_valid.update(|v| *v = false);
} else {
self.set_valid.update(|v| *v = true);
}
}
pub fn is_valid(&self) -> bool {
self.valid.get()
}
pub fn message(&self) -> Option<String> {
self.message.get()
}
pub fn messages(&self) -> Option<Vec<String>> {
self.messages.get()
}
}
Loading…
Cancel
Save