TSK-1647: Implemented global frontend error handling using backend error keys

Co-authored-by: Tristan Eisermann<19949441+Tristan2357@users.noreply.github.com>
Co-authored-by: Tim Gerversmann<72377965+tge20@users.noreply.github.com>
Co-authored-by: Sofie Hofmann<29145005+sofie29@users.noreply.github.com>
This commit is contained in:
Mustapha Zorgati 2021-07-01 13:11:36 +02:00
parent 34d2bbfa92
commit 8edb488bd3
47 changed files with 397 additions and 694 deletions

View File

@ -43,12 +43,13 @@ class ServiceLevelHandler {
ServiceLevelHandler(
InternalTaskanaEngine taskanaEngine,
TaskMapper taskMapper,
AttachmentMapper attachmentMapper) {
AttachmentMapper attachmentMapper,
TaskServiceImpl taskServiceImpl) {
this.taskanaEngine = taskanaEngine;
this.taskMapper = taskMapper;
this.attachmentMapper = attachmentMapper;
converter = taskanaEngine.getEngine().getWorkingDaysToDaysConverter();
taskServiceImpl = (TaskServiceImpl) taskanaEngine.getEngine().getTaskService();
this.taskServiceImpl = taskServiceImpl;
}
// use the same algorithm as setPlannedPropertyOfTasksImpl to refresh

View File

@ -113,7 +113,8 @@ public class TaskServiceImpl implements TaskService {
this.createTaskPreprocessorManager = taskanaEngine.getCreateTaskPreprocessorManager();
this.taskTransferrer = new TaskTransferrer(taskanaEngine, taskMapper, this);
this.taskCommentService = new TaskCommentServiceImpl(taskanaEngine, taskCommentMapper, this);
this.serviceLevelHandler = new ServiceLevelHandler(taskanaEngine, taskMapper, attachmentMapper);
this.serviceLevelHandler =
new ServiceLevelHandler(taskanaEngine, taskMapper, attachmentMapper, this);
this.attachmentHandler = new AttachmentHandler(attachmentMapper, classificationService);
}

View File

@ -55,6 +55,7 @@ context('TASKANA Workbaskets', () => {
cy.wait(Cypress.env('dropdownWait'));
cy.get('mat-option').contains('Clearance').click();
cy.saveWorkbaskets();
cy.wait(3050); //wait for toasts to disappear
// assure that its Clearance now
cy.get('mat-form-field').contains('mat-form-field', 'Type').contains('Clearance').should('be.visible');

View File

@ -34,6 +34,8 @@
"@circlon/angular-tree-component": "11.0.4",
"@ngxs/store": "3.7.2",
"angular-svg-icon": "12.0.0",
"@ngneat/hot-toast": "3.1.0",
"@ngneat/overview": "2.0.2",
"chart.js": "2.9.4",
"core-js": "3.15.1",
"file-saver": "2.0.5",

View File

@ -12,7 +12,6 @@ import { ClassificationCategoriesService } from '../../../shared/services/classi
import { AccessItemsManagementState } from '../../../shared/store/access-items-management-store/access-items-management.state';
import { Observable } from 'rxjs';
import { GetAccessItems } from '../../../shared/store/access-items-management-store/access-items-management.actions';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { TypeAheadComponent } from '../../../shared/components/type-ahead/type-ahead.component';
import { TypeaheadModule } from 'ngx-bootstrap/typeahead';
@ -78,7 +77,6 @@ describe('AccessItemsManagementComponent', () => {
NgxsModule.forRoot([EngineConfigurationState, AccessItemsManagementState]),
FormsModule,
ReactiveFormsModule,
MatSnackBarModule,
MatDialogModule,
TypeaheadModule.forRoot(),
BrowserAnimationsModule,

View File

@ -145,7 +145,8 @@ export class AccessItemsManagementComponent implements OnInit {
revokeAccess() {
this.notificationService.showDialog(
`You are going to delete all access related: ${this.accessId.accessId}. Can you confirm this action?`,
'ACCESS_ITEM_MANAGEMENT_REVOKE_ACCESS',
{ accessId: this.accessId.accessId },
() => {
this.store
.dispatch(new RemoveAccessItemsPermissions(this.accessId.accessId))

View File

@ -36,7 +36,7 @@
<mat-icon class="action-toolbar__aquamarine-button">content_copy</mat-icon>
<span>Copy</span>
</button>
<button mat-menu-item class="action-toolbar__dropdown" matTooltip="Delete this classification" (click)="onRemoveClassification()">
<button *ngIf="this.classification?.classificationId" mat-menu-item class="action-toolbar__dropdown" matTooltip="Delete this classification" (click)="onRemoveClassification()">
<mat-icon class="action-toolbar__red-button">delete</mat-icon>
<span>Delete</span>
</button>

View File

@ -90,9 +90,9 @@ const formsValidatorServiceSpy: Partial<FormsValidatorService> = {
};
const notificationServiceSpy: Partial<NotificationService> = {
showToast: jest.fn().mockReturnValue(of()),
showDialog: jest.fn().mockReturnValue(of()),
triggerError: jest.fn().mockReturnValue(of())
showWarning: jest.fn().mockReturnValue(of()),
showSuccess: jest.fn().mockReturnValue(of()),
showDialog: jest.fn().mockReturnValue(of())
};
describe('ClassificationDetailsComponent', () => {
@ -157,9 +157,9 @@ describe('ClassificationDetailsComponent', () => {
it('should show warning when onCopy() is called and isCreatingNewClassification is true', () => {
component.isCreatingNewClassification = true;
const notificationService = TestBed.inject(NotificationService);
const showToastSpy = jest.spyOn(notificationService, 'showToast');
const showWarningSpy = jest.spyOn(notificationService, 'showWarning');
component.onCopy();
expect(showToastSpy).toHaveBeenCalled();
expect(showWarningSpy).toHaveBeenCalled();
});
it('should dispatch action when onCopy() is called and isCreatingNewClassification is false', async () => {
@ -203,22 +203,6 @@ describe('ClassificationDetailsComponent', () => {
expect(isActionDispatched).toBe(true);
});
it('should trigger an error in removeClassificationConfirmation() when classification does not exist', () => {
component.classification = undefined;
const notificationService = TestBed.inject(NotificationService);
const triggerErrorSpy = jest.spyOn(notificationService, 'triggerError');
component.removeClassificationConfirmation();
expect(triggerErrorSpy).toHaveBeenCalled();
});
it('should trigger an error in removeClassificationConfirmation() when classificationId does not exist', () => {
component.classification = { key: 'Key01' };
const notificationService = TestBed.inject(NotificationService);
const triggerErrorSpy = jest.spyOn(notificationService, 'triggerError');
component.removeClassificationConfirmation();
expect(triggerErrorSpy).toHaveBeenCalled();
});
it('should dispatch action in removeClassificationConfirmation() when classification and classificationId exist', () => {
component.classification = { classificationId: 'ID01' };
const requestInProgressService = TestBed.inject(RequestInProgressService);
@ -298,6 +282,16 @@ describe('ClassificationDetailsComponent', () => {
expect(buttonsInDropdown.length).toEqual(3);
});
it('should not show delete button when creating or copying a Classification', () => {
component.classification.classificationId = null;
const button = debugElement.nativeElement.querySelector('#action-toolbar__more-buttons');
expect(button).toBeTruthy();
button.click();
fixture.detectChanges();
const buttonsInDropdown = debugElement.queryAll(By.css('.action-toolbar__dropdown'));
expect(buttonsInDropdown.length).toEqual(2);
});
it('should call onCopy() when button is clicked', () => {
const button = debugElement.nativeElement.querySelector('#action-toolbar__more-buttons');
expect(button).toBeTruthy();

View File

@ -14,7 +14,6 @@ import { map, take, takeUntil } from 'rxjs/operators';
import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors';
import { ClassificationSelectors } from 'app/shared/store/classification-store/classification.selectors';
import { Location } from '@angular/common';
import { NOTIFICATION_TYPES } from '../../../shared/models/notifications';
import { NotificationService } from '../../../shared/services/notifications/notification.service';
import { ClassificationCategoryImages, CustomField, getCustomFields } from '../../../shared/models/customisation';
import { Classification } from '../../../shared/models/classification';
@ -121,13 +120,13 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
.dispatch(new RestoreSelectedClassification(this.classification.classificationId))
.pipe(take(1))
.subscribe(() => {
this.notificationsService.showToast(NOTIFICATION_TYPES.INFO_ALERT);
this.notificationsService.showSuccess('CLASSIFICATION_RESTORE');
});
}
onCopy() {
if (this.isCreatingNewClassification) {
this.notificationsService.showToast(NOTIFICATION_TYPES.WARNING_CANT_COPY);
this.notificationsService.showWarning('CLASSIFICATION_COPY_NOT_CREATED');
} else {
this.store.dispatch(new CopyClassification());
}
@ -165,27 +164,20 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
this.store
.dispatch(new SaveCreatedClassification(this.classification))
.pipe(take(1))
.subscribe(
(store) => {
this.notificationsService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_2,
new Map<string, string>([['classificationKey', store.classification.selectedClassification.key]])
);
this.location.go(
this.location
.path()
.replace(
/(classifications).*/g,
`classifications/(detail:${store.classification.selectedClassification.classificationId})`
)
);
this.afterRequest();
},
(error) => {
this.notificationsService.triggerError(NOTIFICATION_TYPES.CREATE_ERR, error);
this.afterRequest();
}
);
.subscribe((store) => {
this.notificationsService.showSuccess('CLASSIFICATION_CREATE', {
classificationKey: store.classification.selectedClassification.key
});
this.location.go(
this.location
.path()
.replace(
/(classifications).*/g,
`classifications/(detail:${store.classification.selectedClassification.classificationId})`
)
);
this.afterRequest();
});
} else {
try {
this.store
@ -193,13 +185,11 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
.pipe(take(1))
.subscribe(() => {
this.afterRequest();
this.notificationsService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_3,
new Map<string, string>([['classificationKey', this.classification.key]])
);
this.notificationsService.showSuccess('CLASSIFICATION_UPDATE', {
classificationKey: this.classification.key
});
});
} catch (error) {
this.notificationsService.triggerError(NOTIFICATION_TYPES.SAVE_ERR, error);
this.afterRequest();
}
}
@ -207,26 +197,20 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
onRemoveClassification() {
this.notificationsService.showDialog(
`You are going to delete classification: ${this.classification.key}. Can you confirm this action?`,
'CLASSIFICATION_DELETE',
{ classificationKey: this.classification.key },
this.removeClassificationConfirmation.bind(this)
);
}
removeClassificationConfirmation() {
if (!this.classification || !this.classification.classificationId) {
this.notificationsService.triggerError(NOTIFICATION_TYPES.SELECT_ERR);
return;
}
this.requestInProgressService.setRequestInProgress(true);
this.store
.dispatch(new RemoveSelectedClassification())
.pipe(take(1))
.subscribe(() => {
this.notificationsService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_4,
new Map<string, string>([['classificationKey', this.classification.key]])
);
this.notificationsService.showSuccess('CLASSIFICATION_REMOVE', { classificationKey: this.classification.key });
this.afterRequest();
});
this.location.go(this.location.path().replace(/(classifications).*/g, 'classifications'));

View File

@ -42,8 +42,7 @@ xdescribe('ImportExportComponent', () => {
const notificationServiceSpy = jest.fn().mockImplementation(
(): Partial<NotificationService> => ({
showDialog: showDialogFn,
showToast: showDialogFn,
triggerError: showDialogFn
showSuccess: showDialogFn
})
);

View File

@ -6,8 +6,6 @@ import { TaskanaType } from 'app/shared/models/taskana-type';
import { environment } from 'environments/environment';
import { UploadService } from 'app/shared/services/upload/upload.service';
import { ImportExportService } from 'app/administration/services/import-export.service';
import { HttpErrorResponse } from '@angular/common/http';
import { NOTIFICATION_TYPES } from '../../../shared/models/notifications';
import { NotificationService } from '../../../shared/services/notifications/notification.service';
@Component({
@ -23,15 +21,13 @@ export class ImportExportComponent implements OnInit {
selectedFileInput;
domains: string[] = [];
errorWhileUploadingText: string;
constructor(
private domainService: DomainService,
private workbasketDefinitionService: WorkbasketDefinitionService,
private classificationDefinitionService: ClassificationDefinitionService,
private notificationsService: NotificationService,
public uploadService: UploadService,
private errorsService: NotificationService,
private notificationService: NotificationService,
private importExportService: ImportExportService
) {}
@ -85,7 +81,7 @@ export class ImportExportComponent implements OnInit {
check = true;
} else {
file.value = '';
this.errorsService.triggerError(NOTIFICATION_TYPES.FILE_ERR);
this.notificationService.showError('IMPORT_EXPORT_UPLOAD_FILE_FORMAT');
}
return check;
}
@ -98,32 +94,32 @@ export class ImportExportComponent implements OnInit {
private onReadyStateChangeHandler(event) {
if (event.readyState === 4 && event.status >= 400) {
let title;
let key: NOTIFICATION_TYPES;
let key = 'FALLBACK';
if (event.status === 401) {
key = NOTIFICATION_TYPES.IMPORT_ERR_1;
title = 'Import was not successful, you have no access to apply this operation.';
key = 'IMPORT_EXPORT_UPLOAD_FAILED_AUTH';
} else if (event.status === 404) {
key = NOTIFICATION_TYPES.IMPORT_ERR_2;
key = 'IMPORT_EXPORT_UPLOAD_FAILED_NOT_FOUND';
} else if (event.status === 409) {
key = NOTIFICATION_TYPES.IMPORT_ERR_3;
key = 'IMPORT_EXPORT_UPLOAD_FAILED_CONFLICTS';
} else if (event.status === 413) {
key = NOTIFICATION_TYPES.IMPORT_ERR_4;
key = 'IMPORT_EXPORT_UPLOAD_FAILED_SIZE';
}
this.errorHandler(key, event);
} else if (event.readyState === 4 && event.status === 200) {
this.notificationsService.showToast(NOTIFICATION_TYPES.SUCCESS_ALERT_6);
this.errorHandler(key);
} else if (event.readyState === 4 && event.status === 204) {
const message = this.currentSelection === TaskanaType.WORKBASKETS ? 'WORKBASKET_IMPORT' : 'CLASSIFICATION_IMPORT';
this.notificationService.showSuccess(message);
this.importExportService.setImportingFinished(true);
this.resetProgress();
}
}
private onFailedResponse() {
this.errorHandler(NOTIFICATION_TYPES.UPLOAD_ERR);
this.errorHandler('IMPORT_EXPORT_UPLOAD_FAILED');
}
private errorHandler(key: NOTIFICATION_TYPES, passedError?: HttpErrorResponse) {
this.errorsService.triggerError(key, passedError);
private errorHandler(key: string) {
this.notificationService.showError(key);
delete this.selectedFileInput.files;
this.resetProgress();
}

View File

@ -19,7 +19,6 @@ import { Select, Store } from '@ngxs/store';
import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors';
import { Location } from '@angular/common';
import { NOTIFICATION_TYPES } from 'app/shared/models/notifications';
import { NotificationService } from 'app/shared/services/notifications/notification.service';
import { Classification } from '../../../shared/models/classification';
import { ClassificationsService } from '../../../shared/services/classifications/classifications.service';
@ -246,10 +245,7 @@ export class TaskanaTreeComponent implements OnInit, AfterViewChecked, OnDestroy
private updateClassification(classification: Classification) {
this.store.dispatch(new UpdateClassification(classification)).subscribe(() => {
this.notificationsService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_5,
new Map<string, string>([['classificationKey', classification.key]])
);
this.notificationsService.showSuccess('CLASSIFICATION_MOVE', { classificationKey: classification.key });
this.switchTaskanaSpinner(false);
});
}

View File

@ -51,8 +51,7 @@ const requestInProgressServiceSpy: Partial<RequestInProgressService> = {
const showDialogFn = jest.fn().mockReturnValue(true);
const notificationServiceSpy: Partial<NotificationService> = {
triggerError: showDialogFn,
showToast: showDialogFn
showSuccess: showDialogFn
};
const validateFormInformationFn = jest.fn().mockImplementation((): Promise<any> => Promise.resolve(true));

View File

@ -22,7 +22,6 @@ import { FormsValidatorService } from 'app/shared/services/forms-validator/forms
import { AccessIdDefinition } from 'app/shared/models/access-id';
import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors';
import { filter, take, takeUntil, tap } from 'rxjs/operators';
import { NOTIFICATION_TYPES } from '../../../shared/models/notifications';
import { NotificationService } from '../../../shared/services/notifications/notification.service';
import { AccessItemsCustomisation, CustomField, getCustomFields } from '../../../shared/models/customisation';
import {
@ -272,7 +271,7 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
this.AccessItemsForm.reset();
this.setAccessItemsGroups(this.accessItemsResetClone);
this.accessItemsClone = this.cloneAccessItems(this.accessItemsResetClone);
this.notificationsService.showToast(NOTIFICATION_TYPES.INFO_ALERT);
this.notificationsService.showSuccess('WORKBASKET_ACCESS_ITEM_RESTORE');
}
isFieldValid(field: string, index: number): boolean {

View File

@ -13,7 +13,6 @@ import { WorkbasketService } from '../../../shared/services/workbasket/workbaske
import { RouterTestingModule } from '@angular/router/testing';
import { RequestInProgressService } from '../../../shared/services/request-in-progress/request-in-progress.service';
import { SelectedRouteService } from '../../../shared/services/selected-route/selected-route';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialogModule } from '@angular/material/dialog';
import { selectedWorkbasketMock, workbasketReadStateMock } from '../../../shared/store/mock-data/mock-store';
import { StartupService } from '../../../shared/services/startup/startup.service';
@ -83,7 +82,6 @@ describe('WorkbasketDetailsComponent', () => {
NgxsModule.forRoot([WorkbasketState]),
HttpClientTestingModule,
RouterTestingModule.withRoutes([]),
MatSnackBarModule,
MatDialogModule,
MatIconModule,
MatProgressBarModule,

View File

@ -47,7 +47,7 @@ const workbasketServiceSpy: Partial<WorkbasketService> = {
};
const notificationsServiceSpy: Partial<NotificationService> = {
showToast: jest.fn().mockReturnValue(true)
showSuccess: jest.fn().mockReturnValue(true)
};
const requestInProgressServiceSpy: Partial<RequestInProgressService> = {
setRequestInProgress: jest.fn().mockReturnValue(of())

View File

@ -8,7 +8,6 @@ import { WorkbasketDistributionTargets } from 'app/shared/models/workbasket-dist
import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service';
import { Actions, ofActionCompleted, Select, Store } from '@ngxs/store';
import { filter, take, takeUntil } from 'rxjs/operators';
import { NOTIFICATION_TYPES } from '../../../shared/models/notifications';
import { NotificationService } from '../../../shared/services/notifications/notification.service';
import {
GetAvailableDistributionTargets,
@ -294,7 +293,7 @@ export class WorkbasketDistributionTargetsComponent implements OnInit, OnDestroy
}
onClear() {
this.notificationsService.showToast(NOTIFICATION_TYPES.INFO_ALERT);
this.notificationsService.showSuccess('WORKBASKET_DISTRIBUTION_TARGET_RESTORE');
this.availableDistributionTargets = Object.assign([], this.availableDistributionTargetsUndoClone);
this.availableDistributionTargetsFilterClone = Object.assign([], this.availableDistributionTargetsUndoClone);
this.selectedDistributionTargets = Object.assign([], this.selectedDistributionTargetsUndoClone);

View File

@ -12,7 +12,6 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RequestInProgressService } from '../../../shared/services/request-in-progress/request-in-progress.service';
import { FormsValidatorService } from '../../../shared/services/forms-validator/forms-validator.service';
import { NotificationService } from '../../../shared/services/notifications/notification.service';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialogModule } from '@angular/material/dialog';
import { EngineConfigurationState } from '../../../shared/store/engine-configuration-store/engine-configuration.state';
import { WorkbasketState } from '../../../shared/store/workbasket-store/workbasket.state';
@ -79,8 +78,7 @@ const formValidatorServiceMock: Partial<FormsValidatorService> = {
const showDialogFn = jest.fn().mockReturnValue(true);
const notificationServiceMock: Partial<NotificationService> = {
showDialog: showDialogFn,
showToast: showDialogFn,
triggerError: showDialogFn
showSuccess: showDialogFn
};
describe('WorkbasketInformationComponent', () => {
@ -95,7 +93,6 @@ describe('WorkbasketInformationComponent', () => {
imports: [
FormsModule,
HttpClientTestingModule,
MatSnackBarModule,
MatDialogModule,
NgxsModule.forRoot([EngineConfigurationState, WorkbasketState]),
TypeaheadModule.forRoot(),
@ -171,9 +168,9 @@ describe('WorkbasketInformationComponent', () => {
it('should reset workbasket information when onUndo is called', () => {
component.workbasketClone = selectedWorkbasketMock;
const notificationService = TestBed.inject(NotificationService);
const toastSpy = jest.spyOn(notificationService, 'showToast');
const showSuccessSpy = jest.spyOn(notificationService, 'showSuccess');
component.onUndo();
expect(toastSpy).toHaveBeenCalled();
expect(showSuccessSpy).toHaveBeenCalled();
expect(component.workbasket).toMatchObject(component.workbasketClone);
});

View File

@ -10,7 +10,6 @@ import { RequestInProgressService } from 'app/shared/services/request-in-progres
import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service';
import { filter, map, takeUntil } from 'rxjs/operators';
import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors';
import { NOTIFICATION_TYPES } from '../../../shared/models/notifications';
import { NotificationService } from '../../../shared/services/notifications/notification.service';
import { CustomField, getCustomFields, WorkbasketsCustomisation } from '../../../shared/models/customisation';
import {
@ -136,13 +135,14 @@ export class WorkbasketInformationComponent implements OnInit, OnChanges, OnDest
onUndo() {
this.formsValidatorService.formSubmitAttempt = false;
this.notificationService.showToast(NOTIFICATION_TYPES.INFO_ALERT);
this.notificationService.showSuccess('WORKBASKET_RESTORE');
this.workbasket = { ...this.workbasketClone };
}
removeWorkbasket() {
this.notificationService.showDialog(
`You are going to delete workbasket: ${this.workbasket.key}. Can you confirm this action?`,
'WORKBASKET_DELETE',
{ workbasketKey: this.workbasket.key },
this.onRemoveConfirmed.bind(this)
);
}

View File

@ -13,7 +13,6 @@ import { Direction, Sorting, WorkbasketQuerySortParameter } from '../../../share
import { ACTION } from '../../../shared/models/action';
import { TaskanaType } from '../../../shared/models/taskana-type';
import { MatIconModule } from '@angular/material/icon';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialogModule } from '@angular/material/dialog';
import { RouterTestingModule } from '@angular/router/testing';
import { RequestInProgressService } from '../../../shared/services/request-in-progress/request-in-progress.service';
@ -59,7 +58,6 @@ describe('WorkbasketListToolbarComponent', () => {
NgxsModule.forRoot([WorkbasketState]),
BrowserAnimationsModule,
MatIconModule,
MatSnackBarModule,
MatDialogModule
],
declarations: [WorkbasketListToolbarComponent, ImportExportStub, SortStub, FilterStub],

View File

@ -5,7 +5,6 @@ import { Actions, NgxsModule, ofActionDispatched, Store } from '@ngxs/store';
import { Observable, of } from 'rxjs';
import { WorkbasketState } from '../../../shared/store/workbasket-store/workbasket.state';
import { WorkbasketService } from '../../../shared/services/workbasket/workbasket.service';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialogModule } from '@angular/material/dialog';
import { OrientationService } from '../../../shared/services/orientation/orientation.service';
import { ImportExportService } from '../../services/import-export.service';
@ -95,7 +94,6 @@ describe('WorkbasketListComponent', () => {
imports: [
NgxsModule.forRoot([WorkbasketState]),
RouterTestingModule,
MatSnackBarModule,
MatDialogModule,
FormsModule,
MatProgressBarModule,

View File

@ -22,8 +22,7 @@ import { take } from 'rxjs/operators';
const showDialogFn = jest.fn().mockReturnValue(true);
const NotificationServiceSpy: Partial<NotificationService> = {
triggerError: showDialogFn,
showToast: showDialogFn
showSuccess: showDialogFn
};
const domainServiceSpy: Partial<DomainService> = {

View File

@ -7,7 +7,6 @@ import { RequestInProgressService } from './shared/services/request-in-progress/
import { OrientationService } from './shared/services/orientation/orientation.service';
import { SelectedRouteService } from './shared/services/selected-route/selected-route';
import { UploadService } from './shared/services/upload/upload.service';
import { ErrorModel } from './shared/models/error-model';
import { TaskanaEngineService } from './shared/services/taskana-engine/taskana-engine.service';
import { WindowRefService } from 'app/shared/services/window/window.service';
import { environment } from 'environments/environment';
@ -26,7 +25,6 @@ export class AppComponent implements OnInit, OnDestroy {
requestInProgress = false;
currentProgressValue = 0;
error: ErrorModel;
version: string;
toggle: boolean = false;

View File

@ -1,17 +1,7 @@
<h4 class="modal-title" mat-dialog-title id="errorModalLabel">{{title}}</h4>
<div mat-dialog-content class="modal-body">
<div class="alert alert-danger" role="alert">
<span class="material-icons md-20" data-toggle="tooltip">error</span>
<span *ngIf="!isDialog" class="sr-only">Error:</span>
{{message}}
</div>
</div>
<div mat-dialog-actions align="end">
<button mat-dialog-close mat-stroked-button data-dismiss="modal" type="button">
<span data-toggle="tooltip" class="material-icons md-20 red">cancel</span>
</button>
<button *ngIf="isDialog" [mat-dialog-close]="callback" mat-raised-button color="primary"
data-dismiss="modal" type="button">
<span data-toggle="tooltip" class="material-icons md-20 white">done</span>
</button>
</div>
<mat-dialog-content class="mat-typography">
<h4> {{message}} </h4>
</mat-dialog-content>
<mat-dialog-actions style="justify-content: flex-end">
<button mat-dialog-close mat-stroked-button> Cancel </button>
<button *ngIf="isDataSpecified" [mat-dialog-close]="callback" mat-raised-button color="primary"> Delete </button>
</mat-dialog-actions>

View File

@ -1,8 +1 @@
.alert {
color: #a94442;
background-color: #f2dede;
padding: 15px;
margin-bottom: 20px;
border: 1px solid #ebccd1;
border-radius: 4px;
}

View File

@ -1,6 +1,7 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { notifications } from '../../models/notifications';
import { ObtainMessageService } from '../../services/obtain-message/obtain-message.service';
import { messageTypes } from '../../services/obtain-message/message-types';
@Component({
selector: 'taskana-shared-dialog-pop-up',
@ -8,43 +9,19 @@ import { notifications } from '../../models/notifications';
styleUrls: ['./dialog-pop-up.component.scss']
})
export class DialogPopUpComponent implements OnInit {
title: string;
message: string;
isDialog: false;
callback: Function;
isDataSpecified: boolean;
constructor(@Inject(MAT_DIALOG_DATA) private data: any) {}
constructor(@Inject(MAT_DIALOG_DATA) private data: any, private obtainMessageService: ObtainMessageService) {}
ngOnInit() {
if (this.data) {
this.isDialog = this.data.isDialog;
if (this.isDialog) {
this.initDialog();
} else {
this.initError();
}
this.isDataSpecified = this.data?.message && this.data?.callback;
if (this.isDataSpecified) {
this.message = this.data.message;
this.callback = this.data.callback;
} else {
this.message = 'There was an error with this PopUp. \nPlease contact your administrator.';
this.message = this.obtainMessageService.getMessage('POPUP_CONFIGURATION', {}, messageTypes.DIALOG);
}
}
initError() {
this.title = notifications.get(this.data.key).left || '';
this.message =
notifications.get(this.data.key).right || (this.data && this.data.passedError && this.data.passedError.error)
? this.data.passedError.error.message
: '';
if (this.data.additions) {
this.data.additions.forEach((value: string, replacementKey: string) => {
this.message = this.message.replace(`{${replacementKey}}`, value);
this.title = this.title.replace(`{${replacementKey}}`, value);
});
}
}
initDialog() {
this.message = this.data.message;
this.title = 'Please confirm your action';
this.callback = this.data.callback;
}
}

View File

@ -1,5 +1,4 @@
import { Component, EventEmitter, Input, OnDestroy, Output, ViewChild } from '@angular/core';
import { NOTIFICATION_TYPES } from '../../models/notifications';
import { NotificationService } from '../../services/notifications/notification.service';
declare let $: any;
@ -29,7 +28,7 @@ export class SpinnerComponent implements OnDestroy {
@ViewChild('spinnerModal', { static: true })
private modal;
constructor(private errorsService: NotificationService) {}
constructor(private notificationService: NotificationService) {}
set isDelayedRunning(value: boolean) {
this.showSpinner = value;
@ -62,7 +61,7 @@ export class SpinnerComponent implements OnDestroy {
this.isDelayedRunning = value;
this.cancelTimeout();
this.requestTimeout = setTimeout(() => {
this.errorsService.triggerError(NOTIFICATION_TYPES.TIMEOUT_ERR);
this.notificationService.showError('SPINNER_TIMEOUT');
this.cancelTimeout();
this.isRunning = false;
}, this.maxRequestTimeout);

View File

@ -1,2 +0,0 @@
<span id="alert-icon" class="material-icons md-20">{{ type }}</span>
{{ message }}

View File

@ -1,35 +0,0 @@
import { Component, Inject, Input, OnInit } from '@angular/core';
import { MAT_SNACK_BAR_DATA } from '@angular/material/snack-bar';
import { NOTIFICATION_TYPES, notifications } from '../../models/notifications';
@Component({
selector: 'taskana-shared-toast',
templateUrl: './toast.component.html',
styleUrls: ['./toast.component.scss']
})
export class ToastComponent implements OnInit {
message: string;
type: string = 'info';
constructor(@Inject(MAT_SNACK_BAR_DATA) private data: any) {}
ngOnInit(): void {
if (this.data) {
this.message = notifications.get(this.data.key).right;
if (this.data.additions) {
this.data.additions.forEach((value: string, replacementKey: string) => {
this.message = this.message.replace(`{${replacementKey}}`, value);
});
}
this.type = NOTIFICATION_TYPES[this.data.key].split('_')[0].toLowerCase();
if (this.type === 'danger') {
this.type = 'error';
}
if (this.type === 'success') {
this.type = 'done';
}
} else {
this.message = 'There was an error with this toast. \nPlease contact your administrator.';
}
}
}

View File

@ -3,18 +3,15 @@ import { CanActivate } from '@angular/router';
import { Injectable } from '@angular/core';
import { DomainService } from 'app/shared/services/domain/domain.service';
import { catchError, map } from 'rxjs/operators';
import { NotificationService } from '../services/notifications/notification.service';
import { NOTIFICATION_TYPES } from '../models/notifications';
@Injectable()
export class DomainGuard implements CanActivate {
constructor(private domainService: DomainService, private errorsService: NotificationService) {}
constructor(private domainService: DomainService) {}
canActivate() {
return this.domainService.getDomains().pipe(
map((domain) => true),
map(() => true),
catchError(() => {
this.errorsService.triggerError(NOTIFICATION_TYPES.FETCH_ERR_5);
return of(false);
})
);

View File

@ -3,18 +3,12 @@ import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from
import { Observable, of } from 'rxjs';
import { TaskanaEngineService } from 'app/shared/services/taskana-engine/taskana-engine.service';
import { catchError, map } from 'rxjs/operators';
import { NOTIFICATION_TYPES } from '../models/notifications';
import { NotificationService } from '../services/notifications/notification.service';
@Injectable({
providedIn: 'root'
})
export class HistoryGuard implements CanActivate {
constructor(
private taskanaEngineService: TaskanaEngineService,
public router: Router,
private errorsService: NotificationService
) {}
constructor(private taskanaEngineService: TaskanaEngineService, public router: Router) {}
canActivate(
next: ActivatedRouteSnapshot,
@ -28,7 +22,6 @@ export class HistoryGuard implements CanActivate {
return this.navigateToWorkplace();
}),
catchError(() => {
this.errorsService.triggerError(NOTIFICATION_TYPES.FETCH_ERR_6);
return of(this.navigateToWorkplace());
})
);

View File

@ -1,21 +0,0 @@
import { HttpErrorResponse } from '@angular/common/http';
import { NOTIFICATION_TYPES, notifications } from './notifications';
export class ErrorModel {
public readonly errObj: HttpErrorResponse;
public readonly title: string;
public readonly message: string;
constructor(key: NOTIFICATION_TYPES, passedError?: HttpErrorResponse, addition?: Map<String, String>) {
this.title = notifications.get(key).left;
let messageTemp = notifications.get(key).right;
this.errObj = passedError;
if (addition) {
addition.forEach((value: string, replacementKey: string) => {
messageTemp = messageTemp.replace(`{${replacementKey}}`, value);
this.title.replace(`{${replacementKey}}`, value);
});
}
this.message = messageTemp;
}
}

View File

@ -1,252 +0,0 @@
import { Pair } from './pair';
export enum NOTIFICATION_TYPES {
// ERRORS
FETCH_ERR,
FETCH_ERR_2,
FETCH_ERR_3,
FETCH_ERR_4,
FETCH_ERR_5,
FETCH_ERR_6,
FETCH_ERR_7,
DELETE_ERR,
DELETE_ERR_2,
CREATE_ERR,
CREATE_ERR_2,
REMOVE_ERR,
REMOVE_ERR_2,
SAVE_ERR,
SAVE_ERR_2,
SAVE_ERR_3,
SAVE_ERR_4,
SELECT_ERR,
FILE_ERR,
IMPORT_ERR_1,
IMPORT_ERR_2,
IMPORT_ERR_3,
IMPORT_ERR_4,
UPLOAD_ERR,
TIMEOUT_ERR,
GENERAL_ERR,
ACCESS_ERR,
MARK_ERR,
// ALERTS
// currently their names are used as a way to determine the type of the alert
// e.g. we extract from 'SUCCESS_ALERT_2' in notification.service, that this is a success alert
// and should therefore have the color green, so please __keep this in mind when refactoring__
// usages of this undocumented sideffect: notification.service.ts and toast.component.ts
INFO_ALERT,
INFO_ALERT_2,
DANGER_ALERT,
DANGER_ALERT_2,
SUCCESS_ALERT,
SUCCESS_ALERT_2,
SUCCESS_ALERT_3,
SUCCESS_ALERT_4,
SUCCESS_ALERT_5,
SUCCESS_ALERT_6,
SUCCESS_ALERT_7,
SUCCESS_ALERT_8,
SUCCESS_ALERT_9,
SUCCESS_ALERT_10,
SUCCESS_ALERT_11,
SUCCESS_ALERT_12,
SUCCESS_ALERT_13,
SUCCESS_ALERT_14,
WARNING_ALERT,
WARNING_ALERT_2,
WARNING_CANT_COPY
}
export const notifications = new Map<NOTIFICATION_TYPES, Pair<string, string>>([
// access-items-management.component.ts
[
NOTIFICATION_TYPES.FETCH_ERR,
{ left: 'There was an error while retrieving your access ids with groups.', right: '' }
],
// access-items-management.component.ts
[NOTIFICATION_TYPES.FETCH_ERR_2, { left: 'There was an error while retrieving your access items ', right: '' }],
// access-items-management.component.ts
[NOTIFICATION_TYPES.DELETE_ERR, { left: "You can't delete a group", right: '' }],
// classification-details.component
[NOTIFICATION_TYPES.CREATE_ERR, { left: 'There was an error while creating this classification', right: '' }],
// classification-details.component
[NOTIFICATION_TYPES.REMOVE_ERR, { left: 'There was an error while removing your classification', right: '' }],
// classification-details.component
[NOTIFICATION_TYPES.SAVE_ERR, { left: 'There was an error while saving your classification', right: '' }],
// classification-details.component
[
NOTIFICATION_TYPES.SELECT_ERR,
{ left: 'There is no classification selected', right: 'Please check if you are creating a classification' }
],
// import-export.component
[
NOTIFICATION_TYPES.FILE_ERR,
{ left: 'Wrong format', right: 'This file format is not allowed! Please use a .json file.' }
],
// import-export.component
[
NOTIFICATION_TYPES.IMPORT_ERR_1,
{
left: 'Import was not successful',
right: 'Import was not successful, you have no access to apply this operation.'
}
],
// import-export.component
[
NOTIFICATION_TYPES.IMPORT_ERR_2,
{ left: 'Import was not successful', right: 'Import was not successful, operation was not found.' }
],
// import-export.component
[
NOTIFICATION_TYPES.IMPORT_ERR_3,
{ left: 'Import was not successful', right: 'Import was not successful, operation has some conflicts.' }
],
// import-export.component
[
NOTIFICATION_TYPES.IMPORT_ERR_4,
{ left: 'Import was not successful', right: 'Import was not successful, maximum file size exceeded.' }
],
// import-export.component
[
NOTIFICATION_TYPES.UPLOAD_ERR,
{
left: 'Upload failed',
right: `The upload didn't proceed sucessfully.
\n The uploaded file probably exceeded the maximum file size of 10 MB.`
}
],
// task-details.component
[NOTIFICATION_TYPES.FETCH_ERR_3, { left: '', right: 'An error occurred while fetching the task' }],
// workbasket-details.component
[NOTIFICATION_TYPES.FETCH_ERR_4, { left: '', right: 'An error occurred while fetching the workbasket' }],
// access-items.component
[
NOTIFICATION_TYPES.SAVE_ERR_2,
{ left: "There was an error while saving your workbasket's access items", right: '' }
],
// workbaskets-distribution-targets.component
[
NOTIFICATION_TYPES.SAVE_ERR_3,
{ left: "There was an error while saving your workbasket's distribution targets", right: '' }
],
// workbasket-information.component
[
NOTIFICATION_TYPES.REMOVE_ERR_2,
{ left: 'There was an error removing distribution target for {workbasketId}.', right: '' }
],
// workbasket-information.component
[NOTIFICATION_TYPES.SAVE_ERR_4, { left: 'There was an error while saving your workbasket', right: '' }],
// workbasket-information.component
[NOTIFICATION_TYPES.CREATE_ERR_2, { left: 'There was an error while creating this workbasket', right: '' }],
// workbasket-information.component
[
NOTIFICATION_TYPES.MARK_ERR,
{
left: 'Workbasket was marked for deletion.',
right:
'The Workbasket {workbasketId} still contains completed tasks and could not be deleted.' +
' Instead is was marked for deletion and will be deleted automatically ' +
'as soon as the completed tasks are cleared from the database.'
}
],
// domain.guard
[
NOTIFICATION_TYPES.FETCH_ERR_5,
{ left: 'There was an error, please contact your administrator', right: 'There was an error getting Domains' }
],
// history.guard
[
NOTIFICATION_TYPES.FETCH_ERR_6,
{
left: 'There was an error, please contact your administrator',
right: 'There was an error getting history provider'
}
],
// http-client-interceptor.service
[NOTIFICATION_TYPES.ACCESS_ERR, { left: 'You have no access to this resource', right: '' }],
// http-client-interceptor.service
[NOTIFICATION_TYPES.GENERAL_ERR, { left: 'There was an error, please contact your administrator', right: '' }],
// spinner.component
[
NOTIFICATION_TYPES.TIMEOUT_ERR,
{
left: 'There was an error with your request, please make sure you have internet connection',
right: 'Request time exceeded'
}
],
// task-details.component
[NOTIFICATION_TYPES.FETCH_ERR_7, { left: 'An error occurred while fetching the task', right: '' }],
// task-details.component
[NOTIFICATION_TYPES.DELETE_ERR_2, { left: 'An error occurred while deleting the task', right: '' }],
// ALERTS
// access-items-management.component
[NOTIFICATION_TYPES.SUCCESS_ALERT, { left: '', right: '{accessId} was removed successfully' }],
// classification-details.component
[
NOTIFICATION_TYPES.SUCCESS_ALERT_2,
{ left: '', right: 'Classification {classificationKey} was created successfully' }
],
// classification-details.component
[
NOTIFICATION_TYPES.SUCCESS_ALERT_3,
{ left: '', right: 'Classification {classificationKey} was saved successfully' }
],
// classification-details.component
// access-items.component
// workbasket.distribution-targets.component
// workbasket-information.component
// task-details.component
[NOTIFICATION_TYPES.INFO_ALERT, { left: '', right: 'Information restored' }],
// classification-details.component
[
NOTIFICATION_TYPES.SUCCESS_ALERT_4,
{ left: '', right: 'Classification {classificationKey} was removed successfully' }
],
// classification-list.component
[
NOTIFICATION_TYPES.SUCCESS_ALERT_5,
{ left: '', right: 'Classification {classificationKey} was moved successfully' }
],
// import-export.component
[NOTIFICATION_TYPES.SUCCESS_ALERT_6, { left: '', right: 'Import was successful' }],
// access-items.component
[
NOTIFICATION_TYPES.SUCCESS_ALERT_7,
{ left: '', right: 'Workbasket {workbasketKey} Access items were saved successfully' }
],
// workbasket.distribution-targets.component
[
NOTIFICATION_TYPES.SUCCESS_ALERT_8,
{ left: '', right: 'Workbasket {workbasketName} Distribution targets were saved successfully' }
],
// workbasket-information.component
[
NOTIFICATION_TYPES.SUCCESS_ALERT_9,
{ left: '', right: 'DistributionTargets for workbasketID {workbasketId} was removed successfully' }
],
// workbasket-information.component
[NOTIFICATION_TYPES.SUCCESS_ALERT_10, { left: '', right: 'Workbasket {workbasketKey} was saved successfully' }],
// workbasket-information.component
[NOTIFICATION_TYPES.SUCCESS_ALERT_11, { left: '', right: 'Workbasket {workbasketKey} was created successfully' }],
// workbasket-information.component
[NOTIFICATION_TYPES.SUCCESS_ALERT_12, { left: '', right: 'The Workbasket {workbasketId} has been deleted.' }],
// forms-validator.service
[NOTIFICATION_TYPES.WARNING_ALERT, { left: '', right: 'There are some empty fields which are required.' }],
// forms-validator.service x2
[NOTIFICATION_TYPES.WARNING_ALERT_2, { left: '', right: 'The {owner} introduced is not valid.' }],
// task-details.component
[NOTIFICATION_TYPES.DANGER_ALERT, { left: '', right: 'There was an error while updating.' }],
// task-details.component
[NOTIFICATION_TYPES.SUCCESS_ALERT_13, { left: '', right: 'Task {taskId} was created successfully.' }],
// task-details.component
[NOTIFICATION_TYPES.SUCCESS_ALERT_14, { left: '', right: 'Updating was successful.' }],
// task-details.component
[NOTIFICATION_TYPES.DANGER_ALERT_2, { left: '', right: 'There was an error while creating a new task.' }],
// task-master.component
[NOTIFICATION_TYPES.INFO_ALERT_2, { left: '', right: 'The selected Workbasket is empty!' }],
[NOTIFICATION_TYPES.WARNING_CANT_COPY, { left: '', right: "Can't copy a not created classification" }]
]);

View File

@ -1,7 +1,6 @@
import { FormArray, NgForm, NgModel } from '@angular/forms';
import { Injectable } from '@angular/core';
import { AccessIdsService } from 'app/shared/services/access-ids/access-ids.service';
import { NOTIFICATION_TYPES } from '../../models/notifications';
import { NotificationService } from '../notifications/notification.service';
import { Observable, Subject, Subscription, timer } from 'rxjs';
@ -54,12 +53,9 @@ export class FormsValidatorService {
const responseOwner = new ResponseOwner(values[1]);
if (!(values[0] && responseOwner.valid)) {
if (!responseOwner.valid) {
this.notificationsService.showToast(
NOTIFICATION_TYPES.WARNING_ALERT_2,
new Map<string, string>([['owner', responseOwner.field]])
);
this.notificationsService.showWarning('OWNER_NOT_VALID', { owner: responseOwner.field });
} else {
this.notificationsService.showToast(NOTIFICATION_TYPES.WARNING_ALERT);
this.notificationsService.showWarning('EMPTY_FIELDS');
}
}
return values[0] && responseOwner.valid;
@ -88,10 +84,9 @@ export class FormsValidatorService {
result = result && responseOwner.valid;
});
if (!result) {
this.notificationsService.showToast(
NOTIFICATION_TYPES.WARNING_ALERT_2,
new Map<string, string>([['owner', responseOwner ? responseOwner.field : 'owner']])
);
this.notificationsService.showWarning('OWNER_NOT_VALID', {
owner: responseOwner ? responseOwner.field : 'owner'
});
}
return result;
}

View File

@ -12,14 +12,13 @@ import { RequestInProgressService } from 'app/shared/services/request-in-progres
import { environment } from 'environments/environment';
import { tap } from 'rxjs/operators';
import { NotificationService } from '../notifications/notification.service';
import { NOTIFICATION_TYPES } from '../../models/notifications';
@Injectable()
export class HttpClientInterceptor implements HttpInterceptor {
constructor(
private requestInProgressService: RequestInProgressService,
private errorsService: NotificationService,
private tokenExtractor: HttpXsrfTokenExtractor
private tokenExtractor: HttpXsrfTokenExtractor,
private notificationService: NotificationService
) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
@ -36,22 +35,16 @@ export class HttpClientInterceptor implements HttpInterceptor {
() => {},
(error) => {
this.requestInProgressService.setRequestInProgress(false);
if (error instanceof HttpErrorResponse && (error.status === 401 || error.status === 403)) {
this.errorsService.triggerError(NOTIFICATION_TYPES.ACCESS_ERR, error);
} else if (
error instanceof HttpErrorResponse &&
error.status === 404 &&
error.url.indexOf('environment-information.json')
if (
error.status !== 404 ||
!(error instanceof HttpErrorResponse) ||
error.url.indexOf('environment-information.json') === -1
) {
// ignore this error
} else if (
(error.status === 409 && error.error.exception.endsWith('WorkbasketAccessItemAlreadyExistException')) ||
error.error.exception.endsWith('WorkbasketAlreadyExistException') ||
error.error.exception.endsWith('ClassificationAlreadyExistException')
) {
return;
} else {
this.errorsService.triggerError(NOTIFICATION_TYPES.GENERAL_ERR, error);
const { key, messageVariables } = error.error.error || {
key: 'FALLBACK',
messageVariables: {}
};
this.notificationService.showError(key, messageVariables);
}
}
)

View File

@ -1,34 +1,71 @@
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { ErrorModel } from '../../models/error-model';
import { NOTIFICATION_TYPES } from '../../models/notifications';
import { ToastComponent } from '../../components/toast/toast.component';
import { MatDialog } from '@angular/material/dialog';
import { DialogPopUpComponent } from '../../components/popup/dialog-pop-up.component';
import { HotToastService } from '@ngneat/hot-toast';
import { ObtainMessageService } from '../obtain-message/obtain-message.service';
import { messageTypes } from '../obtain-message/message-types';
@Injectable({
providedIn: 'root'
})
export class NotificationService {
constructor(private matSnack: MatSnackBar, private popup: MatDialog) {}
constructor(
private popup: MatDialog,
private toastService: HotToastService,
private obtainMessageService: ObtainMessageService
) {}
triggerError(key: NOTIFICATION_TYPES, passedError?: HttpErrorResponse, additions?: Map<String, String>): void {
this.popup.open(DialogPopUpComponent, {
data: { key, passedError, additions, isDialog: false },
backdropClass: 'backdrop',
position: { top: '3em' },
autoFocus: true,
maxWidth: '50em'
generateToastId(errorKey: string, messageVariables: object): string {
let id = errorKey;
for (const [replacementKey, value] of Object.entries(messageVariables)) {
id = id.concat(replacementKey, value);
}
return id;
}
showError(errorKey: string, messageVariables: object = {}) {
this.toastService.error(this.obtainMessageService.getMessage(errorKey, messageVariables, messageTypes.ERROR), {
dismissible: true,
autoClose: false,
id: this.generateToastId(errorKey, messageVariables)
});
}
showDialog(message: string, callback?: Function): MatDialogRef<DialogPopUpComponent> {
showSuccess(successKey: string, messageVariables: object = {}) {
this.toastService.success(
this.obtainMessageService.getMessage(successKey, messageVariables, messageTypes.SUCCESS),
{
duration: 5000
}
);
}
showWarning(warningKey: string, messageVariables: object = {}) {
this.toastService.warning(this.obtainMessageService.getMessage(warningKey, messageVariables, messageTypes.WARNING));
}
showInformation(informationKey: string, messageVariables: object = {}) {
this.toastService.show(
`
<span class="material-icons">info</span> ${this.obtainMessageService.getMessage(
informationKey,
messageVariables,
messageTypes.INFORMATION
)}
`,
// prevents duplicated toast because of double call in task-master
// TODO: delete while frontend refactoring
{ id: 'empty-workbasket' }
);
}
showDialog(key: string, messageVariables: object = {}, callback: Function) {
const message = this.obtainMessageService.getMessage(key, messageVariables, messageTypes.DIALOG);
const ref = this.popup.open(DialogPopUpComponent, {
data: { isDialog: true, message, callback },
data: { message: message, callback },
backdropClass: 'backdrop',
position: { top: '3em' },
position: { top: '5em' },
autoFocus: true,
maxWidth: '50em'
});
@ -39,30 +76,4 @@ export class NotificationService {
});
return ref;
}
showToast(key: NOTIFICATION_TYPES, additions?: Map<string, string>) {
let colorClass: string[];
const type = NOTIFICATION_TYPES[key].split('_')[0].toLowerCase();
switch (type) {
case 'danger':
colorClass = ['red', 'background-white'];
break;
case 'success':
colorClass = ['white', 'background-bluegreen'];
break;
case 'info':
colorClass = ['white', 'background-darkgreen'];
break;
case 'warning':
colorClass = ['brown', 'background-white'];
break;
default:
colorClass = ['white', 'background-darkgreen'];
}
return this.matSnack.openFromComponent(ToastComponent, {
duration: 5000,
data: { key, additions },
panelClass: colorClass
});
}
}

View File

@ -0,0 +1,106 @@
import { messageTypes } from './message-types';
export const messageByErrorCode = {
[messageTypes.ERROR]: {
FALLBACK:
'An error occurred, but there is no error code. Please contact your administrator to specify the error code.',
CRITICAL_SYSTEM_ERROR: 'A system error occurred. Please contact your administrator.',
UNKNOWN_ERROR: 'An unknown error occurred. Please contact your administrator.',
ENTITY_NOT_UP_TO_DATE:
'Cannot be saved because there has been a modification while editing. Please reload to get the current version.',
DOMAIN_NOT_FOUND: 'Domain {domain} cannot be found',
ROLE_MISMATCHED: 'Current user {currentUserId} is not authorized. User must be member of role(s) {roles}.',
SPINNER_TIMEOUT: 'Request time exceeded. Please make sure you have internet connection.',
HISTORY_EVENT_NOT_FOUND: 'History Event with id {historyEventId} cannot be found',
PAYLOAD_TOO_LARGE: 'Maximum upload size was exceeded',
CLASSIFICATION_SERVICE_LEVEL_MALFORMED:
'Service level {serviceLevel} of Classification with key {classificationKey} and domain {domain} is invalid. ' +
'The service level has to be a positive ISO-8601 duration format and only whole days are supported. ' +
"The format must be 'PnD'.",
INVALID_ARGUMENT: 'A method was called with an invalid argument.',
CLASSIFICATION_IN_USE:
'Classification with key {classificationKey} in domain {domain} cannot be deleted since there are Tasks associated with this Classification.',
CLASSIFICATION_ALREADY_EXISTS:
'Classification with key {classificationKey} cannot be saved since a Classification with the same key already exists in domain {domain}',
CLASSIFICATION_WITH_ID_NOT_FOUND: 'Classification with id {classificationId} cannot be found',
WORKBASKET_WITH_ID_NOT_FOUND: 'Workbasket with id {workbasketId} cannot be found',
WORKBASKET_WITH_KEY_NOT_FOUND: 'Workbasket with key {workbasketKey} cannot be found in domain {domain}',
WORKBASKET_ALREADY_EXISTS:
'Workbasket with key {workbasketKey} cannot be saved since a Workbasket with the same key already exists in domain {domain}',
WORKBASKET_IN_USE: 'Workbasket with id {workbasketId} cannot be deleted since it contains non-completed Tasks',
WORKBASKET_ACCESS_ITEM_ALREADY_EXISTS:
'Workbasket Access Item with access id {accessId} for Workbasket with id {workbasketId} cannot be created since it already exists',
WORKBASKET_WITH_ID_MISMATCHED_PERMISSION:
'Current user {currentUserId} has no permission for Workbasket with id {workbasketId}. Required permission(s): {requiredPermissions}.',
WORKBASKET_WITH_KEY_MISMATCHED_PERMISSION:
'Current user {currentUserId} has no permission for Workbasket with key {workbasketKey}. Required permission(s): {requiredPermissions}.',
TASK_ALREADY_EXISTS:
'Task with external id {externalTaskId} cannot be created, because a Task with the same external id already exists.',
TASK_NOT_FOUND: 'Task with id {taskId} cannot be found',
TASK_INVALID_CALLBACK_STATE:
'Callback state {taskCallbackState} for Task with id {taskId} is invalid. Required callback states: {requiredCallbackStates}',
TASK_INVALID_OWNER: 'Current user {currentUserId} is not the owner of the Task with id {taskId}',
TASK_INVALID_STATE: 'Task with id {taskId} is in state {taskState}. Required state(s): {requiredTaskStates}.',
IMPORT_EXPORT_UPLOAD_FAILED: 'Upload failed. The uploaded file probably exceeded the maximum file size of 10 MB.',
IMPORT_EXPORT_UPLOAD_FAILED_AUTH: 'Upload failed because you have no access to apply this operation.',
IMPORT_EXPORT_UPLOAD_FAILED_NOT_FOUND: 'Upload failed because operation was not found',
IMPORT_EXPORT_UPLOAD_FAILED_CONFLICTS: 'Upload failed because operation has conflicts',
IMPORT_EXPORT_UPLOAD_FAILED_SIZE: 'Upload failed because maximum file size exceeded',
IMPORT_EXPORT_UPLOAD_FILE_FORMAT: 'File format is not allowed. Please use a .json file.'
},
[messageTypes.SUCCESS]: {
FALLBACK:
'Action was completed successfully, but this success message was not configured properly. ' +
'Please ask your administrator to configure this message.',
CLASSIFICATION_CREATE: 'Classification with key {classificationKey} was created',
CLASSIFICATION_UPDATE: 'Classification with key {classificationKey} was updated',
CLASSIFICATION_REMOVE: 'Classification with key {classificationKey} was removed',
CLASSIFICATION_MOVE: 'Classification with key {classificationKey} was moved',
CLASSIFICATION_RESTORE: 'Classification restored',
CLASSIFICATION_IMPORT: 'Classifications imported',
WORKBASKET_CREATE: 'Workbasket with key {workbasketKey} was created',
WORKBASKET_UPDATE: 'Workbasket with key {workbasketKey} was updated',
WORKBASKET_REMOVE: 'Workbasket with key {workbasketKey} was removed',
WORKBASKET_RESTORE: 'Workbasket restored',
WORKBASKET_IMPORT: 'Workbaskets imported',
WORKBASKET_ACCESS_ITEM_SAVE: 'Workbasket Access Items were saved',
WORKBASKET_ACCESS_ITEM_RESTORE: 'Workbasket Access Items restored',
WORKBASKET_DISTRIBUTION_TARGET_SAVE: 'Workbasket Distribution Targets were saved',
WORKBASKET_DISTRIBUTION_TARGET_RESTORE: 'Workbasket Distribution Targets restored',
WORKBASKET_DISTRIBUTION_TARGET_REMOVE:
'Workbasket with key {workbasketKey} was removed as Workbasket Distribution Target',
WORKBASKET_ACCESS_ITEM_REMOVE_PERMISSION: '{accessId} was removed',
TASK_CREATE: 'Task with name {taskName} was created',
TASK_UPDATE: 'Task with name {taskName} was updated',
TASK_DELETE: 'Task with name {taskName} was deleted',
TASK_RESTORE: 'Task restored'
},
[messageTypes.WARNING]: {
CLASSIFICATION_COPY_NOT_CREATED: 'Cannot copy a not created Classification',
EMPTY_FIELDS: 'There are empty fields which are required',
OWNER_NOT_VALID: 'The {owner} introduced is not valid'
},
[messageTypes.INFORMATION]: {
EMPTY_WORKBASKET: 'Selected Workbasket is empty'
},
[messageTypes.DIALOG]: {
POPUP_CONFIGURATION: 'This Popup was not configured properly for this request. Please contact your administrator.',
WORKBASKET_DELETE: 'Delete Workbasket with key {workbasketKey}?',
CLASSIFICATION_DELETE: 'Delete Classification with key {classificationKey}?',
TASK_DELETE: 'Delete Task with id {taskId}?',
ACCESS_ITEM_MANAGEMENT_REVOKE_ACCESS: 'Delete all access related to {accessId}?'
}
};

View File

@ -0,0 +1,7 @@
export enum messageTypes {
ERROR,
SUCCESS,
WARNING,
INFORMATION,
DIALOG
}

View File

@ -0,0 +1,21 @@
import { Injectable } from '@angular/core';
import { messageByErrorCode } from './message-by-error-code';
import { messageTypes } from './message-types';
@Injectable({
providedIn: 'root'
})
export class ObtainMessageService {
getMessage(key: string, messageVariables: object = {}, type: messageTypes): string {
let message =
messageByErrorCode[type][key] ||
messageByErrorCode[type]['FALLBACK'] ||
`The message with type '${type}' and key '${key}' is not configured`;
for (const [replacementKey, value] of Object.entries(messageVariables)) {
message = message.replace(`{${replacementKey}}`, `'${value}'`);
}
return message;
}
}

View File

@ -8,7 +8,7 @@ import { TreeModule } from '@circlon/angular-tree-component';
import { AlertModule } from 'ngx-bootstrap/alert';
import { TypeaheadModule } from 'ngx-bootstrap/typeahead';
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { HotToastModule } from '@ngneat/hot-toast';
import { AccordionModule } from 'ngx-bootstrap/accordion';
/**
@ -20,7 +20,6 @@ import { TaskanaTreeComponent } from 'app/administration/components/tree/tree.co
import { TypeAheadComponent } from 'app/shared/components/type-ahead/type-ahead.component';
import { IconTypeComponent } from 'app/administration/components/type-icon/icon-type.component';
import { FieldErrorDisplayComponent } from 'app/shared/components/field-error-display/field-error-display.component';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { MatRadioModule } from '@angular/material/radio';
@ -43,7 +42,6 @@ import { DateTimeZonePipe } from './pipes/date-time-zone.pipe';
* Services
*/
import { HttpClientInterceptor } from './services/http-client-interceptor/http-client-interceptor.service';
import { ToastComponent } from './components/toast/toast.component';
import { DialogPopUpComponent } from './components/popup/dialog-pop-up.component';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
@ -56,6 +54,10 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { WorkbasketFilterComponent } from './components/workbasket-filter/workbasket-filter.component';
import { TaskFilterComponent } from './components/task-filter/task-filter.component';
import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service';
import { ClassificationsService } from 'app/shared/services/classifications/classifications.service';
import { ObtainMessageService } from './services/obtain-message/obtain-message.service';
import { AccessIdsService } from './services/access-ids/access-ids.service';
const MODULES = [
CommonModule,
@ -66,12 +68,16 @@ const MODULES = [
BsDatepickerModule.forRoot(),
AngularSvgIconModule,
HttpClientModule,
MatSnackBarModule,
MatDialogModule,
MatButtonModule,
RouterModule,
TreeModule,
MatAutocompleteModule
MatAutocompleteModule,
HotToastModule.forRoot({
style: {
'max-width': '520px'
}
})
];
const DECLARATIONS = [
@ -91,7 +97,6 @@ const DECLARATIONS = [
FieldErrorDisplayComponent,
PaginationComponent,
ProgressSpinnerComponent,
ToastComponent,
DialogPopUpComponent,
WorkbasketFilterComponent,
TaskFilterComponent
@ -118,8 +123,12 @@ const DECLARATIONS = [
provide: HTTP_INTERCEPTORS,
useClass: HttpClientInterceptor,
multi: true
}
},
AccessIdsService,
ClassificationsService,
WorkbasketService,
ObtainMessageService
],
entryComponents: [DialogPopUpComponent, ToastComponent]
entryComponents: [DialogPopUpComponent]
})
export class SharedModule {}

View File

@ -9,7 +9,6 @@ import { Observable, of } from 'rxjs';
import { AccessIdsService } from '../../services/access-ids/access-ids.service';
import { take, tap } from 'rxjs/operators';
import { AccessIdDefinition } from '../../models/access-id';
import { NOTIFICATION_TYPES } from '../../models/notifications';
import { NotificationService } from '../../services/notifications/notification.service';
import { WorkbasketAccessItemsRepresentation } from '../../models/workbasket-access-items-representation';
import { RequestInProgressService } from '../../services/request-in-progress/request-in-progress.service';
@ -50,9 +49,8 @@ export class AccessItemsManagementState implements NgxsAfterBootstrap {
groups
});
},
(error) => {
() => {
this.requestInProgressService.setRequestInProgress(false);
this.notificationService.triggerError(NOTIFICATION_TYPES.FETCH_ERR, error);
}
)
);
@ -72,9 +70,8 @@ export class AccessItemsManagementState implements NgxsAfterBootstrap {
accessItemsResource
});
},
(error) => {
() => {
this.requestInProgressService.setRequestInProgress(false);
this.notificationService.triggerError(NOTIFICATION_TYPES.FETCH_ERR_2, error);
}
)
);
@ -91,14 +88,12 @@ export class AccessItemsManagementState implements NgxsAfterBootstrap {
tap(
() => {
this.requestInProgressService.setRequestInProgress(false);
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT,
new Map<string, string>([['accessId', action.accessId]])
);
this.notificationService.showSuccess('WORKBASKET_ACCESS_ITEM_REMOVE_PERMISSION', {
accessId: action.accessId
});
},
(error) => {
() => {
this.requestInProgressService.setRequestInProgress(false);
this.notificationService.triggerError(NOTIFICATION_TYPES.DELETE_ERR, error);
}
)
);

View File

@ -25,7 +25,6 @@ import {
} from './workbasket.actions';
import { WorkbasketSummaryRepresentation } from '../../models/workbasket-summary-representation';
import { ACTION } from '../../models/action';
import { NOTIFICATION_TYPES } from '../../models/notifications';
import { NotificationService } from '../../services/notifications/notification.service';
import { WorkbasketAccessItemsRepresentation } from '../../models/workbasket-access-items-representation';
import { WorkbasketDistributionTargets } from '../../models/workbasket-distribution-targets';
@ -195,19 +194,11 @@ export class WorkbasketState implements NgxsAfterBootstrap {
ctx.dispatch(new OnButtonPressed(undefined));
return this.workbasketService.createWorkbasket(action.workbasket).pipe(
take(1),
tap(
(workbasketUpdated) => {
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_11,
new Map<string, string>([['workbasketKey', workbasketUpdated.key]])
);
tap((workbasketUpdated) => {
this.notificationService.showSuccess('WORKBASKET_CREATE', { workbasketKey: workbasketUpdated.key });
this.location.go(this.location.path().replace(/(workbaskets).*/g, 'workbaskets'));
},
(error) => {
this.notificationService.triggerError(NOTIFICATION_TYPES.CREATE_ERR_2, error);
}
),
this.location.go(this.location.path().replace(/(workbaskets).*/g, 'workbaskets'));
}),
concatMap((workbasketUpdated) => ctx.dispatch(new SelectWorkbasket(workbasketUpdated.workbasketId)))
);
}
@ -276,27 +267,19 @@ export class WorkbasketState implements NgxsAfterBootstrap {
ctx.dispatch(new OnButtonPressed(undefined));
return this.workbasketService.updateWorkbasket(action.url, action.workbasket).pipe(
take(1),
tap(
(updatedWorkbasket) => {
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_10,
new Map<string, string>([['workbasketKey', updatedWorkbasket.key]])
);
tap((updatedWorkbasket) => {
this.notificationService.showSuccess('WORKBASKET_UPDATE', { workbasketKey: updatedWorkbasket.key });
const paginatedWorkbasketSummary = { ...ctx.getState().paginatedWorkbasketsSummary };
paginatedWorkbasketSummary.workbaskets = updateWorkbasketSummaryRepresentation(
paginatedWorkbasketSummary.workbaskets,
action.workbasket
);
ctx.patchState({
selectedWorkbasket: updatedWorkbasket,
paginatedWorkbasketsSummary: paginatedWorkbasketSummary
});
},
(error) => {
this.notificationService.triggerError(NOTIFICATION_TYPES.SAVE_ERR_4, error);
}
)
const paginatedWorkbasketSummary = { ...ctx.getState().paginatedWorkbasketsSummary };
paginatedWorkbasketSummary.workbaskets = updateWorkbasketSummaryRepresentation(
paginatedWorkbasketSummary.workbaskets,
action.workbasket
);
ctx.patchState({
selectedWorkbasket: updatedWorkbasket,
paginatedWorkbasketsSummary: paginatedWorkbasketSummary
});
})
);
}
@ -305,21 +288,11 @@ export class WorkbasketState implements NgxsAfterBootstrap {
ctx.dispatch(new OnButtonPressed(undefined));
return this.workbasketService.removeDistributionTarget(action.url).pipe(
take(1),
tap(
() => {
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_9,
new Map<string, string>([['workbasketId', ctx.getState().selectedWorkbasket.workbasketId]])
);
},
(error) => {
this.notificationService.triggerError(
NOTIFICATION_TYPES.REMOVE_ERR_2,
error,
new Map<String, String>([['workbasketId', ctx.getState().selectedWorkbasket.workbasketId]])
);
}
)
tap(() => {
this.notificationService.showSuccess('WORKBASKET_DISTRIBUTION_TARGET_REMOVE', {
workbasketKey: ctx.getState().selectedWorkbasket.key
});
})
);
}
@ -329,17 +302,10 @@ export class WorkbasketState implements NgxsAfterBootstrap {
return this.workbasketService.markWorkbasketForDeletion(action.url).pipe(
take(1),
tap((response) => {
if (response.status === 202) {
this.notificationService.triggerError(
NOTIFICATION_TYPES.MARK_ERR,
undefined,
new Map<String, String>([['workbasketId', ctx.getState().selectedWorkbasket.workbasketId]])
);
} else {
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_12,
new Map<string, string>([['workbasketId', ctx.getState().selectedWorkbasket.workbasketId]])
);
if (response.status !== 202) {
this.notificationService.showSuccess('WORKBASKET_REMOVE', {
workbasketKey: ctx.getState().selectedWorkbasket.key
});
ctx.dispatch(new DeselectWorkbasket());
}
@ -369,21 +335,15 @@ export class WorkbasketState implements NgxsAfterBootstrap {
.updateWorkBasketAccessItem(action.url, { accessItems: action.workbasketAccessItems })
.pipe(
take(1),
tap(
(workbasketAccessItems) => {
ctx.patchState({
workbasketAccessItems
});
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_7,
new Map<string, string>([['workbasketKey', ctx.getState().selectedWorkbasket.key]])
);
return of(null);
},
(error) => {
this.notificationService.triggerError(NOTIFICATION_TYPES.SAVE_ERR_2, error);
}
)
tap((workbasketAccessItems) => {
ctx.patchState({
workbasketAccessItems
});
this.notificationService.showSuccess('WORKBASKET_ACCESS_ITEM_SAVE', {
workbasketKey: ctx.getState().selectedWorkbasket.key
});
return of(null);
})
);
}
@ -440,15 +400,13 @@ export class WorkbasketState implements NgxsAfterBootstrap {
});
}
this.requestInProgressService.setRequestInProgress(false);
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_8,
new Map<string, string>([['workbasketName', ctx.getState().selectedWorkbasket.name]])
);
this.notificationService.showSuccess('WORKBASKET_DISTRIBUTION_TARGET_SAVE', {
workbasketName: ctx.getState().selectedWorkbasket.name
});
return of(null);
},
(error) => {
this.notificationService.triggerError(NOTIFICATION_TYPES.SAVE_ERR_3, error);
() => {
this.requestInProgressService.setRequestInProgress(false);
}
)

View File

@ -10,9 +10,8 @@ import { ObjectReference } from 'app/workplace/models/object-reference';
import { Workbasket } from 'app/shared/models/workbasket';
import { WorkplaceService } from 'app/workplace/services/workplace.service';
import { MasterAndDetailService } from 'app/shared/services/master-and-detail/master-and-detail.service';
import { NOTIFICATION_TYPES } from '../../../shared/models/notifications';
import { NotificationService } from '../../../shared/services/notifications/notification.service';
import { takeUntil } from 'rxjs/operators';
import { take, takeUntil } from 'rxjs/operators';
@Component({
selector: 'taskana-task-details',
@ -75,7 +74,7 @@ export class TaskDetailsComponent implements OnInit, OnDestroy {
this.task.customAttributes = this.taskClone.customAttributes.slice(0);
this.task.callbackInfo = this.taskClone.callbackInfo.slice(0);
this.task.primaryObjRef = { ...this.taskClone.primaryObjRef };
this.notificationService.showToast(NOTIFICATION_TYPES.INFO_ALERT);
this.notificationService.showSuccess('TASK_RESTORE');
}
getTask(): void {
@ -91,8 +90,8 @@ export class TaskDetailsComponent implements OnInit, OnDestroy {
this.cloneTask();
this.taskService.selectTask(task);
},
(error) => {
this.notificationService.triggerError(NOTIFICATION_TYPES.FETCH_ERR_7, error);
() => {
this.requestInProgressService.setRequestInProgress(false);
}
);
}
@ -111,22 +110,22 @@ export class TaskDetailsComponent implements OnInit, OnDestroy {
deleteTask(): void {
this.notificationService.showDialog(
`You are going to delete Task: ${this.currentId}. Can you confirm this action?`,
'TASK_DELETE',
{ taskId: this.currentId },
this.deleteTaskConfirmation.bind(this)
);
}
deleteTaskConfirmation(): void {
this.deleteTaskSubscription = this.taskService.deleteTask(this.task).subscribe(
() => {
this.deleteTaskSubscription = this.taskService
.deleteTask(this.task)
.pipe(take(1))
.subscribe(() => {
this.notificationService.showSuccess('TASK_DELETE', { taskName: this.task.name });
this.taskService.publishTaskDeletion();
this.task = null;
this.router.navigate(['taskana/workplace/tasks'], { queryParamsHandling: 'merge' });
},
(error) => {
this.notificationService.triggerError(NOTIFICATION_TYPES.DELETE_ERR_2, error);
}
);
});
}
selectTab(tab: string): void {
@ -169,11 +168,10 @@ export class TaskDetailsComponent implements OnInit, OnDestroy {
this.task = task;
this.cloneTask();
this.taskService.publishUpdatedTask(task);
this.notificationService.showToast(NOTIFICATION_TYPES.SUCCESS_ALERT_14);
this.notificationService.showSuccess('TASK_UPDATE', { taskName: task.name });
},
() => {
this.requestInProgressService.setRequestInProgress(false);
this.notificationService.showToast(NOTIFICATION_TYPES.DANGER_ALERT);
}
);
}
@ -184,10 +182,7 @@ export class TaskDetailsComponent implements OnInit, OnDestroy {
this.taskService.createTask(this.task).subscribe(
(task) => {
this.requestInProgressService.setRequestInProgress(false);
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_13,
new Map<string, string>([['taskId', task.name]])
);
this.notificationService.showSuccess('TASK_CREATE', { taskName: task.name });
this.task = task;
this.taskService.selectTask(this.task);
this.taskService.publishUpdatedTask(task);
@ -195,7 +190,6 @@ export class TaskDetailsComponent implements OnInit, OnDestroy {
},
() => {
this.requestInProgressService.setRequestInProgress(false);
this.notificationService.showToast(NOTIFICATION_TYPES.DANGER_ALERT_2);
}
);
}

View File

@ -10,7 +10,6 @@ import { Page } from 'app/shared/models/page';
import { take, takeUntil } from 'rxjs/operators';
import { Search } from '../task-list-toolbar/task-list-toolbar.component';
import { NotificationService } from '../../../shared/services/notifications/notification.service';
import { NOTIFICATION_TYPES } from '../../../shared/models/notifications';
import { QueryPagingParameter } from '../../../shared/models/query-paging-parameter';
import { TaskQueryFilterParameter } from '../../../shared/models/task-query-filter-parameter';
import { Select, Store } from '@ngxs/store';
@ -144,7 +143,7 @@ export class TaskMasterComponent implements OnInit, OnDestroy {
} else {
this.tasks = [];
if (this.selectedSearchType === Search.byWorkbasket) {
this.notificationsService.showToast(NOTIFICATION_TYPES.INFO_ALERT_2);
this.notificationsService.showInformation('EMPTY_WORKBASKET');
}
}
this.tasksPageInformation = taskResource.page;

View File

@ -502,3 +502,7 @@ li.list-group-item:hover {
.mat-select-value-text {
color: #4a5568 !important;
}
.hot-toast-icon {
align-self: center !important
}

View File

@ -1502,6 +1502,20 @@
merge-source-map "^1.1.0"
schema-utils "^2.7.0"
"@ngneat/hot-toast@3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@ngneat/hot-toast/-/hot-toast-3.1.0.tgz#1338530c70d77643abf9fc344c1e6f600f2e8683"
integrity sha512-lbBEkPf2/I6L7yXFK3bssP/yNwT/uV5P/fuMgWeB8t4gHIPl9aTwF+j2DWhYEJGoDDlztvaXaimV6Yt2C8l0+Q==
dependencies:
tslib "^2.0.0"
"@ngneat/overview@2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@ngneat/overview/-/overview-2.0.2.tgz#557dbb801e8b3e42d3cda57f5feb79319ef2f120"
integrity sha512-BARS4lUrWW5BidZS6jKtZc3okV7ro+3QKxJKs1FgeDDSomNdNACEjB0BMITeulEQ+agE4n9U+sbGpF2guOXDQA==
dependencies:
tslib "^2.0.0"
"@ngtools/webpack@12.0.5":
version "12.0.5"
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-12.0.5.tgz#3d858c1df3a4f5a13450fb0cffe0d5db8e61d0e1"