UX: Add progress bar to the registration flow (#27694)

This commit is contained in:
Jan Cernik 2024-08-28 06:43:39 -05:00 committed by GitHub
parent 3a04443632
commit b092ccbdc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 674 additions and 467 deletions

View File

@ -21,16 +21,17 @@
this.model.authOptions.auth_provider
}}
>
<div class="login-welcome-header" id="create-account-title">
<h1 class="login-title">{{i18n "create_account.header_title"}}</h1>
<img src={{this.wavingHandURL}} alt="" class="waving-hand" />
<p class="login-subheader">{{i18n "create_account.subheader_title"}}</p>
<SignupProgressBar @step="signup" />
<WelcomeHeader
id="create-account-title"
@header={{i18n "create_account.header_title"}}
@subheader={{i18n "create_account.subheader_title"}}
>
<PluginOutlet
@name="create-account-header-bottom"
@outletArgs={{hash showLogin=(route-action "showLogin")}}
/>
</div>
</WelcomeHeader>
{{#if this.showCreateForm}}
<form id="login-form">
{{#if this.associateHtml}}
@ -86,7 +87,6 @@
id="username-validation"
/>
{{#unless this.usernameValidation.reason}}
<span class="more-info" id="username-validation-more-info">
{{i18n "user.username.instructions"}}
</span>
@ -307,9 +307,11 @@
<:footer>
{{#if (and this.showCreateForm this.site.mobileView)}}
<div class="disclaimer">
{{html-safe this.disclaimerHtml}}
</div>
{{#if this.disclaimerHtml}}
<div class="disclaimer">
{{html-safe this.disclaimerHtml}}
</div>
{{/if}}
<div class="d-modal__footer-buttons">
<DButton
@action={{this.createAccount}}

View File

@ -11,7 +11,6 @@ import { setting } from "discourse/lib/computed";
import cookie, { removeCookie } from "discourse/lib/cookie";
import { userPath } from "discourse/lib/url";
import { emailValid } from "discourse/lib/utilities";
import { wavingHandURL } from "discourse/lib/waving-hand-url";
import NameValidation from "discourse/mixins/name-validation";
import PasswordValidation from "discourse/mixins/password-validation";
import UserFieldsValidation from "discourse/mixins/user-fields-validation";
@ -106,11 +105,6 @@ export default class CreateAccount extends Component.extend(
return this.formSubmitted;
}
@discourseComputed()
wavingHandURL() {
return wavingHandURL();
}
@discourseComputed("userFields", "hasAtLeastOneLoginButton", "hasAuthOptions")
modalBodyClasses(userFields, hasAtLeastOneLoginButton, hasAuthOptions) {
const classes = [];

View File

@ -27,10 +27,15 @@
</div>
{{else}}
{{#if this.site.mobileView}}
<Modal::Login::WelcomeHeader
@wavingHandURL={{this.wavingHandURL}}
@createAccount={{this.createAccount}}
/>
<WelcomeHeader
@header={{i18n "login.header_title"}}
@subheader={{i18n "login.subheader_title"}}
>
<PluginOutlet
@name="login-header-bottom"
@outletArgs={{hash createAccount=this.createAccount}}
/>
</WelcomeHeader>
{{#if this.showLoginButtons}}
<LoginButtons
@externalLogin={{this.externalLoginAction}}
@ -43,10 +48,15 @@
{{#if this.canLoginLocal}}
<div class={{if this.site.desktopView "login-left-side"}}>
{{#if this.site.desktopView}}
<Modal::Login::WelcomeHeader
@wavingHandURL={{this.wavingHandURL}}
@createAccount={{this.createAccount}}
/>
<WelcomeHeader
@header={{i18n "login.header_title"}}
@subheader={{i18n "login.subheader_title"}}
>
<PluginOutlet
@name="login-header-bottom"
@outletArgs={{hash createAccount=this.createAccount}}
/>
</WelcomeHeader>
{{/if}}
<Modal::Login::LocalLoginForm
@loginName={{this.loginName}}
@ -91,8 +101,9 @@
{{#if (and this.showLoginButtons this.site.desktopView)}}
{{#unless this.canLoginLocal}}
<div class="login-left-side">
<Modal::Login::WelcomeHeader
@wavingHandURL={{this.wavingHandURL}}
<WelcomeHeader
@header={{i18n "login.header_title"}}
@subheader={{i18n "login.subheader_title"}}
/>
</div>
{{/unless}}

View File

@ -10,7 +10,6 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
import cookie, { removeCookie } from "discourse/lib/cookie";
import { wantsNewWindow } from "discourse/lib/intercept-click";
import { areCookiesEnabled } from "discourse/lib/utilities";
import { wavingHandURL } from "discourse/lib/waving-hand-url";
import {
getPasskeyCredential,
isWebauthnSupported,
@ -64,10 +63,6 @@ export default class Login extends Component {
return this.loggingIn || this.loggedIn;
}
get wavingHandURL() {
return wavingHandURL();
}
get modalBodyClasses() {
const classes = ["login-modal-body"];
if (this.awaitingApproval) {

View File

@ -1,9 +0,0 @@
<div class="login-welcome-header">
<h1 class="login-title">{{i18n "login.header_title"}}</h1>
<img src={{@wavingHandURL}} alt="" class="waving-hand" />
<p class="login-subheader">{{i18n "login.subheader_title"}}</p>
<PluginOutlet
@name="login-header-bottom"
@outletArgs={{hash createAccount=@createAccount}}
/>
</div>

View File

@ -0,0 +1,80 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { concat } from "@ember/helper";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { eq } from "truth-helpers";
import concatClass from "discourse/helpers/concat-class";
import dIcon from "discourse-common/helpers/d-icon";
import I18n from "discourse-i18n";
export default class SignupProgressBar extends Component {
@service siteSettings;
@tracked steps = [];
constructor() {
super(...arguments);
if (this.siteSettings.must_approve_users) {
this.steps = ["signup", "activate", "approve", "login"];
} else {
this.steps = ["signup", "activate", "login"];
}
}
stepText(step) {
return I18n.t(`create_account.progress_bar.${step}`);
}
get currentStepIndex() {
return this.steps.findIndex((step) => step === this.args.step);
}
get lastStepIndex() {
return this.steps.length - 1;
}
@action
getStepState(index) {
if (index === this.currentStepIndex) {
return "active";
} else if (index < this.currentStepIndex) {
return "completed";
} else if (index > this.currentStepIndex) {
return "incomplete";
}
}
<template>
{{#if @step}}
<div class="signup-progress-bar">
{{#each this.steps as |step index|}}
<div class="signup-progress-bar__segment">
<div
class={{concatClass
"signup-progress-bar__step"
(concat "--" (this.getStepState index))
}}
>
<div class="signup-progress-bar__circle">
{{#if (eq (this.getStepState index) "completed")}}
{{dIcon "check"}}
{{/if}}
</div>
{{#unless (eq index this.lastStepIndex)}}
<span
class={{concatClass
"signup-progress-bar__line"
(concat "--" (this.getStepState index))
}}
></span>
{{/unless}}
</div>
<span class="signup-progress-bar__step-text">
{{this.stepText step}}
</span>
</div>
{{/each}}
</div>
{{/if}}
</template>
}

View File

@ -0,0 +1,14 @@
import { wavingHandURL } from "discourse/lib/waving-hand-url";
const WelcomeHeader = <template>
<div class="login-welcome-header" ...attributes>
<h1 class="login-title">{{@header}}</h1>
<img src={{(wavingHandURL)}} alt="" class="waving-hand" />
{{#if @subheader}}
<p class="login-subheader">{{@subheader}}</p>
{{/if}}
{{yield}}
</div>
</template>;
export default WelcomeHeader;

View File

@ -2,16 +2,12 @@ import Controller from "@ember/controller";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { resendActivationEmail } from "discourse/lib/user-activation";
import { wavingHandURL } from "discourse/lib/waving-hand-url";
import getUrl from "discourse-common/lib/get-url";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "discourse-i18n";
export default class AccountCreatedIndexController extends Controller {
@service router;
envelopeImageUrl = getUrl("/images/envelope.svg");
@discourseComputed
welcomeTitle() {
return I18n.t("invites.welcome_to", {
@ -19,11 +15,6 @@ export default class AccountCreatedIndexController extends Controller {
});
}
@discourseComputed
wavingHandURL() {
return wavingHandURL();
}
@action
sendActivationEmail() {
resendActivationEmail(this.get("accountCreated.username")).then(() => {

View File

@ -6,7 +6,6 @@ import { ajax } from "discourse/lib/ajax";
import { extractError } from "discourse/lib/ajax-error";
import DiscourseURL from "discourse/lib/url";
import { emailValid } from "discourse/lib/utilities";
import { wavingHandURL } from "discourse/lib/waving-hand-url";
import NameValidation from "discourse/mixins/name-validation";
import PasswordValidation from "discourse/mixins/password-validation";
import UserFieldsValidation from "discourse/mixins/user-fields-validation";
@ -42,7 +41,6 @@ export default class InvitesShowController extends Controller.extend(
errorMessage = null;
userFields = null;
authOptions = null;
inviteImageUrl = getUrl("/images/envelope.svg");
rejectedEmails = [];
maskPassword = true;
@ -261,11 +259,6 @@ export default class InvitesShowController extends Controller.extend(
return matchingProvider ? matchingProvider.get("prettyName") : providerName;
}
@discourseComputed
wavingHandURL() {
return wavingHandURL();
}
@discourseComputed
ssoPath() {
return getUrl("/session/sso");

View File

@ -1,9 +1,12 @@
import { service } from "@ember/service";
import PreloadStore from "discourse/lib/preload-store";
import DiscourseRoute from "discourse/routes/discourse";
import { deepMerge } from "discourse-common/lib/object";
import I18n from "discourse-i18n";
export default class InvitesShow extends DiscourseRoute {
@service siteSettings;
titleToken() {
return I18n.t("invites.accept_title");
}
@ -21,17 +24,21 @@ export default class InvitesShow extends DiscourseRoute {
activate() {
super.activate(...arguments);
this.controllerFor("application").setProperties({
showSiteHeader: false,
});
if (this.siteSettings.login_required) {
this.controllerFor("application").setProperties({
showSiteHeader: false,
});
}
}
deactivate() {
super.deactivate(...arguments);
this.controllerFor("application").setProperties({
showSiteHeader: true,
});
if (this.siteSettings.login_required) {
this.controllerFor("application").setProperties({
showSiteHeader: true,
});
}
}
setupController(controller, model) {

View File

@ -1,5 +1,6 @@
<div id="simple-container">
<div class="account-created">
{{outlet}}
</div>
{{body-class "account-created-page"}}
{{hide-application-header-buttons "search" "login" "signup"}}
{{hide-application-sidebar}}
<div class="account-created">
{{outlet}}
</div>

View File

@ -1,10 +1,10 @@
<SignupProgressBar @step="activate" />
<div class="ac-message">
<ActivationEmailForm
@email={{this.newEmail}}
@updateNewEmail={{this.updateNewEmail}}
/>
</div>
<div class="activation-controls">
<DButton
@action={{this.changeEmail}}

View File

@ -1,26 +1,11 @@
{{body-class "account-activation-page"}}
<div class="container invites-show">
<div class="login-welcome-header">
<h1 class="login-title">{{this.welcomeTitle}}</h1>
<img src={{this.wavingHandURL}} alt="" class="waving-hand" />
</div>
<div class="ac-page">
<div class="two-col">
<div class="col-image">
<img src={{this.envelopeImageUrl}} alt="" />
</div>
<div class="col-form">
<div class="success-info">
{{html-safe this.accountCreated.message}}
</div>
{{#if this.accountCreated.show_controls}}
<ActivationControls
@sendActivationEmail={{action "sendActivationEmail"}}
@editActivationEmail={{action "editActivationEmail"}}
/>
{{/if}}
</div>
</div>
</div>
</div>
<SignupProgressBar @step="activate" />
<WelcomeHeader @header={{this.welcomeTitle}} />
<div class="success-info">
{{html-safe this.accountCreated.message}}
</div>
{{#if this.accountCreated.show_controls}}
<ActivationControls
@sendActivationEmail={{action "sendActivationEmail"}}
@editActivationEmail={{action "editActivationEmail"}}
/>
{{/if}}

View File

@ -1,3 +1,4 @@
<SignupProgressBar @step="activate" />
<div class="ac-message">
{{#if this.email}}
{{html-safe

View File

@ -4,11 +4,13 @@ import { action } from "@ember/object";
import { service } from "@ember/service";
import RouteTemplate from "ember-route-template";
import DButton from "discourse/components/d-button";
import SignupProgressBar from "discourse/components/signup-progress-bar";
import WelcomeHeader from "discourse/components/welcome-header";
import bodyClass from "discourse/helpers/body-class";
import hideApplicationHeaderButtons from "discourse/helpers/hide-application-header-buttons";
import hideApplicationSidebar from "discourse/helpers/hide-application-sidebar";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { wavingHandURL } from "discourse/lib/waving-hand-url";
import i18n from "discourse-common/helpers/i18n";
import getURL from "discourse-common/lib/get-url";
@ -21,6 +23,16 @@ export default RouteTemplate(
@tracked needsApproval = false;
@tracked errorMessage = null;
get signupStep() {
if (this.needsApproval) {
return "approve";
} else if (this.accountActivated) {
return "login";
} else {
return "activate";
}
}
@action
async activate() {
this.isLoading = true;
@ -71,59 +83,52 @@ export default RouteTemplate(
}
<template>
{{hideApplicationSidebar}}
{{bodyClass "activate-account-page"}}
{{hideApplicationHeaderButtons "search" "login" "signup"}}
<div id="simple-container">
{{#if this.errorMessage}}
<div class="alert alert-error">
{{this.errorMessage}}
</div>
{{else}}
<div class="activate-account">
<h1 class="activate-title">{{i18n
"user.activate_account.welcome_to"
site_name=this.siteSettings.title
}}
<img src={{(wavingHandURL)}} alt="" class="waving-hand" />
</h1>
<br />
{{#if this.accountActivated}}
<div class="perform-activation">
<div class="image">
<img
src="/images/wizard/tada.svg"
class="waving-hand"
alt="tada emoji"
/>
</div>
{{#if this.needsApproval}}
<p>{{i18n "user.activate_account.approval_required"}}</p>
{{else}}
<p>{{i18n "user.activate_account.please_continue"}}</p>
<p>
<DButton
class="continue-button"
@translatedLabel={{i18n
"user.activate_account.continue_button"
site_name=this.siteSettings.title
}}
@action={{this.loadHomepage}}
/>
</p>
{{/if}}
{{hideApplicationSidebar}}
{{#if this.errorMessage}}
<div class="alert alert-error">
{{this.errorMessage}}
</div>
{{else}}
<div class="activate-account">
<SignupProgressBar @step={{this.signupStep}} />
<WelcomeHeader
@header={{i18n
"user.activate_account.welcome_to"
site_name=this.siteSettings.title
}}
/>
<br />
{{#if this.accountActivated}}
<div class="account-activated">
<div class="tada-image">
<img src="/images/wizard/tada.svg" alt="tada emoji" />
</div>
{{else}}
<DButton
id="activate-account-button"
class="btn-primary"
@action={{this.activate}}
@label="user.activate_account.action"
@disabled={{this.isLoading}}
/>
{{/if}}
</div>
{{/if}}
</div>
{{#if this.needsApproval}}
<p>{{i18n "user.activate_account.approval_required"}}</p>
{{else}}
<p>{{i18n "user.activate_account.please_continue"}}</p>
<DButton
class="continue-button"
@translatedLabel={{i18n
"user.activate_account.continue_button"
site_name=this.siteSettings.title
}}
@action={{this.loadHomepage}}
/>
{{/if}}
</div>
{{else}}
<DButton
class="activate-account-button btn-primary"
@action={{this.activate}}
@label="user.activate_account.action"
@disabled={{this.isLoading}}
/>
{{/if}}
</div>
{{/if}}
</template>
}
);

View File

@ -1,240 +1,241 @@
{{body-class "invite-page"}}
{{hide-application-header-buttons "search" "login" "signup"}}
{{hide-application-sidebar}}
<section>
<div class="container invites-show clearfix">
<div class="login-welcome-header">
<h1 class="login-title">{{this.welcomeTitle}}</h1>
<img src={{this.wavingHandURL}} alt="" class="waving-hand" />
{{#unless this.successMessage}}
<p class="login-subheader">{{this.subheaderMessage}}</p>
{{/unless}}
</div>
{{#unless this.externalAuthsOnly}}
<SignupProgressBar @step={{if this.successMessage "activate" "signup"}} />
{{/unless}}
<WelcomeHeader
@header={{this.welcomeTitle}}
@subheader={{unless this.successMessage this.subheaderMessage}}
/>
<div class={{if this.successMessage "invite-success" "invite-form"}}>
<div class="two-col">
<div class="col-image">
<img src={{this.inviteImageUrl}} alt="" />
</div>
<div class="col-form">
{{#if this.successMessage}}
<div class="success-info">
<p>{{html-safe this.successMessage}}</p>
</div>
{{else}}
<p>{{i18n "invites.invited_by"}}</p>
<p><UserInfo @user={{this.invitedBy}} /></p>
<div class="col-form">
{{#if this.successMessage}}
<div class="success-info">
<p>{{html-safe this.successMessage}}</p>
</div>
{{else}}
<p>{{i18n "invites.invited_by"}}</p>
<p><UserInfo @user={{this.invitedBy}} /></p>
{{#if this.associateHtml}}
<p class="create-account-associate-link">
{{html-safe this.associateHtml}}
</p>
{{/if}}
{{#if this.associateHtml}}
<p class="create-account-associate-link">
{{html-safe this.associateHtml}}
</p>
{{#unless this.isInviteLink}}
<p class="email-message">
{{html-safe this.yourEmailMessage}}
{{#if this.showSocialLoginAvailable}}
{{i18n "invites.social_login_available"}}
{{/if}}
</p>
{{/unless}}
{{#if this.externalAuthsOnly}}
{{! authOptions are present once the user has followed the OmniAuth flow (e.g. twitter/google/etc) }}
{{#if this.authOptions}}
{{#unless this.isInviteLink}}
<InputTip
@validation={{this.emailValidation}}
id="account-email-validation"
/>
{{/unless}}
{{else}}
<LoginButtons
@externalLogin={{action "externalLogin"}}
@context="invite"
/>
{{/if}}
{{/if}}
{{#unless this.isInviteLink}}
<p class="email-message">
{{html-safe this.yourEmailMessage}}
{{#if this.showSocialLoginAvailable}}
{{i18n "invites.social_login_available"}}
{{/if}}
</p>
{{/unless}}
{{#if this.discourseConnectEnabled}}
<a
class="btn btn-primary discourse-connect raw-link"
href={{this.ssoPath}}
>
{{i18n "continue"}}
</a>
{{/if}}
{{#if this.externalAuthsOnly}}
{{! authOptions are present once the user has followed the OmniAuth flow (e.g. twitter/google/etc) }}
{{#if this.authOptions}}
{{#unless this.isInviteLink}}
{{#if this.shouldDisplayForm}}
<form>
{{#if this.isInviteLink}}
<div class="input email-input input-group">
<Input
@type="email"
@value={{this.email}}
id="new-account-email"
name="email"
class={{value-entered this.email}}
autofocus="autofocus"
disabled={{this.externalAuthsOnly}}
/>
<label class="alt-placeholder" for="new-account-email">
{{i18n "user.email.title"}}
</label>
<InputTip
@validation={{this.emailValidation}}
id="account-email-validation"
/>
{{/unless}}
{{else}}
<LoginButtons
@externalLogin={{action "externalLogin"}}
@context="invite"
/>
{{/if}}
{{/if}}
{{#if this.discourseConnectEnabled}}
<a
class="btn btn-primary discourse-connect raw-link"
href={{this.ssoPath}}
>
{{i18n "continue"}}
</a>
{{/if}}
{{#if this.shouldDisplayForm}}
<form>
{{#if this.isInviteLink}}
<div class="input email-input input-group">
<Input
@type="email"
@value={{this.email}}
id="new-account-email"
name="email"
class={{value-entered this.email}}
autofocus="autofocus"
disabled={{this.externalAuthsOnly}}
/>
<label class="alt-placeholder" for="new-account-email">
{{i18n "user.email.title"}}
<span class="required">*</span>
</label>
<InputTip
@validation={{this.emailValidation}}
id="account-email-validation"
/>
{{#unless this.emailValidation.reason}}
<div class="instructions">{{i18n
"user.email.instructions"
}}</div>
</div>
{{/if}}
{{/unless}}
</div>
{{/if}}
<div class="input username-input input-group">
<Input
@value={{this.accountUsername}}
class={{value-entered this.accountUsername}}
id="new-account-username"
name="username"
maxlength={{this.maxUsernameLength}}
autocomplete="off"
/>
<label class="alt-placeholder" for="new-account-username">
{{i18n "user.username.title"}}
<span class="required">*</span>
</label>
<InputTip
@validation={{this.usernameValidation}}
id="username-validation"
/>
<div class="input username-input input-group">
<Input
@value={{this.accountUsername}}
class={{value-entered this.accountUsername}}
id="new-account-username"
name="username"
maxlength={{this.maxUsernameLength}}
autocomplete="off"
/>
<label class="alt-placeholder" for="new-account-username">
{{i18n "user.username.title"}}
</label>
<InputTip
@validation={{this.usernameValidation}}
id="username-validation"
/>
{{#unless this.usernameValidation.reason}}
<div class="instructions">{{i18n
"user.username.instructions"
}}</div>
</div>
{{/unless}}
</div>
{{#if this.fullnameRequired}}
<div class="input name-input input-group">
<Input
@value={{this.accountName}}
class={{value-entered this.accountName}}
id="new-account-name"
name="name"
/>
<label class="alt-placeholder" for="new-account-name">
{{i18n "invites.name_label"}}
{{#if this.siteSettings.full_name_required}}
<span class="required">*</span>
{{/if}}
</label>
<div class="instructions">{{this.nameInstructions}}</div>
</div>
{{/if}}
{{#unless this.externalAuthsOnly}}
<div class="input password-input input-group">
<PasswordField
@value={{this.accountPassword}}
@capsLockOn={{this.capsLockOn}}
type={{if this.maskPassword "password" "text"}}
autocomplete="new-password"
id="new-account-password"
class={{value-entered this.accountPassword}}
/>
<label class="alt-placeholder" for="new-account-password">
{{i18n "invites.password_label"}}
<span class="required">*</span>
</label>
<div class="create-account__password-info">
<div class="create-account__password-tip-validation">
<InputTip
@validation={{this.passwordValidation}}
id="password-validation"
/>
{{#unless this.externalAuthsOnly}}
<div class="input password-input input-group">
<PasswordField
@value={{this.accountPassword}}
@capsLockOn={{this.capsLockOn}}
type={{if this.maskPassword "password" "text"}}
autocomplete="new-password"
id="new-account-password"
class={{value-entered this.accountPassword}}
/>
<label class="alt-placeholder" for="new-account-password">
{{i18n "invites.password_label"}}
</label>
<div class="create-account__password-info">
<div class="create-account__password-tip-validation">
<InputTip
@validation={{this.passwordValidation}}
id="password-validation"
/>
{{#unless this.passwordValidation.reason}}
<span
class="more-info"
>{{this.passwordInstructions}}</span>
<div
class="caps-lock-warning
{{unless this.capsLockOn 'hidden'}}"
>
{{d-icon "exclamation-triangle"}}
{{i18n "login.caps_lock_warning"}}
</div>
{{/unless}}
<div
class="caps-lock-warning
{{unless this.capsLockOn 'hidden'}}"
>
{{d-icon "exclamation-triangle"}}
{{i18n "login.caps_lock_warning"}}
</div>
<TogglePasswordMask
@maskPassword={{this.maskPassword}}
@togglePasswordMask={{this.togglePasswordMask}}
@parentController="invites-show"
/>
</div>
</div>
{{/unless}}
{{#if this.userFields}}
<div class="user-fields">
{{#each this.userFields as |f|}}
<div class="input-group">
<UserField
@field={{f.field}}
@value={{f.value}}
class={{value-entered f.value}}
/>
</div>
{{/each}}
</div>
{{/if}}
<div class="invitation-cta">
<DButton
@action={{action "submit"}}
@disabled={{this.submitDisabled}}
@label="invites.accept_invite"
type="submit"
class="btn-primary invitation-cta__accept"
/>
<div class="invitation-cta__info">
<span class="invitation-cta__signed-up">{{i18n
"login.previous_sign_up"
}}</span>
<DButton
@action={{route-action "showLogin"}}
@label="log_in"
class="btn-flat invitation-cta__sign-in"
<TogglePasswordMask
@maskPassword={{this.maskPassword}}
@togglePasswordMask={{this.togglePasswordMask}}
@parentController="invites-show"
/>
</div>
</div>
{{/unless}}
<div class="disclaimer">
{{html-safe this.disclaimerHtml}}
{{#if this.fullnameRequired}}
<div
class={{concat-class
"input"
"name-input"
"input-group"
(if this.siteSettings.full_name_required "name-required")
}}
>
<Input
@value={{this.accountName}}
class={{value-entered this.accountName}}
id="new-account-name"
name="name"
/>
<label class="alt-placeholder" for="new-account-name">
{{i18n "invites.name_label"}}
</label>
<div class="instructions">{{this.nameInstructions}}</div>
</div>
{{/if}}
{{#if this.errorMessage}}
<br /><br />
<div class="alert alert-error">{{this.errorMessage}}</div>
{{/if}}
</form>
{{/if}}
{{#if this.existingUserRedeeming}}
{{#if this.existingUserCanRedeem}}
{{#if this.userFields}}
<div class="user-fields">
{{#each this.userFields as |f|}}
<div class="input-group">
<UserField
@field={{f.field}}
@value={{f.value}}
class={{value-entered f.value}}
/>
</div>
{{/each}}
</div>
{{/if}}
<div class="invitation-cta">
<DButton
@action={{action "submit"}}
@disabled={{this.submitDisabled}}
@label="invites.accept_invite"
type="submit"
class="btn-primary"
class="btn-primary invitation-cta__accept"
/>
{{else}}
<div
class="alert alert-error"
>{{this.existingUserCanRedeemError}}</div>
<div class="invitation-cta__info">
<span class="invitation-cta__signed-up">{{i18n
"login.previous_sign_up"
}}</span>
<DButton
@action={{route-action "showLogin"}}
@label="log_in"
class="btn-flat invitation-cta__sign-in"
/>
</div>
</div>
<div class="disclaimer">
{{html-safe this.disclaimerHtml}}
</div>
{{#if this.errorMessage}}
<br /><br />
<div class="alert alert-error">{{this.errorMessage}}</div>
{{/if}}
</form>
{{/if}}
{{#if this.existingUserRedeeming}}
{{#if this.existingUserCanRedeem}}
<DButton
@action={{action "submit"}}
@disabled={{this.submitDisabled}}
@label="invites.accept_invite"
type="submit"
class="btn-primary"
/>
{{else}}
<div
class="alert alert-error"
>{{this.existingUserCanRedeemError}}</div>
{{/if}}
{{/if}}
</div>
{{/if}}
</div>
</div>
</div>

View File

@ -80,6 +80,7 @@ acceptance("Invite accept", function (needs) {
});
test("invite link", async function (assert) {
this.siteSettings.login_required = true;
PreloadStore.store("invite_info", {
invited_by: {
id: 123,
@ -166,7 +167,9 @@ acceptance("Invite accept", function (needs) {
test("invite name is required only if full name is required", async function (assert) {
preloadInvite();
await visit("/invites/my-valid-invite-token");
assert.dom(".name-input .required").exists("Full name is required");
assert
.dom(".name-input .required")
.doesNotExist("Full name is implicitly required");
});
});

View File

@ -1,73 +1,70 @@
// Styles used before the user is logged into discourse. For example, activating their
// account or changing their email.
.account-activation-page {
.desktop-view & {
background: var(--primary-very-low);
}
#main-outlet-wrapper {
display: block;
.sidebar-wrapper {
display: none;
}
}
.header-sidebar-toggle {
display: none;
}
.account-created-page,
.activate-account-page {
background: var(--secondary);
#main-outlet {
padding: 0;
height: 100%;
}
#simple-container {
}
.activate-account-page .alert-error {
margin: 1em;
}
.account-created,
.activate-account {
max-width: 500px;
padding: 2rem 3rem;
background: var(--secondary);
box-shadow: var(--shadow-menu-panel);
margin: 10vh auto 1em auto;
@media screen and (max-height: 700px) {
margin: 1em auto 1em auto;
}
}
.account-created {
.ac-message {
font-size: var(--font-up-1);
line-height: var(--line-height-large);
}
.activation-controls {
display: flex;
align-items: center;
box-sizing: border-box;
border-radius: 10px;
width: 100%;
padding: 20px;
justify-content: center;
margin-top: var(--header-offset);
.account-created {
.ac-message {
font-size: var(--font-up-1);
line-height: var(--line-height-large);
}
.activation-controls {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
}
.ac-page {
border-radius: 10px;
margin-top: 25px;
}
}
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
}
.edit-cancel {
text-transform: capitalize;
}
.success-info p:last-child {
margin-bottom: 0;
}
}
.activate-account {
.activate-title {
text-align: center;
.waving-hand {
height: 32px;
margin-bottom: 13px;
}
}
#activate-account-button {
margin-top: 20px;
margin-left: auto;
margin-right: auto;
.activate-account-button,
.continue-button {
margin-top: 1em;
margin-inline: auto;
display: block;
width: fit-content;
}
.perform-activation {
border-top: 6px solid var(--tertiary);
box-shadow: 0 1px 10px 1px rgba(var(--primary-low-rgb), 1.25);
border-radius: 10px;
padding: 1em 2.5em 1em 2.5em;
.image {
width: 150px;
margin: auto;
padding-bottom: 1em;
}
.login-welcome-header {
margin-inline: auto;
width: fit-content;
}
.tada-image {
width: 150px;
margin: auto;
padding-bottom: 1em;
}
}

View File

@ -97,16 +97,6 @@ body.invite-page {
}
}
.invite-error {
box-shadow: 0 1px 10px 1px rgba(var(--primary-low-rgb), 1.25);
border-radius: 10px;
padding: 1em 2.5em 1em 2.5em;
.error-image {
text-align: center;
padding-bottom: 1em;
}
}
.email-login {
border-radius: 10px;
background-color: var(--secondary);

View File

@ -37,6 +37,7 @@
@import "sidebar/edit-navigation-menu/categories-modal";
@import "sidebar/edit-navigation-menu/modal";
@import "sidebar/edit-navigation-menu/tags-modal";
@import "signup-progress-bar";
@import "svg";
@import "tap-tile";
@import "time-input";
@ -51,3 +52,4 @@
@import "user-stream";
@import "widget-dropdown";
@import "dropdown-menu";
@import "welcome-header";

View File

@ -3,7 +3,8 @@
.has-full-page-chat &,
.static-login &,
.invite-page &,
.account-activation-page & {
.account-created-page &,
.activate-account-page & {
display: none !important;
}
grid-area: below-content;

View File

@ -0,0 +1,96 @@
$progress-bar-line-width: 2px;
$progress-bar-circle-size: 1.2rem;
$progress-bar-icon-size: 0.8rem;
.signup-progress-bar {
width: 100%;
display: flex;
color: var(--primary-low-mid);
box-sizing: border-box;
margin-bottom: 1.2em;
&__segment {
width: 100%;
display: flex;
flex-direction: column;
gap: 0.5rem;
&:first-child .signup-progress-bar__circle {
transform: translateX(50%);
z-index: 1;
}
&:last-child {
width: $progress-bar-circle-size;
.signup-progress-bar__circle {
transform: translateX(-50%);
z-index: 1;
}
}
}
&__step-text {
white-space: nowrap;
width: fit-content;
transform: translateX(calc(calc($progress-bar-circle-size / 2) - 50%));
.signup-progress-bar__segment:first-child & {
transform: translateX(0%);
}
.signup-progress-bar__segment:last-child & {
transform: translateX(
calc(calc($progress-bar-circle-size + $progress-bar-line-width) - 100%)
);
}
}
&__step {
display: flex;
}
&__line {
transform: translateY(
calc(calc($progress-bar-circle-size + $progress-bar-line-width) / 2)
);
height: $progress-bar-line-width;
width: 100%;
background-color: var(--primary-low-mid);
}
&__circle {
flex-shrink: 0;
font-size: $progress-bar-icon-size;
color: var(--secondary);
display: flex;
justify-content: center;
align-items: center;
height: $progress-bar-circle-size;
width: $progress-bar-circle-size;
border-radius: 50%;
border: $progress-bar-line-width solid var(--primary-low-mid);
background-color: var(--secondary);
}
&__step.--completed {
color: var(--primary);
.signup-progress-bar__circle {
background-color: var(--success);
border: $progress-bar-line-width solid var(--success);
}
}
&__line.--completed {
background-color: var(--success);
}
&__step.--active {
.signup-progress-bar__circle {
border: $progress-bar-line-width solid var(--success);
}
+ .signup-progress-bar__step-text {
font-weight: bold;
}
}
}

View File

@ -0,0 +1,24 @@
.login-welcome-header {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr;
overflow-wrap: anywhere;
.login-title {
font-size: var(--font-up-6);
margin: 0;
line-height: var(--line-height-medium);
}
.login-subheader {
font-size: var(--font-up-1);
margin: 0;
}
.waving-hand {
width: 35px;
height: 35px;
margin-left: 0.5em;
align-self: center;
}
}

View File

@ -2,28 +2,8 @@
padding: 2rem 3rem;
background: var(--secondary);
.login-welcome-header {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr;
overflow-wrap: anywhere;
}
.login-title {
font-size: var(--font-up-6);
margin: 0;
}
.waving-hand {
width: 35px;
height: 35px;
margin-left: 0.5em;
align-self: center;
}
.login-subheader {
font-size: var(--font-up-1);
margin: 0;
.success-info p:last-child {
margin-bottom: 0;
}
.user-info {
@ -36,10 +16,6 @@
height: 30px;
}
.col-image {
display: none;
}
.create-account__password {
&-info {
display: flex;
@ -65,4 +41,17 @@
color: var(--primary-medium);
margin-top: 0.5em;
}
.invite-form form {
display: flex;
flex-direction: column;
.input-group {
&.email-input,
&.username-input,
&.name-input.name-required {
order: -1;
}
}
}
}

View File

@ -36,11 +36,6 @@
}
}
.login-subheader {
font-size: var(--font-up-1);
margin: 0;
}
.login-left-side {
box-sizing: border-box;
width: 100%;
@ -48,25 +43,6 @@
overflow: auto;
}
.login-welcome-header {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr;
}
.login-title {
font-size: var(--font-up-6);
margin: 0;
line-height: var(--line-height-medium);
}
.waving-hand {
width: 35px;
height: 35px;
margin-left: 0.5em;
align-self: center;
}
.login-right-side {
background: var(--tertiary-or-tertiary-low);
padding: 3.5rem 3rem;
@ -151,13 +127,12 @@
flex-direction: column;
overflow: auto;
gap: 1em;
.login-welcome-header,
.d-modal__footer {
font-size: var(--font-down-1);
}
.login-left-side {
overflow: unset;
padding: 1em 2.75em 1em 1em;
padding: 1em;
}
.login-right-side {
padding: 1em;
@ -166,6 +141,9 @@
#login-form {
margin: 1.5em 0;
}
.signup-progress-bar {
display: none;
}
}
}
}
@ -173,7 +151,14 @@
/* end shared styles */
.d-modal.create-account {
&:not(:has(.has-alt-auth)) .d-modal__container {
max-width: 500px;
}
.d-modal {
&__container {
width: 100%;
}
&__footer {
flex-direction: column;
align-items: flex-start;

View File

@ -1,8 +1,9 @@
.invite-page {
background: var(--primary-50);
background: var(--secondary);
}
.invites-show {
.invites-show,
#simple-container .invite-error {
max-width: 500px;
padding: 2rem 3rem;
background: var(--secondary);
@ -12,3 +13,14 @@
margin: 1em auto 1em auto;
}
}
#simple-container .invite-error {
.error-info {
text-align: center;
}
.error-image {
text-align: center;
padding-bottom: 1em;
}
}

View File

@ -4,3 +4,4 @@
@import "topic-map";
@import "user-card";
@import "user-stream-item";
@import "welcome-header";

View File

@ -0,0 +1,14 @@
.login-welcome-header {
.login-title {
font-size: var(--font-up-5);
}
.login-subheader {
font-size: var(--font-0);
}
.waving-hand {
width: 30px;
height: 30px;
}
}

View File

@ -97,3 +97,21 @@
display: none;
}
}
.d-modal.create-account {
.d-modal__footer-buttons {
flex-direction: row;
gap: 8px;
button {
width: auto;
}
}
.login-welcome-header {
padding-bottom: 0.25rem;
}
.signup-progress-bar {
display: none;
}
}

View File

@ -2336,6 +2336,11 @@ en:
associate: "Already have an account? <a href='%{associate_link}'>Log In</a> to link your %{provider} account."
activation_title: "Activate your account"
already_have_account: "Already have an account?"
progress_bar:
signup: "Sign Up"
activate: "Activate"
approve: "Approve"
login: "Log In"
forgot_password:
title: "Password Reset"

View File

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 47.5 47.5" style="enable-background:new 0 0 47.5 47.5;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,38 38,38 38,0 0,0 0,38 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,47.5)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(37,10)" id="g20"><path id="path22" style="fill:#ccd6dd;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,18 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(12.9497,19.3643)" id="g24"><path id="path26" style="fill:#99aab5;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 -11.313,-11.313 c -0.027,-0.028 -0.037,-0.063 -0.06,-0.091 0.34,-0.571 0.814,-1.043 1.384,-1.384 0.029,0.023 0.063,0.033 0.09,0.059 L 1.415,-1.414 c 0.39,0.391 0.39,1.022 0,1.414 C 1.023,0.391 0.391,0.391 0,0"/></g><g transform="translate(36.4229,7.96)" id="g28"><path id="path30" style="fill:#99aab5;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c -0.021,0.028 -0.033,0.063 -0.06,0.09 l -11.312,11.314 c -0.392,0.391 -1.024,0.391 -1.415,0 -0.391,-0.391 -0.391,-1.023 0,-1.414 L -1.474,-1.324 c 0.027,-0.027 0.062,-0.037 0.09,-0.06 C -0.812,-1.044 -0.34,-0.57 0,0"/></g><g transform="translate(33,32)" id="g32"><path id="path34" style="fill:#99aab5;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 -28,0 c -2.209,0 -4,-1.791 -4,-4 l 0,-1.03 14.528,-14.495 c 1.894,-1.894 4.988,-1.894 6.884,0 L 4,-5.009 4,-4 C 4,-1.791 2.209,0 0,0"/></g><g transform="translate(33,32)" id="g36"><path id="path38" style="fill:#e1e8ed;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 -28,0 c -1.588,0 -2.949,-0.934 -3.595,-2.275 l 14.766,-14.767 c 1.562,-1.562 4.096,-1.562 5.657,0 L 3.595,-2.275 C 2.949,-0.934 1.589,0 0,0"/></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -4,15 +4,15 @@ module PageObjects
module Pages
class ActivateAccount < PageObjects::Pages::Base
def click_activate_account
find("#activate-account-button").click
find(".activate-account-button").click
end
def click_continue
find(".perform-activation .continue-button").click
find(".account-activated .continue-button").click
end
def has_error?
has_css?("#simple-container .alert-error")
has_css?(".alert-error")
has_content?(I18n.t("js.user.activate_account.already_done"))
end
end

View File

@ -30,7 +30,7 @@ describe "Account activation", type: :system do
expect(user.reload.active).to eq(false)
find("#activate-account-button").click
find(".activate-account-button").click
wait_for(timeout: 5) { user.reload.active }
end