Panics · P36 Account: handlers in new; gone accounts, workspaces, providers and roles mark none; expiries refuse overflow
This commit is contained in:
1 parent
ad472490e0
commit
e7aec1651e
19 files changed
+476
-518
No files matched your search
@@ -73,23 +73,20 @@ pub fn switchers(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
|
||||
.gap_6()
|
||||
.child(
|
||||
div().w(px(260.)).flex().flex_col().gap_2().child(
|
||||
WorkspaceSwitcher::new("account-workspaces", workspaces, workspace)
|
||||
.on_select(move |key, _, cx| change(&to_workspace, cx, |places| places.workspace = key.clone()))
|
||||
WorkspaceSwitcher::new("account-workspaces", workspaces, workspace, move |key, _, cx| change(&to_workspace, cx, |places| places.workspace = key.clone()))
|
||||
.on_create(|_, _| log::info!("gallery: create workspace")),
|
||||
)
|
||||
.child(
|
||||
AccountSwitcher::new("account-accounts", accounts, account)
|
||||
.on_select(move |key, _, cx| change(&to_account, cx, |places| places.account = key.clone()))
|
||||
AccountSwitcher::new("account-accounts", accounts, account, move |key, _, cx| change(&to_account, cx, |places| places.account = key.clone()))
|
||||
.on_add(|_, _| log::info!("gallery: add account")),
|
||||
),
|
||||
)
|
||||
.child(probe(
|
||||
"account-user",
|
||||
UserMenu::new("account-user", "Ada Lovelace", "ada@atlas.dev")
|
||||
UserMenu::new("account-user", "Ada Lovelace", "ada@atlas.dev", |_, _| log::info!("gallery: sign out"))
|
||||
.presence(Presence::Online)
|
||||
.item(MenuItem::new("Profile").icon(IconName::User).on_click(|_, _| log::info!("gallery: profile")))
|
||||
.item(MenuItem::new("Settings").icon(IconName::Settings).on_click(|_, _| log::info!("gallery: settings")))
|
||||
.on_sign_out(|_, _| log::info!("gallery: sign out")),
|
||||
.item(MenuItem::new("Settings").icon(IconName::Settings).on_click(|_, _| log::info!("gallery: settings"))),
|
||||
)),
|
||||
)
|
||||
}
|
||||
@@ -133,7 +130,7 @@ pub fn profile(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
.action(Button::new("account-card-profile", "View profile")),
|
||||
),
|
||||
)
|
||||
.child(div().w(px(400.)).child(ProfileEditor::new("account-editor", now).on_save(move |profile, _, cx| {
|
||||
.child(div().w(px(400.)).child(ProfileEditor::new("account-editor", now, move |profile, _, cx| {
|
||||
let profile = profile.clone();
|
||||
change(&saved, cx, move |now| *now = profile)
|
||||
}))),
|
||||
@@ -215,8 +212,10 @@ pub fn security_page(window: &mut Window, cx: &mut App) -> impl IntoElement + us
|
||||
let now = state.read(cx);
|
||||
let (sessions, keys, secret) = (now.sessions.clone(), now.keys.clone(), now.secret.clone());
|
||||
let [out, others, made, revoked, done] = [(); 5].map(|_| state.clone());
|
||||
let mut manager = ApiKeyManager::new("account-keys", keys)
|
||||
.on_create(move |name, _, cx| {
|
||||
let mut manager = ApiKeyManager::new(
|
||||
"account-keys",
|
||||
keys,
|
||||
move |name, _, cx| {
|
||||
let name = name.clone();
|
||||
change(&made, cx, move |security| {
|
||||
security.made += 1;
|
||||
@@ -233,14 +232,15 @@ pub fn security_page(window: &mut Window, cx: &mut App) -> impl IntoElement + us
|
||||
);
|
||||
security.secret = Some("ely_live_5f1c0a9e2b7d4c8a3e6f7d3e".into());
|
||||
})
|
||||
})
|
||||
.on_revoke(move |key, _, cx| {
|
||||
},
|
||||
move |key, _, cx| {
|
||||
let key = key.clone();
|
||||
change(&revoked, cx, move |security| {
|
||||
security.keys.retain(|each| each.key != key)
|
||||
})
|
||||
})
|
||||
.on_dismiss(move |_, cx| change(&done, cx, |security| security.secret = None));
|
||||
},
|
||||
move |_, cx| change(&done, cx, |security| security.secret = None),
|
||||
);
|
||||
if let Some(secret) = secret {
|
||||
manager = manager.secret(secret);
|
||||
}
|
||||
@@ -256,8 +256,7 @@ pub fn security_page(window: &mut Window, cx: &mut App) -> impl IntoElement + us
|
||||
.items_start()
|
||||
.gap_8()
|
||||
.child(div().w(px(380.)).child(
|
||||
SessionList::new("account-sessions", sessions)
|
||||
.on_sign_out(move |key, _, cx| {
|
||||
SessionList::new("account-sessions", sessions, move |key, _, cx| {
|
||||
let key = key.clone();
|
||||
change(&out, cx, move |security| security.sessions.retain(|each| each.key != key))
|
||||
})
|
||||
|
||||
@@ -68,7 +68,7 @@ pub fn plan(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
),
|
||||
)
|
||||
.child(div().w(px(360.)).flex().flex_col().gap_3().child(
|
||||
PaymentMethodForm::new("account-card-form").on_submit(move |card, _, cx| {
|
||||
PaymentMethodForm::new("account-card-form", move |card, _, cx| {
|
||||
let saved_as: SharedString = format!("{} ending {}", card.brand.name(), &card.number[card.number.len() - 4..]).into();
|
||||
change(&saved, cx, move |plan| plan.card = Some(saved_as))
|
||||
}),
|
||||
@@ -116,18 +116,16 @@ pub fn history(_: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.child(
|
||||
UpgradePrompt::new("account-nudge", "9 of 10 projects used")
|
||||
UpgradePrompt::new("account-nudge", "9 of 10 projects used", |_, _| log::info!("gallery: upgrade"))
|
||||
.body("Pro has no limit on projects.")
|
||||
.on_upgrade(|_, _| log::info!("gallery: upgrade"))
|
||||
.on_dismiss(|_, _| log::info!("gallery: not now")),
|
||||
)
|
||||
.child(
|
||||
UpgradePrompt::new("account-wall", "Version history is on Pro")
|
||||
UpgradePrompt::new("account-wall", "Version history is on Pro", |_, _| log::info!("gallery: upgrade"))
|
||||
.body("See and restore every version of a document.")
|
||||
.benefit("Every version, kept a year")
|
||||
.benefit("Restore with one press")
|
||||
.benefit("Compare any two")
|
||||
.on_upgrade(|_, _| log::info!("gallery: upgrade"))
|
||||
.on_dismiss(|_, _| log::info!("gallery: not now")),
|
||||
),
|
||||
),
|
||||
@@ -207,27 +205,22 @@ pub fn members(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
.gap_6()
|
||||
.w(px(640.))
|
||||
.child(
|
||||
TeamMemberTable::new("account-members", members, roles())
|
||||
.on_role(move |key, next, _, cx| {
|
||||
TeamMemberTable::new("account-members", members, roles(), move |key, next, _, cx| {
|
||||
let (key, next) = (key.clone(), next.clone());
|
||||
change(&role, cx, move |team| team.members.iter_mut().filter(|member| member.key == key).for_each(|member| member.role = next.clone()))
|
||||
})
|
||||
.on_remove(move |key, _, cx| {
|
||||
}, move |key, _, cx| {
|
||||
let key = key.clone();
|
||||
change(&removed, cx, move |team| team.members.retain(|member| member.key != key))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
InvitationList::new("account-invites", invitations, roles())
|
||||
.on_invite(move |email, role, _, cx| {
|
||||
InvitationList::new("account-invites", invitations, roles(), move |email, role, _, cx| {
|
||||
let (email, role) = (email.clone(), role.clone());
|
||||
change(&invited, cx, move |team| {
|
||||
team.sent += 1;
|
||||
team.invitations.insert(0, Invitation { key: format!("new-{}", team.sent).into(), email, role, sent: "Sent just now".into(), expired: false });
|
||||
})
|
||||
})
|
||||
.on_resend(|key, _, _| log::info!("gallery: resend {key}"))
|
||||
.on_revoke(move |key, _, cx| {
|
||||
}, |key, _, _| log::info!("gallery: resend {key}"), move |key, _, cx| {
|
||||
let key = key.clone();
|
||||
change(&revoked, cx, move |team| team.invitations.retain(|each| each.key != key))
|
||||
}),
|
||||
|
||||
@@ -30,19 +30,18 @@ pub fn sign_in(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
("google", "Google"),
|
||||
("apple", "Apple"),
|
||||
],
|
||||
)
|
||||
.on_pick(move |key, _, cx| change(&picked, cx, |state| state.provider = Some(key.clone())));
|
||||
move |key, _, cx| change(&picked, cx, |state| state.provider = Some(key.clone())),
|
||||
);
|
||||
if let Some(provider) = provider {
|
||||
providers = providers.busy(provider);
|
||||
}
|
||||
let mut form = LoginForm::new("account-login")
|
||||
.on_submit(move |_, _, cx| {
|
||||
change(&tried, cx, |state| {
|
||||
state.error = Some("No account uses that email and password.".into())
|
||||
})
|
||||
let mut form = LoginForm::new("account-login", move |_, _, cx| {
|
||||
change(&tried, cx, |state| {
|
||||
state.error = Some("No account uses that email and password.".into())
|
||||
})
|
||||
.on_forgot(|_, _| log::info!("gallery: forgot password"))
|
||||
.on_sign_up(|_, _| log::info!("gallery: sign up"));
|
||||
})
|
||||
.on_forgot(|_, _| log::info!("gallery: forgot password"))
|
||||
.on_sign_up(|_, _| log::info!("gallery: sign up"));
|
||||
if let Some(error) = error {
|
||||
form = form.error(error);
|
||||
}
|
||||
@@ -71,13 +70,12 @@ pub fn sign_up(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep("account-sign-up", SignUp::default, window, cx);
|
||||
let error = state.read(cx).error.clone();
|
||||
let tried = state.clone();
|
||||
let mut form = SignupForm::new("account-signup")
|
||||
.terms("I agree to the terms of service")
|
||||
.on_submit(move |signup, _, cx| {
|
||||
let taken = format!("An account already uses {}.", signup.email);
|
||||
change(&tried, cx, |state| state.error = Some(taken.into()))
|
||||
})
|
||||
.on_sign_in(|_, _| log::info!("gallery: sign in"));
|
||||
let mut form = SignupForm::new("account-signup", move |signup, _, cx| {
|
||||
let taken = format!("An account already uses {}.", signup.email);
|
||||
change(&tried, cx, |state| state.error = Some(taken.into()))
|
||||
})
|
||||
.terms("I agree to the terms of service")
|
||||
.on_sign_in(|_, _| log::info!("gallery: sign in"));
|
||||
if let Some(error) = error {
|
||||
form = form.error(error);
|
||||
}
|
||||
@@ -115,9 +113,8 @@ pub fn by_email(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
"account-magic",
|
||||
div().w(px(360.)).child(
|
||||
Card::new().header(CardHeader::new("Sign in by email")).child(
|
||||
MagicLinkForm::new("account-magic")
|
||||
MagicLinkForm::new("account-magic", move |_, _, cx| change(&sent, cx, |state| state.magic = true))
|
||||
.sent(magic)
|
||||
.on_send(move |_, _, cx| change(&sent, cx, |state| state.magic = true))
|
||||
.on_other(move |_, cx| change(&other, cx, |state| state.magic = false)),
|
||||
),
|
||||
),
|
||||
@@ -125,9 +122,8 @@ pub fn by_email(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
.child(
|
||||
div().w(px(360.)).child(
|
||||
Card::new().header(CardHeader::new("Reset your password")).child(
|
||||
ForgotPassword::new("account-forgot")
|
||||
ForgotPassword::new("account-forgot", move |_, _, cx| change(&asked, cx, |state| state.reset = true))
|
||||
.sent(reset)
|
||||
.on_send(move |_, _, cx| change(&asked, cx, |state| state.reset = true))
|
||||
.on_back(move |_, cx| change(&back, cx, |state| state.reset = false)),
|
||||
),
|
||||
),
|
||||
@@ -161,14 +157,13 @@ pub fn second_factor(window: &mut Window, cx: &mut App) -> impl IntoElement + us
|
||||
Card::new()
|
||||
.header(CardHeader::new("Two-step sign-in"))
|
||||
.child(
|
||||
TwoFactorInput::new("account-factor")
|
||||
.failed(failed)
|
||||
.on_code(move |code, _, cx| {
|
||||
TwoFactorInput::new("account-factor", move |code, _, cx| {
|
||||
change(&checked, cx, |state| match code.as_ref() == CODE {
|
||||
true => state.passed = true,
|
||||
false => state.failed += 1,
|
||||
})
|
||||
})
|
||||
.failed(failed)
|
||||
.on_recovery(|_, _| log::info!("gallery: recovery code")),
|
||||
)
|
||||
.children(passed.then(|| InlineMessage::new(Severity::Success, "Signed in."))),
|
||||
|
||||
+7
-14
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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());
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
|
||||
+2
-1
@@ -50,7 +50,7 @@ Each item reads every site in its folders, fixes what the rules name, updates ca
|
||||
- [x] P33 DB & Dev Tools — `src/devtools`
|
||||
- [x] P34 Dashboard — `src/dashboard`
|
||||
- [x] P35 Settings — `src/settings`
|
||||
- [ ] P36 Account — `src/account`
|
||||
- [x] P36 Account — `src/account`
|
||||
- [ ] P37 Onboarding — `src/onboarding`
|
||||
- [ ] P38 Interaction — `src/interaction`
|
||||
- [ ] P39 Theme — `src/theme`
|
||||
@@ -101,3 +101,4 @@ One line per item: what changed, what stayed and why, review rounds.
|
||||
- P33: six devtools take their handlers in `new` (contrast checker, environment selector, GraphQL explorer, WebSocket console, structure editor, index manager); `collection::moved` returns `Option` for a gone key or target or a drop into a request; a gone environment shows none chosen; an ER table without a place stays out. Kept: readings, costs, lookups over the components' own tables, button-gated expects, JSON that read writes back. Review: 1 round.
|
||||
- P34: the refresh interval selector, filter bar and dashboard grid take their handlers in `new`; `tiles::arranged` and `stepped` return `Option` for a tile gone from the grid, so a landing or step after the owner removed it moves nothing; tiles without cards and cards without tiles stay out. Kept: columns, cells, intervals offered, services and uptimes, lookups from one render. Review: 1 round.
|
||||
- P35: eighteen settings components take their handlers in `new`; a section or syntax theme not offered marks none; a font size or default outside its range pegs. Kept: options offered by construction, the proxy's Apply behind its read, drops that bring a file. Review: 1 round.
|
||||
- P36: seventeen account components take their handlers in `new`, the email step's `on_send` with them; a busy provider, a current account or workspace, and a role no longer listed mark none and log; an invitation waits for its role to be offered; an expiry year that overflows reads as unread. Kept: quotas, providers, one current session, roles to invite as, the card behind Save. Review: 2 rounds.
|
||||
Reference in new issue
Block a user