Invite Users step

This commit is contained in:
Robin Ward 2016-09-13 15:14:17 -04:00
parent 35b767f6af
commit ef84981e38
19 changed files with 373 additions and 62 deletions

View file

@ -0,0 +1,16 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
classNames: ['invite-list-user'],
@computed('user.role')
roleName(role) {
return this.get('roles').findProperty('id', role).label;
},
actions: {
removeUser(user) {
this.sendAction('removeUser', user);
}
}
});

View file

@ -0,0 +1,62 @@
export default Ember.Component.extend({
classNames: ['invite-list'],
users: null,
inviteEmail: '',
inviteRole: '',
invalid: false,
init() {
this._super();
this.set('users', []);
this.set('roles', [
{id: 'moderator', label: I18n.t('wizard.invites.roles.moderator') },
{id: 'regular', label: I18n.t('wizard.invites.roles.regular') },
]);
this.updateField();
},
keyPress(e) {
if (e.keyCode === 13) {
e.preventDefault();
e.stopPropagation();
this.send('addUser');
}
},
updateField() {
this.set('field.value', JSON.stringify(this.get('users')));
},
actions: {
addUser() {
const user = {
email: this.get('inviteEmail') || '',
role: this.get('inviteRole')
};
if (!/(.+)@(.+){2,}\.(.+){2,}/.test(user.email)) {
return this.set('invalid', true);
}
const users = this.get('users');
if (users.findProperty('email', user.email)) {
return this.set('invalid', true);
}
this.set('invalid', false);
users.pushObject(user);
this.updateField();
this.set('inviteEmail', '');
Ember.run.scheduleOnce('afterRender', () => this.$('.invite-email').focus());
},
removeUser(user) {
this.get('users').removeObject(user);
this.updateField();
}
}
});

View file

@ -0,0 +1,6 @@
<span class='email'>{{user.email}}</span>
<span class='role'>{{roleName}}</span>
<button class="wizard-btn small danger remove-user" {{action "removeUser" user}}>
{{fa-icon "times"}}
</button>

View file

@ -0,0 +1,20 @@
{{#if users}}
<div class="users-list">
{{#each users as |user|}}
{{invite-list-user user=user roles=roles removeUser="removeUser"}}
{{/each}}
</div>
{{/if}}
<div class="new-user">
<div class="text-field {{if invalid 'invalid'}}">
{{input class="invite-email" value=inviteEmail placeholder="user@example.com"}}
</div>
{{combo-box value=inviteRole content=roles nameProperty="label" width="200px"}}
<button class="wizard-btn small add-user" {{action "addUser"}}>
{{fa-icon "plus"}}{{i18n "wizard.invites.add_user"}}
</button>
</div>

View file

@ -1 +1,6 @@
{{combo-box class=fieldClass value=field.value content=field.choices nameProperty="label" width="400px"}}
{{combo-box elementId=field.id
class=fieldClass
value=field.value
content=field.choices
nameProperty="label"
width="400px"}}

View file

@ -1 +1 @@
{{input value=field.value class=fieldClass placeholder=field.placeholder}}
{{input elementId=field.id value=field.value class=fieldClass placeholder=field.placeholder}}

View file

@ -1,16 +1,15 @@
<label>
<label for={{field.id}}>
<span class='label-value'>{{field.label}}</span>
{{#if field.description}}
<div class='field-description'>{{{field.description}}}</div>
{{/if}}
<div class='input-area'>
{{component inputComponentName field=field step=step fieldClass=fieldClass}}
</div>
{{#if field.errorDescription}}
<div class='field-error-description'>{{field.errorDescription}}</div>
{{/if}}
</label>
<div class='input-area'>
{{component inputComponentName field=field step=step fieldClass=fieldClass}}
</div>
{{#if field.errorDescription}}
<div class='field-error-description'>{{field.errorDescription}}</div>
{{/if}}

View file

@ -29,7 +29,7 @@
{{/if}}
{{#if showNextButton}}
<button class='wizard-btn next' {{action "nextStep"}} disabled={{saving}}>
<button class='wizard-btn next primary' {{action "nextStep"}} disabled={{saving}}>
{{i18n "wizard.next"}}
{{fa-icon "chevron-right"}}
</button>

View file

@ -1,4 +1,15 @@
module("Acceptance: wizard");
import startApp from 'wizard/test/helpers/start-app';
var wizard;
module("Acceptance: wizard", {
beforeEach() {
wizard = startApp();
},
teardown() {
Ember.run(wizard, 'destroy');
}
});
test("Wizard starts", assert => {
visit("/");

View file

@ -0,0 +1,60 @@
import { componentTest } from 'wizard/test/helpers/component-test';
moduleForComponent('invite-list', { integration: true });
componentTest('can add users', {
template: `{{invite-list field=field}}`,
setup() {
this.set('field', {});
},
test(assert) {
assert.ok(this.$('.users-list .invite-list-user').length === 0, 'no users at first');
assert.ok(this.$('.new-user .invalid').length === 0, 'not invalid at first');
const firstVal = JSON.parse(this.get('field.value'));
assert.equal(firstVal.length, 0, 'empty JSON at first');
click('.add-user');
andThen(() => {
assert.ok(this.$('.users-list .invite-list-user').length === 0, "doesn't add a blank user");
assert.ok(this.$('.new-user .invalid').length === 1);
});
fillIn('.invite-email', 'eviltrout@example.com');
click('.add-user');
andThen(() => {
assert.ok(this.$('.users-list .invite-list-user').length === 1, 'adds the user');
assert.ok(this.$('.new-user .invalid').length === 0);
const val = JSON.parse(this.get('field.value'));
assert.equal(val.length, 1);
assert.equal(val[0].email, 'eviltrout@example.com', 'adds the email to the JSON');
assert.ok(val[0].role.length, 'adds the role to the JSON');
});
fillIn('.invite-email', 'eviltrout@example.com');
click('.add-user');
andThen(() => {
assert.ok(this.$('.users-list .invite-list-user').length === 1, "can't add the same user twice");
assert.ok(this.$('.new-user .invalid').length === 1);
});
fillIn('.invite-email', 'not-an-email');
click('.add-user');
andThen(() => {
assert.ok(this.$('.users-list .invite-list-user').length === 1, "won't add an invalid email");
assert.ok(this.$('.new-user .invalid').length === 1);
});
click('.invite-list .invite-list-user:eq(0) .remove-user');
andThen(() => {
assert.ok(this.$('.users-list .invite-list-user').length === 0, 'removed the user');
});
}
});

View file

@ -0,0 +1,16 @@
import initializer from 'wizard/initializers/load-helpers';
export function componentTest(name, opts) {
opts = opts || {};
test(name, function(assert) {
initializer.initialize();
if (opts.setup) {
opts.setup.call(this);
}
andThen(() => this.render(opts.template));
andThen(() => opts.test.call(this, assert));
});
}

View file

@ -0,0 +1,19 @@
import Wizard from 'wizard/wizard';
import initializer from 'wizard/initializers/load-helpers';
let app;
let started = false;
export default function() {
Ember.run(() => app = Wizard.create({ rootElement: '#ember-testing' }));
if (!started) {
initializer.initialize();
app.start();
started = true;
}
app.setupForTesting();
app.injectTestHelpers();
return app;
}

View file

@ -12,8 +12,10 @@
//= require wizard-application
//= require wizard-vendor
//= require helpers/assertions
//= require_tree ./helpers
//= require_tree ./acceptance
//= require_tree ./models
//= require_tree ./components
//= require locales/en
//= require fake_xml_http_request
//= require route-recognizer
@ -42,13 +44,10 @@ QUnit.testDone(function() {
server.shutdown();
});
var wizard = require('wizard/wizard').default.create({
rootElement: '#ember-testing'
});
require('wizard/test/helpers/start-app').default();
wizard.setupForTesting();
wizard.injectTestHelpers();
wizard.start();
var buildResolver = require('discourse-common/resolver').buildResolver;
window.setResolver(buildResolver('wizard').create());
Object.keys(requirejs.entries).forEach(function(entry) {
if ((/\-test/).test(entry)) {

View file

@ -84,6 +84,11 @@ body.wizard {
color: #333;
box-shadow: 0 1px 4px rgba(0, 0, 0, .4);
&.small {
padding: 0.25em 0.5em;
font-size: 0.9em;
}
&:hover {
background-color: #eee;
}
@ -110,6 +115,42 @@ body.wizard {
}
}
.wizard-btn.primary {
background-color: #6699ff;
color: white;
box-shadow: 0 1px 4px rgba(0, 0, 0, .6);
&:hover {
background-color: #80B3FF;
}
&:active {
background-color: #4D80E6;
}
&:disabled {
background-color: #000167;
}
}
.wizard-btn.danger {
background-color: #E60000;
color: white;
&:hover {
background-color: #CC0000;
}
&:active {
background-color: #B30000;
}
&:disabled {
background-color: #990000;
}
}
.wizard-btn-upload {
clear: both;
display: inline-block;
@ -125,22 +166,6 @@ body.wizard {
align-items: center;
.wizard-btn.next {
background-color: #6699ff;
color: white;
box-shadow: 0 1px 4px rgba(0, 0, 0, .6);
&:hover {
background-color: #80B3FF;
}
&:active {
background-color: #4D80E6;
}
&:disabled {
background-color: #000167;
}
min-width: 70px;
i.fa-chevron-right {
@ -240,29 +265,29 @@ body.wizard {
}
}
&.text-field {
input {
width: 100%;
font-size: 1.2em;
padding: 6px;
border: 1px solid #ccc;
transition: border-color .5s;
outline: none;
}
&.invalid {
input {
padding: 3px;
border: 4px solid red;
border-radius: 3px;
}
}
}
margin-bottom: 2em;
}
}
.text-field {
input {
width: 100%;
font-size: 1.2em;
padding: 6px;
border: 1px solid #ccc;
transition: border-color .5s;
outline: none;
}
&.invalid {
input {
padding: 3px;
border: 4px solid red;
border-radius: 3px;
}
}
}
.radio-field-choice {
margin-bottom: 1.5em;
@ -280,3 +305,42 @@ body.wizard {
color: #777;
}
}
.invite-list {
.users-list {
margin-bottom: 1em;
.invite-list-user {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 1em;
.email {
width: 330px;
}
.role {
width: 200px;
}
}
}
.new-user {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 1em;
.invite-email {
width: 350px;
}
}
button.add-user {
.fa {
margin-right: 0.5em;
}
}
}

View file

@ -3234,3 +3234,11 @@ en:
upload: "Upload"
uploading: "Uploading..."
invites:
add_user: "add"
roles:
admin: "Admin"
moderator: "Moderator"
regular: "Regular User"

View file

@ -3311,6 +3311,10 @@ en:
label: "Large Icon"
description: "Icon used for Apple touch devices. Recommended size is 144px by 144px."
invites:
title: "Invite Staff"
description: "We recommend you invite some staff members to help you get things started."
finished:
title: "Your Discourse Forum is Ready!"
description: |

View file

@ -129,6 +129,18 @@ class Wizard
end
end
@wizard.append_step('invites') do |step|
step.add_field(id: 'invite_list', type: 'component')
step.on_update do |updater|
users = JSON.parse(updater.fields[:invite_list])
users.each do |u|
Invite.create_invite_by_email(u['email'], @wizard.user)
end
end
end
DiscourseEvent.trigger(:build_wizard, @wizard)
@wizard.append_step('finished')

View file

@ -141,7 +141,6 @@ describe Wizard::StepUpdater do
end
context "logos step" do
it "updates the fields correctly" do
updater = wizard.create_updater('logos',
logo_url: '/uploads/logo.png',
@ -158,5 +157,22 @@ describe Wizard::StepUpdater do
end
end
context "invites step" do
let(:invites) {
return [{ email: 'regular@example.com', role: 'regular'},
{ email: 'moderator@example.com', role: 'moderator'}]
}
it "updates the fields correctly" do
updater = wizard.create_updater('invites', invite_list: invites.to_json)
updater.update
expect(updater).to be_success
expect(Invite.where(email: 'regular@example.com')).to be_present
expect(Invite.where(email: 'moderator@example.com')).to be_present
end
end
end

View file

@ -18,12 +18,6 @@ describe ExtraLocalesController do
get :show, bundle: '-invalid..character!!'
expect(response).to_not be_success
end
it "works with a valid bundle" do
get :show, bundle: 'admin'
expect(response).to be_success
expect(response.body).to be_present
end
end
end