Added frontend
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
<h1>
|
||||
{{'admin.auth_users.header' | translate}}
|
||||
</h1>
|
||||
<div class="content-wrapper">
|
||||
<div class="content">
|
||||
<p-table #dt [value]="users" dataKey="id" editMode="row" [rowHover]="true" [rows]="10"
|
||||
[rowsPerPageOptions]="[10,25,50]" [paginator]="true" [loading]="loading" [totalRecords]="totalRecords"
|
||||
[lazy]="true" (onLazyLoad)="nextPage($event)">
|
||||
|
||||
<ng-template pTemplate="caption">
|
||||
<div class="table-caption">
|
||||
<div class="table-caption-text">
|
||||
<ng-container *ngIf="!loading">{{users.length}} {{'admin.auth_users.of' | translate}}
|
||||
{{dt.totalRecords}}
|
||||
</ng-container>
|
||||
{{'admin.auth_users.users' | translate}}
|
||||
</div>
|
||||
|
||||
<div class="table-caption-btn-wrapper btn-wrapper">
|
||||
<button pButton label="{{'admin.auth_users.add' | translate}}" class="icon-btn btn"
|
||||
icon="pi pi-user-plus" (click)="addUser(dt)" [disabled]="isEditingNew">
|
||||
</button>
|
||||
<button pButton label="{{'admin.auth_users.reset_filters' | translate}}" icon="pi pi-undo"
|
||||
class="icon-btn btn" (click)="resetFilters(dt)">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="firstName">
|
||||
<div class="table-header-label">
|
||||
<div class="table-header-text">{{'admin.auth_users.headers.first_name' | translate}}</div>
|
||||
<p-sortIcon icon="sort" field="firstName" class="table-header-icon"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th pSortableColumn="lastName">
|
||||
<div class="table-header-label">
|
||||
<div class="table-header-text">{{'admin.auth_users.headers.last_name' | translate}}</div>
|
||||
<p-sortIcon field="lastName" class="table-header-icon"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th pSortableColumn="eMail">
|
||||
<div class="table-header-label">
|
||||
<div class="table-header-text">{{'admin.auth_users.headers.e_mail' | translate}}</div>
|
||||
<p-sortIcon field="eMail" class="table-header-icon"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th class="table-header-actions" pSortableColumn="confirmationId">
|
||||
<div class="table-header-label">
|
||||
<div class="table-header-text">{{'admin.auth_users.headers.active' | translate}}</div>
|
||||
<p-sortIcon field="confirmationId" class="table-header-icon"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th class="table-header-small-dropdown" pSortableColumn="authRole">
|
||||
<div class="table-header-label">
|
||||
<div class="table-header-text">{{'admin.auth_users.headers.role' | translate}}</div>
|
||||
<p-sortIcon field="authRole" class="table-header-icon"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th>
|
||||
<div class="table-header-label">
|
||||
<div class="table-header-text">{{'admin.auth_users.headers.password' | translate}}</div>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th class="table-header-actions">
|
||||
<div class="table-header-label">
|
||||
<div class="table-header-text">{{'admin.auth_users.headers.actions' | translate}}</div>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
<form [formGroup]="filterForm">
|
||||
<input type="text" pInputText formControlName="firstName" placeholder="{{'admin.auth_users.headers.first_name' | translate}}">
|
||||
</form>
|
||||
</th>
|
||||
<th>
|
||||
<form [formGroup]="filterForm">
|
||||
<input type="text" pInputText formControlName="lastName" placeholder="{{'admin.auth_users.headers.last_name' | translate}}">
|
||||
</form>
|
||||
</th>
|
||||
<th>
|
||||
<form [formGroup]="filterForm">
|
||||
<input type="email" pInputText formControlName="eMail" placeholder="{{'admin.auth_users.headers.e_mail' | translate}}">
|
||||
</form>
|
||||
</th>
|
||||
<th></th>
|
||||
<th>
|
||||
<form [formGroup]="filterForm">
|
||||
<p-dropdown formControlName="authRole" [options]="authRoles"></p-dropdown>
|
||||
</form>
|
||||
</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-user let-editing="editing" let-ri="rowIndex">
|
||||
<tr [pEditableRow]="user">
|
||||
<td>
|
||||
<p-cellEditor>
|
||||
<ng-template pTemplate="input">
|
||||
<input class="table-edit-input" pInputText type="text" [(ngModel)]="user.firstName"
|
||||
[ngClass]="{ 'invalid-feedback-input': isFirstNameInvalid}">
|
||||
</ng-template>
|
||||
<ng-template pTemplate="output">
|
||||
{{user.firstName}}
|
||||
</ng-template>
|
||||
</p-cellEditor>
|
||||
</td>
|
||||
<td>
|
||||
<p-cellEditor>
|
||||
<ng-template pTemplate="input">
|
||||
<input class="table-edit-input" pInputText type="text" [(ngModel)]="user.lastName"
|
||||
[ngClass]="{ 'invalid-feedback-input': isLastNameInvalid}">
|
||||
</ng-template>
|
||||
<ng-template pTemplate="output">
|
||||
{{user.lastName}}
|
||||
</ng-template>
|
||||
</p-cellEditor>
|
||||
</td>
|
||||
<td>
|
||||
<p-cellEditor>
|
||||
<ng-template pTemplate="input">
|
||||
<input class="table-edit-input" pInputText type="email" [(ngModel)]="user.eMail"
|
||||
[ngClass]="{ 'invalid-feedback-input': isEMailInvalid}">
|
||||
</ng-template>
|
||||
<ng-template pTemplate="output">
|
||||
{{user.eMail}}
|
||||
</ng-template>
|
||||
</p-cellEditor>
|
||||
</td>
|
||||
<td>
|
||||
<p-cellEditor>
|
||||
<ng-template pTemplate="input">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.isConfirmed"
|
||||
[ngClass]="{ 'invalid-feedback-input': isEMailInvalid}" [disabled]="isEditingNew">
|
||||
</p-checkbox>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="output">
|
||||
<p-checkbox [binary]="true" [ngModel]="user.isConfirmed" [disabled]="true">
|
||||
</p-checkbox>
|
||||
</ng-template>
|
||||
</p-cellEditor>
|
||||
</td>
|
||||
<td>
|
||||
<p-cellEditor>
|
||||
<ng-template pTemplate="input">
|
||||
<p-dropdown [options]="authRoles" [(ngModel)]="user.authRole"
|
||||
[disabled]="user.eMail == loggedInUserEMail"></p-dropdown>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="output">
|
||||
{{user.authRole | authRole}}
|
||||
</ng-template>
|
||||
</p-cellEditor>
|
||||
</td>
|
||||
<td>
|
||||
<p-cellEditor>
|
||||
<ng-template pTemplate="input">
|
||||
<input class="table-edit-input" pInputText type="password" [(ngModel)]="user.password"
|
||||
[ngClass]="{ 'invalid-feedback-input': isPasswordInvalid}">
|
||||
</ng-template>
|
||||
<ng-template pTemplate="output">
|
||||
</ng-template>
|
||||
</p-cellEditor>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-wrapper">
|
||||
<button *ngIf="!editing" pButton pInitEditableRow class="btn icon-btn" icon="pi pi-pencil"
|
||||
(click)="onRowEditInit(dt, user, ri)"></button>
|
||||
<button *ngIf="!editing" pButton class="btn icon-btn danger-icon-btn" icon="pi pi-trash"
|
||||
(click)="deleteUser(user)"></button>
|
||||
|
||||
<button *ngIf="editing" pButton pSaveEditableRow class="btn icon-btn"
|
||||
icon="pi pi-check-circle" (click)="onRowEditSave(dt, user, ri)"></button>
|
||||
<button *ngIf="editing" pButton pCancelEditableRow class="btn icon-btn danger-icon-btn"
|
||||
icon="pi pi-times-circle" (click)="onRowEditCancel(user, ri)"></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="Invalidmessage">
|
||||
<tr>
|
||||
<td colspan="7">{{'admin.auth_users.no_entries_found' | translate}}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="paginatorleft">
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AuthUserComponent } from './auth-user.component';
|
||||
|
||||
describe('AuthUserComponent', () => {
|
||||
let component: AuthUserComponent;
|
||||
let fixture: ComponentFixture<AuthUserComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AuthUserComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AuthUserComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,325 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { catchError, debounceTime, last } from 'rxjs/operators';
|
||||
import { AuthRoles } from 'src/app/models/auth/auth-roles.enum';
|
||||
import { AuthUserDTO } from 'src/app/models/auth/auth-user.dto';
|
||||
import { AuthService } from 'src/app/services/auth/auth.service';
|
||||
import { ConfirmationDialogService } from 'src/app/services/confirmation-dialog/confirmation-dialog.service';
|
||||
import { SpinnerService } from 'src/app/services/spinner/spinner.service';
|
||||
import { ToastService } from 'src/app/services/toast/toast.service';
|
||||
import { Table } from 'primeng/table';
|
||||
import { ServiceErrorCode } from 'src/app/models/error/service-error-code.enum';
|
||||
import { RegisterErrorMessages } from 'src/app/models/auth/register-error-messages.enum';
|
||||
import { ErrorDTO } from 'src/app/models/error/error-dto';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { AuthUserSelectCriterion } from 'src/app/models/selection/auth-user/auth-user-select-criterion.dto';
|
||||
import { LazyLoadEvent } from 'primeng/api';
|
||||
import { throwError } from 'rxjs';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-auth-user',
|
||||
templateUrl: './auth-user.component.html',
|
||||
styleUrls: ['./auth-user.component.scss']
|
||||
})
|
||||
export class AuthUserComponent implements OnInit {
|
||||
|
||||
users: AuthUserDTO[];
|
||||
statuses: any[];
|
||||
loading = true;
|
||||
activityValues: number[] = [0, 100];
|
||||
|
||||
clonedUsers: { [s: string]: AuthUserDTO; } = {};
|
||||
isEditingNew: boolean = false;
|
||||
|
||||
authRoles = [
|
||||
{ label: AuthRoles[AuthRoles.Normal].toString(), value: AuthRoles.Normal },
|
||||
{ label: AuthRoles[AuthRoles.Admin].toString(), value: AuthRoles.Admin }
|
||||
]
|
||||
|
||||
newUserTemplate: AuthUserDTO = {
|
||||
id: null,
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
eMail: "",
|
||||
password: "",
|
||||
authRole: AuthRoles.Normal
|
||||
};
|
||||
|
||||
isFirstNameInvalid: boolean = false;
|
||||
isLastNameInvalid: boolean = false;
|
||||
isEMailInvalid: boolean = false;
|
||||
isPasswordInvalid: boolean = false;
|
||||
|
||||
loggedInUserEMail: string = "";
|
||||
|
||||
filterForm: FormGroup;
|
||||
searchCriterions: AuthUserSelectCriterion;
|
||||
totalRecords: number;
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private spinnerService: SpinnerService,
|
||||
private toastService: ToastService,
|
||||
private confirmDialog: ConfirmationDialogService,
|
||||
private fb: FormBuilder,
|
||||
private translate: TranslateService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loggedInUserEMail = this.authService.getEMailFromDecodedToken(this.authService.getDecodedToken());
|
||||
this.searchCriterions = {
|
||||
firstName: null,
|
||||
lastName: null,
|
||||
eMail: null,
|
||||
authRole: null,
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
sortColumn: null,
|
||||
sortDirection: null
|
||||
};
|
||||
|
||||
this.setFilterForm();
|
||||
this.loadNextPage();
|
||||
}
|
||||
|
||||
setFilterForm() {
|
||||
this.filterForm = this.fb.group({
|
||||
firstName: [null],
|
||||
lastName: [null],
|
||||
eMail: [null],
|
||||
authRole: [null]
|
||||
});
|
||||
|
||||
this.filterForm.valueChanges.pipe(
|
||||
debounceTime(600)
|
||||
).subscribe(changes => {
|
||||
if (changes.firstName) {
|
||||
this.searchCriterions.firstName = changes.firstName;
|
||||
} else {
|
||||
this.searchCriterions.firstName = undefined;
|
||||
}
|
||||
|
||||
if (changes.lastName) {
|
||||
this.searchCriterions.lastName = changes.lastName;
|
||||
} else {
|
||||
this.searchCriterions.lastName = undefined;
|
||||
}
|
||||
|
||||
if (changes.eMail) {
|
||||
this.searchCriterions.eMail = changes.eMail;
|
||||
} else {
|
||||
this.searchCriterions.eMail = undefined;
|
||||
}
|
||||
|
||||
if (changes.authRole != null) {
|
||||
this.searchCriterions.authRole = changes.authRole;
|
||||
} else {
|
||||
this.searchCriterions.authRole = undefined;
|
||||
}
|
||||
|
||||
if (this.searchCriterions.pageSize)
|
||||
this.searchCriterions.pageSize = 10;
|
||||
|
||||
if (this.searchCriterions.pageSize)
|
||||
this.searchCriterions.pageIndex = 0;
|
||||
|
||||
this.loadNextPage();
|
||||
});
|
||||
}
|
||||
|
||||
loadNextPage() {
|
||||
this.authService.getFilteredUsers(this.searchCriterions).pipe(catchError(err => {
|
||||
this.loading = false;
|
||||
return throwError(err);
|
||||
})).subscribe(list => {
|
||||
this.totalRecords = list.totalCount;
|
||||
this.users = list.users;
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
nextPage(event: LazyLoadEvent) {
|
||||
this.searchCriterions.pageSize = event.rows;
|
||||
if (event.first != null && event.rows != null)
|
||||
this.searchCriterions.pageIndex = event.first / event.rows;
|
||||
this.searchCriterions.sortColumn = event.sortField;
|
||||
this.searchCriterions.sortDirection = event.sortOrder === 1 ? 'asc' : event.sortOrder === -1 ? 'desc' : 'asc';
|
||||
|
||||
if (event.filters) {
|
||||
// + "" => convert to string
|
||||
this.searchCriterions.firstName = event.filters.firstName ? event.filters.firstName + "" : undefined;
|
||||
this.searchCriterions.lastName = event.filters.lastName ? event.filters.lastName + "" : undefined;
|
||||
this.searchCriterions.eMail = event.filters.eMail ? event.filters.eMail + "" : undefined;
|
||||
this.searchCriterions.authRole = event.filters.authRole ? +event.filters.authRole : undefined;
|
||||
}
|
||||
|
||||
this.loadNextPage();
|
||||
}
|
||||
|
||||
resetFilters(table: Table) {
|
||||
this.filterForm.reset();
|
||||
}
|
||||
|
||||
initUserList(): void {
|
||||
this.spinnerService.showSpinner();
|
||||
this.authService.getAllUsers()
|
||||
.pipe(catchError(err => {
|
||||
this.spinnerService.hideSpinner();
|
||||
throw err;
|
||||
}))
|
||||
.subscribe(users => {
|
||||
this.users = users;
|
||||
this.spinnerService.hideSpinner();
|
||||
});
|
||||
}
|
||||
|
||||
onRowEditInit(table: Table, user: AuthUserDTO, index: number) {
|
||||
this.clonedUsers[index] = { ...user };
|
||||
}
|
||||
|
||||
onRowEditSave(table: Table, newUser: AuthUserDTO, index: number) {
|
||||
const oldUser = this.clonedUsers[index];
|
||||
delete this.clonedUsers[index];
|
||||
|
||||
if (JSON.stringify(oldUser) === JSON.stringify(newUser) && !this.isEditingNew) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isEditingNew && JSON.stringify(newUser) === JSON.stringify(this.newUserTemplate)) {
|
||||
this.isEditingNew = false;
|
||||
this.users.splice(index, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isFirstNameInvalid = newUser.firstName == "";
|
||||
this.isLastNameInvalid = newUser.lastName == "";
|
||||
this.isEMailInvalid = newUser.eMail == "";
|
||||
this.isPasswordInvalid = newUser.password == "";
|
||||
|
||||
if (
|
||||
this.isEditingNew && (
|
||||
newUser.firstName == "" ||
|
||||
newUser.lastName == "" ||
|
||||
newUser.eMail == ""
|
||||
)
|
||||
) {
|
||||
table.initRowEdit(newUser);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isEditingNew) {
|
||||
this.spinnerService.showSpinner();
|
||||
this.authService.register(newUser).pipe(catchError(error => {
|
||||
this.spinnerService.hideSpinner();
|
||||
|
||||
if (error.error !== null) {
|
||||
const err: ErrorDTO = error.error;
|
||||
|
||||
if (err.errorCode === ServiceErrorCode.InvalidData && err.message === RegisterErrorMessages.InvalidEMail) {
|
||||
this.isEMailInvalid = true;
|
||||
this.toastService.error(this.translate.instant('admin.auth_users.message.invalid_email'), this.translate.instant('admin.auth_users.message.invalid_email_d', { eMail: newUser.eMail }));
|
||||
} else if (err.errorCode === ServiceErrorCode.InvalidUser && err.message === RegisterErrorMessages.UserAlreadyExists) {
|
||||
this.isEMailInvalid = true;
|
||||
this.toastService.error(this.translate.instant('admin.auth_users.message.user_already_exists'), this.translate.instant('admin.auth_users.message.user_already_exists_d', { eMail: newUser.eMail }));
|
||||
}
|
||||
error.error = null;
|
||||
table.initRowEdit(newUser);
|
||||
}
|
||||
this.spinnerService.hideSpinner();
|
||||
|
||||
throw error;
|
||||
}))
|
||||
.subscribe(_ => {
|
||||
this.initUserList();
|
||||
this.spinnerService.hideSpinner();
|
||||
this.toastService.success(this.translate.instant('admin.auth_users.message.user_added'), this.translate.instant('admin.auth_users.message.user_added_d', { eMail: newUser.eMail }));
|
||||
this.isEditingNew = false;
|
||||
});
|
||||
this.triggerUserChangeDetection();
|
||||
return;
|
||||
}
|
||||
|
||||
this.spinnerService.showSpinner();
|
||||
this.authService.updateUserAsAdmin({
|
||||
authUserDTO: oldUser,
|
||||
newAuthUserDTO: newUser,
|
||||
changePassword: newUser.password != ""
|
||||
}).pipe(catchError(err => {
|
||||
this.spinnerService.hideSpinner();
|
||||
this.toastService.error(this.translate.instant('admin.auth_users.message.user_change_failed'), this.translate.instant('admin.auth_users.message.user_change_failed_d', { eMail: newUser.eMail }));
|
||||
this.initUserList();
|
||||
throw err;
|
||||
}))
|
||||
.subscribe(_ => {
|
||||
this.initUserList();
|
||||
this.spinnerService.hideSpinner();
|
||||
this.toastService.success(this.translate.instant('admin.auth_users.message.user_changed'), this.translate.instant('admin.auth_users.message.user_changed_d', { eMail: newUser.eMail }));
|
||||
});
|
||||
this.triggerUserChangeDetection();
|
||||
}
|
||||
|
||||
onRowEditCancel(user: AuthUserDTO, index: number) {
|
||||
this.isFirstNameInvalid = false;
|
||||
this.isLastNameInvalid = false;
|
||||
this.isEMailInvalid = false;
|
||||
this.isPasswordInvalid = false;
|
||||
|
||||
if (this.isEditingNew) {
|
||||
this.users.splice(index, 1);
|
||||
this.triggerUserChangeDetection();
|
||||
delete this.clonedUsers[index];
|
||||
this.isEditingNew = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.users[index] = this.clonedUsers[index];
|
||||
this.triggerUserChangeDetection();
|
||||
delete this.clonedUsers[index];
|
||||
}
|
||||
|
||||
deleteUser(user: AuthUserDTO) {
|
||||
if (user.eMail == this.loggedInUserEMail) {
|
||||
this.toastService.error(this.translate.instant('admin.auth_users.message.cannot_delete_user'), this.translate.instant('admin.auth_users.message.logon_with_another_user'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.confirmDialog.confirmDialog(
|
||||
this.translate.instant('admin.auth_users.message.user_delete'), this.translate.instant('admin.auth_users.message.user_delete_q', { eMail: user.eMail }),
|
||||
() => {
|
||||
this.spinnerService.showSpinner();
|
||||
this.authService.deleteUserByMail(user.eMail)
|
||||
.pipe(catchError(err => {
|
||||
this.spinnerService.hideSpinner();
|
||||
throw err;
|
||||
}))
|
||||
.subscribe(_ => {
|
||||
this.initUserList();
|
||||
this.spinnerService.hideSpinner();
|
||||
this.toastService.success(this.translate.instant('admin.auth_users.message.user_deleted'), this.translate.instant('admin.auth_users.message.user_deleted_d', { eMail: user.eMail }));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addUser(table: Table) {
|
||||
const newUser = JSON.parse(JSON.stringify(this.newUserTemplate));
|
||||
newUser.id = Math.max.apply(Math, this.users.map(function (u) { return u.id; })) + 1;
|
||||
console.log(newUser);
|
||||
|
||||
this.users.push(newUser);
|
||||
this.triggerUserChangeDetection();
|
||||
|
||||
table.initRowEdit(newUser);
|
||||
|
||||
const index = this.users.findIndex(u => u.eMail == newUser.eMail);
|
||||
this.onRowEditInit(table, newUser, index);
|
||||
|
||||
this.isEditingNew = true;
|
||||
}
|
||||
|
||||
triggerUserChangeDetection() {
|
||||
// trigger change detection (https://github.com/primefaces/primeng/issues/2219)
|
||||
this.users = this.users.slice();
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user