assign user / member owner / reviewer dialogs improvements

This commit is contained in:
Timo Bejan 2020-10-22 01:18:46 +03:00
parent d576009547
commit dc5c315159
14 changed files with 192 additions and 173 deletions

View File

@ -3,7 +3,8 @@
## Swagger Generated Code
To regnerate http rune swaagger
```
URL=https://timo-redaction-dev.iqser.cloud/v2/api-docs?group=redaction-gateway-v1
BASE=http://ingress.redaction-timo-dev-3011.178.63.47.73.xip.io/
URL="$BASE"v2/api-docs?group=redaction-gateway-v1
mkdir -p /tmp/swagger
swagger-codegen generate -i "$URL" -l typescript-angular -o /tmp/swagger
```

View File

@ -55,7 +55,6 @@ import { AnnotationIconComponent } from './components/annotation-icon/annotation
import { AuthGuard } from './auth/auth.guard';
import { AuthErrorComponent } from './screens/auth-error/auth-error.component';
import { RedRoleGuard } from './auth/red-role.guard';
import { AssignProjectMembersDialogComponent } from './screens/project-overview-screen/project-members-dialog/assign-project-members-dialog.component';
import { MatListModule } from '@angular/material/list';
import { AssignOwnerDialogComponent } from './components/project-members-dialog/assign-owner-dialog.component';
@ -75,7 +74,6 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
PdfViewerComponent,
FileDetailsDialogComponent,
ProjectDetailsDialogComponent,
AssignProjectMembersDialogComponent,
AssignOwnerDialogComponent,
FullPageLoadingIndicatorComponent,
InitialsAvatarComponent,

View File

@ -3,17 +3,34 @@
class="dialog-header heading-l">
</div>
<div class="dialog-content">
<mat-list class="list-50vh">
<ng-container *ngFor="let user of userService.allUsers">
<mat-list-item class="pointer" [class.owner]="isOwner(user)"
(click)="assignOwner(user)"
*ngIf="userService.isManager(user)">
{{ userService.getNameForId(user.userId) }}
</mat-list-item>
</ng-container>
</mat-list>
</div>
<form (submit)="saveUsers()" [formGroup]="usersForm">
<div class="dialog-content">
<mat-form-field>
<mat-label>{{'assign-' + data.type + '-owner.dialog.single-user.label' | translate}}</mat-label>
<mat-select formControlName="singleUser">
<mat-option *ngFor="let user of userService.managerUsers" [value]="user.userId">
{{userService.getNameForId(user.userId)}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngIf="data.type === 'project' ">
<mat-label>{{'assign-' + data.type + '-owner.dialog.multi-user.label' | translate}}</mat-label>
<mat-select formControlName="userList" multiple="true">
<mat-option *ngFor="let user of userService.allUsers" [value]="user.userId">
{{userService.getNameForId(user.userId)}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="dialog-actions">
<button color="primary" mat-flat-button type="submit"
[disabled]="!usersForm.valid"> {{'assign-' + data.type + '-owner.dialog.save.label' | translate}}</button>
</div>
</form>
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
<mat-icon svgIcon="red:close"></mat-icon>

View File

@ -1,14 +1,15 @@
import { Component, Inject } from '@angular/core';
import { Project, ProjectControllerService, User } from '@redaction/red-ui-http';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AppStateService } from '../../state/app-state.service';
import { UserService } from '../../user/user.service';
import { NotificationService, NotificationType } from '../../notification/notification.service';
import {Component, Inject} from '@angular/core';
import {FileStatus, Project, ProjectControllerService, StatusControllerService} from '@redaction/red-ui-http';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {AppStateService} from '../../state/app-state.service';
import {UserService} from '../../user/user.service';
import {NotificationService} from '../../notification/notification.service';
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
class DialogData {
type: 'file' | 'project';
projectId?: string;
fileId?: string;
project?: Project;
file?: FileStatus;
}
@Component({
@ -17,43 +18,74 @@ class DialogData {
styleUrls: ['./assign-owner-dialog.component.scss']
})
export class AssignOwnerDialogComponent {
private project: Project;
public memberIds: string[];
constructor(private readonly _projectControllerService: ProjectControllerService,
usersForm: FormGroup;
constructor(public readonly userService: UserService,
private readonly _projectControllerService: ProjectControllerService,
private readonly _notificationService: NotificationService,
public readonly userService: UserService,
private readonly _formBuilder: FormBuilder,
private readonly _statusControllerService: StatusControllerService,
private readonly _appStateService: AppStateService,
public dialogRef: MatDialogRef<AssignOwnerDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: DialogData) {
this._loadData();
}
public isOwner(user: User) {
return this.data.type === 'project' ? this.project.ownerId === user.userId : false;
}
public assignOwner(user: User) {
if (this.data.type === 'project') {
this._projectControllerService.assignProjectOwner(this.project.projectId, user.userId).subscribe(() => {
this._notificationService.showToastNotification('Successfully assigned ' + this.userService.getNameForId(user.userId) + ' to project: ' + this.project.projectName);
}, error => {
this._notificationService.showToastNotification('Failed: ' + error.error.message, null, NotificationType.ERROR);
}).add(() => this._reloadProject());
} else if (this.data.type === 'file') {
console.log('not implemented yet');
}
}
private _loadData() {
if (this.data.type === 'project') {
this.project = this._appStateService.getProjectById(this.data.projectId).project;
const project = this.data.project;
this.usersForm = this._formBuilder.group({
singleUser: [project?.ownerId, Validators.required],
userList: [project?.memberIds]
});
}
if (this.data.type === 'file') {
const file = this.data.file;
this.usersForm = this._formBuilder.group({
singleUser: [file?.currentReviewer],
});
}
}
private _reloadProject() {
this._appStateService.addOrUpdateProject(this.project).then(() => {
this._loadData();
});
async saveUsers() {
if (this.data.type === 'project') {
const ownerId = this.usersForm.get('singleUser').value;
const memberIds = this.usersForm.get('userList').value;
const project = this.data.project;
await this._projectControllerService.addMembersToProject({memberIds: memberIds}, project.projectId).toPromise();
await this._projectControllerService.assignProjectOwner(project.projectId, ownerId).toPromise();
const updatedProject = await this._projectControllerService.getProject(project.projectId).toPromise();
const toRemoveMembers = updatedProject.memberIds.filter(m => memberIds.indexOf(m) < 0);
if (toRemoveMembers.length > 0) {
await this._projectControllerService.deleteMembersToProject({memberIds: toRemoveMembers}, project.projectId).toPromise();
}
project.ownerId = ownerId;
project.memberIds = [...new Set([ownerId, ...memberIds])];
this._notificationService.showToastNotification('Successfully assigned ' + this.userService.getNameForId(ownerId) + ' to project: ' + project.projectName);
//this._notificationService.showToastNotification('Failed: ' + error.error.message, null, NotificationType.ERROR);
}
if (this.data.type === 'file') {
const reviewerId = this.usersForm.get('singleUser').value;
await this._statusControllerService.assignProjectOwner1(this._appStateService.activeProjectId, this.data.file.fileId, reviewerId).toPromise();
this.data.file.currentReviewer = reviewerId;
this._notificationService.showToastNotification('Successfully assigned ' + this.userService.getNameForId(reviewerId) + ' to file: ' + this.data.file.filename);
}
this.dialogRef.close();
}
}

View File

@ -1,22 +0,0 @@
<section class="dialog">
<div [translate]="'project-members.dialog.title.label'"
class="dialog-header heading-l">
</div>
<div class="dialog-content">
<mat-selection-list class="list-50vh" color="primary"
[(ngModel)]="memberIds"
(selectionChange)="selectionChange($event)">
<ng-container *ngFor="let user of userService.allUsers">
<mat-list-option *ngIf="userService.isManager(user) || userService.isUser(user)" [value]="user.userId" checkboxPosition="before">
{{ userService.getNameForId(user.userId) }}
</mat-list-option>
</ng-container>
</mat-selection-list>
</div>
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
<mat-icon svgIcon="red:close"></mat-icon>
</button>
</section>

View File

@ -1,56 +0,0 @@
import { Component, Inject } from '@angular/core';
import { Project, ProjectControllerService } from '@redaction/red-ui-http';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AppStateService, ProjectWrapper } from '../../../state/app-state.service';
import { UserService } from '../../../user/user.service';
import { MatSelectionListChange } from '@angular/material/list';
import { NotificationService, NotificationType } from '../../../notification/notification.service';
@Component({
selector: 'redaction-project-details-dialog',
templateUrl: './assign-project-members-dialog.component.html',
styleUrls: ['./assign-project-members-dialog.component.scss']
})
export class AssignProjectMembersDialogComponent {
private _project: Project;
public memberIds: string[];
constructor(private readonly _projectControllerService: ProjectControllerService,
private readonly _notificationService: NotificationService,
public readonly userService: UserService,
private readonly _appStateService: AppStateService,
public dialogRef: MatDialogRef<AssignProjectMembersDialogComponent>) {
this._loadProject();
}
private _loadProject() {
this._project = this._appStateService.activeProject.project;
this.memberIds = [...this._project.memberIds];
}
private _reloadProject() {
this._appStateService.addOrUpdateProject(this._project).then(() => {
this._loadProject();
});
}
public selectionChange($event: MatSelectionListChange) {
const userId = $event.option.value;
const selected = $event.option.selected;
const userName = this.userService.getNameForId(userId);
if (selected) {
this._projectControllerService.addMembersToProject({ memberIds: [userId] }, this._project.projectId).subscribe(() => {
this._notificationService.showToastNotification('Successfully assigned ' + userName + ' to project: ' + this._project.projectName);
}, error => {
this._notificationService.showToastNotification('Failed: ' + error.error.message, null, NotificationType.ERROR);
}).add(() => this._reloadProject());
} else {
this._projectControllerService.deleteMembersToProject({ memberIds: [userId] }, this._project.projectId).subscribe(() => {
this._notificationService.showToastNotification('Successfully removed ' + userName + ' from project: ' + this._project.projectName);
}, error => {
this._notificationService.showToastNotification('Failed: ' + error.error.message, null, NotificationType.ERROR);
}).add(() => this._reloadProject());
}
}
}

View File

@ -128,7 +128,7 @@
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
1
{{ appStateService.activeProject.project.memberIds.length }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
@ -155,13 +155,12 @@
<div class="project-team mt-20">
<div class="all-caps-label" translate="project-overview.project-details.project-team.label"></div>
<div class="flex mt-20 members-container">
<div *ngFor="let username of members" class="member">
<div *ngFor="let username of displayMembers" class="member">
<redaction-initials-avatar [username]="username" size="large"></redaction-initials-avatar>
</div>
<!-- TODO THIS IS OVERFLOW-->
<!-- <div class="member">-->
<!-- <div class="oval large white-dark">+2</div>-->
<!-- </div>-->
<div class="member">
<div class="oval large white-dark">+{{overflowCount}}</div>
</div>
<div class="member pointer" (click)="openAssignProjectMembersDialog()">
<div class="oval red-white large">+</div>
</div>

View File

@ -1,5 +1,5 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import {ChangeDetectorRef, Component, OnDestroy, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {
FileStatus,
FileUploadControllerService,
@ -7,22 +7,22 @@ import {
ReanalysisControllerService,
StatusControllerService
} from '@redaction/red-ui-http';
import { NotificationService, NotificationType } from '../../notification/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { ConfirmationDialogComponent } from '../../common/confirmation-dialog/confirmation-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { AppStateService } from '../../state/app-state.service';
import { ProjectDetailsDialogComponent } from './project-details-dialog/project-details-dialog.component';
import { FileDropOverlayService } from '../../upload/file-drop/service/file-drop-overlay.service';
import { FileUploadModel } from '../../upload/model/file-upload.model';
import { FileUploadService } from '../../upload/file-upload.service';
import { UploadStatusOverlayService } from '../../upload/upload-status-dialog/service/upload-status-overlay.service';
import { AddEditProjectDialogComponent } from '../project-listing-screen/add-edit-project-dialog/add-edit-project-dialog.component';
import { UserService } from '../../user/user.service';
import { SortingOption } from '../../utils/types';
import { DoughnutChartConfig } from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
import { groupBy } from '../../utils/functions';
import { AssignProjectMembersDialogComponent } from './project-members-dialog/assign-project-members-dialog.component';
import {NotificationService, NotificationType} from '../../notification/notification.service';
import {TranslateService} from '@ngx-translate/core';
import {ConfirmationDialogComponent} from '../../common/confirmation-dialog/confirmation-dialog.component';
import {MatDialog} from '@angular/material/dialog';
import {AppStateService} from '../../state/app-state.service';
import {ProjectDetailsDialogComponent} from './project-details-dialog/project-details-dialog.component';
import {FileDropOverlayService} from '../../upload/file-drop/service/file-drop-overlay.service';
import {FileUploadModel} from '../../upload/model/file-upload.model';
import {FileUploadService} from '../../upload/file-upload.service';
import {UploadStatusOverlayService} from '../../upload/upload-status-dialog/service/upload-status-overlay.service';
import {AddEditProjectDialogComponent} from '../project-listing-screen/add-edit-project-dialog/add-edit-project-dialog.component';
import {UserService} from '../../user/user.service';
import {SortingOption} from '../../utils/types';
import {DoughnutChartConfig} from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
import {groupBy} from '../../utils/functions';
import {AssignOwnerDialogComponent} from "../../components/project-members-dialog/assign-owner-dialog.component";
@Component({
@ -34,10 +34,10 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
private _fileStatusInterval;
public sortingOptions: SortingOption[] = [
{ label: 'project-overview.sorting.recent.label', order: 'desc', column: 'lastUpdated' },
{ label: 'project-overview.sorting.alphabetically.label', order: 'asc', column: 'filename' },
{ label: 'project-overview.sorting.number-of-pages.label', order: 'asc', column: 'numberOfPages' },
{ label: 'project-overview.sorting.number-of-analyses.label', order: 'desc', column: 'numberOfAnalyses' }
{label: 'project-overview.sorting.recent.label', order: 'desc', column: 'lastUpdated'},
{label: 'project-overview.sorting.alphabetically.label', order: 'asc', column: 'filename'},
{label: 'project-overview.sorting.number-of-pages.label', order: 'asc', column: 'numberOfPages'},
{label: 'project-overview.sorting.number-of-analyses.label', order: 'desc', column: 'numberOfAnalyses'}
];
public sortingOption: SortingOption = this.sortingOptions[0];
public documentsChartData: DoughnutChartConfig[] = [];
@ -90,6 +90,14 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
return this.activeProject.memberIds.map(m => this._userService.getName(this._userService.getUserById(m)));
}
public get displayMembers() {
return this.members.slice(0, 6)
}
public get overflowCount() {
return this.members.length - 6;
}
public get ownerName() {
return this._userService.getNameForId(this.activeProject.ownerId);
}
@ -104,7 +112,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
const groups = groupBy(this.appStateService.activeProject.files, 'status');
this.documentsChartData = [];
for (const key of Object.keys(groups)) {
this.documentsChartData.push({ value: groups[key].length, color: key, label: key });
this.documentsChartData.push({value: groups[key].length, color: key, label: key});
}
}
@ -162,11 +170,14 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
}
public openAssignProjectMembersDialog() {
this._dialog.open(AssignProjectMembersDialogComponent, {
this._dialog.open(AssignOwnerDialogComponent, {
width: '400px',
maxWidth: '90vw',
autoFocus: false
});
autoFocus: false,
data: {type: 'project', project: this.appStateService.activeProject.project }
}).afterClosed().subscribe( ()=>{
this._getFileStatus();
})
}
public reanalyseFile($event: MouseEvent, fileStatus: FileStatus) {
@ -203,12 +214,16 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
public assignPeopleToFile($event: MouseEvent, fileStatus: FileStatus) {
$event.stopPropagation();
this._statusControllerService.assignProjectOwner1(this.appStateService.activeProjectId, fileStatus.fileId, this.user.id).subscribe(() => {
this._notificationService.showToastNotification('Successfully assigned ' + this.user.name + ' to file: ' + fileStatus.filename);
});
this._dialog.open(AssignOwnerDialogComponent, {
width: '400px',
maxWidth: '90vw',
data: {type: 'file', file: fileStatus}
}).afterClosed().subscribe( ()=>{
this._getFileStatus();
})
}
public getFileOwnerUsername(fileStatus: FileStatus) {
return undefined;
return this._userService.getNameForId(fileStatus.currentReviewer);
}
}

View File

@ -57,15 +57,15 @@ export class AppStateService {
get isActiveProjectOwner() {
return this._appState.activeProject.project.ownerId === this._userService.userId
return this._appState.activeProject?.project?.ownerId === this._userService.userId
}
get isActiveProjectMember() {
return this._appState.activeProject.project.memberIds.indexOf(this._userService.userId) >= 0;
return this._appState.activeProject?.project?.memberIds?.indexOf(this._userService.userId) >= 0;
}
get isActiveFileDocumentReviewer() {
return false;
return this._appState.activeFile?.currentReviewer === this._userService.userId;
}

View File

@ -56,6 +56,10 @@ export class UserService {
return this._allUsers;
}
get managerUsers() {
return this._allUsers.filter(u => u.roles.indexOf('RED_MANAGER') >= 0);
}
async loadAllUsersIfNecessary() {
if (!this._allUsers) {
await this.loadAllUsers();
@ -64,7 +68,7 @@ export class UserService {
async loadAllUsers() {
const allUsers = await this._userControllerService.getAllUsers({}, 0, 100).toPromise();
this._allUsers = allUsers.users;
this._allUsers = allUsers.users.filter(u => this._hasAnyRedRole(u));
return allUsers;
}
@ -100,4 +104,8 @@ export class UserService {
isUser(user: User) {
return user.roles.indexOf('RED_USER') >= 0;
}
private _hasAnyRedRole(u: User) {
return u.roles.indexOf('RED_USER') >= 0 || u.roles.indexOf('RED_MANAGER') >= 0 || u.roles.indexOf('RED_ADMIN') >= 0;
}
}

View File

@ -457,6 +457,38 @@
"label": "Unassigned"
}
},
"assign-file-owner":{
"dialog": {
"single-user": {
"label": "Reviewer"
},
"title":{
"label": "Manage File Reviewer"
},
"save": {
"label": "Save"
}
}
},
"assign-project-owner":{
"dialog": {
"single-user": {
"label": "Owner"
},
"multi-user": {
"label": "Members"
},
"title":{
"label": "Manage Project Owner and Members"
},
"save": {
"label": "Save"
}
}
},
"unassigned": "Unassigned",
"under-review": "Under review",
"under-approval": "Under approval",

View File

@ -257,15 +257,6 @@ export class ProjectControllerService {
headers = headers.set('Accept', httpHeaderAcceptSelected);
}
// to determine the Content-Type header
const consumes: string[] = [
'*/*'
];
const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes);
if (httpContentTypeSelected !== undefined) {
headers = headers.set('Content-Type', httpContentTypeSelected);
}
return this.httpClient.request<any>('delete', `${this.basePath}/project/members/${encodeURIComponent(String(projectId))}`,
{
body: body,

View File

@ -18,6 +18,10 @@ export interface FileStatus {
* Date and time when the file was added to the system.
*/
added?: string;
/**
* The current reviewer's (if any) user id.
*/
currentReviewer?: string;
/**
* The ID of the file.
*/