Add "Users Management" page
This commit is contained in:
parent
79eb2a3be0
commit
6a14339c56
|
|
@ -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",
|
||||
|
|
|
|||
271
resources/assets/src/components/admin/Users.vue
Normal file
271
resources/assets/src/components/admin/Users.vue
Normal 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>
|
||||
|
|
@ -9,4 +9,9 @@ export default [
|
|||
component: () => import('./user/profile'),
|
||||
el: '.content'
|
||||
},
|
||||
{
|
||||
path: 'admin/users',
|
||||
component: () => import('./admin/users'),
|
||||
el: '.content'
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'core-js/fn/array/includes';
|
||||
import 'core-js/fn/array/find';
|
||||
import 'es6-promise/auto';
|
||||
|
||||
Number.parseInt = parseInt;
|
||||
|
|
|
|||
2
resources/assets/src/shims.d.ts
vendored
2
resources/assets/src/shims.d.ts
vendored
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
resources/assets/tests/__mocks__/style.js
Normal file
1
resources/assets/tests/__mocks__/style.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default {};
|
||||
479
resources/assets/tests/components/admin/Users.test.js
Normal file
479
resources/assets/tests/components/admin/Users.test.js
Normal 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);
|
||||
});
|
||||
|
|
@ -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
12
resources/assets/tests/shims.d.ts
vendored
Normal 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>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: 全部
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
38
yarn.lock
38
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user