add table view mode for players management

This commit is contained in:
Pig Fang 2020-05-01 08:25:02 +08:00
parent 3df0f1c65a
commit aff278622c
6 changed files with 274 additions and 22 deletions

View File

@ -219,9 +219,9 @@
"node"
],
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/resources/assets/src/$1",
"\\.(s?css|styl)$": "<rootDir>/resources/assets/tests/__mocks__/style.ts",
"\\.(png|jpg)$": "<rootDir>/resources/assets/tests/__mocks__/file.ts"
"\\.(png|jpg)$": "<rootDir>/resources/assets/tests/__mocks__/file.ts",
"^@/(.*)$": "<rootDir>/resources/assets/src/$1"
},
"setupFilesAfterEnv": [
"<rootDir>/resources/assets/tests/setup.ts"

View File

@ -0,0 +1,13 @@
import { useState, useEffect } from 'react'
export default function useIsLargeScreen() {
const [isLarge, setIsLarge] = useState(false)
useEffect(() => {
if (window.innerWidth >= 992) {
setIsLarge(true)
}
}, [])
return isLarge
}

View File

@ -0,0 +1,19 @@
@use './breakpoints';
.header {
display: flex;
& > div {
margin-left: 4px;
& label {
cursor: pointer;
}
}
@include breakpoints.less-than('sm') {
flex-wrap: wrap;
& > div {
margin: 7px 0 0 0;
}
}
}

View File

@ -0,0 +1,81 @@
import React from 'react'
import { t } from '@/scripts/i18n'
import { Player } from '@/scripts/types'
import ButtonEdit from '@/components/ButtonEdit'
interface Props {
player: Player
onUpdateName(): void
onUpdateOwner(): void
onUpdateTexture(): void
onDelete(): void
}
const Row: React.FC<Props> = (props) => {
const { player } = props
return (
<tr>
<td>{player.pid}</td>
<td>
{player.name}
<span className="ml-1">
<ButtonEdit
title={t('admin.changePlayerName')}
onClick={props.onUpdateName}
/>
</span>
</td>
<td>
{player.uid}
<span className="ml-1">
<ButtonEdit
title={t('admin.changeOwner')}
onClick={props.onUpdateOwner}
/>
</span>
</td>
<td>
{player.tid_skin > 0 && (
<a
href={`${blessing.base_url}/skinlib/show/${player.tid_skin}`}
target="_blank"
className="mr-1"
>
<img
src={`${blessing.base_url}/preview/${player.tid_skin}`}
alt={`${player.name} - ${t('general.skin')}`}
width="64"
/>
</a>
)}
{player.tid_cape > 0 && (
<a
href={`${blessing.base_url}/skinlib/show/${player.tid_cape}`}
target="_blank"
>
<img
src={`${blessing.base_url}/preview/${player.tid_cape}`}
alt={`${player.name} - ${t('general.cape')}`}
width="64"
/>
</a>
)}
</td>
<td>{player.last_modified}</td>
<td className="d-flex flex-wrap">
<button
className="btn btn-default mr-2"
onClick={props.onUpdateTexture}
>
{t('admin.changeTexture')}
</button>
<button className="btn btn-danger" onClick={props.onDelete}>
{t('admin.deletePlayer')}
</button>
</td>
</tr>
)
}
export default Row

View File

@ -1,13 +1,16 @@
import React, { useState, useEffect } from 'react'
import { hot } from 'react-hot-loader/root'
import { useImmer } from 'use-immer'
import useIsLargeScreen from '@/scripts/hooks/useIsLargeScreen'
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import { Player, Paginator } from '@/scripts/types'
import { toast, showModal } from '@/scripts/notify'
import modeSwitchStyles from '@/styles/table-mode-switch.module.scss'
import Loading from '@/components/Loading'
import Pagination from '@/components/Pagination'
import Card from './Card'
import Row from './Row'
import ModalUpdateTexture from './ModalUpdateTexture'
const PlayersManagement: React.FC = () => {
@ -15,9 +18,17 @@ const PlayersManagement: React.FC = () => {
const [page, setPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const [isLoading, setIsLoading] = useState(false)
const isLargeScreen = useIsLargeScreen()
const [isTableMode, setIsTableMode] = useState(false)
const [query, setQuery] = useState('')
const [textureUpdating, setTextureUpdating] = useState(-1)
useEffect(() => {
if (isLargeScreen) {
setIsTableMode(true)
}
}, [isLargeScreen])
const getPlayers = async () => {
setIsLoading(true)
const { data, last_page }: Paginator<Player> = await fetch.get(
@ -36,6 +47,10 @@ const PlayersManagement: React.FC = () => {
getPlayers()
}, [page])
const handleModeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsTableMode(event.target.value === 'table')
}
const handleQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value)
}
@ -147,7 +162,7 @@ const PlayersManagement: React.FC = () => {
return (
<div className="card">
<div className="card-header">
<div className={`card-header ${modeSwitchStyles.header}`}>
<form className="input-group" onSubmit={handleSubmitQuery}>
<input
type="text"
@ -162,27 +177,80 @@ const PlayersManagement: React.FC = () => {
</button>
</div>
</form>
<div className="btn-group btn-group-toggle">
<label
className={`btn btn-secondary ${isTableMode ? 'active' : ''}`}
title="Table Mode"
>
<input
type="radio"
value="table"
checked={isTableMode}
onChange={handleModeChange}
/>
<i className="fas fa-list"></i>
</label>
<label
className={`btn btn-secondary ${isTableMode ? '' : 'active'}`}
title="Card Mode"
>
<input
type="radio"
value="card"
checked={!isTableMode}
onChange={handleModeChange}
/>
<i className="fas fa-grip-vertical"></i>
</label>
</div>
</div>
<div className="card-body">
{isLoading ? (
{isLoading ? (
<div className="card-body">
<Loading />
) : players.length === 0 ? (
<div className="text-center">No players.</div>
) : (
<div className="d-flex flex-wrap">
{players.map((player, i) => (
<Card
key={player.pid}
player={player}
onUpdateName={() => handleUpdateName(player, i)}
onUpdateOwner={() => handleUpdateOwner(player, i)}
onUpdateTexture={() => setTextureUpdating(i)}
onDelete={() => handleDelete(player)}
/>
))}
</div>
)}
</div>
</div>
) : players.length === 0 ? (
<div className="card-body text-center">No players.</div>
) : isTableMode ? (
<div className="card-body table-responsive p-0">
<table className="table table-striped">
<thead>
<tr>
<th>PID</th>
<th>{t('general.player.player-name')}</th>
<th>{t('general.player.owner')}</th>
<th>{t('general.player.previews')}</th>
<th>{t('general.player.last-modified')}</th>
<th>{t('admin.operationsTitle')}</th>
</tr>
</thead>
<tbody>
{players.map((player, i) => (
<Row
key={player.pid}
player={player}
onUpdateName={() => handleUpdateName(player, i)}
onUpdateOwner={() => handleUpdateOwner(player, i)}
onUpdateTexture={() => setTextureUpdating(i)}
onDelete={() => handleDelete(player)}
/>
))}
</tbody>
</table>
</div>
) : (
<div className="card-body d-flex flex-wrap">
{players.map((player, i) => (
<Card
key={player.pid}
player={player}
onUpdateName={() => handleUpdateName(player, i)}
onUpdateOwner={() => handleUpdateOwner(player, i)}
onUpdateTexture={() => setTextureUpdating(i)}
onDelete={() => handleDelete(player)}
/>
))}
</div>
)}
<div className="card-footer">
<div className="float-right">
<Pagination page={page} totalPages={totalPages} onChange={setPage} />

View File

@ -27,6 +27,14 @@ function createPaginator(data: Player[]): Paginator<Player> {
}
}
beforeAll(() => {
Object.assign(window, { innerWidth: 500 })
})
afterAll(() => {
Object.assign(window, { innerWidth: 1024 })
})
test('search players', async () => {
fetch.get.mockResolvedValue(createPaginator([]))
@ -360,3 +368,66 @@ describe('delete player', () => {
expect(queryByText(fixture.name)).toBeInTheDocument()
})
})
describe('table mode', () => {
beforeEach(() => {
fetch.get.mockResolvedValue(createPaginator([fixture]))
})
it('large screen', async () => {
Object.assign(window, { innerWidth: 1024 })
const { queryByText } = render(<PlayersManagement />)
await waitFor(() => expect(fetch.get).toBeCalled())
expect(queryByText(t('admin.operationsTitle'))).toBeInTheDocument()
Object.assign(window, { innerWidth: 500 })
})
it('update player name', async () => {
const { getByText, getByTitle, queryByText } = render(<PlayersManagement />)
await waitFor(() => expect(fetch.get).toBeCalled())
fireEvent.click(getByTitle('Table Mode'))
fireEvent.click(getByTitle(t('admin.changePlayerName')))
expect(queryByText(t('admin.changePlayerNameNotice'))).toBeInTheDocument()
fireEvent.click(getByText(t('general.cancel')))
})
it('update owner', async () => {
const { getByText, getByTitle, queryByText } = render(<PlayersManagement />)
await waitFor(() => expect(fetch.get).toBeCalled())
fireEvent.click(getByTitle('Table Mode'))
fireEvent.click(getByTitle(t('admin.changeOwner')))
expect(queryByText(t('admin.changePlayerOwner'))).toBeInTheDocument()
fireEvent.click(getByText(t('general.cancel')))
})
it('update texture', async () => {
const { getByText, getByTitle, queryByPlaceholderText } = render(
<PlayersManagement />,
)
await waitFor(() => expect(fetch.get).toBeCalled())
fireEvent.click(getByTitle('Table Mode'))
fireEvent.click(getByText(t('admin.changeTexture')))
expect(queryByPlaceholderText(t('admin.pidNotice'))).toBeInTheDocument()
fireEvent.click(getByText(t('general.cancel')))
})
it('delete player', async () => {
const { getByText, getByTitle, queryByText } = render(<PlayersManagement />)
await waitFor(() => expect(fetch.get).toBeCalled())
fireEvent.click(getByTitle('Table Mode'))
fireEvent.click(getByText(t('admin.deletePlayer')))
expect(queryByText(t('admin.deletePlayerNotice'))).toBeInTheDocument()
fireEvent.click(getByText(t('general.cancel')))
})
})