[IMPROVE] Setup Wizard username validation, step progress and optin/optout (#11254)

Closes #11198 

- [x] Show all steps on info box
- [x] Properly display errors in registration process of admin user
- [x] Break steps in subtemplates
- [x] Redesign "Register Server" step
      ![Register Server](https://user-images.githubusercontent.com/2263066/42013253-cd691bc2-7a72-11e8-8832-fe9ccd9e89b4.png)
- [x] Update end-to-end tests
pull/11343/head
Tasso Evangelista 7 years ago committed by Rodrigo Nascimento
parent a0da53ba57
commit afa2d434ea
No known key found for this signature in database
GPG Key ID: CFCE33B7B01AC335
  1. 14
      packages/rocketchat-i18n/i18n/en.i18n.json
  2. 3
      packages/rocketchat-lib/server/startup/settings.js
  3. 18
      packages/rocketchat-setup-wizard/client/final.html
  4. 51
      packages/rocketchat-setup-wizard/client/final.js
  5. 360
      packages/rocketchat-setup-wizard/client/setupWizard.html
  6. 509
      packages/rocketchat-setup-wizard/client/setupWizard.js
  7. 4
      packages/rocketchat-setup-wizard/package.js
  8. 18
      packages/rocketchat-setup-wizard/server/getSetupWizardParameters.js
  9. 9
      packages/rocketchat-setup-wizard/server/lib/getWizardSettings.js
  10. 143
      packages/rocketchat-theme/client/imports/components/setup-wizard.css
  11. 18
      tests/end-to-end/ui/00-login.js
  12. 2
      tests/pageobjects/setup-wizard.page.js

@ -234,6 +234,7 @@
"Allow_Invalid_SelfSigned_Certs": "Allow Invalid Self-Signed Certs",
"Allow_Invalid_SelfSigned_Certs_Description": "Allow invalid and self-signed SSL certificate's for link validation and previews.",
"Allow_switching_departments": "Allow Visitor to Switch Departments",
"Allow_Marketing_Emails": "Allow Marketing Emails",
"Alphabetical": "Alphabetical",
"Always_open_in_new_window": "Always Open in New Window",
"Analytics_features_enabled": "Features Enabled",
@ -1370,6 +1371,7 @@
"InternalHubot_Username_Description": "This must be a valid username of a bot registered on your server.",
"Invalid_confirm_pass": "The password confirmation does not match password",
"Invalid_email": "The email entered is invalid",
"Invalid_username": "The username entered is invalid",
"Invalid_Export_File": "The file uploaded isn't a valid %s export file.",
"Invalid_Import_File_Type": "Invalid Import file type.",
"Invalid_name": "The name must not be empty",
@ -2035,6 +2037,17 @@
"Regenerate_codes": "Regenerate codes",
"Register": "Register a new account",
"Register_Server": "Register Server",
"Register_Server_Info": "Use the preconfigured gateways and proxies provided by Rocket.Chat Technologies Corp.",
"Register_Server_Registered": "Register to access",
"Register_Server_Registered_Push_Notifications": "Mobile push notifications gateway",
"Register_Server_Registered_Livechat": "Livechat omnichannel proxy",
"Register_Server_Registered_OAuth": "OAuth proxy for social network",
"Register_Server_Registered_Marketplace": "Apps Marketplace",
"Register_Server_Opt_In": "Newsletter, offers and product updates",
"Register_Server_Standalone": "Keep standalone, you'll need to",
"Register_Server_Standalone_Service_Providers": "Create accounts with service providers",
"Register_Server_Standalone_Update_Settings": "Update the preconfigured settings",
"Register_Server_Standalone_Own_Certificates": "Recompile the mobile apps with your own certificates",
"Registration": "Registration",
"Registration_Succeeded": "Registration Succeeded",
"Registration_via_Admin": "Registration via Admin",
@ -2215,6 +2228,7 @@
"Shared_Location": "Shared Location",
"Should_be_a_URL_of_an_image": "Should be a URL of an image.",
"Should_exists_a_user_with_this_username": "The user must already exist.",
"Show_Setup_Wizard": "Show Setup Wizard",
"Show_agent_email": "Show agent email",
"Show_all": "Show All",
"Show_Avatars": "Show Avatars",

@ -2902,6 +2902,9 @@ RocketChat.settings.addGroup('Setup_Wizard', function() {
order: 2
}
});
this.add('Allow_Marketing_Emails', true, {
type: 'boolean'
});
});
});

@ -0,0 +1,18 @@
<template name="setupWizardFinal">
<div class="setup-wizard">
<div class="setup-wizard-final">
<header class="setup-wizard-info__header setup-wizard-final__header">
<img class="setup-wizard-info__header-logo" src="images/logo/logo-black.svg">
</header>
<main class="setup-wizard-final__box">
<span class="setup-wizard-forms__header-step">{{_ "Launched_successfully"}}</span>
<h1 class="setup-wizard-info__content-title setup-wizard-final__box-title">{{_ "Your_workspace_is_ready"}}</h1>
<span class="setup-wizard-final__link-text">{{_ "Your_server_link"}}</span>
<span class="setup-wizard-final__link">{{siteUrl}}</span>
<button class="rc-button rc-button--primary js-finish">
<span>{{_ "Go_to_your_workspace"}}</span>
</button>
</main>
</div>
</div>
</template>

@ -0,0 +1,51 @@
Template.setupWizardFinal.onCreated(function() {
const isSetupWizardDone = localStorage.getItem('wizardFinal');
if (isSetupWizardDone === null) {
FlowRouter.go('setup-wizard');
}
this.autorun(c => {
const showSetupWizard = RocketChat.settings.get('Show_Setup_Wizard');
if (!showSetupWizard) {
// Setup Wizard state is not defined yet
return;
}
const userId = Meteor.userId();
const user = userId && RocketChat.models.Users.findOne(userId, { fields: { status: true } });
if (userId && (!user || !user.status)) {
// User and its status are not defined yet
return;
}
c.stop();
const isComplete = showSetupWizard === 'completed';
const noUserLoggedInAndIsNotPending = !userId && showSetupWizard !== 'pending';
const userIsLoggedButIsNotAdmin = userId && !RocketChat.authz.hasRole(userId, 'admin');
if (isComplete || noUserLoggedInAndIsNotPending || userIsLoggedButIsNotAdmin) {
FlowRouter.go('home');
return;
}
});
});
Template.setupWizardFinal.onRendered(function() {
$('#initial-page-loading').remove();
});
Template.setupWizardFinal.events({
'click .js-finish'() {
RocketChat.settings.set('Show_Setup_Wizard', 'completed', function() {
localStorage.removeItem('wizard');
localStorage.removeItem('wizardFinal');
FlowRouter.go('home');
});
}
});
Template.setupWizardFinal.helpers({
siteUrl() {
return RocketChat.settings.get('Site_Url');
}
});

@ -1,196 +1,214 @@
<template name="setupWizardFinal">
<div class="setup-wizard">
<div class="setup-wizard-final">
<header class="setup-wizard-info__header setup-wizard-final__header">
<img class="setup-wizard-info__header-logo" src="images/logo/logo-black.svg">
</header>
<main class="setup-wizard-final__box">
<span class="setup-wizard-forms__header-step">{{_ "Launched_successfully"}}</span>
<h1 class="setup-wizard-info__content-title setup-wizard-final__box-title">{{_ "Your_workspace_is_ready"}}</h1>
<span class="setup-wizard-final__link-text">{{_ "Your_server_link"}}</span>
<span class="setup-wizard-final__link">{{siteUrl}}</span>
<button class="rc-button rc-button--primary js-finish">
<span>{{_ "Go_to_your_workspace"}}</span>
</button>
</main>
</div>
</div>
</template>
<template name="setupWizard">
<div class="rc-old connection-status">
{{> status}}
</div>
<div class="setup-wizard">
<section class="setup-wizard-info">
<header class="setup-wizard-info__header">
<img class="setup-wizard-info__header-logo" src="images/logo/logo-black.svg">
<span class="setup-wizard-info__header-tag">{{_ "Setup_Wizard"}}</span>
</header>
<div class="setup-wizard-info__content">
<h1 class="setup-wizard-info__content-title">{{_ "Setup_Wizard"}}</h1>
<p class="setup-wizard-info__content-text">{{_ "Setup_Wizard_Info"}}</p>
<ol class="setup-wizard-info__steps">
{{#unless hasAdmin}}<li class="setup-wizard-info__steps-item {{itemModifier 1}}">{{headerTitle 1}}</li>{{/unless}}
<li class="setup-wizard-info__steps-item {{itemModifier 2}}">{{headerTitle 2}}{{#unless hasAdmin}}<span class="setup-wizard-info__steps-item-bonding"></span>{{/unless}}</li>
<li class="setup-wizard-info__steps-item {{itemModifier 3}}">{{headerTitle 3}}<span class="setup-wizard-info__steps-item-bonding"></span></li>
{{#unless hasAdmin}}<li class="setup-wizard-info__steps-item {{itemModifier 4}}">{{headerTitle 4}}<span class="setup-wizard-info__steps-item-bonding"></span></li>{{/unless}}
</ol>
</div>
</section>
{{> setupWizardInfo infoArgs}}
<section class="setup-wizard-forms">
<div class="setup-wizard-forms__wrapper">
<div class="setup-wizard-forms__box">
<form class="setup-wizard-forms__box {{formLoadStateClass}}" novalidate>
<header class="setup-wizard-forms__header">
<span class="setup-wizard-forms__header-step">{{_ "Step"}} {{currentStep}}</span>
<h1 class="setup-wizard-forms__header-title">{{headerTitle}}</h1>
<h1 class="setup-wizard-forms__header-title">{{_ currentStepTitle}}</h1>
</header>
<main class="setup-wizard-forms__content">
<div class="setup-wizard-forms__content-step {{#if $eq currentStep 1}}setup-wizard-forms__content-step--active{{/if}}">
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Name"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-sv" icon="user"}}
</div>
<input type="text" class="rc-input__element js-setting-data" name="registration-name" placeholder="{{_ 'Type_your_name'}}" value="{{getValue 'registration-name'}}">
</div>
</label>
</div>
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Username"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-sv" icon="at"}}
</div>
<input type="text" class="rc-input__element js-setting-data" name="registration-username" placeholder="{{_ 'Type_your_username'}}" value="{{getValue 'registration-username'}}">
</div>
</label>
</div>
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Organization_Email"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-sv" icon="mail"}}
</div>
<input type="email" class="rc-input__element js-setting-data" name="registration-email" placeholder="{{_ 'Type_your_email'}}" value="{{getValue 'registration-email'}}">
</div>
</label>
{{#if invalidEmail}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{_ "Invalid_email"}}</div>
</div>
{{/if}}
</div>
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Password"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-sv" icon="key"}}
</div>
<input type="password" class="rc-input__element js-setting-data" name="registration-pass" placeholder="{{_ 'Type_your_password'}}">
</div>
</label>
</div>
</div>
<div class="setup-wizard-forms__content-step {{showStep}}">
{{#each getSettings currentStep}}
{{#if $eq type 'select'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-select">
<select class="rc-select__element js-setting-data" name="{{_id}}">
<option value="" disabled selected="{{selectedValue _id undefined}}">{{_ "Select_an_option"}}</option>
{{#each values}}
<option class="rc-select__option" value="{{key}}" selected="{{selectedValue ../_id key}}">{{_ i18nLabel}}</option>
{{/each}}
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
</label>
</div>
{{/if}}
{{#if $eq type 'string'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-input__wrapper">
<input type="text" class="rc-input__element js-setting-data" name="{{_id}}" value="{{getValue _id}}">
</div>
</label>
</div>
{{/if}}
{{#if $eq type 'language'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-select">
<select class="rc-select__element js-setting-data" name="{{_id}}">
{{#each languages}}
<option class="rc-select__option" value="{{key}}" selected="{{selectedValue ../_id key}}" dir="auto">{{name}}</option>
{{/each}}
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
</label>
</div>
{{/if}}
{{/each}}
</div>
<div class="setup-wizard-forms__content-step {{#if $eq currentStep 4}}setup-wizard-forms__content-step--active{{/if}}">
<p class="setup-wizard-forms__content-text">To receive free Rocket.Chat supplied features you need to register your server.</p>
<form class="setup-wizard-forms__content-register">
<label class="setup-wizard-forms__content-register-option">
<div class="setup-wizard-forms__content-register-radio">
<input type="radio" name="registerServer" value="true" class="setup-wizard-forms__content-register-radio-element js-setting-data" checked>
<span class="setup-wizard-forms__content-register-radio-fake"></span>
<span class="setup-wizard-forms__content-register-radio-text">Registered Server</span>
</div>
<ul>
<li class="setup-wizard-forms__content-register-item">{{> icon block="setup-wizard-forms__content-register-radio-icon" icon="check"}}Mobile push notifications</li>
<li class="setup-wizard-forms__content-register-item">{{> icon block="setup-wizard-forms__content-register-radio-icon" icon="check"}}Omnichannel proxy</li>
<li class="setup-wizard-forms__content-register-item">{{> icon block="setup-wizard-forms__content-register-radio-icon" icon="check"}}Social Network oAuth proxy</li>
<li class="setup-wizard-forms__content-register-item">{{> icon block="setup-wizard-forms__content-register-radio-icon" icon="check"}}Apps Marketplace</li>
</ul>
</label>
<label class="setup-wizard-forms__content-register-option">
<div class="setup-wizard-forms__content-register-radio">
<input type="radio" name="registerServer" value="false" class="setup-wizard-forms__content-register-radio-element js-setting-data">
<span class="setup-wizard-forms__content-register-radio-fake"></span>
<span class="setup-wizard-forms__content-register-radio-text">Standalone Server</span>
</div>
<ul>
<li class="setup-wizard-forms__content-register-item">{{> icon block="setup-wizard-forms__content-register-radio-icon" icon="cross"}}Mobile push notifications</li>
<li class="setup-wizard-forms__content-register-item">{{> icon block="setup-wizard-forms__content-register-radio-icon" icon="cross"}}Omnichannel proxy</li>
<li class="setup-wizard-forms__content-register-item">{{> icon block="setup-wizard-forms__content-register-radio-icon" icon="cross"}}Social Network oAuth proxy</li>
<li class="setup-wizard-forms__content-register-item">{{> icon block="setup-wizard-forms__content-register-radio-icon" icon="cross"}}Apps Marketplace</li>
</ul>
</label>
</form>
</div>
<main class="setup-wizard-forms__content">
{{> setupWizardAdminInfo adminInfoArgs}}
{{> setupWizardCustomStep (customStepArgs 2)}}
{{> setupWizardCustomStep (customStepArgs 3)}}
{{> setupWizardRegisterServer registerServerArgs}}
</main>
<footer class="setup-wizard-forms__footer">
{{#if showBackButton}}
<button class="rc-button rc-button--secondary setup-wizard-forms__footer-back">
<span>{{_ "Back"}}</span>
</button>
{{/if}}
<button class="rc-button rc-button--primary setup-wizard-forms__footer-next" {{isDisabled}}>
<button type="submit" class="rc-button rc-button--primary setup-wizard-forms__footer-next" disabled={{isContinueDisabled}}>
<span>{{_ "Continue"}}</span>
</button>
</footer>
</div>
</form>
</div>
</section>
</div>
</template>
<template name="setupWizardInfo">
<section class="setup-wizard-info">
<header class="setup-wizard-info__header">
<img class="setup-wizard-info__header-logo" src="images/logo/logo-black.svg">
<span class="setup-wizard-info__header-tag">{{_ "Setup Wizard"}}</span>
</header>
<div class="setup-wizard-info__content">
<h1 class="setup-wizard-info__content-title">{{_ "Setup_Wizard"}}</h1>
<p class="setup-wizard-info__content-text">{{_ "Setup_Wizard_Info"}}</p>
<ol class="setup-wizard-info__steps">
<li class="setup-wizard-info__steps-item {{stepItemModifier 1}}">{{_ (stepTitle 1)}}</li>
<li class="setup-wizard-info__steps-item {{stepItemModifier 2}}">{{_ (stepTitle 2)}}<span class="setup-wizard-info__steps-item-bonding"></span></li>
<li class="setup-wizard-info__steps-item {{stepItemModifier 3}}">{{_ (stepTitle 3)}}<span class="setup-wizard-info__steps-item-bonding"></span></li>
<li class="setup-wizard-info__steps-item {{stepItemModifier 4}}">{{_ (stepTitle 4)}}<span class="setup-wizard-info__steps-item-bonding"></span></li>
</ol>
</div>
</section>
</template>
<template name="setupWizardAdminInfo">
<div class="setup-wizard-forms__content-step {{#if $eq currentStep 1}}setup-wizard-forms__content-step--active{{/if}}">
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Name"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-sv" icon="user"}}
</div>
<input type="text" class="rc-input__element js-setting-data" name="registration-name" placeholder="{{_ 'Type_your_name'}}" value="{{name}}">
</div>
</label>
</div>
<div class="rc-input {{#if invalidUsername}}rc-input--error{{/if}}">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Username"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-sv" icon="at"}}
</div>
<input type="text" class="rc-input__element js-setting-data" name="registration-username" placeholder="{{_ 'Type_your_username'}}" value="{{username}}">
</div>
</label>
{{#if invalidUsername}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{_ "Invalid_username"}}</div>
</div>
{{/if}}
</div>
<div class="rc-input {{#if invalidEmail}}rc-input--error{{/if}}">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Organization_Email"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-sv" icon="mail"}}
</div>
<input type="email" class="rc-input__element js-setting-data" name="registration-email" placeholder="{{_ 'Type_your_email'}}" value="{{email}}">
</div>
</label>
{{#if invalidEmail}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{_ "Invalid_email"}}</div>
</div>
{{/if}}
</div>
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Password"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-sv" icon="key"}}
</div>
<input type="password" class="rc-input__element js-setting-data" name="registration-pass" placeholder="{{_ 'Type_your_password'}}" value="{{password}}">
</div>
</label>
</div>
</div>
</template>
<template name="setupWizardRegisterServer">
<div class="setup-wizard-forms__content-step {{#if $eq currentStep 4}}setup-wizard-forms__content-step--active{{/if}}">
<p class="setup-wizard-forms__content-text">{{_ "Register_Server_Info"}}</p>
<form class="setup-wizard-forms__content-register">
<label class="setup-wizard-forms__content-register-option {{#if registerServer}}setup-wizard-forms__content-register-option--selected{{/if}}">
<div class="setup-wizard-forms__content-register-radio">
<input type="radio" name="registerServer" value="true" class="setup-wizard-forms__content-register-radio-element" checked="{{registerServer}}">
<span class="setup-wizard-forms__content-register-radio-fake"></span>
<span class="setup-wizard-forms__content-register-radio-text">{{_ "Register_Server_Registered"}}</span>
</div>
<ul class="setup-wizard-forms__content-register-items">
<li class="setup-wizard-forms__content-register-item">{{> icon block="setup-wizard-forms__content-register-radio-icon" icon="check"}}{{_ "Register_Server_Registered_Push_Notifications"}}</li>
<li class="setup-wizard-forms__content-register-item">{{> icon block="setup-wizard-forms__content-register-radio-icon" icon="check"}}{{_ "Register_Server_Registered_Livechat"}}</li>
<li class="setup-wizard-forms__content-register-item">{{> icon block="setup-wizard-forms__content-register-radio-icon" icon="check"}}{{_ "Register_Server_Registered_OAuth"}}</li>
<li class="setup-wizard-forms__content-register-item">{{> icon block="setup-wizard-forms__content-register-radio-icon" icon="check"}}{{_ "Register_Server_Registered_Marketplace"}}</li>
</ul>
<div>
<label class="setup-wizard-forms__content-register-checkbox">
<input type="checkbox" name="optIn" value="true" class="setup-wizard-forms__content-register-checkbox-element" checked="{{optIn}}" disabled={{$not registerServer}}>
<span class="setup-wizard-forms__content-register-checkbox-fake">
{{> icon block="setup-wizard-forms__content-register-checkbox-fake-icon" icon="check"}}
</span>
<span class="setup-wizard-forms__content-register-checkbox-text">{{_ "Register_Server_Opt_In"}}</span>
</label>
</div>
</label>
<label class="setup-wizard-forms__content-register-option {{#if $not registerServer}}setup-wizard-forms__content-register-option--selected{{/if}} {{#if $not allowStandaloneServer}}setup-wizard-forms__content-register-option--disabled{{/if}}">
<div class="setup-wizard-forms__content-register-radio">
<input type="radio" name="registerServer" value="false" class="setup-wizard-forms__content-register-radio-element" checked="{{$not registerServer}}" disabled={{$not allowStandaloneServer}}>
<span class="setup-wizard-forms__content-register-radio-fake"></span>
<span class="setup-wizard-forms__content-register-radio-text">{{_ "Register_Server_Standalone"}}</span>
</div>
<ul class="setup-wizard-forms__content-register-items">
<li class="setup-wizard-forms__content-register-item">{{> icon block="setup-wizard-forms__content-register-radio-icon" icon="circle"}}{{_ "Register_Server_Standalone_Service_Providers"}}</li>
<li class="setup-wizard-forms__content-register-item">{{> icon block="setup-wizard-forms__content-register-radio-icon" icon="circle"}}{{_ "Register_Server_Standalone_Update_Settings"}}</li>
<li class="setup-wizard-forms__content-register-item">{{> icon block="setup-wizard-forms__content-register-radio-icon" icon="circle"}}{{_ "Register_Server_Standalone_Own_Certificates"}}</li>
</ul>
</label>
</form>
</div>
</template>
<template name="setupWizardCustomStep">
<div class="setup-wizard-forms__content-step {{#if $eq currentStep step}}setup-wizard-forms__content-step--active{{/if}}">
{{#each settings}}
{{#if $eq type 'string'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ label}}</div>
<div class="rc-input__wrapper">
<input type="text" class="rc-input__element js-setting-data" name="{{id}}" value="{{value}}">
</div>
</label>
</div>
{{/if}}
{{#if $eq type 'select'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ label}}</div>
<div class="rc-select">
<select class="rc-select__element js-setting-data" name="{{id}}">
<option value="" disabled selected="{{this.isValueSelected undefined}}">{{_ "Select_an_option"}}</option>
{{#each options}}
<option class="rc-select__option" value="{{optionValue}}" selected="{{this.isValueSelected optionValue}}">{{_ optionLabel}}</option>
{{/each}}
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
</label>
</div>
{{/if}}
{{#if $eq type 'language'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ label}}</div>
<div class="rc-select">
<select class="rc-select__element js-setting-data" name="{{id}}">
<option value="" disabled selected="{{this.isValueSelected undefined}}">{{_ "Default"}}</option>
{{#each options}}
<option class="rc-select__option" value="{{optionValue}}" selected="{{this.isValueSelected optionValue}}" dir="auto">{{_ optionLabel}}</option>
{{/each}}
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
</label>
</div>
{{/if}}
{{/each}}
</div>
</template>

@ -1,90 +1,135 @@
import s from 'underscore.string';
const cannotSetup = () => {
const showSetupWizard = RocketChat.settings.get('Show_Setup_Wizard');
if (!showSetupWizard) {
// Setup Wizard state is not defined yet
return;
}
const setSettingsAndGo = (settings, registerServer = true) => {
const settingsFilter = Object.entries(settings)
.filter(([key]) => !/registration-|registerServer|currentStep/.test(key))
.map(([_id, value]) => ({_id, value}));
const userId = Meteor.userId();
const user = userId && RocketChat.models.Users.findOne(userId, { fields: { status: true } });
if (userId && (!user || !user.status)) {
// User and its status are not defined yet
return;
}
settingsFilter.push({
_id: 'Statistics_reporting',
value: registerServer
});
const isComplete = showSetupWizard === 'completed';
const noUserLoggedInAndIsNotPending = !userId && showSetupWizard !== 'pending';
const userIsLoggedButIsNotAdmin = userId && !RocketChat.authz.hasRole(userId, 'admin');
RocketChat.settings.batchSet(settingsFilter, function(err) {
if (err) {
return handleError(err);
return isComplete || noUserLoggedInAndIsNotPending || userIsLoggedButIsNotAdmin;
};
const registerAdminUser = (state, callback) => {
const registrationData = Object.entries(state)
.filter(([ key ]) => /registration-/.test(key))
.map(([ key, value ]) => ([ key.replace('registration-', ''), value ]))
.reduce((o, [ key, value ]) => ({ ...o, [key]: value }), {});
Meteor.call('registerUser', registrationData, error => {
if (error) {
return handleError(error);
}
localStorage.setItem('wizardFinal', true);
FlowRouter.go('setup-wizard-final');
RocketChat.callbacks.run('userRegistered');
Meteor.loginWithPassword(registrationData.email, registrationData.pass, error => {
if (error) {
if (error.error === 'error-invalid-email') {
toastr.success(t('We_have_sent_registration_email'));
return false;
} else {
return handleError(error);
}
}
Session.set('forceLogin', false);
Meteor.call('setUsername', registrationData.username, error => {
if (error) {
return handleError(error);
}
RocketChat.callbacks.run('usernameSet');
callback && callback();
});
});
});
};
Template.setupWizard.onCreated(function() {
const userId = Meteor.userId();
this.autorun((c) => {
const Show_Setup_Wizard = RocketChat.settings.get('Show_Setup_Wizard');
const user = Meteor.user();
const persistSettings = (state, callback) => {
const settings = Object.entries(state)
.filter(([ key ]) => !/registration-|registerServer|optIn|currentStep|invalidUsername|invalidEmail/.test(key))
.map(([ _id, value ]) => ({ _id, value }))
.concat([
{
_id: 'Statistics_reporting',
value: state['registerServer']
},
{
_id: 'Allow_Marketing_Emails',
value: state['optIn']
}
]);
// Wait for roles and setup wizard setting
if ((userId && (!user || !user.status)) || !Show_Setup_Wizard) {
return;
RocketChat.settings.batchSet(settings, error => {
if (error) {
return handleError(error);
}
c.stop();
if ((!userId && Show_Setup_Wizard !== 'pending') || Show_Setup_Wizard === 'completed' || (userId && !RocketChat.authz.hasRole(userId, 'admin'))) {
FlowRouter.go('home');
}
callback && callback();
});
};
Template.setupWizard.onCreated(function() {
this.state = new ReactiveDict();
this.state.set('currentStep', 1);
this.state.set('registerServer', true);
this.state.set('optIn', true);
this.wizardSettings = new ReactiveVar([]);
this.allowStandaloneServer = new ReactiveVar(false);
if (localStorage.getItem('wizardFinal')) {
FlowRouter.go('setup-wizard-final');
return;
}
this.hasAdmin = new ReactiveVar(false);
this.state = new ReactiveDict();
this.wizardSettings = new ReactiveVar([]);
this.invalidEmail = new ReactiveVar(false);
const jsonString = localStorage.getItem('wizard');
const state = jsonString && JSON.parse(jsonString) || {};
Object.entries(state).forEach(entry => this.state.set(...entry));
const storage = JSON.parse(localStorage.getItem('wizard'));
if (storage) {
Object.entries(storage).forEach(([key, value]) => {
this.state.set(key, value);
});
}
this.autorun(c => {
const cantSetup = cannotSetup();
if (typeof cantSetup === 'undefined') {
return;
}
this.autorun(() => {
const user = Meteor.user();
if (user) {
if (!this.hasAdmin.get()) {
if (user.roles && user.roles.includes('admin')) {
this.state.set('currentStep', 2);
this.hasAdmin.set(true);
} else {
this.hasAdmin.set(false);
}
}
if (cantSetup) {
c.stop();
FlowRouter.go('home');
return;
}
const state = this.state.all();
state['registration-pass'] = '';
localStorage.setItem('wizard', JSON.stringify(state));
Meteor.call('getWizardSettings', (error, result) => {
if (Meteor.userId()) {
Meteor.call('getSetupWizardParameters', (error, { settings, allowStandaloneServer }) => {
if (error) {
return handleError(error);
}
this.wizardSettings.set(result);
this.wizardSettings.set(settings);
this.allowStandaloneServer.set(allowStandaloneServer);
});
} else {
this.state.set('currentStep', 1);
}
if (RocketChat.settings.get('Show_Setup_Wizard') === 'completed') {
FlowRouter.go('home');
if (this.state.get('currentStep') === 1) {
this.state.set('currentStep', 2);
} else {
this.state.set('registration-pass', '');
}
} else if (this.state.get('currentStep') !== 1) {
this.state.set('currentStep', 1);
}
const states = this.state.all();
states['registration-pass'] = '';
localStorage.setItem('wizard', JSON.stringify(states));
});
});
@ -93,67 +138,83 @@ Template.setupWizard.onRendered(function() {
});
Template.setupWizard.events({
'submit .setup-wizard-forms__box'() {
return false;
},
'click .setup-wizard-forms__footer-next'(e, t) {
const currentStep = t.state.get('currentStep');
const hasAdmin = t.hasAdmin.get();
if (!hasAdmin && currentStep === 1) {
const emailValue = t.state.get('registration-email');
const invalidEmail = !/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]+\b/i.test(emailValue);
t.invalidEmail.set(invalidEmail);
switch (t.state.get('currentStep')) {
case 1: {
const usernameValue = t.state.get('registration-username');
const usernameRegex = new RegExp(`^${ RocketChat.settings.get('UTF8_Names_Validation') }$`);
t.state.set('invalidUsername', !usernameRegex.test(usernameValue));
const emailValue = t.state.get('registration-email');
const emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]+$/i;
t.state.set('invalidEmail', !emailRegex.test(emailValue));
if (t.state.get('invalidUsername') || t.state.get('invalidEmail')) {
return false;
}
if (invalidEmail) {
registerAdminUser(t.state.all(), () => t.state.set('currentStep', 2));
return false;
}
const state = t.state.all();
const registration = Object.entries(state).filter(([key]) => /registration-/.test(key));
const registrationData = Object.assign(...registration.map(d => ({[d[0].replace('registration-', '')]: d[1]})));
Meteor.call('registerUser', registrationData, error => {
if (error) {
return handleError(error);
}
RocketChat.callbacks.run('userRegistered');
Meteor.loginWithPassword(s.trim(registrationData.email), registrationData.pass, error => {
if (error && error.error === 'error-invalid-email') {
toastr.success(t('We_have_sent_registration_email'));
return false;
}
Session.set('forceLogin', false);
Meteor.call('setUsername', registrationData.username, error => {
if (error) {
return handleError(error);
}
RocketChat.callbacks.run('usernameSet');
});
case 2: {
t.state.set('currentStep', 3);
return false;
}
case 3: {
t.state.set('currentStep', 4);
return false;
}
case 4: {
persistSettings(t.state.all(), () => {
localStorage.removeItem('wizard');
localStorage.setItem('wizardFinal', true);
FlowRouter.go('setup-wizard-final');
});
});
return false;
}
}
if (hasAdmin && currentStep === 3) {
setSettingsAndGo(t.state.all());
return false;
return false;
},
'click .setup-wizard-forms__footer-back'(e, t) {
switch (t.state.get('currentStep')) {
case 2:
t.state.set('currentStep', 1);
break;
case 3:
t.state.set('currentStep', 2);
break;
case 4:
t.state.set('currentStep', 3);
break;
}
if (currentStep === 4) {
setSettingsAndGo(t.state.all(), JSON.parse(t.state.get('registerServer') || true));
return false;
},
'input .js-setting-data'({ currentTarget: { name, value } }, t) {
t.state.set(name, value);
},
'click input[name="registerServer"]'({ currentTarget: { value } }, t) {
const oldValue = t.state.get('registerServer');
const newValue = value === 'true';
t.state.set('registerServer', newValue);
return false;
if (!oldValue && newValue) {
t.state.set('optIn', true);
}
t.state.set('currentStep', currentStep + 1);
},
'click .setup-wizard-forms__footer-back'(e, t) {
t.state.set('currentStep', t.state.get('currentStep') - 1);
if (!newValue) {
t.state.set('optIn', false);
}
return false;
},
'input .js-setting-data'(e, t) {
t.state.set(e.currentTarget.name, e.currentTarget.value);
'click input[name="optIn"]'({ currentTarget: { checked } }, t) {
t.state.set('optIn', checked);
return false;
}
});
@ -161,144 +222,138 @@ Template.setupWizard.helpers({
currentStep() {
return Template.instance().state.get('currentStep');
},
itemModifier(step) {
const current = Template.instance().state.get('currentStep');
if (current === step) {
return 'setup-wizard-info__steps-item--active';
}
if (current > step) {
return 'setup-wizard-info__steps-item--past';
currentStepTitle() {
switch (Template.instance().state.get('currentStep')) {
case 1:
return 'Admin_Info';
case 2:
return 'Organization_Info';
case 3:
return 'Server_Info';
case 4:
return 'Register_Server';
}
return '';
},
getValue(name) {
return Template.instance().state.get(name);
},
selectedValue(setting, optionValue) {
return Template.instance().state.get(setting) === optionValue;
},
isDisabled() {
const user = Meteor.user();
if (user && user.roles && !user.roles.includes('admin')) {
return 'disabled';
}
if (Template.instance().state.get('currentStep') === 1) {
const state = Template.instance().state.all();
if (Object.entries(state).filter(([key, value]) => /registration-/.test(key) && !value).length) {
return 'disabled';
}
formLoadStateClass() {
switch (Template.instance().state.get('currentStep')) {
case 1:
return RocketChat.settings.get('Show_Setup_Wizard') === 'pending' && 'setup-wizard-forms__box--loaded';
case 2:
case 3:
return Template.instance().wizardSettings.get().length > 0 && 'setup-wizard-forms__box--loaded';
case 4:
return 'setup-wizard-forms__box--loaded';
}
return '';
},
headerTitle(step) {
if (!step) {
step = Template.instance().state.get('currentStep');
showBackButton() {
switch (Template.instance().state.get('currentStep')) {
case 3:
return true;
case 4:
return true;
}
switch (step) {
case 1: return t('Admin_Info');
case 2: return t('Organization_Info');
case 3: return t('Server_Info');
case 4: return t('Register_Server');
}
return false;
},
showStep() {
const currentStep = Template.instance().state.get('currentStep');
if (currentStep === 2 || currentStep === 3) {
return 'setup-wizard-forms__content-step--active';
isContinueDisabled() {
switch (Template.instance().state.get('currentStep')) {
case 1:
return Object.entries(Template.instance().state.all())
.filter(([key, value]) => /registration-/.test(key) && !value)
.length !== 0;
}
return '';
},
getSettings(step) {
return Template.instance().wizardSettings.get()
.filter(setting => setting.wizard.step === step)
.sort((a, b) => a.wizard.order - b.wizard.order);
return false;
},
languages() {
const languages = TAPi18n.getLanguages();
const result = Object.entries(languages).map(language => {
const obj = language[1];
obj.key = language[0];
return obj;
}).sort((a, b) => a.key - b.key);
result.unshift({
'name': 'Default',
'en': 'Default',
'key': ''
});
infoArgs() {
const t = Template.instance();
return result;
return {
currentStep: t.state.get('currentStep')
};
},
hasAdmin() {
return Template.instance().hasAdmin.get();
adminInfoArgs() {
const t = Template.instance();
return {
currentStep: t.state.get('currentStep'),
name: t.state.get('registration-name'),
username: t.state.get('registration-username'),
email: t.state.get('registration-email'),
password: t.state.get('registration-pass'),
invalidUsername: t.state.get('invalidUsername'),
invalidEmail: t.state.get('invalidEmail')
};
},
invalidEmail() {
return Template.instance().invalidEmail.get();
registerServerArgs() {
const t = Template.instance();
return {
currentStep: t.state.get('currentStep'),
allowStandaloneServer: t.allowStandaloneServer.get(),
registerServer: t.allowStandaloneServer.get() ? t.state.get('registerServer') : true,
optIn: t.state.get('optIn')
};
},
showBackButton() {
if (Template.instance().hasAdmin.get()) {
if (Template.instance().state.get('currentStep') > 2) {
return true;
}
return false;
}
if (Template.instance().state.get('currentStep') > 1) {
return true;
}
return false;
customStepArgs(step) {
const t = Template.instance();
return {
currentStep: t.state.get('currentStep'),
step,
settings: t.wizardSettings.get()
.filter(setting => setting.wizard.step === step)
.sort((a, b) => a.wizard.order - b.wizard.order)
.map(({ type, _id, i18nLabel, values }) => ({
type,
id: _id,
label: i18nLabel,
value: t.state.get(_id),
options: (
type === 'select' &&
values &&
values.map(({ i18nLabel, key }) => ({ optionLabel: i18nLabel, optionValue: key }))
) || (
type === 'language' &&
([{
optionLabel: 'Default',
optionValue: ''
}].concat(
Object.entries(TAPi18n.getLanguages())
.map(([ key, { name } ]) => ({ optionLabel: name, optionValue: key }))
.sort((a, b) => a.key - b.key)
))
),
isValueSelected: (value) => value === t.state.get(_id)
}))
};
}
});
Template.setupWizardFinal.onCreated(function() {
this.autorun(() => {
const userId = Meteor.userId();
this.autorun((c) => {
const Show_Setup_Wizard = RocketChat.settings.get('Show_Setup_Wizard');
const user = Meteor.user();
// Wait for roles and setup wizard setting
if ((userId && (!user || !user.status)) || !Show_Setup_Wizard) {
return;
}
c.stop();
if ((!userId && Show_Setup_Wizard !== 'pending') || Show_Setup_Wizard === 'completed' || (userId && !RocketChat.authz.hasRole(userId, 'admin'))) {
FlowRouter.go('home');
}
});
});
});
Template.setupWizardInfo.helpers({
stepItemModifier(step) {
const { currentStep } = Template.currentData();
Template.setupWizardFinal.onRendered(function() {
$('#initial-page-loading').remove();
});
if (currentStep === step) {
return 'setup-wizard-info__steps-item--active';
}
Template.setupWizardFinal.events({
'click .js-finish'() {
RocketChat.settings.set('Show_Setup_Wizard', 'completed', function() {
localStorage.removeItem('wizard');
localStorage.removeItem('wizardFinal');
FlowRouter.go('home');
});
}
});
if (currentStep > step) {
return 'setup-wizard-info__steps-item--past';
}
Template.setupWizardFinal.helpers({
siteUrl() {
return RocketChat.settings.get('Site_Url');
return '';
},
stepTitle(step) {
switch (step) {
case 1:
return 'Admin_Info';
case 2:
return 'Organization_Info';
case 3:
return 'Server_Info';
case 4:
return 'Register_Server';
}
}
});

@ -12,6 +12,8 @@ Package.onUse(function(api) {
api.addFiles('client/setupWizard.html', 'client');
api.addFiles('client/setupWizard.js', 'client');
api.addFiles('client/final.html', 'client');
api.addFiles('client/final.js', 'client');
api.addFiles('server/lib/getWizardSettings.js', 'server');
api.addFiles('server/getSetupWizardParameters.js', 'server');
});

@ -0,0 +1,18 @@
Meteor.methods({
getSetupWizardParameters() {
const userId = Meteor.userId();
const userHasAdminRole = userId && RocketChat.authz.hasRole(userId, 'admin');
if (!userHasAdminRole) {
throw new Meteor.Error('error-not-allowed');
}
const settings = RocketChat.models.Settings.findSetupWizardSettings().fetch();
const allowStandaloneServer = process.env.DEPLOY_PLATFORM !== 'rocket-cloud';
return {
settings,
allowStandaloneServer
};
}
});

@ -1,9 +0,0 @@
Meteor.methods({
getWizardSettings() {
if (RocketChat.authz.hasRole(Meteor.userId(), 'admin') && RocketChat.models && RocketChat.models.Settings) {
return RocketChat.models.Settings.findSetupWizardSettings().fetch();
}
throw new Meteor.Error('settings-are-not-ready', 'Settings are not ready');
}
});

@ -1,4 +1,7 @@
.setup-wizard {
--step-color: var(--rc-color-button-primary);
--highlight-color: var(--rc-color-button-primary);
display: flex;
width: 100%;
@ -12,6 +15,7 @@
flex: 0 1 350px;
margin: 55px 65px 30px 80px;
overflow: hidden;
&__header{
display: flex;
@ -125,28 +129,30 @@
}
&--active {
color: #1d74f5;
color: var(--rc-color-button-primary);
&::before {
color: #1d74f5;
border-color: #1d74f5;
color: var(--rc-color-button-primary);
background-color: transparent;
border-color: var(--rc-color-button-primary);
}
}
&--past {
color: #2f343d;
color: var(--rc-color-primary);
&::before {
color: #ffffff;
background-color: #1d74f5;
color: var(--rc-color-content);
background-color: var(--rc-color-button-primary);
border-color: var(--rc-color-button-primary);
}
&::after {
background-color: #1d74f5 !important;
background-color: var(--rc-color-button-primary) !important;
}
& .setup-wizard-info__steps-item-bonding {
background-color: #1d74f5;
background-color: var(--rc-color-button-primary);
}
}
@ -177,8 +183,6 @@
height: calc(100% - 2rem);
margin: 1rem 1rem 1rem 0;
padding: 3rem;
border-radius: 2px;
background: #ffffff;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08);
@ -190,6 +194,17 @@
flex-direction: column;
width: 350px;
min-height: min-content;
margin: 3rem;
visibility: hidden;
opacity: 0;
transition: opacity 1s linear;
&--loaded {
visibility: visible;
opacity: 1;
}
}
&__header {
@ -259,7 +274,7 @@
cursor: pointer;
color: #2f343d;
border: 1px solid #e7ebf2;
border: 2px solid #e7ebf2;
border-radius: 2px;
@ -267,6 +282,14 @@
line-height: 1.25rem;
&--selected {
border-color: var(--highlight-color);
}
&--disabled {
opacity: 0.25;
}
&:first-child {
margin-bottom: 1rem;
}
@ -292,22 +315,21 @@
position: relative;
border-color: #1d74f5;
border-color: var(--highlight-color);
&::before {
position: absolute;
top: 3px;
left: 3px;
content: "";
width: 12px;
height: 12px;
position: absolute;
content: "";
width: 100%;
height: 100%;
background-clip: padding-box;
border-radius: 50%;
border: 2px solid transparent;
background-color: #1d74f5;
background-color: var(--highlight-color);
}
}
}
@ -319,7 +341,7 @@
height: 20px;
margin: 0 0.5rem;
border: 1px solid #cfd8e6;
border: 2px solid #cfd8e6;
border-radius: 50px;
}
@ -328,6 +350,63 @@
}
}
&-checkbox {
position: relative;
display: flex;
margin: 0 -0.5rem 1rem;
cursor: inherit;
&-element {
position: absolute;
z-index: -1;
top: 0;
left: 0;
width: 0;
height: 0;
&:checked + .setup-wizard-forms__content-register-checkbox-fake {
position: relative;
border-color: var(--highlight-color);
background-color: var(--highlight-color);
color: var(--rc-color-content);
.setup-wizard-forms__content-register-checkbox-fake-icon {
display: block;
}
}
}
&-fake {
display: block;
width: 16px;
height: 16px;
margin: 2px 0.5rem;
border: 2px solid #cfd8e6;
border-radius: 2px;
&-icon {
width: 100%;
height: 100%;
display: none;
}
}
&-text {
color: #666666;
}
}
&-items + * {
margin-top: 1rem;
}
&-item {
display: flex;
@ -340,13 +419,20 @@
}
& .setup-wizard-forms__content-register-radio-icon {
margin: 0 calc(4px + 0.5rem);
min-width: 20px;
width: 20px;
height: 20px;
margin: 0 0.5rem;
align-self: baseline;
font-size: 10px;
}
&--check {
color: var(--highlight-color);
}
& .setup-wizard-forms__content-register-radio-icon--check {
color: #1d74f5;
&--circle {
height: 6px;
margin: 7px 0.5rem;
}
}
}
}
@ -354,8 +440,9 @@
&__footer {
display: flex;
flex-direction: row;
margin: 0 -0.5rem;
margin: 0 -0.5rem 2rem -0.5rem;
& .rc-button {
margin: 0 0.5rem;
@ -390,7 +477,7 @@
letter-spacing: 0;
color: #1d74f5;
color: var(--highlight-color);
font-size: 1rem;

@ -123,6 +123,24 @@ describe('[Setup Wizard]', () => {
});
});
describe('[Render - Step 3]', () => {
it('it should have option for registered server', () => {
setupWizard.registeredServer.isExisting().should.be.true;
});
it('it should have option for standalone server', () => {
setupWizard.standaloneServer.isExisting().should.be.true;
});
it('it should check option for registered server by default', () => {
setupWizard.registeredServer.isSelected().should.be.true;
});
after(() => {
setupWizard.goNext();
});
});
describe('[Render - Final Step]', () => {
it('it should render "Go to your workspace button', () => {
setupWizard.goToWorkspace.waitForVisible(15000);

@ -14,6 +14,8 @@ class SetupWizard extends Page {
get siteName() { return browser.element('input[name="Site_Name"]'); }
get language() { return browser.element('select[name="Language"]'); }
get serverType() { return browser.element('select[name="Server_Type"]'); }
get registeredServer() { return browser.element('input[name="registerServer"][value="true"]'); }
get standaloneServer() { return browser.element('input[name="registerServer"][value="false"]'); }
login() {
browser.execute(function(email, password) {

Loading…
Cancel
Save