Merge pull request #79 from houdiniproject/fix_for_71
Reactify the login form. Fixes #71
This commit is contained in:
commit
4330541be6
15 changed files with 221 additions and 23 deletions
|
@ -5,7 +5,8 @@
|
||||||
"styles": {
|
"styles": {
|
||||||
"mixins": true,
|
"mixins": true,
|
||||||
"grid": true,
|
"grid": true,
|
||||||
"forms": true
|
"forms": true,
|
||||||
|
"responsive-utilities":true
|
||||||
},
|
},
|
||||||
|
|
||||||
"scripts": false
|
"scripts": false
|
||||||
|
|
3
app/assets/stylesheets/users/page.scss
Normal file
3
app/assets/stylesheets/users/page.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.login-bottom-link {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
|
@ -1,9 +1,17 @@
|
||||||
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
class Users::SessionsController < Devise::SessionsController
|
class Users::SessionsController < Devise::SessionsController
|
||||||
|
layout 'layouts/apified', only: :new
|
||||||
|
|
||||||
|
|
||||||
|
def new
|
||||||
|
@theme = 'minimal'
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
respond_to do |format|
|
@theme = 'minimal'
|
||||||
format.html { super }
|
|
||||||
|
respond_to do |format|
|
||||||
format.json {
|
format.json {
|
||||||
warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new")
|
warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new")
|
||||||
render :status => 200, :json => { :status => "Success" }
|
render :status => 200, :json => { :status => "Success" }
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%>
|
<%- # 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 id="outlet"></div>
|
||||||
<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>
|
|
||||||
|
|
||||||
|
<script>LoadReactPage(document.getElementById('outlet'))</script>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<% content_for :title, t("registration.get_started.header") %>
|
<% content_for :title, t("registration.get_started.header") %>
|
||||||
<% content_for :javascripts do %>
|
<% content_for :javascripts do %>
|
||||||
<%= IncludeAsset.js 'app/registration_pagex.js' %>
|
<%= IncludeAsset.js '/app/registration_pagex.js' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div id="outlet"></div>
|
<div id="outlet"></div>
|
||||||
|
|
|
@ -189,3 +189,11 @@ en:
|
||||||
footer:
|
footer:
|
||||||
terms_and_privacy: "Terms & Privacy"
|
terms_and_privacy: "Terms & Privacy"
|
||||||
about: "About"
|
about: "About"
|
||||||
|
login:
|
||||||
|
header: "Login"
|
||||||
|
email: "Email"
|
||||||
|
password: "Password"
|
||||||
|
login: "Login"
|
||||||
|
logging_in: "Logging you in..."
|
||||||
|
forgot_password: "Forgot Password?"
|
||||||
|
get_started: "Get Started"
|
||||||
|
|
|
@ -195,7 +195,7 @@ Commitchange::Application.routes.draw do
|
||||||
:confirmations => 'users/confirmations'
|
:confirmations => 'users/confirmations'
|
||||||
}
|
}
|
||||||
devise_scope :user do
|
devise_scope :user do
|
||||||
match '/signin' => 'devise/sessions#new'
|
match '/sign_in' => 'users/sessions#new'
|
||||||
match '/signup' => 'devise/registrations#new'
|
match '/signup' => 'devise/registrations#new'
|
||||||
post '/confirm' => 'users/confirmations#confirm'
|
post '/confirm' => 'users/confirmations#confirm'
|
||||||
match '/users/is_confirmed' => 'users/confirmations#is_confirmed'
|
match '/users/is_confirmed' => 'users/confirmations#is_confirmed'
|
||||||
|
|
14
javascripts/app/session_login_page.tsx
Normal file
14
javascripts/app/session_login_page.tsx
Normal 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
|
|
@ -198,7 +198,7 @@ export class InnerRegistrationWizard extends React.Component<RegistrationWizardP
|
||||||
buttonText="registration.wizard.next"/>
|
buttonText="registration.wizard.next"/>
|
||||||
|
|
||||||
<UserInfoPanel tab={this.registrationWizardState.tabsByName['userTab']}
|
<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>
|
</Wizard>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ export interface UserInfoFormProps
|
||||||
{
|
{
|
||||||
form: Field
|
form: Field
|
||||||
buttonText:string
|
buttonText:string
|
||||||
buttonTextInProgress?:string
|
buttonTextOnProgress?:string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ class UserInfoForm extends React.Component<UserInfoFormProps & InjectedIntlProps
|
||||||
disabled={!this.props.form.isValid}
|
disabled={!this.props.form.isValid}
|
||||||
buttonText={this.props.intl.formatMessage({id: this.props.buttonText})}
|
buttonText={this.props.intl.formatMessage({id: this.props.buttonText})}
|
||||||
inProgress={areWeOrAnyParentSubmitting(this.props.form)}
|
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}/>
|
disableOnProgress={true}/>
|
||||||
</fieldset>;
|
</fieldset>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import UserInfoForm from "./UserInfoForm";
|
||||||
|
|
||||||
export interface UserInfoPanelProps extends WizardTabPanelProps {
|
export interface UserInfoPanelProps extends WizardTabPanelProps {
|
||||||
buttonText: string
|
buttonText: string
|
||||||
buttonTextInProgress?:string
|
buttonTextOnProgress?:string
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserInfoPanel extends React.Component<UserInfoPanelProps & InjectedIntlProps, {}> {
|
class UserInfoPanel extends React.Component<UserInfoPanelProps & InjectedIntlProps, {}> {
|
||||||
|
@ -39,7 +39,7 @@ class UserInfoPanel extends React.Component<UserInfoPanelProps & InjectedIntlPro
|
||||||
return <WizardPanel
|
return <WizardPanel
|
||||||
tab={this.wizardTab} key={this.tabName}
|
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>;
|
</WizardPanel>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@ export class WebUserSignInOut {
|
||||||
(xhr: JQueryXHR, textStatus: string, errorThrown: string) => {
|
(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
|
email:string
|
||||||
password:string
|
password:string
|
||||||
}
|
}
|
3
types/mobx-react-form/index.d.ts
vendored
3
types/mobx-react-form/index.d.ts
vendored
|
@ -306,6 +306,9 @@ export class Form implements Base {
|
||||||
readonly submitting: boolean;
|
readonly submitting: boolean;
|
||||||
|
|
||||||
protected validator :any
|
protected validator :any
|
||||||
|
|
||||||
|
readonly isValid :boolean;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue