rewrite players management page with React
This commit is contained in:
parent
c6ef42d477
commit
af351d211b
|
|
@ -6,7 +6,6 @@ use App\Models\Player;
|
|||
use App\Models\Texture;
|
||||
use App\Models\User;
|
||||
use App\Notifications;
|
||||
use App\Rules;
|
||||
use App\Services\OptionForm;
|
||||
use App\Services\PluginManager;
|
||||
use Auth;
|
||||
|
|
@ -599,80 +598,4 @@ class AdminController extends Controller
|
|||
return json(trans('admin.users.operations.invalid'), 1);
|
||||
}
|
||||
}
|
||||
|
||||
public function playerAjaxHandler(Request $request)
|
||||
{
|
||||
$action = $request->input('action');
|
||||
$currentUser = Auth::user();
|
||||
$player = Player::find($request->input('pid'));
|
||||
|
||||
if (!$player) {
|
||||
return json(trans('general.unexistent-player'), 1);
|
||||
}
|
||||
|
||||
$owner = $player->user;
|
||||
if (
|
||||
$owner && $owner->uid !== $currentUser->uid &&
|
||||
$owner->permission >= $currentUser->permission
|
||||
) {
|
||||
return json(trans('admin.players.no-permission'), 1);
|
||||
}
|
||||
|
||||
if ($action == 'texture') {
|
||||
$this->validate($request, [
|
||||
'type' => 'required',
|
||||
'tid' => 'required|integer',
|
||||
]);
|
||||
|
||||
if (!Texture::find($request->tid) && $request->tid != 0) {
|
||||
return json(trans('admin.players.textures.non-existent', ['tid' => $request->tid]), 1);
|
||||
}
|
||||
|
||||
$field = 'tid_'.$request->type;
|
||||
$player->$field = $request->tid;
|
||||
$player->save();
|
||||
|
||||
return json(trans('admin.players.textures.success', ['player' => $player->name]), 0);
|
||||
} elseif ($action == 'owner') {
|
||||
$this->validate($request, [
|
||||
'uid' => 'required|integer',
|
||||
]);
|
||||
|
||||
$user = User::find($request->uid);
|
||||
|
||||
if (!$user) {
|
||||
return json(trans('admin.users.operations.non-existent'), 1);
|
||||
}
|
||||
|
||||
$player->uid = $request->input('uid');
|
||||
$player->save();
|
||||
|
||||
return json(trans('admin.players.owner.success', ['player' => $player->name, 'user' => $user->nickname]), 0);
|
||||
} elseif ($action == 'delete') {
|
||||
$player->delete();
|
||||
|
||||
return json(trans('admin.players.delete.success'), 0);
|
||||
} elseif ($action == 'name') {
|
||||
$name = $this->validate($request, [
|
||||
'name' => [
|
||||
'required',
|
||||
new Rules\PlayerName(),
|
||||
'min:'.option('player_name_length_min'),
|
||||
'max:'.option('player_name_length_max'),
|
||||
],
|
||||
])['name'];
|
||||
|
||||
$player->name = $name;
|
||||
$player->save();
|
||||
|
||||
if (option('single_player', false) && $owner) {
|
||||
$owner->nickname = $name;
|
||||
$owner->save();
|
||||
}
|
||||
|
||||
return json(trans('admin.players.name.success', ['player' => $player->name]), 0);
|
||||
} else {
|
||||
return json(trans('admin.users.operations.invalid'), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
136
app/Http/Controllers/PlayersManagementController.php
Normal file
136
app/Http/Controllers/PlayersManagementController.php
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Player;
|
||||
use App\Models\Texture;
|
||||
use App\Models\User;
|
||||
use App\Rules;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class PlayersManagementController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware(function (Request $request, $next) {
|
||||
/** @var Player */
|
||||
$player = $request->route('player');
|
||||
$owner = $player->user;
|
||||
|
||||
/** @var User */
|
||||
$currentUser = $request->user();
|
||||
|
||||
if (
|
||||
$owner && $owner->uid !== $currentUser->uid &&
|
||||
$owner->permission >= $currentUser->permission
|
||||
) {
|
||||
return json(trans('admin.players.no-permission'), 1)
|
||||
->setStatusCode(403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
})->except(['list']);
|
||||
}
|
||||
|
||||
public function list(Request $request)
|
||||
{
|
||||
$query = $request->query('q');
|
||||
|
||||
return Player::usingSearchString($query)->paginate(10);
|
||||
}
|
||||
|
||||
public function name(
|
||||
Player $player,
|
||||
Request $request,
|
||||
Dispatcher $dispatcher
|
||||
) {
|
||||
$name = $this->validate($request, [
|
||||
'player_name' => [
|
||||
'required',
|
||||
new Rules\PlayerName(),
|
||||
'min:'.option('player_name_length_min'),
|
||||
'max:'.option('player_name_length_max'),
|
||||
'unique:players,name',
|
||||
],
|
||||
])['player_name'];
|
||||
|
||||
$dispatcher->dispatch('player.name.updating', [$player, $name]);
|
||||
|
||||
$oldName = $player->name;
|
||||
$player->name = $name;
|
||||
$player->save();
|
||||
|
||||
$dispatcher->dispatch('player.name.updated', [$player, $oldName]);
|
||||
|
||||
return json(trans('admin.players.name.success', ['player' => $player->name]), 0);
|
||||
}
|
||||
|
||||
public function owner(
|
||||
Player $player,
|
||||
Request $request,
|
||||
Dispatcher $dispatcher
|
||||
) {
|
||||
$uid = $this->validate($request, ['uid' => 'required|integer'])['uid'];
|
||||
|
||||
$dispatcher->dispatch('player.owner.updating', [$player, $uid]);
|
||||
|
||||
/** @var User */
|
||||
$user = User::find($request->uid);
|
||||
if (empty($user)) {
|
||||
return json(trans('admin.users.operations.non-existent'), 1);
|
||||
}
|
||||
|
||||
$player->uid = $uid;
|
||||
$player->save();
|
||||
|
||||
$dispatcher->dispatch('player.owner.updated', [$player, $user]);
|
||||
|
||||
return json(trans('admin.players.owner.success', [
|
||||
'player' => $player->name,
|
||||
'user' => $user->nickname,
|
||||
]), 0);
|
||||
}
|
||||
|
||||
public function texture(
|
||||
Player $player,
|
||||
Request $request,
|
||||
Dispatcher $dispatcher
|
||||
) {
|
||||
$data = $this->validate($request, [
|
||||
'tid' => 'required|integer',
|
||||
'type' => ['required', Rule::in(['skin', 'cape'])],
|
||||
]);
|
||||
$tid = (int) $data['tid'];
|
||||
$type = $data['type'];
|
||||
|
||||
$dispatcher->dispatch('player.texture.updating', [$player, $type, $tid]);
|
||||
|
||||
if (!Texture::find($tid) && $tid !== 0) {
|
||||
return json(trans('admin.players.textures.non-existent', ['tid' => $tid]), 1);
|
||||
}
|
||||
|
||||
$field = 'tid_'.$type;
|
||||
$previousTid = $player->$field;
|
||||
$player->$field = $tid;
|
||||
$player->save();
|
||||
|
||||
$dispatcher->dispatch('player.texture.updated', [$player, $type, $previousTid]);
|
||||
|
||||
return json(trans('admin.players.textures.success', ['player' => $player->name]), 0);
|
||||
}
|
||||
|
||||
public function delete(
|
||||
Player $player,
|
||||
Dispatcher $dispatcher
|
||||
) {
|
||||
$dispatcher->dispatch('player.deleting', [$player]);
|
||||
|
||||
$player->delete();
|
||||
|
||||
$dispatcher->dispatch('player.deleted', [$player]);
|
||||
|
||||
return json(trans('admin.players.delete.success'), 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ class Kernel extends HttpKernel
|
|||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\App\Http\Middleware\EnforceEverGreen::class,
|
||||
\App\Http\Middleware\RedirectToSetup::class,
|
||||
],
|
||||
|
|
|
|||
21
app/Listeners/SinglePlayer/UpdateOwnerNickName.php
Normal file
21
app/Listeners/SinglePlayer/UpdateOwnerNickName.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Listeners\SinglePlayer;
|
||||
|
||||
use App\Models\Player;
|
||||
|
||||
class UpdateOwnerNickName
|
||||
{
|
||||
/**
|
||||
* @param Player $player
|
||||
*/
|
||||
public function handle($player)
|
||||
{
|
||||
$owner = $player->user;
|
||||
|
||||
if (option('single_player', false) && $owner) {
|
||||
$owner->nickname = $player->name;
|
||||
$owner->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ use App\Models;
|
|||
use DateTimeInterface;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Lorisleiva\LaravelSearchString\Concerns\SearchString;
|
||||
|
||||
/**
|
||||
* @property int $pid
|
||||
|
|
@ -22,6 +23,8 @@ use Illuminate\Support\Carbon;
|
|||
*/
|
||||
class Player extends Model
|
||||
{
|
||||
use SearchString;
|
||||
|
||||
public const CREATED_AT = null;
|
||||
public const UPDATED_AT = 'last_modified';
|
||||
|
||||
|
|
@ -40,6 +43,14 @@ class Player extends Model
|
|||
'updated' => PlayerProfileUpdated::class,
|
||||
];
|
||||
|
||||
protected $searchStringColumns = [
|
||||
'pid', 'uid',
|
||||
'tid_skin' => '/^(?:tid_)?skin$/',
|
||||
'tid_cape' => '/^(?:tid_)?cape$/',
|
||||
'name' => ['searchable' => true],
|
||||
'last_modified' => ['date' => true],
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(Models\User::class, 'uid');
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Listeners;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
|
|
@ -13,24 +14,27 @@ class EventServiceProvider extends ServiceProvider
|
|||
*/
|
||||
protected $listen = [
|
||||
'App\Events\PlayerRetrieved' => [
|
||||
'App\Listeners\ResetInvalidTextureForPlayer',
|
||||
Listeners\ResetInvalidTextureForPlayer::class,
|
||||
],
|
||||
'App\Events\TextureDeleting' => [
|
||||
'App\Listeners\TextureRemoved',
|
||||
],
|
||||
'App\Events\PluginWasEnabled' => [
|
||||
'App\Listeners\CopyPluginAssets',
|
||||
'App\Listeners\GeneratePluginTranslations',
|
||||
Listeners\CopyPluginAssets::class,
|
||||
Listeners\GeneratePluginTranslations::class,
|
||||
],
|
||||
'plugin.versionChanged' => [
|
||||
'App\Listeners\CopyPluginAssets',
|
||||
'App\Listeners\GeneratePluginTranslations',
|
||||
],
|
||||
'App\Events\PluginBootFailed' => [
|
||||
'App\Listeners\NotifyFailedPlugin',
|
||||
Listeners\NotifyFailedPlugin::class,
|
||||
],
|
||||
'App\Events\RenderingHeader' => [
|
||||
'App\Listeners\SerializeGlobals',
|
||||
Listeners\SerializeGlobals::class,
|
||||
],
|
||||
'player.name.updated' => [
|
||||
Listeners\SinglePlayer\UpdateOwnerNickName::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"intervention/image": "^2.5",
|
||||
"laravel/framework": "^7.0",
|
||||
"laravel/passport": "^8.4",
|
||||
"lorisleiva/laravel-search-string": "^0.1.6",
|
||||
"nesbot/carbon": "^2.0",
|
||||
"nunomaduro/collision": "^4.1",
|
||||
"rcrowe/twigbridge": "^0.11.3",
|
||||
|
|
|
|||
48
composer.lock
generated
48
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "e9621ab58940ab0702842b6ba6c80ccb",
|
||||
"content-hash": "d1615cec2f58f3b4eb54ac477e348b42",
|
||||
"packages": [
|
||||
{
|
||||
"name": "blessing/filter",
|
||||
|
|
@ -2330,6 +2330,52 @@
|
|||
],
|
||||
"time": "2019-07-13T18:58:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "lorisleiva/laravel-search-string",
|
||||
"version": "v0.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lorisleiva/laravel-search-string.git",
|
||||
"reference": "68189cad7614d0c9cb09a83315e1ccfe49e7f0c1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/lorisleiva/laravel-search-string/zipball/68189cad7614d0c9cb09a83315e1ccfe49e7f0c1",
|
||||
"reference": "68189cad7614d0c9cb09a83315e1ccfe49e7f0c1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/support": "^5.5|^6.0|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"orchestra/testbench": "^5.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Lorisleiva\\LaravelSearchString\\ServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Lorisleiva\\LaravelSearchString\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Loris Leiva",
|
||||
"email": "loris.leiva@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Generates database queries based on one unique string using a simple and customizable syntax.",
|
||||
"time": "2020-04-05T15:40:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
"version": "2.0.2",
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export default [
|
|||
},
|
||||
{
|
||||
path: 'admin/players',
|
||||
component: () => import('../views/admin/Players.vue'),
|
||||
react: () => import('../views/admin/PlayersManagement'),
|
||||
el: '.content > .container-fluid',
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ export type User = {
|
|||
export type Player = {
|
||||
pid: number
|
||||
name: string
|
||||
uid: number
|
||||
tid_skin: number
|
||||
tid_cape: number
|
||||
last_modified: string
|
||||
}
|
||||
|
||||
export type Texture = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
@import './avatar';
|
||||
@import './dropdown';
|
||||
|
||||
a {
|
||||
outline: none;
|
||||
|
|
|
|||
15
resources/assets/src/styles/dropdown.scss
Normal file
15
resources/assets/src/styles/dropdown.scss
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
.dropdown-item {
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: var(--blue);
|
||||
}
|
||||
|
||||
&.dropdown-item-danger {
|
||||
color: var(--danger);
|
||||
|
||||
&:hover, &:active {
|
||||
color: #fff;
|
||||
background-color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
<template>
|
||||
<div class="container-fluid">
|
||||
<vue-good-table
|
||||
mode="remote"
|
||||
:rows="players"
|
||||
:total-rows="totalRecords || players.length"
|
||||
:columns="columns"
|
||||
:search-options="tableOptions.search"
|
||||
:pagination-options="tableOptions.pagination"
|
||||
style-class="vgt-table striped"
|
||||
@on-page-change="onPageChange"
|
||||
@on-sort-change="onSortChange"
|
||||
@on-search="onSearch"
|
||||
@on-per-page-change="onPerPageChange"
|
||||
>
|
||||
<template #table-row="props">
|
||||
<span v-if="props.column.field === 'name'">
|
||||
{{ props.formattedRow[props.column.field] }}
|
||||
<a :title="$t('admin.changePlayerName')" data-test="name" @click="changeName(props.row)">
|
||||
<i class="fas fa-edit btn-edit" />
|
||||
</a>
|
||||
</span>
|
||||
<span v-else-if="props.column.field === 'uid'">
|
||||
<a
|
||||
:href="`${baseUrl}/admin/users?uid=${props.row.uid}`"
|
||||
:title="$t('admin.inspectHisOwner')"
|
||||
data-toggle="tooltip"
|
||||
data-placement="right"
|
||||
>{{ props.formattedRow[props.column.field] }}</a>
|
||||
<a :title="$t('admin.changeOwner')" data-test="owner" @click="changeOwner(props.row)">
|
||||
<i class="fas fa-edit btn-edit" />
|
||||
</a>
|
||||
</span>
|
||||
<span v-else-if="props.column.field === 'preview'">
|
||||
<a
|
||||
v-if="props.row.tid_skin"
|
||||
:href="`${baseUrl}/skinlib/show/${props.row.tid_skin}`"
|
||||
>
|
||||
<img :src="`${baseUrl}/preview/${props.row.tid_skin}?height=64`" width="64">
|
||||
</a>
|
||||
<a
|
||||
v-if="props.row.tid_cape"
|
||||
:href="`${baseUrl}/skinlib/show/${props.row.tid_cape}`"
|
||||
>
|
||||
<img :src="`${baseUrl}/preview/${props.row.tid_cape}?height=64`" width="64">
|
||||
</a>
|
||||
</span>
|
||||
<span v-else-if="props.column.field === 'operations'">
|
||||
<button
|
||||
data-toggle="modal"
|
||||
data-target="#modal-change-texture"
|
||||
class="btn btn-default"
|
||||
@click="textureChanges.originalIndex = props.row.originalIndex"
|
||||
>
|
||||
{{ $t('admin.changeTexture') }}
|
||||
</button>
|
||||
<button class="btn btn-danger" @click="deletePlayer(props.row)">
|
||||
{{ $t('admin.deletePlayer') }}
|
||||
</button>
|
||||
</span>
|
||||
<span v-else v-text="props.formattedRow[props.column.field]" />
|
||||
</template>
|
||||
</vue-good-table>
|
||||
|
||||
<modal
|
||||
id="modal-change-texture"
|
||||
:title="$t('admin.changeTexture')"
|
||||
center
|
||||
@confirm="changeTexture"
|
||||
>
|
||||
<div class="form-group">
|
||||
<label v-t="'admin.textureType'" />
|
||||
<select v-model="textureChanges.type" class="form-control">
|
||||
<option v-t="'general.skin'" value="skin" />
|
||||
<option v-t="'general.cape'" value="cape" />
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>TID</label>
|
||||
<input
|
||||
v-model.number="textureChanges.tid"
|
||||
class="form-control"
|
||||
type="text"
|
||||
:placeholder="$t('admin.pidNotice')"
|
||||
>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { VueGoodTable } from 'vue-good-table'
|
||||
import 'vue-good-table/dist/vue-good-table.min.css'
|
||||
import Modal from '../../components/Modal.vue'
|
||||
import tableOptions from '../../components/mixins/tableOptions'
|
||||
import serverTable from '../../components/mixins/serverTable'
|
||||
import emitMounted from '../../components/mixins/emitMounted'
|
||||
import { showModal, toast } from '../../scripts/notify'
|
||||
import { truthy } from '../../scripts/validators'
|
||||
|
||||
export default {
|
||||
name: 'PlayersManagement',
|
||||
components: {
|
||||
Modal,
|
||||
VueGoodTable,
|
||||
},
|
||||
mixins: [
|
||||
emitMounted,
|
||||
tableOptions,
|
||||
serverTable,
|
||||
],
|
||||
props: {
|
||||
baseUrl: {
|
||||
type: String,
|
||||
default: blessing.base_url,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
players: [],
|
||||
columns: [
|
||||
{
|
||||
field: 'pid', label: 'PID', type: 'number',
|
||||
},
|
||||
{ field: 'name', label: this.$t('general.player.player-name') },
|
||||
{
|
||||
field: 'uid', label: this.$t('general.player.owner'), type: 'number', sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'preview', label: this.$t('general.player.previews'), globalSearchDisabled: true, sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'last_modified', label: this.$t('general.player.last-modified'), sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'operations', label: this.$t('admin.operationsTitle'), globalSearchDisabled: true, sortable: false,
|
||||
},
|
||||
],
|
||||
textureChanges: {
|
||||
originalIndex: -1,
|
||||
type: 'skin',
|
||||
tid: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
async fetchData() {
|
||||
const { data, totalRecords } = await this.$http.get(
|
||||
`/admin/players/list${location.search}`,
|
||||
!location.search && this.serverParams,
|
||||
)
|
||||
this.totalRecords = totalRecords
|
||||
this.players = data
|
||||
},
|
||||
async changeTexture() {
|
||||
const player = this.players[this.textureChanges.originalIndex]
|
||||
const { type, tid } = this.textureChanges
|
||||
|
||||
const { code, message } = await this.$http.post(
|
||||
'/admin/players?action=texture',
|
||||
{
|
||||
pid: player.pid, type, tid,
|
||||
},
|
||||
)
|
||||
if (code === 0) {
|
||||
player[`tid_${type}`] = tid
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
},
|
||||
async changeName(player) {
|
||||
let value
|
||||
try {
|
||||
({ value } = await showModal({
|
||||
mode: 'prompt',
|
||||
text: this.$t('admin.changePlayerNameNotice'),
|
||||
input: player.name,
|
||||
validator: truthy(this.$t('admin.emptyPlayerName')),
|
||||
}))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await this.$http.post(
|
||||
'/admin/players?action=name',
|
||||
{ pid: player.pid, name: value },
|
||||
)
|
||||
if (code === 0) {
|
||||
player.name = value
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
},
|
||||
async changeOwner(player) {
|
||||
let value
|
||||
try {
|
||||
({ value } = await showModal({
|
||||
mode: 'prompt',
|
||||
text: this.$t('admin.changePlayerOwner'),
|
||||
input: player.uid,
|
||||
}))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
value = Number.parseInt(value)
|
||||
|
||||
const { code, message } = await this.$http.post(
|
||||
'/admin/players?action=owner',
|
||||
{ pid: player.pid, uid: value },
|
||||
)
|
||||
if (code === 0) {
|
||||
player.uid = value
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
},
|
||||
async deletePlayer({ pid, originalIndex }) {
|
||||
try {
|
||||
await showModal({
|
||||
text: this.$t('admin.deletePlayerNotice'),
|
||||
okButtonType: 'danger',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await this.$http.post(
|
||||
'/admin/players?action=delete',
|
||||
{ pid },
|
||||
)
|
||||
if (code === 0) {
|
||||
this.$delete(this.players, originalIndex)
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
@use '../../../styles/breakpoints';
|
||||
|
||||
.box {
|
||||
width: 48%;
|
||||
margin: 7px;
|
||||
|
||||
@include breakpoints.less-than('lg') {
|
||||
width: 98%;
|
||||
}
|
||||
}
|
||||
148
resources/assets/src/views/admin/PlayersManagement/Card.tsx
Normal file
148
resources/assets/src/views/admin/PlayersManagement/Card.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import React from 'react'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import { showModal } from '@/scripts/notify'
|
||||
import { Player } from '@/scripts/types'
|
||||
import styles from './Card.module.scss'
|
||||
|
||||
interface Props {
|
||||
player: Player
|
||||
onUpdateName(): void
|
||||
onUpdateOwner(): void
|
||||
onUpdateTexture(): void
|
||||
onDelete(): void
|
||||
}
|
||||
|
||||
const Card: React.FC<Props> = (props) => {
|
||||
const { player } = props
|
||||
|
||||
const handlePreviewTextures = () => {
|
||||
showModal({
|
||||
mode: 'alert',
|
||||
title: t('general.player.previews'),
|
||||
children: (
|
||||
<div className="row">
|
||||
<div className="col-6 d-flex justify-content-center">
|
||||
{player.tid_skin > 0 && (
|
||||
<a
|
||||
href={`${blessing.base_url}/skinlib/show/${player.tid_skin}`}
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src={`${blessing.base_url}/preview/${player.tid_skin}`}
|
||||
alt={`${player.name} - ${t('general.skin')}`}
|
||||
width="128"
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-6 d-flex justify-content-center">
|
||||
{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="128"
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`info-box ${styles.box}`}>
|
||||
<div className="info-box-icon">
|
||||
<img
|
||||
className="bs-avatar"
|
||||
src={`${blessing.base_url}/avatar/player/${player.name}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="info-box-content">
|
||||
<div className="row">
|
||||
<div className="col-10">
|
||||
<div>
|
||||
<b>{player.name}</b>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-2">
|
||||
<div className="float-right dropdown">
|
||||
<a
|
||||
className="text-gray"
|
||||
href="#"
|
||||
data-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i className="fas fa-cog"></i>
|
||||
</a>
|
||||
<div className="dropdown-menu dropdown-menu-right">
|
||||
<a
|
||||
href="#"
|
||||
className="dropdown-item"
|
||||
onClick={handlePreviewTextures}
|
||||
>
|
||||
<i className="fas fa-eye mr-2"></i>
|
||||
{t('general.player.previews')}
|
||||
</a>
|
||||
<div className="dropdown-divider"></div>
|
||||
<a
|
||||
href="#"
|
||||
className="dropdown-item"
|
||||
onClick={props.onUpdateName}
|
||||
>
|
||||
<i className="fas fa-signature mr-2"></i>
|
||||
{t('admin.changePlayerName')}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="dropdown-item"
|
||||
onClick={props.onUpdateOwner}
|
||||
>
|
||||
<i className="fas fa-user-edit mr-2"></i>
|
||||
{t('admin.changeOwner')}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="dropdown-item"
|
||||
onClick={props.onUpdateTexture}
|
||||
>
|
||||
<i className="fas fa-tshirt mr-2"></i>
|
||||
{t('admin.changeTexture')}
|
||||
</a>
|
||||
<div className="dropdown-divider"></div>
|
||||
<a
|
||||
href="#"
|
||||
className="dropdown-item dropdown-item-danger"
|
||||
onClick={props.onDelete}
|
||||
>
|
||||
<i className="fas fa-trash mr-2"></i>
|
||||
{t('admin.deletePlayer')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<span className="mr-2">PID: {player.pid}</span>
|
||||
<span>
|
||||
{t('general.player.owner')}: {player.uid}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<small className="text-gray">
|
||||
{`${t('general.player.last-modified')}: `}
|
||||
{player.last_modified}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Card
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import React, { useState } from 'react'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import Modal from '@/components/Modal'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
onSubmit(type: 'skin' | 'cape', tid: number): void
|
||||
onClose(): void
|
||||
}
|
||||
|
||||
const ModalUpdateTexture: React.FC<Props> = (props) => {
|
||||
const [type, setType] = useState<'skin' | 'cape'>('skin')
|
||||
const [tid, setTid] = useState('')
|
||||
|
||||
const handleTypeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setType(event.target.value as 'skin' | 'cape')
|
||||
}
|
||||
|
||||
const handleTidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTid(event.target.value)
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
props.onSubmit(type, Number.parseInt(tid))
|
||||
setType('skin')
|
||||
setTid('')
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setType('skin')
|
||||
setTid('')
|
||||
props.onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={props.open}
|
||||
center
|
||||
onConfirm={handleConfirm}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<div className="form-group">
|
||||
<label>{t('admin.textureType')}</label>
|
||||
<div>
|
||||
<label className="mr-5">
|
||||
<input
|
||||
className="mr-1"
|
||||
type="radio"
|
||||
value="skin"
|
||||
checked={type === 'skin'}
|
||||
onChange={handleTypeChange}
|
||||
/>
|
||||
{t('general.skin')}
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
className="mr-1"
|
||||
type="radio"
|
||||
value="cape"
|
||||
checked={type === 'cape'}
|
||||
onChange={handleTypeChange}
|
||||
/>
|
||||
{t('general.cape')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="update-texture-tid">TID</label>
|
||||
<input
|
||||
type="number"
|
||||
id="update-texture-tid"
|
||||
className="form-control"
|
||||
placeholder={t('admin.pidNotice')}
|
||||
value={tid}
|
||||
onChange={handleTidChange}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalUpdateTexture
|
||||
200
resources/assets/src/views/admin/PlayersManagement/index.tsx
Normal file
200
resources/assets/src/views/admin/PlayersManagement/index.tsx
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { hot } from 'react-hot-loader/root'
|
||||
import { useImmer } from 'use-immer'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import * as fetch from '@/scripts/net'
|
||||
import { Player, Paginator } from '@/scripts/types'
|
||||
import { toast, showModal } from '@/scripts/notify'
|
||||
import Loading from '@/components/Loading'
|
||||
import Pagination from '@/components/Pagination'
|
||||
import Card from './Card'
|
||||
import ModalUpdateTexture from './ModalUpdateTexture'
|
||||
|
||||
const PlayersManagement: React.FC = () => {
|
||||
const [players, setPlayers] = useImmer<Player[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [textureUpdating, setTextureUpdating] = useState(-1)
|
||||
|
||||
const getPlayers = async () => {
|
||||
setIsLoading(true)
|
||||
const { data, last_page }: Paginator<Player> = await fetch.get(
|
||||
'/admin/players/list',
|
||||
{
|
||||
q: query,
|
||||
page,
|
||||
},
|
||||
)
|
||||
setTotalPages(last_page)
|
||||
setPlayers(() => data)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getPlayers()
|
||||
}, [page])
|
||||
|
||||
const handleQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setQuery(event.target.value)
|
||||
}
|
||||
|
||||
const handleSubmitQuery = (event: React.FormEvent) => {
|
||||
event.preventDefault()
|
||||
getPlayers()
|
||||
}
|
||||
|
||||
const handleUpdateName = async (player: Player, index: number) => {
|
||||
let name: string
|
||||
try {
|
||||
const { value } = await showModal({
|
||||
mode: 'prompt',
|
||||
text: t('admin.changePlayerNameNotice'),
|
||||
input: player.name,
|
||||
validator: (value: string) => {
|
||||
if (!value) {
|
||||
return t('admin.emptyPlayerName')
|
||||
}
|
||||
},
|
||||
})
|
||||
name = value
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.put<fetch.ResponseBody>(
|
||||
`/admin/players/${player.pid}/name`,
|
||||
{ player_name: name },
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
setPlayers((players) => {
|
||||
players[index].name = name
|
||||
})
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateOwner = async (player: Player, index: number) => {
|
||||
let uid: number
|
||||
try {
|
||||
const { value } = await showModal({
|
||||
mode: 'prompt',
|
||||
text: t('admin.changePlayerOwner'),
|
||||
input: player.uid.toString(),
|
||||
inputType: 'number',
|
||||
})
|
||||
uid = Number.parseInt(value)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.put<fetch.ResponseBody>(
|
||||
`/admin/players/${player.pid}/owner`,
|
||||
{ uid },
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
setPlayers((players) => {
|
||||
players[index].uid = uid
|
||||
})
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCloseModalUpdateTexture = () => setTextureUpdating(-1)
|
||||
|
||||
const handleUpdateTexture = async (type: 'skin' | 'cape', tid: number) => {
|
||||
const { code, message } = await fetch.put<fetch.ResponseBody>(
|
||||
`/admin/players/${players[textureUpdating].pid}/textures`,
|
||||
{ type, tid },
|
||||
)
|
||||
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
setPlayers((players) => {
|
||||
const field = `tid_${type}` as 'tid_skin' | 'tid_cape'
|
||||
players[textureUpdating][field] = tid
|
||||
})
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (player: Player) => {
|
||||
try {
|
||||
await showModal({
|
||||
text: t('admin.deletePlayerNotice'),
|
||||
okButtonType: 'danger',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.del<fetch.ResponseBody>(
|
||||
`/admin/players/${player.pid}`,
|
||||
)
|
||||
if (code === 0) {
|
||||
setPlayers((players) => players.filter(({ pid }) => pid !== player.pid))
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<form className="input-group" onSubmit={handleSubmitQuery}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
title={t('vendor.datatable.search')}
|
||||
value={query}
|
||||
onChange={handleQueryChange}
|
||||
/>
|
||||
<div className="input-group-append">
|
||||
<button className="btn btn-primary" type="submit">
|
||||
{t('vendor.datatable.search')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{isLoading ? (
|
||||
<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 className="card-footer">
|
||||
<div className="float-right">
|
||||
<Pagination page={page} totalPages={totalPages} onChange={setPage} />
|
||||
</div>
|
||||
</div>
|
||||
<ModalUpdateTexture
|
||||
open={textureUpdating > -1}
|
||||
onSubmit={handleUpdateTexture}
|
||||
onClose={handleCloseModalUpdateTexture}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default hot(PlayersManagement)
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { flushPromises } from '../../utils'
|
||||
import { showModal } from '@/scripts/notify'
|
||||
import Modal from '@/components/Modal.vue'
|
||||
import Players from '@/views/admin/Players.vue'
|
||||
|
||||
jest.mock('@/scripts/notify')
|
||||
|
||||
test('fetch data after initializing', () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: [] })
|
||||
mount(Players)
|
||||
expect(Vue.prototype.$http.get).toBeCalledWith(
|
||||
'/admin/players/list',
|
||||
{
|
||||
page: 1, perPage: 10, search: '', sortField: 'pid', sortType: 'asc',
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('change texture', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{ pid: 1, tid_skin: 0 },
|
||||
],
|
||||
})
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
||||
.mockResolvedValueOnce({ code: 0, message: '0' })
|
||||
|
||||
const wrapper = mount(Players)
|
||||
await flushPromises()
|
||||
const modal = wrapper.find(Modal)
|
||||
wrapper.findAll('.btn-default').trigger('click')
|
||||
|
||||
wrapper.find('.modal-body input').setValue('5')
|
||||
|
||||
modal.vm.$emit('confirm')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/admin/players?action=texture',
|
||||
{
|
||||
pid: 1, tid: 5, type: 'skin',
|
||||
},
|
||||
)
|
||||
modal.vm.$emit('confirm')
|
||||
await flushPromises()
|
||||
expect(wrapper.html()).toContain('/preview/5?height=64')
|
||||
})
|
||||
|
||||
test('change player name', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{ pid: 1, name: 'old' },
|
||||
],
|
||||
})
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
||||
.mockResolvedValueOnce({ code: 0, message: '0' })
|
||||
showModal
|
||||
.mockRejectedValueOnce(null)
|
||||
.mockResolvedValue({ value: 'new' })
|
||||
const wrapper = mount(Players)
|
||||
await flushPromises()
|
||||
const button = wrapper.find('[data-test="name"]')
|
||||
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/admin/players?action=name',
|
||||
{ pid: 1, name: 'new' },
|
||||
)
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('new')
|
||||
})
|
||||
|
||||
test('change owner', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{ pid: 1, uid: 2 },
|
||||
],
|
||||
})
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
||||
.mockResolvedValueOnce({ code: 0, message: '0' })
|
||||
showModal
|
||||
.mockRejectedValueOnce(null)
|
||||
.mockResolvedValue({ value: '3' })
|
||||
|
||||
const wrapper = mount(Players)
|
||||
await flushPromises()
|
||||
const button = wrapper.find('[data-test="owner"]')
|
||||
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/admin/players?action=owner',
|
||||
{ pid: 1, uid: 3 },
|
||||
)
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('3')
|
||||
})
|
||||
|
||||
test('delete player', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{ pid: 1, name: 'to-be-deleted' },
|
||||
],
|
||||
})
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
||||
.mockResolvedValueOnce({ code: 0, message: '0' })
|
||||
showModal
|
||||
.mockRejectedValueOnce(null)
|
||||
.mockResolvedValue({ value: '' })
|
||||
|
||||
const wrapper = mount(Players)
|
||||
await flushPromises()
|
||||
const button = wrapper.find('.btn-danger')
|
||||
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/admin/players?action=delete',
|
||||
{ pid: 1 },
|
||||
)
|
||||
expect(wrapper.text()).toContain('to-be-deleted')
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('No data')
|
||||
})
|
||||
362
resources/assets/tests/views/admin/PlayersManagement.test.tsx
Normal file
362
resources/assets/tests/views/admin/PlayersManagement.test.tsx
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
import React from 'react'
|
||||
import { render, waitFor, fireEvent } from '@testing-library/react'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import * as fetch from '@/scripts/net'
|
||||
import { Player, Paginator } from '@/scripts/types'
|
||||
import PlayersManagement from '@/views/admin/PlayersManagement'
|
||||
|
||||
jest.mock('@/scripts/net')
|
||||
|
||||
const fixture: Readonly<Player> = Object.freeze<Player>({
|
||||
pid: 1,
|
||||
name: 'kumiko',
|
||||
uid: 1,
|
||||
tid_skin: 1,
|
||||
tid_cape: 2,
|
||||
last_modified: new Date().toString(),
|
||||
})
|
||||
|
||||
function createPaginator(data: Player[]): Paginator<Player> {
|
||||
return {
|
||||
data,
|
||||
total: data.length,
|
||||
from: 1,
|
||||
to: data.length,
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
}
|
||||
}
|
||||
|
||||
test('search players', async () => {
|
||||
fetch.get.mockResolvedValue(createPaginator([]))
|
||||
|
||||
const { getByTitle, getByText } = render(<PlayersManagement />)
|
||||
await waitFor(() =>
|
||||
expect(fetch.get).toBeCalledWith('/admin/players/list', { q: '', page: 1 }),
|
||||
)
|
||||
|
||||
fireEvent.input(getByTitle(t('vendor.datatable.search')), {
|
||||
target: { value: 's' },
|
||||
})
|
||||
fireEvent.click(getByText(t('vendor.datatable.search')))
|
||||
await waitFor(() =>
|
||||
expect(fetch.get).toBeCalledWith('/admin/players/list', {
|
||||
q: 's',
|
||||
page: 1,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test('preview textures', async () => {
|
||||
fetch.get.mockResolvedValue(createPaginator([fixture]))
|
||||
|
||||
const { getByText, queryByAltText } = render(<PlayersManagement />)
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
|
||||
fireEvent.click(getByText(t('general.player.previews')))
|
||||
|
||||
expect(
|
||||
queryByAltText(`${fixture.name} - ${t('general.skin')}`),
|
||||
).toHaveAttribute('src', `${blessing.base_url}/preview/${fixture.tid_skin}`)
|
||||
expect(
|
||||
queryByAltText(`${fixture.name} - ${t('general.cape')}`),
|
||||
).toHaveAttribute('src', `${blessing.base_url}/preview/${fixture.tid_cape}`)
|
||||
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
})
|
||||
|
||||
describe('update player name', () => {
|
||||
beforeEach(() => {
|
||||
fetch.get.mockResolvedValue(createPaginator([fixture]))
|
||||
})
|
||||
|
||||
it('empty value', async () => {
|
||||
const { getByText, getByDisplayValue, queryByText } = render(
|
||||
<PlayersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changePlayerName')))
|
||||
fireEvent.input(getByDisplayValue(fixture.name), {
|
||||
target: { value: '' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
expect(queryByText(t('admin.emptyPlayerName'))).toBeInTheDocument()
|
||||
expect(fetch.put).not.toBeCalled()
|
||||
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
|
||||
expect(queryByText(fixture.name)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, getByDisplayValue, queryByText, queryByRole } = render(
|
||||
<PlayersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changePlayerName')))
|
||||
fireEvent.input(getByDisplayValue(fixture.name), {
|
||||
target: { value: 'reina' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(`/admin/players/${fixture.pid}/name`, {
|
||||
player_name: 'reina',
|
||||
}),
|
||||
)
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(queryByRole('status')).toHaveClass('alert-success')
|
||||
expect(queryByText('reina')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const { getByText, getByDisplayValue, queryByText, queryByRole } = render(
|
||||
<PlayersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changePlayerName')))
|
||||
fireEvent.input(getByDisplayValue(fixture.name), {
|
||||
target: { value: 'reina' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(`/admin/players/${fixture.pid}/name`, {
|
||||
player_name: 'reina',
|
||||
}),
|
||||
)
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(queryByRole('alert')).toHaveClass('alert-danger')
|
||||
expect(queryByText(fixture.name)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('update owner', () => {
|
||||
beforeEach(() => {
|
||||
fetch.get.mockResolvedValue(createPaginator([fixture]))
|
||||
})
|
||||
|
||||
it('cancelled', async () => {
|
||||
const { getByText, queryByText } = render(<PlayersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changeOwner')))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
|
||||
expect(fetch.put).not.toBeCalled()
|
||||
expect(
|
||||
queryByText(`${t('general.player.owner')}: ${fixture.uid}`),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, getByDisplayValue, queryByText, queryByRole } = render(
|
||||
<PlayersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changeOwner')))
|
||||
fireEvent.input(getByDisplayValue(fixture.uid.toString()), {
|
||||
target: { value: '2' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(`/admin/players/${fixture.pid}/owner`, {
|
||||
uid: 2,
|
||||
}),
|
||||
)
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(queryByRole('status')).toHaveClass('alert-success')
|
||||
expect(queryByText(`${t('general.player.owner')}: 2`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const { getByText, getByDisplayValue, queryByText, queryByRole } = render(
|
||||
<PlayersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changeOwner')))
|
||||
fireEvent.input(getByDisplayValue(fixture.uid.toString()), {
|
||||
target: { value: '2' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(`/admin/players/${fixture.pid}/owner`, {
|
||||
uid: 2,
|
||||
}),
|
||||
)
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(queryByRole('alert')).toHaveClass('alert-danger')
|
||||
expect(
|
||||
queryByText(`${t('general.player.owner')}: ${fixture.uid}`),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('update texture', () => {
|
||||
beforeEach(() => {
|
||||
fetch.get.mockResolvedValue(createPaginator([fixture]))
|
||||
})
|
||||
|
||||
it('cancelled', async () => {
|
||||
const { getByText } = render(<PlayersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changeTexture')))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
|
||||
expect(fetch.put).not.toBeCalled()
|
||||
})
|
||||
|
||||
it('skin', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, getByLabelText, queryByText, queryByRole } = render(
|
||||
<PlayersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changeTexture')))
|
||||
fireEvent.input(getByLabelText('TID'), {
|
||||
target: { value: '2' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(
|
||||
`/admin/players/${fixture.pid}/textures`,
|
||||
{
|
||||
type: 'skin',
|
||||
tid: 2,
|
||||
},
|
||||
),
|
||||
)
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(queryByRole('status')).toHaveClass('alert-success')
|
||||
})
|
||||
|
||||
it('cape', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, getByLabelText, queryByText, queryByRole } = render(
|
||||
<PlayersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changeTexture')))
|
||||
fireEvent.click(getByLabelText(t('general.cape')))
|
||||
fireEvent.input(getByLabelText('TID'), {
|
||||
target: { value: '2' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(
|
||||
`/admin/players/${fixture.pid}/textures`,
|
||||
{
|
||||
type: 'cape',
|
||||
tid: 2,
|
||||
},
|
||||
),
|
||||
)
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(queryByRole('status')).toHaveClass('alert-success')
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const { getByText, getByLabelText, queryByText, queryByRole } = render(
|
||||
<PlayersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changeTexture')))
|
||||
fireEvent.input(getByLabelText('TID'), {
|
||||
target: { value: '2' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(
|
||||
`/admin/players/${fixture.pid}/textures`,
|
||||
{
|
||||
type: 'skin',
|
||||
tid: 2,
|
||||
},
|
||||
),
|
||||
)
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(queryByRole('alert')).toHaveClass('alert-danger')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete player', () => {
|
||||
beforeEach(() => {
|
||||
fetch.get.mockResolvedValue(createPaginator([fixture]))
|
||||
})
|
||||
|
||||
it('cancelled', async () => {
|
||||
const { getByText, queryByText } = render(<PlayersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.deletePlayer')))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
|
||||
expect(fetch.del).not.toBeCalled()
|
||||
expect(queryByText(fixture.name)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.del.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, queryByText, queryByRole } = render(
|
||||
<PlayersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.deletePlayer')))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.del).toBeCalledWith(`/admin/players/${fixture.pid}`),
|
||||
)
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(queryByRole('status')).toHaveClass('alert-success')
|
||||
expect(queryByText(fixture.name)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.del.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const { getByText, queryByText, queryByRole } = render(
|
||||
<PlayersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.deletePlayer')))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.del).toBeCalledWith(`/admin/players/${fixture.pid}`),
|
||||
)
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(queryByRole('alert')).toHaveClass('alert-danger')
|
||||
expect(queryByText(fixture.name)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -45,8 +45,10 @@ const fixtureCape: Readonly<ClosetItem> = Object.freeze<ClosetItem>({
|
|||
const fixturePlayer: Readonly<Player> = Object.freeze<Player>({
|
||||
pid: 1,
|
||||
name: 'kumiko',
|
||||
uid: 1,
|
||||
tid_skin: 1,
|
||||
tid_cape: 2,
|
||||
last_modified: new Date().toString(),
|
||||
})
|
||||
|
||||
function createPaginator(data: ClosetItem[]): Paginator<ClosetItem> {
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ jest.mock('@/scripts/net')
|
|||
const fixture: Readonly<Player> = Object.freeze<Player>({
|
||||
pid: 1,
|
||||
name: 'kumiko',
|
||||
uid: 1,
|
||||
tid_skin: 1,
|
||||
tid_cape: 2,
|
||||
last_modified: new Date().toString(),
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -38,7 +40,14 @@ test('loading indicator', () => {
|
|||
})
|
||||
|
||||
test('search players', async () => {
|
||||
const fixture2: Player = { pid: 2, name: 'reina', tid_skin: 3, tid_cape: 4 }
|
||||
const fixture2: Player = {
|
||||
pid: 2,
|
||||
name: 'reina',
|
||||
uid: 2,
|
||||
tid_skin: 3,
|
||||
tid_cape: 4,
|
||||
last_modified: new Date().toString(),
|
||||
}
|
||||
fetch.get.mockResolvedValue({ data: [fixture, fixture2] })
|
||||
|
||||
const { getByPlaceholderText, queryByText } = render(<Players />)
|
||||
|
|
@ -63,7 +72,14 @@ describe('select player automatically', () => {
|
|||
})
|
||||
|
||||
it('more players', async () => {
|
||||
const fixture2: Player = { pid: 2, name: 'reina', tid_skin: 3, tid_cape: 4 }
|
||||
const fixture2: Player = {
|
||||
pid: 2,
|
||||
name: 'reina',
|
||||
uid: 2,
|
||||
tid_skin: 3,
|
||||
tid_cape: 4,
|
||||
last_modified: new Date().toString(),
|
||||
}
|
||||
fetch.get.mockResolvedValue({ data: [fixture, fixture2] })
|
||||
render(<Players />)
|
||||
await waitFor(() => expect(fetch.get).toBeCalledTimes(1))
|
||||
|
|
|
|||
|
|
@ -133,11 +133,16 @@ Route::prefix('admin')
|
|||
Route::get('list', 'AdminController@getUserData');
|
||||
});
|
||||
|
||||
Route::prefix('players')->group(function () {
|
||||
Route::view('', 'admin.players');
|
||||
Route::post('', 'AdminController@playerAjaxHandler');
|
||||
Route::get('list', 'AdminController@getPlayerData');
|
||||
});
|
||||
Route::prefix('players')
|
||||
->name('players.')
|
||||
->group(function () {
|
||||
Route::view('', 'admin.players');
|
||||
Route::get('list', 'PlayersManagementController@list')->name('list');
|
||||
Route::put('{player}/name', 'PlayersManagementController@name')->name('name');
|
||||
Route::put('{player}/owner', 'PlayersManagementController@owner')->name('owner');
|
||||
Route::put('{player}/textures', 'PlayersManagementController@texture')->name('texture');
|
||||
Route::delete('{player}', 'PlayersManagementController@delete')->name('delete');
|
||||
});
|
||||
|
||||
Route::prefix('closet')->group(function () {
|
||||
Route::post('{uid}', 'ClosetManagementController@add');
|
||||
|
|
|
|||
|
|
@ -387,202 +387,4 @@ class AdminControllerTest extends TestCase
|
|||
]);
|
||||
$this->assertNull(User::find($user->uid));
|
||||
}
|
||||
|
||||
public function testPlayerAjaxHandler()
|
||||
{
|
||||
$player = factory(Player::class)->create();
|
||||
|
||||
// Operate on a not-existed player
|
||||
$this->postJson('/admin/players', ['pid' => -1])
|
||||
->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('general.unexistent-player'),
|
||||
]);
|
||||
|
||||
// An admin cannot operate another admin's player
|
||||
$admin = factory(User::class)->states('admin')->create();
|
||||
$this->postJson(
|
||||
'/admin/players',
|
||||
['pid' => factory(Player::class)->create(['uid' => $admin->uid])->pid]
|
||||
)->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('admin.players.no-permission'),
|
||||
]);
|
||||
$superAdmin = factory(User::class)->states('superAdmin')->create();
|
||||
$this->postJson(
|
||||
'/admin/players',
|
||||
['pid' => factory(Player::class)->create(['uid' => $superAdmin->uid])->pid]
|
||||
)->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('admin.players.no-permission'),
|
||||
]);
|
||||
// For self is OK
|
||||
$this->actingAs($admin)->postJson(
|
||||
'/admin/players',
|
||||
['pid' => factory(Player::class)->create(['uid' => $admin->uid])->pid]
|
||||
)->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('admin.users.operations.invalid'),
|
||||
]);
|
||||
|
||||
// Change texture without `type` field
|
||||
$this->postJson('/admin/players', [
|
||||
'pid' => $player->pid,
|
||||
'action' => 'texture',
|
||||
])->assertJsonValidationErrors(['type']);
|
||||
|
||||
// Change texture without `tid` field
|
||||
$this->postJson('/admin/players', [
|
||||
'pid' => $player->pid,
|
||||
'action' => 'texture',
|
||||
'type' => 'skin',
|
||||
])->assertJsonValidationErrors(['tid']);
|
||||
|
||||
// Change texture with a not-integer value
|
||||
$this->postJson('/admin/players', [
|
||||
'pid' => $player->pid,
|
||||
'action' => 'texture',
|
||||
'type' => 'skin',
|
||||
'tid' => 'string',
|
||||
])->assertJsonValidationErrors(['tid']);
|
||||
|
||||
// Invalid texture
|
||||
$this->postJson('/admin/players', [
|
||||
'pid' => $player->pid,
|
||||
'action' => 'texture',
|
||||
'type' => 'skin',
|
||||
'tid' => -1,
|
||||
])->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('admin.players.textures.non-existent', ['tid' => -1]),
|
||||
]);
|
||||
|
||||
$skin = factory(Texture::class)->create();
|
||||
$cape = factory(Texture::class)->states('cape')->create();
|
||||
|
||||
// Skin
|
||||
$this->postJson('/admin/players', [
|
||||
'pid' => $player->pid,
|
||||
'action' => 'texture',
|
||||
'type' => 'skin',
|
||||
'tid' => $skin->tid,
|
||||
])->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.players.textures.success', ['player' => $player->name]),
|
||||
]);
|
||||
$player = Player::find($player->pid);
|
||||
$this->assertEquals($skin->tid, $player->tid_skin);
|
||||
|
||||
// Cape
|
||||
$this->postJson('/admin/players', [
|
||||
'pid' => $player->pid,
|
||||
'action' => 'texture',
|
||||
'type' => 'cape',
|
||||
'tid' => $cape->tid,
|
||||
])->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.players.textures.success', ['player' => $player->name]),
|
||||
]);
|
||||
$player = Player::find($player->pid);
|
||||
$this->assertEquals($cape->tid, $player->tid_cape);
|
||||
|
||||
// Reset texture
|
||||
$this->postJson('/admin/players', [
|
||||
'pid' => $player->pid,
|
||||
'action' => 'texture',
|
||||
'type' => 'skin',
|
||||
'tid' => 0,
|
||||
])->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.players.textures.success', ['player' => $player->name]),
|
||||
]);
|
||||
$player = Player::find($player->pid);
|
||||
$this->assertEquals(0, $player->tid_skin);
|
||||
$this->assertNotEquals(0, $player->tid_cape);
|
||||
|
||||
$this->postJson('/admin/players', [
|
||||
'pid' => $player->pid,
|
||||
'action' => 'texture',
|
||||
'type' => 'cape',
|
||||
'tid' => 0,
|
||||
])->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.players.textures.success', ['player' => $player->name]),
|
||||
]);
|
||||
$player = Player::find($player->pid);
|
||||
$this->assertEquals(0, $player->tid_cape);
|
||||
|
||||
// Change owner without `uid` field
|
||||
$this->postJson('/admin/players', [
|
||||
'pid' => $player->pid,
|
||||
'action' => 'owner',
|
||||
])->assertJsonValidationErrors(['uid']);
|
||||
|
||||
// Change owner with a not-integer `uid` value
|
||||
$this->postJson('/admin/players', [
|
||||
'pid' => $player->pid,
|
||||
'action' => 'owner',
|
||||
'uid' => 'string',
|
||||
])->assertJsonValidationErrors(['uid']);
|
||||
|
||||
// Change owner to a not-existed user
|
||||
$this->postJson('/admin/players', [
|
||||
'pid' => $player->pid,
|
||||
'action' => 'owner',
|
||||
'uid' => -1,
|
||||
])->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('admin.users.operations.non-existent'),
|
||||
]);
|
||||
|
||||
// Change owner successfully
|
||||
$user = factory(User::class)->create();
|
||||
$this->postJson('/admin/players', [
|
||||
'pid' => $player->pid,
|
||||
'action' => 'owner',
|
||||
'uid' => $user->uid,
|
||||
])->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans(
|
||||
'admin.players.owner.success',
|
||||
['player' => $player->name, 'user' => $user->nickname]
|
||||
),
|
||||
]);
|
||||
|
||||
// Rename a player without `name` field
|
||||
$this->postJson('/admin/players', [
|
||||
'pid' => $player->pid,
|
||||
'action' => 'name',
|
||||
])->assertJsonValidationErrors(['name']);
|
||||
|
||||
// Rename a player successfully
|
||||
$this->postJson('/admin/players', [
|
||||
'pid' => $player->pid,
|
||||
'action' => 'name',
|
||||
'name' => 'new_name',
|
||||
])->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.players.name.success', ['player' => 'new_name']),
|
||||
]);
|
||||
|
||||
// Single player
|
||||
option(['single_player' => true]);
|
||||
$this->postJson('/admin/players', [
|
||||
'pid' => $player->pid,
|
||||
'action' => 'name',
|
||||
'name' => 'abc',
|
||||
])->assertJson(['code' => 0]);
|
||||
$player->refresh();
|
||||
$this->assertEquals('abc', $player->user->nickname);
|
||||
|
||||
// Delete a player
|
||||
$this->postJson('/admin/players', [
|
||||
'pid' => $player->pid,
|
||||
'action' => 'delete',
|
||||
])->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.players.delete.success'),
|
||||
]);
|
||||
$this->assertNull(Player::find($player->pid));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,381 @@
|
|||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use App\Models\Player;
|
||||
use App\Models\Texture;
|
||||
use App\Models\User;
|
||||
use Event;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
|
||||
class PlayersManagementControllerTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->actingAs(factory(\App\Models\User::class)->states('admin')->create());
|
||||
}
|
||||
|
||||
public function testList()
|
||||
{
|
||||
$player = factory(Player::class)->create();
|
||||
|
||||
$this->getJson(route('admin.players.list'))
|
||||
->assertJson(['data' => [$player->toArray()]]);
|
||||
}
|
||||
|
||||
public function testAccessControl()
|
||||
{
|
||||
// an admin can't operate another admin's player
|
||||
$admin = factory(User::class)->states('admin')->create();
|
||||
/** @var Player */
|
||||
$player = factory(Player::class)->create(['uid' => $admin->uid]);
|
||||
$this->putJson(
|
||||
route('admin.players.name', ['player' => $player->pid]),
|
||||
['player_name' => 'abcd']
|
||||
)->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('admin.players.no-permission'),
|
||||
])->assertForbidden();
|
||||
|
||||
// for self is OK
|
||||
$this->actingAs($admin)
|
||||
->putJson(
|
||||
route('admin.players.name', ['player' => $player->pid]),
|
||||
['player_name' => 'abcd']
|
||||
)->assertJson(['code' => 0]);
|
||||
|
||||
// super admin
|
||||
$superAdmin = factory(User::class)->states('superAdmin')->create();
|
||||
/** @var Player */
|
||||
$player = factory(Player::class)->create(['uid' => $superAdmin->uid]);
|
||||
$this->putJson(
|
||||
route('admin.players.name', ['player' => $player->pid]),
|
||||
['player_name' => 'abcd']
|
||||
)->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('admin.players.no-permission'),
|
||||
])->assertForbidden();
|
||||
}
|
||||
|
||||
public function testName()
|
||||
{
|
||||
/** @var Player */
|
||||
$player = factory(Player::class)->create();
|
||||
|
||||
// missing `player_name` field
|
||||
$this->putJson(
|
||||
route('admin.players.name', ['player' => $player->pid])
|
||||
)->assertJsonValidationErrors(['player_name']);
|
||||
|
||||
// duplicated player name
|
||||
$this->putJson(
|
||||
route('admin.players.name', ['player' => $player->pid]),
|
||||
['player_name' => $player->name]
|
||||
)->assertJsonValidationErrors(['player_name']);
|
||||
|
||||
// single player
|
||||
option(['single_player' => true]);
|
||||
$this->putJson(
|
||||
route('admin.players.name', ['player' => $player->pid]),
|
||||
['player_name' => 'abc']
|
||||
)->assertJson(['code' => 0]);
|
||||
$player->refresh();
|
||||
$this->assertEquals('abc', $player->user->nickname);
|
||||
option(['single_player' => false]);
|
||||
|
||||
// rename a player successfully
|
||||
Event::fake();
|
||||
$this->putJson(
|
||||
route('admin.players.name', ['player' => $player->pid]),
|
||||
['player_name' => 'new_name']
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.players.name.success', ['player' => 'new_name']),
|
||||
]);
|
||||
$oldName = $player->name;
|
||||
$player->refresh();
|
||||
$this->assertEquals('new_name', $player->name);
|
||||
Event::assertDispatched(
|
||||
'player.name.updating',
|
||||
function ($eventName, $payload) use ($player) {
|
||||
$this->assertEquals($player->pid, $payload[0]->pid);
|
||||
$this->assertEquals('new_name', $payload[1]);
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
Event::assertDispatched(
|
||||
'player.name.updated',
|
||||
function ($eventName, $payload) use ($player, $oldName) {
|
||||
$this->assertEquals($player->pid, $payload[0]->pid);
|
||||
$this->assertEquals($oldName, $payload[1]);
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function testOwner()
|
||||
{
|
||||
Event::fake();
|
||||
|
||||
/** @var Player */
|
||||
$player = factory(Player::class)->create();
|
||||
|
||||
// missing `uid` field
|
||||
$this->putJson(route('admin.players.owner', ['player' => $player->pid]))
|
||||
->assertJsonValidationErrors(['uid']);
|
||||
|
||||
// with a non-integer `uid` value
|
||||
$this->putJson(
|
||||
route('admin.players.owner', ['player' => $player->pid]),
|
||||
['uid' => 's']
|
||||
)->assertJsonValidationErrors(['uid']);
|
||||
|
||||
// change owner to a not-existed user
|
||||
$this->putJson(
|
||||
route('admin.players.owner', ['player' => $player->pid]),
|
||||
['uid' => -1]
|
||||
)->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('admin.users.operations.non-existent'),
|
||||
]);
|
||||
Event::assertDispatched(
|
||||
'player.owner.updating',
|
||||
function ($eventName, $payload) use ($player) {
|
||||
$this->assertEquals($player->pid, $payload[0]->pid);
|
||||
$this->assertEquals(-1, $payload[1]);
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
Event::assertNotDispatched('player.owner.updated');
|
||||
|
||||
// change owner successfully
|
||||
Event::fake();
|
||||
/** @var User */
|
||||
$user = factory(User::class)->create();
|
||||
$this->putJson(
|
||||
route('admin.players.owner', ['player' => $player->pid]),
|
||||
['uid' => $user->uid]
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans(
|
||||
'admin.players.owner.success',
|
||||
['player' => $player->name, 'user' => $user->nickname]
|
||||
),
|
||||
]);
|
||||
Event::assertDispatched(
|
||||
'player.owner.updating',
|
||||
function ($eventName, $payload) use ($player, $user) {
|
||||
$this->assertEquals($player->pid, $payload[0]->pid);
|
||||
$this->assertEquals($user->uid, $payload[1]);
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
Event::assertDispatched(
|
||||
'player.owner.updated',
|
||||
function ($eventName, $payload) use ($player, $user) {
|
||||
$this->assertEquals($player->pid, $payload[0]->pid);
|
||||
$this->assertEquals($user->uid, $payload[1]->uid);
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function testTexture()
|
||||
{
|
||||
Event::fake();
|
||||
|
||||
/** @var Player */
|
||||
$player = factory(Player::class)->create();
|
||||
|
||||
// missing `tid` field
|
||||
$this->putJson(
|
||||
route('admin.players.texture', ['player' => $player->pid])
|
||||
)->assertJsonValidationErrors(['tid']);
|
||||
|
||||
// change texture with a non-integer value
|
||||
$this->putJson(
|
||||
route('admin.players.texture', ['player' => $player->pid]),
|
||||
['tid' => 's']
|
||||
)->assertJsonValidationErrors(['tid']);
|
||||
|
||||
// missing `type` field
|
||||
$this->putJson(
|
||||
route('admin.players.texture', ['player' => $player->pid]),
|
||||
['tid' => -1]
|
||||
)->assertJsonValidationErrors(['type']);
|
||||
|
||||
// invalid type
|
||||
$this->putJson(
|
||||
route('admin.players.texture', ['player' => $player->pid]),
|
||||
['tid' => -1, 'type' => 'elytra']
|
||||
)->assertJsonValidationErrors(['type']);
|
||||
|
||||
// invalid texture
|
||||
$this->putJson(
|
||||
route('admin.players.texture', ['player' => $player->pid]),
|
||||
['tid' => -1, 'type' => 'skin']
|
||||
)->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('admin.players.textures.non-existent', ['tid' => -1]),
|
||||
]);
|
||||
Event::assertDispatched(
|
||||
'player.texture.updating',
|
||||
function ($eventName, $payload) use ($player) {
|
||||
$this->assertEquals($player->pid, $payload[0]->pid);
|
||||
$this->assertEquals('skin', $payload[1]);
|
||||
$this->assertEquals(-1, $payload[2]);
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
Event::assertNotDispatched('player.texture.updated');
|
||||
|
||||
/** @var Texture */
|
||||
$skin = factory(Texture::class)->create();
|
||||
/** @var Texture */
|
||||
$cape = factory(Texture::class)->states('cape')->create();
|
||||
|
||||
// skin
|
||||
Event::fake();
|
||||
$this->putJson(
|
||||
route('admin.players.texture', ['player' => $player->pid]),
|
||||
['tid' => $skin->tid, 'type' => 'skin']
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.players.textures.success', ['player' => $player->name]),
|
||||
]);
|
||||
$previousTid = $player->tid_skin;
|
||||
$player->refresh();
|
||||
$this->assertEquals($skin->tid, $player->tid_skin);
|
||||
Event::assertDispatched(
|
||||
'player.texture.updating',
|
||||
function ($eventName, $payload) use ($player, $skin) {
|
||||
$this->assertEquals($player->pid, $payload[0]->pid);
|
||||
$this->assertEquals('skin', $payload[1]);
|
||||
$this->assertEquals($skin->tid, $payload[2]);
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
Event::assertDispatched(
|
||||
'player.texture.updated',
|
||||
function ($eventName, $payload) use ($player, $previousTid) {
|
||||
$this->assertEquals($player->pid, $payload[0]->pid);
|
||||
$this->assertEquals('skin', $payload[1]);
|
||||
$this->assertEquals($previousTid, $payload[2]);
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
// cape
|
||||
Event::fake();
|
||||
$this->putJson(
|
||||
route('admin.players.texture', ['player' => $player->pid]),
|
||||
['tid' => $cape->tid, 'type' => 'cape']
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.players.textures.success', ['player' => $player->name]),
|
||||
]);
|
||||
$previousTid = $player->tid_cape;
|
||||
$player->refresh();
|
||||
$this->assertEquals($cape->tid, $player->tid_cape);
|
||||
Event::assertDispatched(
|
||||
'player.texture.updating',
|
||||
function ($eventName, $payload) use ($player, $cape) {
|
||||
$this->assertEquals($player->pid, $payload[0]->pid);
|
||||
$this->assertEquals('cape', $payload[1]);
|
||||
$this->assertEquals($cape->tid, $payload[2]);
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
Event::assertDispatched(
|
||||
'player.texture.updated',
|
||||
function ($eventName, $payload) use ($player, $previousTid) {
|
||||
$this->assertEquals($player->pid, $payload[0]->pid);
|
||||
$this->assertEquals('cape', $payload[1]);
|
||||
$this->assertEquals($previousTid, $payload[2]);
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
// reset texture
|
||||
Event::fake();
|
||||
$this->putJson(
|
||||
route('admin.players.texture', ['player' => $player->pid]),
|
||||
['tid' => 0, 'type' => 'skin']
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.players.textures.success', ['player' => $player->name]),
|
||||
]);
|
||||
$previousTid = $player->tid_skin;
|
||||
$player->refresh();
|
||||
$this->assertEquals(0, $player->tid_skin);
|
||||
$this->assertNotEquals(0, $player->tid_cape);
|
||||
Event::assertDispatched(
|
||||
'player.texture.updating',
|
||||
function ($eventName, $payload) use ($player) {
|
||||
$this->assertEquals($player->pid, $payload[0]->pid);
|
||||
$this->assertEquals('skin', $payload[1]);
|
||||
$this->assertEquals(0, $payload[2]);
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
Event::assertDispatched(
|
||||
'player.texture.updated',
|
||||
function ($eventName, $payload) use ($player, $previousTid) {
|
||||
$this->assertEquals($player->pid, $payload[0]->pid);
|
||||
$this->assertEquals('skin', $payload[1]);
|
||||
$this->assertEquals($previousTid, $payload[2]);
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
$this->putJson(
|
||||
route('admin.players.texture', ['player' => $player->pid]),
|
||||
['tid' => 0, 'type' => 'cape']
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.players.textures.success', ['player' => $player->name]),
|
||||
]);
|
||||
$player->refresh();
|
||||
$this->assertEquals(0, $player->tid_cape);
|
||||
}
|
||||
|
||||
public function testDelete()
|
||||
{
|
||||
Event::fake();
|
||||
|
||||
/** @var Player */
|
||||
$player = factory(Player::class)->create();
|
||||
|
||||
$this->deleteJson(route('admin.players.delete', ['player' => $player->pid]))
|
||||
->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.players.delete.success'),
|
||||
]);
|
||||
Event::assertDispatched('player.deleting', function ($eventName, $payload) use ($player) {
|
||||
$this->assertEquals($player->pid, $payload[0]->pid);
|
||||
|
||||
return true;
|
||||
});
|
||||
Event::assertDispatched('player.deleted', function ($eventName, $payload) use ($player) {
|
||||
$this->assertEquals($player->pid, $payload[0]->pid);
|
||||
|
||||
return true;
|
||||
});
|
||||
$this->assertNull(Player::find($player->pid));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user