Skip to content
This repository has been archived by the owner on Mar 23, 2022. It is now read-only.

Commit

Permalink
ADD - UI to manage name references (#188)
Browse files Browse the repository at this point in the history
Signed-off-by: RaenonX <[email protected]>
  • Loading branch information
RaenonX committed Aug 5, 2021
1 parent 5bef5c3 commit 7a34fec
Show file tree
Hide file tree
Showing 14 changed files with 607 additions and 1 deletion.
4 changes: 4 additions & 0 deletions pages/[lang]/name.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import {UnitNameRefPage} from '../../src/components/pages/gameData/nameRef/main';


export default UnitNameRefPage;
2 changes: 1 addition & 1 deletion src/api-def
81 changes: 81 additions & 0 deletions src/components/pages/gameData/nameRef/entry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from 'react';

import Col from 'react-bootstrap/Col';
import Form from 'react-bootstrap/Form';
import Row from 'react-bootstrap/Row';

import {UnitNameRefEntry as UnitNameRefEntryApi} from '../../../../api-def/api';
import {useI18n} from '../../../../i18n/hook';
import {useUnitInfo} from '../../../../utils/services/resources/unitInfo/hooks';
import {UnitIcon} from '../../../elements/gameData/unitIcon';
import {ArrayDataFormOnChangedHandler} from '../../../elements/posts/shared/form/array';


type UnitNameRefEntryProps = {
entry: UnitNameRefEntryApi,
onChanged: ArrayDataFormOnChangedHandler<UnitNameRefEntryApi>,
}

export const UnitNameRefEntry = ({entry, onChanged}: UnitNameRefEntryProps) => {
const {t, lang} = useI18n();
const {unitInfoMap} = useUnitInfo();

const unitInfo = unitInfoMap.get(entry.unitId);

// This is the height of `form-control`
const unitIconHeight = 'calc(1.5em + 0.75rem + 2px)';

const isValid = !!unitInfo;

return (
<div className="bg-black-32 rounded p-2">
<Form.Row>
<Col lg={2}>
<Form.Label>{t((t) => t.game.nameRef.unitId)}</Form.Label>
<Form.Control
isValid={isValid} isInvalid={!isValid}
onChange={(e) => {
if (Number(e.target.value) || !e.target.value) {
onChanged('unitId')(+e.target.value);
}
}}
value={entry.unitId}
/>
</Col>
<Col lg={5}>
<Form.Label>{t((t) => t.game.nameRef.actualName)}</Form.Label>
<Row noGutters>
{
unitInfo ?
<>
<Col xs="auto">
<UnitIcon unitInfo={unitInfo} className="ml-1" style={{height: unitIconHeight}}/>
</Col>
<Col className="d-flex align-items-center justify-content-center">
<span className="h5 mb-0">{unitInfo.name[lang]}</span>
</Col>
</> :
<Col
className="d-flex align-items-center justify-content-center text-danger"
style={{height: unitIconHeight}}
>
<span className="h5 mb-0">
{t((t) => t.game.nameRef.error.invalidUnitId)}
</span>
</Col>
}
</Row>
</Col>
<Col lg={5}>
<Form.Label>{t((t) => t.game.nameRef.desiredName)}</Form.Label>
<Form.Control
onChange={(e) => onChanged('name')(e.target.value)}
isInvalid={!entry.name}
disabled={!isValid}
value={entry.name}
/>
</Col>
</Form.Row>
</div>
);
};
239 changes: 239 additions & 0 deletions src/components/pages/gameData/nameRef/main.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import React from 'react';

import {screen, waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import {renderReact} from '../../../../../test/render/main';
import {typeInput} from '../../../../../test/utils/event';
import {ApiResponseCode, SupportedLanguages, UnitType} from '../../../../api-def/api';
import {Element} from '../../../../api-def/resources/types/element';
import {Weapon} from '../../../../api-def/resources/types/weapon';
import {translation as translationEN} from '../../../../i18n/translations/en/translation';
import {ApiRequestSender} from '../../../../utils/services/api/requestSender';
import {ResourceLoader} from '../../../../utils/services/resources/loader';
import {UnitNameRefPage} from './main';


describe('Name reference management', () => {
let fnGetRefs: jest.SpyInstance;
let fnSetRefs: jest.SpyInstance;
let fnGetUnitInfo: jest.SpyInstance;

beforeEach(() => {
fnGetRefs = jest.spyOn(ApiRequestSender, 'getUnitNameRefManage').mockResolvedValue({
code: ApiResponseCode.SUCCESS,
success: true,
refs: [
{
unitId: 10950101,
name: 'G!Leon',
},
{
unitId: 10950201,
name: 'Furis',
},
],
});
fnSetRefs = jest.spyOn(ApiRequestSender, 'updateUnitNameRefs').mockResolvedValue({
code: ApiResponseCode.SUCCESS,
success: true,
});
fnGetUnitInfo = jest.spyOn(ResourceLoader, 'getCharacterInfo').mockResolvedValue([
{
id: 10950101,
element: Element.FLAME,
cvEn: {
[SupportedLanguages.CHT]: 'CV EN CHT 1',
[SupportedLanguages.EN]: 'CV EN EN 1',
[SupportedLanguages.JP]: 'CV EN JP 1',
},
cvJp: {
[SupportedLanguages.CHT]: 'CV JP CHT 1',
[SupportedLanguages.EN]: 'CV JP EN 1',
[SupportedLanguages.JP]: 'CV JP JP 1',
},
hasUniqueDragon: false,
iconName: 'icon 1',
name: {
[SupportedLanguages.CHT]: 'CHARA CHT 1',
[SupportedLanguages.EN]: 'CHARA EN 1',
[SupportedLanguages.JP]: 'CHARA JP 1',
},
rarity: 5,
releaseEpoch: 0,
weapon: Weapon.SWORD,
type: UnitType.CHARACTER,
},
{
id: 10950201,
element: Element.FLAME,
cvEn: {
[SupportedLanguages.CHT]: 'CV EN CHT 2',
[SupportedLanguages.EN]: 'CV EN EN 2',
[SupportedLanguages.JP]: 'CV EN JP 2',
},
cvJp: {
[SupportedLanguages.CHT]: 'CV JP CHT 2',
[SupportedLanguages.EN]: 'CV JP EN 2',
[SupportedLanguages.JP]: 'CV JP JP 2',
},
hasUniqueDragon: false,
iconName: 'icon 2',
name: {
[SupportedLanguages.CHT]: 'CHARA CHT 2',
[SupportedLanguages.EN]: 'CHARA EN 2',
[SupportedLanguages.JP]: 'CHARA JP 2',
},
rarity: 5,
releaseEpoch: 0,
weapon: Weapon.SWORD,
type: UnitType.CHARACTER,
},
{
id: 10850101,
element: Element.FLAME,
cvEn: {
[SupportedLanguages.CHT]: 'CV EN CHT 3',
[SupportedLanguages.EN]: 'CV EN EN 3',
[SupportedLanguages.JP]: 'CV EN JP 3',
},
cvJp: {
[SupportedLanguages.CHT]: 'CV JP CHT 3',
[SupportedLanguages.EN]: 'CV JP EN 3',
[SupportedLanguages.JP]: 'CV JP JP 3',
},
hasUniqueDragon: false,
iconName: 'icon 3',
name: {
[SupportedLanguages.CHT]: 'CHARA CHT 3',
[SupportedLanguages.EN]: 'CHARA EN 3',
[SupportedLanguages.JP]: 'CHARA JP 3',
},
rarity: 5,
releaseEpoch: 0,
weapon: Weapon.SWORD,
type: UnitType.CHARACTER,
},
]);
});

it('gets all references on load', async () => {
renderReact(() => <UnitNameRefPage/>, {hasSession: true, user: {isAdmin: true}});

await waitFor(() => expect(fnGetRefs).toHaveBeenCalled());
});

it('disables update if any of the unit name is empty', async () => {
renderReact(() => <UnitNameRefPage/>, {hasSession: true, user: {isAdmin: true}});

const desiredNameInput = await screen.findByDisplayValue('G!Leon');
userEvent.clear(desiredNameInput);

expect(desiredNameInput).toHaveClass('is-invalid');

const updateButton = screen.getByText(translationEN.misc.update);
expect(updateButton).toBeDisabled();
});

it('disables update if any of the unit ID is invalid', async () => {
const {rerender} = renderReact(() => <UnitNameRefPage/>, {hasSession: true, user: {isAdmin: true}});

const unitIdInput = await screen.findByDisplayValue('10950101');
typeInput(unitIdInput, '1', {rerender});

expect(unitIdInput).toHaveClass('is-invalid');

const updateButton = screen.getByText(translationEN.misc.update);
expect(updateButton).toBeDisabled();
});

it('allows update if no invalid input', async () => {
const {rerender} = renderReact(() => <UnitNameRefPage/>, {hasSession: true, user: {isAdmin: true}});
await waitFor(() => expect(fnGetUnitInfo).toHaveBeenCalled());

const unitIdInput = await screen.findByDisplayValue('10950101');
userEvent.clear(unitIdInput);
typeInput(unitIdInput, '10850101', {rerender});

await waitFor(() => expect(unitIdInput).toHaveClass('is-valid'));

const updateButton = screen.getByText(translationEN.misc.update);
await waitFor(() => expect(updateButton).toBeEnabled());
});

it('updates correctly', async () => {
const {rerender} = renderReact(() => <UnitNameRefPage/>, {hasSession: true, user: {isAdmin: true}});

const unitIdInput = await screen.findByDisplayValue('10950101');
userEvent.clear(unitIdInput);
typeInput(unitIdInput, '10850101', {rerender});

const updateButton = screen.getByText(translationEN.misc.update);
userEvent.click(updateButton);

// Correct data sent?
await waitFor(() => expect(fnSetRefs).toHaveBeenCalled());
expect(fnSetRefs.mock.calls[0][2]).toStrictEqual([
{
unitId: 10850101,
name: 'G!Leon',
},
{
unitId: 10950201,
name: 'Furis',
},
]);

// Updated marker shown?
expect(screen.getByText('', {selector: 'i.bi-cloud-check'})).toBeInTheDocument();

// Blocks re-update?
expect(updateButton).toBeDisabled();
});

it('shows warning and does not block update on submission failed', async () => {
fnSetRefs.mockResolvedValueOnce({
code: ApiResponseCode.FAILED_INTERNAL_ERROR,
success: false,
});

const {rerender} = renderReact(() => <UnitNameRefPage/>, {hasSession: true, user: {isAdmin: true}});

// Trigger update
const unitIdInput = await screen.findByDisplayValue('10950101');
userEvent.clear(unitIdInput);
typeInput(unitIdInput, '10850101', {rerender});

const updateButton = screen.getByText(translationEN.misc.update);
userEvent.click(updateButton);

expect(await screen.findByText('', {selector: 'i.bi-exclamation-circle'})).toBeInTheDocument();
expect(screen.getByText(new RegExp(ApiResponseCode[ApiResponseCode.FAILED_INTERNAL_ERROR]))).toBeInTheDocument();
});

it('disables update button on load', async () => {
renderReact(() => <UnitNameRefPage/>, {hasSession: true, user: {isAdmin: true}});

const updateButton = await screen.findByText(translationEN.misc.update);
expect(updateButton).toBeDisabled();
});

it('enables update button after any change', async () => {
const {rerender} = renderReact(() => <UnitNameRefPage/>, {hasSession: true, user: {isAdmin: true}});

const unitIdInput = await screen.findByDisplayValue('10950101');
userEvent.clear(unitIdInput);
typeInput(unitIdInput, '10850101', {rerender});

const updateButton = screen.getByText(translationEN.misc.update);
userEvent.click(updateButton);

await waitFor(() => expect(fnSetRefs).toHaveBeenCalled());
expect(screen.getByText('', {selector: 'i.bi-cloud-check'})).toBeInTheDocument();
expect(updateButton).toBeDisabled();

userEvent.clear(unitIdInput);
typeInput(unitIdInput, '10950101', {rerender});
expect(updateButton).toBeEnabled();
});
});
47 changes: 47 additions & 0 deletions src/components/pages/gameData/nameRef/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';

import {FailedResponse, isFailedResponse, UnitNameRefManageResponse} from '../../../../api-def/api';
import {AppReactContext} from '../../../../context/app/main';
import {useI18n} from '../../../../i18n/hook';
import {ApiRequestSender} from '../../../../utils/services/api/requestSender';
import {useFetchState} from '../../../elements/common/fetch';
import {Loading} from '../../../elements/common/loading';
import {AccessDenied} from '../../layout/accessDenied';
import {ProtectedLayout} from '../../layout/protected';
import {UnitNameRefManagement} from './manage';


export const UnitNameRefPage = () => {
const {lang} = useI18n();
const context = React.useContext(AppReactContext);

if (!context?.session?.user.isAdmin) {
return <AccessDenied/>;
}

const uid = context.session.user.id.toString();

const {
fetchStatus: unitRefs,
fetchFunction: fetchUnitRefs,
} = useFetchState<UnitNameRefManageResponse | FailedResponse | undefined>(
undefined,
() => ApiRequestSender.getUnitNameRefManage(uid, lang),
'Failed to fetch unit name references.',
);

fetchUnitRefs();

if (!unitRefs.fetched || !unitRefs.data || isFailedResponse(unitRefs.data)) {
return <Loading/>;
}

return (
<ProtectedLayout>
<UnitNameRefManagement
refs={unitRefs.data.refs}
uid={uid}
/>
</ProtectedLayout>
);
};
Loading

0 comments on commit 7a34fec

Please sign in to comment.