Archived
1
0
This repository has been archived on 2025-01-25. You can view files and clone it, but cannot push or open issues or pull requests.
Pslink/app/src/pages/list_links.rs
2021-08-12 15:48:02 +02:00

611 lines
20 KiB
Rust

use std::cell::RefCell;
use enum_map::EnumMap;
use fluent::fluent_args;
use seed::{
a, attrs, button, div, h1, img, input, log, prelude::*, section, span, table, td, th, tr, Url,
C,
};
use shared::{
apirequests::general::Ordering,
apirequests::{
general::{EditMode, Message, Operation, Status},
links::{LinkDelta, LinkOverviewColumns, LinkRequestForm},
},
datatypes::FullLink,
};
use crate::i18n::I18n;
/// Unwrap a result and return it's content, or return from the function with another expression.
macro_rules! unwrap_or_return {
( $e:expr, $result:expr) => {
match $e {
Ok(x) => x,
Err(_) => return $result,
}
};
}
/// Setup the page
pub fn init(mut url: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model {
log!(url);
orders.send_msg(Msg::Query(QueryMsg::Fetch));
let edit_link = match url.next_path_part() {
Some("create_link") => Some(RefCell::new(LinkDelta::default())),
None | Some(_) => None,
};
log!(edit_link);
Model {
links: Vec::new(),
i18n,
formconfig: LinkRequestForm::default(),
inputs: EnumMap::default(),
edit_link,
last_message: None,
question: None,
}
}
#[derive(Debug)]
pub struct Model {
links: Vec<FullLink>,
i18n: I18n,
formconfig: LinkRequestForm,
inputs: EnumMap<LinkOverviewColumns, FilterInput>,
edit_link: Option<RefCell<LinkDelta>>,
last_message: Option<Status>,
question: Option<EditMsg>,
}
#[derive(Default, Debug, Clone)]
struct FilterInput {
filter_input: ElRef<web_sys::HtmlInputElement>,
}
#[derive(Clone)]
pub enum Msg {
Query(QueryMsg),
Edit(EditMsg),
ClearAll,
SetMessage(String),
}
#[derive(Clone)]
pub enum QueryMsg {
Fetch,
OrderBy(LinkOverviewColumns),
Received(Vec<FullLink>),
CodeFilterChanged(String),
DescriptionFilterChanged(String),
TargetFilterChanged(String),
AuthorFilterChanged(String),
}
/// All the messages on link editing
#[derive(Clone, Debug)]
pub enum EditMsg {
EditSelected(LinkDelta),
CreateNewLink,
Created(Status),
EditCodeChanged(String),
EditDescriptionChanged(String),
EditTargetChanged(String),
MayDeleteSelected(LinkDelta),
DeleteSelected(LinkDelta),
SaveLink,
FailedToCreateLink,
FailedToDeleteLink,
DeletedLink(Status),
}
/// React to environment changes
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::Query(msg) => process_query_messages(msg, model, orders),
Msg::Edit(msg) => process_edit_messages(msg, model, orders),
Msg::ClearAll => {
model.edit_link = None;
model.last_message = None;
model.question = None;
}
Msg::SetMessage(msg) => {
model.edit_link = None;
model.question = None;
model.last_message = Some(Status::Error(Message { message: msg }));
}
}
}
/// Process all messages for loading the information from the server.
pub fn process_query_messages(msg: QueryMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
QueryMsg::Fetch => {
orders.skip(); // No need to rerender
load_links(model, orders)
}
QueryMsg::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::Query(QueryMsg::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)
}
})
}
QueryMsg::Received(response) => {
model.links = response;
}
QueryMsg::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::Query(QueryMsg::Fetch));
}
QueryMsg::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::Query(QueryMsg::Fetch));
}
QueryMsg::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::Query(QueryMsg::Fetch));
}
QueryMsg::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::Query(QueryMsg::Fetch));
}
}
}
/// Perform a request to the server to load the links from the server.
fn load_links(model: &Model, orders: &mut impl Orders<Msg>) {
let data = model.formconfig.clone();
orders.perform_cmd(async {
let data = data;
let request = unwrap_or_return!(
Request::new("/admin/json/list_links/")
.method(Method::Post)
.json(&data),
Msg::SetMessage("Failed to parse data".to_string())
);
let response = unwrap_or_return!(
fetch(request).await,
Msg::SetMessage("Failed to send data".to_string())
);
let response = unwrap_or_return!(
response.check_status(),
Msg::SetMessage("Wrong response code".to_string())
);
let links: Vec<FullLink> = unwrap_or_return!(
response.json().await,
Msg::SetMessage("Invalid response".to_string())
);
Msg::Query(QueryMsg::Received(links))
});
}
/// Process all the events related to editing links.
pub fn process_edit_messages(msg: EditMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
EditMsg::EditSelected(link) => {
log!("Editing link: ", link);
model.last_message = None;
model.edit_link = Some(RefCell::new(link))
}
EditMsg::CreateNewLink => {
log!("Create new link!");
model.edit_link = Some(RefCell::new(LinkDelta::default()))
}
EditMsg::Created(success_msg) => {
model.last_message = Some(success_msg);
model.edit_link = None;
orders.send_msg(Msg::Query(QueryMsg::Fetch));
}
EditMsg::EditCodeChanged(s) => {
if let Some(ref le) = model.edit_link {
le.try_borrow_mut().expect("Failed to borrow mutably").code = s;
}
}
EditMsg::EditDescriptionChanged(s) => {
if let Some(ref le) = model.edit_link {
le.try_borrow_mut().expect("Failed to borrow mutably").title = s;
}
}
EditMsg::EditTargetChanged(s) => {
if let Some(ref le) = model.edit_link {
le.try_borrow_mut()
.expect("Failed to borrow mutably")
.target = s;
}
}
EditMsg::SaveLink => {
save_link(model, orders);
}
EditMsg::FailedToCreateLink => {
log!("Failed to create Link");
}
link @ EditMsg::MayDeleteSelected(..) => {
log!("Deleting link: ", link);
model.last_message = None;
model.edit_link = None;
model.question = Some(link)
}
EditMsg::DeleteSelected(link) => {
orders.perform_cmd(async {
let data = link;
let request = unwrap_or_return!(
Request::new("/admin/json/delete_link/")
.method(Method::Post)
.json(&data),
Msg::SetMessage("serialization failed".to_string())
);
let response =
unwrap_or_return!(fetch(request).await, Msg::Edit(EditMsg::FailedToDeleteLink));
let response = unwrap_or_return!(
response.check_status(),
Msg::SetMessage("Wrong response code!".to_string())
);
let message: Status = unwrap_or_return!(
response.json().await,
Msg::SetMessage(
"Failed to parse the response the link might be deleted however!"
.to_string()
)
);
Msg::Edit(EditMsg::DeletedLink(message))
});
}
EditMsg::FailedToDeleteLink => {
log!("Failed to delete Link");
}
EditMsg::DeletedLink(message) => {
log!("Deleted link", message);
}
}
}
/// Save a link to the server.
fn save_link(model: &Model, orders: &mut impl Orders<Msg>) {
let edit_link = if let Some(e) = model.edit_link.as_ref() {
e
} else {
orders.send_msg(Msg::SetMessage("Please enter a link".to_string()));
return;
};
let data = edit_link.borrow().clone();
orders.perform_cmd(async {
let data = data;
let request = unwrap_or_return!(
Request::new(match data.edit {
EditMode::Create => "/admin/json/create_link/",
EditMode::Edit => "/admin/json/edit_link/",
})
.method(Method::Post)
.json(&data),
Msg::SetMessage("Failed to encode the link!".to_string())
);
let response =
unwrap_or_return!(fetch(request).await, Msg::Edit(EditMsg::FailedToCreateLink));
log!(response);
let response = unwrap_or_return!(
response.check_status(),
Msg::SetMessage("Wrong response code".to_string())
);
let message: Status = unwrap_or_return!(
response.json().await,
Msg::SetMessage("Invalid response!".to_string())
);
Msg::Edit(EditMsg::Created(message))
});
}
/// view the page
/// * messages
/// * questions
/// * the table of links including sorting and searching
#[must_use]
pub fn view(model: &Model) -> Node<Msg> {
let lang = &model.i18n.clone();
let t = move |key: &str| lang.translate(key, None);
section![
if let Some(message) = &model.last_message {
div![
C!["message", "center"],
div![
C!["closebutton"],
a!["\u{d7}"],
ev(Ev::Click, |_| Msg::ClearAll)
],
match message {
Status::Success(m) | Status::Error(m) => &m.message,
}
]
} else {
section![]
},
if let Some(question) = &model.question {
div![
C!["message", "center"],
div![
C!["closebutton"],
a!["\u{d7}"],
ev(Ev::Click, |_| Msg::ClearAll)
],
if let EditMsg::MayDeleteSelected(l) = question.clone() {
div![
lang.translate(
"really-delete",
Some(&fluent_args!["code" => l.code.clone()])
),
a![t("no"), C!["button"], ev(Ev::Click, |_| Msg::ClearAll)],
a![
t("yes"),
C!["button"],
ev(Ev::Click, move |_| Msg::Edit(EditMsg::DeleteSelected(l)))
]
]
} else {
span!()
}
]
} else {
section![]
},
table![
// Add the headlines
view_link_table_head(&t),
// Add filter fields right below the headlines
view_link_table_filter_input(model, &t),
// Add all the content lines
model.links.iter().map(view_link)
],
button![
ev(Ev::Click, |_| Msg::Query(QueryMsg::Fetch)),
"Fetch links"
],
if let Some(l) = &model.edit_link {
edit_or_create_link(l, t)
} else {
section!()
}
]
}
/// Create the headlines of the link table
fn view_link_table_head<F: Fn(&str) -> String>(t: F) -> Node<Msg> {
tr![
th![
ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Code
))),
t("link-code")
],
th![
ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Description
))),
t("link-description")
],
th![
ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Target
))),
t("link-target")
],
th![
ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Author
))),
t("username")
],
th![
ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Statistics
))),
t("statistics")
],
th![]
]
}
/// Create the filter fields in the table columns
fn view_link_table_filter_input<F: Fn(&str) -> String>(model: &Model, t: F) -> Node<Msg> {
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, |s| Msg::Query(QueryMsg::CodeFilterChanged(s))),
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, |s| Msg::Query(
QueryMsg::DescriptionFilterChanged(s)
)),
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, |s| Msg::Query(QueryMsg::TargetFilterChanged(s))),
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, |s| Msg::Query(QueryMsg::AuthorFilterChanged(s))),
el_ref(&model.inputs[LinkOverviewColumns::Author].filter_input),
]],
td![],
td![],
]
}
/// display a single link
fn view_link(l: &FullLink) -> Node<Msg> {
// Ugly hack
let link = LinkDelta::from(l.clone());
let link2 = LinkDelta::from(l.clone());
let link3 = LinkDelta::from(l.clone());
let link4 = LinkDelta::from(l.clone());
let link5 = LinkDelta::from(l.clone());
tr![
{
td![
ev(Ev::Click, |_| Msg::Edit(EditMsg::EditSelected(link))),
&l.link.code
]
},
{
td![
ev(Ev::Click, |_| Msg::Edit(EditMsg::EditSelected(link2))),
&l.link.title
]
},
td![a![attrs![At::Href => &l.link.target], &l.link.target]],
{
td![
ev(Ev::Click, |_| Msg::Edit(EditMsg::EditSelected(link3))),
&l.user.username
]
},
{
td![
ev(Ev::Click, |_| Msg::Edit(EditMsg::EditSelected(link4))),
&l.clicks.number
]
},
{
td![img![
ev(Ev::Click, |_| Msg::Edit(EditMsg::MayDeleteSelected(link5))),
C!["trashicon"],
attrs!(At::Src => "/static/trash.svg")
]]
},
]
}
/// display a link editing dialog with save and close button
fn edit_or_create_link<F: Fn(&str) -> String>(l: &RefCell<LinkDelta>, t: F) -> Node<Msg> {
let link = l.borrow();
div![
C!["editdialog", "center"],
div![
C!["closebutton"],
a!["\u{d7}"],
ev(Ev::Click, |_| Msg::ClearAll)
],
h1![match &link.edit {
EditMode::Edit => t("edit-link"),
EditMode::Create => t("create-link"),
}],
table![
tr![
th![t("link-description")],
td![input![
attrs! {
At::Value => &link.title,
At::Type => "text",
At::Placeholder => t("link-description")
},
input_ev(Ev::Input, |s| {
Msg::Edit(EditMsg::EditDescriptionChanged(s))
}),
]]
],
tr![
th![t("link-target")],
td![input![
attrs! {
At::Value => &link.target,
At::Type => "text",
At::Placeholder => t("link-target")
},
input_ev(Ev::Input, |s| { Msg::Edit(EditMsg::EditTargetChanged(s)) }),
]]
],
tr![
th![t("link-code")],
td![input![
attrs! {
At::Value => &link.code,
At::Type => "text",
At::Placeholder => t("password")
},
input_ev(Ev::Input, |s| { Msg::Edit(EditMsg::EditCodeChanged(s)) }),
],]
]
],
a![
match &link.edit {
EditMode::Edit => t("edit-link"),
EditMode::Create => t("create-link"),
},
C!["button"],
ev(Ev::Click, |_| Msg::Edit(EditMsg::SaveLink))
]
]
}