Skip to content

Commit ed1e11e

Browse files
authored
Merge pull request #145 from rambit-systems/push-tpkluuntmztl
Incremental UI Push #26
2 parents 91ef614 + a021673 commit ed1e11e

File tree

10 files changed

+308
-190
lines changed

10 files changed

+308
-190
lines changed

crates/grid/src/handlers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pub fn context_provider(
5959
move || {
6060
provide_context(app_state.prime_domain.clone());
6161
provide_context(app_state.auth_domain.clone());
62+
provide_context(auth_session.clone());
6263
if let Some(auth_user) = auth_session.user.clone() {
6364
provide_context(auth_user);
6465
}

crates/site-app/src/hooks.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
//! Hooks are reusable pieces of reactive logic extracted from render contexts.
22
33
mod cache_hook;
4+
mod create_cache_hook;
45
mod entry_hook;
56
mod login_hook;
67
mod org_hook;
78
mod signup_hook;
89

910
// pub use self::cache_hook::*;
10-
pub use self::{entry_hook::*, login_hook::*, org_hook::*, signup_hook::*};
11+
pub use self::{
12+
create_cache_hook::*, entry_hook::*, login_hook::*, org_hook::*,
13+
signup_hook::*,
14+
};
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
use leptos::{ev::Event, prelude::*};
2+
use leptos_fetch::QueryClient;
3+
use models::{
4+
dvf::{EntityName, RecordId, StrictSlug, Visibility},
5+
Cache, Org,
6+
};
7+
8+
use super::OrgHook;
9+
use crate::{
10+
components::InputIcon, navigation::navigate_to,
11+
reactive_utils::touched_input_bindings,
12+
};
13+
14+
pub struct CreateCacheHook {
15+
org_hook: OrgHook,
16+
name_signal: RwSignal<String>,
17+
visibility_signal: RwSignal<Visibility>,
18+
sanitized_name_memo: Memo<Option<EntityName>>,
19+
name_after_icon_memo: Memo<Option<InputIcon>>,
20+
is_available_resource: LocalResource<Option<Result<bool, ServerFnError>>>,
21+
action: ServerAction<CreateCache>,
22+
}
23+
24+
impl CreateCacheHook {
25+
pub fn new() -> Self {
26+
let org_hook = OrgHook::new_requested();
27+
28+
let name_signal = RwSignal::new(String::new());
29+
let sanitized_name_memo = Memo::new(move |_| {
30+
Some(EntityName::new(StrictSlug::new(name_signal())))
31+
.filter(|n| !n.to_string().is_empty())
32+
});
33+
let visibility_signal = RwSignal::new(Visibility::Private);
34+
35+
let query_client = expect_context::<QueryClient>();
36+
let is_available_key_fn =
37+
move || sanitized_name_memo().map(|n| n.to_string());
38+
let is_available_query_scope =
39+
crate::resources::cache::cache_name_is_available_query_scope();
40+
let is_available_resource = expect_context::<QueryClient>()
41+
.local_resource(is_available_query_scope.clone(), is_available_key_fn);
42+
let is_available_fetching = query_client
43+
.subscribe_is_fetching(is_available_query_scope, is_available_key_fn);
44+
45+
let name_after_icon_memo = Memo::new(move |_| {
46+
match (is_available_fetching(), is_available_resource.get()) {
47+
(true, _) => Some(InputIcon::Loading),
48+
(_, Some(Some(Ok(true)))) => Some(InputIcon::Check),
49+
(_, Some(Some(Ok(false)))) => Some(InputIcon::XMark),
50+
_ => None,
51+
}
52+
});
53+
54+
let action = ServerAction::<CreateCache>::new();
55+
56+
Self {
57+
org_hook,
58+
name_signal,
59+
visibility_signal,
60+
sanitized_name_memo,
61+
name_after_icon_memo,
62+
is_available_resource,
63+
action,
64+
}
65+
}
66+
67+
pub fn name_after_icon(&self) -> Memo<Option<InputIcon>> {
68+
self.name_after_icon_memo
69+
}
70+
71+
pub fn name_warn_hint(&self) -> Signal<Option<String>> {
72+
let (name_signal, sanitized_name_memo) =
73+
(self.name_signal, self.sanitized_name_memo);
74+
Signal::derive(move || {
75+
let (name, Some(sanitized_name)) =
76+
(name_signal.get(), sanitized_name_memo())
77+
else {
78+
return None;
79+
};
80+
if name != sanitized_name.clone().to_string() {
81+
return Some(format!(
82+
"This name will be converted to \"{sanitized_name}\"."
83+
));
84+
}
85+
None
86+
})
87+
}
88+
89+
pub fn name_error_hint(&self) -> Signal<Option<String>> {
90+
let (is_available_resource, sanitized_name_memo) =
91+
(self.is_available_resource, self.sanitized_name_memo);
92+
Signal::derive(move || {
93+
match (is_available_resource.get(), sanitized_name_memo()) {
94+
(Some(Some(Ok(false))), Some(sanitized_name)) => {
95+
Some(format!("The name \"{sanitized_name}\" is unavailable."))
96+
}
97+
(Some(Some(Err(_))), _) => {
98+
Some("Sorry, something went wrong.".to_owned())
99+
}
100+
_ => None,
101+
}
102+
})
103+
}
104+
105+
pub fn name_bindings(&self) -> (Callback<(), String>, Callback<Event>) {
106+
touched_input_bindings(self.name_signal)
107+
}
108+
109+
pub fn visibility_signal(&self) -> RwSignal<Visibility> {
110+
self.visibility_signal
111+
}
112+
113+
pub fn show_spinner(&self) -> Signal<bool> {
114+
let (pending, value) = (self.action.pending(), self.action.value());
115+
// show if the action is loading or completed successfully
116+
Signal::derive(move || pending() || matches!(value.get(), Some(Ok(_))))
117+
}
118+
119+
pub fn action_trigger(&self) -> Callback<()> {
120+
let (
121+
org,
122+
visibility_signal,
123+
sanitized_name_memo,
124+
is_available_resource,
125+
action,
126+
) = (
127+
self.org_hook.key(),
128+
self.visibility_signal,
129+
self.sanitized_name_memo,
130+
self.is_available_resource,
131+
self.action,
132+
);
133+
Callback::new(move |()| {
134+
// the name has been checked and is available
135+
if sanitized_name_memo().is_some()
136+
&& matches!(is_available_resource.get(), Some(Some(Ok(true))))
137+
{
138+
action.dispatch_local(CreateCache {
139+
org: org(),
140+
name: sanitized_name_memo().unwrap().to_string(),
141+
visibility: visibility_signal(),
142+
});
143+
}
144+
})
145+
}
146+
147+
pub fn create_redirect_effect(&self) -> Effect<LocalStorage> {
148+
let (dashboard_url, action) = (self.org_hook.dashboard_url(), self.action);
149+
Effect::new(move || {
150+
if matches!(action.value().get(), Some(Ok(_))) {
151+
navigate_to(&dashboard_url());
152+
}
153+
})
154+
}
155+
}
156+
157+
#[server(prefix = "/api/sfn")]
158+
pub async fn create_cache(
159+
org: RecordId<Org>,
160+
name: String,
161+
visibility: Visibility,
162+
) -> Result<RecordId<Cache>, ServerFnError> {
163+
use prime_domain::PrimeDomainService;
164+
165+
crate::resources::authorize_for_org(org)?;
166+
167+
let prime_domain_service: PrimeDomainService = expect_context();
168+
169+
let sanitized_name = EntityName::new(StrictSlug::new(name.clone()));
170+
if name != sanitized_name.clone().to_string() {
171+
return Err(ServerFnError::new("name is unsanitized"));
172+
}
173+
174+
prime_domain_service
175+
.create_cache(org, sanitized_name, visibility)
176+
.await
177+
.map_err(|e| {
178+
tracing::error!("failed to create cache: {e}");
179+
ServerFnError::new("internal error")
180+
})
181+
}

crates/site-app/src/hooks/login_hook.rs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@ impl LoginHook {
6464
touched_input_bindings(self.password_signal)
6565
}
6666

67-
pub fn email_error_hint(&self) -> impl Fn() -> Option<String> {
67+
pub fn email_error_hint(&self) -> Signal<Option<String>> {
6868
let (email_signal, submit_touched_signal) =
6969
(self.email_signal, self.submit_touched_signal);
70-
move || {
70+
Signal::derive(move || {
7171
let email = email_signal.get();
7272
if !submit_touched_signal() {
7373
return None;
@@ -84,13 +84,13 @@ impl LoginHook {
8484
Some("That email address doesn't look right.".into())
8585
}
8686
}
87-
}
87+
})
8888
}
8989

90-
pub fn password_error_hint(&self) -> impl Fn() -> Option<String> {
90+
pub fn password_error_hint(&self) -> Signal<Option<String>> {
9191
let (password_signal, submit_touched_signal) =
9292
(self.password_signal, self.submit_touched_signal);
93-
move || {
93+
Signal::derive(move || {
9494
let password = password_signal.get();
9595
if !submit_touched_signal() {
9696
return None;
@@ -99,42 +99,42 @@ impl LoginHook {
9999
return Some("Password required.".into());
100100
}
101101
None
102-
}
102+
})
103103
}
104104

105-
pub fn show_spinner(&self) -> impl Fn() -> bool {
105+
pub fn show_spinner(&self) -> Signal<bool> {
106106
let (pending, value) = (self.action.pending(), self.action.value());
107107
// show if the action is loading or completed successfully
108-
move || pending() || matches!(value.get(), Some(Ok(true)))
108+
Signal::derive(move || pending() || matches!(value.get(), Some(Ok(true))))
109109
}
110110

111-
pub fn button_text(&self) -> impl Fn() -> &'static str {
111+
pub fn button_text(&self) -> Signal<&'static str> {
112112
let (pending, value) = (self.action.pending(), self.action.value());
113-
move || match (value.get(), pending()) {
113+
Signal::derive(move || match (value.get(), pending()) {
114114
// if the action is loading at all
115115
(_, true) => "Loading...",
116116
// if it's completed successfully
117117
(Some(Ok(true)), _) => "Redirecting...",
118118
// any other state
119119
_ => "Log In",
120-
}
120+
})
121121
}
122122

123-
pub fn action_trigger(&self) -> impl Fn() {
123+
pub fn action_trigger(&self) -> Callback<()> {
124124
let submit_touched_signal = self.submit_touched_signal;
125125
let email_error_hint = self.email_error_hint();
126126
let password_error_hint = self.password_error_hint();
127127
let action = self.action;
128128

129-
move || {
129+
Callback::new(move |()| {
130130
submit_touched_signal.set(true);
131131

132132
if email_error_hint().is_some() || password_error_hint().is_some() {
133133
return;
134134
}
135135

136136
action.dispatch_local(());
137-
}
137+
})
138138
}
139139

140140
pub fn create_redirect_effect(&self) -> Effect<LocalStorage> {

0 commit comments

Comments
 (0)