mirror of
https://github.com/knadh/listmonk.git
synced 2025-10-11 07:46:11 +08:00
Add missing user UI files.
This commit is contained in:
parent
0968e58766
commit
10f1c38996
3 changed files with 398 additions and 1 deletions
|
@ -570,7 +570,7 @@ body.is-noscroll {
|
||||||
border: 1px solid lighten($color, 37%);
|
border: 1px solid lighten($color, 37%);
|
||||||
box-shadow: 1px 1px 0 lighten($color, 37%);
|
box-shadow: 1px 1px 0 lighten($color, 37%);
|
||||||
}
|
}
|
||||||
&.public, &.running, &.list, &.campaign {
|
&.public, &.running, &.list, &.campaign, &.super {
|
||||||
$color: $primary;
|
$color: $primary;
|
||||||
color: lighten($color, 20%);;
|
color: lighten($color, 20%);;
|
||||||
background: #e6f7ff;
|
background: #e6f7ff;
|
||||||
|
@ -891,6 +891,14 @@ section.analytics {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Users */
|
||||||
|
section.users {
|
||||||
|
td .tag {
|
||||||
|
min-width: 100px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* C3 charting lib */
|
/* C3 charting lib */
|
||||||
.c3 {
|
.c3 {
|
||||||
.c3-text.c3-empty {
|
.c3-text.c3-empty {
|
||||||
|
|
169
frontend/src/views/UserForm.vue
Normal file
169
frontend/src/views/UserForm.vue
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent="onSubmit">
|
||||||
|
<div class="modal-card content" style="width: auto">
|
||||||
|
<header class="modal-card-head">
|
||||||
|
<p v-if="isEditing" class="has-text-grey-light is-size-7">
|
||||||
|
{{ $t('globals.fields.id') }}: <copy-text :text="`${data.id}`" />
|
||||||
|
</p>
|
||||||
|
<h4 v-if="isEditing">
|
||||||
|
{{ data.name }}
|
||||||
|
</h4>
|
||||||
|
<h4 v-else>
|
||||||
|
{{ $t('users.newUser') }}
|
||||||
|
</h4>
|
||||||
|
</header>
|
||||||
|
<section expanded class="modal-card-body">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-8">
|
||||||
|
<b-field :label="$t('users.username')" label-position="on-border">
|
||||||
|
<b-input :maxlength="200" v-model="form.username" name="username" :ref="'focus'"
|
||||||
|
:placeholder="$t('users.username')" required :message="$t('users.usernameHelp')" autocomplete="off" />
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
<div class="column is-4">
|
||||||
|
<b-field :label="$t('globals.fields.status')" label-position="on-border">
|
||||||
|
<b-select v-model="form.status" name="status" required expanded>
|
||||||
|
<option value="enabled">
|
||||||
|
{{ $t('users.status.enabled') }}
|
||||||
|
</option>
|
||||||
|
<option value="disabled">
|
||||||
|
{{ $t('users.status.disabled') }}
|
||||||
|
</option>
|
||||||
|
<option value="super">
|
||||||
|
{{ $t('users.status.super') }}
|
||||||
|
</option>
|
||||||
|
</b-select>
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<b-field :label="$t('subscribers.email')" label-position="on-border">
|
||||||
|
<b-input :maxlength="200" v-model="form.email" name="email" :placeholder="$t('subscribers.email')" required />
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field :label="$t('globals.fields.name')" label-position="on-border">
|
||||||
|
<b-input :maxlength="200" v-model="form.name" name="name" :placeholder="$t('globals.fields.name')" />
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field>
|
||||||
|
<b-checkbox v-model="form.passwordLogin" :native-value="true">
|
||||||
|
{{ $t('users.passwordEnable') }}
|
||||||
|
</b-checkbox>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-6">
|
||||||
|
<b-field :label="$t('users.password')" label-position="on-border">
|
||||||
|
<b-input :disabled="!form.passwordLogin" minlength="8" :maxlength="200" v-model="form.password"
|
||||||
|
type="password" name="password" :placeholder="$t('users.password')"
|
||||||
|
:required="form.passwordLogin && !isEditing" />
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
<div class="column is-6">
|
||||||
|
<b-field :label="$t('users.passwordRepeat')" label-position="on-border">
|
||||||
|
<b-input :disabled="!form.passwordLogin" minlength="8" :maxlength="200" v-model="form.password2"
|
||||||
|
type="password" name="password" :required="form.passwordLogin && !isEditing && form.password" />
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<footer class="modal-card-foot has-text-right">
|
||||||
|
<b-button @click="$parent.close()">
|
||||||
|
{{ $t('globals.buttons.close') }}
|
||||||
|
</b-button>
|
||||||
|
<b-button native-type="submit" type="is-primary" :loading="loading.lists" data-cy="btn-save">
|
||||||
|
{{ $t('globals.buttons.save') }}
|
||||||
|
</b-button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { mapState } from 'vuex';
|
||||||
|
import CopyText from '../components/CopyText.vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'UserForm',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CopyText,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
data: { type: Object, default: () => ({}) },
|
||||||
|
isEditing: { type: Boolean, default: false },
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// Binds form input values.
|
||||||
|
form: {
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
name: '',
|
||||||
|
password: '',
|
||||||
|
passwordLogin: false,
|
||||||
|
status: 'enabled',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onSubmit() {
|
||||||
|
if (!this.form.passwordLogin) {
|
||||||
|
this.form.password = null;
|
||||||
|
this.form.password2 = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isEditing) {
|
||||||
|
if (this.form.passwordLogin && this.form.password && this.form.password !== this.form.password2) {
|
||||||
|
this.$utils.toast(this.$t('users.passwordMismatch'), 'is-danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateUser();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.form.passwordLogin && this.form.password !== this.form.password2) {
|
||||||
|
this.$utils.toast(this.$t('users.passwordMismatch'), 'is-danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.createUser();
|
||||||
|
},
|
||||||
|
|
||||||
|
createUser() {
|
||||||
|
const form = { ...this.form, password_login: this.form.passwordLogin };
|
||||||
|
this.$api.createUser(form).then((data) => {
|
||||||
|
this.$emit('finished');
|
||||||
|
this.$parent.close();
|
||||||
|
this.$utils.toast(this.$t('globals.messages.created', { name: data.name }));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUser() {
|
||||||
|
const form = { ...this.form, password_login: this.form.passwordLogin };
|
||||||
|
this.$api.updateUser({ id: this.data.id, ...form }).then((data) => {
|
||||||
|
this.$emit('finished');
|
||||||
|
this.$parent.close();
|
||||||
|
this.$utils.toast(this.$t('globals.messages.updated', { name: data.name }));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(['loading']),
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.form = { ...this.form, ...this.$props.data };
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.focus.focus();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
220
frontend/src/views/Users.vue
Normal file
220
frontend/src/views/Users.vue
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
<template>
|
||||||
|
<section class="users">
|
||||||
|
<header class="columns page-header">
|
||||||
|
<div class="column is-10">
|
||||||
|
<h1 class="title is-4">
|
||||||
|
{{ $t('globals.terms.users') }}
|
||||||
|
<span v-if="!isNaN(users.length)">({{ users.length }})</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="column has-text-right">
|
||||||
|
<b-field expanded>
|
||||||
|
<b-button expanded type="is-primary" icon-left="plus" class="btn-new" @click="showNewForm" data-cy="btn-new">
|
||||||
|
{{ $t('globals.buttons.new') }}
|
||||||
|
</b-button>
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<b-table :data="users" :loading="loading.users" hoverable checkable :checked-rows.sync="checked"
|
||||||
|
default-sort="createdAt" backend-sorting @sort="onSort" @check-all="onTableCheck" @check="onTableCheck">
|
||||||
|
<template #top-left>
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-6">
|
||||||
|
<form @submit.prevent="getUsers">
|
||||||
|
<div>
|
||||||
|
<b-field>
|
||||||
|
<b-input v-model="queryParams.query" name="query" expanded icon="magnify" ref="query"
|
||||||
|
data-cy="query" />
|
||||||
|
<p class="controls">
|
||||||
|
<b-button native-type="submit" type="is-primary" icon-left="magnify" data-cy="btn-query" />
|
||||||
|
</p>
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<b-table-column v-slot="props" field="username" :label="$t('users.username')" header-class="cy-username" sortable
|
||||||
|
:td-attrs="$utils.tdID">
|
||||||
|
<a :href="`/users/${props.row.id}`" @click.prevent="showEditForm(props.row)"
|
||||||
|
:class="{ 'has-text-grey': props.row.status === 'disabled' }">
|
||||||
|
{{ props.row.username }}
|
||||||
|
</a>
|
||||||
|
</b-table-column>
|
||||||
|
|
||||||
|
<b-table-column v-slot="props" field="status" :label="$t('globals.fields.status')" header-class="cy-status"
|
||||||
|
sortable :td-attrs="$utils.tdID">
|
||||||
|
<b-tag :class="{ 'is-small': true, [props.row.status]: true }">
|
||||||
|
{{ $t(`users.status.${props.row.status}`) }}
|
||||||
|
</b-tag>
|
||||||
|
</b-table-column>
|
||||||
|
<b-table-column v-slot="props" field="name" :label="$t('globals.fields.name')" header-class="cy-name" sortable
|
||||||
|
:td-attrs="$utils.tdID">
|
||||||
|
<div>
|
||||||
|
<a :href="`/users/${props.row.id}`" @click.prevent="showEditForm(props.row)">
|
||||||
|
{{ props.row.name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</b-table-column>
|
||||||
|
|
||||||
|
<b-table-column v-slot="props" field="name" :label="$t('subscribers.email')" header-class="cy-name" sortable
|
||||||
|
:td-attrs="$utils.tdID">
|
||||||
|
<div>
|
||||||
|
<a :href="`/users/${props.row.id}`" @click.prevent="showEditForm(props.row)">
|
||||||
|
{{ props.row.email }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</b-table-column>
|
||||||
|
|
||||||
|
<b-table-column v-slot="props" field="created_at" :label="$t('globals.fields.createdAt')"
|
||||||
|
header-class="cy-created_at" sortable>
|
||||||
|
{{ $utils.niceDate(props.row.createdAt) }}
|
||||||
|
</b-table-column>
|
||||||
|
|
||||||
|
<b-table-column v-slot="props" field="updated_at" :label="$t('globals.fields.updatedAt')"
|
||||||
|
header-class="cy-updated_at" sortable>
|
||||||
|
{{ $utils.niceDate(props.row.updatedAt) }}
|
||||||
|
</b-table-column>
|
||||||
|
|
||||||
|
<b-table-column v-slot="props" field="last_login" :label="$t('users.lastLogin')" header-class="cy-updated_at"
|
||||||
|
sortable>
|
||||||
|
{{ props.row.loggedinAt ? $utils.niceDate(props.row.loggedinAt) : '—' }}
|
||||||
|
</b-table-column>
|
||||||
|
|
||||||
|
<b-table-column v-slot="props" cell-class="actions" align="right">
|
||||||
|
<div>
|
||||||
|
<a href="#" @click.prevent="showEditForm(props.row)" data-cy="btn-edit"
|
||||||
|
:aria-label="$t('globals.buttons.edit')">
|
||||||
|
<b-tooltip :label="$t('globals.buttons.edit')" type="is-dark">
|
||||||
|
<b-icon icon="pencil-outline" size="is-small" />
|
||||||
|
</b-tooltip>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" @click.prevent="deleteUser(props.row)" data-cy="btn-delete"
|
||||||
|
:aria-label="$t('globals.buttons.delete')">
|
||||||
|
<b-tooltip :label="$t('globals.buttons.delete')" type="is-dark">
|
||||||
|
<b-icon icon="trash-can-outline" size="is-small" />
|
||||||
|
</b-tooltip>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</b-table-column>
|
||||||
|
|
||||||
|
<template #empty v-if="!loading.users">
|
||||||
|
<empty-placeholder />
|
||||||
|
</template>
|
||||||
|
</b-table>
|
||||||
|
|
||||||
|
<!-- Add / edit form modal -->
|
||||||
|
<b-modal scroll="keep" :aria-modal="true" :active.sync="isFormVisible" :width="600" @close="onFormClose">
|
||||||
|
<user-form :data="curItem" :is-editing="isEditing" @finished="formFinished" />
|
||||||
|
</b-modal>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { mapState } from 'vuex';
|
||||||
|
import EmptyPlaceholder from '../components/EmptyPlaceholder.vue';
|
||||||
|
import UserForm from './UserForm.vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
components: {
|
||||||
|
EmptyPlaceholder,
|
||||||
|
UserForm,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
curItem: null,
|
||||||
|
isEditing: false,
|
||||||
|
isFormVisible: false,
|
||||||
|
users: [],
|
||||||
|
checked: [],
|
||||||
|
queryParams: {
|
||||||
|
page: 1,
|
||||||
|
query: '',
|
||||||
|
orderBy: 'id',
|
||||||
|
order: 'asc',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onSort(field, direction) {
|
||||||
|
this.queryParams.orderBy = field;
|
||||||
|
this.queryParams.order = direction;
|
||||||
|
this.getUsers();
|
||||||
|
},
|
||||||
|
|
||||||
|
onTableCheck() {
|
||||||
|
// Disable bulk.all selection if there are no rows checked in the table.
|
||||||
|
if (this.bulk.checked.length !== this.subscribers.total) {
|
||||||
|
this.bulk.all = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show the edit form.
|
||||||
|
showEditForm(item) {
|
||||||
|
this.curItem = item;
|
||||||
|
this.isFormVisible = true;
|
||||||
|
this.isEditing = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show the new form.
|
||||||
|
showNewForm() {
|
||||||
|
this.curItem = {};
|
||||||
|
this.isFormVisible = true;
|
||||||
|
this.isEditing = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
formFinished() {
|
||||||
|
this.getUsers();
|
||||||
|
},
|
||||||
|
|
||||||
|
onFormClose() {
|
||||||
|
if (this.$route.params.id) {
|
||||||
|
this.$router.push({ name: 'users' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getUsers() {
|
||||||
|
this.$api.queryUsers({
|
||||||
|
query: this.queryParams.query.replace(/[^\p{L}\p{N}\s]/gu, ' '),
|
||||||
|
order_by: this.queryParams.orderBy,
|
||||||
|
order: this.queryParams.order,
|
||||||
|
}).then((resp) => {
|
||||||
|
this.users = resp;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteUser(item) {
|
||||||
|
this.$utils.confirm(
|
||||||
|
this.$t('globals.messages.confirm'),
|
||||||
|
() => {
|
||||||
|
this.$api.deleteUser(item.id).then(() => {
|
||||||
|
this.getUsers();
|
||||||
|
|
||||||
|
this.$utils.toast(this.$t('globals.messages.deleted', { name: item.name }));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(['loading', 'settings']),
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
if (this.$route.params.id) {
|
||||||
|
this.$api.getUser(parseInt(this.$route.params.id, 10)).then((data) => {
|
||||||
|
this.showEditForm(data);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.getUsers();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
Loading…
Add table
Reference in a new issue