Working user and link list, (big)restructure
This is now a workspace consisting of: * the pslink app (a wasm frontend for the admin interface) * the pslink binary * shared - modules for communication between the two above.
This commit is contained in:
+147
@@ -0,0 +1,147 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use fluent::{FluentArgs, FluentBundle, FluentResource};
|
||||
use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
|
||||
use unic_langid::LanguageIdentifier;
|
||||
|
||||
// ------ I18n ------
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct I18n {
|
||||
lang: Lang,
|
||||
ftl_bundle: Arc<FluentBundle<FluentResource>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for I18n {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self.lang)
|
||||
}
|
||||
}
|
||||
|
||||
impl I18n {
|
||||
#[must_use]
|
||||
pub fn new(lang: Lang) -> Self {
|
||||
Self {
|
||||
lang,
|
||||
ftl_bundle: Arc::new(lang.create_ftl_bundle()),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn lang(&self) -> &Lang {
|
||||
&self.lang
|
||||
}
|
||||
|
||||
pub fn set_lang(&mut self, lang: Lang) -> &Self {
|
||||
self.lang = lang;
|
||||
self.ftl_bundle = Arc::new(lang.create_ftl_bundle());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn translate(&self, key: impl AsRef<str>, args: Option<&FluentArgs>) -> String {
|
||||
let msg = self
|
||||
.ftl_bundle
|
||||
.get_message(key.as_ref())
|
||||
.expect("get fluent message");
|
||||
|
||||
let pattern = msg.value().expect("get value for fluent message");
|
||||
|
||||
self.ftl_bundle
|
||||
.format_pattern(pattern, args, &mut vec![])
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// ------ Lang ------
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Debug, Copy, Clone, Display, EnumIter, EnumString, AsRefStr, Eq, PartialEq)]
|
||||
pub enum Lang {
|
||||
#[strum(serialize = "en-US")]
|
||||
EnUS,
|
||||
#[strum(serialize = "de-DE")]
|
||||
DeDE,
|
||||
}
|
||||
|
||||
impl Lang {
|
||||
#[must_use]
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::EnUS => "English (US)",
|
||||
Self::DeDE => "Deutsch (Deutschland)",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn ftl_messages(self) -> &'static str {
|
||||
macro_rules! include_ftl_messages {
|
||||
( $lang_id:literal ) => {
|
||||
include_str!(concat!("../locales/", $lang_id, "/main.ftl"))
|
||||
};
|
||||
}
|
||||
match self {
|
||||
Self::EnUS => include_ftl_messages!("en"),
|
||||
Self::DeDE => include_ftl_messages!("de"),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn to_language_identifier(self) -> LanguageIdentifier {
|
||||
self.as_ref()
|
||||
.parse()
|
||||
.expect("parse Lang to LanguageIdentifier")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn create_ftl_bundle(self) -> FluentBundle<FluentResource> {
|
||||
let ftl_resource =
|
||||
FluentResource::try_new(self.ftl_messages().to_owned()).expect("parse FTL messages");
|
||||
|
||||
let mut bundle = FluentBundle::new(vec![self.to_language_identifier()]);
|
||||
bundle.add_resource(ftl_resource).expect("add FTL resource");
|
||||
bundle
|
||||
}
|
||||
}
|
||||
|
||||
// ------ create_t ------
|
||||
|
||||
/// Convenience macro to improve readability of `view`s with many translations.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
///```rust,no_run
|
||||
/// fn view(model: &Model) -> Node<Msg> {
|
||||
/// let args_male_sg = fluent_args![
|
||||
/// "userName" => "Stephan",
|
||||
/// "userGender" => "male",
|
||||
/// ];
|
||||
///
|
||||
/// create_t!(model.i18n);
|
||||
/// div![
|
||||
/// p![t!("hello-world")],
|
||||
/// p![t!("hello-user", args_male_sg)],
|
||||
/// ]
|
||||
/// }
|
||||
///```
|
||||
#[macro_export]
|
||||
macro_rules! create_t {
|
||||
( $i18n:expr ) => {
|
||||
// This replaces $d with $ in the inner macro.
|
||||
seed::with_dollar_sign! {
|
||||
($d:tt) => {
|
||||
macro_rules! t {
|
||||
{ $d key:expr } => {
|
||||
{
|
||||
$i18n.translate($d key, None)
|
||||
}
|
||||
};
|
||||
{ $d key:expr, $d args:expr } => {
|
||||
{
|
||||
$i18n.translate($d key, Some(&$d args))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
pub mod i18n;
|
||||
pub mod navigation;
|
||||
pub mod pages;
|
||||
|
||||
use pages::list_links;
|
||||
use pages::list_users;
|
||||
use seed::{div, log, prelude::*, App, Url, C};
|
||||
|
||||
use crate::i18n::{I18n, Lang};
|
||||
|
||||
// ------ ------
|
||||
// Init
|
||||
// ------ ------
|
||||
|
||||
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
||||
orders.subscribe(Msg::UrlChanged);
|
||||
|
||||
log!(url);
|
||||
|
||||
let lang = I18n::new(Lang::DeDE);
|
||||
|
||||
Model {
|
||||
index: 0,
|
||||
base_url: Url::new().add_path_part("app"),
|
||||
page: Page::init(url, orders, lang.clone()),
|
||||
i18n: lang,
|
||||
}
|
||||
}
|
||||
|
||||
// ------ ------
|
||||
// Model
|
||||
// ------ ------
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Model {
|
||||
index: usize,
|
||||
base_url: Url,
|
||||
page: Page,
|
||||
i18n: i18n::I18n,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Page {
|
||||
Home(pages::list_links::Model),
|
||||
ListUsers(pages::list_users::Model),
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl Page {
|
||||
fn init(mut url: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Self {
|
||||
url.next_path_part();
|
||||
let result = match url.remaining_path_parts().as_slice() {
|
||||
[] | ["list_links"] => Self::Home(pages::list_links::init(
|
||||
url,
|
||||
&mut orders.proxy(Msg::ListLinksMsg),
|
||||
i18n,
|
||||
)),
|
||||
["list_users"] => Self::ListUsers(pages::list_users::init(
|
||||
url,
|
||||
&mut orders.proxy(Msg::ListUsersMsg),
|
||||
i18n,
|
||||
)),
|
||||
_other => Self::NotFound,
|
||||
};
|
||||
|
||||
log!("Page initialized");
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
// ------ ------
|
||||
// Update
|
||||
// ------ ------
|
||||
#[allow(renamed_and_removed_lints, pub_enum_variant_names)]
|
||||
#[derive(Clone)]
|
||||
pub enum Msg {
|
||||
UrlChanged(subs::UrlChanged),
|
||||
ListLinksMsg(list_links::Msg),
|
||||
ListUsersMsg(list_users::Msg),
|
||||
NoMessage,
|
||||
}
|
||||
|
||||
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::UrlChanged(url) => {
|
||||
log!("Url changed!");
|
||||
|
||||
model.page = Page::init(url.0, orders, model.i18n.clone());
|
||||
}
|
||||
Msg::ListLinksMsg(msg) => {
|
||||
if let Page::Home(model) = &mut model.page {
|
||||
list_links::update(msg, model, &mut orders.proxy(Msg::ListLinksMsg))
|
||||
}
|
||||
}
|
||||
Msg::ListUsersMsg(msg) => {
|
||||
if let Page::ListUsers(model) = &mut model.page {
|
||||
list_users::update(msg, model, &mut orders.proxy(Msg::ListUsersMsg))
|
||||
}
|
||||
}
|
||||
Msg::NoMessage => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Urls<'a> {
|
||||
base_url: std::borrow::Cow<'a, Url>,
|
||||
}
|
||||
|
||||
impl<'a> Urls<'a> {
|
||||
/// Create a new `Urls` instance.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// Urls::new(base_url).home()
|
||||
/// ```
|
||||
pub fn new(base_url: impl Into<std::borrow::Cow<'a, Url>>) -> Self {
|
||||
Self {
|
||||
base_url: base_url.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return base `Url`. If `base_url` isn't owned, it will be cloned.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// pub fn admin_urls(self) -> page::admin::Urls<'a> {
|
||||
/// page::admin::Urls::new(self.base_url().add_path_part(ADMIN))
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn base_url(self) -> Url {
|
||||
self.base_url.into_owned()
|
||||
}
|
||||
#[must_use]
|
||||
pub fn home(self) -> Url {
|
||||
self.base_url()
|
||||
}
|
||||
#[must_use]
|
||||
pub fn list_links(self) -> Url {
|
||||
self.base_url().add_path_part("list_links")
|
||||
}
|
||||
#[must_use]
|
||||
pub fn list_users(self) -> Url {
|
||||
self.base_url().add_path_part("list_users")
|
||||
}
|
||||
}
|
||||
|
||||
// ------ ------
|
||||
// View
|
||||
// ------ ------
|
||||
|
||||
fn view(model: &Model) -> Node<Msg> {
|
||||
div![
|
||||
C!["page"],
|
||||
navigation::navigation(&model.i18n, &model.base_url,),
|
||||
view_content(&model.page, &model.base_url),
|
||||
]
|
||||
}
|
||||
fn view_content(page: &Page, url: &Url) -> Node<Msg> {
|
||||
div![
|
||||
C!["container"],
|
||||
match page {
|
||||
Page::Home(model) => pages::list_links::view(model).map_msg(Msg::ListLinksMsg),
|
||||
Page::ListUsers(model) => pages::list_users::view(model).map_msg(Msg::ListUsersMsg),
|
||||
Page::NotFound => div![div![url.to_string()], "Page not found!"],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// ------ ------
|
||||
// Start
|
||||
// ------ ------
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn main() {
|
||||
App::start("app", init, update, view);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use fluent::fluent_args;
|
||||
use seed::{a, attrs, div, li, nav, ol, prelude::*, Url};
|
||||
|
||||
use crate::{i18n::I18n, Msg};
|
||||
#[must_use]
|
||||
pub fn navigation(i18n: &I18n, base_url: &Url) -> Node<Msg> {
|
||||
let username = fluent_args![ "username" => "enaut"];
|
||||
macro_rules! t {
|
||||
{ $key:expr } => {
|
||||
{
|
||||
i18n.translate($key, None)
|
||||
}
|
||||
};
|
||||
{ $key:expr, $args:expr } => {
|
||||
{
|
||||
i18n.translate($key, Some(&$args))
|
||||
}
|
||||
};
|
||||
}
|
||||
nav![
|
||||
ol![
|
||||
li![a![
|
||||
attrs! {At::Href => crate::Urls::new(base_url).list_links()},
|
||||
t!("list-links"),
|
||||
],],
|
||||
li![a![ev(Ev::Click, |_| Msg::NoMessage), t!("add-link"),],],
|
||||
li![a![
|
||||
attrs! {At::Href => "#"},
|
||||
ev(Ev::Click, |_| Msg::NoMessage),
|
||||
t!("invite-user"),
|
||||
],],
|
||||
li![a![
|
||||
attrs! {At::Href => crate::Urls::new(base_url).list_users()},
|
||||
t!("list-users"),
|
||||
],],
|
||||
],
|
||||
ol![
|
||||
li![div![t!("welcome-user", username)]],
|
||||
li![a![
|
||||
attrs! {At::Href => "#"},
|
||||
ev(Ev::Click, |_| Msg::NoMessage),
|
||||
t!("logout"),
|
||||
]]
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
use seed::{div, prelude::*};
|
||||
|
||||
#[must_use]
|
||||
pub fn view<Ms>() -> Node<Ms> {
|
||||
div!["List Links"]
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
use enum_map::EnumMap;
|
||||
use seed::{a, attrs, button, h1, input, log, prelude::*, section, table, td, th, tr, Url, C};
|
||||
|
||||
use shared::{
|
||||
apirequests::general::Ordering,
|
||||
apirequests::{
|
||||
general::Operation,
|
||||
links::{LinkOverviewColumns, LinkRequestForm},
|
||||
},
|
||||
datatypes::FullLink,
|
||||
};
|
||||
|
||||
use crate::i18n::I18n;
|
||||
|
||||
pub fn init(_: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model {
|
||||
orders.send_msg(Msg::Fetch);
|
||||
|
||||
Model {
|
||||
links: Vec::new(),
|
||||
i18n,
|
||||
formconfig: LinkRequestForm::default(),
|
||||
inputs: EnumMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Model {
|
||||
links: Vec<FullLink>,
|
||||
i18n: I18n,
|
||||
formconfig: LinkRequestForm,
|
||||
inputs: EnumMap<LinkOverviewColumns, FilterInput>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct FilterInput {
|
||||
filter_input: ElRef<web_sys::HtmlInputElement>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Msg {
|
||||
Fetch,
|
||||
OrderBy(LinkOverviewColumns),
|
||||
Received(Vec<FullLink>),
|
||||
CodeFilterChanged(String),
|
||||
DescriptionFilterChanged(String),
|
||||
TargetFilterChanged(String),
|
||||
AuthorFilterChanged(String),
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
/// Sould only panic on bugs.
|
||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::Fetch => {
|
||||
orders.skip(); // No need to rerender
|
||||
let data = model.formconfig.clone(); // complicated way to move into the closure
|
||||
orders.perform_cmd(async {
|
||||
let data = data;
|
||||
let response = fetch(
|
||||
Request::new("/admin/json/list_links/")
|
||||
.method(Method::Post)
|
||||
.json(&data)
|
||||
.expect("serialization failed"),
|
||||
)
|
||||
.await
|
||||
.expect("HTTP request failed");
|
||||
|
||||
let user: Vec<FullLink> = response
|
||||
.check_status() // ensure we've got 2xx status
|
||||
.expect("status check failed")
|
||||
.json()
|
||||
.await
|
||||
.expect("deserialization failed");
|
||||
|
||||
Msg::Received(user)
|
||||
});
|
||||
}
|
||||
Msg::OrderBy(column) => {
|
||||
model.formconfig.order = model.formconfig.order.as_ref().map_or_else(
|
||||
|| {
|
||||
Some(Operation {
|
||||
column: column.clone(),
|
||||
value: Ordering::Ascending,
|
||||
})
|
||||
},
|
||||
|order| {
|
||||
Some(Operation {
|
||||
column: column.clone(),
|
||||
value: if order.column == column && order.value == Ordering::Ascending {
|
||||
Ordering::Descending
|
||||
} else {
|
||||
Ordering::Ascending
|
||||
},
|
||||
})
|
||||
},
|
||||
);
|
||||
orders.send_msg(Msg::Fetch);
|
||||
|
||||
model.links.sort_by(match column {
|
||||
LinkOverviewColumns::Code => {
|
||||
|o: &FullLink, t: &FullLink| o.link.code.cmp(&t.link.code)
|
||||
}
|
||||
LinkOverviewColumns::Description => {
|
||||
|o: &FullLink, t: &FullLink| o.link.title.cmp(&t.link.title)
|
||||
}
|
||||
LinkOverviewColumns::Target => {
|
||||
|o: &FullLink, t: &FullLink| o.link.target.cmp(&t.link.target)
|
||||
}
|
||||
LinkOverviewColumns::Author => {
|
||||
|o: &FullLink, t: &FullLink| o.user.username.cmp(&t.user.username)
|
||||
}
|
||||
LinkOverviewColumns::Statistics => {
|
||||
|o: &FullLink, t: &FullLink| o.clicks.number.cmp(&t.clicks.number)
|
||||
}
|
||||
})
|
||||
}
|
||||
Msg::Received(response) => {
|
||||
model.links = response;
|
||||
}
|
||||
Msg::CodeFilterChanged(s) => {
|
||||
log!("Filter is: ", &s);
|
||||
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
|
||||
model.formconfig.filter[LinkOverviewColumns::Code].sieve = sanit;
|
||||
orders.send_msg(Msg::Fetch);
|
||||
}
|
||||
Msg::DescriptionFilterChanged(s) => {
|
||||
log!("Filter is: ", &s);
|
||||
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
|
||||
model.formconfig.filter[LinkOverviewColumns::Description].sieve = sanit;
|
||||
orders.send_msg(Msg::Fetch);
|
||||
}
|
||||
Msg::TargetFilterChanged(s) => {
|
||||
log!("Filter is: ", &s);
|
||||
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
|
||||
model.formconfig.filter[LinkOverviewColumns::Target].sieve = sanit;
|
||||
orders.send_msg(Msg::Fetch);
|
||||
}
|
||||
Msg::AuthorFilterChanged(s) => {
|
||||
log!("Filter is: ", &s);
|
||||
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
|
||||
model.formconfig.filter[LinkOverviewColumns::Author].sieve = sanit;
|
||||
orders.send_msg(Msg::Fetch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// # Panics
|
||||
/// Sould only panic on bugs.
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
macro_rules! t {
|
||||
{ $key:expr } => {
|
||||
{
|
||||
model.i18n.translate($key, None)
|
||||
}
|
||||
};
|
||||
{ $key:expr, $args:expr } => {
|
||||
{
|
||||
model.i18n.translate($key, Some(&$args))
|
||||
}
|
||||
};
|
||||
}
|
||||
section![
|
||||
h1!("List Links Page from list_links"),
|
||||
table![
|
||||
tr![
|
||||
th![
|
||||
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Code)),
|
||||
t!("link-code")
|
||||
],
|
||||
th![
|
||||
ev(Ev::Click, |_| Msg::OrderBy(
|
||||
LinkOverviewColumns::Description
|
||||
)),
|
||||
t!("link-description")
|
||||
],
|
||||
th![
|
||||
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Target)),
|
||||
t!("link-target")
|
||||
],
|
||||
th![
|
||||
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Author)),
|
||||
t!("username")
|
||||
],
|
||||
th![
|
||||
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Statistics)),
|
||||
t!("statistics")
|
||||
]
|
||||
],
|
||||
tr![
|
||||
C!["filters"],
|
||||
td![input![
|
||||
attrs! {
|
||||
At::Value => &model.formconfig.filter[LinkOverviewColumns::Code].sieve,
|
||||
At::Type => "search",
|
||||
At::Placeholder => t!("search-placeholder")
|
||||
},
|
||||
input_ev(Ev::Input, Msg::CodeFilterChanged),
|
||||
el_ref(&model.inputs[LinkOverviewColumns::Code].filter_input),
|
||||
]],
|
||||
td![input![
|
||||
attrs! {At::Value =>
|
||||
&model
|
||||
.formconfig.filter[LinkOverviewColumns::Description].sieve,
|
||||
At::Type => "search",
|
||||
At::Placeholder => t!("search-placeholder")
|
||||
},
|
||||
input_ev(Ev::Input, Msg::DescriptionFilterChanged),
|
||||
el_ref(&model.inputs[LinkOverviewColumns::Description].filter_input),
|
||||
]],
|
||||
td![input![
|
||||
attrs! {At::Value =>
|
||||
&model
|
||||
.formconfig.filter[LinkOverviewColumns::Target].sieve,
|
||||
At::Type => "search",
|
||||
At::Placeholder => t!("search-placeholder")
|
||||
},
|
||||
input_ev(Ev::Input, Msg::TargetFilterChanged),
|
||||
el_ref(&model.inputs[LinkOverviewColumns::Target].filter_input),
|
||||
]],
|
||||
td![input![
|
||||
attrs! {At::Value =>
|
||||
&model
|
||||
.formconfig.filter[LinkOverviewColumns::Author].sieve,
|
||||
At::Type => "search",
|
||||
At::Placeholder => t!("search-placeholder")
|
||||
},
|
||||
input_ev(Ev::Input, Msg::AuthorFilterChanged),
|
||||
el_ref(&model.inputs[LinkOverviewColumns::Author].filter_input),
|
||||
]],
|
||||
td![]
|
||||
],
|
||||
model.links.iter().map(view_link)
|
||||
],
|
||||
button![ev(Ev::Click, |_| Msg::Fetch), "Fetch links"]
|
||||
]
|
||||
}
|
||||
|
||||
fn view_link(l: &FullLink) -> Node<Msg> {
|
||||
tr![
|
||||
td![&l.link.code],
|
||||
td![&l.link.title],
|
||||
td![a![attrs![At::Href => &l.link.target], &l.link.target]],
|
||||
td![&l.user.username],
|
||||
td![&l.clicks.number]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
use enum_map::EnumMap;
|
||||
use seed::{attrs, button, h1, input, log, prelude::*, section, table, td, th, tr, Url, C};
|
||||
use shared::{
|
||||
apirequests::general::{Operation, Ordering},
|
||||
apirequests::users::{UserOverviewColumns, UserRequestForm},
|
||||
datatypes::User,
|
||||
};
|
||||
|
||||
use crate::i18n::I18n;
|
||||
#[must_use]
|
||||
pub fn init(_: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model {
|
||||
orders.send_msg(Msg::Fetch);
|
||||
Model {
|
||||
users: Vec::new(),
|
||||
i18n,
|
||||
formconfig: UserRequestForm::default(),
|
||||
inputs: EnumMap::default(),
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct Model {
|
||||
users: Vec<User>,
|
||||
i18n: I18n,
|
||||
formconfig: UserRequestForm,
|
||||
inputs: EnumMap<UserOverviewColumns, FilterInput>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct FilterInput {
|
||||
filter_input: ElRef<web_sys::HtmlInputElement>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Msg {
|
||||
Fetch,
|
||||
OrderBy(UserOverviewColumns),
|
||||
Received(Vec<User>),
|
||||
IdFilterChanged(String),
|
||||
EmailFilterChanged(String),
|
||||
UsernameFilterChanged(String),
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
/// Sould only panic on bugs.
|
||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::Fetch => {
|
||||
orders.skip(); // No need to rerender
|
||||
let data = model.formconfig.clone(); // complicated way to move into the closure
|
||||
orders.perform_cmd(async {
|
||||
let data = data;
|
||||
let response = fetch(
|
||||
Request::new("/admin/json/list_users/")
|
||||
.method(Method::Post)
|
||||
.json(&data)
|
||||
.expect("serialization failed"),
|
||||
)
|
||||
.await
|
||||
.expect("HTTP request failed");
|
||||
|
||||
let users: Vec<User> = response
|
||||
.check_status() // ensure we've got 2xx status
|
||||
.expect("status check failed")
|
||||
.json()
|
||||
.await
|
||||
.expect("deserialization failed");
|
||||
|
||||
Msg::Received(users)
|
||||
});
|
||||
}
|
||||
Msg::OrderBy(column) => {
|
||||
model.formconfig.order = model.formconfig.order.as_ref().map_or_else(
|
||||
|| {
|
||||
Some(Operation {
|
||||
column: column.clone(),
|
||||
value: Ordering::Ascending,
|
||||
})
|
||||
},
|
||||
|order| {
|
||||
Some(Operation {
|
||||
column: column.clone(),
|
||||
value: if order.column == column && order.value == Ordering::Ascending {
|
||||
Ordering::Descending
|
||||
} else {
|
||||
Ordering::Ascending
|
||||
},
|
||||
})
|
||||
},
|
||||
);
|
||||
orders.send_msg(Msg::Fetch);
|
||||
|
||||
model.users.sort_by(match column {
|
||||
UserOverviewColumns::Id => |o: &User, t: &User| o.id.cmp(&t.id),
|
||||
UserOverviewColumns::Username => |o: &User, t: &User| o.username.cmp(&t.username),
|
||||
UserOverviewColumns::Email => |o: &User, t: &User| o.email.cmp(&t.email),
|
||||
})
|
||||
}
|
||||
Msg::Received(response) => {
|
||||
model.users = response;
|
||||
}
|
||||
Msg::IdFilterChanged(s) => {
|
||||
log!("Filter is: ", &s);
|
||||
let sanit = s.chars().filter(|x| x.is_numeric()).collect();
|
||||
model.formconfig.filter[UserOverviewColumns::Id].sieve = sanit;
|
||||
orders.send_msg(Msg::Fetch);
|
||||
}
|
||||
Msg::UsernameFilterChanged(s) => {
|
||||
log!("Filter is: ", &s);
|
||||
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
|
||||
model.formconfig.filter[UserOverviewColumns::Username].sieve = sanit;
|
||||
orders.send_msg(Msg::Fetch);
|
||||
}
|
||||
Msg::EmailFilterChanged(s) => {
|
||||
log!("Filter is: ", &s);
|
||||
// FIXME: Sanitazion does not work for @
|
||||
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
|
||||
model.formconfig.filter[UserOverviewColumns::Email].sieve = sanit;
|
||||
orders.send_msg(Msg::Fetch);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[must_use]
|
||||
/// # Panics
|
||||
/// Sould only panic on bugs.
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
macro_rules! t {
|
||||
{ $key:expr } => {
|
||||
{
|
||||
model.i18n.translate($key, None)
|
||||
}
|
||||
};
|
||||
{ $key:expr, $args:expr } => {
|
||||
{
|
||||
model.i18n.translate($key, Some(&$args))
|
||||
}
|
||||
};
|
||||
}
|
||||
section![
|
||||
h1!("List Users Page from list_users"),
|
||||
table![
|
||||
tr![
|
||||
th![
|
||||
ev(Ev::Click, |_| Msg::OrderBy(UserOverviewColumns::Id)),
|
||||
t!("userid")
|
||||
],
|
||||
th![
|
||||
ev(Ev::Click, |_| Msg::OrderBy(UserOverviewColumns::Email)),
|
||||
t!("email")
|
||||
],
|
||||
th![
|
||||
ev(Ev::Click, |_| Msg::OrderBy(UserOverviewColumns::Username)),
|
||||
t!("username")
|
||||
],
|
||||
],
|
||||
tr![
|
||||
C!["filters"],
|
||||
td![input![
|
||||
attrs! {
|
||||
At::Value => &model.formconfig.filter[UserOverviewColumns::Id].sieve,
|
||||
At::Type => "search",
|
||||
At::Placeholder => t!("search-placeholder")
|
||||
},
|
||||
input_ev(Ev::Input, Msg::IdFilterChanged),
|
||||
el_ref(&model.inputs[UserOverviewColumns::Id].filter_input),
|
||||
]],
|
||||
td![input![
|
||||
attrs! {At::Value =>
|
||||
&model
|
||||
.formconfig.filter[UserOverviewColumns::Email].sieve,
|
||||
At::Type => "search",
|
||||
At::Placeholder => t!("search-placeholder")
|
||||
},
|
||||
input_ev(Ev::Input, Msg::EmailFilterChanged),
|
||||
el_ref(&model.inputs[UserOverviewColumns::Email].filter_input),
|
||||
]],
|
||||
td![input![
|
||||
attrs! {At::Value =>
|
||||
&model
|
||||
.formconfig.filter[UserOverviewColumns::Username].sieve,
|
||||
At::Type => "search",
|
||||
At::Placeholder => t!("search-placeholder")
|
||||
},
|
||||
input_ev(Ev::Input, Msg::UsernameFilterChanged),
|
||||
el_ref(&model.inputs[UserOverviewColumns::Username].filter_input),
|
||||
]],
|
||||
],
|
||||
model.users.iter().map(view_user)
|
||||
],
|
||||
button![ev(Ev::Click, |_| Msg::Fetch), "Refresh"]
|
||||
]
|
||||
}
|
||||
|
||||
fn view_user(l: &User) -> Node<Msg> {
|
||||
tr![
|
||||
td![&l.id],
|
||||
td![&l.email],
|
||||
//td![a![attrs![At::Href => &l.link.target], &l.link.target]],
|
||||
td![&l.username],
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod home;
|
||||
pub mod list_links;
|
||||
pub mod list_users;
|
||||
Reference in New Issue
Block a user