Panics · P36 Account: handlers in new; gone accounts, workspaces, providers and roles mark none; expiries refuse overflow

This commit is contained in:
ZacharyZhang-NY committed 2026-10-07 05:59:27 -04:00
1 parent ad472490e0
commit e7aec1651e
19 files changed
+476 -518

No files matched your search

+7 -14
View File
@@ -24,17 +24,20 @@ pub struct TwoFactorInput {
id: ElementId,
failed: usize,
busy: bool,
on_code: Option<OnText>,
on_code: OnText,
on_recovery: Option<Run>,
}
impl TwoFactorInput {
pub fn new(id: impl Into<ElementId>) -> Self {
pub fn new(
id: impl Into<ElementId>,
on_code: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
failed: 0,
busy: false,
on_code: None,
on_code: Rc::new(on_code),
on_recovery: None,
}
}
@@ -51,14 +54,6 @@ impl TwoFactorInput {
self
}
pub fn on_code(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_code = Some(Rc::new(handler));
self
}
/// Shows "Use a recovery code".
pub fn on_recovery(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_recovery = Some(Rc::new(handler));
@@ -69,9 +64,7 @@ impl TwoFactorInput {
impl RenderOnce for TwoFactorInput {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id;
let on_code = self
.on_code
.unwrap_or_else(|| panic!("two-factor input {id:?} has no on_code"));
let on_code = self.on_code;
let theme = cx.theme();
let note = match (self.busy, self.failed) {
(true, _) => Some(
+16 -39
View File
@@ -33,20 +33,26 @@ pub struct ApiKeyManager {
id: ElementId,
keys: Vec<ApiKey>,
secret: Option<SharedString>,
on_create: Option<OnKey>,
on_revoke: Option<OnKey>,
on_dismiss: Option<Run>,
on_create: OnKey,
on_revoke: OnKey,
on_dismiss: Run,
}
impl ApiKeyManager {
pub fn new(id: impl Into<ElementId>, keys: impl IntoIterator<Item = ApiKey>) -> Self {
pub fn new(
id: impl Into<ElementId>,
keys: impl IntoIterator<Item = ApiKey>,
on_create: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
on_revoke: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
on_dismiss: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
keys: keys.into_iter().collect(),
secret: None,
on_create: None,
on_revoke: None,
on_dismiss: None,
on_create: Rc::new(on_create),
on_revoke: Rc::new(on_revoke),
on_dismiss: Rc::new(on_dismiss),
}
}
@@ -55,40 +61,13 @@ impl ApiKeyManager {
self.secret = Some(secret.into());
self
}
/// Runs with the new key's name.
pub fn on_create(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_create = Some(Rc::new(handler));
self
}
pub fn on_revoke(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_revoke = Some(Rc::new(handler));
self
}
/// Runs on Done under a secret; the owner clears it.
pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_dismiss = Some(Rc::new(handler));
self
}
}
impl RenderOnce for ApiKeyManager {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id;
let on_create = self
.on_create
.unwrap_or_else(|| panic!("api key manager {id:?} has no on_create"));
let on_revoke = self
.on_revoke
.unwrap_or_else(|| panic!("api key manager {id:?} has no on_revoke"));
let on_create = self.on_create;
let on_revoke = self.on_revoke;
let name = field(&id, "name", "A name, such as Deploys", false, window, cx);
let named: SharedString = name.read(cx).text().trim().to_string().into();
let ready = !named.is_empty();
@@ -102,9 +81,7 @@ impl RenderOnce for ApiKeyManager {
let theme = cx.theme();
let mono = theme.mono_family.clone();
let secret = self.secret.map(|secret| {
let dismiss = self.on_dismiss.clone().unwrap_or_else(|| {
panic!("api key manager {id:?} shows a secret with no on_dismiss")
});
let dismiss = self.on_dismiss.clone();
Callout::new(Severity::Warning)
.title("Copy this key now. It will not show again.")
.child(
+7 -11
View File
@@ -67,18 +67,21 @@ pub struct LoginForm {
id: ElementId,
error: Option<SharedString>,
busy: bool,
on_submit: Option<OnLogin>,
on_submit: OnLogin,
on_forgot: Option<Run>,
on_sign_up: Option<Run>,
}
impl LoginForm {
pub fn new(id: impl Into<ElementId>) -> Self {
pub fn new(
id: impl Into<ElementId>,
on_submit: impl Fn(&Login, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
error: None,
busy: false,
on_submit: None,
on_submit: Rc::new(on_submit),
on_forgot: None,
on_sign_up: None,
}
@@ -96,11 +99,6 @@ impl LoginForm {
self
}
pub fn on_submit(mut self, handler: impl Fn(&Login, &mut Window, &mut App) + 'static) -> Self {
self.on_submit = Some(Rc::new(handler));
self
}
/// Shows "Forgot password?".
pub fn on_forgot(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_forgot = Some(Rc::new(handler));
@@ -127,9 +125,7 @@ impl RenderOnce for LoginForm {
};
let remembered = login.remember;
let ready = is_email(&login.email) && !login.password.is_empty() && !self.busy;
let on_submit = self
.on_submit
.unwrap_or_else(|| panic!("login form {id:?} has no on_submit"));
let on_submit = self.on_submit;
let submit: Run = Rc::new(move |window, cx| {
log::info!("login form: sign in");
on_submit(&login, window, cx);
+13 -26
View File
@@ -33,7 +33,7 @@ struct Step {
words: Words,
sent: bool,
busy: bool,
on_send: Option<OnText>,
on_send: OnText,
back: Way,
other: Way,
}
@@ -52,7 +52,6 @@ fn email_step(step: Step, window: &mut Window, cx: &mut App) -> AnyElement {
let email = field(&id, "email", "name@example.com", false, window, cx);
let address: SharedString = email.read(cx).text().trim().to_string().into();
let ready = is_email(&address) && !busy;
let on_send = on_send.unwrap_or_else(|| panic!("email step {id:?} has no on_send"));
let send: Run = {
let address = address.clone();
Rc::new(move |window, cx| {
@@ -151,17 +150,20 @@ pub struct MagicLinkForm {
id: ElementId,
sent: bool,
busy: bool,
on_send: Option<OnText>,
on_send: OnText,
on_other: Option<Run>,
}
impl MagicLinkForm {
pub fn new(id: impl Into<ElementId>) -> Self {
pub fn new(
id: impl Into<ElementId>,
on_send: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
sent: false,
busy: false,
on_send: None,
on_send: Rc::new(on_send),
on_other: None,
}
}
@@ -178,15 +180,6 @@ impl MagicLinkForm {
self
}
/// Runs with the address, for the first link and each one after.
pub fn on_send(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_send = Some(Rc::new(handler));
self
}
/// Shows "Use another email" once sent; the owner clears `sent`.
pub fn on_other(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_other = Some(Rc::new(handler));
@@ -224,17 +217,20 @@ pub struct ForgotPassword {
id: ElementId,
sent: bool,
busy: bool,
on_send: Option<OnText>,
on_send: OnText,
on_back: Option<Run>,
}
impl ForgotPassword {
pub fn new(id: impl Into<ElementId>) -> Self {
pub fn new(
id: impl Into<ElementId>,
on_send: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
sent: false,
busy: false,
on_send: None,
on_send: Rc::new(on_send),
on_back: None,
}
}
@@ -251,15 +247,6 @@ impl ForgotPassword {
self
}
/// Runs with the address, for the first link and each one after.
pub fn on_send(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_send = Some(Rc::new(handler));
self
}
/// Shows "Back to sign in".
pub fn on_back(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_back = Some(Rc::new(handler));
+9 -17
View File
@@ -14,7 +14,7 @@ pub struct OAuthButtons {
id: ElementId,
providers: Vec<(SharedString, SharedString)>,
busy: Option<SharedString>,
on_pick: Option<OnKey>,
on_pick: OnKey,
}
impl OAuthButtons {
@@ -22,6 +22,7 @@ impl OAuthButtons {
pub fn new(
id: impl Into<ElementId>,
providers: impl IntoIterator<Item = (impl Into<SharedString>, impl Into<SharedString>)>,
on_pick: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
let id = id.into();
let providers: Vec<_> = providers
@@ -33,36 +34,27 @@ impl OAuthButtons {
id,
providers,
busy: None,
on_pick: None,
on_pick: Rc::new(on_pick),
}
}
/// The provider on its way.
pub fn busy(mut self, key: impl Into<SharedString>) -> Self {
let key = key.into();
assert!(
self.providers.iter().any(|(each, _)| *each == key),
"oauth buttons: no provider {key}"
);
if !self.providers.iter().any(|(each, _)| *each == key) {
log::error!("oauth buttons: no provider {key}; none busy");
self.busy = None;
return self;
}
self.busy = Some(key);
self
}
pub fn on_pick(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_pick = Some(Rc::new(handler));
self
}
}
impl RenderOnce for OAuthButtons {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let id = self.id;
let on_pick = self
.on_pick
.unwrap_or_else(|| panic!("oauth buttons {id:?} have no on_pick"));
let on_pick = self.on_pick;
let busy = self.busy;
div()
.flex()
+18 -19
View File
@@ -97,11 +97,12 @@ pub fn expiry(text: &str, now: (i32, i8)) -> Result<(i8, i32), ExpiryError> {
.trim()
.parse::<i8>()
.map_err(|_| ExpiryError::Unread)?;
let year = 2000
+ year
.trim()
.parse::<i32>()
.map_err(|_| ExpiryError::Unread)?;
let year = year
.trim()
.parse::<i32>()
.ok()
.and_then(|year| year.checked_add(2000))
.ok_or(ExpiryError::Unread)?;
if !(1..=12).contains(&month) {
return Err(ExpiryError::NoMonth(month));
}
@@ -146,16 +147,19 @@ pub struct PaymentMethodForm {
id: ElementId,
error: Option<SharedString>,
busy: bool,
on_submit: Option<OnCard>,
on_submit: OnCard,
}
impl PaymentMethodForm {
pub fn new(id: impl Into<ElementId>) -> Self {
pub fn new(
id: impl Into<ElementId>,
on_submit: impl Fn(&CardDetails, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
error: None,
busy: false,
on_submit: None,
on_submit: Rc::new(on_submit),
}
}
@@ -170,22 +174,12 @@ impl PaymentMethodForm {
self.busy = busy;
self
}
pub fn on_submit(
mut self,
handler: impl Fn(&CardDetails, &mut Window, &mut App) + 'static,
) -> Self {
self.on_submit = Some(Rc::new(handler));
self
}
}
impl RenderOnce for PaymentMethodForm {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id;
let on_submit = self
.on_submit
.unwrap_or_else(|| panic!("payment method form {id:?} has no on_submit"));
let on_submit = self.on_submit;
let number = field(&id, "number", "", false, window, cx);
let until = field(&id, "expiry", "", false, window, cx);
let code = field(&id, "cvc", "", false, window, cx);
@@ -315,6 +309,11 @@ mod tests {
"this month still counts"
);
assert_eq!(expiry("12/31", now), Ok((12, 2031)));
assert_eq!(
expiry("12/2147483647", now),
Err(ExpiryError::Unread),
"a year past any clock"
);
assert_eq!(expiry("08/26", now), Err(ExpiryError::Passed));
assert_eq!(expiry("13/30", now), Err(ExpiryError::NoMonth(13)));
assert_eq!(expiry("1230", now), Err(ExpiryError::Unread));
+8 -11
View File
@@ -212,30 +212,27 @@ fn savable(edited: &Profile, seed: &Profile) -> bool {
pub struct ProfileEditor {
id: ElementId,
profile: Profile,
on_save: Option<OnProfile>,
on_save: OnProfile,
}
impl ProfileEditor {
pub fn new(id: impl Into<ElementId>, profile: Profile) -> Self {
pub fn new(
id: impl Into<ElementId>,
profile: Profile,
on_save: impl Fn(&Profile, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
profile,
on_save: None,
on_save: Rc::new(on_save),
}
}
pub fn on_save(mut self, handler: impl Fn(&Profile, &mut Window, &mut App) + 'static) -> Self {
self.on_save = Some(Rc::new(handler));
self
}
}
impl RenderOnce for ProfileEditor {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let (id, profile) = (self.id, self.profile);
let on_save = self
.on_save
.unwrap_or_else(|| panic!("profile editor {id:?} has no on_save"));
let on_save = self.on_save;
let fields = window.use_keyed_state((id.clone(), "fields"), cx, |window, cx| {
Fields::filled(&profile, window, cx)
});
+8 -14
View File
@@ -33,12 +33,16 @@ pub struct Session {
pub struct SessionList {
id: ElementId,
sessions: Vec<Session>,
on_sign_out: Option<OnKey>,
on_sign_out: OnKey,
on_sign_out_others: Option<Run>,
}
impl SessionList {
pub fn new(id: impl Into<ElementId>, sessions: impl IntoIterator<Item = Session>) -> Self {
pub fn new(
id: impl Into<ElementId>,
sessions: impl IntoIterator<Item = Session>,
on_sign_out: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
let mut sessions: Vec<_> = sessions.into_iter().collect();
assert!(
sessions.iter().filter(|session| session.current).count() <= 1,
@@ -48,19 +52,11 @@ impl SessionList {
Self {
id: id.into(),
sessions,
on_sign_out: None,
on_sign_out: Rc::new(on_sign_out),
on_sign_out_others: None,
}
}
pub fn on_sign_out(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_sign_out = Some(Rc::new(handler));
self
}
/// Shows "Sign out of all other devices" while there are others.
pub fn on_sign_out_others(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_sign_out_others = Some(Rc::new(handler));
@@ -71,9 +67,7 @@ impl SessionList {
impl RenderOnce for SessionList {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id;
let on_sign_out = self
.on_sign_out
.unwrap_or_else(|| panic!("session list {id:?} has no on_sign_out"));
let on_sign_out = self.on_sign_out;
let others = self.sessions.iter().any(|session| !session.current);
let theme = cx.theme();
let rows = self.sessions.into_iter().enumerate().map(|(ix, session)| {
+7 -11
View File
@@ -40,18 +40,21 @@ pub struct SignupForm {
terms: Option<SharedString>,
error: Option<SharedString>,
busy: bool,
on_submit: Option<OnSignup>,
on_submit: OnSignup,
on_sign_in: Option<Run>,
}
impl SignupForm {
pub fn new(id: impl Into<ElementId>) -> Self {
pub fn new(
id: impl Into<ElementId>,
on_submit: impl Fn(&Signup, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
terms: None,
error: None,
busy: false,
on_submit: None,
on_submit: Rc::new(on_submit),
on_sign_in: None,
}
}
@@ -74,11 +77,6 @@ impl SignupForm {
self
}
pub fn on_submit(mut self, handler: impl Fn(&Signup, &mut Window, &mut App) + 'static) -> Self {
self.on_submit = Some(Rc::new(handler));
self
}
/// Shows "Have an account? Sign in".
pub fn on_sign_in(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_sign_in = Some(Rc::new(handler));
@@ -105,9 +103,7 @@ impl RenderOnce for SignupForm {
&& rules.iter().all(|(_, kept)| *kept)
&& (self.terms.is_none() || agreed)
&& !self.busy;
let on_submit = self
.on_submit
.unwrap_or_else(|| panic!("sign-up form {id:?} has no on_submit"));
let on_submit = self.on_submit;
let submit: Run = Rc::new(move |window, cx| {
log::info!("sign-up form: create account");
on_submit(&signup, window, cx);
+40 -63
View File
@@ -109,7 +109,7 @@ pub struct AccountSwitcher {
id: ElementId,
accounts: Vec<Account>,
current: SharedString,
on_select: Option<OnKey>,
on_select: OnKey,
on_add: Option<Run>,
}
@@ -118,30 +118,19 @@ impl AccountSwitcher {
id: impl Into<ElementId>,
accounts: impl IntoIterator<Item = Account>,
current: impl Into<SharedString>,
on_select: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
let (accounts, current): (Vec<_>, SharedString) =
(accounts.into_iter().collect(), current.into());
assert!(
accounts.iter().any(|account| account.key == current),
"account switcher: no account {current}"
);
Self {
id: id.into(),
accounts,
current,
on_select: None,
on_select: Rc::new(on_select),
on_add: None,
}
}
pub fn on_select(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_select = Some(Rc::new(handler));
self
}
/// Shows "Add account".
pub fn on_add(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_add = Some(Rc::new(handler));
@@ -152,9 +141,7 @@ impl AccountSwitcher {
impl RenderOnce for AccountSwitcher {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id;
let on_select = self
.on_select
.unwrap_or_else(|| panic!("account switcher {id:?} has no on_select"));
let on_select = self.on_select;
let items = self.accounts.iter().map(|account| {
let (key, pick) = (account.key.clone(), on_select.clone());
MenuItem::radio(account.email.clone(), account.key == self.current).on_click(
@@ -175,18 +162,22 @@ impl RenderOnce for AccountSwitcher {
let now = self
.accounts
.into_iter()
.find(|account| account.key == self.current)
.expect("the current account");
let mut avatar = Avatar::new((id.clone(), "avatar"), now.name.clone()).size(AvatarSize::Sm);
if let Some(image) = now.image {
.find(|account| account.key == self.current);
if now.is_none() {
log::error!(
"account switcher {id:?}: no account {}; none current",
self.current
);
}
let name = now
.as_ref()
.map_or_else(|| SharedString::from("No account"), |now| now.name.clone());
let mut avatar = Avatar::new((id.clone(), "avatar"), name.clone()).size(AvatarSize::Sm);
if let Some(image) = now.as_ref().and_then(|now| now.image.clone()) {
avatar = avatar.image(image);
}
let trigger = row(
(id.clone(), "trigger"),
avatar,
named(now.name, now.email, cx),
cx,
);
let email = now.map(|now| now.email).unwrap_or_default();
let trigger = row((id.clone(), "trigger"), avatar, named(name, email, cx), cx);
opening(id, &menu, trigger, window, cx)
}
}
@@ -197,7 +188,7 @@ pub struct WorkspaceSwitcher {
id: ElementId,
workspaces: Vec<Workspace>,
current: SharedString,
on_select: Option<OnKey>,
on_select: OnKey,
on_create: Option<Run>,
}
@@ -206,30 +197,19 @@ impl WorkspaceSwitcher {
id: impl Into<ElementId>,
workspaces: impl IntoIterator<Item = Workspace>,
current: impl Into<SharedString>,
on_select: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
let (workspaces, current): (Vec<_>, SharedString) =
(workspaces.into_iter().collect(), current.into());
assert!(
workspaces.iter().any(|each| each.key == current),
"workspace switcher: no workspace {current}"
);
Self {
id: id.into(),
workspaces,
current,
on_select: None,
on_select: Rc::new(on_select),
on_create: None,
}
}
pub fn on_select(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_select = Some(Rc::new(handler));
self
}
/// Shows "Create workspace".
pub fn on_create(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_create = Some(Rc::new(handler));
@@ -240,9 +220,7 @@ impl WorkspaceSwitcher {
impl RenderOnce for WorkspaceSwitcher {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id;
let on_select = self
.on_select
.unwrap_or_else(|| panic!("workspace switcher {id:?} has no on_select"));
let on_select = self.on_select;
let items: Vec<_> = self
.workspaces
.iter()
@@ -267,17 +245,22 @@ impl RenderOnce for WorkspaceSwitcher {
let now = self
.workspaces
.into_iter()
.find(|each| each.key == self.current)
.expect("the current workspace");
let mark = Avatar::new((id.clone(), "mark"), now.name.clone())
.find(|each| each.key == self.current);
if now.is_none() {
log::error!(
"workspace switcher {id:?}: no workspace {}; none current",
self.current
);
}
let name = now.as_ref().map_or_else(
|| SharedString::from("No workspace"),
|now| now.name.clone(),
);
let mark = Avatar::new((id.clone(), "mark"), name.clone())
.square()
.size(AvatarSize::Sm);
let trigger = row(
(id.clone(), "trigger"),
mark,
named(now.name, now.detail, cx),
cx,
);
let detail = now.map(|now| now.detail).unwrap_or_default();
let trigger = row((id.clone(), "trigger"), mark, named(name, detail, cx), cx);
opening(id, &menu, trigger, window, cx)
}
}
@@ -291,7 +274,7 @@ pub struct UserMenu {
image: Option<ImageSource>,
presence: Option<Presence>,
items: Vec<MenuItem>,
on_sign_out: Option<Run>,
on_sign_out: Run,
}
impl UserMenu {
@@ -299,6 +282,7 @@ impl UserMenu {
id: impl Into<ElementId>,
name: impl Into<SharedString>,
email: impl Into<SharedString>,
on_sign_out: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
@@ -307,7 +291,7 @@ impl UserMenu {
image: None,
presence: None,
items: Vec::new(),
on_sign_out: None,
on_sign_out: Rc::new(on_sign_out),
}
}
@@ -326,19 +310,12 @@ impl UserMenu {
self.items.push(item);
self
}
pub fn on_sign_out(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_sign_out = Some(Rc::new(handler));
self
}
}
impl RenderOnce for UserMenu {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id;
let sign_out = self
.on_sign_out
.unwrap_or_else(|| panic!("user menu {id:?} has no on_sign_out"));
let sign_out = self.on_sign_out;
let menu =
Menu::new()
.group(format!("Signed in as {}", self.email), self.items)
+47 -111
View File
@@ -25,11 +25,13 @@ pub struct Role {
pub description: SharedString,
}
fn role_named<'a>(roles: &'a [Role], key: &SharedString, owner: &str) -> &'a Role {
roles
.iter()
.find(|role| role.key == *key)
.unwrap_or_else(|| panic!("{owner}: no role {key}"))
/// The role of `key`; none, logged for `owner`, when the roles lost it.
fn role_named<'a>(roles: &'a [Role], key: &SharedString, owner: &str) -> Option<&'a Role> {
let role = roles.iter().find(|role| role.key == *key);
if role.is_none() {
log::error!("{owner}: no role {key}; none shown");
}
role
}
/// A role to pick, each with what it may do beneath its name.
@@ -39,7 +41,7 @@ pub struct RoleSelector {
roles: Vec<Role>,
selected: SharedString,
disabled: bool,
on_change: Option<OnKey>,
on_change: OnKey,
}
impl RoleSelector {
@@ -47,6 +49,7 @@ impl RoleSelector {
id: impl Into<ElementId>,
roles: impl IntoIterator<Item = Role>,
selected: impl Into<SharedString>,
on_change: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
let (roles, selected): (Vec<_>, SharedString) =
(roles.into_iter().collect(), selected.into());
@@ -56,7 +59,7 @@ impl RoleSelector {
roles,
selected,
disabled: false,
on_change: None,
on_change: Rc::new(on_change),
}
}
@@ -64,22 +67,12 @@ impl RoleSelector {
self.disabled = disabled;
self
}
pub fn on_change(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
}
impl RenderOnce for RoleSelector {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let id = self.id;
let on_change = self
.on_change
.unwrap_or_else(|| panic!("role selector {id:?} has no on_change"));
let on_change = self.on_change;
let choices = self
.roles
.into_iter()
@@ -113,8 +106,8 @@ pub struct TeamMemberTable {
id: ElementId,
members: Vec<Member>,
roles: Vec<Role>,
on_role: Option<OnPair>,
on_remove: Option<OnKey>,
on_role: OnPair,
on_remove: OnKey,
}
impl TeamMemberTable {
@@ -122,48 +115,26 @@ impl TeamMemberTable {
id: impl Into<ElementId>,
members: impl IntoIterator<Item = Member>,
roles: impl IntoIterator<Item = Role>,
on_role: impl Fn(&SharedString, &SharedString, &mut Window, &mut App) + 'static,
on_remove: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
let (members, roles): (Vec<Member>, Vec<Role>) =
(members.into_iter().collect(), roles.into_iter().collect());
for member in members.iter().filter(|member| !member.owner) {
role_named(&roles, &member.role, "team member table");
}
Self {
id: id.into(),
members,
roles,
on_role: None,
on_remove: None,
on_role: Rc::new(on_role),
on_remove: Rc::new(on_remove),
}
}
/// Runs with a member's key and their new role's.
pub fn on_role(
mut self,
handler: impl Fn(&SharedString, &SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_role = Some(Rc::new(handler));
self
}
pub fn on_remove(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_remove = Some(Rc::new(handler));
self
}
}
impl RenderOnce for TeamMemberTable {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id;
let on_role = self
.on_role
.unwrap_or_else(|| panic!("team member table {id:?} has no on_role"));
let on_remove = self
.on_remove
.unwrap_or_else(|| panic!("team member table {id:?} has no on_remove"));
let on_role = self.on_role;
let on_remove = self.on_remove;
let theme = cx.theme();
let rows = self.members.into_iter().enumerate().map(|(ix, member)| {
let mut avatar = Avatar::new(
@@ -189,8 +160,8 @@ impl RenderOnce for TeamMemberTable {
(id.clone(), format!("role-{}", member.key)),
self.roles.clone(),
member.role.clone(),
)
.on_change(move |role, window, cx| set(&changed, role, window, cx));
move |role, window, cx| set(&changed, role, window, cx),
);
let button = ConfirmButton::new(
(id.clone(), format!("remove-{}", member.key)),
"Remove",
@@ -272,9 +243,9 @@ pub struct InvitationList {
id: ElementId,
invitations: Vec<Invitation>,
roles: Vec<Role>,
on_invite: Option<OnPair>,
on_resend: Option<OnKey>,
on_revoke: Option<OnKey>,
on_invite: OnPair,
on_resend: OnKey,
on_revoke: OnKey,
}
impl InvitationList {
@@ -282,63 +253,32 @@ impl InvitationList {
id: impl Into<ElementId>,
invitations: impl IntoIterator<Item = Invitation>,
roles: impl IntoIterator<Item = Role>,
on_invite: impl Fn(&SharedString, &SharedString, &mut Window, &mut App) + 'static,
on_resend: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
on_revoke: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
let (invitations, roles): (Vec<Invitation>, Vec<Role>) = (
invitations.into_iter().collect(),
roles.into_iter().collect(),
);
assert!(!roles.is_empty(), "invitation list: no role to invite as");
for invitation in &invitations {
role_named(&roles, &invitation.role, "invitation list");
}
Self {
id: id.into(),
invitations,
roles,
on_invite: None,
on_resend: None,
on_revoke: None,
on_invite: Rc::new(on_invite),
on_resend: Rc::new(on_resend),
on_revoke: Rc::new(on_revoke),
}
}
/// Runs with the address and the role's key.
pub fn on_invite(
mut self,
handler: impl Fn(&SharedString, &SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_invite = Some(Rc::new(handler));
self
}
pub fn on_resend(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_resend = Some(Rc::new(handler));
self
}
pub fn on_revoke(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_revoke = Some(Rc::new(handler));
self
}
}
impl RenderOnce for InvitationList {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id;
let on_invite = self
.on_invite
.unwrap_or_else(|| panic!("invitation list {id:?} has no on_invite"));
let on_resend = self
.on_resend
.unwrap_or_else(|| panic!("invitation list {id:?} has no on_resend"));
let on_revoke = self
.on_revoke
.unwrap_or_else(|| panic!("invitation list {id:?} has no on_revoke"));
let on_invite = self.on_invite;
let on_resend = self.on_resend;
let on_revoke = self.on_revoke;
let email = field(&id, "email", "name@example.com", false, window, cx);
let first = self.roles[0].key.clone();
let role = window.use_keyed_state((id.clone(), "role"), cx, move |_, _| first);
@@ -346,7 +286,8 @@ impl RenderOnce for InvitationList {
email.read(cx).text().trim().to_string().into(),
role.read(cx).clone(),
);
let ready = is_email(&address);
let offered = self.roles.iter().any(|role| role.key == chosen);
let ready = is_email(&address) && offered;
let clear = email.clone();
let invite = Rc::new(move |window: &mut Window, cx: &mut App| {
log::info!("invitation list: invite as {chosen}");
@@ -358,8 +299,7 @@ impl RenderOnce for InvitationList {
let rows =
self.invitations.iter().map(|invitation| {
let named = role_named(&self.roles, &invitation.role, "invitation list")
.name
.clone();
.map_or_else(|| SharedString::from("No role"), |role| role.name.clone());
let [again, gone] = [(); 2].map(|_| invitation.key.clone());
let (resend, revoke) = (on_resend.clone(), on_revoke.clone());
div()
@@ -448,21 +388,17 @@ impl RenderOnce for InvitationList {
.min_w(theme.label_width())
.child(EmailInput::new(&email)),
)
.child(
div().w(theme.label_width()).child(
RoleSelector::new(
(id.clone(), "invite-role"),
self.roles.clone(),
role.read(cx).clone(),
)
.on_change(move |key, _, cx| {
picked.update(cx, |role, cx| {
*role = key.clone();
cx.notify();
})
}),
),
)
.child(div().w(theme.label_width()).child(RoleSelector::new(
(id.clone(), "invite-role"),
self.roles.clone(),
role.read(cx).clone(),
move |key, _, cx| {
picked.update(cx, |role, cx| {
*role = key.clone();
cx.notify();
})
},
)))
.child(
Button::new((id.clone(), "invite"), "Invite")
.variant(ButtonVariant::Primary)
+37 -26
View File
@@ -21,8 +21,8 @@ fn accounts(_: &Bench, owner: Entity<Bench>) -> AnyElement {
account("home", "Ada", "ada@home.com"),
],
"ada",
move |key, _, cx| say(&owner, key.to_string(), cx),
)
.on_select(move |key, _, cx| say(&owner, key.to_string(), cx))
.into_any_element()
}
@@ -48,10 +48,13 @@ fn sessions(_: &Bench, owner: Entity<Bench>) -> AnyElement {
current,
};
let everyone = owner.clone();
SessionList::new("sessions", [session("phone", false), session("mac", true)])
.on_sign_out(move |key, _, cx| say(&owner, format!("out {key}"), cx))
.on_sign_out_others(move |_, cx| say(&everyone, "others".into(), cx))
.into_any_element()
SessionList::new(
"sessions",
[session("phone", false), session("mac", true)],
move |key, _, cx| say(&owner, format!("out {key}"), cx),
)
.on_sign_out_others(move |_, cx| say(&everyone, "others".into(), cx))
.into_any_element()
}
/// Stops: the phone's Sign out, then Sign out of all other devices; this device has none.
@@ -81,10 +84,13 @@ fn keys(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
used: "Never used".into(),
};
let [made, revoked, done] = [(); 3].map(|_| owner.clone());
let manager = ApiKeyManager::new("keys", [key])
.on_create(move |name, _, cx| say(&made, format!("make {name}"), cx))
.on_revoke(move |key, _, cx| say(&revoked, format!("revoke {key}"), cx))
.on_dismiss(move |_, cx| say(&done, "done".into(), cx));
let manager = ApiKeyManager::new(
"keys",
[key],
move |name, _, cx| say(&made, format!("make {name}"), cx),
move |key, _, cx| say(&revoked, format!("revoke {key}"), cx),
move |_, cx| say(&done, "done".into(), cx),
);
match bench.sent {
true => manager.secret("sk_live_0123456789").into_any_element(),
false => manager.into_any_element(),
@@ -138,11 +144,10 @@ fn profile() -> Profile {
}
fn editor(_: &Bench, owner: Entity<Bench>) -> AnyElement {
ProfileEditor::new("profile", profile())
.on_save(move |profile, _, cx| {
say(&owner, format!("{} {}", profile.name, profile.email), cx)
})
.into_any_element()
ProfileEditor::new("profile", profile(), move |profile, _, cx| {
say(&owner, format!("{} {}", profile.name, profile.email), cx)
})
.into_any_element()
}
/// Stops: the picture, name, username, email, about, location; Discard and Save once changed.
@@ -167,10 +172,11 @@ fn a_profile_saves_only_what_changed_and_discard_puts_it_back(cx: &mut TestAppCo
fn menu(_: &Bench, owner: Entity<Bench>) -> AnyElement {
let [profile, out] = [(); 2].map(|_| owner.clone());
UserMenu::new("user", "Ada", "ada@example.com")
.item(MenuItem::new("Profile").on_click(move |_, cx| say(&profile, "profile".into(), cx)))
.on_sign_out(move |_, cx| say(&out, "out".into(), cx))
.into_any_element()
UserMenu::new("user", "Ada", "ada@example.com", move |_, cx| {
say(&out, "out".into(), cx)
})
.item(MenuItem::new("Profile").on_click(move |_, cx| say(&profile, "profile".into(), cx)))
.into_any_element()
}
/// Stops: the avatar; the menu's rows are Profile, then Sign out.
@@ -191,10 +197,14 @@ fn workspaces(_: &Bench, owner: Entity<Bench>) -> AnyElement {
detail: "Pro".into(),
};
let made = owner.clone();
WorkspaceSwitcher::new("spaces", [space("north"), space("south")], "south")
.on_select(move |key, _, cx| say(&owner, key.to_string(), cx))
.on_create(move |_, cx| say(&made, "create".into(), cx))
.into_any_element()
WorkspaceSwitcher::new(
"spaces",
[space("north"), space("south")],
"south",
move |key, _, cx| say(&owner, key.to_string(), cx),
)
.on_create(move |_, cx| say(&made, "create".into(), cx))
.into_any_element()
}
/// Stops: the switcher; its rows are north, south, then Create workspace.
@@ -222,10 +232,11 @@ fn alone(_: &Bench, owner: Entity<Bench>) -> AnyElement {
current: true,
};
let everyone = owner.clone();
SessionList::new("sessions", [here])
.on_sign_out(move |key, _, cx| say(&owner, format!("out {key}"), cx))
.on_sign_out_others(move |_, cx| say(&everyone, "others".into(), cx))
.into_any_element()
SessionList::new("sessions", [here], move |key, _, cx| {
say(&owner, format!("out {key}"), cx)
})
.on_sign_out_others(move |_, cx| say(&everyone, "others".into(), cx))
.into_any_element()
}
/// With this device alone there is nothing to sign out, so the list has no stop.
+152 -38
View File
@@ -51,23 +51,22 @@ fn a_plan_cancels_on_the_second_press_and_comes_back(cx: &mut TestAppContext) {
}
fn card(_: &Bench, owner: Entity<Bench>) -> AnyElement {
PaymentMethodForm::new("card")
.on_submit(move |card, _, cx| {
say(
&owner,
format!(
"{} {} {}/{} {} {}",
card.brand.name(),
card.number,
card.month,
card.year,
card.cvc,
card.name
),
cx,
)
})
.into_any_element()
PaymentMethodForm::new("card", move |card, _, cx| {
say(
&owner,
format!(
"{} {} {}/{} {} {}",
card.brand.name(),
card.number,
card.month,
card.year,
card.cvc,
card.name
),
cx,
)
})
.into_any_element()
}
/// Stops: number, expiry, code, name, then Save once the card reads.
@@ -116,11 +115,15 @@ fn invites(_: &Bench, owner: Entity<Bench>) -> AnyElement {
expired: false,
};
let [invited, again, gone] = [(); 3].map(|_| owner.clone());
InvitationList::new("invites", [sent], roles())
.on_invite(move |email, role, _, cx| say(&invited, format!("invite {email} {role}"), cx))
.on_resend(move |key, _, cx| say(&again, format!("resend {key}"), cx))
.on_revoke(move |key, _, cx| say(&gone, format!("revoke {key}"), cx))
.into_any_element()
InvitationList::new(
"invites",
[sent],
roles(),
move |email, role, _, cx| say(&invited, format!("invite {email} {role}"), cx),
move |key, _, cx| say(&again, format!("resend {key}"), cx),
move |key, _, cx| say(&gone, format!("revoke {key}"), cx),
)
.into_any_element()
}
/// Stops: the address, the role, then Resend and Revoke while Invite rests.
@@ -149,10 +152,14 @@ fn team(_: &Bench, owner: Entity<Bench>) -> AnyElement {
owner,
};
let removed = owner.clone();
TeamMemberTable::new("team", [member("ada", true), member("bo", false)], roles())
.on_role(move |key, role, _, cx| say(&owner, format!("{key} {role}"), cx))
.on_remove(move |key, _, cx| say(&removed, format!("remove {key}"), cx))
.into_any_element()
TeamMemberTable::new(
"team",
[member("ada", true), member("bo", false)],
roles(),
move |key, role, _, cx| say(&owner, format!("{key} {role}"), cx),
move |key, _, cx| say(&removed, format!("remove {key}"), cx),
)
.into_any_element()
}
/// Stops: bo's role, then bo's Remove; the owner has neither.
@@ -167,11 +174,12 @@ fn a_member_leaves_on_the_second_press_and_the_owner_stays(cx: &mut TestAppConte
fn wall(_: &Bench, owner: Entity<Bench>) -> AnyElement {
let later = owner.clone();
UpgradePrompt::new("wall", "Unlock history")
.benefit("Every message, kept")
.on_upgrade(move |_, cx| say(&owner, "upgrade".into(), cx))
.on_dismiss(move |_, cx| say(&later, "later".into(), cx))
.into_any_element()
UpgradePrompt::new("wall", "Unlock history", move |_, cx| {
say(&owner, "upgrade".into(), cx)
})
.benefit("Every message, kept")
.on_dismiss(move |_, cx| say(&later, "later".into(), cx))
.into_any_element()
}
/// Stops: Upgrade, then Not now.
@@ -186,10 +194,11 @@ fn a_paywall_upgrades_or_waits(cx: &mut TestAppContext) {
}
fn busy_card(_: &Bench, owner: Entity<Bench>) -> AnyElement {
PaymentMethodForm::new("card")
.busy(true)
.on_submit(move |card, _, cx| say(&owner, card.number.to_string(), cx))
.into_any_element()
PaymentMethodForm::new("card", move |card, _, cx| {
say(&owner, card.number.to_string(), cx)
})
.busy(true)
.into_any_element()
}
/// Stops: number, expiry, code, name; Save rests while the owner checks the card.
@@ -354,9 +363,10 @@ fn a_quota_past_its_limit_draws_full(cx: &mut TestAppContext) {
}
fn line(_: &Bench, owner: Entity<Bench>) -> AnyElement {
UpgradePrompt::new("line", "Unlock history")
.on_upgrade(move |_, cx| say(&owner, "upgrade".into(), cx))
.into_any_element()
UpgradePrompt::new("line", "Unlock history", move |_, cx| {
say(&owner, "upgrade".into(), cx)
})
.into_any_element()
}
/// With benefits the prompt is a paywall that lists them.
@@ -372,3 +382,107 @@ fn a_prompt_without_benefits_stands_as_a_line(cx: &mut TestAppContext) {
let (_, cx) = bench(line, cx);
assert!(cx.debug_bounds("paywall-benefits").is_none());
}
/// Accounts, workspaces, a provider and roles that left their lists.
fn departed(_: &Bench, _: Entity<Bench>) -> AnyElement {
let account = crate::account::Account {
key: "ada".into(),
name: "Ada".into(),
email: "ada@work.com".into(),
image: None,
};
let space = crate::account::Workspace {
key: "north".into(),
name: "North".into(),
detail: "Pro".into(),
};
let invitation = Invitation {
key: "bo".into(),
email: "bo@example.com".into(),
role: "gone".into(),
sent: "Sent 2 days ago".into(),
expired: false,
};
let member = Member {
key: "bo".into(),
name: "Bo".into(),
email: "bo@example.com".into(),
role: "gone".into(),
seen: "Active today".into(),
image: None,
owner: false,
};
div()
.w(px(360.0))
.child(crate::account::AccountSwitcher::new(
"accounts",
[account],
"gone",
|_, _, _| {},
))
.child(crate::account::WorkspaceSwitcher::new(
"spaces",
[space],
"gone",
|_, _, _| {},
))
.child(
crate::account::OAuthButtons::new("oauth", [("github", "GitHub")], |_, _, _| {})
.busy("gone"),
)
.child(InvitationList::new(
"invites",
[invitation],
roles(),
|_, _, _, _| {},
|_, _, _| {},
|_, _, _| {},
))
.child(TeamMemberTable::new(
"team",
[member],
roles(),
|_, _, _, _| {},
|_, _, _| {},
))
.into_any_element()
}
#[gpui::test]
fn accounts_workspaces_and_roles_that_left_draw(cx: &mut TestAppContext) {
let (_, cx) = bench(departed, cx);
settle(cx);
}
/// Invitations whose first role leaves once the bench says sent.
fn departing_role(state: &Bench, owner: Entity<Bench>) -> AnyElement {
let offered: Vec<Role> = roles()
.into_iter()
.filter(|role| !state.sent || role.key != "member")
.collect();
InvitationList::new(
"invites",
[],
offered,
move |email, role, _, cx| say(&owner, format!("invite {email} {role}"), cx),
|_, _, _| {},
|_, _, _| {},
)
.into_any_element()
}
#[gpui::test]
fn an_invitation_as_a_role_that_left_sends_nothing(cx: &mut TestAppContext) {
let (host, cx) = bench(departing_role, cx);
tab(1, cx);
write("cy@example.com", cx);
host.update(cx, |state, cx| {
state.sent = true;
cx.notify();
});
settle(cx);
tap("enter", cx);
tab(3, cx);
tap("space", cx);
assert_eq!(said(&host, cx), Vec::<String>::new());
}
+57 -53
View File
@@ -6,16 +6,15 @@ use crate::account::{
};
fn login(_: &Bench, owner: Entity<Bench>) -> AnyElement {
LoginForm::new("login")
.on_submit(move |login, _, cx| {
say(
&owner,
format!("{} {} {}", login.email, login.password, login.remember),
cx,
)
})
.on_forgot(|_, _| {})
.into_any_element()
LoginForm::new("login", move |login, _, cx| {
say(
&owner,
format!("{} {} {}", login.email, login.password, login.remember),
cx,
)
})
.on_forgot(|_, _| {})
.into_any_element()
}
/// Stops: email, password, its eye, the box, the forgotten password, Sign in.
@@ -41,16 +40,15 @@ fn sign_in_waits_for_an_address_and_a_password_and_enter_sends(cx: &mut TestAppC
}
fn signup(_: &Bench, owner: Entity<Bench>) -> AnyElement {
SignupForm::new("signup")
.terms("I agree to the terms")
.on_submit(move |signup, _, cx| {
say(
&owner,
format!("{} {} {}", signup.name, signup.email, signup.password),
cx,
)
})
.into_any_element()
SignupForm::new("signup", move |signup, _, cx| {
say(
&owner,
format!("{} {} {}", signup.name, signup.email, signup.password),
cx,
)
})
.terms("I agree to the terms")
.into_any_element()
}
/// Stops: name, email, password, its eye, the terms, then Create account once it holds.
@@ -80,8 +78,11 @@ fn sign_up_waits_for_every_rule_and_the_terms(cx: &mut TestAppContext) {
}
fn providers(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
let buttons = OAuthButtons::new("oauth", [("github", "GitHub"), ("google", "Google")])
.on_pick(move |key, _, cx| say(&owner, key.to_string(), cx));
let buttons = OAuthButtons::new(
"oauth",
[("github", "GitHub"), ("google", "Google")],
move |key, _, cx| say(&owner, key.to_string(), cx),
);
match bench.sent {
true => buttons.busy("github").into_any_element(),
false => buttons.into_any_element(),
@@ -113,19 +114,18 @@ fn a_provider_hands_its_key_and_rests_while_one_is_on_its_way(cx: &mut TestAppCo
fn magic(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
let other = owner.clone();
MagicLinkForm::new("magic")
.sent(bench.sent)
.on_send(move |address, _, cx| {
say(&owner, format!("send {address}"), cx);
owner.update(cx, |bench, _| bench.sent = true);
MagicLinkForm::new("magic", move |address, _, cx| {
say(&owner, format!("send {address}"), cx);
owner.update(cx, |bench, _| bench.sent = true);
})
.sent(bench.sent)
.on_other(move |_, cx| {
other.update(cx, |bench, cx| {
bench.sent = false;
cx.notify();
})
.on_other(move |_, cx| {
other.update(cx, |bench, cx| {
bench.sent = false;
cx.notify();
})
})
.into_any_element()
})
.into_any_element()
}
/// Stops: email, then the link button; once sent, Send again and Use another email.
@@ -151,11 +151,12 @@ fn a_link_goes_to_the_address_and_again_on_request(cx: &mut TestAppContext) {
fn forgot(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
let back = owner.clone();
ForgotPassword::new("forgot")
.sent(bench.sent)
.on_send(move |address, _, cx| say(&owner, format!("reset {address}"), cx))
.on_back(move |_, cx| say(&back, "back".into(), cx))
.into_any_element()
ForgotPassword::new("forgot", move |address, _, cx| {
say(&owner, format!("reset {address}"), cx)
})
.sent(bench.sent)
.on_back(move |_, cx| say(&back, "back".into(), cx))
.into_any_element()
}
/// Stops: email, then Back to sign in while the send rests.
@@ -172,10 +173,11 @@ fn a_reset_waits_for_an_address_and_offers_the_way_back(cx: &mut TestAppContext)
}
fn factor(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
TwoFactorInput::new("factor")
.failed(bench.failed)
.on_code(move |code, _, cx| say(&owner, code.to_string(), cx))
.into_any_element()
TwoFactorInput::new("factor", move |code, _, cx| {
say(&owner, code.to_string(), cx)
})
.failed(bench.failed)
.into_any_element()
}
/// Stops: the code.
@@ -207,10 +209,11 @@ fn sign_in_waits_for_a_password(cx: &mut TestAppContext) {
}
fn busy_login(_: &Bench, owner: Entity<Bench>) -> AnyElement {
LoginForm::new("login")
.busy(true)
.on_submit(move |login, _, cx| say(&owner, login.email.to_string(), cx))
.into_any_element()
LoginForm::new("login", move |login, _, cx| {
say(&owner, login.email.to_string(), cx)
})
.busy(true)
.into_any_element()
}
/// Stops: email, password, its eye, the box; Sign in rests while busy, so a fifth Tab wraps to the email.
@@ -248,12 +251,13 @@ fn sign_up_waits_for_a_name(cx: &mut TestAppContext) {
fn busy_sent(_: &Bench, owner: Entity<Bench>) -> AnyElement {
let other = owner.clone();
MagicLinkForm::new("magic")
.sent(true)
.busy(true)
.on_send(move |address, _, cx| say(&owner, format!("send {address}"), cx))
.on_other(move |_, cx| say(&other, "other".into(), cx))
.into_any_element()
MagicLinkForm::new("magic", move |address, _, cx| {
say(&owner, format!("send {address}"), cx)
})
.sent(true)
.busy(true)
.on_other(move |_, cx| say(&other, "other".into(), cx))
.into_any_element()
}
/// While a link is on its way Send again rests, so the first stop is Use another email.
+8 -11
View File
@@ -20,18 +20,22 @@ pub struct UpgradePrompt {
title: SharedString,
body: Option<SharedString>,
benefits: Vec<SharedString>,
on_upgrade: Option<Run>,
on_upgrade: Run,
on_dismiss: Option<Run>,
}
impl UpgradePrompt {
pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self {
pub fn new(
id: impl Into<ElementId>,
title: impl Into<SharedString>,
on_upgrade: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
title: title.into(),
body: None,
benefits: Vec::new(),
on_upgrade: None,
on_upgrade: Rc::new(on_upgrade),
on_dismiss: None,
}
}
@@ -47,11 +51,6 @@ impl UpgradePrompt {
self
}
pub fn on_upgrade(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_upgrade = Some(Rc::new(handler));
self
}
/// Shows "Not now".
pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_dismiss = Some(Rc::new(handler));
@@ -62,9 +61,7 @@ impl UpgradePrompt {
impl RenderOnce for UpgradePrompt {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id;
let upgrade = self
.on_upgrade
.unwrap_or_else(|| panic!("upgrade prompt {id:?} has no on_upgrade"));
let upgrade = self.on_upgrade;
let theme = cx.theme();
let wall = !self.benefits.is_empty();
let actions = div()