Migrate CreateServer component to Typescript

This commit is contained in:
Alejandro Celaya 2020-08-22 17:58:44 +02:00
parent 2db85c2783
commit 7c67fa4149
7 changed files with 88 additions and 46 deletions

6
package-lock.json generated
View file

@ -3056,6 +3056,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/classnames": {
"version": "2.2.10",
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.10.tgz",
"integrity": "sha512-1UzDldn9GfYYEsWWnn/P4wkTlkZDH7lDb0wBMGbtIQc9zXEQq7FlKBdZUn6OBqD8sKZZ2RQO2mAjGpXiDGoRmQ==",
"dev": true
},
"@types/color-name": { "@types/color-name": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",

View file

@ -75,6 +75,7 @@
"@stryker-mutator/javascript-mutator": "^3.2.4", "@stryker-mutator/javascript-mutator": "^3.2.4",
"@stryker-mutator/jest-runner": "^3.2.4", "@stryker-mutator/jest-runner": "^3.2.4",
"@svgr/webpack": "^4.3.3", "@svgr/webpack": "^4.3.3",
"@types/classnames": "^2.2.10",
"@types/enzyme": "^3.10.5", "@types/enzyme": "^3.10.5",
"@types/jest": "^26.0.10", "@types/jest": "^26.0.10",
"@types/moment": "^2.13.0", "@types/moment": "^2.13.0",

View file

@ -1,7 +1,9 @@
import React, { FC, useEffect } from 'react'; import React, { FC, useEffect } from 'react';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { RouterProps } from 'react-router'; import { RouterProps } from 'react-router';
import classNames from 'classnames';
import NoMenuLayout from '../common/NoMenuLayout'; import NoMenuLayout from '../common/NoMenuLayout';
import { StateFlagTimeout } from '../utils/helpers/hooks';
import { ServerForm } from './helpers/ServerForm'; import { ServerForm } from './helpers/ServerForm';
import { ImportServersBtnProps } from './helpers/ImportServersBtn'; import { ImportServersBtnProps } from './helpers/ImportServersBtn';
import { NewServerData, RegularServer } from './data'; import { NewServerData, RegularServer } from './data';
@ -14,10 +16,26 @@ interface CreateServerProps extends RouterProps {
resetSelectedServer: Function; resetSelectedServer: Function;
} }
const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagTimeout: Function) => ( const Result: FC<{ type: 'success' | 'error' }> = ({ children, type }) => (
<div className="row">
<div className="col-md-10 offset-md-1">
<div
className={classNames('p-2 mt-3 text-white text-center', {
'bg-main': type === 'success',
'bg-danger': type === 'error',
})}
>
{children}
</div>
</div>
</div>
);
const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagTimeout: StateFlagTimeout) => (
{ createServer, history: { push }, resetSelectedServer }: CreateServerProps, { createServer, history: { push }, resetSelectedServer }: CreateServerProps,
) => { ) => {
const [ serversImported, setServersImported ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME); const [ serversImported, setServersImported ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
const [ errorImporting, setErrorImporting ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
const handleSubmit = (serverData: NewServerData) => { const handleSubmit = (serverData: NewServerData) => {
const id = uuid(); const id = uuid();
@ -32,19 +50,12 @@ const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagT
return ( return (
<NoMenuLayout> <NoMenuLayout>
<ServerForm onSubmit={handleSubmit}> <ServerForm onSubmit={handleSubmit}>
<ImportServersBtn onImport={setServersImported} /> <ImportServersBtn onImport={setServersImported} onImportError={setErrorImporting} />
<button className="btn btn-outline-primary">Create server</button> <button className="btn btn-outline-primary">Create server</button>
</ServerForm> </ServerForm>
{serversImported && ( {serversImported && <Result type="success">Servers properly imported. You can now select one from the list :)</Result>}
<div className="row create-server__import-success-msg"> {errorImporting && <Result type="error">The servers could not be imported. Make sure the format is correct.</Result>}
<div className="col-md-10 offset-md-1">
<div className="p-2 mt-3 bg-main text-white text-center">
Servers properly imported. You can now select one from the list :)
</div>
</div>
</div>
)}
</NoMenuLayout> </NoMenuLayout>
); );
}; };

View file

@ -7,16 +7,20 @@ type Ref<T> = RefObject<T> | MutableRefObject<T>;
export interface ImportServersBtnProps { export interface ImportServersBtnProps {
onImport?: () => void; onImport?: () => void;
onImportError?: () => void;
} }
interface ImportServersBtnConnectProps { interface ImportServersBtnConnectProps extends ImportServersBtnProps {
createServers: (servers: Server[]) => void; createServers: (servers: Server[]) => void;
fileRef: Ref<HTMLInputElement>; fileRef: Ref<HTMLInputElement>;
} }
const ImportServersBtn = ({ importServersFromFile }: ServersImporter) => ( const ImportServersBtn = ({ importServersFromFile }: ServersImporter) => ({
{ createServers, fileRef, onImport = () => {} }: ImportServersBtnConnectProps & ImportServersBtnProps, createServers,
) => { fileRef,
onImport = () => {},
onImportError = () => {},
}: ImportServersBtnConnectProps) => {
const ref = fileRef ?? useRef<HTMLInputElement>(); const ref = fileRef ?? useRef<HTMLInputElement>();
const onChange = async ({ target }: ChangeEvent<HTMLInputElement>) => const onChange = async ({ target }: ChangeEvent<HTMLInputElement>) =>
importServersFromFile(target.files?.[0]) importServersFromFile(target.files?.[0])
@ -25,7 +29,8 @@ const ImportServersBtn = ({ importServersFromFile }: ServersImporter) => (
.then(() => { .then(() => {
// Reset input after processing file // Reset input after processing file
(target as { value: string | null }).value = null; (target as { value: string | null }).value = null;
}); })
.catch(onImportError);
return ( return (
<React.Fragment> <React.Fragment>

View file

@ -1,26 +0,0 @@
import { useState, useRef } from 'react';
const DEFAULT_DELAY = 2000;
export const useStateFlagTimeout = (setTimeout, clearTimeout) => (initialValue = false, delay = DEFAULT_DELAY) => {
const [ flag, setFlag ] = useState(initialValue);
const timeout = useRef(undefined);
const callback = () => {
setFlag(!initialValue);
if (timeout.current) {
clearTimeout(timeout.current);
}
timeout.current = setTimeout(() => setFlag(initialValue), delay);
};
return [ flag, callback ];
};
// Return [ flag, toggle, enable, disable ]
export const useToggle = (initialValue = false) => {
const [ flag, setFlag ] = useState(initialValue);
return [ flag, () => setFlag(!flag), () => setFlag(true), () => setFlag(false) ];
};

View file

@ -0,0 +1,32 @@
import { useState, useRef } from 'react';
const DEFAULT_DELAY = 2000;
export type StateFlagTimeout = (initialValue?: boolean, delay?: number) => [ boolean, () => void ];
export const useStateFlagTimeout = (
setTimeout: (callback: Function, timeout: number) => number,
clearTimeout: (timer: number) => void,
): StateFlagTimeout => (initialValue = false, delay = DEFAULT_DELAY) => {
const [ flag, setFlag ] = useState<boolean>(initialValue);
const timeout = useRef<number | undefined>(undefined);
const callback = () => {
setFlag(!initialValue);
if (timeout.current) {
clearTimeout(timeout.current);
}
timeout.current = setTimeout(() => setFlag(initialValue), delay);
};
return [ flag, callback ];
};
type ToggleResult = [ boolean, (flag: boolean) => void, () => void, () => void ];
export const useToggle = (initialValue = false): ToggleResult => {
const [ flag, setFlag ] = useState<boolean>(initialValue);
return [ flag, () => setFlag(!flag), () => setFlag(true), () => setFlag(false) ];
};

View file

@ -12,8 +12,11 @@ describe('<CreateServer />', () => {
const createServerMock = jest.fn(); const createServerMock = jest.fn();
const push = jest.fn(); const push = jest.fn();
const historyMock = Mock.of<History>({ push }); const historyMock = Mock.of<History>({ push });
const createWrapper = (serversImported = false) => { const createWrapper = (serversImported = false, importFailed = false) => {
const CreateServer = createServerConstruct(ImportServersBtn, () => [ serversImported, () => '' ]); const useStateFlagTimeout = jest.fn()
.mockReturnValueOnce([ serversImported, () => '' ])
.mockReturnValueOnce([ importFailed, () => '' ]);
const CreateServer = createServerConstruct(ImportServersBtn, useStateFlagTimeout);
wrapper = shallow( wrapper = shallow(
<CreateServer createServer={createServerMock} resetSelectedServer={identity} history={historyMock} />, <CreateServer createServer={createServerMock} resetSelectedServer={identity} history={historyMock} />,
@ -31,13 +34,23 @@ describe('<CreateServer />', () => {
const wrapper = createWrapper(); const wrapper = createWrapper();
expect(wrapper.find(ServerForm)).toHaveLength(1); expect(wrapper.find(ServerForm)).toHaveLength(1);
expect(wrapper.find('.create-server__import-success-msg')).toHaveLength(0); expect(wrapper.find('Result')).toHaveLength(0);
}); });
it('shows success message when imported is true', () => { it('shows success message when imported is true', () => {
const wrapper = createWrapper(true); const wrapper = createWrapper(true);
const result = wrapper.find('Result');
expect(wrapper.find('.create-server__import-success-msg')).toHaveLength(1); expect(result).toHaveLength(1);
expect(result.prop('type')).toEqual('success');
});
it('shows error message when import failed', () => {
const wrapper = createWrapper(false, true);
const result = wrapper.find('Result');
expect(result).toHaveLength(1);
expect(result.prop('type')).toEqual('error');
}); });
it('creates server and redirects to it when form is submitted', () => { it('creates server and redirects to it when form is submitted', () => {