diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts
index f5966db37..f26458a8e 100644
--- a/apps/red-ui/src/app/app.module.ts
+++ b/apps/red-ui/src/app/app.module.ts
@@ -64,7 +64,6 @@ import { ManualAnnotationDialogComponent } from './dialogs/manual-redaction-dial
import { ToastComponent } from './components/toast/toast.component';
import { FilterComponent } from './common/filter/filter.component';
import { AppInfoComponent } from './screens/app-info/app-info.component';
-import { SortingComponent } from './components/sorting/sorting.component';
import { TableColNameComponent } from './components/table-col-name/table-col-name.component';
import { ProjectDetailsComponent } from './screens/project-overview-screen/project-details/project-details.component';
import { PageIndicatorComponent } from './screens/file/page-indicator/page-indicator.component';
@@ -77,6 +76,7 @@ import { FileActionsComponent } from './common/file-actions/file-actions.compone
import { TypeAnnotationIconComponent } from './components/type-annotation-icon/type-annotation-icon.component';
import { TypeFilterComponent } from './components/type-filter/type-filter.component';
import { DictionaryAnnotationIconComponent } from './components/dictionary-annotation-icon/dictionary-annotation-icon.component';
+import { BulkActionsComponent } from './screens/project-overview-screen/bulk-actions/bulk-actions.component';
export function HttpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
@@ -108,7 +108,6 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
ToastComponent,
FilterComponent,
AppInfoComponent,
- SortingComponent,
TableColNameComponent,
ProjectDetailsComponent,
PageIndicatorComponent,
@@ -121,7 +120,9 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
FileActionsComponent,
TypeAnnotationIconComponent,
TypeFilterComponent,
- DictionaryAnnotationIconComponent
+ DictionaryAnnotationIconComponent,
+ BulkActionsComponent,
+ FileActionsComponent
],
imports: [
BrowserModule,
diff --git a/apps/red-ui/src/app/common/file-actions/file-actions.component.ts b/apps/red-ui/src/app/common/file-actions/file-actions.component.ts
index 6d5caa78b..5d8b1c77d 100644
--- a/apps/red-ui/src/app/common/file-actions/file-actions.component.ts
+++ b/apps/red-ui/src/app/common/file-actions/file-actions.component.ts
@@ -52,7 +52,7 @@ export class FileActionsComponent implements OnInit, AfterViewInit {
}
openDeleteFileDialog($event: MouseEvent, fileStatusWrapper: FileStatusWrapper) {
- this._dialogService.openDeleteFileDialog($event, fileStatusWrapper.projectId, fileStatusWrapper.fileId, () => {
+ this._dialogService.openDeleteFilesDialog($event, fileStatusWrapper.projectId, [fileStatusWrapper.fileId], () => {
this.actionPerformed.emit('delete');
});
}
diff --git a/apps/red-ui/src/app/common/service/permissions.service.ts b/apps/red-ui/src/app/common/service/permissions.service.ts
index 302f51e02..eda23950d 100644
--- a/apps/red-ui/src/app/common/service/permissions.service.ts
+++ b/apps/red-ui/src/app/common/service/permissions.service.ts
@@ -81,13 +81,17 @@ export class PermissionsService {
return fileStatus.status === 'APPROVED';
}
- canApprove(fileStatus?: FileStatusWrapper) {
+ canSetUnderReview(fileStatus?: FileStatusWrapper) {
if (!fileStatus) {
fileStatus = this._appStateService.activeFile;
}
return fileStatus.status === 'UNDER_APPROVAL' && this.isManagerAndOwner();
}
+ canApprove(fileStatus?: FileStatusWrapper) {
+ return this.canSetUnderReview && !fileStatus.hasRequests;
+ }
+
canSetUnderApproval(fileStatus?: FileStatusWrapper) {
if (!fileStatus) {
fileStatus = this._appStateService.activeFile;
diff --git a/apps/red-ui/src/app/components/sorting/sorting.component.html b/apps/red-ui/src/app/components/sorting/sorting.component.html
deleted file mode 100644
index 9cee56903..000000000
--- a/apps/red-ui/src/app/components/sorting/sorting.component.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
- {{ option.label | translate }}
-
-
-
diff --git a/apps/red-ui/src/app/components/sorting/sorting.component.scss b/apps/red-ui/src/app/components/sorting/sorting.component.scss
deleted file mode 100644
index e69de29bb..000000000
diff --git a/apps/red-ui/src/app/components/sorting/sorting.component.ts b/apps/red-ui/src/app/components/sorting/sorting.component.ts
deleted file mode 100644
index e7a5526b0..000000000
--- a/apps/red-ui/src/app/components/sorting/sorting.component.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
-
-export class SortingOption {
- label?: string;
- order: 'asc' | 'desc';
- column: string;
-}
-
-const SORTING_OPTIONS: { [key: string]: SortingOption[] } = {
- 'project-listing': [
- { label: 'sorting.recent', order: 'desc', column: 'projectDate' },
- { label: 'sorting.alphabetically', order: 'asc', column: 'project.projectName' }
- ],
- 'project-overview': [
- { label: 'sorting.recent', order: 'desc', column: 'added' },
- { label: 'sorting.alphabetically', order: 'asc', column: 'filename' },
- { label: 'sorting.number-of-pages', order: 'asc', column: 'numberOfPages' },
- { label: 'sorting.number-of-analyses', order: 'desc', column: 'numberOfAnalyses' }
- ]
-};
-
-@Component({
- selector: 'redaction-sorting',
- templateUrl: './sorting.component.html',
- styleUrls: ['./sorting.component.scss']
-})
-export class SortingComponent implements OnInit {
- @Input() initialOption: SortingOption;
-
- @Input()
- private type: 'project-overview' | 'project-listing';
-
- @Output()
- private optionChanged = new EventEmitter();
-
- public sortingOptions: SortingOption[];
- public activeOption: SortingOption;
-
- constructor() {}
-
- public ngOnInit(): void {
- if (this.initialOption) {
- this.setOption(this.initialOption);
- }
- }
-
- private _addCustomOption(option: Partial) {
- const customOption = {
- label: 'sorting.custom',
- column: option.column,
- order: option.order
- };
- this.sortingOptions.push(customOption);
- this.activeOption = customOption;
- }
-
- private _resetOptions() {
- if (this.activeOption?.label !== 'sorting.custom') {
- this.sortingOptions = [...SORTING_OPTIONS[this.type]];
- }
- }
-
- public dropdownSelect() {
- this._resetOptions();
- this.optionChanged.emit(this.activeOption);
- }
-
- public setOption(option: { column: string; order: 'asc' | 'desc' }) {
- if (!this.sortingOptions) {
- this._resetOptions();
- }
- const existingOption = this.sortingOptions.find(
- (o) => o.column === option.column && o.order === option.order
- );
- if (existingOption) {
- this.activeOption = existingOption;
- this._resetOptions();
- } else {
- this._addCustomOption(option);
- }
- }
-
- public toggleSort(column: string) {
- if (this.activeOption.column === column) {
- const currentOrder = this.activeOption.order;
- this.setOption({ column, order: currentOrder === 'asc' ? 'desc' : 'asc' });
- } else {
- this.setOption({ column, order: 'asc' });
- }
- this.optionChanged.emit(this.activeOption);
- }
-}
diff --git a/apps/red-ui/src/app/components/table-col-name/table-col-name.component.ts b/apps/red-ui/src/app/components/table-col-name/table-col-name.component.ts
index a127a3292..915491e58 100644
--- a/apps/red-ui/src/app/components/table-col-name/table-col-name.component.ts
+++ b/apps/red-ui/src/app/components/table-col-name/table-col-name.component.ts
@@ -1,5 +1,5 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
-import { SortingOption } from '../sorting/sorting.component';
+import { SortingOption } from '../../utils/sorting.service';
@Component({
selector: 'redaction-table-col-name',
@@ -19,16 +19,8 @@ export class TableColNameComponent implements OnInit {
ngOnInit(): void {}
public get arrowColor(): { up: string; down: string } {
- const up =
- this.activeSortingOption.order === 'desc' &&
- this.activeSortingOption.column === this.column
- ? 'primary'
- : 'accent';
- const down =
- this.activeSortingOption.order === 'asc' &&
- this.activeSortingOption.column === this.column
- ? 'primary'
- : 'accent';
+ const up = this.activeSortingOption.order === 'desc' && this.activeSortingOption.column === this.column ? 'primary' : 'accent';
+ const down = this.activeSortingOption.order === 'asc' && this.activeSortingOption.column === this.column ? 'primary' : 'accent';
return { up, down };
}
}
diff --git a/apps/red-ui/src/app/dialogs/assign-owner-dialog/assign-owner-dialog.component.ts b/apps/red-ui/src/app/dialogs/assign-owner-dialog/assign-owner-dialog.component.ts
index e93a7a232..e6eb29fec 100644
--- a/apps/red-ui/src/app/dialogs/assign-owner-dialog/assign-owner-dialog.component.ts
+++ b/apps/red-ui/src/app/dialogs/assign-owner-dialog/assign-owner-dialog.component.ts
@@ -10,7 +10,7 @@ import { FileStatusWrapper } from '../../screens/file/model/file-status.wrapper'
class DialogData {
type: 'file' | 'project';
project?: Project;
- file?: FileStatusWrapper;
+ files?: FileStatusWrapper[];
}
@Component({
@@ -44,9 +44,13 @@ export class AssignOwnerDialogComponent {
}
if (this.data.type === 'file') {
- const file = this.data.file;
+ const uniqueReviewers = new Set();
+ for (const file of this.data.files) {
+ uniqueReviewers.add(file.currentReviewer);
+ }
+ const singleUser = uniqueReviewers.size === 1 ? uniqueReviewers.values().next().value : [];
this.usersForm = this._formBuilder.group({
- singleUser: [file?.currentReviewer]
+ singleUser: [singleUser]
});
}
}
@@ -61,47 +65,35 @@ export class AssignOwnerDialogComponent {
project.ownerId = ownerId;
await this._appStateService.addOrUpdateProject(project);
this._notificationService.showToastNotification(
- 'Successfully assigned ' +
- this.userService.getNameForId(ownerId) +
- ' to project: ' +
- project.projectName
+ 'Successfully assigned ' + this.userService.getNameForId(ownerId) + ' to project: ' + project.projectName
);
}
if (this.data.type === 'file') {
const reviewerId = this.usersForm.get('singleUser').value;
- await this._statusControllerService
- .assignProjectOwner(
- this._appStateService.activeProjectId,
- this.data.file.fileId,
- reviewerId
- )
- .toPromise();
- this.data.file.currentReviewer = reviewerId;
- this.data.file.reviewerName = this.userService.getNameForId(reviewerId);
- this._notificationService.showToastNotification(
- 'Successfully assigned ' +
- this.userService.getNameForId(reviewerId) +
- ' to file: ' +
- this.data.file.filename
+ const promises = this.data.files.map((file) =>
+ this._statusControllerService.assignProjectOwner(this._appStateService.activeProjectId, file.fileId, reviewerId).toPromise()
);
+
+ await Promise.all(promises);
+ for (const file of this.data.files) {
+ file.currentReviewer = reviewerId;
+ file.reviewerName = this.userService.getNameForId(reviewerId);
+ this._notificationService.showToastNotification(
+ 'Successfully assigned ' + this.userService.getNameForId(reviewerId) + ' to file: ' + file.filename
+ );
+ }
}
} catch (error) {
- this._notificationService.showToastNotification(
- 'Failed: ' + error.error.message,
- null,
- NotificationType.ERROR
- );
+ this._notificationService.showToastNotification('Failed: ' + error.error.message, null, NotificationType.ERROR);
}
this.dialogRef.close();
}
get singleUsersSelectOptions() {
- return this.data.type === 'file'
- ? this._appStateService.activeProject.project.memberIds
- : this.userService.managerUsers.map((m) => m.userId);
+ return this.data.type === 'file' ? this._appStateService.activeProject.project.memberIds : this.userService.managerUsers.map((m) => m.userId);
}
get multiUsersSelectOptions() {
diff --git a/apps/red-ui/src/app/dialogs/dialog.service.ts b/apps/red-ui/src/app/dialogs/dialog.service.ts
index f07b6e792..ee8f80ee9 100644
--- a/apps/red-ui/src/app/dialogs/dialog.service.ts
+++ b/apps/red-ui/src/app/dialogs/dialog.service.ts
@@ -1,12 +1,7 @@
import { Injectable } from '@angular/core';
import { FileDetailsDialogComponent } from './file-details-dialog/file-details-dialog.component';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
-import {
- FileStatus,
- FileUploadControllerService,
- ManualRedactionControllerService,
- Project
-} from '@redaction/red-ui-http';
+import { FileStatus, FileUploadControllerService, ManualRedactionControllerService, Project } from '@redaction/red-ui-http';
import { ConfirmationDialogComponent } from '../common/confirmation-dialog/confirmation-dialog.component';
import { NotificationService, NotificationType } from '../notification/notification.service';
import { TranslateService } from '@ngx-translate/core';
@@ -38,10 +33,7 @@ export class DialogService {
private readonly _manualRedactionControllerService: ManualRedactionControllerService
) {}
- public openFileDetailsDialog(
- $event: MouseEvent,
- file: FileStatus
- ): MatDialogRef {
+ public openFileDetailsDialog($event: MouseEvent, file: FileStatus): MatDialogRef {
$event.stopPropagation();
return this._dialog.open(FileDetailsDialogComponent, {
...dialogConfig,
@@ -49,41 +41,32 @@ export class DialogService {
});
}
- public openDeleteFileDialog(
- $event: MouseEvent,
- projectId: string,
- fileId: string,
- cb?: Function
- ): MatDialogRef {
- $event.stopPropagation();
+ public openDeleteFilesDialog($event: MouseEvent, projectId: string, fileIds: string[], cb?: Function): MatDialogRef {
+ $event?.stopPropagation();
+
const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig);
ref.afterClosed().subscribe((result) => {
if (result) {
- const file = this._appStateService.getFileById(projectId, fileId);
- this._fileUploadControllerService.deleteFile(file.projectId, file.fileId).subscribe(
- async () => {
+ const promises = fileIds
+ .map((fileId) => this._appStateService.getFileById(projectId, fileId))
+ .map((file) => this._fileUploadControllerService.deleteFile(file.projectId, file.fileId).toPromise());
+
+ Promise.all(promises)
+ .then(async () => {
await this._appStateService.reloadActiveProjectFiles();
if (cb) cb();
- },
- () => {
- this._notificationService.showToastNotification(
- this._translateService.instant('delete-file-error', file),
- null,
- NotificationType.ERROR
- );
- }
- );
+ })
+ .catch(() => {
+ this._notificationService.showToastNotification(this._translateService.instant('delete-files-error'), null, NotificationType.ERROR);
+ });
}
});
return ref;
}
- public openManualRedactionDialog(
- $event: ManualRedactionEntryWrapper,
- cb?: Function
- ): MatDialogRef {
+ public openManualRedactionDialog($event: ManualRedactionEntryWrapper, cb?: Function): MatDialogRef {
const ref = this._dialog.open(ManualAnnotationDialogComponent, {
...dialogConfig,
autoFocus: true,
@@ -99,34 +82,24 @@ export class DialogService {
return ref;
}
- public openAcceptSuggestionModal(
- $event: MouseEvent,
- annotation: AnnotationWrapper,
- callback?: Function
- ): MatDialogRef {
+ public openAcceptSuggestionModal($event: MouseEvent, annotation: AnnotationWrapper, callback?: Function): MatDialogRef {
$event.stopPropagation();
const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig);
ref.afterClosed().subscribe((result) => {
if (result) {
- this._manualAnnotationService
- .approveRequest(annotation.id)
- .subscribe((acceptResult) => {
- if (callback) {
- callback(acceptResult);
- }
- });
+ this._manualAnnotationService.approveRequest(annotation.id).subscribe((acceptResult) => {
+ if (callback) {
+ callback(acceptResult);
+ }
+ });
}
});
return ref;
}
- public openRejectSuggestionModal(
- $event: MouseEvent,
- annotation: AnnotationWrapper,
- rejectCallback: () => void
- ): MatDialogRef {
+ public openRejectSuggestionModal($event: MouseEvent, annotation: AnnotationWrapper, rejectCallback: () => void): MatDialogRef {
$event.stopPropagation();
const ref = this._dialog.open(ConfirmationDialogComponent, {
@@ -145,10 +118,7 @@ export class DialogService {
return ref;
}
- public openEditProjectDialog(
- $event: MouseEvent,
- project: Project
- ): MatDialogRef {
+ public openEditProjectDialog($event: MouseEvent, project: Project): MatDialogRef {
$event.stopPropagation();
return this._dialog.open(AddEditProjectDialogComponent, {
...dialogConfig,
@@ -157,11 +127,7 @@ export class DialogService {
});
}
- public openDeleteProjectDialog(
- $event: MouseEvent,
- project: Project,
- cb?: Function
- ): MatDialogRef {
+ public openDeleteProjectDialog($event: MouseEvent, project: Project, cb?: Function): MatDialogRef {
$event.stopPropagation();
const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig);
ref.afterClosed().subscribe(async (result) => {
@@ -173,11 +139,7 @@ export class DialogService {
return ref;
}
- public openAssignProjectMembersAndOwnerDialog(
- $event: MouseEvent,
- project: Project,
- cb?: Function
- ): MatDialogRef {
+ public openAssignProjectMembersAndOwnerDialog($event: MouseEvent, project: Project, cb?: Function): MatDialogRef {
$event?.stopPropagation();
const ref = this._dialog.open(AssignOwnerDialogComponent, {
...dialogConfig,
@@ -189,13 +151,27 @@ export class DialogService {
return ref;
}
- public openAssignFileReviewerDialog(
- file: FileStatus,
- cb?: Function
- ): MatDialogRef {
+ public openAssignFileReviewerDialog(file: FileStatus, cb?: Function): MatDialogRef {
const ref = this._dialog.open(AssignOwnerDialogComponent, {
...dialogConfig,
- data: { type: 'file', file: file }
+ data: { type: 'file', files: [file] }
+ });
+
+ ref.afterClosed().subscribe(() => {
+ if (cb) cb();
+ });
+
+ return ref;
+ }
+
+ public openBulkAssignFileReviewerDialog(fileIds: string[], cb?: Function): MatDialogRef {
+ const projectId = this._appStateService.activeProject.project.projectId;
+ const ref = this._dialog.open(AssignOwnerDialogComponent, {
+ ...dialogConfig,
+ data: {
+ type: 'file',
+ files: fileIds.map((fileId) => this._appStateService.getFileById(projectId, fileId))
+ }
});
ref.afterClosed().subscribe(() => {
@@ -218,11 +194,7 @@ export class DialogService {
return ref;
}
- openRemoveAnnotationModal(
- $event: MouseEvent,
- annotation: AnnotationWrapper,
- callback: () => void
- ) {
+ openRemoveAnnotationModal($event: MouseEvent, annotation: AnnotationWrapper, callback: () => void) {
$event.stopPropagation();
const ref = this._dialog.open(ConfirmationDialogComponent, {
@@ -232,13 +204,11 @@ export class DialogService {
ref.afterClosed().subscribe((result) => {
if (result) {
- this._manualAnnotationService
- .removeOrSuggestRemoveAnnotation(annotation)
- .subscribe(() => {
- if (callback) {
- callback();
- }
- });
+ this._manualAnnotationService.removeOrSuggestRemoveAnnotation(annotation).subscribe(() => {
+ if (callback) {
+ callback();
+ }
+ });
}
});
diff --git a/apps/red-ui/src/app/icons/icons.module.ts b/apps/red-ui/src/app/icons/icons.module.ts
index a01107d44..9f2df675b 100644
--- a/apps/red-ui/src/app/icons/icons.module.ts
+++ b/apps/red-ui/src/app/icons/icons.module.ts
@@ -36,6 +36,8 @@ export class IconsModule {
'pages',
'plus',
'preview',
+ 'radio-indeterminate',
+ 'radio-selected',
'refresh',
'report',
'secret',
@@ -51,11 +53,7 @@ export class IconsModule {
];
for (const icon of icons) {
- iconRegistry.addSvgIconInNamespace(
- 'red',
- icon,
- sanitizer.bypassSecurityTrustResourceUrl(`/assets/icons/general/${icon}.svg`)
- );
+ iconRegistry.addSvgIconInNamespace('red', icon, sanitizer.bypassSecurityTrustResourceUrl(`/assets/icons/general/${icon}.svg`));
}
}
}
diff --git a/apps/red-ui/src/app/screens/project-listing-screen/project-listing-screen.component.html b/apps/red-ui/src/app/screens/project-listing-screen/project-listing-screen.component.html
index 2c63eade2..643961d01 100644
--- a/apps/red-ui/src/app/screens/project-listing-screen/project-listing-screen.component.html
+++ b/apps/red-ui/src/app/screens/project-listing-screen/project-listing-screen.component.html
@@ -40,33 +40,19 @@