-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add LoginForm and initial versions of dependent components
- Loading branch information
Showing
18 changed files
with
824 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,6 @@ | |
**/*.js.snap | ||
**/*.d.ts | ||
node_modules | ||
coverage | ||
dist | ||
.awcache |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
!**/*.d.ts | ||
**/*.spec.* | ||
**/__mocks__/** | ||
**/__snapshots__/** | ||
**/__snapshots__/** | ||
stories | ||
coverage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import moment from 'moment'; | ||
|
||
import { CredentialStore } from './CredentialStore'; | ||
|
||
const localStorage = { | ||
setItem: jest.fn(), | ||
getItem: jest.fn(), | ||
removeItem: jest.fn() | ||
} | ||
|
||
Object.assign(global, { localStorage }); | ||
|
||
let store: CredentialStore; | ||
|
||
beforeEach(() => { | ||
store = new CredentialStore('TEST_TOKEN'); | ||
}); | ||
|
||
afterEach(() => jest.resetAllMocks()); | ||
|
||
const makeToken = (payload: {}): string => { | ||
const header = btoa(JSON.stringify({ | ||
alg: 'hs256', | ||
typ: 'JWT' | ||
})); | ||
|
||
const _payload = btoa(JSON.stringify(payload)); | ||
|
||
return `${header}.${_payload}`; | ||
} | ||
|
||
describe('setToken()', () => { | ||
it('sets stored token', () => { | ||
store.setToken('test'); | ||
|
||
expect(localStorage.setItem).toHaveBeenCalledWith('TEST_TOKEN', 'test'); | ||
}); | ||
}); | ||
|
||
describe('getToken()', () => { | ||
describe('when token is missing', () => { | ||
it('returns `undefined`', () => { | ||
expect(store.getToken()).toBeUndefined(); | ||
}); | ||
}); | ||
|
||
describe('when token has no expiry', () => { | ||
const token = makeToken({ id: 500 }); | ||
|
||
it('returns the token', () => { | ||
localStorage.getItem.mockImplementation(() => token); | ||
expect(store.getToken()).toBe(token); | ||
}); | ||
}); | ||
|
||
describe('when the token has an expiry in the future', () => { | ||
const token = makeToken({ | ||
id: 500, | ||
exp: moment().unix() + 1000 | ||
}); | ||
|
||
it('returns the token', () => { | ||
localStorage.getItem.mockImplementation(() => token); | ||
expect(store.getToken()).toBe(token); | ||
}); | ||
}); | ||
|
||
describe('when the token has an expiry in the past', () => { | ||
const token = makeToken({ | ||
id: 500, | ||
exp: 1000 | ||
}); | ||
|
||
it('returns `undefined`', () => { | ||
localStorage.getItem.mockImplementation(() => token); | ||
expect(store.getToken()).toBeUndefined(); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('clearToken()', () => { | ||
it('clears stored token', () => { | ||
store.clearToken(); | ||
expect(localStorage.removeItem).toHaveBeenCalledWith('TEST_TOKEN'); | ||
}); | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import moment from 'moment'; | ||
|
||
export class CredentialStore { | ||
constructor(protected _key: string) { } | ||
|
||
setToken(token: string) { | ||
localStorage.setItem(this._key, token); | ||
} | ||
|
||
getToken(): string | undefined { | ||
const token = localStorage.getItem(this._key); | ||
|
||
if (!token) { | ||
return; | ||
} | ||
|
||
const [, payload] = token.split('.'); | ||
|
||
const { exp } = JSON.parse(atob(payload)); | ||
|
||
if (exp && exp < moment().unix()) { | ||
return; | ||
} | ||
|
||
return token; | ||
} | ||
|
||
clearToken() { | ||
localStorage.removeItem(this._key); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import React from 'react'; | ||
import { shallow } from 'enzyme'; | ||
import { Button } from 'antd'; | ||
|
||
import { Form } from './Form'; | ||
|
||
const mockForm = { | ||
properties: {}, | ||
submit: jest.fn().mockReturnValue(Promise.resolve()) | ||
} as any; | ||
|
||
describe('default behaviour', () => { | ||
it('renders an empty Ant form', () => { | ||
const form = ( | ||
<Form | ||
resourceForm={mockForm} | ||
/> | ||
); | ||
|
||
expect(form).toMatchSnapshot(); | ||
}); | ||
|
||
it('triggers form submission', async () => { | ||
const form = shallow( | ||
<Form | ||
resourceForm={mockForm} | ||
/> | ||
); | ||
|
||
expect(form.state().submitting).toBe(false); | ||
|
||
form | ||
.find(Button) | ||
.first() | ||
.simulate('click'); | ||
}); | ||
}); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { Resource, Form as HALForm } from '@optics/hal-client'; | ||
import { Data, Schema } from '@optics/hal-client/dist/Form'; | ||
import { Form as AntForm, Button } from 'antd'; | ||
import { FormProps as AntFormProps, WrappedFormUtils } from 'antd/lib/form/Form'; | ||
import { FormItemProps as AntFormItemProps } from 'antd/lib/form/FormItem'; | ||
|
||
const DEFAULT_SUBMIT_BUTTON = ( | ||
<Button type="primary"> | ||
Submit | ||
</Button> | ||
); | ||
|
||
export const childContextTypes = { | ||
schema: PropTypes.object, | ||
defaultItemProps: PropTypes.object | ||
} | ||
|
||
export interface Props { | ||
resourceForm: HALForm; | ||
submitButton?: React.ReactElement<any> | false; | ||
onSuccess?: (result: Resource) => void; | ||
onFailure?: (error: Error) => void; | ||
defaultItemProps?: Partial<AntFormItemProps>; | ||
} | ||
|
||
export interface State { | ||
submitting: boolean; | ||
} | ||
|
||
export interface Context { | ||
schema: Schema; | ||
defaultItemProps: Partial<AntFormItemProps>; | ||
} | ||
|
||
class FormComponent extends React.Component<Props & AntFormProps> { | ||
state: State = { | ||
submitting: false | ||
}; | ||
|
||
static childContextTypes = childContextTypes; | ||
|
||
render() { | ||
const { | ||
resourceForm, | ||
submitButton, | ||
onSuccess, | ||
onFailure, | ||
children, | ||
form, | ||
...formProps | ||
} = this.props; | ||
|
||
return ( | ||
<AntForm | ||
{...formProps} | ||
onSubmit={this._onSubmit} | ||
> | ||
{children} | ||
{this._renderSubmitButton()} | ||
</AntForm> | ||
); | ||
} | ||
|
||
getChildContext() { | ||
return { | ||
schema: this.props.resourceForm.schema, | ||
defaultItemProps: this.props.defaultItemProps | ||
} | ||
} | ||
|
||
protected _renderSubmitButton() { | ||
const { | ||
submitButton = DEFAULT_SUBMIT_BUTTON | ||
} = this.props; | ||
|
||
if (submitButton === false) { | ||
return; | ||
} | ||
|
||
return React.cloneElement(submitButton, { | ||
loading: this.state.submitting, | ||
htmlType: 'submit' | ||
}); | ||
} | ||
|
||
protected _validate = () => | ||
new Promise<Data>((resolve, reject) => { | ||
(this.props.form as WrappedFormUtils) | ||
.validateFields((err, values) => { | ||
if (err) { | ||
return reject(Error('One or more fields are invalid')); | ||
} | ||
|
||
resolve(values); | ||
}); | ||
}) | ||
|
||
protected _onSubmit: React.EventHandler<React.FormEvent<any>> = e => { | ||
console.log('SUBMIT'); | ||
e.preventDefault(); | ||
|
||
this.setState({ submitting: true }); | ||
|
||
this._validate() | ||
.then(values => { | ||
return this.props.resourceForm.submit(values); | ||
}) | ||
.then(result => { | ||
if (this.props.onSuccess) { | ||
return this.props.onSuccess(result); | ||
} | ||
}) | ||
.catch(error => { | ||
if (this.props.onFailure) { | ||
return this.props.onFailure(error); | ||
} | ||
|
||
throw error; | ||
}) | ||
.finally(() => { | ||
this.setState({ submitting: false }); | ||
}); | ||
} | ||
} | ||
|
||
export const Form = AntForm.create()(FormComponent as any) as React.ComponentClass<Props>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
@import "~antd/lib/style/themes/default"; | ||
|
||
.login-form { | ||
width: 320px; | ||
|
||
&__alert { | ||
margin-bottom: @form-item-margin-bottom; | ||
} | ||
|
||
&__submit-button { | ||
width: 100%; | ||
} | ||
} |
Oops, something went wrong.