Adding paged loading of links.
This commit is contained in:
parent
2b276a5130
commit
b11177a943
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -454,6 +454,7 @@ dependencies = [
|
|||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
"unic-langid",
|
"unic-langid",
|
||||||
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -26,4 +26,12 @@ enum-map = "1"
|
|||||||
qrcode = "0.12"
|
qrcode = "0.12"
|
||||||
image = "0.23"
|
image = "0.23"
|
||||||
|
|
||||||
shared = { path = "../shared" }
|
shared = { path = "../shared" }
|
||||||
|
|
||||||
|
[dependencies.web-sys]
|
||||||
|
version = "0.3"
|
||||||
|
features = [
|
||||||
|
"IntersectionObserver",
|
||||||
|
"IntersectionObserverInit",
|
||||||
|
"IntersectionObserverEntry",
|
||||||
|
]
|
@ -42,7 +42,7 @@ impl I18n {
|
|||||||
|
|
||||||
/// Get a localized string. Optionally with parameters provided in `args`.
|
/// Get a localized string. Optionally with parameters provided in `args`.
|
||||||
pub fn translate(&self, key: impl AsRef<str>, args: Option<&FluentArgs>) -> String {
|
pub fn translate(&self, key: impl AsRef<str>, args: Option<&FluentArgs>) -> String {
|
||||||
log!(key.as_ref());
|
// log!(key.as_ref());
|
||||||
let msg = self
|
let msg = self
|
||||||
.ftl_bundle
|
.ftl_bundle
|
||||||
.get_message(key.as_ref())
|
.get_message(key.as_ref())
|
||||||
|
@ -26,8 +26,8 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
|||||||
|
|
||||||
Model {
|
Model {
|
||||||
index: 0,
|
index: 0,
|
||||||
location: Location::new(url.clone()),
|
location: Location::new(url),
|
||||||
page: Page::init(url, orders, lang.clone()),
|
page: Page::NotFound,
|
||||||
i18n: lang,
|
i18n: lang,
|
||||||
user: Loadable::Data(None),
|
user: Loadable::Data(None),
|
||||||
login_form: LoginForm::default(),
|
login_form: LoginForm::default(),
|
||||||
|
@ -9,6 +9,7 @@ use seed::{
|
|||||||
a, attrs, div, h1, img, input, log, nodes, prelude::*, raw, section, span, table, td, th, tr,
|
a, attrs, div, h1, img, input, log, nodes, prelude::*, raw, section, span, table, td, th, tr,
|
||||||
Url, C, IF,
|
Url, C, IF,
|
||||||
};
|
};
|
||||||
|
use web_sys::{IntersectionObserver, IntersectionObserverEntry, IntersectionObserverInit};
|
||||||
|
|
||||||
use shared::{
|
use shared::{
|
||||||
apirequests::general::Ordering,
|
apirequests::general::Ordering,
|
||||||
@ -25,6 +26,7 @@ use crate::{get_host, i18n::I18n, unwrap_or_return};
|
|||||||
pub fn init(mut url: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model {
|
pub fn init(mut url: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model {
|
||||||
// fetch the links to fill the list.
|
// fetch the links to fill the list.
|
||||||
orders.send_msg(Msg::Query(QueryMsg::Fetch));
|
orders.send_msg(Msg::Query(QueryMsg::Fetch));
|
||||||
|
orders.perform_cmd(cmds::timeout(50, || Msg::SetupObserver));
|
||||||
// if the url contains create_link set the edit_link variable.
|
// if the url contains create_link set the edit_link variable.
|
||||||
// This variable then opens the create link dialog.
|
// This variable then opens the create link dialog.
|
||||||
let dialog = match url.next_path_part() {
|
let dialog = match url.next_path_part() {
|
||||||
@ -37,24 +39,35 @@ pub fn init(mut url: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model {
|
|||||||
|
|
||||||
Model {
|
Model {
|
||||||
links: Vec::new(), // will contain the links to display
|
links: Vec::new(), // will contain the links to display
|
||||||
i18n, // to translate
|
load_more: ElRef::new(), // will contain a reference to the load more button to be able to load more links after scrolling to the bottom.
|
||||||
|
i18n, // to translate
|
||||||
formconfig: LinkRequestForm::default(), // when requesting links the form is stored here
|
formconfig: LinkRequestForm::default(), // when requesting links the form is stored here
|
||||||
inputs: EnumMap::default(), // the input fields for the searches
|
inputs: EnumMap::default(), // the input fields for the searches
|
||||||
dialog,
|
dialog,
|
||||||
handle_render: None,
|
handle_render: None,
|
||||||
handle_timeout: None,
|
handle_timeout: None,
|
||||||
|
observer: None, // load more on scroll - this is used to see if the load-more button is completely visible
|
||||||
|
observer_callback: None,
|
||||||
|
observer_entries: None,
|
||||||
|
everything_loaded: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
links: Vec<Cached<FullLink>>, // will contain the links to display
|
links: Vec<Cached<FullLink>>, // will contain the links to display
|
||||||
i18n: I18n, // to translate
|
load_more: ElRef<web_sys::Element>,
|
||||||
formconfig: LinkRequestForm, // when requesting links the form is stored here
|
i18n: I18n, // to translate
|
||||||
|
formconfig: LinkRequestForm, // when requesting links the form is stored here
|
||||||
inputs: EnumMap<LinkOverviewColumns, FilterInput>, // the input fields for the searches
|
inputs: EnumMap<LinkOverviewColumns, FilterInput>, // the input fields for the searches
|
||||||
dialog: Dialog, // User interaction - there can only ever be one dialog open.
|
dialog: Dialog, // User interaction - there can only ever be one dialog open.
|
||||||
handle_render: Option<CmdHandle>, // Rendering qr-codes takes time... it is aborted when this handle is dropped and replaced.
|
handle_render: Option<CmdHandle>, // Rendering qr-codes takes time... it is aborted when this handle is dropped and replaced.
|
||||||
handle_timeout: Option<CmdHandle>, // Rendering qr-codes takes time... it is aborted when this handle is dropped and replaced.
|
handle_timeout: Option<CmdHandle>, // Rendering qr-codes takes time... it is aborted when this handle is dropped and replaced.
|
||||||
|
|
||||||
|
observer: Option<IntersectionObserver>,
|
||||||
|
observer_callback: Option<Closure<dyn Fn(Vec<JsValue>)>>,
|
||||||
|
observer_entries: Option<Vec<IntersectionObserverEntry>>,
|
||||||
|
everything_loaded: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
@ -133,9 +146,11 @@ struct FilterInput {
|
|||||||
/// A message can either edit or query. (or set a dialog)
|
/// A message can either edit or query. (or set a dialog)
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
Query(QueryMsg), // Messages related to querying links
|
Query(QueryMsg), // Messages related to querying links
|
||||||
Edit(EditMsg), // Messages related to editing links
|
Edit(EditMsg), // Messages related to editing links
|
||||||
ClearAll, // Clear all messages
|
ClearAll, // Clear all messages
|
||||||
|
SetupObserver, // Make an observer for endles scroll
|
||||||
|
Observed(Vec<IntersectionObserverEntry>),
|
||||||
SetMessage(String), // Set a message to the user
|
SetMessage(String), // Set a message to the user
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,8 +158,10 @@ pub enum Msg {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum QueryMsg {
|
pub enum QueryMsg {
|
||||||
Fetch,
|
Fetch,
|
||||||
|
FetchAdditional,
|
||||||
OrderBy(LinkOverviewColumns),
|
OrderBy(LinkOverviewColumns),
|
||||||
Received(Vec<FullLink>),
|
Received(Vec<FullLink>),
|
||||||
|
ReceivedAdditional(Vec<FullLink>),
|
||||||
CodeFilterChanged(String),
|
CodeFilterChanged(String),
|
||||||
DescriptionFilterChanged(String),
|
DescriptionFilterChanged(String),
|
||||||
TargetFilterChanged(String),
|
TargetFilterChanged(String),
|
||||||
@ -181,10 +198,54 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
Msg::Query(msg) => process_query_messages(msg, model, orders),
|
Msg::Query(msg) => process_query_messages(msg, model, orders),
|
||||||
Msg::Edit(msg) => process_edit_messages(msg, model, orders),
|
Msg::Edit(msg) => process_edit_messages(msg, model, orders),
|
||||||
Msg::ClearAll => clear_all(model),
|
Msg::ClearAll => clear_all(model),
|
||||||
|
Msg::Observed(entries) => {
|
||||||
|
if let Some(entry) = entries.first() {
|
||||||
|
log!(entry);
|
||||||
|
if entry.is_intersecting() {
|
||||||
|
orders.send_msg(Msg::Query(QueryMsg::FetchAdditional));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Msg::SetMessage(msg) => {
|
Msg::SetMessage(msg) => {
|
||||||
clear_all(model);
|
clear_all(model);
|
||||||
model.dialog = Dialog::Message(Status::Error(Message { message: msg }));
|
model.dialog = Dialog::Message(Status::Error(Message { message: msg }));
|
||||||
}
|
}
|
||||||
|
Msg::SetupObserver => {
|
||||||
|
orders.skip();
|
||||||
|
|
||||||
|
// ---- observer callback ----
|
||||||
|
let sender = orders.msg_sender();
|
||||||
|
let callback = move |entries: Vec<JsValue>| {
|
||||||
|
let entries = entries
|
||||||
|
.into_iter()
|
||||||
|
.map(IntersectionObserverEntry::from)
|
||||||
|
.collect();
|
||||||
|
sender(Some(Msg::Observed(entries)));
|
||||||
|
};
|
||||||
|
let callback = Closure::wrap(Box::new(callback) as Box<dyn Fn(Vec<JsValue>)>);
|
||||||
|
|
||||||
|
// ---- observer options ----
|
||||||
|
let mut options = IntersectionObserverInit::new();
|
||||||
|
options.threshold(&JsValue::from(1));
|
||||||
|
// ---- observer ----
|
||||||
|
log!("Trying to register observer");
|
||||||
|
if let Ok(observer) =
|
||||||
|
IntersectionObserver::new_with_options(callback.as_ref().unchecked_ref(), &options)
|
||||||
|
{
|
||||||
|
if let Some(element) = model.load_more.get() {
|
||||||
|
log!("element registered! ", element);
|
||||||
|
observer.observe(&element);
|
||||||
|
|
||||||
|
// Note: Drop `observer` is not enough. We have to call `observer.disconnect()`.
|
||||||
|
model.observer = Some(observer);
|
||||||
|
model.observer_callback = Some(callback);
|
||||||
|
} else {
|
||||||
|
log!("element not yet registered! ");
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
log!("Failed to get observer!")
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +254,11 @@ pub fn process_query_messages(msg: QueryMsg, model: &mut Model, orders: &mut imp
|
|||||||
match msg {
|
match msg {
|
||||||
QueryMsg::Fetch => {
|
QueryMsg::Fetch => {
|
||||||
orders.skip(); // No need to rerender
|
orders.skip(); // No need to rerender
|
||||||
load_links(model, orders)
|
initial_load(model, orders)
|
||||||
|
}
|
||||||
|
QueryMsg::FetchAdditional => {
|
||||||
|
orders.skip(); // No need to rerender
|
||||||
|
consecutive_load(model, orders)
|
||||||
}
|
}
|
||||||
// Default to ascending ordering but if the links are already sorted according to this collumn toggle between ascending and descending ordering.
|
// Default to ascending ordering but if the links are already sorted according to this collumn toggle between ascending and descending ordering.
|
||||||
QueryMsg::OrderBy(column) => {
|
QueryMsg::OrderBy(column) => {
|
||||||
@ -247,6 +312,20 @@ pub fn process_query_messages(msg: QueryMsg, model: &mut Model, orders: &mut imp
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
QueryMsg::ReceivedAdditional(response) => {
|
||||||
|
if response.len() < model.formconfig.amount {
|
||||||
|
log!("There are no more links! ");
|
||||||
|
model.everything_loaded = true
|
||||||
|
};
|
||||||
|
let mut new_links = response
|
||||||
|
.into_iter()
|
||||||
|
.map(|l| {
|
||||||
|
let cache = generate_qr_from_code(&l.link.code);
|
||||||
|
Cached { data: l, cache }
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
model.links.append(&mut new_links);
|
||||||
|
}
|
||||||
QueryMsg::CodeFilterChanged(s) => {
|
QueryMsg::CodeFilterChanged(s) => {
|
||||||
log!("Filter is: ", &s);
|
log!("Filter is: ", &s);
|
||||||
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
|
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
|
||||||
@ -274,9 +353,19 @@ pub fn process_query_messages(msg: QueryMsg, model: &mut Model, orders: &mut imp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn initial_load(model: &Model, orders: &mut impl Orders<Msg>) {
|
||||||
|
let mut data = model.formconfig.clone();
|
||||||
|
data.offset = 0;
|
||||||
|
load_links(orders, data);
|
||||||
|
}
|
||||||
|
fn consecutive_load(model: &Model, orders: &mut impl Orders<Msg>) {
|
||||||
|
let mut data = model.formconfig.clone();
|
||||||
|
data.offset = model.links.len();
|
||||||
|
load_links(orders, data);
|
||||||
|
}
|
||||||
|
|
||||||
/// Perform a request to the server to load the links from the server.
|
/// Perform a request to the server to load the links from the server.
|
||||||
fn load_links(model: &Model, orders: &mut impl Orders<Msg>) {
|
fn load_links(orders: &mut impl Orders<Msg>, data: LinkRequestForm) {
|
||||||
let data = model.formconfig.clone();
|
|
||||||
orders.perform_cmd(async {
|
orders.perform_cmd(async {
|
||||||
let data = data;
|
let data = data;
|
||||||
// create a request
|
// create a request
|
||||||
@ -302,7 +391,11 @@ fn load_links(model: &Model, orders: &mut impl Orders<Msg>) {
|
|||||||
Msg::SetMessage("Invalid response".to_string())
|
Msg::SetMessage("Invalid response".to_string())
|
||||||
);
|
);
|
||||||
// The message that is sent by perform_cmd after this async block is completed
|
// The message that is sent by perform_cmd after this async block is completed
|
||||||
Msg::Query(QueryMsg::Received(links))
|
match data.offset.cmp(&0) {
|
||||||
|
std::cmp::Ordering::Less => unreachable!(),
|
||||||
|
std::cmp::Ordering::Equal => Msg::Query(QueryMsg::Received(links)),
|
||||||
|
std::cmp::Ordering::Greater => Msg::Query(QueryMsg::ReceivedAdditional(links)),
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -530,6 +623,17 @@ pub fn view(model: &Model, logged_in_user: &User) -> Node<Msg> {
|
|||||||
// Add all the content lines
|
// Add all the content lines
|
||||||
model.links.iter().map(|l| { view_link(l, logged_in_user) })
|
model.links.iter().map(|l| { view_link(l, logged_in_user) })
|
||||||
],
|
],
|
||||||
|
if not(model.everything_loaded) {
|
||||||
|
a![
|
||||||
|
C!["loadmore", "button"],
|
||||||
|
el_ref(&model.load_more),
|
||||||
|
ev(Ev::Click, move |_| Msg::Query(QueryMsg::FetchAdditional)),
|
||||||
|
img![C!["reloadicon"], attrs!(At::Src => "/static/reload.svg")],
|
||||||
|
t("load-more-links")
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
a![C!["loadmore", "button"], t("no-more-links")]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,8 @@ language = Sprache:
|
|||||||
|
|
||||||
not-found = Dieser Link existiert nicht, oder wurde gelöscht.
|
not-found = Dieser Link existiert nicht, oder wurde gelöscht.
|
||||||
|
|
||||||
|
load-more-links = Lade mehr Links
|
||||||
|
no-more-links = Es gibt keine weiteren Links
|
||||||
edit-link-headline = Zu editierender Link: {$linktitle}
|
edit-link-headline = Zu editierender Link: {$linktitle}
|
||||||
edit-link = Link Editieren
|
edit-link = Link Editieren
|
||||||
create-link = Link Erstellen
|
create-link = Link Erstellen
|
||||||
|
@ -12,6 +12,8 @@ language = Language:
|
|||||||
|
|
||||||
not-found = This Link has not been found or has been deleted
|
not-found = This Link has not been found or has been deleted
|
||||||
|
|
||||||
|
load-more-links = load more links
|
||||||
|
no-more-links = there are no additional links available
|
||||||
edit-link-headline = Edit link: {$linktitle}
|
edit-link-headline = Edit link: {$linktitle}
|
||||||
edit-link = Edit link
|
edit-link = Edit link
|
||||||
create-link = Create link
|
create-link = Create link
|
||||||
|
@ -131,7 +131,7 @@ impl UserDbOperations<Self> for User {
|
|||||||
#[instrument()]
|
#[instrument()]
|
||||||
async fn toggle_admin(self, server_config: &ServerConfig) -> Result<(), ServerError> {
|
async fn toggle_admin(self, server_config: &ServerConfig) -> Result<(), ServerError> {
|
||||||
let new_role = match self.role {
|
let new_role = match self.role {
|
||||||
r @ Role::NotAuthenticated | r @ Role::Disabled => r,
|
r @ (Role::NotAuthenticated | Role::Disabled) => r,
|
||||||
Role::Regular => Role::Admin,
|
Role::Regular => Role::Admin,
|
||||||
Role::Admin => Role::Regular,
|
Role::Admin => Role::Regular,
|
||||||
};
|
};
|
||||||
|
@ -113,6 +113,7 @@ pub async fn list_all_allowed(
|
|||||||
querystring.push_str(&generate_order_sql(&order));
|
querystring.push_str(&generate_order_sql(&order));
|
||||||
}
|
}
|
||||||
querystring.push_str(&format!("\n LIMIT {}", parameters.amount));
|
querystring.push_str(&format!("\n LIMIT {}", parameters.amount));
|
||||||
|
querystring.push_str(&format!("\n OFFSET {}", parameters.offset));
|
||||||
info!("{}", querystring);
|
info!("{}", querystring);
|
||||||
|
|
||||||
let links = sqlx::query(&querystring)
|
let links = sqlx::query(&querystring)
|
||||||
|
@ -8,21 +8,23 @@ form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.center {
|
.center {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
width: 600px;
|
width: 600px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: 400px;
|
height: 500px;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
margin-left: -300px;
|
margin-left: -300px;
|
||||||
margin-top: -200px;
|
margin-top: -250px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
overflow-y: scroll;
|
||||||
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.center input {
|
.center input {
|
||||||
@ -229,4 +231,14 @@ img.trashicon {
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
margin: 3px;
|
margin: 3px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.loadmore {
|
||||||
|
display: flex;
|
||||||
|
color: darkgray;
|
||||||
|
margin: auto;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
border: none;
|
||||||
|
border-radius: 15px;
|
||||||
}
|
}
|
3
pslink/static/reload.svg
Normal file
3
pslink/static/reload.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="48" height="48" version="1.1" viewBox="0 0 12.7 12.7" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path transform="rotate(220.03)" d="m-6.5691-4.5466c-1.7227-1.0829-3.96-0.86298-5.4388 0.53473-1.4788 1.3977-1.8246 3.619-0.84044 5.4 0.98412 1.781 3.0487 2.6705 5.019 2.1624 1.9704-0.50816 3.3472-2.2852 3.3472-4.32l-1.902 1.7128 1.902-1.7128 0.72362 2.5702" style="fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.1607;stroke:#a8a8a8"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 467 B |
@ -12,6 +12,7 @@ use super::general::{EditMode, Filter, Operation, Ordering};
|
|||||||
pub struct LinkRequestForm {
|
pub struct LinkRequestForm {
|
||||||
pub filter: EnumMap<LinkOverviewColumns, Filter>,
|
pub filter: EnumMap<LinkOverviewColumns, Filter>,
|
||||||
pub order: Option<Operation<LinkOverviewColumns, Ordering>>,
|
pub order: Option<Operation<LinkOverviewColumns, Ordering>>,
|
||||||
|
pub offset: usize,
|
||||||
pub amount: usize,
|
pub amount: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,7 +21,8 @@ impl Default for LinkRequestForm {
|
|||||||
Self {
|
Self {
|
||||||
filter: EnumMap::default(),
|
filter: EnumMap::default(),
|
||||||
order: None,
|
order: None,
|
||||||
amount: 20,
|
offset: 0,
|
||||||
|
amount: 60,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user