From a4d0d15f6507a83b9e7847832ce861059d7d752a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adina=20=C8=9Aeudan?= Date: Tue, 6 Apr 2021 01:17:56 +0300 Subject: [PATCH 1/5] User management WIP --- .../src/app/modules/admin/admin.module.ts | 10 +- .../add-edit-user-dialog.component.html | 56 ++++++++ .../add-edit-user-dialog.component.scss | 7 + .../add-edit-user-dialog.component.ts | 124 ++++++++++++++++++ ...elete-file-attribute-dialog.component.scss | 8 +- .../confirm-delete-user-dialog.component.html | 19 +++ .../confirm-delete-user-dialog.component.scss | 17 +++ .../confirm-delete-user-dialog.component.ts | 38 ++++++ .../user-listing-screen.component.html | 44 ++++++- .../user-listing-screen.component.scss | 19 ++- .../user-listing-screen.component.ts | 64 +++++++-- .../services/admin-dialog-service.service.ts | 37 +++++- .../initials-avatar.component.html | 2 +- .../initials-avatar.component.scss | 4 + .../initials-avatar.component.ts | 35 +++-- .../src/app/services/notification.service.ts | 4 +- apps/red-ui/src/app/services/user.service.ts | 14 ++ .../red-ui/src/app/state/app-state.service.ts | 1 - apps/red-ui/src/assets/i18n/en.json | 47 ++++++- apps/red-ui/src/assets/styles/red-button.scss | 1 - .../src/assets/styles/red-checkbox.scss | 1 - .../src/assets/styles/red-components.scss | 5 + apps/red-ui/src/assets/styles/red-input.scss | 5 + apps/red-ui/src/assets/styles/red-list.scss | 1 - .../src/assets/styles/red-material-theme.scss | 6 + apps/red-ui/src/assets/styles/red-menu.scss | 1 - .../src/assets/styles/red-page-layout.scss | 2 +- apps/red-ui/src/assets/styles/red-select.scss | 2 - apps/red-ui/src/assets/styles/red-toggle.scss | 4 - .../src/assets/styles/red-tooltips.scss | 1 - 30 files changed, 525 insertions(+), 54 deletions(-) create mode 100644 apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.html create mode 100644 apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.scss create mode 100644 apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.ts create mode 100644 apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html create mode 100644 apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.scss create mode 100644 apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.ts diff --git a/apps/red-ui/src/app/modules/admin/admin.module.ts b/apps/red-ui/src/app/modules/admin/admin.module.ts index ddb76c6fd..62f42167a 100644 --- a/apps/red-ui/src/app/modules/admin/admin.module.ts +++ b/apps/red-ui/src/app/modules/admin/admin.module.ts @@ -28,6 +28,8 @@ import { NgxChartsModule } from '@swimlane/ngx-charts'; import { AdminDialogService } from './services/admin-dialog-service.service'; import { SmtpConfigScreenComponent } from './screens/smtp-config/smtp-config-screen.component'; import { SmtpAuthDialogComponent } from './dialogs/smtp-auth-dialog/smtp-auth-dialog.component'; +import { AddEditUserDialogComponent } from './dialogs/add-edit-user-dialog/add-edit-user-dialog.component'; +import { ConfirmDeleteUserDialogComponent } from './dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component'; const dialogs = [ AddEditRuleSetDialogComponent, @@ -35,7 +37,9 @@ const dialogs = [ AddEditFileAttributeDialogComponent, ConfirmDeleteFileAttributeDialogComponent, EditColorDialogComponent, - SmtpAuthDialogComponent + SmtpAuthDialogComponent, + AddEditUserDialogComponent, + ConfirmDeleteUserDialogComponent ]; const screens = [ @@ -49,7 +53,8 @@ const screens = [ FileAttributesListingScreenComponent, LicenseInformationScreenComponent, UserListingScreenComponent, - WatermarkScreenComponent + WatermarkScreenComponent, + SmtpConfigScreenComponent ]; const components = [ @@ -58,7 +63,6 @@ const components = [ TabsComponent, ComboChartComponent, ComboSeriesVerticalComponent, - SmtpConfigScreenComponent, ...dialogs, ...screens ]; diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.html b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.html new file mode 100644 index 000000000..f06139aa1 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.html @@ -0,0 +1,56 @@ +
+
+ {{ (user ? 'add-edit-user.title.edit' : 'add-edit-user.title.new') | translate }} +
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + + + + +
+ +
+ + {{ 'roles.' + role | translate }} + +
+
+
+ +
+ + + + +
+
+
+ + +
diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.scss b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.scss new file mode 100644 index 000000000..65fd1e545 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.scss @@ -0,0 +1,7 @@ +.roles-wrapper { + display: grid; + grid-template-columns: 1fr 1fr; + grid-row-gap: 8px; + margin-top: 8px; + width: 300px; +} diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.ts new file mode 100644 index 000000000..4a18b6705 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.ts @@ -0,0 +1,124 @@ +import { Component, Inject } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { User, UserControllerService } from '@redaction/red-ui-http'; +import { UserService } from '../../../../services/user.service'; +import { MatCheckboxChange } from '@angular/material/checkbox'; +import { AdminDialogService } from '../../services/admin-dialog-service.service'; + +@Component({ + selector: 'redaction-add-edit-user-dialog', + templateUrl: './add-edit-user-dialog.component.html', + styleUrls: ['./add-edit-user-dialog.component.scss'] +}) +export class AddEditUserDialogComponent { + public userForm: FormGroup; + public ROLES = ['RED_USER', 'RED_MANAGER', 'RED_USER_ADMIN', 'RED_ADMIN']; + + constructor( + private readonly _formBuilder: FormBuilder, + private readonly _userControllerService: UserControllerService, + private readonly _userService: UserService, + private readonly _dialogService: AdminDialogService, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public user: User + ) { + this.userForm = this._formBuilder.group({ + firstName: [this.user?.firstName, Validators.required], + lastName: [this.user?.lastName, Validators.required], + email: [{ value: this.user?.email, disabled: !!user }, [Validators.required, Validators.email]], + // password: [this.user?.password, Validators.required], + admin: [user && this._userService.isAdmin(user)], + roles: [user?.roles || []] + }); + } + + public get changed(): boolean { + if (!this.user) return true; + + for (const key of Object.keys(this.userForm.getRawValue())) { + if (this.user[key] !== this.userForm.get(key).value) { + return true; + } + } + + return false; + } + + private async _updateProfile() { + const profileKeys = ['firstName', 'lastName']; + let profileChanged = false; + for (const key of profileKeys) { + if (this.userForm.get(key).value !== this.user[key]) { + profileChanged = true; + } + } + if (!profileChanged) { + return; + } + await this._userControllerService.updateProfile(this.userForm.getRawValue()).toPromise(); + } + + private async _updateRoles() { + const newAdminValue = this.userForm.get('admin').value; + if (newAdminValue === this._userService.isAdmin(this.user)) { + return; + } + let roles = [...this.user.roles]; + if (newAdminValue) { + roles.push('RED_ADMIN'); + } else { + roles = roles.filter((role) => role !== 'RED_ADMIN'); + } + await this._userControllerService.addRoleToUsers(roles, this.user.userId).toPromise(); + } + + private async _update() { + await this._updateProfile(); + await this._updateRoles(); + } + + private async _create() { + const roles = ['RED_USER']; + if (this.userForm.get('admin').value) { + roles.push('RED_ADMIN'); + } + await this._userControllerService.createUser({ ...this.userForm.getRawValue(), roles: [] }, 'body').toPromise(); + } + + public async save() { + if (this.user) { + await this._update(); + } else { + await this._create(); + } + this.dialogRef.close(true); + } + + public async delete() { + this._dialogService.openConfirmDeleteUserDialog(this.user); + // this.dialogRef.close(); + } + + public changeRoleValue(role: string, { checked }: MatCheckboxChange) { + const roles = [...this.activeRoles]; + if (checked) { + roles.push(role); + } else { + roles.splice(roles.indexOf(role), 1); + } + this.userForm.patchValue({ roles }); + } + + public get activeRoles(): string[] { + return this.userForm.get('roles').value; + } + + public getChecked(role: string) { + return this.activeRoles.indexOf(role) !== -1; + } + + public getDisabled(role: string) { + return this.getChecked(role) && role === 'RED_USER' && this.getChecked('RED_ADMIN'); + } +} diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-file-attribute-dialog/confirm-delete-file-attribute-dialog.component.scss b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-file-attribute-dialog/confirm-delete-file-attribute-dialog.component.scss index 910d55c69..ddf782c9b 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-file-attribute-dialog/confirm-delete-file-attribute-dialog.component.scss +++ b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-file-attribute-dialog/confirm-delete-file-attribute-dialog.component.scss @@ -8,6 +8,10 @@ margin-bottom: 24px; } -mat-checkbox:not(:last-of-type) { - margin-bottom: 6px; +mat-checkbox { + width: 100%; + + &:not(:last-of-type) { + margin-bottom: 6px; + } } diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html new file mode 100644 index 000000000..bd83c23e7 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html @@ -0,0 +1,19 @@ +
+
+ +
+
+ + + {{ 'confirm-delete-user.checkbox-' + (idx + 1) | translate: { count: 20 } }} + +
+ +
+ +
+
+ +
diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.scss b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.scss new file mode 100644 index 000000000..ddf782c9b --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.scss @@ -0,0 +1,17 @@ +@import '../../../../../assets/styles/red-variables'; + +.dialog-header { + color: $primary; +} + +.heading { + margin-bottom: 24px; +} + +mat-checkbox { + width: 100%; + + &:not(:last-of-type) { + margin-bottom: 6px; + } +} diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.ts new file mode 100644 index 000000000..8b288077f --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.ts @@ -0,0 +1,38 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { User, UserControllerService } from '@redaction/red-ui-http'; + +@Component({ + selector: 'redaction-confirm-delete-user-dialog', + templateUrl: './confirm-delete-user-dialog.component.html', + styleUrls: ['./confirm-delete-user-dialog.component.scss'] +}) +export class ConfirmDeleteUserDialogComponent implements OnInit { + public checkboxes = [{ value: false }, { value: false }]; + public showToast = false; + + constructor( + @Inject(MAT_DIALOG_DATA) public user: User, + private readonly _userControllerService: UserControllerService, + public dialogRef: MatDialogRef + ) {} + + ngOnInit(): void {} + + async deleteUser() { + if (this.valid) { + await this._userControllerService.deleteUser(this.user.userId).toPromise(); + this.dialogRef.close(true); + } else { + this.showToast = true; + } + } + + public get valid() { + return this.checkboxes[0].value && this.checkboxes[1].value; + } + + public cancel() { + this.dialogRef.close(); + } +} diff --git a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html index 3d60e39bf..a5d86d764 100644 --- a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html @@ -2,9 +2,15 @@ + + diff --git a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.scss b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.scss index 94fa2e029..dc3ae3d31 100644 --- a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.scss +++ b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.scss @@ -3,18 +3,22 @@ cdk-virtual-scroll-viewport { ::ng-deep.cdk-virtual-scroll-content-wrapper { - grid-template-columns: 1fr 1fr 11px; + grid-template-columns: 2fr 1fr 1fr 1fr auto 11px; .table-item { - > div { + > div:not(.scrollbar-placeholder) { padding: 0 24px; + + &.center { + align-items: center; + } } } } &.has-scrollbar:hover { ::ng-deep.cdk-virtual-scroll-content-wrapper { - grid-template-columns: 1fr 1fr; + grid-template-columns: 2fr 1fr 1fr 1fr auto; } } } @@ -27,6 +31,13 @@ } .page-header .actions { - width: calc(353px - 24px); justify-content: flex-end; + + redaction-search-input:not(:last-child) { + margin-right: 16px; + } + + redaction-icon-button:not(:last-child) { + margin-right: 6px; + } } diff --git a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts index c9980cc1b..1bb07f820 100644 --- a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts @@ -1,33 +1,75 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { PermissionsService } from '../../../../services/permissions.service'; import { UserService } from '../../../../services/user.service'; -import { User } from '@redaction/red-ui-http'; +import { User, UserControllerService } from '@redaction/red-ui-http'; import { FormBuilder, FormGroup } from '@angular/forms'; import { debounce } from '../../../../utils/debounce'; +import { AdminDialogService } from '../../services/admin-dialog-service.service'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'redaction-user-listing-screen', templateUrl: './user-listing-screen.component.html', styleUrls: ['./user-listing-screen.component.scss'] }) -export class UserListingScreenComponent { +export class UserListingScreenComponent implements OnInit { + public viewReady = false; + public users: User[]; - public displayedUsers: User[]; + public displayedUsers: User[] = []; public searchForm: FormGroup; - constructor(public readonly permissionsService: PermissionsService, private readonly userService: UserService, private readonly _formBuilder: FormBuilder) { - this.users = this.userService.allUsers; - this.displayedUsers = [...this.users]; - + constructor( + public readonly permissionsService: PermissionsService, + public readonly userService: UserService, + private readonly _formBuilder: FormBuilder, + private readonly _translateService: TranslateService, + private readonly _adminDialogService: AdminDialogService, + private readonly _userControllerService: UserControllerService + ) { this.searchForm = this._formBuilder.group({ query: [''] }); - this.searchForm.valueChanges.subscribe((value) => this._executeSearch(value)); + this.searchForm.valueChanges.subscribe(() => this._executeSearch()); + } + + public async ngOnInit() { + await this._loadData(); } @debounce(200) - private _executeSearch(value: { query: string }) { - this.displayedUsers = this.users.filter((user) => this.userService.getName(user).toLowerCase().includes(value.query.toLowerCase())); + private _executeSearch() { + const value = this.searchForm.get('query').value; + this.displayedUsers = this.users.filter((user) => this.userService.getName(user).toLowerCase().includes(value.toLowerCase())); + } + + public openAddEditUserDialog($event: MouseEvent, user?: User) { + $event.stopPropagation(); + this._adminDialogService.openAddEditUserDialog(user, async () => { + await this._loadData(); + }); + } + + public openDeleteUserDialog($event: MouseEvent, user: User) { + $event.stopPropagation(); + } + + private async _loadData() { + this.viewReady = false; + this.users = (await this._userControllerService.getAllUsers({}).toPromise()).users; + this._executeSearch(); + this.viewReady = true; + } + + public getDisplayRoles(user: User) { + return user.roles.map((role) => this._translateService.instant('roles.' + role)).join(', ') || this._translateService.instant('roles.NO_ROLE'); + } + + public async changeActive(user: User) { + this.viewReady = false; + user.roles = this.userService.isActive(user) ? [] : ['RED_USER']; + await this._userControllerService.addRoleToUsers(user.roles, user.userId).toPromise(); + await this._loadData(); } } diff --git a/apps/red-ui/src/app/modules/admin/services/admin-dialog-service.service.ts b/apps/red-ui/src/app/modules/admin/services/admin-dialog-service.service.ts index d43997a39..850320795 100644 --- a/apps/red-ui/src/app/modules/admin/services/admin-dialog-service.service.ts +++ b/apps/red-ui/src/app/modules/admin/services/admin-dialog-service.service.ts @@ -9,7 +9,8 @@ import { RuleSetControllerService, RuleSetModel, SMTPConfigurationModel, - TypeValue + TypeValue, + User } from '@redaction/red-ui-http'; import { AddEditFileAttributeDialogComponent } from '../dialogs/add-edit-file-attribute-dialog/add-edit-file-attribute-dialog.component'; import { AddEditDictionaryDialogComponent } from '../dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component'; @@ -21,6 +22,8 @@ import { ConfirmDeleteFileAttributeDialogComponent } from '../dialogs/confirm-de import { EditColorDialogComponent } from '../dialogs/edit-color-dialog/edit-color-dialog.component'; import { TranslateService } from '@ngx-translate/core'; import { SmtpAuthDialogComponent } from '../dialogs/smtp-auth-dialog/smtp-auth-dialog.component'; +import { AddEditUserDialogComponent } from '../dialogs/add-edit-user-dialog/add-edit-user-dialog.component'; +import { ConfirmDeleteUserDialogComponent } from '../dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component'; const dialogConfig = { width: '662px', @@ -168,4 +171,36 @@ export class AdminDialogService { return ref; } + + public openAddEditUserDialog(user?: User, cb?: Function): MatDialogRef { + const ref = this._dialog.open(AddEditUserDialogComponent, { + ...dialogConfig, + data: user, + autoFocus: true + }); + + ref.afterClosed().subscribe((result) => { + if (result && cb) { + cb(result); + } + }); + + return ref; + } + + public openConfirmDeleteUserDialog(user?: User, cb?: Function): MatDialogRef { + const ref = this._dialog.open(ConfirmDeleteUserDialogComponent, { + ...dialogConfig, + data: user, + autoFocus: true + }); + + ref.afterClosed().subscribe((result) => { + if (result && cb) { + cb(result); + } + }); + + return ref; + } } diff --git a/apps/red-ui/src/app/modules/shared/components/initials-avatar/initials-avatar.component.html b/apps/red-ui/src/app/modules/shared/components/initials-avatar/initials-avatar.component.html index 8bac6c9dc..21a46b5db 100644 --- a/apps/red-ui/src/app/modules/shared/components/initials-avatar/initials-avatar.component.html +++ b/apps/red-ui/src/app/modules/shared/components/initials-avatar/initials-avatar.component.html @@ -2,7 +2,7 @@
{{ initials }}
-
+
{{ displayName || ('initials-avatar.unassigned' | translate) }}
diff --git a/apps/red-ui/src/app/modules/shared/components/initials-avatar/initials-avatar.component.scss b/apps/red-ui/src/app/modules/shared/components/initials-avatar/initials-avatar.component.scss index f7cc321aa..b4ab89ac3 100644 --- a/apps/red-ui/src/app/modules/shared/components/initials-avatar/initials-avatar.component.scss +++ b/apps/red-ui/src/app/modules/shared/components/initials-avatar/initials-avatar.component.scss @@ -7,6 +7,10 @@ .username { margin-left: 6px; + + &.disabled { + opacity: 0.7; + } } } diff --git a/apps/red-ui/src/app/modules/shared/components/initials-avatar/initials-avatar.component.ts b/apps/red-ui/src/app/modules/shared/components/initials-avatar/initials-avatar.component.ts index eed1e9c6b..58bbfad9f 100644 --- a/apps/red-ui/src/app/modules/shared/components/initials-avatar/initials-avatar.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/initials-avatar/initials-avatar.component.ts @@ -1,6 +1,7 @@ import { Component, Input, OnChanges, OnInit } from '@angular/core'; import { UserService } from '../../../../services/user.service'; import { User } from '@redaction/red-ui-http'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'redaction-initials-avatar', @@ -9,11 +10,12 @@ import { User } from '@redaction/red-ui-http'; }) export class InitialsAvatarComponent implements OnInit, OnChanges { @Input() public userId: string; + @Input() public user: User; @Input() public color = 'lightgray'; @Input() public size: 'small' | 'large' = 'small'; @Input() public withName = false; - - public _user: User; + @Input() public showYou = false; + @Input() public alwaysShowName = false; public displayName: string; @@ -21,17 +23,23 @@ export class InitialsAvatarComponent implements OnInit, OnChanges { public colorClass: string; - constructor(private readonly _userService: UserService) {} + constructor(private readonly _userService: UserService, private readonly _translateService: TranslateService) {} ngOnInit(): void {} ngOnChanges(): void { const isSystemUser = this.userId && this.userId.toLowerCase() === 'system'; if (!isSystemUser) { - this._user = this._userService.getUserById(this.userId); - this.displayName = this._userService.getName(this._user); + if (this.user) { + this.userId = this.user.userId; + } else { + this.user = this._userService.getUserById(this.userId); + } + this.displayName = + this._userService.getName(this.user) + + (this.showYou && this._userService.userId === this.userId ? ` (${this._translateService.instant('initials-avatar.you')})` : ''); this.initials = this._getInitials(); - this.colorClass = this._colorClass(); + this.colorClass = this._colorClass; } else { this.displayName = 'System'; this.initials = 'SY'; @@ -40,11 +48,11 @@ export class InitialsAvatarComponent implements OnInit, OnChanges { } private _getInitials() { - if (!this._user) { + if (!this.user) { return '?'; } else { return this._userService - .getName(this._user) + .getName(this.user) .split(' ') .filter((value) => value !== ' ') .filter((value, idx) => idx < 2) @@ -53,10 +61,13 @@ export class InitialsAvatarComponent implements OnInit, OnChanges { } } - private _colorClass() { + private get _colorClass() { if (this._userService.userId === this.userId) { return 'red-white'; } + if (this.disabled) { + return 'inactive'; + } if (this.color.includes('-')) { return this.color; } @@ -64,6 +75,10 @@ export class InitialsAvatarComponent implements OnInit, OnChanges { } public get hasBorder(): boolean { - return !!this._user && this._userService.userId !== this.userId && this._userService.isManager(this._user); + return !!this.user && this._userService.userId !== this.userId && this._userService.isManager(this.user); + } + + public get disabled(): boolean { + return !this._userService.isActive(this.user); } } diff --git a/apps/red-ui/src/app/services/notification.service.ts b/apps/red-ui/src/app/services/notification.service.ts index c4486493b..71e6453f8 100644 --- a/apps/red-ui/src/app/services/notification.service.ts +++ b/apps/red-ui/src/app/services/notification.service.ts @@ -10,7 +10,7 @@ export enum NotificationType { INFO = 'INFO' } -export class Action { +export class ToastAction { title: string; action?: Function; } @@ -31,7 +31,7 @@ export class NotificationService { message: string, title?: string, notificationType: NotificationType = NotificationType.INFO, - options?: Partial & { actions?: Action[] } + options?: Partial & { actions?: ToastAction[] } ): ActiveToast { switch (notificationType) { case NotificationType.ERROR: diff --git a/apps/red-ui/src/app/services/user.service.ts b/apps/red-ui/src/app/services/user.service.ts index eeb7c93e2..fa07b9c20 100644 --- a/apps/red-ui/src/app/services/user.service.ts +++ b/apps/red-ui/src/app/services/user.service.ts @@ -114,6 +114,20 @@ export class UserService { return user.roles?.indexOf('RED_USER') >= 0; } + isAdmin(user?: User): boolean { + if (!user) { + user = this.user; + } + return user.roles?.indexOf('RED_ADMIN') >= 0; + } + + isActive(user?: User): boolean { + if (!user) { + user = this.user; + } + return user.roles?.length > 0; + } + private _hasAnyRedRole(u: User) { return u.roles.indexOf('RED_USER') >= 0 || u.roles.indexOf('RED_MANAGER') >= 0 || u.roles.indexOf('RED_ADMIN') >= 0; } diff --git a/apps/red-ui/src/app/state/app-state.service.ts b/apps/red-ui/src/app/state/app-state.service.ts index 55b2a8f4f..60d4315a2 100644 --- a/apps/red-ui/src/app/state/app-state.service.ts +++ b/apps/red-ui/src/app/state/app-state.service.ts @@ -23,7 +23,6 @@ import { tap } from 'rxjs/operators'; import { humanize } from '../utils/functions'; import { FileStatusWrapper } from '../models/file/file-status.wrapper'; import { ProjectWrapper } from './model/project.wrapper'; -import { saveAs } from 'file-saver'; export interface AppState { projects: ProjectWrapper[]; diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index d196e3a41..dc6e08925 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -417,7 +417,8 @@ "show": "Show" }, "initials-avatar": { - "unassigned": "Unassigned" + "unassigned": "Unassigned", + "you": "You" }, "assign-file-owner": { "dialog": { @@ -756,6 +757,14 @@ "checkbox-1": "All documents it is used on will be impacted", "checkbox-2": "All inputted details on the documents will be lost" }, + "confirm-delete-user": { + "title": "Delete User from Workspace", + "warning": "Warning: this cannot be undone!", + "checkbox-1": "{{count}} projects will be impacted", + "checkbox-2": "{{count}} documents waiting review will be impacted", + "delete": "Delete User", + "cancel": "Keep User" + }, "document-info": { "title": "Introduce File Attributes", "save": "Save Document Info", @@ -767,9 +776,34 @@ }, "table-col-names": { "name": "Name", - "email": "Email" + "email": "Email", + "active": "Active", + "roles": "Roles" }, - "search": "Search..." + "action": { + "edit": "Edit User", + "delete": "Delete User" + }, + "search": "Search...", + "add-new": "New User" + }, + "add-edit-user": { + "title": { + "new": "Add New User", + "edit": "Edit User" + }, + "form": { + "first-name": "First Name", + "last-name": "Last Name", + "email": "Email", + "role": "Role" + }, + "actions": { + "save": "Save User", + "save-changes": "Save Changes", + "delete": "Delete User", + "cancel": "Cancel" + } }, "rules-screen": { "error": { @@ -1015,5 +1049,12 @@ "stream-download": { "abort": "You have an active download, closing this window will cause it to stop!", "error": "Failed to download {{filename}}. Please try again ..." + }, + "roles": { + "RED_USER": "User", + "RED_MANAGER": "Manager", + "RED_USER_ADMIN": "Users Admin", + "RED_ADMIN": "Application Admin", + "NO_ROLE": "No role defined" } } diff --git a/apps/red-ui/src/assets/styles/red-button.scss b/apps/red-ui/src/assets/styles/red-button.scss index 8ddf17bef..d63f68b46 100644 --- a/apps/red-ui/src/assets/styles/red-button.scss +++ b/apps/red-ui/src/assets/styles/red-button.scss @@ -2,7 +2,6 @@ .mat-button, .mat-flat-button { - font-family: Inter, sans-serif !important; border-radius: 17px !important; font-size: 13px !important; height: 34px; diff --git a/apps/red-ui/src/assets/styles/red-checkbox.scss b/apps/red-ui/src/assets/styles/red-checkbox.scss index 10f4ede37..ad06c1eee 100644 --- a/apps/red-ui/src/assets/styles/red-checkbox.scss +++ b/apps/red-ui/src/assets/styles/red-checkbox.scss @@ -18,7 +18,6 @@ } .mat-checkbox-label { - font-family: 'Inter', sans-serif; font-size: 13px; color: $accent; diff --git a/apps/red-ui/src/assets/styles/red-components.scss b/apps/red-ui/src/assets/styles/red-components.scss index 13236f052..e0cef767b 100644 --- a/apps/red-ui/src/assets/styles/red-components.scss +++ b/apps/red-ui/src/assets/styles/red-components.scss @@ -58,6 +58,11 @@ &.white-dark { border: 1px solid $grey-4; } + + &.inactive { + background-color: $grey-6; + color: $grey-7; + } } .oval { diff --git a/apps/red-ui/src/assets/styles/red-input.scss b/apps/red-ui/src/assets/styles/red-input.scss index 313f74129..6dc75c330 100644 --- a/apps/red-ui/src/assets/styles/red-input.scss +++ b/apps/red-ui/src/assets/styles/red-input.scss @@ -125,6 +125,11 @@ form { border-color: $red-1; } } + + &:disabled { + background-color: $grey-2; + color: rgba($grey-1, 0.3); + } } .hex-color-input { diff --git a/apps/red-ui/src/assets/styles/red-list.scss b/apps/red-ui/src/assets/styles/red-list.scss index 3c2d24243..48289a7b9 100644 --- a/apps/red-ui/src/assets/styles/red-list.scss +++ b/apps/red-ui/src/assets/styles/red-list.scss @@ -1,7 +1,6 @@ @import 'red-variables'; .mat-list-item { - font-family: Inter, sans-serif; color: $grey-1 !important; font-size: 13px !important; line-height: 16px !important; diff --git a/apps/red-ui/src/assets/styles/red-material-theme.scss b/apps/red-ui/src/assets/styles/red-material-theme.scss index fbee50daa..5242c9bbc 100644 --- a/apps/red-ui/src/assets/styles/red-material-theme.scss +++ b/apps/red-ui/src/assets/styles/red-material-theme.scss @@ -55,6 +55,12 @@ $gn-next-mat-theme: mat-light-theme( @include angular-material-theme($gn-next-mat-theme); +$custom-typography: mat-typography-config( + $font-family: 'Inter, sans-serif' +); + +@include angular-material-typography($custom-typography); + .mat-flat-button { min-width: unset !important; } diff --git a/apps/red-ui/src/assets/styles/red-menu.scss b/apps/red-ui/src/assets/styles/red-menu.scss index 2a2fe9792..31745e2f0 100644 --- a/apps/red-ui/src/assets/styles/red-menu.scss +++ b/apps/red-ui/src/assets/styles/red-menu.scss @@ -13,7 +13,6 @@ } .mat-menu-item { - font-family: 'Inter', sans-serif; font-size: 13px; color: $accent; padding: 0 8px; diff --git a/apps/red-ui/src/assets/styles/red-page-layout.scss b/apps/red-ui/src/assets/styles/red-page-layout.scss index 030b1072c..d10a8fc83 100644 --- a/apps/red-ui/src/assets/styles/red-page-layout.scss +++ b/apps/red-ui/src/assets/styles/red-page-layout.scss @@ -121,7 +121,7 @@ body { } @media only screen and (max-width: 1600px) { - redaction-initials-avatar .username { + redaction-initials-avatar .username:not(.always-visible) { display: none; } } diff --git a/apps/red-ui/src/assets/styles/red-select.scss b/apps/red-ui/src/assets/styles/red-select.scss index b43b61ba5..eaf80a030 100644 --- a/apps/red-ui/src/assets/styles/red-select.scss +++ b/apps/red-ui/src/assets/styles/red-select.scss @@ -1,8 +1,6 @@ @import 'red-variables'; .mat-select-panel .mat-option { - font-family: Inter, sans-serif; - &:hover:not(.mat-option-disabled), &:focus:not(.mat-option-disabled) { background-color: $grey-6; diff --git a/apps/red-ui/src/assets/styles/red-toggle.scss b/apps/red-ui/src/assets/styles/red-toggle.scss index 65875cbc2..b4cadc526 100644 --- a/apps/red-ui/src/assets/styles/red-toggle.scss +++ b/apps/red-ui/src/assets/styles/red-toggle.scss @@ -26,10 +26,6 @@ display: none; } - .mat-slide-toggle-content { - font-family: Inter, sans-serif; - } - &.mat-primary.mat-checked { .mat-slide-toggle-bar { background-color: $primary; diff --git a/apps/red-ui/src/assets/styles/red-tooltips.scss b/apps/red-ui/src/assets/styles/red-tooltips.scss index f673f2393..ac70272ca 100644 --- a/apps/red-ui/src/assets/styles/red-tooltips.scss +++ b/apps/red-ui/src/assets/styles/red-tooltips.scss @@ -5,7 +5,6 @@ border-radius: 3px !important; padding: 10px; margin: 12px !important; - font-family: Inter, sans-serif; font-size: 11px; line-height: 14px; color: $white !important; From fe57bba05e83bc5ac48a87e1889844944cee2dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adina=20=C8=9Aeudan?= Date: Tue, 6 Apr 2021 01:39:16 +0300 Subject: [PATCH 2/5] Delete user --- .../add-edit-user-dialog.component.ts | 4 +--- .../confirm-delete-user-dialog.component.html | 7 +++++++ .../user-listing-screen.component.html | 2 +- .../user-listing/user-listing-screen.component.ts | 15 +++++++++++---- apps/red-ui/src/assets/i18n/en.json | 6 ++++-- apps/red-ui/src/assets/styles/red-toasts.scss | 10 +++++++++- 6 files changed, 33 insertions(+), 11 deletions(-) diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.ts index 4a18b6705..dd439a765 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.ts @@ -19,7 +19,6 @@ export class AddEditUserDialogComponent { private readonly _formBuilder: FormBuilder, private readonly _userControllerService: UserControllerService, private readonly _userService: UserService, - private readonly _dialogService: AdminDialogService, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public user: User ) { @@ -96,8 +95,7 @@ export class AddEditUserDialogComponent { } public async delete() { - this._dialogService.openConfirmDeleteUserDialog(this.user); - // this.dialogRef.close(); + this.dialogRef.close('DELETE'); } public changeRoleValue(role: string, { checked }: MatCheckboxChange) { diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html index bd83c23e7..0d4366f84 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html +++ b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html @@ -1,6 +1,13 @@
+
+
+ + + +
+
diff --git a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html index a5d86d764..13a13a447 100644 --- a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html @@ -64,7 +64,7 @@ > { - await this._loadData(); + this._adminDialogService.openAddEditUserDialog(user, async (action) => { + if (action === 'DELETE') { + this.openDeleteUserDialog(user); + } else { + await this._loadData(); + } }); } - public openDeleteUserDialog($event: MouseEvent, user: User) { - $event.stopPropagation(); + public openDeleteUserDialog(user: User, $event?: MouseEvent) { + $event?.stopPropagation(); + this._adminDialogService.openConfirmDeleteUserDialog(user, async () => { + await this._loadData(); + }); } private async _loadData() { diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index dc6e08925..4a312b587 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -755,7 +755,8 @@ "delete": "Delete Attribute", "cancel": "Keep Attribute", "checkbox-1": "All documents it is used on will be impacted", - "checkbox-2": "All inputted details on the documents will be lost" + "checkbox-2": "All inputted details on the documents will be lost", + "toast-error": "Please confirm that you understand the ramifications of your action!" }, "confirm-delete-user": { "title": "Delete User from Workspace", @@ -763,7 +764,8 @@ "checkbox-1": "{{count}} projects will be impacted", "checkbox-2": "{{count}} documents waiting review will be impacted", "delete": "Delete User", - "cancel": "Keep User" + "cancel": "Keep User", + "toast-error": "Please confirm that you understand the ramifications of your action!" }, "document-info": { "title": "Introduce File Attributes", diff --git a/apps/red-ui/src/assets/styles/red-toasts.scss b/apps/red-ui/src/assets/styles/red-toasts.scss index a98a82c49..12276c23c 100644 --- a/apps/red-ui/src/assets/styles/red-toasts.scss +++ b/apps/red-ui/src/assets/styles/red-toasts.scss @@ -2,7 +2,8 @@ $toast-width: 400px; -.toast-container .ngx-toastr { +.toast-container .ngx-toastr, +.inline-dialog-toast { padding: 11px 16px; border-radius: 8px; background-image: none; @@ -90,6 +91,7 @@ $toast-width: 400px; .toast-error { background-color: $red-1; + color: $white; } .toast-warning { @@ -99,3 +101,9 @@ $toast-width: 400px; .toast-info { background-color: $grey-1; } + +.inline-dialog-toast { + width: calc(100% - 96px); + margin-left: 32px; + margin-top: 14px; +} From 7289c8675e2986799aed887d249f933d5bd28e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adina=20=C8=9Aeudan?= Date: Wed, 7 Apr 2021 00:49:43 +0300 Subject: [PATCH 3/5] User management updates --- .../src/app/modules/admin/admin.module.ts | 2 + .../users-stats/users-stats.component.html | 25 +++++ .../users-stats/users-stats.component.scss | 19 ++++ .../users-stats/users-stats.component.ts | 16 +++ .../add-edit-user-dialog.component.html | 8 +- .../add-edit-user-dialog.component.ts | 106 ++++++------------ .../confirm-delete-user-dialog.component.html | 2 +- .../confirm-delete-user-dialog.component.ts | 11 +- .../user-listing-screen.component.html | 6 +- .../user-listing-screen.component.scss | 9 ++ .../user-listing-screen.component.ts | 62 +++++++++- .../project-details.component.html | 2 +- .../project-details.component.scss | 2 +- .../simple-doughnut-chart.component.scss | 3 +- .../app/services/translate-chart.service.ts | 4 + apps/red-ui/src/app/services/user.service.ts | 7 ++ apps/red-ui/src/assets/i18n/en.json | 17 ++- .../src/assets/styles/red-components.scss | 20 +++- 18 files changed, 221 insertions(+), 100 deletions(-) create mode 100644 apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.html create mode 100644 apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.scss create mode 100644 apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.ts diff --git a/apps/red-ui/src/app/modules/admin/admin.module.ts b/apps/red-ui/src/app/modules/admin/admin.module.ts index 62f42167a..2005979f5 100644 --- a/apps/red-ui/src/app/modules/admin/admin.module.ts +++ b/apps/red-ui/src/app/modules/admin/admin.module.ts @@ -30,6 +30,7 @@ import { SmtpConfigScreenComponent } from './screens/smtp-config/smtp-config-scr import { SmtpAuthDialogComponent } from './dialogs/smtp-auth-dialog/smtp-auth-dialog.component'; import { AddEditUserDialogComponent } from './dialogs/add-edit-user-dialog/add-edit-user-dialog.component'; import { ConfirmDeleteUserDialogComponent } from './dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component'; +import { UsersStatsComponent } from './components/users-stats/users-stats.component'; const dialogs = [ AddEditRuleSetDialogComponent, @@ -63,6 +64,7 @@ const components = [ TabsComponent, ComboChartComponent, ComboSeriesVerticalComponent, + UsersStatsComponent, ...dialogs, ...screens ]; diff --git a/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.html b/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.html new file mode 100644 index 000000000..36a3114e6 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.html @@ -0,0 +1,25 @@ +
+ +
+
+ +
+
+ +
+ +
+ +
diff --git a/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.scss b/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.scss new file mode 100644 index 000000000..8938e46bf --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.scss @@ -0,0 +1,19 @@ +.header-wrapper { + display: flex; + flex-direction: row; + position: relative; + + .heading-xl { + max-width: 88%; + } + + redaction-circle-button { + position: absolute; + top: -8px; + left: 270px; + } +} + +.mt-44 { + margin-top: 44px; +} diff --git a/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.ts b/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.ts new file mode 100644 index 000000000..79a35e9ed --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.ts @@ -0,0 +1,16 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { DoughnutChartConfig } from '../../../shared/components/simple-doughnut-chart/simple-doughnut-chart.component'; + +@Component({ + selector: 'redaction-users-stats', + templateUrl: './users-stats.component.html', + styleUrls: ['./users-stats.component.scss'] +}) +export class UsersStatsComponent implements OnInit { + @Output() public toggleCollapse = new EventEmitter(); + @Input() chartData: DoughnutChartConfig[]; + + constructor() {} + + ngOnInit(): void {} +} diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.html b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.html index f06139aa1..742318f8e 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.html +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.html @@ -28,13 +28,7 @@
- + {{ 'roles.' + role | translate }}
diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.ts index dd439a765..91add8a27 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.ts @@ -1,10 +1,8 @@ import { Component, Inject } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { User, UserControllerService } from '@redaction/red-ui-http'; +import { User } from '@redaction/red-ui-http'; import { UserService } from '../../../../services/user.service'; -import { MatCheckboxChange } from '@angular/material/checkbox'; -import { AdminDialogService } from '../../services/admin-dialog-service.service'; @Component({ selector: 'redaction-add-edit-user-dialog', @@ -17,19 +15,39 @@ export class AddEditUserDialogComponent { constructor( private readonly _formBuilder: FormBuilder, - private readonly _userControllerService: UserControllerService, private readonly _userService: UserService, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public user: User ) { + const rolesControls = this.ROLES.reduce( + (prev, role) => ({ + ...prev, + [role]: [this.user && this.user.roles.indexOf(role) !== -1] + }), + {} + ); this.userForm = this._formBuilder.group({ firstName: [this.user?.firstName, Validators.required], lastName: [this.user?.lastName, Validators.required], email: [{ value: this.user?.email, disabled: !!user }, [Validators.required, Validators.email]], // password: [this.user?.password, Validators.required], - admin: [user && this._userService.isAdmin(user)], - roles: [user?.roles || []] + ...rolesControls }); + this._setRolesRequirements(); + } + + private _setRolesRequirements() { + const requirements = { RED_MANAGER: 'RED_USER', RED_ADMIN: 'RED_USER_ADMIN' }; + for (const key of Object.keys(requirements)) { + this.userForm.controls[key].valueChanges.subscribe((checked) => { + if (checked) { + this.userForm.patchValue({ [requirements[key]]: true }); + this.userForm.controls[requirements[key]].disable(); + } else { + this.userForm.controls[requirements[key]].enable(); + } + }); + } } public get changed(): boolean { @@ -44,79 +62,23 @@ export class AddEditUserDialogComponent { return false; } - private async _updateProfile() { - const profileKeys = ['firstName', 'lastName']; - let profileChanged = false; - for (const key of profileKeys) { - if (this.userForm.get(key).value !== this.user[key]) { - profileChanged = true; - } - } - if (!profileChanged) { - return; - } - await this._userControllerService.updateProfile(this.userForm.getRawValue()).toPromise(); - } - - private async _updateRoles() { - const newAdminValue = this.userForm.get('admin').value; - if (newAdminValue === this._userService.isAdmin(this.user)) { - return; - } - let roles = [...this.user.roles]; - if (newAdminValue) { - roles.push('RED_ADMIN'); - } else { - roles = roles.filter((role) => role !== 'RED_ADMIN'); - } - await this._userControllerService.addRoleToUsers(roles, this.user.userId).toPromise(); - } - - private async _update() { - await this._updateProfile(); - await this._updateRoles(); - } - - private async _create() { - const roles = ['RED_USER']; - if (this.userForm.get('admin').value) { - roles.push('RED_ADMIN'); - } - await this._userControllerService.createUser({ ...this.userForm.getRawValue(), roles: [] }, 'body').toPromise(); - } - public async save() { - if (this.user) { - await this._update(); - } else { - await this._create(); - } - this.dialogRef.close(true); + this.dialogRef.close({ + action: this.user ? 'UPDATE' : 'CREATE', + user: { ...this.userForm.getRawValue(), roles: this.activeRoles } + }); } public async delete() { this.dialogRef.close('DELETE'); } - public changeRoleValue(role: string, { checked }: MatCheckboxChange) { - const roles = [...this.activeRoles]; - if (checked) { - roles.push(role); - } else { - roles.splice(roles.indexOf(role), 1); - } - this.userForm.patchValue({ roles }); - } - public get activeRoles(): string[] { - return this.userForm.get('roles').value; - } - - public getChecked(role: string) { - return this.activeRoles.indexOf(role) !== -1; - } - - public getDisabled(role: string) { - return this.getChecked(role) && role === 'RED_USER' && this.getChecked('RED_ADMIN'); + return this.ROLES.reduce((acc, role) => { + if (this.userForm.get(role).value) { + acc.push(role); + } + return acc; + }, []); } } diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html index 0d4366f84..cc1570ce2 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html +++ b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html @@ -12,7 +12,7 @@
- {{ 'confirm-delete-user.checkbox-' + (idx + 1) | translate: { count: 20 } }} + {{ 'confirm-delete-user.checkbox-' + (idx + 1) | translate: { projectsCount: projectsCount } }}
diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.ts index 8b288077f..797b28590 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.ts @@ -1,6 +1,7 @@ import { Component, Inject, OnInit } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { User, UserControllerService } from '@redaction/red-ui-http'; +import { User } from '@redaction/red-ui-http'; +import { AppStateService } from '../../../../state/app-state.service'; @Component({ selector: 'redaction-confirm-delete-user-dialog', @@ -10,18 +11,20 @@ import { User, UserControllerService } from '@redaction/red-ui-http'; export class ConfirmDeleteUserDialogComponent implements OnInit { public checkboxes = [{ value: false }, { value: false }]; public showToast = false; + public projectsCount: number; constructor( @Inject(MAT_DIALOG_DATA) public user: User, - private readonly _userControllerService: UserControllerService, + private readonly _appStateService: AppStateService, public dialogRef: MatDialogRef - ) {} + ) { + this.projectsCount = this._appStateService.allProjects.filter((pw) => pw.memberIds.indexOf(user.userId) !== -1).length; + } ngOnInit(): void {} async deleteUser() { if (this.valid) { - await this._userControllerService.deleteUser(this.user.userId).toPromise(); this.dialogRef.close(true); } else { this.showToast = true; diff --git a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html index 13a13a447..e96a3431e 100644 --- a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html @@ -23,7 +23,7 @@
-
+
{{ 'user-listing.table-header.title' | translate: { length: displayedUsers.length } }} @@ -78,7 +78,9 @@
-
+
+ +
diff --git a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.scss b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.scss index dc3ae3d31..89360c074 100644 --- a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.scss +++ b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.scss @@ -28,6 +28,15 @@ display: flex; width: 353px; min-width: 353px; + padding: 16px 16px 16px 24px; + + &.has-scrollbar:hover { + padding-right: 5px; + } + + redaction-users-stats { + width: 100%; + } } .page-header .actions { diff --git a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts index dd09f094d..d57f90fa6 100644 --- a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts @@ -6,6 +6,8 @@ import { FormBuilder, FormGroup } from '@angular/forms'; import { debounce } from '../../../../utils/debounce'; import { AdminDialogService } from '../../services/admin-dialog-service.service'; import { TranslateService } from '@ngx-translate/core'; +import { DoughnutChartConfig } from '../../../shared/components/simple-doughnut-chart/simple-doughnut-chart.component'; +import { TranslateChartService } from '../../../../services/translate-chart.service'; @Component({ selector: 'redaction-user-listing-screen', @@ -14,7 +16,8 @@ import { TranslateService } from '@ngx-translate/core'; }) export class UserListingScreenComponent implements OnInit { public viewReady = false; - + public collapsedDetails = false; + public chartData: DoughnutChartConfig[]; public users: User[]; public displayedUsers: User[] = []; public searchForm: FormGroup; @@ -25,7 +28,8 @@ export class UserListingScreenComponent implements OnInit { private readonly _formBuilder: FormBuilder, private readonly _translateService: TranslateService, private readonly _adminDialogService: AdminDialogService, - private readonly _userControllerService: UserControllerService + private readonly _userControllerService: UserControllerService, + private readonly _translateChartService: TranslateChartService ) { this.searchForm = this._formBuilder.group({ query: [''] @@ -46,10 +50,16 @@ export class UserListingScreenComponent implements OnInit { public openAddEditUserDialog($event: MouseEvent, user?: User) { $event.stopPropagation(); - this._adminDialogService.openAddEditUserDialog(user, async (action) => { - if (action === 'DELETE') { + this._adminDialogService.openAddEditUserDialog(user, async (result) => { + if (result === 'DELETE') { this.openDeleteUserDialog(user); } else { + this.viewReady = false; + if (result.action === 'CREATE') { + await this._userControllerService.createUser(result.user).toPromise(); + } else if (result.action === 'UPDATE') { + await this._userControllerService.updateProfile(result.user).toPromise(); + } await this._loadData(); } }); @@ -58,6 +68,8 @@ export class UserListingScreenComponent implements OnInit { public openDeleteUserDialog(user: User, $event?: MouseEvent) { $event?.stopPropagation(); this._adminDialogService.openConfirmDeleteUserDialog(user, async () => { + this.viewReady = false; + await this._userControllerService.deleteUser(user.userId).toPromise(); await this._loadData(); }); } @@ -66,9 +78,47 @@ export class UserListingScreenComponent implements OnInit { this.viewReady = false; this.users = (await this._userControllerService.getAllUsers({}).toPromise()).users; this._executeSearch(); + this._computeStats(); this.viewReady = true; } + private _computeStats() { + this.chartData = this._translateChartService.translateRoles( + [ + { + value: this.users.filter((user) => !this.userService.isActive(user)).length, + color: 'INACTIVE', + label: 'INACTIVE' + }, + { + value: this.users.filter((user) => user.roles.length === 1 && user.roles[0] === 'RED_USER').length, + color: 'REGULAR', + label: 'REGULAR' + }, + { + value: this.users.filter((user) => this.userService.isManager(user) && !this.userService.isAdmin(user)).length, + color: 'MANAGER', + label: 'RED_MANAGER' + }, + { + value: this.users.filter((user) => this.userService.isManager(user) && this.userService.isAdmin(user)).length, + color: 'MANAGER_ADMIN', + label: 'MANAGER_ADMIN' + }, + { + value: this.users.filter((user) => this.userService.isUserAdmin(user) && !this.userService.isAdmin(user)).length, + color: 'USER_ADMIN', + label: 'RED_USER_ADMIN' + }, + { + value: this.users.filter((user) => this.userService.isAdmin(user) && !this.userService.isManager(user)).length, + color: 'ADMIN', + label: 'RED_ADMIN' + } + ].filter((type) => type.value > 0) + ); + } + public getDisplayRoles(user: User) { return user.roles.map((role) => this._translateService.instant('roles.' + role)).join(', ') || this._translateService.instant('roles.NO_ROLE'); } @@ -79,4 +129,8 @@ export class UserListingScreenComponent implements OnInit { await this._userControllerService.addRoleToUsers(user.roles, user.userId).toPromise(); await this._loadData(); } + + public toggleCollapsedDetails() { + this.collapsedDetails = !this.collapsedDetails; + } } diff --git a/apps/red-ui/src/app/modules/projects/components/project-details/project-details.component.html b/apps/red-ui/src/app/modules/projects/components/project-details/project-details.component.html index 6c27d7bc5..f92abf6a9 100644 --- a/apps/red-ui/src/app/modules/projects/components/project-details/project-details.component.html +++ b/apps/red-ui/src/app/modules/projects/components/project-details/project-details.component.html @@ -1,5 +1,5 @@ -
+
*:not(:last-child) { - margin-right: 20px; + margin-right: 16px; } } @@ -27,7 +27,6 @@ .text-container { position: absolute; - top: 0; display: flex; flex-direction: column; justify-content: center; diff --git a/apps/red-ui/src/app/services/translate-chart.service.ts b/apps/red-ui/src/app/services/translate-chart.service.ts index 7e2b0f502..d1ea1582f 100644 --- a/apps/red-ui/src/app/services/translate-chart.service.ts +++ b/apps/red-ui/src/app/services/translate-chart.service.ts @@ -11,4 +11,8 @@ export class TranslateChartService { public translateStatus(config: DoughnutChartConfig[]): DoughnutChartConfig[] { return config.map((val) => ({ ...val, label: this._translateService.instant(val.label) })); } + + public translateRoles(config: DoughnutChartConfig[]): DoughnutChartConfig[] { + return config.map((val) => ({ ...val, label: this._translateService.instant(`roles.${val.label}`).toLowerCase() })); + } } diff --git a/apps/red-ui/src/app/services/user.service.ts b/apps/red-ui/src/app/services/user.service.ts index fa07b9c20..bb174b215 100644 --- a/apps/red-ui/src/app/services/user.service.ts +++ b/apps/red-ui/src/app/services/user.service.ts @@ -114,6 +114,13 @@ export class UserService { return user.roles?.indexOf('RED_USER') >= 0; } + isUserAdmin(user?: User): boolean { + if (!user) { + user = this.user; + } + return user.roles?.indexOf('RED_USER_ADMIN') >= 0; + } + isAdmin(user?: User): boolean { if (!user) { user = this.user; diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index 4a312b587..b01f4e634 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -761,8 +761,8 @@ "confirm-delete-user": { "title": "Delete User from Workspace", "warning": "Warning: this cannot be undone!", - "checkbox-1": "{{count}} projects will be impacted", - "checkbox-2": "{{count}} documents waiting review will be impacted", + "checkbox-1": "{{projectsCount}} projects will be impacted", + "checkbox-2": "All documents waiting review from the user will be impacted", "delete": "Delete User", "cancel": "Keep User", "toast-error": "Please confirm that you understand the ramifications of your action!" @@ -807,6 +807,14 @@ "cancel": "Cancel" } }, + "user-stats": { + "title": "Users", + "chart": { + "users": "Users in Workspace" + }, + "expand": "Show Details", + "collapse": "Hide Details" + }, "rules-screen": { "error": { "generic": "Something went wrong... Rules update failed!" @@ -1057,6 +1065,9 @@ "RED_MANAGER": "Manager", "RED_USER_ADMIN": "Users Admin", "RED_ADMIN": "Application Admin", - "NO_ROLE": "No role defined" + "NO_ROLE": "No role defined", + "INACTIVE": "Inactive", + "MANAGER_ADMIN": "Manager & Admin", + "REGULAR": "Regular" } } diff --git a/apps/red-ui/src/assets/styles/red-components.scss b/apps/red-ui/src/assets/styles/red-components.scss index e0cef767b..ff2abf5c5 100644 --- a/apps/red-ui/src/assets/styles/red-components.scss +++ b/apps/red-ui/src/assets/styles/red-components.scss @@ -111,7 +111,8 @@ background-color: $grey-3; } -.UNDER_REVIEW { +.UNDER_REVIEW, +.REGULAR { stroke: $yellow-1; background-color: $yellow-1; } @@ -121,7 +122,8 @@ background-color: $blue-4; } -.APPROVED { +.APPROVED, +.ADMIN { stroke: $blue-3; background-color: $blue-3; } @@ -131,7 +133,8 @@ background-color: $grey-1; } -.OCR_PROCESSING { +.OCR_PROCESSING, +.USER_ADMIN { stroke: $green-2; background-color: $green-2; } @@ -161,6 +164,17 @@ background-color: rgba(#0389ec, 0.1); } +.INACTIVE { + stroke: $grey-5; + background-color: $grey-5; +} + +.MANAGER, +.MANAGER_ADMIN { + stroke: $red-1; + background-color: $red-1; +} + .select-oval { width: 20px; height: 20px; From bf684108e4b166d923e584bde315e3eee4d6cb87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adina=20=C8=9Aeudan?= Date: Wed, 7 Apr 2021 15:02:17 +0300 Subject: [PATCH 4/5] Delete file attribute / user warning styles --- ...m-delete-file-attribute-dialog.component.html | 16 ++++++++++++++-- ...irm-delete-file-attribute-dialog.component.ts | 9 +++++++-- .../confirm-delete-user-dialog.component.html | 7 ++++++- apps/red-ui/src/assets/styles/red-checkbox.scss | 4 ++++ 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-file-attribute-dialog/confirm-delete-file-attribute-dialog.component.html b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-file-attribute-dialog/confirm-delete-file-attribute-dialog.component.html index a9f8b28d8..2af43a9c1 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-file-attribute-dialog/confirm-delete-file-attribute-dialog.component.html +++ b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-file-attribute-dialog/confirm-delete-file-attribute-dialog.component.html @@ -3,16 +3,28 @@ {{ 'confirm-delete-file-attribute.title' | translate: { name: fileAttribute.label } }}
+
+
+ + + +
+
- + {{ 'confirm-delete-file-attribute.checkbox-' + (idx + 1) | translate }}
-
diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-file-attribute-dialog/confirm-delete-file-attribute-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-file-attribute-dialog/confirm-delete-file-attribute-dialog.component.ts index 75f80f5b7..8e86022bc 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-file-attribute-dialog/confirm-delete-file-attribute-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-file-attribute-dialog/confirm-delete-file-attribute-dialog.component.ts @@ -12,6 +12,7 @@ export class ConfirmDeleteFileAttributeDialogComponent { public fileAttribute: FileAttributeConfig; public ruleSetId: string; public checkboxes = [{ value: false }, { value: false }]; + public showToast = false; constructor( private readonly _appStateService: AppStateService, @@ -28,8 +29,12 @@ export class ConfirmDeleteFileAttributeDialogComponent { } async deleteFileAttribute() { - await this._fileAttributesService.deleteFileAttributesConfiguration(this.ruleSetId, this.fileAttribute.id).toPromise(); - this.dialogRef.close(true); + if (this.valid) { + await this._fileAttributesService.deleteFileAttributesConfiguration(this.ruleSetId, this.fileAttribute.id).toPromise(); + this.dialogRef.close(true); + } else { + this.showToast = true; + } } public cancel() { diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html index cc1570ce2..521203939 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html +++ b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html @@ -11,7 +11,12 @@
- + {{ 'confirm-delete-user.checkbox-' + (idx + 1) | translate: { projectsCount: projectsCount } }}
diff --git a/apps/red-ui/src/assets/styles/red-checkbox.scss b/apps/red-ui/src/assets/styles/red-checkbox.scss index ad06c1eee..fe39f654f 100644 --- a/apps/red-ui/src/assets/styles/red-checkbox.scss +++ b/apps/red-ui/src/assets/styles/red-checkbox.scss @@ -29,3 +29,7 @@ } } } + +.mat-checkbox.error .mat-checkbox-label { + color: $red-1; +} From 37b9236f615fed03e1b218273549646110294acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adina=20=C8=9Aeudan?= Date: Wed, 7 Apr 2021 16:33:14 +0300 Subject: [PATCH 5/5] Bulk delete users & other improvements --- .../src/app/modules/admin/admin.module.ts | 4 +- .../admin-breadcrumbs.component.html | 2 +- .../add-edit-user-dialog.component.ts | 21 ++++-- .../confirm-delete-user-dialog.component.ts | 41 ------------ ...onfirm-delete-users-dialog.component.html} | 12 ++-- ...onfirm-delete-users-dialog.component.scss} | 0 .../confirm-delete-users-dialog.component.ts | 55 ++++++++++++++++ .../user-listing-screen.component.html | 42 ++++++++++-- .../user-listing-screen.component.scss | 16 ++++- .../user-listing-screen.component.ts | 64 +++++++++++++++---- .../services/admin-dialog-service.service.ts | 8 +-- .../src/app/services/permissions.service.ts | 14 ++++ apps/red-ui/src/app/services/user.service.ts | 4 ++ apps/red-ui/src/assets/i18n/en.json | 28 ++++++-- .../src/assets/styles/red-tooltips.scss | 6 ++ 15 files changed, 232 insertions(+), 85 deletions(-) delete mode 100644 apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.ts rename apps/red-ui/src/app/modules/admin/dialogs/{confirm-delete-user-dialog/confirm-delete-user-dialog.component.html => confirm-delete-users-dialog/confirm-delete-users-dialog.component.html} (59%) rename apps/red-ui/src/app/modules/admin/dialogs/{confirm-delete-user-dialog/confirm-delete-user-dialog.component.scss => confirm-delete-users-dialog/confirm-delete-users-dialog.component.scss} (100%) create mode 100644 apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-users-dialog/confirm-delete-users-dialog.component.ts diff --git a/apps/red-ui/src/app/modules/admin/admin.module.ts b/apps/red-ui/src/app/modules/admin/admin.module.ts index 2005979f5..eddefe81f 100644 --- a/apps/red-ui/src/app/modules/admin/admin.module.ts +++ b/apps/red-ui/src/app/modules/admin/admin.module.ts @@ -29,8 +29,8 @@ import { AdminDialogService } from './services/admin-dialog-service.service'; import { SmtpConfigScreenComponent } from './screens/smtp-config/smtp-config-screen.component'; import { SmtpAuthDialogComponent } from './dialogs/smtp-auth-dialog/smtp-auth-dialog.component'; import { AddEditUserDialogComponent } from './dialogs/add-edit-user-dialog/add-edit-user-dialog.component'; -import { ConfirmDeleteUserDialogComponent } from './dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component'; import { UsersStatsComponent } from './components/users-stats/users-stats.component'; +import { ConfirmDeleteUsersDialogComponent } from './dialogs/confirm-delete-users-dialog/confirm-delete-users-dialog.component'; const dialogs = [ AddEditRuleSetDialogComponent, @@ -40,7 +40,7 @@ const dialogs = [ EditColorDialogComponent, SmtpAuthDialogComponent, AddEditUserDialogComponent, - ConfirmDeleteUserDialogComponent + ConfirmDeleteUsersDialogComponent ]; const screens = [ diff --git a/apps/red-ui/src/app/modules/admin/components/breadcrumbs/admin-breadcrumbs.component.html b/apps/red-ui/src/app/modules/admin/components/breadcrumbs/admin-breadcrumbs.component.html index 0bb7f56fd..1062de200 100644 --- a/apps/red-ui/src/app/modules/admin/components/breadcrumbs/admin-breadcrumbs.component.html +++ b/apps/red-ui/src/app/modules/admin/components/breadcrumbs/admin-breadcrumbs.component.html @@ -41,7 +41,7 @@ [routerLinkActiveOptions]="{ exact: true }" routerLinkActive="active" translate="user-management" - *ngIf="root && userPreferenceService.areDevFeaturesEnabled" + *ngIf="root && permissionService.canManageUsers() && userPreferenceService.areDevFeaturesEnabled" > ({ ...prev, - [role]: [this.user && this.user.roles.indexOf(role) !== -1] + [role]: [ + { + value: this.user && this.user.roles.indexOf(role) !== -1, + disabled: + this.user && + Object.keys(this.ROLE_REQUIREMENTS).reduce((value, key) => { + return value || (role === this.ROLE_REQUIREMENTS[key] && this.user.roles.indexOf(key) !== -1); + }, false) + } + ] }), {} ); @@ -37,14 +47,13 @@ export class AddEditUserDialogComponent { } private _setRolesRequirements() { - const requirements = { RED_MANAGER: 'RED_USER', RED_ADMIN: 'RED_USER_ADMIN' }; - for (const key of Object.keys(requirements)) { + for (const key of Object.keys(this.ROLE_REQUIREMENTS)) { this.userForm.controls[key].valueChanges.subscribe((checked) => { if (checked) { - this.userForm.patchValue({ [requirements[key]]: true }); - this.userForm.controls[requirements[key]].disable(); + this.userForm.patchValue({ [this.ROLE_REQUIREMENTS[key]]: true }); + this.userForm.controls[this.ROLE_REQUIREMENTS[key]].disable(); } else { - this.userForm.controls[requirements[key]].enable(); + this.userForm.controls[this.ROLE_REQUIREMENTS[key]].enable(); } }); } diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.ts deleted file mode 100644 index 797b28590..000000000 --- a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Component, Inject, OnInit } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { User } from '@redaction/red-ui-http'; -import { AppStateService } from '../../../../state/app-state.service'; - -@Component({ - selector: 'redaction-confirm-delete-user-dialog', - templateUrl: './confirm-delete-user-dialog.component.html', - styleUrls: ['./confirm-delete-user-dialog.component.scss'] -}) -export class ConfirmDeleteUserDialogComponent implements OnInit { - public checkboxes = [{ value: false }, { value: false }]; - public showToast = false; - public projectsCount: number; - - constructor( - @Inject(MAT_DIALOG_DATA) public user: User, - private readonly _appStateService: AppStateService, - public dialogRef: MatDialogRef - ) { - this.projectsCount = this._appStateService.allProjects.filter((pw) => pw.memberIds.indexOf(user.userId) !== -1).length; - } - - ngOnInit(): void {} - - async deleteUser() { - if (this.valid) { - this.dialogRef.close(true); - } else { - this.showToast = true; - } - } - - public get valid() { - return this.checkboxes[0].value && this.checkboxes[1].value; - } - - public cancel() { - this.dialogRef.close(); - } -} diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-users-dialog/confirm-delete-users-dialog.component.html similarity index 59% rename from apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html rename to apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-users-dialog/confirm-delete-users-dialog.component.html index 521203939..d6d712eb2 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html +++ b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-users-dialog/confirm-delete-users-dialog.component.html @@ -1,15 +1,15 @@
-
+
-
+
- {{ 'confirm-delete-user.checkbox-' + (idx + 1) | translate: { projectsCount: projectsCount } }} + {{ 'confirm-delete-users.' + checkbox.label | translate: { projectsCount: projectsCount } }}
-
+
diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.scss b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-users-dialog/confirm-delete-users-dialog.component.scss similarity index 100% rename from apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.scss rename to apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-users-dialog/confirm-delete-users-dialog.component.scss diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-users-dialog/confirm-delete-users-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-users-dialog/confirm-delete-users-dialog.component.ts new file mode 100644 index 000000000..ffe134598 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-users-dialog/confirm-delete-users-dialog.component.ts @@ -0,0 +1,55 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { User } from '@redaction/red-ui-http'; +import { AppStateService } from '../../../../state/app-state.service'; + +@Component({ + selector: 'redaction-confirm-delete-users-dialog', + templateUrl: './confirm-delete-users-dialog.component.html', + styleUrls: ['./confirm-delete-users-dialog.component.scss'] +}) +export class ConfirmDeleteUsersDialogComponent implements OnInit { + public checkboxes = [ + { value: false, label: 'impacted-projects' }, + { value: false, label: 'impacted-documents.' + this.type } + ]; + public showToast = false; + public projectsCount: number; + + constructor( + @Inject(MAT_DIALOG_DATA) public users: User[], + private readonly _appStateService: AppStateService, + public dialogRef: MatDialogRef + ) { + this.projectsCount = this._appStateService.allProjects.filter((pw) => { + for (const user of this.users) { + if (pw.memberIds.indexOf(user.userId) !== -1) { + return true; + } + } + return false; + }).length; + } + + ngOnInit(): void {} + + async deleteUser() { + if (this.valid) { + this.dialogRef.close(true); + } else { + this.showToast = true; + } + } + + public get valid() { + return this.checkboxes[0].value && this.checkboxes[1].value; + } + + public cancel() { + this.dialogRef.close(); + } + + public get type(): 'bulk' | 'single' { + return this.users.length > 1 ? 'bulk' : 'single'; + } +} diff --git a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html index e96a3431e..3b20c9a98 100644 --- a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html @@ -25,17 +25,47 @@
+
+
+ + +
{{ 'user-listing.table-header.title' | translate: { length: displayedUsers.length } }} + + + + + +
+
+ - + @@ -46,12 +76,16 @@
+
+
+ +
{{ user.email || '-' }}
-
- +
+
{{ getDisplayRoles(user) }}
@@ -64,7 +98,7 @@ > div { + padding: 0 13px 0 10px !important; + } + } + cdk-virtual-scroll-viewport { ::ng-deep.cdk-virtual-scroll-content-wrapper { - grid-template-columns: 2fr 1fr 1fr 1fr auto 11px; + grid-template-columns: auto 2fr 1fr 1fr 1fr auto 11px; .table-item { > div:not(.scrollbar-placeholder) { - padding: 0 24px; + padding-left: 10px; &.center { align-items: center; @@ -18,7 +28,7 @@ &.has-scrollbar:hover { ::ng-deep.cdk-virtual-scroll-content-wrapper { - grid-template-columns: 2fr 1fr 1fr 1fr auto; + grid-template-columns: auto 2fr 1fr 1fr 1fr auto; } } } diff --git a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts index d57f90fa6..819d007a5 100644 --- a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { PermissionsService } from '../../../../services/permissions.service'; import { UserService } from '../../../../services/user.service'; -import { User, UserControllerService } from '@redaction/red-ui-http'; +import { RuleSetModel, User, UserControllerService } from '@redaction/red-ui-http'; import { FormBuilder, FormGroup } from '@angular/forms'; import { debounce } from '../../../../utils/debounce'; import { AdminDialogService } from '../../services/admin-dialog-service.service'; @@ -16,11 +16,13 @@ import { TranslateChartService } from '../../../../services/translate-chart.serv }) export class UserListingScreenComponent implements OnInit { public viewReady = false; + public loading = false; public collapsedDetails = false; - public chartData: DoughnutChartConfig[]; + public chartData: DoughnutChartConfig[] = []; public users: User[]; public displayedUsers: User[] = []; public searchForm: FormGroup; + public selectedUsersIds: string[] = []; constructor( public readonly permissionsService: PermissionsService, @@ -52,34 +54,34 @@ export class UserListingScreenComponent implements OnInit { $event.stopPropagation(); this._adminDialogService.openAddEditUserDialog(user, async (result) => { if (result === 'DELETE') { - this.openDeleteUserDialog(user); + this.openDeleteUserDialog([user]); } else { - this.viewReady = false; + this.loading = true; if (result.action === 'CREATE') { await this._userControllerService.createUser(result.user).toPromise(); } else if (result.action === 'UPDATE') { - await this._userControllerService.updateProfile(result.user).toPromise(); + await this._userControllerService.updateProfile(result.user, user.userId).toPromise(); } await this._loadData(); } }); } - public openDeleteUserDialog(user: User, $event?: MouseEvent) { + public openDeleteUserDialog(users: User[], $event?: MouseEvent) { $event?.stopPropagation(); - this._adminDialogService.openConfirmDeleteUserDialog(user, async () => { - this.viewReady = false; - await this._userControllerService.deleteUser(user.userId).toPromise(); + this._adminDialogService.openConfirmDeleteUsersDialog(users, async () => { + this.loading = true; + await this._userControllerService.deleteUsers(users.map((u) => u.userId)).toPromise(); await this._loadData(); }); } private async _loadData() { - this.viewReady = false; this.users = (await this._userControllerService.getAllUsers({}).toPromise()).users; this._executeSearch(); this._computeStats(); this.viewReady = true; + this.loading = false; } private _computeStats() { @@ -123,8 +125,8 @@ export class UserListingScreenComponent implements OnInit { return user.roles.map((role) => this._translateService.instant('roles.' + role)).join(', ') || this._translateService.instant('roles.NO_ROLE'); } - public async changeActive(user: User) { - this.viewReady = false; + public async toggleActive(user: User) { + this.loading = true; user.roles = this.userService.isActive(user) ? [] : ['RED_USER']; await this._userControllerService.addRoleToUsers(user.roles, user.userId).toPromise(); await this._loadData(); @@ -133,4 +135,42 @@ export class UserListingScreenComponent implements OnInit { public toggleCollapsedDetails() { this.collapsedDetails = !this.collapsedDetails; } + + toggleUserSelected($event: MouseEvent, user: User) { + $event.stopPropagation(); + const idx = this.selectedUsersIds.indexOf(user.userId); + if (idx === -1) { + this.selectedUsersIds.push(user.userId); + } else { + this.selectedUsersIds.splice(idx, 1); + } + } + + public toggleSelectAll() { + if (this.areSomeUsersSelected) { + this.selectedUsersIds = []; + } else { + this.selectedUsersIds = this.displayedUsers.map((user) => user.userId); + } + } + + public get areAllUsersSelected() { + return this.displayedUsers.length !== 0 && this.selectedUsersIds.length === this.displayedUsers.length; + } + + public get areSomeUsersSelected() { + return this.selectedUsersIds.length > 0; + } + + public isUserSelected(user: User) { + return this.selectedUsersIds.indexOf(user.userId) !== -1; + } + + public async bulkDelete() { + this.openDeleteUserDialog(this.users.filter((u) => this.isUserSelected(u))); + } + + public get canDeleteSelected(): boolean { + return this.selectedUsersIds.indexOf(this.userService.userId) === -1; + } } diff --git a/apps/red-ui/src/app/modules/admin/services/admin-dialog-service.service.ts b/apps/red-ui/src/app/modules/admin/services/admin-dialog-service.service.ts index 850320795..b117385a8 100644 --- a/apps/red-ui/src/app/modules/admin/services/admin-dialog-service.service.ts +++ b/apps/red-ui/src/app/modules/admin/services/admin-dialog-service.service.ts @@ -23,7 +23,7 @@ import { EditColorDialogComponent } from '../dialogs/edit-color-dialog/edit-colo import { TranslateService } from '@ngx-translate/core'; import { SmtpAuthDialogComponent } from '../dialogs/smtp-auth-dialog/smtp-auth-dialog.component'; import { AddEditUserDialogComponent } from '../dialogs/add-edit-user-dialog/add-edit-user-dialog.component'; -import { ConfirmDeleteUserDialogComponent } from '../dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component'; +import { ConfirmDeleteUsersDialogComponent } from '../dialogs/confirm-delete-users-dialog/confirm-delete-users-dialog.component'; const dialogConfig = { width: '662px', @@ -188,10 +188,10 @@ export class AdminDialogService { return ref; } - public openConfirmDeleteUserDialog(user?: User, cb?: Function): MatDialogRef { - const ref = this._dialog.open(ConfirmDeleteUserDialogComponent, { + public openConfirmDeleteUsersDialog(users: User[], cb?: Function): MatDialogRef { + const ref = this._dialog.open(ConfirmDeleteUsersDialogComponent, { ...dialogConfig, - data: user, + data: users, autoFocus: true }); diff --git a/apps/red-ui/src/app/services/permissions.service.ts b/apps/red-ui/src/app/services/permissions.service.ts index 7ea8c5ad1..3c6e021d8 100644 --- a/apps/red-ui/src/app/services/permissions.service.ts +++ b/apps/red-ui/src/app/services/permissions.service.ts @@ -278,6 +278,16 @@ export class PermissionsService { return user.isAdmin; } + isUserAdmin(user?: UserWrapper) { + if (!user) { + user = this._userService.user; + } + if (!user) { + return false; + } + return user.isUserAdmin; + } + isUser(user?: UserWrapper) { if (!user) { user = this._userService.user; @@ -297,4 +307,8 @@ export class PermissionsService { } return fileStatus.status === 'UNASSIGNED' || fileStatus.status === 'UNDER_REVIEW' || fileStatus.status === 'UNDER_APPROVAL'; } + + canManageUsers(user?: UserWrapper) { + return this.isUserAdmin(user); + } } diff --git a/apps/red-ui/src/app/services/user.service.ts b/apps/red-ui/src/app/services/user.service.ts index bb174b215..9a376387c 100644 --- a/apps/red-ui/src/app/services/user.service.ts +++ b/apps/red-ui/src/app/services/user.service.ts @@ -30,6 +30,10 @@ export class UserWrapper { return this.roles.indexOf('RED_ADMIN') >= 0; } + get isUserAdmin() { + return this.roles.indexOf('RED_USER_ADMIN') >= 0; + } + get hasAnyREDRoles() { return this.isUser || this.isManager || this.isAdmin; } diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index b01f4e634..409cce1cb 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -758,13 +758,25 @@ "checkbox-2": "All inputted details on the documents will be lost", "toast-error": "Please confirm that you understand the ramifications of your action!" }, - "confirm-delete-user": { - "title": "Delete User from Workspace", + "confirm-delete-users": { + "title": { + "single": "Delete User from Workspace", + "bulk": "Delete Users from Workspace" + }, "warning": "Warning: this cannot be undone!", - "checkbox-1": "{{projectsCount}} projects will be impacted", - "checkbox-2": "All documents waiting review from the user will be impacted", - "delete": "Delete User", - "cancel": "Keep User", + "impacted-projects": "{{projectsCount}} projects will be impacted", + "impacted-documents": { + "single": "All documents pending review from the user will be impacted", + "bulk": "All documents pending review from the users will be impacted" + }, + "delete": { + "single": "Delete User", + "bulk": "Delete Users" + }, + "cancel": { + "single": "Keep User", + "bulk": "Keep Users" + }, "toast-error": "Please confirm that you understand the ramifications of your action!" }, "document-info": { @@ -786,6 +798,10 @@ "edit": "Edit User", "delete": "Delete User" }, + "bulk": { + "delete": "Delete Users", + "delete-disabled": "You cannot delete your own account." + }, "search": "Search...", "add-new": "New User" }, diff --git a/apps/red-ui/src/assets/styles/red-tooltips.scss b/apps/red-ui/src/assets/styles/red-tooltips.scss index ac70272ca..55e95caad 100644 --- a/apps/red-ui/src/assets/styles/red-tooltips.scss +++ b/apps/red-ui/src/assets/styles/red-tooltips.scss @@ -54,3 +54,9 @@ left: 100%; transform: rotate(-90deg) translateY(3px) translateX(3px); } + +.mat-tooltip[style*='transform-origin: left center']:after { + top: 50%; + left: 0; + transform: rotate(90deg) translateY(3px) translateX(-3px); +}