add closet command for Web CLI
This commit is contained in:
parent
d57dabe188
commit
4c981529e1
34
app/Http/Controllers/ClosetManagementController.php
Normal file
34
app/Http/Controllers/ClosetManagementController.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Texture;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ClosetManagementController extends Controller
|
||||||
|
{
|
||||||
|
public function add(Request $request, $uid)
|
||||||
|
{
|
||||||
|
/** @var Texture */
|
||||||
|
$texture = Texture::findOrFail($request->input('tid'));
|
||||||
|
|
||||||
|
/** @var User */
|
||||||
|
$user = User::findOrFail($uid);
|
||||||
|
$user->closet()->attach($texture->tid, ['item_name' => $texture->name]);
|
||||||
|
|
||||||
|
return json('', 0, compact('user', 'texture'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove(Request $request, $uid)
|
||||||
|
{
|
||||||
|
/** @var Texture */
|
||||||
|
$texture = Texture::findOrFail($request->input('tid'));
|
||||||
|
|
||||||
|
/** @var User */
|
||||||
|
$user = User::findOrFail($uid);
|
||||||
|
$user->closet()->detach($texture->tid);
|
||||||
|
|
||||||
|
return json('', 0, compact('user', 'texture'));
|
||||||
|
}
|
||||||
|
}
|
@ -21,7 +21,7 @@
|
|||||||
"@hot-loader/react-dom": "^16.11.0",
|
"@hot-loader/react-dom": "^16.11.0",
|
||||||
"@tweenjs/tween.js": "^18.4.2",
|
"@tweenjs/tween.js": "^18.4.2",
|
||||||
"admin-lte": "^3.0.1",
|
"admin-lte": "^3.0.1",
|
||||||
"blessing-skin-shell": "^0.1.0",
|
"blessing-skin-shell": "^0.2.0",
|
||||||
"echarts": "^4.6.0",
|
"echarts": "^4.6.0",
|
||||||
"jquery": "^3.4.1",
|
"jquery": "^3.4.1",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
@ -224,7 +224,8 @@
|
|||||||
"coveragePathIgnorePatterns": [
|
"coveragePathIgnorePatterns": [
|
||||||
"/node_modules/",
|
"/node_modules/",
|
||||||
"<rootDir>/resources/assets/tests/setup",
|
"<rootDir>/resources/assets/tests/setup",
|
||||||
"<rootDir>/resources/assets/tests/utils"
|
"<rootDir>/resources/assets/tests/utils",
|
||||||
|
"<rootDir>/resources/assets/tests/scripts/cli/stdio"
|
||||||
],
|
],
|
||||||
"testMatch": [
|
"testMatch": [
|
||||||
"<rootDir>/resources/assets/tests/**/*.test.ts",
|
"<rootDir>/resources/assets/tests/**/*.test.ts",
|
||||||
|
@ -5,6 +5,7 @@ import { FitAddon } from 'xterm-addon-fit'
|
|||||||
import { Shell } from 'blessing-skin-shell'
|
import { Shell } from 'blessing-skin-shell'
|
||||||
import 'xterm/css/xterm.css'
|
import 'xterm/css/xterm.css'
|
||||||
import Draggable from 'react-draggable'
|
import Draggable from 'react-draggable'
|
||||||
|
import ClosetCommand from './cli/ClosetCommand'
|
||||||
import styles from '@/styles/terminal.module.scss'
|
import styles from '@/styles/terminal.module.scss'
|
||||||
|
|
||||||
let launched = false
|
let launched = false
|
||||||
@ -29,6 +30,8 @@ const TerminalWindow: React.FC<{ onClose(): void }> = props => {
|
|||||||
fitAddon.fit()
|
fitAddon.fit()
|
||||||
|
|
||||||
const shell = new Shell(terminal)
|
const shell = new Shell(terminal)
|
||||||
|
shell.addExternal('closet', ClosetCommand)
|
||||||
|
|
||||||
const unbind = terminal.onData(e => shell.input(e))
|
const unbind = terminal.onData(e => shell.input(e))
|
||||||
launched = true
|
launched = true
|
||||||
|
|
||||||
|
63
resources/assets/src/scripts/cli/ClosetCommand.ts
Normal file
63
resources/assets/src/scripts/cli/ClosetCommand.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import type { Stdio } from 'blessing-skin-shell'
|
||||||
|
import * as fetch from '../net'
|
||||||
|
import { User, Texture } from '../types'
|
||||||
|
|
||||||
|
type Subcommand = 'add' | 'remove'
|
||||||
|
|
||||||
|
type Response = fetch.ResponseBody<{ user: User; texture: Texture }>
|
||||||
|
|
||||||
|
export default async function closet(stdio: Stdio, args: string[]) {
|
||||||
|
if (args.includes('-h') || args.includes('--help')) {
|
||||||
|
stdio.println('Usage: closet <add|remove> <uid> <tid>')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = args[0] as Subcommand
|
||||||
|
const uid = args[1]
|
||||||
|
const tid = args[2]
|
||||||
|
|
||||||
|
if (!command) {
|
||||||
|
stdio.println('Supported subcommand: add, remove.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!uid) {
|
||||||
|
stdio.println('User ID must be provided.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!tid) {
|
||||||
|
stdio.println('Texture ID must be provided.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'add': {
|
||||||
|
const { code, data } = await fetch.post<Response>(
|
||||||
|
`/admin/closet/${uid}`,
|
||||||
|
{ tid },
|
||||||
|
)
|
||||||
|
if (code === 0) {
|
||||||
|
const { texture, user } = data
|
||||||
|
stdio.println(
|
||||||
|
`Texture "${texture.name}" was added to user ${user.nickname}'s closet.`,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
stdio.println('Error occurred.')
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'remove': {
|
||||||
|
const { code, data } = await fetch.del<Response>(`/admin/closet/${uid}`, {
|
||||||
|
tid,
|
||||||
|
})
|
||||||
|
if (code === 0) {
|
||||||
|
const { texture, user } = data
|
||||||
|
stdio.println(
|
||||||
|
`Texture "${texture.name}" was removed from user ${user.nickname}'s closet.`,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
stdio.println('Error occurred.')
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,16 @@
|
|||||||
|
export type User = {
|
||||||
|
uid: number
|
||||||
|
email: string
|
||||||
|
nickname: string
|
||||||
|
score: number
|
||||||
|
avatar: number
|
||||||
|
permission: number
|
||||||
|
ip: string
|
||||||
|
last_sign_at: string
|
||||||
|
register_at: string
|
||||||
|
verified: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type Player = {
|
export type Player = {
|
||||||
pid: number
|
pid: number
|
||||||
name: string
|
name: string
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
@use '../styles/breakpoints';
|
@use '../styles/breakpoints';
|
||||||
|
|
||||||
.terminal {
|
.terminal {
|
||||||
z-index: 1060;
|
z-index: 1040;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 7vh;
|
bottom: 7vh;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
101
resources/assets/tests/scripts/cli/ClosetCommand.test.ts
Normal file
101
resources/assets/tests/scripts/cli/ClosetCommand.test.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import * as fetch from '@/scripts/net'
|
||||||
|
import runCommand from '@/scripts/cli/ClosetCommand'
|
||||||
|
import { Stdio } from './stdio'
|
||||||
|
|
||||||
|
jest.mock('@/scripts/net')
|
||||||
|
|
||||||
|
test('help message', async () => {
|
||||||
|
let stdio = new Stdio()
|
||||||
|
await runCommand(stdio, ['-h'])
|
||||||
|
expect(stdio.getStdout()).toInclude('Usage')
|
||||||
|
|
||||||
|
stdio = new Stdio()
|
||||||
|
await runCommand(stdio, ['--help'])
|
||||||
|
expect(stdio.getStdout()).toInclude('Usage')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('missing subcommand', async () => {
|
||||||
|
const stdio = new Stdio()
|
||||||
|
await runCommand(stdio, [])
|
||||||
|
expect(fetch.post).not.toBeCalled()
|
||||||
|
expect(fetch.del).not.toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unsupported subcommand', async () => {
|
||||||
|
const stdio = new Stdio()
|
||||||
|
await runCommand(stdio, ['abc'])
|
||||||
|
expect(fetch.post).not.toBeCalled()
|
||||||
|
expect(fetch.del).not.toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('missing uid', async () => {
|
||||||
|
const stdio = new Stdio()
|
||||||
|
await runCommand(stdio, ['add'])
|
||||||
|
expect(stdio.getStdout()).toInclude('User ID')
|
||||||
|
expect(fetch.post).not.toBeCalled()
|
||||||
|
expect(fetch.del).not.toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('missing tid', async () => {
|
||||||
|
const stdio = new Stdio()
|
||||||
|
await runCommand(stdio, ['add', '1'])
|
||||||
|
expect(stdio.getStdout()).toInclude('Texture ID')
|
||||||
|
expect(fetch.post).not.toBeCalled()
|
||||||
|
expect(fetch.del).not.toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('add texture', () => {
|
||||||
|
it('succeeded', async () => {
|
||||||
|
fetch.post.mockResolvedValue({
|
||||||
|
code: 0,
|
||||||
|
data: { user: { nickname: 'kumiko' }, texture: { name: 'eupho' } },
|
||||||
|
})
|
||||||
|
|
||||||
|
const stdio = new Stdio()
|
||||||
|
await runCommand(stdio, ['add', '1', '2'])
|
||||||
|
|
||||||
|
const stdout = stdio.getStdout()
|
||||||
|
expect(stdout).toInclude('kumiko')
|
||||||
|
expect(stdout).toInclude('eupho')
|
||||||
|
expect(fetch.post).toBeCalledWith('/admin/closet/1', { tid: '2' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('failed', async () => {
|
||||||
|
fetch.post.mockResolvedValue({ code: 1 })
|
||||||
|
|
||||||
|
const stdio = new Stdio()
|
||||||
|
await runCommand(stdio, ['add', '1', '2'])
|
||||||
|
|
||||||
|
const stdout = stdio.getStdout()
|
||||||
|
expect(stdout).toInclude('Error occurred.')
|
||||||
|
expect(fetch.post).toBeCalledWith('/admin/closet/1', { tid: '2' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('remove texture', () => {
|
||||||
|
it('succeeded', async () => {
|
||||||
|
fetch.del.mockResolvedValue({
|
||||||
|
code: 0,
|
||||||
|
data: { user: { nickname: 'kumiko' }, texture: { name: 'eupho' } },
|
||||||
|
})
|
||||||
|
|
||||||
|
const stdio = new Stdio()
|
||||||
|
await runCommand(stdio, ['remove', '1', '2'])
|
||||||
|
|
||||||
|
const stdout = stdio.getStdout()
|
||||||
|
expect(stdout).toInclude('kumiko')
|
||||||
|
expect(stdout).toInclude('eupho')
|
||||||
|
expect(fetch.del).toBeCalledWith('/admin/closet/1', { tid: '2' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('failed', async () => {
|
||||||
|
fetch.del.mockResolvedValue({ code: 1 })
|
||||||
|
|
||||||
|
const stdio = new Stdio()
|
||||||
|
await runCommand(stdio, ['remove', '1', '2'])
|
||||||
|
|
||||||
|
const stdout = stdio.getStdout()
|
||||||
|
expect(stdout).toInclude('Error occurred.')
|
||||||
|
expect(fetch.del).toBeCalledWith('/admin/closet/1', { tid: '2' })
|
||||||
|
})
|
||||||
|
})
|
24
resources/assets/tests/scripts/cli/stdio.ts
Normal file
24
resources/assets/tests/scripts/cli/stdio.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export class Stdio {
|
||||||
|
private stdout = ''
|
||||||
|
|
||||||
|
public print(data: string) {
|
||||||
|
this.stdout += data
|
||||||
|
}
|
||||||
|
|
||||||
|
public println(data: string) {
|
||||||
|
this.stdout += data
|
||||||
|
this.stdout += '\r\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
public reset() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public free() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public getStdout() {
|
||||||
|
return this.stdout
|
||||||
|
}
|
||||||
|
}
|
@ -140,6 +140,11 @@ Route::prefix('admin')
|
|||||||
Route::get('list', 'AdminController@getPlayerData');
|
Route::get('list', 'AdminController@getPlayerData');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::prefix('closet')->group(function () {
|
||||||
|
Route::post('{uid}', 'ClosetManagementController@add');
|
||||||
|
Route::delete('{uid}', 'ClosetManagementController@remove');
|
||||||
|
});
|
||||||
|
|
||||||
Route::prefix('reports')->group(function () {
|
Route::prefix('reports')->group(function () {
|
||||||
Route::view('', 'admin.reports');
|
Route::view('', 'admin.reports');
|
||||||
Route::post('', 'ReportController@review');
|
Route::post('', 'ReportController@review');
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests;
|
||||||
|
|
||||||
|
use App\Models\Texture;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
|
||||||
|
class ClosetManagementControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->actingAs(factory(\App\Models\User::class)->states('admin')->create());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAdd()
|
||||||
|
{
|
||||||
|
$user = factory(User::class)->create();
|
||||||
|
$texture = factory(Texture::class)->create();
|
||||||
|
|
||||||
|
$this->postJson('/admin/closet/'.$user->uid, ['tid' => $texture->tid])
|
||||||
|
->assertJson([
|
||||||
|
'code' => 0,
|
||||||
|
'data' => [
|
||||||
|
'user' => $user->toArray(),
|
||||||
|
'texture' => $texture->toArray(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$item = $user->closet()->first();
|
||||||
|
$this->assertEquals($texture->tid, $item->tid);
|
||||||
|
$this->assertEquals($texture->name, $item->pivot->item_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRemove()
|
||||||
|
{
|
||||||
|
$user = factory(User::class)->create();
|
||||||
|
$texture = factory(Texture::class)->create();
|
||||||
|
$user->closet()->attach($texture->tid, ['item_name' => '']);
|
||||||
|
|
||||||
|
$this->deleteJson('/admin/closet/'.$user->uid, ['tid' => $texture->tid])
|
||||||
|
->assertJson([
|
||||||
|
'code' => 0,
|
||||||
|
'data' => [
|
||||||
|
'user' => $user->toArray(),
|
||||||
|
'texture' => $texture->toArray(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$this->assertCount(0, $user->closet);
|
||||||
|
}
|
||||||
|
}
|
@ -2267,10 +2267,10 @@ binary-extensions@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
|
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
|
||||||
integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
|
integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
|
||||||
|
|
||||||
blessing-skin-shell@^0.1.0:
|
blessing-skin-shell@^0.2.0:
|
||||||
version "0.1.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/blessing-skin-shell/-/blessing-skin-shell-0.1.0.tgz#6251b154d2e5b225d65f1b6e8c7489bb2eb67303"
|
resolved "https://registry.yarnpkg.com/blessing-skin-shell/-/blessing-skin-shell-0.2.0.tgz#2454e305c69de134164491d2715a34d599c0d8a1"
|
||||||
integrity sha512-6c7RwPqLS1mXsrLd6QU7xUjr/gfmDgAY5L+CqOMWwwjLfjuohw6SK6cnGyfWxeK8KoDj5zX3QyoYuCKapXlkUg==
|
integrity sha512-bwVHWep3jlPwgY7rAmr+KZJr1RdfVS9OTQB25avRW4EzONflQRiRgU76T8X/DeWa3PItEEGf3qNUJJuRhN5orw==
|
||||||
|
|
||||||
bluebird@^3.1.1, bluebird@^3.5.5:
|
bluebird@^3.1.1, bluebird@^3.5.5:
|
||||||
version "3.5.5"
|
version "3.5.5"
|
||||||
|
Loading…
Reference in New Issue
Block a user