This repository has been archived by the owner on Mar 23, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ADD - UI to manage name references (#188)
Signed-off-by: RaenonX <[email protected]>
- Loading branch information
Showing
14 changed files
with
607 additions
and
1 deletion.
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 |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import {UnitNameRefPage} from '../../src/components/pages/gameData/nameRef/main'; | ||
|
||
|
||
export default UnitNameRefPage; |
Submodule api-def
updated
2 files
+5 −0 | api/data/unitNameRef/payload.ts | |
+2 −0 | api/data/unitNameRef/response.ts |
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,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> | ||
); | ||
}; |
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,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(); | ||
}); | ||
}); |
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,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> | ||
); | ||
}; |
Oops, something went wrong.