mirror of https://github.com/Chocobozzz/PeerTube
Add bulk actions in users table
parent
80c7336a89
commit
791645e620
|
@ -10,10 +10,31 @@
|
|||
<p-table
|
||||
[value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
|
||||
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
|
||||
[(selection)]="selectedUsers"
|
||||
>
|
||||
<ng-template pTemplate="caption">
|
||||
<div class="caption">
|
||||
<div>
|
||||
<my-action-dropdown
|
||||
*ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange"
|
||||
[actions]="bulkUserActions" [entry]="selectedUsers"
|
||||
>
|
||||
</my-action-dropdown>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th style="width: 40px"></th>
|
||||
<th style="width: 40px">
|
||||
</th>
|
||||
<th i18n pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th>
|
||||
<th i18n>Email</th>
|
||||
<th i18n>Video quota</th>
|
||||
|
@ -25,12 +46,17 @@
|
|||
|
||||
<ng-template pTemplate="body" let-expanded="expanded" let-user>
|
||||
|
||||
<tr [ngClass]="{ banned: user.blocked }">
|
||||
<tr [pSelectableRow]="user" [ngClass]="{ banned: user.blocked }">
|
||||
<td>
|
||||
<p-tableCheckbox [value]="user"></p-tableCheckbox>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span *ngIf="user.blockedReason" class="expander" [pRowToggler]="user">
|
||||
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{{ user.username }}
|
||||
<span *ngIf="user.blocked" class="banned-info">(banned)</span>
|
||||
|
@ -40,7 +66,7 @@
|
|||
<td>{{ user.roleLabel }}</td>
|
||||
<td>{{ user.createdAt }}</td>
|
||||
<td class="action-cell">
|
||||
<my-user-moderation-dropdown [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()">
|
||||
<my-user-moderation-dropdown *ngIf="!isInSelectionMode()" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()">
|
||||
</my-user-moderation-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -56,3 +82,4 @@
|
|||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
<my-user-ban-modal #userBanModal (userBanned)="onUsersBanned()"></my-user-ban-modal>
|
||||
|
|
|
@ -16,3 +16,14 @@ tr.banned {
|
|||
.ban-reason-label {
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
|
||||
.caption {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
@include peertube-input-text(250px);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { SortMeta } from 'primeng/components/common/sortmeta'
|
||||
import { ConfirmService } from '../../../core'
|
||||
import { RestPagination, RestTable, UserService } from '../../../shared'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { User } from '../../../../../../shared'
|
||||
import { UserBanModalComponent } from '@app/shared/moderation'
|
||||
import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-user-list',
|
||||
|
@ -12,12 +14,17 @@ import { User } from '../../../../../../shared'
|
|||
styleUrls: [ './user-list.component.scss' ]
|
||||
})
|
||||
export class UserListComponent extends RestTable implements OnInit {
|
||||
@ViewChild('userBanModal') userBanModal: UserBanModalComponent
|
||||
|
||||
users: User[] = []
|
||||
totalRecords = 0
|
||||
rowsPerPage = 10
|
||||
sort: SortMeta = { field: 'createdAt', order: 1 }
|
||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
|
||||
selectedUsers: User[] = []
|
||||
bulkUserActions: DropdownAction<User>[] = []
|
||||
|
||||
constructor (
|
||||
private notificationsService: NotificationsService,
|
||||
private confirmService: ConfirmService,
|
||||
|
@ -29,13 +36,28 @@ export class UserListComponent extends RestTable implements OnInit {
|
|||
|
||||
ngOnInit () {
|
||||
this.loadSort()
|
||||
}
|
||||
|
||||
onUserChanged () {
|
||||
this.loadData()
|
||||
this.bulkUserActions = [
|
||||
{
|
||||
label: this.i18n('Delete'),
|
||||
handler: users => this.removeUsers(users)
|
||||
},
|
||||
{
|
||||
label: this.i18n('Ban'),
|
||||
handler: users => this.openBanUserModal(users),
|
||||
isDisplayed: users => users.every(u => u.blocked === false)
|
||||
},
|
||||
{
|
||||
label: this.i18n('Unban'),
|
||||
handler: users => this.unbanUsers(users),
|
||||
isDisplayed: users => users.every(u => u.blocked === true)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
protected loadData () {
|
||||
this.selectedUsers = []
|
||||
|
||||
this.userService.getUsers(this.pagination, this.sort)
|
||||
.subscribe(
|
||||
resultList => {
|
||||
|
@ -46,4 +68,67 @@ export class UserListComponent extends RestTable implements OnInit {
|
|||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
)
|
||||
}
|
||||
|
||||
openBanUserModal (users: User[]) {
|
||||
for (const user of users) {
|
||||
if (user.username === 'root') {
|
||||
this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot ban root.'))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.userBanModal.openModal(users)
|
||||
}
|
||||
|
||||
onUsersBanned () {
|
||||
this.loadData()
|
||||
}
|
||||
|
||||
async unbanUsers (users: User[]) {
|
||||
const message = this.i18n('Do you really want to unban {{num}} users?', { num: users.length })
|
||||
|
||||
const res = await this.confirmService.confirm(message, this.i18n('Unban'))
|
||||
if (res === false) return
|
||||
|
||||
this.userService.unbanUsers(users)
|
||||
.subscribe(
|
||||
() => {
|
||||
const message = this.i18n('{{num}} users unbanned.', { num: users.length })
|
||||
|
||||
this.notificationsService.success(this.i18n('Success'), message)
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
)
|
||||
}
|
||||
|
||||
async removeUsers (users: User[]) {
|
||||
for (const user of users) {
|
||||
if (user.username === 'root') {
|
||||
this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot delete root.'))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const message = this.i18n('If you remove these users, you will not be able to create others with the same username!')
|
||||
const res = await this.confirmService.confirm(message, this.i18n('Delete'))
|
||||
if (res === false) return
|
||||
|
||||
this.userService.removeUser(users).subscribe(
|
||||
() => {
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('{{num}} users deleted.', { num: users.length })
|
||||
)
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
)
|
||||
}
|
||||
|
||||
isInSelectionMode () {
|
||||
return this.selectedUsers.length !== 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<div class="dropdown-root" ngbDropdown [placement]="placement">
|
||||
<div class="action-button" [ngClass]="{ small: buttonSize === 'small' }" ngbDropdownToggle role="button">
|
||||
<span class="icon icon-action"></span>
|
||||
<div
|
||||
class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange' }"
|
||||
ngbDropdownToggle role="button"
|
||||
>
|
||||
<span *ngIf="!label" class="icon icon-action"></span>
|
||||
<span *ngIf="label" class="dropdown-toggle">{{ label }}</span>
|
||||
</div>
|
||||
|
||||
<div ngbDropdownMenu class="dropdown-menu">
|
||||
|
|
|
@ -3,7 +3,14 @@
|
|||
|
||||
.action-button {
|
||||
@include peertube-button;
|
||||
|
||||
&.grey {
|
||||
@include grey-button;
|
||||
}
|
||||
|
||||
&.orange {
|
||||
@include orange-button;
|
||||
}
|
||||
|
||||
display: inline-block;
|
||||
padding: 0 10px;
|
||||
|
@ -30,6 +37,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
.dropdown-item {
|
||||
cursor: pointer;
|
||||
|
|
|
@ -16,6 +16,8 @@ export type DropdownAction<T> = {
|
|||
export class ActionDropdownComponent<T> {
|
||||
@Input() actions: DropdownAction<T>[] = []
|
||||
@Input() entry: T
|
||||
@Input() placement = 'left'
|
||||
@Input() placement = 'bottom-left'
|
||||
@Input() buttonSize: 'normal' | 'small' = 'normal'
|
||||
@Input() label: string
|
||||
@Input() theme: 'orange' | 'grey' = 'grey'
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<ng-template #modal>
|
||||
<div class="modal-header">
|
||||
<h4 i18n class="modal-title">Ban {{ userToBan.username }}</h4>
|
||||
<h4 i18n class="modal-title">Ban</h4>
|
||||
<span class="close" aria-hidden="true" (click)="hideBanUserModal()"></span>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -15,9 +15,9 @@ import { User } from '../../../../../shared'
|
|||
})
|
||||
export class UserBanModalComponent extends FormReactive implements OnInit {
|
||||
@ViewChild('modal') modal: NgbModal
|
||||
@Output() userBanned = new EventEmitter<User>()
|
||||
@Output() userBanned = new EventEmitter<User | User[]>()
|
||||
|
||||
private userToBan: User
|
||||
private usersToBan: User | User[]
|
||||
private openedModal: NgbModalRef
|
||||
|
||||
constructor (
|
||||
|
@ -37,28 +37,29 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
|
|||
})
|
||||
}
|
||||
|
||||
openModal (user: User) {
|
||||
this.userToBan = user
|
||||
openModal (user: User | User[]) {
|
||||
this.usersToBan = user
|
||||
this.openedModal = this.modalService.open(this.modal)
|
||||
}
|
||||
|
||||
hideBanUserModal () {
|
||||
this.userToBan = undefined
|
||||
this.usersToBan = undefined
|
||||
this.openedModal.close()
|
||||
}
|
||||
|
||||
async banUser () {
|
||||
const reason = this.form.value['reason'] || undefined
|
||||
|
||||
this.userService.banUser(this.userToBan, reason)
|
||||
this.userService.banUsers(this.usersToBan, reason)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('User {{username}} banned.', { username: this.userToBan.username })
|
||||
)
|
||||
const message = Array.isArray(this.usersToBan)
|
||||
? this.i18n('{{num}} users banned.', { num: this.usersToBan.length })
|
||||
: this.i18n('User {{username}} banned.', { username: this.usersToBan.username })
|
||||
|
||||
this.userBanned.emit(this.userToBan)
|
||||
this.notificationsService.success(this.i18n('Success'), message)
|
||||
|
||||
this.userBanned.emit(this.usersToBan)
|
||||
this.hideBanUserModal()
|
||||
},
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<ng-container *ngIf="user && userActions.length !== 0">
|
||||
<my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal>
|
||||
|
||||
<my-action-dropdown i18n-label label="Actions" [actions]="userActions" [entry]="user" [buttonSize]="buttonSize"></my-action-dropdown>
|
||||
<my-action-dropdown [actions]="userActions" [entry]="user" [buttonSize]="buttonSize"></my-action-dropdown>
|
||||
</ng-container>
|
|
@ -2,7 +2,6 @@ import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angu
|
|||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
|
||||
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
||||
import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component'
|
||||
import { UserService } from '@app/shared/users'
|
||||
import { AuthService, ConfirmService } from '@app/core'
|
||||
|
@ -24,8 +23,6 @@ export class UserModerationDropdownComponent implements OnInit {
|
|||
|
||||
userActions: DropdownAction<User>[] = []
|
||||
|
||||
private openedModal: NgbModalRef
|
||||
|
||||
constructor (
|
||||
private authService: AuthService,
|
||||
private notificationsService: NotificationsService,
|
||||
|
@ -38,10 +35,6 @@ export class UserModerationDropdownComponent implements OnInit {
|
|||
this.buildActions()
|
||||
}
|
||||
|
||||
hideBanUserModal () {
|
||||
this.openedModal.close()
|
||||
}
|
||||
|
||||
openBanUserModal (user: User) {
|
||||
if (user.username === 'root') {
|
||||
this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot ban root.'))
|
||||
|
@ -60,7 +53,7 @@ export class UserModerationDropdownComponent implements OnInit {
|
|||
const res = await this.confirmService.confirm(message, this.i18n('Unban'))
|
||||
if (res === false) return
|
||||
|
||||
this.userService.unbanUser(user)
|
||||
this.userService.unbanUsers(user)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.notificationsService.success(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Observable } from 'rxjs'
|
||||
import { catchError, map } from 'rxjs/operators'
|
||||
import { from, Observable } from 'rxjs'
|
||||
import { catchError, concatMap, map, toArray } from 'rxjs/operators'
|
||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ResultList, User, UserCreate, UserRole, UserUpdate, UserUpdateMe, UserVideoQuota } from '../../../../../shared'
|
||||
|
@ -170,21 +170,38 @@ export class UserService {
|
|||
)
|
||||
}
|
||||
|
||||
removeUser (user: { id: number }) {
|
||||
return this.authHttp.delete(UserService.BASE_USERS_URL + user.id)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
removeUser (usersArg: User | User[]) {
|
||||
const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
|
||||
|
||||
return from(users)
|
||||
.pipe(
|
||||
concatMap(u => this.authHttp.delete(UserService.BASE_USERS_URL + u.id)),
|
||||
toArray(),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
}
|
||||
|
||||
banUser (user: { id: number }, reason?: string) {
|
||||
banUsers (usersArg: User | User[], reason?: string) {
|
||||
const body = reason ? { reason } : {}
|
||||
const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
|
||||
|
||||
return this.authHttp.post(UserService.BASE_USERS_URL + user.id + '/block', body)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
return from(users)
|
||||
.pipe(
|
||||
concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/block', body)),
|
||||
toArray(),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
}
|
||||
|
||||
unbanUser (user: { id: number }) {
|
||||
return this.authHttp.post(UserService.BASE_USERS_URL + user.id + '/unblock', {})
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
unbanUsers (usersArg: User | User[]) {
|
||||
const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
|
||||
|
||||
return from(users)
|
||||
.pipe(
|
||||
concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/unblock', {})),
|
||||
toArray(),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
}
|
||||
|
||||
private formatUser (user: User) {
|
||||
|
|
|
@ -14,8 +14,11 @@
|
|||
p-table {
|
||||
font-size: 15px !important;
|
||||
|
||||
.ui-table-caption {
|
||||
border: none;
|
||||
}
|
||||
|
||||
td {
|
||||
// border: 1px solid #E5E5E5 !important;
|
||||
padding-left: 15px !important;
|
||||
|
||||
&:not(.action-cell) {
|
||||
|
@ -28,6 +31,11 @@ p-table {
|
|||
tr {
|
||||
background-color: var(--mainBackgroundColor) !important;
|
||||
height: 46px;
|
||||
|
||||
&.ui-state-highlight {
|
||||
background-color:var(--submenuColor) !important;
|
||||
color:var(--mainForegroundColor) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-table-tbody {
|
||||
|
@ -217,3 +225,31 @@ p-calendar .ui-datepicker {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-chkbox-box {
|
||||
&.ui-state-active {
|
||||
border-color: var(--mainColor) !important;
|
||||
background-color: var(--mainColor) !important;
|
||||
}
|
||||
|
||||
.ui-chkbox-icon {
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
width: 5px;
|
||||
height: 12px;
|
||||
opacity: 0;
|
||||
transform: rotate(45deg) scale(0);
|
||||
border-right: 2px solid var(--mainBackgroundColor);
|
||||
border-bottom: 2px solid var(--mainBackgroundColor);
|
||||
}
|
||||
|
||||
&.pi-check:after {
|
||||
opacity: 1;
|
||||
transform: rotate(45deg) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue