Commit 7245e804 authored by Stéphane Veyret's avatar Stéphane Veyret

Convert into react hooks

parent 9afc11e0
Pipeline #4928 canceled with stages
......@@ -10,7 +10,7 @@ before_script:
build:
stage: build
tags:
- duniter
- redshift
script:
- npx lerna run build
artifacts:
......@@ -24,7 +24,7 @@ build:
test:
stage: test
tags:
- duniter
- redshift
dependencies:
- build
script:
......
......@@ -52,17 +52,18 @@
"@gecogvidanto/client": "^1.0.0",
"@material-ui/core": "3.9.2",
"@material-ui/icons": "3.0.2",
"@material-ui/styles": "^3.0.0-alpha.10",
"classnames": "2.2.6",
"deepmerge": "3.2.0",
"is-plain-object": "2.0.4",
"jss": "9.8.7",
"mobx": "5.9.0",
"mobx-loadable": "1.0.1",
"mobx-react": "5.4.3",
"mobx-react-lite": "1.1.0",
"path-to-regexp": "3.0.0",
"places.js": "1.15.5",
"react": "16.8.3",
"react-dom": "16.8.3",
"react-jss": "8.6.1",
"react-router": "4.3.1",
"react-router-dom": "4.3.1",
"recharts": "1.5.0",
......@@ -86,11 +87,11 @@
"@types/chai-as-promised": "7.1.0",
"@types/classnames": "2.2.7",
"@types/html-webpack-plugin": "3.2.0",
"@types/jss": "9.5.8",
"@types/mocha": "5.2.6",
"@types/node": "11.9.5",
"@types/react": "16.8.4",
"@types/react-dom": "16.8.2",
"@types/react-jss": "8.6.3",
"@types/react-router": "4.4.4",
"@types/react-router-dom": "4.3.1",
"@types/recharts": "1.1.13",
......
......@@ -16,88 +16,86 @@
*/
import {
MuiThemeProvider,
StylesProvider,
ThemeProvider,
createGenerateClassName,
} from '@material-ui/core/styles'
} from '@material-ui/styles'
import { LanguageMap } from 'intl-ts'
import * as React from 'react'
import { Component, ReactNode } from 'react'
import { JssProvider } from 'react-jss'
import { FunctionComponent, useEffect } from 'react'
import { BrowserRouter } from 'react-router-dom'
import Routes from '../layout/Routes'
import { StoreSet, StoreSetProvider, buildStoreSet } from '../stores'
import createDefaultTheme from '../themes/default'
import { AppContext, Provider, buildContext } from '../tools/AppContext'
import UiUrlAnalyzer from '../tools/UiUrlAnalyzer'
const appTheme = createDefaultTheme()
const appStoreSet: StoreSet = (() => {
window.__PRELOADED_STATE__ = window.__PRELOADED_SENT__ as any
if (!window.__PRELOADED_STATE__) {
throw new Error('Application was not properly initialized')
}
export default class Main extends Component {
private static readonly appContext: AppContext = (() => {
window.__PRELOADED_STATE__ = window.__PRELOADED_SENT__ as any
if (!window.__PRELOADED_STATE__) {
throw new Error('Application was not properly initialized')
}
// Convert values into methods
if (window.__PRELOADED_STATE__.users) {
window.__PRELOADED_STATE__.users.read = Main.valueToMethod(
window.__PRELOADED_SENT__!.users!.read!
)
window.__PRELOADED_STATE__.users.checkEmail = Main.valueToMethod(
window.__PRELOADED_SENT__!.users!.checkEmail!
)
}
if (window.__PRELOADED_STATE__.games) {
window.__PRELOADED_STATE__.games.search = Main.valueToMethod(
window.__PRELOADED_SENT__!.games!.search!
)
}
if (window.__PRELOADED_STATE__.economicSystems) {
window.__PRELOADED_STATE__.economicSystems.search = Main.valueToMethod(
window.__PRELOADED_SENT__!.economicSystems!.search!
)
}
// Delete sent values
delete window.__PRELOADED_SENT__
// Get preloaded values
const {
tools: toolsDesc,
users: userDesc,
games: gameDesc,
} = window.__PRELOADED_STATE__
// Create context
const { preferences, languageMap } = toolsDesc.lang
const logged = userDesc && userDesc.logged
const game = gameDesc && gameDesc.current
return buildContext(
toolsDesc.serverInfo,
toolsDesc.captchaKey,
new LanguageMap(languageMap),
preferences,
logged,
game
// Convert values into methods
if (window.__PRELOADED_STATE__.users) {
window.__PRELOADED_STATE__.users.read = valueToMethod(
window.__PRELOADED_SENT__!.users!.read!
)
window.__PRELOADED_STATE__.users.checkEmail = valueToMethod(
window.__PRELOADED_SENT__!.users!.checkEmail!
)
}
if (window.__PRELOADED_STATE__.games) {
window.__PRELOADED_STATE__.games.search = valueToMethod(
window.__PRELOADED_SENT__!.games!.search!
)
})()
}
if (window.__PRELOADED_STATE__.economicSystems) {
window.__PRELOADED_STATE__.economicSystems.search = valueToMethod(
window.__PRELOADED_SENT__!.economicSystems!.search!
)
}
private readonly basename: string
// Delete sent values
delete window.__PRELOADED_SENT__
public constructor(props: {}) {
super(props)
this.basename = new UiUrlAnalyzer(
window.location.pathname,
Main.appContext.toolsStore.lang.$languageMap.availables
).langUrl
}
// Get preloaded values
const {
tools: toolsDesc,
users: userDesc,
games: gameDesc,
} = window.__PRELOADED_STATE__
private static valueToMethod<T>(value: T): () => T {
return () => value
}
// Create context
const { preferences, languageMap } = toolsDesc.lang
const logged = userDesc && userDesc.logged
const game = gameDesc && gameDesc.current
return buildStoreSet(
toolsDesc.serverInfo,
toolsDesc.captchaKey,
new LanguageMap(languageMap),
preferences,
logged,
game
)
})()
const basename = new UiUrlAnalyzer(
window.location.pathname,
appStoreSet.toolsStore.lang.$languageMap.availables
).langUrl
function valueToMethod<T>(value: T): () => T {
return () => value
}
public componentDidMount(): void {
/**
* The main entry point.
*/
// tslint:disable-next-line:variable-name (JSX)
const Main: FunctionComponent = () => {
// Terminate initialisation when first called
useEffect(() => {
if ('__PRELOADED_STATE__' in window) {
delete window.__PRELOADED_STATE__
}
......@@ -105,19 +103,19 @@ export default class Main extends Component {
if (jssStyles && jssStyles.parentNode) {
jssStyles.parentNode.removeChild(jssStyles)
}
}
}, [])
public render(): ReactNode {
return (
<JssProvider generateClassName={createGenerateClassName()}>
<MuiThemeProvider theme={appTheme}>
<Provider value={Main.appContext}>
<BrowserRouter basename={this.basename}>
<Routes />
</BrowserRouter>
</Provider>
</MuiThemeProvider>
</JssProvider>
)
}
return (
<BrowserRouter basename={basename}>
<StylesProvider generateClassName={createGenerateClassName()}>
<ThemeProvider theme={appTheme}>
<StoreSetProvider value={appStoreSet}>
<Routes />
</StoreSetProvider>
</ThemeProvider>
</StylesProvider>
</BrowserRouter>
)
}
export default Main
......@@ -15,11 +15,15 @@
* with @gecogvidanto/client-web. If not, see <http://www.gnu.org/licenses/>.
*/
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { install } from '@material-ui/styles'
import { createElement } from 'react'
import { hydrate } from 'react-dom'
import Main from './Main'
// TODO Temporary workaround - See https://material-ui.com/css-in-js/basics/#migration-for-material-ui-core-users
install()
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.hydrate(React.createElement(Main), document.querySelector('#root'))
hydrate(createElement(Main), document.querySelector('#root'))
})
......@@ -15,58 +15,68 @@
* with @gecogvidanto/client-web. If not, see <http://www.gnu.org/licenses/>.
*/
import Button from '@material-ui/core/Button'
import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent'
import DialogTitle from '@material-ui/core/DialogTitle'
import { observer } from 'mobx-react'
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
} from '@material-ui/core'
import { observer } from 'mobx-react-lite'
import * as React from 'react'
import { Component, ReactNode } from 'react'
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
import { Validator, validators } from '@gecogvidanto/shared'
import GameStore, { isGame } from '../stores/GameStore'
import ToolsStore from '../stores/ToolsStore'
import { inject } from '../tools/AppContext'
import PlayerSameNameValidator from '../tools/PlayerSameNameValidator'
import { ValidableInput } from '../../components'
import { ValidationEvent } from '../../hooks/FieldValidator'
import { useGameStore, useToolsStore } from '../../stores'
import { isGame } from '../../stores/GameStore'
import PlayerSameNameValidator from '../../tools/PlayerSameNameValidator'
import { ValidationEvent } from './FieldValidator'
import { ValidatedInput } from './ValidatedField'
interface Props {
export interface AddPlayerProps {
open: boolean
onClose: () => void
gameStore: GameStore
toolsStore: ToolsStore
}
interface State {
name?: string
}
/**
* A dialog used to add a player.
*/
@observer
class Confirm extends Component<Props, State> {
public constructor(props: Props) {
super(props)
this.state = {}
if (props.open) {
props.toolsStore.initToken()
}
}
// tslint:disable-next-line:variable-name (JSX)
const AddPlayer: FunctionComponent<AddPlayerProps> = observer(
({ open, onClose }) => {
const gameStore = useGameStore()
const toolsStore = useToolsStore()
public componentDidUpdate(previousProps: Props) {
const { open, toolsStore } = this.props
if (open && !previousProps.open) {
toolsStore.initToken()
}
}
// State
const [name, setName] = useState<string>()
public render(): ReactNode {
const { open, gameStore, toolsStore } = this.props
// Constants
const valid: boolean = !!toolsStore.securityToken && !!name
// Events
const onValueValidate = useCallback(
(e: ValidationEvent<HTMLInputElement | HTMLTextAreaElement>): void => {
setName(e.valid ? e.target.value : undefined)
},
[]
)
const onCancel = useCallback((): void => {
onClose()
}, [onClose])
const onOk = useCallback((): void => {
if (valid) {
onClose()
gameStore.addPlayer(toolsStore.securityToken!, name!)
}
}, [onClose, name, valid])
// Security token
useEffect(() => {
open && toolsStore.initToken()
}, [open])
// Render
const { game } = gameStore
const id: number = isGame(game) ? game.players.length + 1 : 0
const addValidators: Validator[] = [
......@@ -76,67 +86,32 @@ class Confirm extends Component<Props, State> {
)
]
return (
<Dialog
open={open}
onClose={this.onCancel}
aria-labelledby="add-player-title"
>
<Dialog open={open} onClose={onCancel} aria-labelledby="add-player-title">
<DialogTitle id="add-player-title">
{toolsStore.lang.menuPlayerAdd()}
</DialogTitle>
<DialogContent>
<ValidatedInput
<ValidableInput
id="player-name"
name="player-name"
label={toolsStore.lang.pageAddPlayersId(id)}
autoFocus
fullWidth
lang={toolsStore.lang}
validators={addValidators}
onValidate={this.onValueValidate}
onValidate={onValueValidate}
/>
</DialogContent>
<DialogActions>
<Button onClick={this.onCancel} color="primary">
<Button onClick={onCancel} color="primary">
{toolsStore.lang.cancel()}
</Button>
<Button
disabled={!this.valid}
onClick={this.onOk}
color="primary"
autoFocus
>
<Button disabled={!valid} onClick={onOk} color="primary" autoFocus>
{toolsStore.lang.ok()}
</Button>
</DialogActions>
</Dialog>
)
}
)
private onValueValidate = (
e: ValidationEvent<HTMLInputElement | HTMLTextAreaElement>
): void => {
this.setState({ name: e.valid ? e.target.value : undefined })
}
private onCancel = (): void => {
this.props.onClose()
}
private onOk = (): void => {
const { gameStore, toolsStore } = this.props
const { name } = this.state
if (this.valid) {
this.props.onClose()
gameStore.addPlayer(toolsStore.securityToken!, name!)
}
}
private get valid(): boolean {
const { toolsStore } = this.props
const { name } = this.state
return !!toolsStore.securityToken && !!name
}
}
export default inject('gameStore', 'toolsStore')(Confirm)
export default AddPlayer
/*
* This file is part of @gecogvidanto/client-web.
*
* @gecogvidanto/client-web is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @gecogvidanto/client-web is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with @gecogvidanto/client-web. If not, see <http://www.gnu.org/licenses/>.
*/
export { default, AddPlayerProps } from './AddPlayer'
......@@ -14,56 +14,54 @@
* You should have received a copy of the GNU General Public License along
* with @gecogvidanto/client-web. If not, see <http://www.gnu.org/licenses/>.
*/
import FormControl from '@material-ui/core/FormControl'
import Input from '@material-ui/core/Input'
import InputLabel from '@material-ui/core/InputLabel'
import { observer } from 'mobx-react'
import { FormControl, Input, InputLabel } from '@material-ui/core'
import { observer } from 'mobx-react-lite'
import * as React from 'react'
import { Component, ReactNode, RefObject } from 'react'
import { FunctionComponent, useCallback } from 'react'
import { Validator } from '@gecogvidanto/shared'
import GameStore from '../stores/GameStore'
import ToolsStore from '../stores/ToolsStore'
import { inject } from '../tools/AppContext'
import FieldValidator, { ValidationEvent } from './FieldValidator'
import { useFieldValidator } from '../../hooks'
import { ValidationEvent } from '../../hooks/FieldValidator'
import { useGameStore, useToolsStore } from '../../stores'
interface Props {
export interface AddPlayerFieldProps {
id: string
name: string
label: string
autoFocus: boolean
validators: Validator[]
onAddPlayer: () => void
gameStore: GameStore
toolsStore: ToolsStore
}
/**
* A field used to add a player.
*/
@observer
class AddPlayerField extends Component<Props> {
public render(): ReactNode {
const { validators, toolsStore } = this.props
return (
<FieldValidator
lang={toolsStore.lang}
validators={validators}
onValidate={this.onValidate}
>
{this.onRenderField}
</FieldValidator>
// tslint:disable-next-line:variable-name (JSX)
const AddPlayerField: FunctionComponent<AddPlayerFieldProps> = observer(
({ id, name, label, autoFocus, validators, onAddPlayer }) => {
const gameStore = useGameStore()
const toolsStore = useToolsStore()
// Events
const onValidate = useCallback(
(e: ValidationEvent<HTMLInputElement | HTMLTextAreaElement>): void => {
if (toolsStore.securityToken && e.valid) {
gameStore.addPlayer(toolsStore.securityToken, e.target.value)
onAddPlayer()
}
},
[onAddPlayer]
)
}
private onRenderField = (
fieldRef: RefObject<HTMLInputElement | HTMLTextAreaElement>,
onChangeValid: () => void,
onBlurValid: () => void
): ReactNode => {
const { id, name, label, autoFocus } = this.props
// Field validator
const { fieldRef, onChange, onBlur } = useFieldValidator({
validators,
onValidate,
})
// Render
return (
<FormControl margin="normal" fullWidth>
<InputLabel htmlFor={id}>{label}</InputLabel>
......@@ -72,22 +70,12 @@ class AddPlayerField extends Component<Props> {
name={name}
autoFocus={autoFocus}
inputRef={fieldRef}
onChange={onChangeValid}
onBlur={onBlurValid}
onChange={onChange}
onBlur={onBlur}
/>
</FormControl>
)
}
)
private onValidate = (
e: ValidationEvent<HTMLInputElement | HTMLTextAreaElement>
): void => {
const { onAddPlayer, gameStore, toolsStore } = this.props
if (toolsStore.securityToken && e.valid) {
gameStore.addPlayer(toolsStore.securityToken, e.target.value)
onAddPlayer()
}
}
}
export default inject('toolsStore', 'gameStore')(AddPlayerField)
export default AddPlayerField
/*
* This file is part of @gecogvidanto/client-web.
*
* @gecogvidanto/client-web is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @gecogvidanto/client-web is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with @gecogvidanto/client-web. If not, see <http://www.gnu.org/licenses/>.
*/
export { default, AddPlayerFieldProps } from './AddPlayerField'
......@@ -15,18 +15,25 @@
* with @gecogvidanto/client-web. If not, see <http://www.gnu.org/licenses/>.
*/
import FormControl, { FormControlProps } from '@material-ui/core/FormControl'
import { FormControl, Input, InputLabel } from '@material-ui/core'
// tslint:disable-next-line:no-submodule-imports
import { FormControlProps } from '@material-ui/core/FormControl'
import FormControlContext, {
FormControlContextProps,
// tslint:disable-next-line:no-submodule-imports
} from '@material-ui/core/FormControl/FormControlContext'
import Input from '@material-ui/core/Input'
import InputLabel from '@material-ui/core/InputLabel'
import * as React from 'react'
import { ChangeEvent, PureComponent, ReactNode } from 'react'
import {
ChangeEvent,
FunctionComponent,
useCallback,
useEffect,
useRef,
} from 'react'
import { IdentifiedAddress } from '@gecogvidanto/shared'
interface Props {
export interface AddressProps {
appId?: string
apiKey?: string
id: string
......@@ -44,114 +51,132 @@ interface Props {