Merge remote-tracking branch 'origin/master' into ui-updates

This commit is contained in:
Adina Țeudan 2020-10-22 01:54:09 +03:00
commit d0aa83f614
12 changed files with 189 additions and 87 deletions

View File

@ -3,7 +3,8 @@
## Swagger Generated Code ## Swagger Generated Code
To regnerate http rune swaagger 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 mkdir -p /tmp/swagger
swagger-codegen generate -i "$URL" -l typescript-angular -o /tmp/swagger swagger-codegen generate -i "$URL" -l typescript-angular -o /tmp/swagger
``` ```

View File

@ -3,18 +3,35 @@
class="dialog-header heading-l"> class="dialog-header heading-l">
</div> </div>
<form (submit)="saveUsers()" [formGroup]="usersForm">
<div class="dialog-content"> <div class="dialog-content">
<mat-list class="list-50vh">
<ng-container *ngFor="let user of userService.allUsers"> <mat-form-field>
<mat-list-item class="pointer" [class.owner]="isOwner(user)" <mat-label>{{'assign-' + data.type + '-owner.dialog.single-user.label' | translate}}</mat-label>
(click)="assignOwner(user)" <mat-select formControlName="singleUser">
*ngIf="userService.isManager(user)"> <mat-option *ngFor="let user of userService.managerUsers" [value]="user.userId">
{{ userService.getNameForId(user.userId) }} {{userService.getNameForId(user.userId)}}
</mat-list-item> </mat-option>
</ng-container> </mat-select>
</mat-list> </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>
<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> <button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
<mat-icon svgIcon="red:close"></mat-icon> <mat-icon svgIcon="red:close"></mat-icon>
</button> </button>

View File

@ -1,14 +1,15 @@
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { Project, ProjectControllerService, User } from '@redaction/red-ui-http'; import { FileStatus, Project, ProjectControllerService, StatusControllerService } from '@redaction/red-ui-http';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AppStateService } from '../../state/app-state.service'; import { AppStateService } from '../../state/app-state.service';
import { UserService } from '../../user/user.service'; import { UserService } from '../../user/user.service';
import { NotificationService, NotificationType } from '../../notification/notification.service'; import { NotificationService } from '../../notification/notification.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
class DialogData { class DialogData {
type: 'file' | 'project'; type: 'file' | 'project';
projectId?: string; project?: Project;
fileId?: string; file?: FileStatus;
} }
@Component({ @Component({
@ -17,43 +18,74 @@ class DialogData {
styleUrls: ['./assign-owner-dialog.component.scss'] styleUrls: ['./assign-owner-dialog.component.scss']
}) })
export class AssignOwnerDialogComponent { 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, private readonly _notificationService: NotificationService,
public readonly userService: UserService, private readonly _formBuilder: FormBuilder,
private readonly _statusControllerService: StatusControllerService,
private readonly _appStateService: AppStateService, private readonly _appStateService: AppStateService,
public dialogRef: MatDialogRef<AssignOwnerDialogComponent>, public dialogRef: MatDialogRef<AssignOwnerDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: DialogData) { @Inject(MAT_DIALOG_DATA) public data: DialogData) {
this._loadData(); 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() { private _loadData() {
if (this.data.type === 'project') { 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() { async saveUsers() {
this._appStateService.addOrUpdateProject(this.project).then(() => { if (this.data.type === 'project') {
this._loadData();
}); 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

@ -22,7 +22,7 @@ import { Annotations } from '@pdftron/webviewer';
const dialogConfig = { const dialogConfig = {
width: '600px', width: '600px',
maxWidth: '90vw', maxWidth: '90vw',
autoFocus: false, autoFocus: false
}; };
@Injectable({ @Injectable({
@ -49,8 +49,7 @@ export class DialogService {
public openDeleteFileDialog($event: MouseEvent, projectId: string, fileId: string, cb?: Function) { public openDeleteFileDialog($event: MouseEvent, projectId: string, fileId: string, cb?: Function) {
$event.stopPropagation(); $event.stopPropagation();
const dialogRef = this._dialog.open(ConfirmationDialogComponent, dialogConfig); this._dialog.open(ConfirmationDialogComponent, dialogConfig).afterClosed().subscribe(result => {
dialogRef.afterClosed().subscribe(result => {
if (result) { if (result) {
const file = this._appStateService.getFileById(projectId, fileId); const file = this._appStateService.getFileById(projectId, fileId);
this._fileUploadControllerService.deleteFile(file.projectId, file.fileId).subscribe(async () => { this._fileUploadControllerService.deleteFile(file.projectId, file.fileId).subscribe(async () => {
@ -76,12 +75,12 @@ export class DialogService {
public acceptSuggestionAnnotation($event: MouseEvent, annotation: Annotations.Annotation, projectId: string, fileId: string) { public acceptSuggestionAnnotation($event: MouseEvent, annotation: Annotations.Annotation, projectId: string, fileId: string) {
$event.stopPropagation(); $event.stopPropagation();
const dialogRef = this._dialog.open(ConfirmationDialogComponent, dialogConfig);
const parts = annotation.Id.split(':'); const parts = annotation.Id.split(':');
const annotationId = parts[parts.length - 1]; const annotationId = parts[parts.length - 1];
dialogRef.afterClosed().subscribe(result => { this._dialog.open(ConfirmationDialogComponent, dialogConfig)
.afterClosed().subscribe(result => {
if (result) { if (result) {
this._manualRedactionControllerService.approveRequest(projectId, fileId, annotationId).subscribe(() => { this._manualRedactionControllerService.approveRequest(projectId, fileId, annotationId).subscribe(() => {
this._notificationService.showToastNotification(this._translateService.instant('manual-redaction.confirm-annotation.success.label'), null, NotificationType.SUCCESS); this._notificationService.showToastNotification(this._translateService.instant('manual-redaction.confirm-annotation.success.label'), null, NotificationType.SUCCESS);
@ -94,15 +93,14 @@ export class DialogService {
public suggestRemoveAnnotation($event: MouseEvent, annotation: Annotations.Annotation, projectId: string, fileId: string) { public suggestRemoveAnnotation($event: MouseEvent, annotation: Annotations.Annotation, projectId: string, fileId: string) {
$event.stopPropagation(); $event.stopPropagation();
const dialogRef = this._dialog.open(ConfirmationDialogComponent, {
width: '400px',
maxWidth: '90vw'
});
const parts = annotation.Id.split(':'); const parts = annotation.Id.split(':');
const annotationId = parts[parts.length - 1]; const annotationId = parts[parts.length - 1];
dialogRef.afterClosed().subscribe(result => { this._dialog.open(ConfirmationDialogComponent, {
width: '400px',
maxWidth: '90vw'
}).afterClosed().subscribe(result => {
if (result) { if (result) {
this._manualRedactionControllerService.undo(projectId, fileId, annotationId).subscribe(ok => { this._manualRedactionControllerService.undo(projectId, fileId, annotationId).subscribe(ok => {
this._notificationService.showToastNotification(this._translateService.instant('manual-redaction.remove-annotation.success.label'), null, NotificationType.SUCCESS); this._notificationService.showToastNotification(this._translateService.instant('manual-redaction.remove-annotation.success.label'), null, NotificationType.SUCCESS);
@ -125,8 +123,8 @@ export class DialogService {
public openDeleteProjectDialog($event: MouseEvent, project: Project, cb?: Function) { public openDeleteProjectDialog($event: MouseEvent, project: Project, cb?: Function) {
$event.stopPropagation(); $event.stopPropagation();
const dialogRef = this._dialog.open(ConfirmationDialogComponent, dialogConfig); this._dialog.open(ConfirmationDialogComponent, dialogConfig)
dialogRef.afterClosed().subscribe(async result => { .afterClosed().subscribe(async result => {
if (result) { if (result) {
await this._appStateService.deleteProject(project); await this._appStateService.deleteProject(project);
if (cb) cb(); if (cb) cb();
@ -142,6 +140,16 @@ export class DialogService {
}); });
} }
public openAssignFileOwnerDialog($event: MouseEvent, file: FileStatus, cb?: Function) {
$event.stopPropagation();
this._dialog.open(AssignOwnerDialogComponent, {
...dialogConfig,
data: { type: 'file', file: file }
}).afterClosed().subscribe(() => {
if (cb) cb();
});
}
public openProjectDetailsDialog($event: MouseEvent, project: ProjectWrapper) { public openProjectDetailsDialog($event: MouseEvent, project: ProjectWrapper) {
$event.stopPropagation(); $event.stopPropagation();
this._dialog.open(ProjectDetailsDialogComponent, { this._dialog.open(ProjectDetailsDialogComponent, {
@ -151,19 +159,20 @@ export class DialogService {
} }
public openAddProjectDialog(cb?: Function): void { public openAddProjectDialog(cb?: Function): void {
const dialogRef = this._dialog.open(AddEditProjectDialogComponent, { this._dialog.open(AddEditProjectDialogComponent, {
...dialogConfig, ...dialogConfig,
autoFocus: true, autoFocus: true
}); }).afterClosed().subscribe(result => {
dialogRef.afterClosed().subscribe(result => {
if (result && cb) cb(); if (result && cb) cb();
}); });
} }
public openAssignProjectMembersDialog(project: Project) { public openAssignProjectMembersDialog(project: Project, cb?: Function) {
this._dialog.open(AssignProjectMembersDialogComponent, { this._dialog.open(AssignProjectMembersDialogComponent, {
...dialogConfig, ...dialogConfig,
data: project data: project
}).afterClosed().subscribe(result => {
if (result && cb) cb();
}); });
} }
} }

View File

@ -96,7 +96,7 @@
[matTooltip]="'project-overview.report.action.label'|translate"> [matTooltip]="'project-overview.report.action.label'|translate">
<mat-icon svgIcon="red:report"></mat-icon> <mat-icon svgIcon="red:report"></mat-icon>
</button> </button>
<button (click)="assignPeopleToFile($event,fileStatus)" color="accent" mat-icon-button <button (click)="openAssignFileOwnerDialog($event,fileStatus)" color="accent" mat-icon-button
[matTooltip]="'project-overview.assign.action.label'|translate"> [matTooltip]="'project-overview.assign.action.label'|translate">
<mat-icon svgIcon="red:assign"></mat-icon> <mat-icon svgIcon="red:assign"></mat-icon>
</button> </button>
@ -128,7 +128,7 @@
</div> </div>
<div> <div>
<mat-icon svgIcon="red:user"></mat-icon> <mat-icon svgIcon="red:user"></mat-icon>
1 {{ appStateService.activeProject.project.memberIds.length }}
</div> </div>
<div> <div>
<mat-icon svgIcon="red:calendar"></mat-icon> <mat-icon svgIcon="red:calendar"></mat-icon>
@ -155,13 +155,12 @@
<div class="project-team mt-20"> <div class="project-team mt-20">
<div class="all-caps-label" translate="project-overview.project-details.project-team.label"></div> <div class="all-caps-label" translate="project-overview.project-details.project-team.label"></div>
<div class="flex mt-20 members-container"> <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> <redaction-initials-avatar [username]="username" size="large"></redaction-initials-avatar>
</div> </div>
<!-- TODO THIS IS OVERFLOW--> <div class="member">
<!-- <div class="member">--> <div class="oval large white-dark">+{{overflowCount}}</div>
<!-- <div class="oval large white-dark">+2</div>--> </div>
<!-- </div>-->
<div class="member pointer" (click)="openAssignProjectMembersDialog()"> <div class="member pointer" (click)="openAssignProjectMembersDialog()">
<div class="oval red-white large">+</div> <div class="oval red-white large">+</div>
</div> </div>

View File

@ -75,6 +75,14 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
return this.activeProject.memberIds.map(m => this._userService.getName(this._userService.getUserById(m))); 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() { public get ownerName() {
return this._userService.getNameForId(this.activeProject.ownerId); return this._userService.getNameForId(this.activeProject.ownerId);
} }
@ -114,7 +122,15 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
} }
public openAssignProjectMembersDialog() { public openAssignProjectMembersDialog() {
this._dialogService.openAssignProjectMembersDialog(this.activeProject); this._dialogService.openAssignProjectMembersDialog(this.activeProject, () => {
this._getFileStatus();
});
}
public openAssignFileOwnerDialog($event: MouseEvent, file: FileStatus) {
this._dialogService.openAssignFileOwnerDialog($event, file, () => {
this._getFileStatus();
});
} }
public reanalyseFile($event: MouseEvent, fileStatus: FileStatus) { public reanalyseFile($event: MouseEvent, fileStatus: FileStatus) {
@ -149,14 +165,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
return fileStatus === 'PROCESSING' || fileStatus === 'REVIEWED' || true; return fileStatus === 'PROCESSING' || fileStatus === 'REVIEWED' || true;
} }
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);
});
}
public getFileOwnerUsername(fileStatus: FileStatus) { public getFileOwnerUsername(fileStatus: FileStatus) {
return undefined; return this._userService.getNameForId(fileStatus.currentReviewer);
} }
} }

View File

@ -57,15 +57,15 @@ export class AppStateService {
get isActiveProjectOwner() { get isActiveProjectOwner() {
return this._appState.activeProject.project.ownerId === this._userService.userId return this._appState.activeProject?.project?.ownerId === this._userService.userId
} }
get isActiveProjectMember() { 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() { get isActiveFileDocumentReviewer() {
return false; return this._appState.activeFile?.currentReviewer === this._userService.userId;
} }

View File

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

View File

@ -257,15 +257,6 @@ export class ProjectControllerService {
headers = headers.set('Accept', httpHeaderAcceptSelected); 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))}`, return this.httpClient.request<any>('delete', `${this.basePath}/project/members/${encodeURIComponent(String(projectId))}`,
{ {
body: body, body: body,

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "redaction", "name": "redaction",
"version": "0.0.69", "version": "0.0.70",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build-lint-all": "ng lint --project=red-ui-http --fix && ng build --project=red-ui-http && ng lint --project=red-ui --fix && ng build --project=red-ui --prod", "build-lint-all": "ng lint --project=red-ui-http --fix && ng build --project=red-ui-http && ng lint --project=red-ui --fix && ng build --project=red-ui --prod",