...
 
Commits (4)
......@@ -20,7 +20,7 @@ import { IconButton, Input, InputAdornment } from '@material-ui/core'
import { InputProps } from '@material-ui/core/Input'
import DeleteIcon from '@material-ui/icons/Delete'
import * as React from 'react'
import { FunctionComponent } from 'react'
import { ForwardRefExoticComponent, forwardRef } from 'react'
import { Omit } from '@gecogvidanto/shared'
......@@ -34,24 +34,23 @@ export interface DeletableFieldProps extends Omit<InputProps, 'endAdornment'> {
* A field with a delete button.
*/
// tslint:disable-next-line:variable-name (JSX)
const DeletableField: FunctionComponent<DeletableFieldProps> = ({
ariaLabel,
deleteDisabled,
onDelete,
...inputProps
}) => {
const adornment = (
<InputAdornment position="end">
<IconButton
aria-label={ariaLabel}
disabled={deleteDisabled}
onClick={onDelete}
>
<DeleteIcon />
</IconButton>
</InputAdornment>
)
return <Input {...inputProps} endAdornment={adornment} />
}
const DeletableField: ForwardRefExoticComponent<
DeletableFieldProps
> = forwardRef<any, DeletableFieldProps>(
({ ariaLabel, deleteDisabled, onDelete, ...inputProps }, ref) => {
const adornment = (
<InputAdornment position="end">
<IconButton
aria-label={ariaLabel}
disabled={deleteDisabled}
onClick={onDelete}
>
<DeleteIcon />
</IconButton>
</InputAdornment>
)
return <Input innerRef={ref} {...inputProps} endAdornment={adornment} />
}
)
export default DeletableField
......@@ -24,20 +24,28 @@ import { Validator } from '@gecogvidanto/shared'
import { useGameStore, useToolsStore } from '../../stores'
import { isGame } from '../../stores/GameStore'
import { DeletableField } from '../../components'
import { DeletableField as InnerDeletableField } from '../../components'
import { DeletableFieldProps } from '../../components/DeletableField'
import validable from '../../components/validableField'
import { ValidationEvent } from '../../hooks/FieldValidator'
const PLAYER_PREFIX = 'player-'
// tslint:disable-next-line:variable-name (JSX)
const DeletableField = validable<
DeletableFieldProps,
HTMLInputElement | HTMLTextAreaElement,
HTMLDivElement
>(InnerDeletableField)
export interface UpdatePlayerFieldProps {
order: number
readOnly: boolean
autoFocus: boolean
validators: Validator[]
onValidPlayer: (order: number, valid: boolean) => void
onDeletePlayer: (order: number) => void
onValidityChange: (order: number, valid: boolean) => void
onBlur: (order: number) => void
onDelete: (order: number) => void
}
/**
......@@ -50,8 +58,9 @@ const UpdatePlayerField: FunctionComponent<UpdatePlayerFieldProps> = observer(
readOnly,
autoFocus,
validators,
onValidPlayer,
onDeletePlayer,
onValidityChange,
onBlur: onBlurPlayer,
onDelete: onDeletePlayer,
}) => {
const gameStore = useGameStore()
const toolsStore = useToolsStore()
......@@ -74,10 +83,12 @@ const UpdatePlayerField: FunctionComponent<UpdatePlayerFieldProps> = observer(
)
const onValidate = useCallback(
(e: ValidationEvent<HTMLInputElement | HTMLTextAreaElement>): void => {
onValidPlayer(order, e.valid)
setValid(e.valid)
if (valid !== e.valid) {
setValid(e.valid)
onValidityChange(order, e.valid)
}
},
[order, onValidPlayer]
[order, onValidityChange, valid]
)
const onBlur = useCallback((): void => {
if (
......@@ -86,23 +97,24 @@ const UpdatePlayerField: FunctionComponent<UpdatePlayerFieldProps> = observer(
game.players[order].name !== name
) {
gameStore.updatePlayer(toolsStore.securityToken, order, name)
onBlurPlayer(order)
}
}, [valid && !!toolsStore.securityToken && name, order])
}, [
valid && !!toolsStore.securityToken && game,
valid && !!toolsStore.securityToken && name,
onBlurPlayer,
order
])
const onDelete = useCallback((): void => {
if (toolsStore.securityToken) {
onDeletePlayer(order)
gameStore.deletePlayer(toolsStore.securityToken, order)
onDeletePlayer(order)
}
}, [order, onDeletePlayer])
// tslint:disable-next-line:variable-name (JSX)
const Field = validable<
DeletableFieldProps,
HTMLInputElement | HTMLTextAreaElement,
HTMLDivElement
>(DeletableField)
// Render
return (
<Field
<DeletableField
id={PLAYER_PREFIX + order}
name={PLAYER_PREFIX + order}
label={lang.pageAddPlayersId(order + 1)}
......
......@@ -16,20 +16,31 @@
*/
import * as React from 'react'
import { FunctionComponent, forwardRef } from 'react'
import { Link } from 'react-router-dom'
import {
ForwardRefExoticComponent,
PropsWithoutRef,
RefAttributes,
forwardRef,
} from 'react'
import { Link, LinkProps } from 'react-router-dom'
import { Omit } from '@gecogvidanto/shared'
import { routes } from '../tools/constants'
export type LinkComponentProps = Omit<LinkProps, 'to' | 'ref'>
/**
* Build a link component to be used by material-ui, especially in menus.
* @param target The link target.
*/
export default function buildLinkComponent(
target: string
): FunctionComponent<any> {
return forwardRef(({ children, ...props }, ref) => (
<Link {...props} to={target} innerRef={ref} >
): ForwardRefExoticComponent<
PropsWithoutRef<LinkComponentProps> & RefAttributes<Link>
> {
return forwardRef<Link, LinkComponentProps>(({ children, ...props }, ref) => (
<Link {...props} to={target} ref={ref}>
{children}
</Link>
))
......
......@@ -23,6 +23,7 @@ import ExitIcon from '@material-ui/icons/MeetingRoom'
import PlayerAddIcon from '@material-ui/icons/PersonAdd'
import AboutIcon from '@material-ui/icons/Place'
import ActionIcon from '@material-ui/icons/Whatshot'
import Intl from 'intl-ts'
import { ComponentType, MouseEvent, useCallback, useMemo } from 'react'
import { CertLevel } from '@gecogvidanto/shared'
......@@ -40,12 +41,14 @@ export enum OpenDialog {
}
type MenuNames = {
[K in keyof langType]: langType[K] extends string ? K : never
}[keyof langType]
[K in keyof Intl<langType>]: Intl<langType>[K] extends (() => string)
? K
: never
}[keyof Intl<langType>]
export interface MenuEntryDescription<T extends MenuNames> {
export interface MenuEntryDescription {
icon: ComponentType<SvgIconProps>
title: T
title: MenuNames
action: string | ((event: MouseEvent<HTMLElement>) => void)
conditions: true | (() => boolean)
}
......@@ -54,7 +57,7 @@ export default function useMenuDescription(
onAddPlayer: () => void,
onPlayerExit: () => void,
onGameAction: (event: MouseEvent<HTMLElement>) => void
): Array<Array<MenuEntryDescription<any>>> {
): MenuEntryDescription[][] {
const userStore = useUserStore()
const gamePlayStore = useGamePlayStore()
......@@ -88,7 +91,7 @@ export default function useMenuDescription(
}, [])
// Menu
return useMemo<Array<Array<MenuEntryDescription<any>>>>(
return useMemo<MenuEntryDescription[][]>(
() => [
[
{
......
......@@ -18,18 +18,17 @@
import { ListItemIcon, ListItemText, MenuItem } from '@material-ui/core'
// tslint:disable-next-line:no-submodule-imports
import { SvgIconProps } from '@material-ui/core/SvgIcon'
import Intl, { Messages } from 'intl-ts'
import { observer } from 'mobx-react-lite'
import * as React from 'react'
import { ComponentType, FunctionComponent, MouseEvent } from 'react'
import buildLinkComponent from '../../components/links'
import buildLinkComponent, { LinkComponentProps } from '../../components/links'
import { useToolsStore } from '../../stores'
import { MenuEntryDescription } from './MenuDescription'
export interface MenuEntryProps {
description: MenuEntryDescription<any>
description: MenuEntryDescription
}
/**
......@@ -38,8 +37,8 @@ export interface MenuEntryProps {
// tslint:disable-next-line:variable-name (JSX)
const MenuEntry: FunctionComponent<MenuEntryProps> = observer(
({ description }) => {
const toolsStore = useToolsStore()
let component: ComponentType | undefined
const { lang } = useToolsStore()
let component: FunctionComponent<LinkComponentProps> | undefined
let onClick: ((event: MouseEvent<HTMLElement>) => void) | undefined
const action = description.action
if (typeof action === 'string') {
......@@ -48,40 +47,24 @@ const MenuEntry: FunctionComponent<MenuEntryProps> = observer(
onClick = action
}
function displayMenuEntryText<
T extends { [key in K]: () => string },
K extends keyof T
>(lang: T, message: K): JSX.Element {
return <ListItemText primary={lang[message]()} />
}
function displayMenuEntryContent<
T extends Messages,
K extends keyof T & string
>(
// tslint:disable-next-line:variable-name (needed for JSX)
Icon: ComponentType<SvgIconProps>,
lang: Intl<T>,
message: K
): JSX.Element {
// TODO To be removed when https://github.com/mui-org/material-ui/issues/14970 is corrected
// tslint:disable-next-line:variable-name (JSX)
const WorkaroundMenuItem: React.ElementType<any> = MenuItem
return (
<WorkaroundMenuItem component={component} onClick={onClick}>
<ListItemIcon>
<Icon />
</ListItemIcon>
{displayMenuEntryText(lang, message)}
</WorkaroundMenuItem>
)
}
// Render
return displayMenuEntryContent(
description.icon,
toolsStore.lang,
description.title
// tslint:disable-next-line:variable-name (needed for JSX)
const Icon: ComponentType<SvgIconProps> = description.icon
const menuEntryContent: JSX.Element[] = []
menuEntryContent.push(
<ListItemIcon key="icon">
<Icon />
</ListItemIcon>
)
menuEntryContent.push(
<ListItemText key="text" primary={lang[description.title]()} />
)
return !component ? (
<MenuItem onClick={onClick}>{...menuEntryContent}</MenuItem>
) : (
<MenuItem component={component} onClick={onClick}>
{...menuEntryContent}
</MenuItem>
)
}
)
......
......@@ -21,6 +21,7 @@ import { observer } from 'mobx-react-lite'
import * as React from 'react'
import { FunctionComponent } from 'react'
import { useEconomicSystemNames } from '../../hooks'
import { useToolsStore } from '../../stores'
import Plugin from './Plugin'
......@@ -43,11 +44,16 @@ const About: FunctionComponent = observer(() => {
const classes = useStyles()
const theme = useTheme<Theme>()
const { lang, serverInfo } = useToolsStore()
const getEconomicSystemName = useEconomicSystemNames()
const { applicationName: appName } = theme.gecogvidanto
// Render
const plugins: JSX.Element[] = serverInfo.plugins.map(plugin => (
<Plugin key={plugin.module} plugin={plugin} />
<Plugin
key={plugin.module}
plugin={plugin}
getEconomicSystemName={getEconomicSystemName}
/>
))
return (
<Paper className={classes.paper}>
......
......@@ -56,37 +56,41 @@ const useStyles = makeStyles((theme: Theme) => ({
export interface PluginProps {
plugin: ServerInfo['plugins'][number]
getEconomicSystemName: (id: string) => string
}
/**
* Display a plugin card.
*/
// tslint:disable-next-line:variable-name (JSX)
const Plugin: FunctionComponent<PluginProps> = observer(({ plugin }) => {
const classes = useStyles()
const { lang } = useToolsStore()
const Plugin: FunctionComponent<PluginProps> = observer(
({ plugin, getEconomicSystemName }) => {
const classes = useStyles()
const { lang } = useToolsStore()
// Render
return (
<Card className={classes.card}>
<div className={classes.mainContent}>
<CardHeader title={plugin.name} />
<CardContent>
<Typography color="textSecondary">{plugin.module}</Typography>
<Typography color="textSecondary">
{lang.pageAboutVersion(plugin.version)}
</Typography>
<Typography>{plugin.description}</Typography>
</CardContent>
</div>
<div className={classes.optionContent}>
<PluginOptions
database={plugin.database}
ecoSysIds={plugin.ecoSysIds}
/>
</div>
</Card>
)
})
// Render
return (
<Card className={classes.card}>
<div className={classes.mainContent}>
<CardHeader title={plugin.name} />
<CardContent>
<Typography color="textSecondary">{plugin.module}</Typography>
<Typography color="textSecondary">
{lang.pageAboutVersion(plugin.version)}
</Typography>
<Typography>{plugin.description}</Typography>
</CardContent>
</div>
<div className={classes.optionContent}>
<PluginOptions
database={plugin.database}
ecoSysIds={plugin.ecoSysIds}
getEconomicSystemName={getEconomicSystemName}
/>
</div>
</Card>
)
}
)
export default Plugin
......@@ -27,7 +27,6 @@ import * as React from 'react'
import { FunctionComponent } from 'react'
import { Center, NewLineJoined } from '../../components'
import { useEconomicSystemNames } from '../../hooks'
const useStyles = makeStyles((theme: Theme) => ({
avatar: {
......@@ -46,6 +45,7 @@ const useStyles = makeStyles((theme: Theme) => ({
export interface PluginOptionsProps {
database: boolean
ecoSysIds: ReadonlyArray<string>
getEconomicSystemName: (id: string) => string
}
/**
......@@ -53,9 +53,8 @@ export interface PluginOptionsProps {
*/
// tslint:disable-next-line:variable-name (JSX)
const PluginOptions: FunctionComponent<PluginOptionsProps> = observer(
({ database, ecoSysIds }) => {
({ database, ecoSysIds, getEconomicSystemName }) => {
const classes = useStyles()
const getEconomicSystemName = useEconomicSystemNames()
// Render
if (database) {
......
......@@ -19,6 +19,7 @@ import { Avatar, TableCell, TableRow, Theme } from '@material-ui/core'
// tslint:disable-next-line:no-submodule-imports
import { PaletteColor } from '@material-ui/core/styles/createPalette'
import { makeStyles } from '@material-ui/styles'
import classNames from 'classnames'
import { observer } from 'mobx-react-lite'
import * as React from 'react'
import { FunctionComponent } from 'react'
......@@ -79,6 +80,10 @@ const useStyles = makeStyles((theme: Theme) => ({
tableCell: {
color: 'inherit',
},
avatarTableCell: {
paddingTop: 0,
paddingBottom: 0,
},
}))
export interface GameRowProps {
......@@ -143,7 +148,9 @@ const GameRow: FunctionComponent<GameRowProps> = observer(
hover
onClick={handleGameSelect(game._id!)}
>
<TableCell className={classes.tableCell}>
<TableCell
className={classNames(classes.tableCell, classes.avatarTableCell)}
>
{gravatar && <Avatar src={gravatar.getImageUrl(PICTURE_SIZE)} />}
</TableCell>
<TableCell className={classes.tableCell}>
......
......@@ -84,11 +84,17 @@ const AddPlayers: FunctionComponent = observer(() => {
// Events
const onValidPlayer = useCallback(
(order: number, nameValid: boolean): void => {
setCurrentEdit(nameValid ? -1 : order)
if (!nameValid) {
setCurrentEdit(order)
}
setValid(nameValid)
},
[]
)
const onBlurPlayer = useCallback((): void => {
setCurrentEdit(-1)
setValid(true)
}, [])
const onDeletePlayer = useCallback((order: number): void => {
setCurrentEdit(previous =>
previous === order
......@@ -158,8 +164,9 @@ const AddPlayers: FunctionComponent = observer(() => {
readOnly={!modifiable || (currentEdit !== index && !valid)}
autoFocus={currentEdit === index}
validators={currentValidators(index)}
onValidPlayer={onValidPlayer}
onDeletePlayer={onDeletePlayer}
onValidityChange={onValidPlayer}
onBlur={onBlurPlayer}
onDelete={onDeletePlayer}
/>
))
if (modifiable && valid) {
......
......@@ -109,7 +109,7 @@ const Play: FunctionComponent<PlayProps> = observer(({ match }) => {
// Load game if parameter
useEffect(() => {
gameStore.loadIfNeeded(match.params.id)
})
}, [match.params.id])
// Set the playing game
useEffect(() => {
......@@ -117,7 +117,7 @@ const Play: FunctionComponent<PlayProps> = observer(({ match }) => {
return () => {
gamePlayStore.setPlayingGame()
}
})
}, [match.params.id])
// Render
const { state, formOption, remaining } = gamePlayStore
......
......@@ -15,6 +15,7 @@
* with @gecogvidanto/client-web. If not, see <http://www.gnu.org/licenses/>.
*/
import Intl from 'intl-ts'
import { observer } from 'mobx-react-lite'
import * as React from 'react'
import { ChangeEvent, FunctionComponent, useMemo } from 'react'
......@@ -29,8 +30,7 @@ import { langType } from '../../tools/ui.locale.en'
import { Mode } from './definitions'
class EmailValidator
implements AsyncValidator<langType, 'pageProfileEmailDuplicate', []> {
class EmailValidator implements AsyncValidator<Intl<langType>, []> {
public readonly params: [] = []
public constructor(private readonly id: string | undefined) {}
......
......@@ -15,6 +15,7 @@
* with @gecogvidanto/client-web. If not, see <http://www.gnu.org/licenses/>.
*/
import Intl from 'intl-ts'
import { observer } from 'mobx-react-lite'
import * as React from 'react'
import { ChangeEvent, FunctionComponent, useMemo } from 'react'
......@@ -29,8 +30,7 @@ import { langType } from '../../tools/ui.locale.en'
import { Mode } from './definitions'
class NameValidator
implements AsyncValidator<langType, 'pageProfileNameDuplicate', []> {
class NameValidator implements AsyncValidator<Intl<langType>, []> {
public readonly params: [] = []
public constructor(private readonly id: string | undefined) {}
......
......@@ -168,16 +168,12 @@ const PlayerDetails: FunctionComponent<PlayerDetailsProps> = observer(
}
listContent.push(<ListItemText key="playerData" primary={playerData} />)
}
// TODO To be removed when https://github.com/mui-org/material-ui/issues/14971 is corrected
const workaroundButton: true | undefined =
((editable && !edited) as true) || undefined
return (
<ListItem
button={workaroundButton}
onClick={editable ? onPlayerEdited : undefined}
>
return editable && !edited ? (
<ListItem button onClick={onPlayerEdited}>
{...listContent}
</ListItem>
) : (
<ListItem>{...listContent}</ListItem>
)
}
)
......
......@@ -28,7 +28,6 @@ import { FunctionComponent } from 'react'
import { Game, RoundSet } from '@gecogvidanto/shared'
import { useEconomicSystemNames } from '../../hooks'
import { useToolsStore } from '../../stores'
import FinishedRoundSet from './FinishedRoundSet'
......@@ -36,6 +35,7 @@ import FinishedRoundSet from './FinishedRoundSet'
export interface RoundSetDetailsProps {
game: Game
roundSet: { roundSet: RoundSet; index: number }
getEconomicSystemName: (id: string) => string
}
/**
......@@ -43,9 +43,8 @@ export interface RoundSetDetailsProps {
*/
// tslint:disable-next-line:variable-name (JSX)
const RoundSetDetails: FunctionComponent<RoundSetDetailsProps> = observer(
({ game, roundSet }) => {
const toolsStore = useToolsStore()
const getEconomicSystemName = useEconomicSystemNames()
({ game, roundSet, getEconomicSystemName }) => {
const { lang } = useToolsStore()
// Render
const finishedSet: boolean =
......@@ -56,13 +55,13 @@ const RoundSetDetails: FunctionComponent<RoundSetDetailsProps> = observer(
const roundSetContent: JSX.Element = finishedSet ? (
<FinishedRoundSet game={game} roundSet={roundSet} />
) : (
<Typography>{toolsStore.lang.pageGameSetInProgress()}</Typography>
<Typography>{lang.pageGameSetInProgress()}</Typography>
)
return (
<ExpansionPanel defaultExpanded={false}>
<ExpansionPanelSummary expandIcon={<ExpandIcon />}>
<Typography variant="h6" color={finishedSet ? 'inherit' : 'primary'}>
{toolsStore.lang.pageGameSet(roundSet.index, economicSystemName)}
{lang.pageGameSet(roundSet.index, economicSystemName)}
</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>{roundSetContent}</ExpansionPanelDetails>
......
......@@ -22,7 +22,7 @@ import { styled } from '@material-ui/styles'
* A separator between elements.
*/
// tslint:disable-next-line:variable-name (JSX)
const Separator = styled('div')((theme: Theme) => ({
const Separator = styled('div')<Theme>(({ theme }) => ({
height: theme.spacing(1),
}))
......
......@@ -25,6 +25,7 @@ import { RouteComponentProps } from 'react-router'
import { CertLevel } from '@gecogvidanto/shared'
import { Loading } from '../../components'
import { useEconomicSystemNames } from '../../hooks'
import { useGameStore, useToolsStore, useUserStore } from '../../stores'
import { isGame } from '../../stores/GameStore'
import { isUser } from '../../stores/UserStore'
......@@ -54,6 +55,7 @@ const View: FunctionComponent<ViewProps> = observer(({ match }) => {
const toolsStore = useToolsStore()
const gameStore = useGameStore()
const userStore = useUserStore()
const getEconomicSystemName = useEconomicSystemNames()
const { securityToken } = toolsStore
const { game } = gameStore
const { logged } = userStore
......@@ -67,12 +69,12 @@ const View: FunctionComponent<ViewProps> = observer(({ match }) => {
// Manage token
useEffect(() => {
toolsStore.initToken()
})
}, [])
// Load game
useEffect(() => {
gameStore.loadIfNeeded(match.params.id)
})
}, [match.params.id])
// Render
if (isGame(game) && game._id === match.params.id) {
......@@ -95,6 +97,7 @@ const View: FunctionComponent<ViewProps> = observer(({ match }) => {
key={id}
game={game}
roundSet={{ roundSet: set, index: id }}
getEconomicSystemName={getEconomicSystemName}
/>
))
)
......
......@@ -22,7 +22,7 @@ import { SinonStub } from 'sinon'
import { notificationManager } from '../tools/constants'
import { NotificationEvent } from '../tools/NotificationManager'
import { langType, messages } from '../tools/ui.locale.en'
import { messages } from '../tools/ui.locale.en'
export const SECURITY_TOKEN = 'security-token'
......@@ -31,7 +31,7 @@ export const languageMap = new LanguageMap(messages, 'en').merge({
eo: {},
})
export const ERROR_MSG: keyof langType = '$client$connection'
export const ERROR_MSG: '$client$connection' = '$client$connection'
export function defer(stub: SinonStub) {
let resolve: (...args: any[]) => any = () => {
......
......@@ -15,12 +15,14 @@
* with @gecogvidanto/client-web. If not, see <http://www.gnu.org/licenses/>.
*/
import Intl from 'intl-ts'
import { Validator } from '@gecogvidanto/shared'
import { langType } from './ui.locale.en'
export default class PlayerSameNameValidator
implements Validator<langType, 'pageAddPlayersSameName', []> {
implements Validator<Intl<langType>, []> {
public readonly params: [] = []
public constructor(private readonly names: string[]) {}
......
......@@ -32,13 +32,11 @@ import { langType } from './ui.locale.en'
*/
export function notifyError(error: unknown, lang: Intl<langType>): void {
const message: string =
error instanceof ApiError
? (lang as any)[error.messageKey]()
: lang.unknownError()
error instanceof ApiError ? error.getMessage(lang) : lang.unknownError()
notificationManager.emitError(message)
}
type Mergeable<P extends any[]> = ((...args: P) => void)
type Mergeable<P extends any[]> = (...args: P) => void
/**
* Create a function which is a merge of many function with similar signatures. Functions are called successively.
......
......@@ -15,26 +15,35 @@
* with @gecogvidanto/client. If not, see <http://www.gnu.org/licenses/>.
*/
import Intl from 'intl-ts'
import { langType } from '../tools/ui.locale.en'
type ApiErrorMessage<T extends Intl<langType>> = {
[K in keyof T]: T[K] extends (() => string) ? K : never
}[keyof T]
/**
* An error in the API, given a message key.
*/
export default class ApiError<
T extends langType & { [key in K]: string } = any,
K extends keyof T = any
T extends Intl<langType> = Intl<any>
> extends Error {
public constructor(public readonly messageKey: K) {
public constructor(private readonly messageKey: ApiErrorMessage<T>) {
super()
// Because of Error special behavior
Object.setPrototypeOf(this, new.target.prototype)
}
public getMessage(lang: T): string {
return (lang as Intl<any>)[this.messageKey]()
}
}
/**
* Build the server connection error.
*/
export function buildConnectionError(): ApiError {
return new ApiError<langType, '$client$connection'>('$client$connection')
return new ApiError('$client$connection')
}
......@@ -17,8 +17,6 @@
import { EconomicSystem, HttpError } from '@gecogvidanto/shared'
import { langType } from '../tools/ui.locale.en'
import ApiError, { buildConnectionError } from './ApiError'
import { httpClient as http } from './helpers'
......@@ -50,9 +48,7 @@ export default class EconomicSystemApi {
if (!error.response || error.response.status !== HttpError.NOT_FOUND) {
throw buildConnectionError()
} else {
throw new ApiError<langType, '$client$economicSystem'>(
'$client$economicSystem'
)
throw new ApiError('$client$economicSystem')
}
}
}
......
......@@ -17,8 +17,6 @@
import { EmailCheck, HttpError } from '@gecogvidanto/shared'
import { langType } from '../tools/ui.locale.en'
import ApiError, { buildConnectionError } from './ApiError'
import { httpClient as http } from './helpers'
......@@ -37,9 +35,7 @@ export default class EmailCheckApi {
if (!error.response || error.response.status !== HttpError.UNAUTHORIZED) {
throw buildConnectionError()
} else {
throw new ApiError<langType, '$client$emailCheck'>(
'$client$emailCheck'
)
throw new ApiError('$client$emailCheck')
}
}
}
......
......@@ -26,8 +26,6 @@ import {
Unlocked,
} from '@gecogvidanto/shared'
import { langType } from '../tools/ui.locale.en'
import ApiError, { buildConnectionError } from './ApiError'
import { httpClient as http } from './helpers'
......@@ -97,7 +95,7 @@ export default class GameApi {
) {
throw buildConnectionError()
} else {
throw new ApiError<langType, '$client$gameClose'>('$client$gameClose')
throw new ApiError('$client$gameClose')
}
}
}
......@@ -149,9 +147,7 @@ export default class GameApi {
if (!error.response || error.response.status !== HttpError.CONFLICT) {
throw buildConnectionError()
} else {
throw new ApiError<langType, '$client$gamePlayers'>(
'$client$gamePlayers'
)
throw new ApiError('$client$gamePlayers')
}
}
}
......@@ -180,9 +176,7 @@ export default class GameApi {
if (!error.response || error.response.status !== HttpError.CONFLICT) {
throw buildConnectionError()
} else {
throw new ApiError<langType, '$client$gamePlayers'>(
'$client$gamePlayers'
)
throw new ApiError('$client$gamePlayers')
}
}
}
......@@ -232,7 +226,7 @@ export default class GameApi {
if (!error.response || error.response.status !== HttpError.CONFLICT) {
throw buildConnectionError()
} else {
throw new ApiError<langType, '$client$gameSets'>('$client$gameSets')
throw new ApiError('$client$gameSets')
}
}
}
......@@ -368,9 +362,7 @@ export default class GameApi {
) {
throw buildConnectionError()
} else {
throw new ApiError<langType, '$client$gameNextRound'>(
'$client$gameNextRound'
)
throw new ApiError('$client$gameNextRound')
}
}
}
......@@ -395,9 +387,7 @@ export default class GameApi {
if (!error.response || error.response.status !== HttpError.CONFLICT) {
throw buildConnectionError()
} else {
throw new ApiError<langType, '$client$gameTechBreak'>(
'$client$gameTechBreak'
)
throw new ApiError('$client$gameTechBreak')
}
}
}
......
......@@ -17,8 +17,6 @@
import { HttpError, User } from '@gecogvidanto/shared'
import { langType } from '../tools/ui.locale.en'
import ApiError, { buildConnectionError } from './ApiError'
import { httpClient as http } from './helpers'
......@@ -39,9 +37,7 @@ export default class UserApi {
if (!error.response || error.response.status !== HttpError.UNAUTHORIZED) {
throw buildConnectionError()
} else {
throw new ApiError<langType, '$client$credentials'>(
'$client$credentials'
)
throw new ApiError('$client$credentials')
}
}
}
......@@ -83,9 +79,7 @@ export default class UserApi {
if (!error.response || error.response.status !== HttpError.CONFLICT) {
throw buildConnectionError()
} else {
throw new ApiError<langType, '$client$userExists'>(
'$client$userExists'
)
throw new ApiError('$client$userExists')
}
}
}
......@@ -141,9 +135,7 @@ export default class UserApi {
if (!error.response || error.response.status !== HttpError.CONFLICT) {
throw buildConnectionError()
} else {
throw new ApiError<langType, '$client$userExists'>(
'$client$userExists'
)
throw new ApiError('$client$userExists')
}
}
}
......
......@@ -34,8 +34,7 @@ export function extractError<R>(method: () => Promise<R>): Promise<R> {
.then(value => resolve(value))
.catch(error => {
if (error instanceof ApiError) {
const message = (lang as any)[error.messageKey]()
reject(new Error(message))
reject(new Error(error.getMessage(lang)))
} else {
reject(error)
}
......
......@@ -134,7 +134,7 @@ export default class HttpsServer {
_res: Response,
next: NextFunction
): void => {
winston.silly(req.originalUrl)
winston.silly(`${req.ip}: ${req.method} ${req.originalUrl}`)
next()
}
......
......@@ -15,25 +15,22 @@
* with @gecogvidanto/shared. If not, see <http://www.gnu.org/licenses/>.
*/
import { Messages } from 'intl-ts'
import Intl, { Messages } from 'intl-ts'
import Validator from './Validator'
import Validator, {ValidatorMessage} from './Validator'
/**
* A wrapper which adds emptiness to validator allowed values.
*/
export default class AllowEmpty<
T extends Messages & { [key in K]: string | ((...args: P) => string) },
K extends keyof T,
P extends any[]
> implements Validator<T, K, P> {
export default class AllowEmpty<T extends Intl<Messages>, P extends any[]>
implements Validator<T, P> {
public readonly params: P
public constructor(private validator: Validator<T, K, P>) {
public constructor(private validator: Validator<T, P>) {
this.params = validator.params
}
public validate(value: string): K | undefined {
public validate(value: string): ValidatorMessage<T, P> | undefined {
if (value.length === 0) {
return undefined
} else {
......
......@@ -27,8 +27,7 @@ import { AsyncValidator, asyncValidate, messages } from '..'
chai.use(chaiAsPromised)
class Validator
implements AsyncValidator<typeof messages, '$shared$nonMatching', []> {
class Validator implements AsyncValidator<Intl<typeof messages>, []> {
public readonly params: [] = []
public async validate(
......
......@@ -15,16 +15,17 @@
* with @gecogvidanto/shared. If not, see <http://www.gnu.org/licenses/>.
*/
import { Messages } from 'intl-ts'
import Intl, { Messages } from 'intl-ts'
import { ValidatorMessage } from './Validator'
/**
* A validator interface.
*/
export default interface AsyncValidator<
T extends Messages & { [key in K]: string | ((...args: P) => string) } = any,
K extends keyof T = any,
T extends Intl<Messages> = Intl<any>,
P extends any[] = any[]
> {
params: P
validate: (value: string) => Promise<K | undefined>
validate: (value: string) => Promise<ValidatorMessage<T, P> | undefined>
}
......@@ -15,6 +15,8 @@
* with @gecogvidanto/shared. If not, see <http://www.gnu.org/licenses/>.
*/
import Intl from 'intl-ts'
import { langType } from '../tools/ui.locale.en'
import { RegExpValidator } from './Validator'
......@@ -22,11 +24,7 @@ import { RegExpValidator } from './Validator'
/**
* An e-mail validator.
*/
export default class Email<T extends langType> extends RegExpValidator<
T,
'$shared$invalidEmail',
[]
> {
export default class Email extends RegExpValidator<Intl<langType>, []> {
/**
* Create the validator.
*/
......
......@@ -15,6 +15,8 @@
* with @gecogvidanto/shared. If not, see <http://www.gnu.org/licenses/>.
*/
import Intl from 'intl-ts'
import { langType } from '../tools/ui.locale.en'
import Validator from './Validator'
......@@ -22,8 +24,7 @@ import Validator from './Validator'
/**
* A validator for identity value. Mostly useful for hidden password control.
*/
export default class Identity<T extends langType>
implements Validator<T, '$shared$nonMatching', []> {
export default class Identity implements Validator<Intl<langType>, []> {
public readonly params: [] = []
/**
......
......@@ -15,6 +15,8 @@
* with @gecogvidanto/shared. If not, see <http://www.gnu.org/licenses/>.
*/
import Intl from 'intl-ts'
import { langType } from '../tools/ui.locale.en'
import Validator from './Validator'
......@@ -22,8 +24,7 @@ import Validator from './Validator'
/**
* A validator for a maximum value length.
*/
export default class MaxLength<T extends langType>
implements Validator<T, '$shared$notInList', [string]> {
export default class MaxLength implements Validator<Intl<langType>, [string]> {
public readonly params: [string]
private readonly values: ReadonlySet<string>
......
......@@ -15,6 +15,8 @@
* with @gecogvidanto/shared. If not, see <http://www.gnu.org/licenses/>.
*/
import Intl from 'intl-ts'
import { langType } from '../tools/ui.locale.en'
import Validator from './Validator'
......@@ -22,8 +24,7 @@ import Validator from './Validator'
/**
* A validator for a maximum value length.
*/
export default class MaxLength<T extends langType>
implements Validator<T, '$shared$maxLength', [number]> {
export default class MaxLength implements Validator<Intl<langType>, [number]> {
public readonly params: [number]
/**
......
......@@ -15,6 +15,8 @@
* with @gecogvidanto/shared. If not, see <http://www.gnu.org/licenses/>.
*/
import Intl from 'intl-ts'
import { langType } from '../tools/ui.locale.en'
import Validator from './Validator'
......@@ -22,8 +24,7 @@ import Validator from './Validator'
/**
* A validator for a minimum value length.
*/
export default class MinLength<T extends langType>
implements Validator<T, '$shared$minLength', [number]> {
export default class MinLength implements Validator<Intl<langType>, [number]> {
public readonly params: [number]
/**
......
......@@ -15,6 +15,8 @@
* with @gecogvidanto/shared. If not, see <http://www.gnu.org/licenses/>.
*/
import Intl from 'intl-ts'
import { langType } from '../tools/ui.locale.en'
import Validator from './Validator'
......@@ -22,8 +24,7 @@ import Validator from './Validator'
/**
* A validator for non-empty value.
*/
export default class NonEmpty<T extends langType>
implements Validator<T, '$shared$empty', []> {
export default class NonEmpty implements Validator<Intl<langType>, []> {
public readonly params: [] = []
public validate(value: string): '$shared$empty' | undefined {
......
......@@ -15,6 +15,8 @@
* with @gecogvidanto/shared. If not, see <http://www.gnu.org/licenses/>.
*/
import Intl from 'intl-ts'
import { langType } from '../tools/ui.locale.en'
import Validator from './Validator'
......@@ -22,8 +24,7 @@ import Validator from './Validator'
/**
* A validator for number values.
*/
export default class NumberValue<T extends langType>
implements Validator<T, '$shared$nonMatching', []> {
export default class NumberValue implements Validator<Intl<langType>, []> {
public readonly params: [] = []
/**
......
......@@ -15,6 +15,8 @@
* with @gecogvidanto/shared. If not, see <http://www.gnu.org/licenses/>.
*/
import Intl from 'intl-ts'
import { langType } from '../tools/ui.locale.en'
import Validator from './Validator'
......@@ -22,8 +24,7 @@ import Validator from './Validator'
/**
* A validator for a string enumeration.
*/
export default class StringEnum<T extends langType>
implements Validator<T, '$shared$nonMatching', []> {
export default class StringEnum implements Validator<Intl<langType>, []> {
public readonly params: [] = []
private readonly values: ReadonlySet<string>
......
......@@ -15,35 +15,35 @@
* with @gecogvidanto/shared. If not, see <http://www.gnu.org/licenses/>.
*/
import { Messages } from 'intl-ts'
import Intl, { Messages } from 'intl-ts'
export type ValidatorMessage<T extends Intl<Messages>, P extends any[]> = {
[K in keyof T]: T[K] extends ((...args: P) => string) ? K : never
}[keyof T]
/**
* A validator interface.
*/
export default interface Validator<
T extends Messages & { [key in K]: string | ((...args: P) => string) } = any,
K extends keyof T = any,
T extends Intl<Messages> = Intl<any>,
P extends any[] = any[]
> {
readonly params: P
validate: (value: string) => K | undefined
validate: (value: string) => ValidatorMessage<T, P> | undefined
}
/**
* A validator based on regular expression.
*/
export class RegExpValidator<
T extends Messages & { [key in K]: string | ((...args: P) => string) },
K extends keyof T,
P extends any[]
> implements Validator<T, K, P> {
export class RegExpValidator<T extends Intl<Messages>, P extends any[]>
implements Validator<T, P> {
public constructor(
private readonly regExp: RegExp,
private readonly msgKey: K,
private readonly msgKey: ValidatorMessage<T, P>,
public readonly params: P
) {}
public validate(value: string): K | undefined {
public validate(value: string): ValidatorMessage<T, P> | undefined {
if (this.regExp.test(value)) {
return undefined
} else {
......
......@@ -18,7 +18,7 @@
import Intl, { Messages } from 'intl-ts'
import AsyncValidator from './AsyncValidator'
import Validator from './Validator'
import Validator, { ValidatorMessage } from './Validator'
/**
* Validate the value with the validator.
......@@ -35,29 +35,21 @@ export function validate(value: string, validator: Validator): boolean
* @param lang The internationalization object.
* @returns undefined if the value is validated, an error message otherwise.
*/
export function validate<
T extends Messages & { [key in K]: string | ((...args: P) => string) },
K extends keyof T,
P extends any[]
>(
export function validate<T extends Intl<Messages>, P extends any[]>(
value: string,
validator: Validator<T, K, P>,
lang: Intl<T>
validator: Validator<T, P>,
lang: T
): string | undefined
/**
* Validation implementation.
*/
export function validate<
T extends Messages & { [key in K]: string | ((...args: P) => string) },
K extends keyof T,
P extends any[]
>(
export function validate<T extends Intl<Messages>, P extends any[]>(
value: string,
validator: Validator<T, K, P>,
lang?: Intl<T>
validator: Validator<T, P>,
lang?: T
): boolean | string | undefined {
return convertResult(validator.validate(value), validator.params, lang)
return validateResult(validator.validate(value), validator.params, lang)
}
/**
......@@ -78,42 +70,38 @@ export function asyncValidate(
* @param lang The internationalization object.
* @returns undefined if the value is validated, an error message otherwise.
*/
export function asyncValidate<
T extends Messages & { [key in K]: string | ((...args: P) => string) },
K extends keyof T,
P extends any[]
>(
export function asyncValidate<T extends Intl<Messages>, P extends any[]>(
value: string,
validator: AsyncValidator<T, K, P>,
lang: Intl<T>
validator: AsyncValidator<T, P>,
lang: T
): Promise<string | undefined>
/**
* Asunc validation implementation.
*/
export async function asyncValidate<
T extends Messages & { [key in K]: string | ((...args: P) => string) },
K extends keyof T,
P extends any[]
>(
export async function asyncValidate<T extends Intl<Messages>, P extends any[]>(
value: string,
validator: AsyncValidator<T, K, P>,
lang?: Intl<T>
validator: AsyncValidator<T, P>,
lang?: T
): Promise<boolean | string | undefined> {
return convertResult(await validator.validate(value), validator.params, lang)
return validateResult(
await validator.validate(value),
validator.params,
lang
)
}
/**
* Convert the result into either a boolean or a string (or undefined), depending on if lang is provided.
* Validate the result into either a boolean or a string (or undefined), depending on if lang is provided.
* @param result The result to convert, which is either a message property or undefined.
* @param params The parameters of the validator.
* @param lang The language to use.
*/
function convertResult<
T extends { [key in K]: (...args: P) => string },
K extends keyof T,
P extends any[]
>(result: K | undefined, params: P, lang?: T): boolean | string | undefined {
function validateResult<T extends Intl<Messages>, P extends any[]>(
result: ValidatorMessage<T, P> | undefined,
params: P,
lang?: T
): boolean | string | undefined {
if (result === undefined) {
return lang ? undefined : true
} else {
......