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); +}