Reactified login form
This commit is contained in:
		
							parent
							
								
									d7ad600e6e
								
							
						
					
					
						commit
						9715059d29
					
				
					 17 changed files with 241 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,10 @@ | ||||||
|  | // License: LGPL-3.0-or-later
 | ||||||
|  | import * as React from 'react'; | ||||||
|  | import 'jest'; | ||||||
|  | import SessionLoginForm from './SessionLoginForm' | ||||||
|  | 
 | ||||||
|  | describe('SessionLoginForm', () => { | ||||||
|  |   test('your test here', () => { | ||||||
|  |     expect(false).toBe(true) | ||||||
|  |   }) | ||||||
|  | }) | ||||||
|  | @ -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,10 @@ | ||||||
|  | // License: LGPL-3.0-or-later
 | ||||||
|  | import * as React from 'react'; | ||||||
|  | import 'jest'; | ||||||
|  | import SessionLoginPage from './SessionLoginPage' | ||||||
|  | 
 | ||||||
|  | describe('SessionLoginPage', () => { | ||||||
|  |   test('your test here', () => { | ||||||
|  |     expect(false).toBe(true) | ||||||
|  |   }) | ||||||
|  | }) | ||||||
|  | @ -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…
	
	Add table
		
		Reference in a new issue
	
	 Eric Schultz
						Eric Schultz