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

View File

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

View File

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

View File

@ -10,7 +10,6 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
import cookie, { removeCookie } from "discourse/lib/cookie"; import cookie, { removeCookie } from "discourse/lib/cookie";
import { wantsNewWindow } from "discourse/lib/intercept-click"; import { wantsNewWindow } from "discourse/lib/intercept-click";
import { areCookiesEnabled } from "discourse/lib/utilities"; import { areCookiesEnabled } from "discourse/lib/utilities";
import { wavingHandURL } from "discourse/lib/waving-hand-url";
import { import {
getPasskeyCredential, getPasskeyCredential,
isWebauthnSupported, isWebauthnSupported,
@ -64,10 +63,6 @@ export default class Login extends Component {
return this.loggingIn || this.loggedIn; return this.loggingIn || this.loggedIn;
} }
get wavingHandURL() {
return wavingHandURL();
}
get modalBodyClasses() { get modalBodyClasses() {
const classes = ["login-modal-body"]; const classes = ["login-modal-body"];
if (this.awaitingApproval) { 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 { action } from "@ember/object";
import { service } from "@ember/service"; import { service } from "@ember/service";
import { resendActivationEmail } from "discourse/lib/user-activation"; 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 discourseComputed from "discourse-common/utils/decorators";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
export default class AccountCreatedIndexController extends Controller { export default class AccountCreatedIndexController extends Controller {
@service router; @service router;
envelopeImageUrl = getUrl("/images/envelope.svg");
@discourseComputed @discourseComputed
welcomeTitle() { welcomeTitle() {
return I18n.t("invites.welcome_to", { return I18n.t("invites.welcome_to", {
@ -19,11 +15,6 @@ export default class AccountCreatedIndexController extends Controller {
}); });
} }
@discourseComputed
wavingHandURL() {
return wavingHandURL();
}
@action @action
sendActivationEmail() { sendActivationEmail() {
resendActivationEmail(this.get("accountCreated.username")).then(() => { 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 { extractError } from "discourse/lib/ajax-error";
import DiscourseURL from "discourse/lib/url"; import DiscourseURL from "discourse/lib/url";
import { emailValid } from "discourse/lib/utilities"; import { emailValid } from "discourse/lib/utilities";
import { wavingHandURL } from "discourse/lib/waving-hand-url";
import NameValidation from "discourse/mixins/name-validation"; import NameValidation from "discourse/mixins/name-validation";
import PasswordValidation from "discourse/mixins/password-validation"; import PasswordValidation from "discourse/mixins/password-validation";
import UserFieldsValidation from "discourse/mixins/user-fields-validation"; import UserFieldsValidation from "discourse/mixins/user-fields-validation";
@ -42,7 +41,6 @@ export default class InvitesShowController extends Controller.extend(
errorMessage = null; errorMessage = null;
userFields = null; userFields = null;
authOptions = null; authOptions = null;
inviteImageUrl = getUrl("/images/envelope.svg");
rejectedEmails = []; rejectedEmails = [];
maskPassword = true; maskPassword = true;
@ -261,11 +259,6 @@ export default class InvitesShowController extends Controller.extend(
return matchingProvider ? matchingProvider.get("prettyName") : providerName; return matchingProvider ? matchingProvider.get("prettyName") : providerName;
} }
@discourseComputed
wavingHandURL() {
return wavingHandURL();
}
@discourseComputed @discourseComputed
ssoPath() { ssoPath() {
return getUrl("/session/sso"); return getUrl("/session/sso");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -80,6 +80,7 @@ acceptance("Invite accept", function (needs) {
}); });
test("invite link", async function (assert) { test("invite link", async function (assert) {
this.siteSettings.login_required = true;
PreloadStore.store("invite_info", { PreloadStore.store("invite_info", {
invited_by: { invited_by: {
id: 123, 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) { test("invite name is required only if full name is required", async function (assert) {
preloadInvite(); preloadInvite();
await visit("/invites/my-valid-invite-token"); 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 // Styles used before the user is logged into discourse. For example, activating their
// account or changing their email. // account or changing their email.
.account-activation-page { .account-created-page,
.desktop-view & { .activate-account-page {
background: var(--primary-very-low); background: var(--secondary);
}
#main-outlet-wrapper {
display: block;
.sidebar-wrapper {
display: none;
}
}
.header-sidebar-toggle {
display: none;
}
#main-outlet { #main-outlet {
padding: 0; padding: 0;
height: 100%; 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; display: flex;
align-items: center; flex-wrap: wrap;
box-sizing: border-box; gap: 0.5em;
border-radius: 10px; margin-top: 1em;
width: 100%; }
padding: 20px;
justify-content: center; .edit-cancel {
margin-top: var(--header-offset); text-transform: capitalize;
.account-created { }
.ac-message {
font-size: var(--font-up-1); .success-info p:last-child {
line-height: var(--line-height-large); margin-bottom: 0;
}
.activation-controls {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
}
.ac-page {
border-radius: 10px;
margin-top: 25px;
}
}
} }
} }
.activate-account { .activate-account {
.activate-title { .activate-account-button,
text-align: center; .continue-button {
.waving-hand { margin-top: 1em;
height: 32px; margin-inline: auto;
margin-bottom: 13px;
}
}
#activate-account-button {
margin-top: 20px;
margin-left: auto;
margin-right: auto;
display: block; display: block;
width: fit-content;
} }
.perform-activation {
border-top: 6px solid var(--tertiary); .login-welcome-header {
box-shadow: 0 1px 10px 1px rgba(var(--primary-low-rgb), 1.25); margin-inline: auto;
border-radius: 10px; width: fit-content;
padding: 1em 2.5em 1em 2.5em; }
.image {
width: 150px; .tada-image {
margin: auto; width: 150px;
padding-bottom: 1em; 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 { .email-login {
border-radius: 10px; border-radius: 10px;
background-color: var(--secondary); background-color: var(--secondary);

View File

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

View File

@ -3,7 +3,8 @@
.has-full-page-chat &, .has-full-page-chat &,
.static-login &, .static-login &,
.invite-page &, .invite-page &,
.account-activation-page & { .account-created-page &,
.activate-account-page & {
display: none !important; display: none !important;
} }
grid-area: below-content; 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; padding: 2rem 3rem;
background: var(--secondary); background: var(--secondary);
.login-welcome-header { .success-info p:last-child {
display: grid; margin-bottom: 0;
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;
} }
.user-info { .user-info {
@ -36,10 +16,6 @@
height: 30px; height: 30px;
} }
.col-image {
display: none;
}
.create-account__password { .create-account__password {
&-info { &-info {
display: flex; display: flex;
@ -65,4 +41,17 @@
color: var(--primary-medium); color: var(--primary-medium);
margin-top: 0.5em; 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 { .login-left-side {
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
@ -48,25 +43,6 @@
overflow: auto; 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 { .login-right-side {
background: var(--tertiary-or-tertiary-low); background: var(--tertiary-or-tertiary-low);
padding: 3.5rem 3rem; padding: 3.5rem 3rem;
@ -151,13 +127,12 @@
flex-direction: column; flex-direction: column;
overflow: auto; overflow: auto;
gap: 1em; gap: 1em;
.login-welcome-header,
.d-modal__footer { .d-modal__footer {
font-size: var(--font-down-1); font-size: var(--font-down-1);
} }
.login-left-side { .login-left-side {
overflow: unset; overflow: unset;
padding: 1em 2.75em 1em 1em; padding: 1em;
} }
.login-right-side { .login-right-side {
padding: 1em; padding: 1em;
@ -166,6 +141,9 @@
#login-form { #login-form {
margin: 1.5em 0; margin: 1.5em 0;
} }
.signup-progress-bar {
display: none;
}
} }
} }
} }
@ -173,7 +151,14 @@
/* end shared styles */ /* end shared styles */
.d-modal.create-account { .d-modal.create-account {
&:not(:has(.has-alt-auth)) .d-modal__container {
max-width: 500px;
}
.d-modal { .d-modal {
&__container {
width: 100%;
}
&__footer { &__footer {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;

View File

@ -1,8 +1,9 @@
.invite-page { .invite-page {
background: var(--primary-50); background: var(--secondary);
} }
.invites-show { .invites-show,
#simple-container .invite-error {
max-width: 500px; max-width: 500px;
padding: 2rem 3rem; padding: 2rem 3rem;
background: var(--secondary); background: var(--secondary);
@ -12,3 +13,14 @@
margin: 1em auto 1em auto; 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 "topic-map";
@import "user-card"; @import "user-card";
@import "user-stream-item"; @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; 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." associate: "Already have an account? <a href='%{associate_link}'>Log In</a> to link your %{provider} account."
activation_title: "Activate your account" activation_title: "Activate your account"
already_have_account: "Already have an account?" already_have_account: "Already have an account?"
progress_bar:
signup: "Sign Up"
activate: "Activate"
approve: "Approve"
login: "Log In"
forgot_password: forgot_password:
title: "Password Reset" 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 module Pages
class ActivateAccount < PageObjects::Pages::Base class ActivateAccount < PageObjects::Pages::Base
def click_activate_account def click_activate_account
find("#activate-account-button").click find(".activate-account-button").click
end end
def click_continue def click_continue
find(".perform-activation .continue-button").click find(".account-activated .continue-button").click
end end
def has_error? def has_error?
has_css?("#simple-container .alert-error") has_css?(".alert-error")
has_content?(I18n.t("js.user.activate_account.already_done")) has_content?(I18n.t("js.user.activate_account.already_done"))
end end
end end

View File

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