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": { | ||||
|     "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 | ||||
| 		respond_to do |format|   | ||||
| 			format.html { super }   | ||||
|     @theme = 'minimal' | ||||
| 
 | ||||
| 		respond_to do |format| | ||||
| 			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,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) => { | ||||
| 
 | ||||
| 
 | ||||
|           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
									
									
								
							|  | @ -306,6 +306,9 @@ export class Form implements Base { | |||
|     readonly submitting: boolean; | ||||
| 
 | ||||
|     protected validator :any | ||||
| 
 | ||||
|     readonly isValid :boolean; | ||||
| 
 | ||||
|      | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Eric Schultz
						Eric Schultz