add table view mode for players management
This commit is contained in:
parent
3df0f1c65a
commit
aff278622c
|
|
@ -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"
|
||||
|
|
|
|||
13
resources/assets/src/scripts/hooks/useIsLargeScreen.ts
Normal file
13
resources/assets/src/scripts/hooks/useIsLargeScreen.ts
Normal 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
|
||||
}
|
||||
19
resources/assets/src/styles/table-mode-switch.module.scss
Normal file
19
resources/assets/src/styles/table-mode-switch.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
81
resources/assets/src/views/admin/PlayersManagement/Row.tsx
Normal file
81
resources/assets/src/views/admin/PlayersManagement/Row.tsx
Normal 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
|
||||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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')))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user