RED-6378: fix selectable unassigned

This commit is contained in:
Dan Percic 2023-03-11 21:09:58 +02:00
parent 322c8e9fe3
commit 4d5dac5a30
7 changed files with 130 additions and 123 deletions

View File

@ -29,7 +29,7 @@ export class BulkActionsService {
this._assignFiles(files, WorkflowFileStatuses.UNDER_APPROVAL, true);
} else {
this._loadingService.start();
await firstValueFrom(this._filesService.setUnderApprovalFor(files, dossier.id, dossier.approverIds[0]));
await this._filesService.setUnderApproval(files, dossier.approverIds[0]);
this._loadingService.stop();
}
}
@ -82,13 +82,13 @@ export class BulkActionsService {
async backToUnderReview(files: File[]): Promise<void> {
this._loadingService.start();
await firstValueFrom(this._filesService.setUnderReviewFor(files, files[0].dossierId));
await this._filesService.setUnderReviewFor(files);
this._loadingService.stop();
}
async setToNew(files: File[]): Promise<void> {
this._loadingService.start();
await firstValueFrom(this._filesService.setToNewFor(files, files[0].dossierId));
await this._filesService.setToNew(files);
this._loadingService.stop();
}
@ -113,13 +113,13 @@ export class BulkActionsService {
}),
async () => {
this._loadingService.start();
await firstValueFrom(this._filesService.setApprovedFor(files, files[0].dossierId));
await this._filesService.setApproved(files);
this._loadingService.stop();
},
);
} else {
this._loadingService.start();
await firstValueFrom(this._filesService.setApprovedFor(files, files[0].dossierId));
await this._filesService.setApproved(files);
this._loadingService.stop();
}
}

View File

@ -7,7 +7,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { UserService } from '@users/user.service';
import { FilesService } from '@services/files/files.service';
import { TranslateService } from '@ngx-translate/core';
import { combineLatest, combineLatestWith, firstValueFrom, Observable, switchMap } from 'rxjs';
import { combineLatest, combineLatestWith, Observable, switchMap } from 'rxjs';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
@ -99,22 +99,21 @@ export class UserManagementComponent {
async assignReviewer(file: File, user: User | string) {
const assigneeId = typeof user === 'string' ? user : user?.id;
const reviewerName = this.userService.getName(assigneeId);
const { dossierId, filename } = file;
this.loadingService.start();
if (!assigneeId || file.isApproved) {
await firstValueFrom(this.filesService.setAssignee([file], dossierId, assigneeId));
await this.filesService.setAssignee(file, assigneeId);
} else if (file.isNew || file.isUnderReview) {
await firstValueFrom(this.filesService.setReviewerFor([file], dossierId, assigneeId));
await this.filesService.setReviewer(file, assigneeId);
} else {
await firstValueFrom(this.filesService.setUnderApprovalFor([file], dossierId, assigneeId));
await this.filesService.setUnderApproval(file, assigneeId);
}
this.loadingService.stop();
this.toaster.success(_('assignment.reviewer'), { params: { reviewerName, filename } });
const translateParams = { reviewerName: this.userService.getName(assigneeId), filename: file.filename };
this.toaster.success(_('assignment.reviewer'), { params: translateParams });
this.editingReviewer = false;
}

View File

@ -1,5 +1,5 @@
import { List } from '@iqser/common-ui';
import { AnnotationWrapper } from '../../../models/file/annotation.wrapper';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ALLOWED_KEYBOARD_SHORTCUTS } from './constants';
export function stopAndPrevent<T extends Event>($event: T) {

View File

@ -188,7 +188,7 @@ export class FileActionsComponent implements OnChanges {
{
id: 'set-file-to-new-btn-' + fileId,
type: ActionTypes.circleBtn,
action: ($event: MouseEvent) => this._setToNew($event),
action: ($event: MouseEvent) => this.#setToNew($event),
tooltip: _('dossier-overview.back-to-new'),
icon: 'red:undo',
show: this.showSetToNew,
@ -295,7 +295,7 @@ export class FileActionsComponent implements OnChanges {
async setFileApproved($event: MouseEvent) {
$event.stopPropagation();
if (!this.file.analysisRequired && !this.file.hasUpdates) {
await this._setFileApproved();
await this.#setFileApproved();
return;
}
@ -314,7 +314,7 @@ export class FileActionsComponent implements OnChanges {
: null,
denyText: this.file.analysisRequired ? _('confirmation-dialog.approve-file-without-analysis.denyText') : null,
}),
() => this._setFileApproved(),
() => this.#setFileApproved(),
);
}
@ -469,16 +469,16 @@ export class FileActionsComponent implements OnChanges {
this._changeRef.markForCheck();
}
private async _setFileApproved() {
async #setFileApproved() {
this._loadingService.start();
await firstValueFrom(this._filesService.setApprovedFor([this.file], this.file.dossierId));
await this._filesService.setApproved(this.file);
this._loadingService.stop();
}
private async _setToNew($event: MouseEvent) {
async #setToNew($event: MouseEvent) {
$event.stopPropagation();
this._loadingService.start();
await firstValueFrom(this._filesService.setToNewFor([this.file], this.file.dossierId));
await this._filesService.setToNew(this.file);
this._loadingService.stop();
}
}

View File

@ -1,14 +1,13 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UserService } from '@users/user.service';
import { IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Dossier, File, WorkflowFileStatus, WorkflowFileStatuses } from '@red/domain';
import { getCurrentUser, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { FormBuilder, Validators } from '@angular/forms';
import { File, User, WorkflowFileStatus, WorkflowFileStatuses } from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { FilesService } from '@services/files/files.service';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { PermissionsService } from '@services/permissions.service';
import { firstValueFrom } from 'rxjs';
import { moveElementInArray } from '@utils/functions';
class DialogData {
@ -24,59 +23,37 @@ class DialogData {
})
export class AssignReviewerApproverDialogComponent {
readonly iconButtonTypes = IconButtonTypes;
readonly form: UntypedFormGroup;
readonly mode: 'reviewer' | 'approver';
dossier: Dossier;
readonly currentUser = getCurrentUser<User>();
readonly mode = this.#mode;
readonly dossier = this._activeDossiersService.find(this.data.files[0].dossierId);
readonly userOptions = this.#userOptions;
readonly form = this.#form;
constructor(
readonly userService: UserService,
private readonly _toaster: Toaster,
private readonly _formBuilder: UntypedFormBuilder,
private readonly _formBuilder: FormBuilder,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _filesService: FilesService,
private readonly _loadingService: LoadingService,
readonly permissionsService: PermissionsService,
private readonly _dialogRef: MatDialogRef<AssignReviewerApproverDialogComponent, boolean>,
@Inject(MAT_DIALOG_DATA) readonly data: DialogData,
) {
this.dossier = this._activeDossiersService.find(this.data.files[0].dossierId);
this.form = this._getForm();
this.mode =
data.targetStatus === WorkflowFileStatuses.UNDER_APPROVAL || data.targetStatus === WorkflowFileStatuses.APPROVED
? 'approver'
: 'reviewer';
}
) {}
get selectedUser(): string {
const value = this.form.get('user').value;
const value = this.form.controls.user.value;
return value === 'undefined' ? undefined : value;
}
get userOptions() {
const unassignUser = this._canUnassignFiles && this.data.withUnassignedOption ? ['undefined'] : [];
const cannotAssignUser = !this.permissionsService.canAssignUser(this.data.files, this.dossier);
if (this.mode === 'reviewer') {
if (this.dossier.hasReviewers && cannotAssignUser) {
return [...unassignUser];
}
return this._customSort([...this.dossier.memberIds, ...unassignUser]);
}
if (this.dossier.approverIds.length > 1 && cannotAssignUser) {
return [...unassignUser];
}
return this._customSort([...this.dossier.approverIds, ...unassignUser]);
}
get changed(): boolean {
if (this.data.ignoreChanged) {
return true;
}
const selectedUser = this.selectedUser;
for (const file of this.data.files) {
if (file.assignee !== this.selectedUser) {
if (file.assignee !== selectedUser) {
return true;
}
}
@ -84,71 +61,100 @@ export class AssignReviewerApproverDialogComponent {
return false;
}
private get _canUnassignFiles() {
get #mode() {
const isUnderApproval = this.data.targetStatus === WorkflowFileStatuses.UNDER_APPROVAL;
const isApproved = this.data.targetStatus === WorkflowFileStatuses.APPROVED;
return isUnderApproval || isApproved ? 'approver' : 'reviewer';
}
get #userOptions() {
const unassignUser = this.#canUnassignUser && this.data.withUnassignedOption ? ['undefined'] : [];
const cannotAssignUser = !this.permissionsService.canAssignUser(this.data.files, this.dossier);
if (this.mode === 'reviewer') {
if (this.dossier.hasReviewers && cannotAssignUser) {
return [...unassignUser];
}
return this.#customSort([...this.dossier.memberIds, ...unassignUser]);
}
if (this.dossier.approverIds.length > 1 && cannotAssignUser) {
return [...unassignUser];
}
return this.#customSort([...this.dossier.approverIds, ...unassignUser]);
}
get #canUnassignUser() {
return this.permissionsService.canUnassignUser(this.data.files, this.dossier);
}
/** Initialize the form with:
* the id of the current reviewer of the files list if there is only one reviewer for all of them;
* or the id of the current user
**/
private get _uniqueReviewers(): Set<string> {
get #uniqueReviewers(): Set<string> {
const uniqueReviewers = new Set<string>();
for (const file of this.data.files) {
if (file.assignee) {
uniqueReviewers.add(file.assignee);
}
}
return uniqueReviewers;
}
private get _user(): string {
get #user(): string {
const userOptions = this.userOptions;
if (this.data.withCurrentUserAsDefault && userOptions.includes(this.userService.currentUser.id)) {
return this.userService.currentUser.id;
if (this.data.withCurrentUserAsDefault && userOptions.includes(this.currentUser.id)) {
return this.currentUser.id;
}
const uniqueReviewers = [...this._uniqueReviewers.values()];
const user = uniqueReviewers.length === 1 ? uniqueReviewers[0] : this.userService.currentUser.id;
const uniqueReviewers = [...this.#uniqueReviewers.values()];
const user = uniqueReviewers.length === 1 ? uniqueReviewers[0] : this.currentUser.id;
return userOptions.indexOf(user) >= 0 ? userOptions[userOptions.indexOf(user)] : user;
}
get #form() {
const user = this.#user;
return this._formBuilder.group({
// Allow a null reviewer if a previous reviewer exists (= it's not the first assignment) & current user is allowed to unassign
user: [user, this.#canUnassignUser && !user ? Validators.required : null],
});
}
async save() {
this._loadingService.start();
const selectedUser = this.selectedUser;
try {
if (!this.selectedUser || this.data.targetStatus === WorkflowFileStatuses.APPROVED) {
await firstValueFrom(this._filesService.setAssignee(this.data.files, this.dossier.id, this.selectedUser));
if (!selectedUser || this.data.targetStatus === WorkflowFileStatuses.APPROVED) {
await this._filesService.setAssignee(this.data.files, selectedUser);
} else if (this.mode === 'reviewer') {
await firstValueFrom(this._filesService.setReviewerFor(this.data.files, this.dossier.id, this.selectedUser));
await this._filesService.setReviewer(this.data.files, selectedUser);
} else {
await firstValueFrom(this._filesService.setUnderApprovalFor(this.data.files, this.dossier.id, this.selectedUser));
await this._filesService.setUnderApproval(this.data.files, selectedUser);
}
} catch (error) {
this._toaster.error(_('error.http.generic'), { params: error });
}
this._loadingService.stop();
this._loadingService.stop();
this._dialogRef.close(true);
}
private _getForm(): UntypedFormGroup {
return this._formBuilder.group({
// Allow a null reviewer if a previous reviewer exists (= it's not the first assignment) & current user is allowed to unassign
user: [this._user, this._canUnassignFiles && !this._user ? Validators.required : null],
});
}
private _customSort(ids: string[]) {
#customSort(ids: string[]) {
let sorted = ids.sort((a, b) => this.userService.getName(a).localeCompare(this.userService.getName(b)));
if (this.data.files.length === 1 && this.data.files[0].assignee) {
const fileHasAssignee = this.data.files.length === 1 && this.data.files[0].assignee;
if (fileHasAssignee) {
sorted = moveElementInArray(sorted, this.data.files[0].assignee, 0);
}
if (this.data.withUnassignedOption) {
sorted = moveElementInArray(sorted, 'undefined', this.data.files[0].assignee && this.data.files.length === 1 ? 1 : 0);
if (sorted.includes('undefined')) {
sorted = moveElementInArray(sorted, 'undefined', fileHasAssignee ? 1 : 0);
}
return sorted;
}
}

View File

@ -1,10 +1,9 @@
import { Injectable } from '@angular/core';
import { UserService } from '@users/user.service';
import { Dossier, File, User, WorkflowFileStatus, WorkflowFileStatuses } from '@red/domain';
import { DossiersDialogService } from './dossiers-dialog.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { FilesService } from '@services/files/files.service';
import { ConfirmationDialogInput, LoadingService, Toaster } from '@iqser/common-ui';
import { ConfirmationDialogInput, getCurrentUser, LoadingService, Toaster } from '@iqser/common-ui';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { firstValueFrom } from 'rxjs';
@ -12,18 +11,15 @@ const atLeastOneAssignee = (files: File[]) => files.reduce((acc, fs) => acc || !
@Injectable()
export class FileAssignService {
readonly currentUser: User;
readonly currentUser = getCurrentUser<User>();
constructor(
userService: UserService,
private readonly _toaster: Toaster,
private readonly _filesService: FilesService,
private readonly _loadingService: LoadingService,
private readonly _dialogService: DossiersDialogService,
private readonly _activeDossiersService: ActiveDossiersService,
) {
this.currentUser = userService.currentUser;
}
) {}
async assignToMe(files: File[]): Promise<unknown> {
const assignReq = async () => {
@ -31,10 +27,11 @@ export class FileAssignService {
if (files[0].isNew) {
await this._makeAssignFileRequest(this.currentUser.id, 'UNDER_REVIEW', files);
} else {
await firstValueFrom(this._filesService.setAssignee(files, files[0].dossierId, this.currentUser.id));
await this._filesService.setAssignee(files, this.currentUser.id);
}
this._loadingService.stop();
};
if (atLeastOneAssignee(files)) {
const dialogInput = new ConfirmationDialogInput({
title: _('confirmation-dialog.assign-file-to-me.title'),
@ -88,17 +85,19 @@ export class FileAssignService {
private async _makeAssignFileRequest(userId: string, targetStatus: WorkflowFileStatus, files: File[]) {
this._loadingService.start();
try {
if (!userId || targetStatus === WorkflowFileStatuses.APPROVED) {
await firstValueFrom(this._filesService.setAssignee(files, files[0].dossierId, userId));
await this._filesService.setAssignee(files, userId);
} else if (targetStatus === WorkflowFileStatuses.UNDER_REVIEW) {
await firstValueFrom(this._filesService.setReviewerFor(files, files[0].dossierId, userId));
await this._filesService.setReviewer(files, userId);
} else {
await firstValueFrom(this._filesService.setUnderApprovalFor(files, files[0].dossierId, userId));
await this._filesService.setUnderApproval(files, userId);
}
} catch (error) {
this._toaster.error(_('error.http.generic'), { params: error });
}
this._loadingService.stop();
}

View File

@ -1,13 +1,15 @@
import { Injectable } from '@angular/core';
import { EntitiesService, List, mapEach, RequiredParam, Validate } from '@iqser/common-ui';
import { EntitiesService, isArray, List, mapEach, QueryParam, RequiredParam, Validate } from '@iqser/common-ui';
import { File, IFile } from '@red/domain';
import { Observable } from 'rxjs';
import { firstValueFrom, Observable } from 'rxjs';
import { UserService } from '@users/user.service';
import { FilesMapService } from './files-map.service';
import { map, switchMap, tap } from 'rxjs/operators';
import { DossierStatsService } from '../dossiers/dossier-stats.service';
import { NGXLogger } from 'ngx-logger';
const asList = <T>(value: T | List<T>): List<T> => (isArray(value) ? value : [value]);
@Injectable({
providedIn: 'root',
})
@ -44,46 +46,47 @@ export class FilesService extends EntitiesService<IFile, File> {
}
@Validate()
setAssignee(@RequiredParam() files: List<File>, @RequiredParam() dossierId: string, assigneeId: string) {
const url = `${this._defaultModelPath}/set-assignee/${dossierId}/bulk`;
const fileIds = files.map(f => f.id);
return this._post<unknown>(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe(switchMap(() => this.loadAll(dossierId)));
async setAssignee(@RequiredParam() files: File | List<File>, assigneeId: string) {
const _files = asList(files);
const url = `${this._defaultModelPath}/set-assignee/${_files[0].dossierId}/bulk`;
return this.#makePost(_files, url, [{ key: 'assigneeId', value: assigneeId }]);
}
@Validate()
setToNewFor(@RequiredParam() files: List<File>, @RequiredParam() dossierId: string) {
const url = `${this._defaultModelPath}/new/${dossierId}/bulk`;
const fileIds = files.map(f => f.id);
return this._post<unknown>(fileIds, url).pipe(switchMap(() => this.loadAll(dossierId)));
async setToNew(@RequiredParam() files: File | List<File>) {
const _files = asList(files);
return this.#makePost(_files, `${this._defaultModelPath}/new/${_files[0].dossierId}/bulk`);
}
@Validate()
setUnderApprovalFor(@RequiredParam() files: List<File>, @RequiredParam() dossierId: string, assigneeId: string) {
const url = `${this._defaultModelPath}/under-approval/${dossierId}/bulk`;
const fileIds = files.map(f => f.id);
return this._post<unknown>(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe(switchMap(() => this.loadAll(dossierId)));
async setUnderApproval(@RequiredParam() files: File | List<File>, assigneeId: string) {
const _files = asList(files);
const url = `${this._defaultModelPath}/under-approval/${_files[0].dossierId}/bulk`;
return this.#makePost(_files, url, [{ key: 'assigneeId', value: assigneeId }]);
}
@Validate()
setReviewerFor(@RequiredParam() files: List<File>, @RequiredParam() dossierId: string, assigneeId: string) {
const url = `${this._defaultModelPath}/under-review/${dossierId}/bulk`;
const fileIds = files.map(f => f.id);
return this._post<unknown>(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe(switchMap(() => this.loadAll(dossierId)));
async setReviewer(@RequiredParam() files: File | List<File>, assigneeId: string) {
const _files = asList(files);
const url = `${this._defaultModelPath}/under-review/${_files[0].dossierId}/bulk`;
return this.#makePost(_files, url, [{ key: 'assigneeId', value: assigneeId }]);
}
@Validate()
setApprovedFor(@RequiredParam() files: List<File>, @RequiredParam() dossierId: string) {
const fileIds = files.map(f => f.id);
return this._post<unknown>(fileIds, `${this._defaultModelPath}/approved/${dossierId}/bulk`).pipe(
switchMap(() => this.loadAll(dossierId)),
);
async setApproved(@RequiredParam() files: File | List<File>) {
const _files = asList(files);
return this.#makePost(_files, `${this._defaultModelPath}/approved/${_files[0].dossierId}/bulk`);
}
@Validate()
setUnderReviewFor(@RequiredParam() files: List<File>, @RequiredParam() dossierId: string) {
async setUnderReviewFor(@RequiredParam() files: File | List<File>) {
const _files = asList(files);
return this.#makePost(_files, `${this._defaultModelPath}/under-review/${_files[0].dossierId}/bulk`);
}
async #makePost(files: List<File>, url: string, queryParams?: List<QueryParam>) {
const fileIds = files.map(f => f.id);
return this._post<unknown>(fileIds, `${this._defaultModelPath}/under-review/${dossierId}/bulk`).pipe(
switchMap(() => this.loadAll(dossierId)),
);
await firstValueFrom(this._post(fileIds, url, queryParams));
return firstValueFrom(this.loadAll(files[0].dossierId));
}
}