<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>
<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>
<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>
</ng-template>
</p-table>
+<my-user-ban-modal #userBanModal (userBanned)="onUsersBanned()"></my-user-ban-modal>
.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);
+ }
}
\ No newline at end of file
-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',
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,
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 => {
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
+ }
}
<input
- type="text" id="search-video" name="search-video" i18n-placeholder placeholder="Search..."
- [(ngModel)]="searchValue" (keyup.enter)="doSearch()"
+ type="text" id="search-video" name="search-video" i18n-placeholder placeholder="Search..."
+ [(ngModel)]="searchValue" (keyup.enter)="doSearch()"
>
<span (click)="doSearch()" class="icon icon-search"></span>
<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">
.action-button {
@include peertube-button;
- @include grey-button;
+
+ &.grey {
+ @include grey-button;
+ }
+
+ &.orange {
+ @include orange-button;
+ }
display: inline-block;
padding: 0 10px;
}
}
+.dropdown-toggle::after {
+ position: relative;
+ top: 1px;
+}
+
.dropdown-menu {
.dropdown-item {
cursor: pointer;
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'
}
<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>
})
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 (
})
}
- 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()
},
<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>
\ No newline at end of file
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'
userActions: DropdownAction<User>[] = []
- private openedModal: NgbModalRef
-
constructor (
private authService: AuthService,
private notificationsService: NotificationsService,
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.'))
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(
-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'
)
}
- 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) {
p-table {
font-size: 15px !important;
+ .ui-table-caption {
+ border: none;
+ }
+
td {
- // border: 1px solid #E5E5E5 !important;
padding-left: 15px !important;
&:not(.action-cell) {
tr {
background-color: var(--mainBackgroundColor) !important;
height: 46px;
+
+ &.ui-state-highlight {
+ background-color:var(--submenuColor) !important;
+ color:var(--mainForegroundColor) !important;
+ }
}
.ui-table-tbody {
@include glyphicon-light;
}
}
+}
+
+.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);
+ }
+ }
}
\ No newline at end of file