Add "Users Management" page

This commit is contained in:
Pig Fang 2018-08-06 12:14:20 +08:00
parent 79eb2a3be0
commit 6a14339c56
14 changed files with 852 additions and 60 deletions

View File

@ -27,7 +27,6 @@
"bootstrap": "^3.3.7",
"bootstrap-fileinput": "^4.4.7",
"chart.js": "^2.7.1",
"datatables.net-bs": "^1.10.16",
"es6-promise": "^4.2.4",
"font-awesome": "^4.7.0",
"highlight.js": "^9.12.0",
@ -37,6 +36,7 @@
"sweetalert2": "^7.26.9",
"toastr": "^2.1.4",
"vue": "^2.5.16",
"vue-good-table": "^2.12.2",
"vuejs-paginate": "^2.0.1"
},
"devDependencies": {
@ -96,6 +96,7 @@
],
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/resources/assets/src/$1",
"\\.css$": "<rootDir>/resources/assets/tests/__mocks__/style.js",
"\\.(png|jpg)$": "<rootDir>/resources/assets/tests/__mocks__/file.js"
},
"setupTestFrameworkScriptFile": "<rootDir>/resources/assets/tests/setup.js",

View File

@ -0,0 +1,271 @@
<template>
<section class="content">
<vue-good-table
:rows="users"
:columns="columns"
:search-options="tableOptions.search"
:pagination-options="tableOptions.pagination"
styleClass="vgt-table striped"
>
<template slot="table-row" slot-scope="props">
<span v-if="props.column.field === 'players_count'">
<a
:href="props.row | playersLink"
:title="$t('admin.inspectHisPlayers')"
data-toggle="tooltip"
data-placement="right"
>{{ props.formattedRow[props.column.field] }}</a>
</span>
<span v-else-if="props.column.field === 'permission'">
{{ props.row | humanizePermission }}
</span>
<div v-else-if="props.column.field === 'operations'">
<div class="btn-group">
<button
class="btn btn-default dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>{{ $t('general.more') }} <span class="caret"></span></button>
<ul class="dropdown-menu operations-menu">
<li><a @click="changeEmail(props.row)" v-t="'admin.changeEmail'" href="#"></a></li>
<li><a @click="changeNickName(props.row)" v-t="'admin.changeNickName'" href="#"></a></li>
<li><a @click="changePassword(props.row)" v-t="'admin.changePassword'" href="#"></a></li>
<li><a @click="changeScore(props.row)" v-t="'admin.changeScore'" href="#"></a></li>
<template v-if="props.row.permission < 2">
<li class="divider"></li>
<li v-if="props.row.operations >= 2 && props.row.permission > -1">
<a
@click="toggleAdmin(props.row)"
v-t="props.row.permission === 0 ? 'admin.setAdmin' : 'admin.unsetAdmin'"
href="#"
></a>
</li>
<li v-if="props.row.permission < 1">
<a
@click="toggleBan(props.row)"
v-t="props.row.permission === 0 ? 'admin.ban' : 'admin.unban'"
href="#"
></a>
</li>
</template>
</ul>
</div>
<button
:disabled="props.row.permission >= 2 || (props.row.operations === 1 && props.row.permission >= 1)"
class="btn btn-danger"
v-t="'admin.deleteUser'"
@click="deleteUser(props.row)"
></button>
</div>
<span v-else v-text="props.formattedRow[props.column.field]" />
</template>
</vue-good-table>
</section>
</template>
<script>
import { VueGoodTable } from 'vue-good-table';
import 'vue-good-table/dist/vue-good-table.min.css';
import { trans } from '../../js/i18n';
import { swal } from '../../js/notify';
import toastr from 'toastr';
export default {
name: 'UsersManagement',
components: {
VueGoodTable
},
filters: {
humanizePermission(user) {
switch (user.permission) {
case -1:
return trans('admin.banned');
case 0:
return trans('admin.normal');
case 1:
return trans('admin.admin');
case 2:
return trans('admin.superAdmin');
}
},
playersLink(user) {
return `${blessing.base_url}/admin/players?uid=${user.uid}`;
}
},
data() {
return {
users: [],
columns: [
{ field: 'uid', label: 'UID', type: 'number' },
{ field: 'email', label: this.$t('general.user.email') },
{ field: 'nickname', label: this.$t('general.user.nickname'), width: '150px' },
{ field: 'score', label: this.$t('general.user.score'), type: 'number', width: '102px' },
{ field: 'players_count', label: this.$t('admin.playersCount'), type: 'number' },
{ field: 'permission', label: this.$t('admin.status'), globalSearchDisabled: true },
{ field: 'register_at', label: this.$t('general.user.register-at') },
{ field: 'operations', label: this.$t('admin.operationsTitle'), sortable: false, globalSearchDisabled: true }
],
tableOptions: {
search: {
enabled: true,
placeholder: this.$t('vendor.datatable.search')
},
pagination: {
enabled: true,
nextLabel: this.$t('vendor.datatable.next'),
prevLabel: this.$t('vendor.datatable.prev'),
rowsPerPageLabel: this.$t('vendor.datatable.rowsPerPage'),
allLabel: this.$t('vendor.datatable.all'),
ofLabel: this.$t('vendor.datatable.of')
}
},
};
},
beforeMount() {
this.fetchData();
},
methods: {
async fetchData() {
const { data } = await this.$http.get('/admin/user-data');
this.users = data;
},
async changeEmail(user) {
const { dismiss, value } = await swal({
text: this.$t('admin.newUserEmail'),
showCancelButton: true,
input: 'email',
inputValue: user.email,
inputValidator: value => !value && this.$t('auth.emptyEmail')
});
if (dismiss) {
return;
}
const { errno, msg } = await this.$http.post(
'/admin/users?action=email',
{ uid: user.uid, email: value }
);
if (errno === 0) {
user.email = value;
toastr.success(msg);
} else {
toastr.warning(msg);
}
},
async changeNickName(user) {
const { dismiss, value } = await swal({
text: this.$t('admin.newUserNickname'),
showCancelButton: true,
input: 'text',
inputValue: user.nickname,
inputValidator: value => !value && this.$t('auth.emptyNickname')
});
if (dismiss) {
return;
}
const { errno, msg } = await this.$http.post(
'/admin/users?action=nickname',
{ uid: user.uid, nickname: value }
);
if (errno === 0) {
user.nickname = value;
toastr.success(msg);
} else {
toastr.warning(msg);
}
},
async changePassword(user) {
const { dismiss, value } = await swal({
text: this.$t('admin.newUserPassword'),
showCancelButton: true,
input: 'password',
});
if (dismiss) {
return;
}
const { errno, msg } = await this.$http.post(
'/admin/users?action=password',
{ uid: user.uid, password: value }
);
errno === 0 ? toastr.success(msg) : toastr.warning(msg);
},
async changeScore(user) {
const { dismiss, value } = await swal({
text: this.$t('admin.newScore'),
showCancelButton: true,
input: 'number',
inputValue: user.score
});
if (dismiss) {
return;
}
const score = Number.parseInt(value);
const { errno, msg } = await this.$http.post(
'/admin/users?action=score',
{ uid: user.uid, score }
);
if (errno === 0) {
user.score = score;
toastr.success(msg);
} else {
toastr.warning(msg);
}
},
async toggleAdmin(user) {
const { errno, msg } = await this.$http.post(
'/admin/users?action=admin',
{ uid: user.uid }
);
if (errno === 0) {
user.permission = ~user.permission + 2;
toastr.success(msg);
} else {
toastr.warning(msg);
}
},
async toggleBan(user) {
const { errno, msg } = await this.$http.post(
'/admin/users?action=ban',
{ uid: user.uid }
);
if (errno === 0) {
user.permission = ~user.permission;
toastr.success(msg);
} else {
toastr.warning(msg);
}
},
async deleteUser(user) {
const { dismiss } = await swal({
text: this.$t('admin.deleteUserNotice'),
type: 'warning',
showCancelButton: true
});
if (dismiss) {
return;
}
const { errno, msg } = await this.$http.post(
'/admin/users?action=delete',
{ uid: user.uid }
);
if (errno === 0) {
this.users = this.users.filter(({ uid }) => uid !== user.uid);
toastr.success(msg);
} else {
toastr.warning(msg);
}
}
},
};
</script>
<style lang="stylus">
.operations-menu {
margin-left -35px
}
</style>

View File

@ -9,4 +9,9 @@ export default [
component: () => import('./user/profile'),
el: '.content'
},
{
path: 'admin/users',
component: () => import('./admin/users'),
el: '.content'
},
];

View File

@ -1,3 +1,5 @@
import 'core-js/fn/array/includes';
import 'core-js/fn/array/find';
import 'es6-promise/auto';
Number.parseInt = parseInt;

View File

@ -21,7 +21,7 @@ declare module 'vue/types/vue' {
$http: {
get(url: string, params?: object)
post(url: string, data?: object)
post(url: string, data?: object): { errno?: number, msg?: string }
}
}
}

View File

@ -0,0 +1 @@
export default {};

View File

@ -0,0 +1,479 @@
import Vue from 'vue';
import { mount } from '@vue/test-utils';
import { flushPromises } from '../../utils';
import Users from '@/components/admin/Users';
import { swal } from '@/js/notify';
import '@/js/i18n';
import toastr from 'toastr';
jest.mock('@/js/notify');
jest.mock('@/js/i18n', () => ({
trans: key => key
}));
test('fetch data after initializing', () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [] });
mount(Users);
expect(Vue.prototype.$http.get).toBeCalledWith('/admin/user-data');
});
test('humanize permission', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: -1 },
{ uid: 2, permission: 0 },
{ uid: 3, permission: 1 },
{ uid: 4, permission: 2 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const text = wrapper.find('.vgt-table').text();
expect(text).toContain('admin.banned');
expect(text).toContain('admin.normal');
expect(text).toContain('admin.admin');
expect(text).toContain('admin.superAdmin');
});
test('generate players page link', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 0 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-toggle="tooltip"]').attributes())
.toHaveProperty('href', '/admin/players?uid=1');
});
test('admin option should not be displayed for super admins', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 2 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const text = wrapper.find('.vgt-table').text();
expect(text).not.toContain('admin.setAdmin');
expect(text).not.toContain('admin.unsetAdmin');
});
test('banning option should not be displayed for super admins', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 2 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const text = wrapper.find('.vgt-table').text();
expect(text).not.toContain('admin.ban');
expect(text).not.toContain('admin.unban');
});
test('admin option should be displayed for admin as super admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 1, operations: 2 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const text = wrapper.find('.vgt-table').text();
expect(text).toContain('admin.unsetAdmin');
expect(text).not.toContain('admin.setAdmin');
});
test('banning option should not be displayed for admin as super admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 1, operations: 2 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const text = wrapper.find('.vgt-table').text();
expect(text).not.toContain('admin.ban');
expect(text).not.toContain('admin.unban');
});
test('admin option should be displayed for normal users as super admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 0, operations: 2 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const text = wrapper.find('.vgt-table').text();
expect(text).toContain('admin.setAdmin');
expect(text).not.toContain('admin.unsetAdmin');
});
test('banning option should be displayed for normal users as super admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 0, operations: 2 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const text = wrapper.find('.vgt-table').text();
expect(text).toContain('admin.ban');
expect(text).not.toContain('admin.unban');
});
test('admin option should not be displayed for banned users as super admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: -1, operations: 2 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const text = wrapper.find('.vgt-table').text();
expect(text).not.toContain('admin.setAdmin');
expect(text).not.toContain('admin.unsetAdmin');
});
test('banning option should be displayed for banned users as super admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: -1, operations: 2 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const text = wrapper.find('.vgt-table').text();
expect(text).toContain('admin.unban');
});
test('admin option should not be displayed for other admins as admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 1, operations: 1 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const text = wrapper.find('.vgt-table').text();
expect(text).not.toContain('admin.setAdmin');
expect(text).not.toContain('admin.unsetAdmin');
});
test('banning option should not be displayed for other admins as admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 1, operations: 1 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const text = wrapper.find('.vgt-table').text();
expect(text).not.toContain('admin.ban');
expect(text).not.toContain('admin.unban');
});
test('admin option should not be displayed for normal users as admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 0, operations: 1 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const text = wrapper.find('.vgt-table').text();
expect(text).not.toContain('admin.setAdmin');
expect(text).not.toContain('admin.unsetAdmin');
});
test('banning option should be displayed for normal users as admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 0, operations: 1 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const text = wrapper.find('.vgt-table').text();
expect(text).toContain('admin.ban');
expect(text).not.toContain('admin.unban');
});
test('admin option should not be displayed for banned users as admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: -1, operations: 1 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const text = wrapper.find('.vgt-table').text();
expect(text).not.toContain('admin.setAdmin');
expect(text).not.toContain('admin.unsetAdmin');
});
test('banning option should be displayed for banned users as admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: -1, operations: 1 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const text = wrapper.find('.vgt-table').text();
expect(text).toContain('admin.unban');
});
test('deletion button should not be displayed for super admins', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 2 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
expect(wrapper.find('.btn-danger').attributes()).toHaveProperty('disabled', 'disabled');
});
test('deletion button should be displayed for admins as super admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 1, operations: 2 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
expect(wrapper.find('.btn-danger').attributes()).not.toHaveProperty('disabled');
});
test('deletion button should be displayed for normal users as super admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 0, operations: 2 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
expect(wrapper.find('.btn-danger').attributes()).not.toHaveProperty('disabled');
});
test('deletion button should be displayed for banned users as super admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: -1, operations: 2 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
expect(wrapper.find('.btn-danger').attributes()).not.toHaveProperty('disabled');
});
test('deletion button should not be displayed for other admins as admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 1, operations: 1 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
expect(wrapper.find('.btn-danger').attributes()).toHaveProperty('disabled', 'disabled');
});
test('deletion button should be displayed for normal users as admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 0, operations: 1 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
expect(wrapper.find('.btn-danger').attributes()).not.toHaveProperty('disabled');
});
test('deletion button should be displayed for banned users as admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: -1, operations: 1 },
] });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
expect(wrapper.find('.btn-danger').attributes()).not.toHaveProperty('disabled');
});
test('change email', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, email: 'a@b.c' },
] });
Vue.prototype.$http.post
.mockResolvedValueOnce({ errno: 1, msg: '1' })
.mockResolvedValueOnce({ errno: 0, msg: '0' });
swal.mockImplementationOnce(() => ({ dismiss: 1 }))
.mockImplementation(options => {
options.inputValidator();
options.inputValidator('value');
return { value: 'd@e.f' };
});
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const button = wrapper.find('.operations-menu > li:nth-child(1) > a');
button.trigger('click');
expect(Vue.prototype.$http.post).not.toBeCalled();
button.trigger('click');
await wrapper.vm.$nextTick();
expect(Vue.prototype.$http.post).toBeCalledWith(
'/admin/users?action=email',
{ uid: 1, email: 'd@e.f' }
);
expect(wrapper.text()).toContain('a@b.c');
button.trigger('click');
await flushPromises();
expect(wrapper.text()).toContain('d@e.f');
});
test('change nickname', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, nickname: 'old' },
] });
Vue.prototype.$http.post
.mockResolvedValueOnce({ errno: 1, msg: '1' })
.mockResolvedValueOnce({ errno: 0, msg: '0' });
swal.mockImplementationOnce(() => ({ dismiss: 1 }))
.mockImplementation(options => {
options.inputValidator();
options.inputValidator('value');
return { value: 'new' };
});
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const button = wrapper.find('.operations-menu > li:nth-child(2) > a');
button.trigger('click');
expect(Vue.prototype.$http.post).not.toBeCalled();
button.trigger('click');
await wrapper.vm.$nextTick();
expect(Vue.prototype.$http.post).toBeCalledWith(
'/admin/users?action=nickname',
{ uid: 1, nickname: 'new' }
);
expect(wrapper.text()).toContain('old');
button.trigger('click');
await flushPromises();
expect(wrapper.text()).toContain('new');
});
test('change password', async () => {
jest.spyOn(toastr, 'success');
jest.spyOn(toastr, 'warning');
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1 },
] });
Vue.prototype.$http.post
.mockResolvedValueOnce({ errno: 0, msg: '0' })
.mockResolvedValueOnce({ errno: 1, msg: '1' });
swal.mockResolvedValueOnce({ dismiss: 1 })
.mockResolvedValue({ value: 'password' });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const button = wrapper.find('.operations-menu > li:nth-child(3) > a');
button.trigger('click');
expect(Vue.prototype.$http.post).not.toBeCalled();
button.trigger('click');
await wrapper.vm.$nextTick();
expect(Vue.prototype.$http.post).toBeCalledWith(
'/admin/users?action=password',
{ uid: 1, password: 'password' }
);
await flushPromises();
expect(toastr.success).toBeCalledWith('0');
button.trigger('click');
await flushPromises();
expect(toastr.warning).toBeCalledWith('1');
});
test('change score', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, score: 23 },
] });
Vue.prototype.$http.post
.mockResolvedValueOnce({ errno: 1, msg: '1' })
.mockResolvedValueOnce({ errno: 0, msg: '0' });
swal.mockResolvedValueOnce({ dismiss: 1 })
.mockResolvedValue({ value: '45' });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const button = wrapper.find('.operations-menu > li:nth-child(4) > a');
button.trigger('click');
expect(Vue.prototype.$http.post).not.toBeCalled();
button.trigger('click');
await wrapper.vm.$nextTick();
expect(Vue.prototype.$http.post).toBeCalledWith(
'/admin/users?action=score',
{ uid: 1, score: 45 }
);
expect(wrapper.text()).toContain('23');
button.trigger('click');
await flushPromises();
expect(wrapper.text()).toContain('45');
});
test('toggle admin', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 0, operations: 2 },
] });
Vue.prototype.$http.post
.mockResolvedValueOnce({ errno: 1, msg: '1' })
.mockResolvedValue({ errno: 0, msg: '0' });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const button = wrapper.find('.operations-menu > li:nth-child(6) > a');
button.trigger('click');
await wrapper.vm.$nextTick();
expect(Vue.prototype.$http.post).toBeCalledWith(
'/admin/users?action=admin',
{ uid: 1 }
);
expect(wrapper.text()).toContain('admin.normal');
button.trigger('click');
await flushPromises();
expect(wrapper.text()).toContain('admin.admin');
button.trigger('click');
await flushPromises();
expect(wrapper.text()).toContain('admin.normal');
});
test('toggle ban', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, permission: 0, operations: 2 },
] });
Vue.prototype.$http.post
.mockResolvedValueOnce({ errno: 1, msg: '1' })
.mockResolvedValue({ errno: 0, msg: '0' });
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const button = wrapper.find('.operations-menu > li:nth-child(7) > a');
button.trigger('click');
await wrapper.vm.$nextTick();
expect(Vue.prototype.$http.post).toBeCalledWith(
'/admin/users?action=ban',
{ uid: 1 }
);
expect(wrapper.text()).toContain('admin.ban');
button.trigger('click');
await flushPromises();
expect(wrapper.text()).toContain('admin.banned');
button.trigger('click');
await flushPromises();
expect(wrapper.text()).toContain('admin.ban');
});
test('delete user', async () => {
Vue.prototype.$http.get.mockResolvedValue({ data: [
{ uid: 1, nickname: 'to-be-deleted' },
] });
Vue.prototype.$http.post
.mockResolvedValueOnce({ errno: 1, msg: '1' })
.mockResolvedValue({ errno: 0, msg: '0' });
swal.mockResolvedValueOnce({ dismiss: 1 })
.mockResolvedValue({});
const wrapper = mount(Users);
await wrapper.vm.$nextTick();
const button = wrapper.find('.btn-danger');
button.trigger('click');
expect(Vue.prototype.$http.post).not.toBeCalled();
button.trigger('click');
await wrapper.vm.$nextTick();
expect(Vue.prototype.$http.post).toBeCalledWith(
'/admin/users?action=delete',
{ uid: 1 }
);
expect(wrapper.text()).toContain('to-be-deleted');
button.trigger('click');
await flushPromises();
expect(wrapper.vm.users).toHaveLength(0);
});

View File

@ -19,3 +19,8 @@ Vue.directive('t', (el, { value }) => {
throw new Error('[i18n] Invalid arguments in `v-t` directive.');
}
});
Vue.prototype.$http = {
get: jest.fn(),
post: jest.fn(),
};

12
resources/assets/tests/shims.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
import Vue from 'vue'
declare module 'vue/types/vue' {
interface VueConstructor {
prototype: Vue & {
$http: {
get: jest.Mock<any>
post: jest.Mock<any>
}
}
}
}

View File

@ -129,21 +129,24 @@ user:
admin:
operationsTitle: Operations
status: Status
playersCount: Players Count
ban: Ban
unban: Unban
setAdmin: Set as admin
unsetAdmin: Remove admin
deleteUser: Delete User
deleteUser: Delete
cannotDeleteAdmin: You can't delete admins.
cannotDeleteSuperAdmin: You can't delete super admin in this way
changeEmail: Edit Email
changeNickName: Edit Nickname
changePassword: Edit Password
changeScore: Edit Score
newUserEmail: 'Please enter the new email:'
newUserNickname: 'Please enter the new nickname:'
newUserPassword: 'Please enter the new password:'
deleteUserNotice: Are you sure to delete this user? It' permanent.
scoreTip: Press ENTER to submit new score
newScore: 'Please enter the new score:'
inspectHisOwner: Click to inspect the owner of this player
inspectHisPlayers: Click to inspect the players he owns
banned: Banned
@ -206,3 +209,17 @@ general:
rotation: Rotation
pause: Pause
reset: Reset
user:
email: Email
nickname: Nick Name
score: Score
register-at: Registered At
vendor:
datatable:
search: Search
rowsPerPage: Rows per page
prev: Prev
next: Next
of: of
all: All

View File

@ -130,21 +130,24 @@ user:
admin:
operationsTitle: 更多操作
status: 状态
playersCount: 角色数量
ban: 封禁
unban: 解封
setAdmin: 设为管理员
unsetAdmin: 解除管理员
deleteUser: 删除用户
deleteUser: 删除
cannotDeleteAdmin: 你不能删除管理员账号哦
cannotDeleteSuperAdmin: 超级管理员账号不能被这样删除的啦
changeEmail: 修改邮箱
changeNickName: 修改昵称
changePassword: 更改密码
changeScore: 更改积分
newUserEmail: 请输入新邮箱:
newUserNickname: 请输入新昵称:
newUserPassword: 请输入新密码:
deleteUserNotice: 真的要删除此用户吗?此操作不可恢复
scoreTip: 输入修改后的积分,回车提交
newScore: 请输入积分值:
inspectHisOwner: 点击查看该角色的所有者
inspectHisPlayers: 点击查看该用户的角色
banned: 封禁
@ -203,6 +206,11 @@ general:
rotation: 旋转
pause: 暂停
reset: 重置
user:
email: 邮箱
nickname: 昵称
score: 积分
register-at: 注册时间
vendor:
fileinput:
@ -285,24 +293,10 @@ vendor:
fullscreen: 全屏
borderless: 无边界模式
close: 关闭当前预览
datatables:
sProcessing: 处理中...
sLengthMenu: 显示 _MENU_ 项结果
sZeroRecords: 没有匹配结果
sInfo: 显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项
sInfoEmpty: 显示第 0 至 0 项结果,共 0 项
sInfoFiltered: (由 _MAX_ 项结果过滤)
sInfoPostFix: ''
sSearch: '搜索:'
sUrl: ''
sEmptyTable: 表中数据为空
sLoadingRecords: 载入中...
sInfoThousands: ','
oPaginate:
sFirst: 首页
sPrevious: 上页
sNext: 下页
sLast: 末页
oAria:
sSortAscending: ': 以升序排列此列'
sSortDescending: ': 以降序排列此列'
datatable:
search: 搜索
rowsPerPage: 每页的项目数
prev: 上一页
next: 下一页
of:
all: 全部

View File

@ -14,37 +14,7 @@
</section>
<!-- Main content -->
<section class="content">
<div class="box">
<div class="box-body table-bordered">
<table id="user-table" class="table table-hover">
<thead>
<tr>
<th>@lang('general.user.uid')</th>
<th>@lang('general.user.email')</th>
<th>@lang('general.user.nickname')</th>
<th>@lang('general.user.score')</th>
<th>@lang('admin.users.players-count.title')</th>
<th>@lang('admin.users.status.title')</th>
<th>@lang('general.user.register-at')</th>
<th>@lang('general.operations')</th>
</tr>
</thead>
</table>
</div>
</div>
</section><!-- /.content -->
<section class="content"></section><!-- /.content -->
</div><!-- /.content-wrapper -->
@endsection
@section('script')
<script type="text/javascript">
$(document).ready(function() {
$('.box-body').css(
'min-height',
$('.content-wrapper').height() - $('.content-header').outerHeight() - 120
);
});
</script>
@endsection

View File

@ -14,7 +14,6 @@ module.exports = [{
style: [
'bootstrap/dist/css/bootstrap.min.css',
'admin-lte/dist/css/AdminLTE.min.css',
'datatables.net-bs/css/dataTables.bootstrap.css',
'bootstrap-fileinput/css/fileinput.min.css',
'font-awesome/css/font-awesome.min.css',
'icheck/skins/square/blue.css',

View File

@ -2519,7 +2519,7 @@ data-urls@^1.0.0:
whatwg-mimetype "^2.0.0"
whatwg-url "^6.4.0"
datatables.net-bs@^1.10.15, datatables.net-bs@^1.10.16:
datatables.net-bs@^1.10.15:
version "1.10.16"
resolved "https://registry.yarnpkg.com/datatables.net-bs/-/datatables.net-bs-1.10.16.tgz#b0854f5b374f713ae3db4156c7cea8a760c3de76"
dependencies:
@ -2532,6 +2532,10 @@ datatables.net@1.10.16, datatables.net@^1.10.15:
dependencies:
jquery ">=1.7"
date-fns@2.0.0-alpha.7:
version "2.0.0-alpha.7"
resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.0.0-alpha.7.tgz#245ad16f95764eababfb2c0a41fd5d033c20e57a"
date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
@ -2680,6 +2684,10 @@ detective@^5.0.2:
acorn "^5.2.1"
defined "^1.0.0"
diacriticless@1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/diacriticless/-/diacriticless-1.0.1.tgz#e7dda978c2919609bb48aee1efc5de6a337bd4c3"
diff@^3.2.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
@ -4970,10 +4978,18 @@ locate-path@^3.0.0:
p-locate "^3.0.0"
path-exists "^3.0.0"
lodash.assign@^4.2.0:
version "4.2.0"
resolved "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
lodash.clone@^4.5.0:
version "4.5.0"
resolved "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6"
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
@ -4982,6 +4998,14 @@ lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
lodash.filter@^4.6.0:
version "4.6.0"
resolved "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace"
lodash.foreach@^4.5.0:
version "4.5.0"
resolved "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@ -7795,6 +7819,18 @@ vue-eslint-parser@^3.2.1:
esquery "^1.0.1"
lodash "^4.17.10"
vue-good-table@^2.12.2:
version "2.12.2"
resolved "https://registry.npmjs.org/vue-good-table/-/vue-good-table-2.12.2.tgz#74b1f9c8fed528961e72dd23f1c6ae55a8d420ba"
dependencies:
date-fns "2.0.0-alpha.7"
diacriticless "1.0.1"
lodash.assign "^4.2.0"
lodash.clone "^4.5.0"
lodash.clonedeep "^4.5.0"
lodash.filter "^4.6.0"
lodash.foreach "^4.5.0"
vue-hot-reload-api@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.0.tgz#97976142405d13d8efae154749e88c4e358cf926"