Merge pull request #79 from houdiniproject/fix_for_71

Reactify the login form. Fixes #71
This commit is contained in:
Eric Schultz 2018-07-10 11:58:45 -05:00 committed by GitHub
commit 4330541be6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 221 additions and 23 deletions

View file

@ -5,7 +5,8 @@
"styles": {
"mixins": true,
"grid": true,
"forms": true
"forms": true,
"responsive-utilities":true
},
"scripts": false

View file

@ -0,0 +1,3 @@
.login-bottom-link {
margin-top: 12px;
}

View file

@ -1,9 +1,17 @@
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
class Users::SessionsController < Devise::SessionsController
layout 'layouts/apified', only: :new
def new
@theme = 'minimal'
super
end
def create
@theme = 'minimal'
respond_to do |format|
format.html { super }
format.json {
warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new")
render :status => 200, :json => { :status => "Success" }

View file

@ -1,14 +1,12 @@
<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%>
<% content_for(:footer_hidden) {'hidden'} %>
<% content_for :title, t("login.header") %>
<% content_for :javascripts do %>
<%= IncludeAsset.js '/app/session_login_pagex.js' %>
<% end %>
<% content_for :stylesheets do %>
<%= stylesheet_link_tag 'users/page' %>
<% end %>
<div class='u-verticallyCenterAbs u-width--full'>
<div class='u-padding--20 u-centered u-maxWidth--300 u-margin--auto'>
<h2>Login</h2>
<%= render 'users/email_login_form' %>
<p class='u-marginTop--10 u-bold'>
<%= link_to "Forgot your password?", new_password_path(resource_name)%>
</p>
<p>Don't have an account? <a open-modal='chooseRoleModal' class='u-bold'>Sign up.</a></p>
</div>
</div>
<div id="outlet"></div>
<script>LoadReactPage(document.getElementById('outlet'))</script>

View file

@ -1,6 +1,6 @@
<% content_for :title, t("registration.get_started.header") %>
<% content_for :javascripts do %>
<%= IncludeAsset.js 'app/registration_pagex.js' %>
<%= IncludeAsset.js '/app/registration_pagex.js' %>
<% end %>
<div id="outlet"></div>

View file

@ -189,3 +189,11 @@ en:
footer:
terms_and_privacy: "Terms & Privacy"
about: "About"
login:
header: "Login"
email: "Email"
password: "Password"
login: "Login"
logging_in: "Logging you in..."
forgot_password: "Forgot Password?"
get_started: "Get Started"

View file

@ -195,7 +195,7 @@ Commitchange::Application.routes.draw do
:confirmations => 'users/confirmations'
}
devise_scope :user do
match '/signin' => 'devise/sessions#new'
match '/sign_in' => 'users/sessions#new'
match '/signup' => 'devise/registrations#new'
post '/confirm' => 'users/confirmations#confirm'
match '/users/is_confirmed' => 'users/confirmations#is_confirmed'

View file

@ -0,0 +1,14 @@
// License: LGPL-3.0-or-later
// require a root component here. This will be treated as the root of a webpack package
import Root from "../src/components/common/Root"
import SessionLoginPage from "../src/components/session_login_page/SessionLoginPage"
import * as ReactDOM from 'react-dom'
import * as React from 'react'
function LoadReactPage(element:HTMLElement) {
ReactDOM.render(<Root><SessionLoginPage/></Root>, element)
}
(window as any).LoadReactPage = LoadReactPage

View file

@ -198,7 +198,7 @@ export class InnerRegistrationWizard extends React.Component<RegistrationWizardP
buttonText="registration.wizard.next"/>
<UserInfoPanel tab={this.registrationWizardState.tabsByName['userTab']}
buttonText="registration.wizard.save_and_finish" buttonTextInProgress="registration.wizard.saving"/>
buttonText="registration.wizard.save_and_finish" buttonTextOnProgress="registration.wizard.saving"/>
</Wizard>
}
}

View file

@ -41,7 +41,7 @@ export interface UserInfoFormProps
{
form: Field
buttonText:string
buttonTextInProgress?:string
buttonTextOnProgress?:string
}
@ -63,7 +63,7 @@ class UserInfoForm extends React.Component<UserInfoFormProps & InjectedIntlProps
disabled={!this.props.form.isValid}
buttonText={this.props.intl.formatMessage({id: this.props.buttonText})}
inProgress={areWeOrAnyParentSubmitting(this.props.form)}
buttonTextOnProgress={this.props.intl.formatMessage({id: this.props.buttonTextInProgress})}
buttonTextOnProgress={this.props.intl.formatMessage({id: this.props.buttonTextOnProgress})}
disableOnProgress={true}/>
</fieldset>;
}

View file

@ -10,7 +10,7 @@ import UserInfoForm from "./UserInfoForm";
export interface UserInfoPanelProps extends WizardTabPanelProps {
buttonText: string
buttonTextInProgress?:string
buttonTextOnProgress?:string
}
class UserInfoPanel extends React.Component<UserInfoPanelProps & InjectedIntlProps, {}> {
@ -39,7 +39,7 @@ class UserInfoPanel extends React.Component<UserInfoPanelProps & InjectedIntlPro
return <WizardPanel
tab={this.wizardTab} key={this.tabName}
>
<UserInfoForm form={this.form} buttonText={this.props.buttonText} buttonTextInProgress={this.props.buttonTextInProgress}/>
<UserInfoForm form={this.form} buttonText={this.props.buttonText} buttonTextOnProgress={this.props.buttonTextOnProgress}/>
</WizardPanel>;
}

View file

@ -0,0 +1,138 @@
// License: LGPL-3.0-or-later
import * as React from 'react';
import { observer, inject} from 'mobx-react';
import {InjectedIntlProps, injectIntl, FormattedMessage} from 'react-intl';
import {Field, FieldDefinition, Form, initializationDefinition} from "../../../../types/mobx-react-form";
import {Validations} from "../../lib/vjf_rules";
import {WebLoginModel, WebUserSignInOut} from "../../lib/api/sign_in";
import {HoudiniForm, StaticFormToErrorAndBackConverter} from "../../lib/houdini_form";
import {observable, action} from 'mobx'
import {ApiManager} from "../../lib/api_manager";
import {BasicField} from "../common/fields";
import ProgressableButton from "../common/ProgressableButton";
export interface SessionLoginFormProps
{
buttonText:string
buttonTextOnProgress:string
ApiManager?: ApiManager
}
export const FieldDefinitions : Array<FieldDefinition> = [
{
name: 'email',
label: 'email',
type: 'text',
validators: [Validations.isFilled]
},
{
name: 'password',
label: 'password',
type: 'password',
validators: [Validations.isFilled]
}
]
export class SessionPageForm extends HoudiniForm {
converter: StaticFormToErrorAndBackConverter<WebLoginModel>
constructor(definition: initializationDefinition, options?: any) {
super(definition, options)
this.converter = new StaticFormToErrorAndBackConverter<WebLoginModel>(this.inputToForm)
}
signinApi: WebUserSignInOut
options() {
return {
validateOnInit: true,
validateOnChange: true,
retrieveOnlyDirtyValues: true,
retrieveOnlyEnabledFields: true
}
}
inputToForm = {
'email': 'email',
'password': 'password'
}
hooks() {
return {
onSuccess: async (f:SessionPageForm) => {
let input = this.converter.convertFormToObject(f)
try{
let r = await this.signinApi.postLogin(input)
window.location.reload()
}
catch(e){
if (e.error) {
f.invalidate(e.error)
}
else {
f.invalidate(e)
}
}
}
}
}
}
class InnerSessionLoginForm extends React.Component<SessionLoginFormProps & InjectedIntlProps, {}> {
constructor(props: SessionLoginFormProps & InjectedIntlProps) {
super(props)
this.createForm();
}
@action.bound
createForm() {
this.form = new SessionPageForm({fields: FieldDefinitions})
}
@observable form: SessionPageForm
render() {
if(!this.form.signinApi){
this.form.signinApi = this.props.ApiManager.get(WebUserSignInOut)
}
let label: {[props:string]: string} = {
'email': "login.email",
"password": 'login.password',
}
for (let key in label){
this.form.$(key).set('label', this.props.intl.formatMessage({id: label[key]}))
}
let errorDiv = !this.form.isValid ? <div className="form-group has-error"><div className="help-block" role="alert">{(this.form as any).error}</div></div> : ''
return <form onSubmit={this.form.onSubmit}>
<BasicField field={this.form.$('email')}/>
<BasicField field={this.form.$('password')}/>
{errorDiv}
<div className={'form-group'}>
<ProgressableButton onClick={this.form.onSubmit} className="button" disabled={!this.form.isValid || this.form.submitting} inProgress={this.form.submitting}
buttonText={this.props.intl.formatMessage({id: this.props.buttonText})}
buttonTextOnProgress={this.props.intl.formatMessage({id: this.props.buttonTextOnProgress})}></ProgressableButton>
</div>
<div className={'row'}>
<div className={'col-xs-12 col-sm-6 login-bottom-link'}><a href={'/users/password/new'}><FormattedMessage id={"login.forgot_password"}/></a></div>
<div className={'col-xs-12 col-sm-6 login-bottom-link'}><a href={'/onboard'}><div className={'visible-xs-block'}><FormattedMessage id={"login.get_started"}/></div><div className={"hidden-xs"} style={{"textAlign":"right"}}><FormattedMessage id={"login.get_started"}/></div></a></div>
</div>
</form>;
}
}
export default injectIntl(
inject('ApiManager')
(observer( InnerSessionLoginForm)
)
)

View file

@ -0,0 +1,25 @@
// License: LGPL-3.0-or-later
import * as React from 'react';
import { observer } from 'mobx-react';
import {InjectedIntlProps, injectIntl, InjectedIntl, FormattedMessage} from 'react-intl';
import SessionLoginForm from "./SessionLoginForm";
export interface SessionLoginPageProps
{
}
class SessionLoginPage extends React.Component<SessionLoginPageProps & InjectedIntlProps, {}> {
render() {
return <div className="container"><div className="row"><div className={'col-sm-6'}>
<h1><FormattedMessage id="login.header"/></h1>
<SessionLoginForm buttonText="login.login" buttonTextOnProgress="login.logging_in"/>
</div></div>
</div>;
}
}
export default injectIntl(observer(SessionLoginPage))

View file

@ -72,7 +72,7 @@ export class WebUserSignInOut {
(xhr: JQueryXHR, textStatus: string, errorThrown: string) => {
dfd.reject(errorThrown)
dfd.reject(xhr.responseJSON)
}
);
@ -80,7 +80,7 @@ export class WebUserSignInOut {
}
}
interface WebLoginModel {
export interface WebLoginModel {
email:string
password:string
}

View file

@ -307,6 +307,9 @@ export class Form implements Base {
protected validator :any
readonly isValid :boolean;
}