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": {
|
||||
"mixins": true,
|
||||
"grid": true,
|
||||
"forms": true
|
||||
"forms": true,
|
||||
"responsive-utilities":true
|
||||
},
|
||||
|
||||
"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
|
||||
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" }
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
|
|
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"/>
|
||||
|
||||
<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>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
||||
|
||||
dfd.reject(errorThrown)
|
||||
dfd.reject(xhr.responseJSON)
|
||||
|
||||
}
|
||||
);
|
||||
|
@ -80,7 +80,7 @@ export class WebUserSignInOut {
|
|||
}
|
||||
}
|
||||
|
||||
interface WebLoginModel {
|
||||
export interface WebLoginModel {
|
||||
email:string
|
||||
password:string
|
||||
}
|
3
types/mobx-react-form/index.d.ts
vendored
3
types/mobx-react-form/index.d.ts
vendored
|
@ -307,6 +307,9 @@ export class Form implements Base {
|
|||
|
||||
protected validator :any
|
||||
|
||||
readonly isValid :boolean;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue