Commit c14146b0 authored by Christopher Keim's avatar Christopher Keim
Browse files

[TOB-169,290,188,53,350,351,26,23,358] feat: v0.8.0

[TOB-169] feat: Add error messages and extended error handling

 * Add utility rxjs operators for error handling
 * Add toast component and effect for displaying toast
 * Add actions and reducers for statement errors
 * Show error messages in details page
 * Show error messages in edit page
 * Show default error message for unexpected errors
 * Improve error handling in effects
 * Reorganize translations

[TOB-290] feat: Add authorization error handling for process tasks

 * Disable side menu buttons if task is claimed by another user
 * Hide side menu buttons if task can not be claimed at all

[TOB-188] feat: Select geographic position in workflow data form

 * Add leaflet configuration to package.json
 * Add styles for leaflet
 * Add rxjs operator for entering/leaving ngZone
 * Add wrapper directives for leaflet
 * Add select component for map coordinates
 * Submit geographic position in workflow form effect

[TOB-53] feat: Add email functionality

* A...
parent ebb6e8bb
......@@ -11,13 +11,98 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
import {Component} from "@angular/core";
import {Component, OnDestroy, OnInit} from "@angular/core";
import {Router} from "@angular/router";
import {select, Store} from "@ngrx/store";
import {Subject} from "rxjs";
import {concatMap, filter, map, switchMap, take, takeUntil} from "rxjs/operators";
import {
deleteEmailFromInboxAction,
downloadEmailAttachmentAction,
fetchEmailAction,
fetchEmailInboxAction
} from "../../../../store/mail/actions";
import {getEmailInboxSelector, getEmailLoadingSelector, getSelectedEmailSelector} from "../../../../store/mail/selectors";
import {queryParamsMailIdSelector} from "../../../../store/root/selectors";
import {arrayJoin} from "../../../../util/store";
@Component({
selector: "app-mail",
templateUrl: "./mail.component.html",
styleUrls: ["./mail.component.scss"]
})
export class MailComponent {
export class MailComponent implements OnInit, OnDestroy {
public loading$ = this.store.pipe(select(getEmailLoadingSelector));
public emailInbox$ = this.store.pipe(select(getEmailInboxSelector));
public selectedEmailId$ = this.store.pipe(select(queryParamsMailIdSelector));
public selectedEmail$ = this.store.pipe(select(getSelectedEmailSelector));
private destroy$ = new Subject();
public constructor(
public store: Store,
public readonly router: Router
) {
}
public ngOnInit() {
this.fetchInbox();
this.selectedEmailId$.pipe(
concatMap((id) => {
return this.emailInbox$.pipe(
filter((_) => arrayJoin(_).length <= 0),
take(1),
map((mails) => {
return mails.find((_) => _.identifier === id) == null ? id : null;
})
);
}),
takeUntil(this.destroy$)
).subscribe((mailId) => this.fetch(mailId));
this.selectedEmailId$.pipe(
takeUntil(this.destroy$),
filter((id) => id == null),
switchMap(() => {
return this.emailInbox$.pipe(
map((mails) => {
return mails[0]?.identifier;
}),
filter((id) => id != null),
take(1)
);
},
)
).subscribe((id) => {
this.router.navigate(["mail"], {queryParams: {mailId: id}});
});
}
public ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
public async fetchInbox() {
this.store.dispatch(fetchEmailInboxAction());
}
public fetch(mailId: string) {
this.store.dispatch(fetchEmailAction({mailId}));
}
public downloadAttachment(mailId: string, name: string) {
this.store.dispatch(downloadEmailAttachmentAction({mailId, name}));
}
public remove(mailId: string) {
this.store.dispatch(deleteEmailFromInboxAction({mailId, navigateTo: "mail"}));
}
}
......@@ -13,6 +13,7 @@
import {NgModule} from "@angular/core";
import {RouterModule, Routes} from "@angular/router";
import {OfficialInChargeRouteGuardService} from "../../store/root/services";
import {MailComponent} from "./components";
import {MailModule} from "./mail.module";
......@@ -20,7 +21,8 @@ const routes: Routes = [
{
path: "",
pathMatch: "full",
component: MailComponent
component: MailComponent,
canActivate: [OfficialInChargeRouteGuardService]
}
];
......
......@@ -13,19 +13,45 @@
import {CommonModule} from "@angular/common";
import {NgModule} from "@angular/core";
import {MatIconModule} from "@angular/material/icon";
import {RouterModule} from "@angular/router";
import {TranslateModule} from "@ngx-translate/core";
import {DateControlModule} from "../../shared/controls/date-control";
import {ActionButtonModule} from "../../shared/layout/action-button";
import {CollapsibleModule} from "../../shared/layout/collapsible";
import {SideMenuModule} from "../../shared/layout/side-menu";
import {MailComponent} from "./components";
import {SharedPipesModule} from "../../shared/pipes";
import {ProgressSpinnerModule} from "../../shared/progress-spinner";
import {MailComponent, MailDetailsComponent, MailInboxComponent} from "./components";
import {EmailTextToArrayPipe} from "./pipes/email-text.pipe";
import {ExtractMailAddressPipe} from "./pipes/extract-mail-address.pipe";
import {SenderSplitNameMailPipe} from "./pipes/sender-split-name-mail.pipe";
@NgModule({
imports: [
CommonModule,
SideMenuModule
SideMenuModule,
RouterModule,
SharedPipesModule,
ActionButtonModule,
MatIconModule,
ProgressSpinnerModule,
DateControlModule,
CollapsibleModule,
TranslateModule
],
declarations: [
MailComponent
MailComponent,
MailInboxComponent,
MailDetailsComponent,
EmailTextToArrayPipe,
SenderSplitNameMailPipe,
ExtractMailAddressPipe
],
exports: [
MailComponent
MailComponent,
MailInboxComponent,
MailDetailsComponent
]
})
export class MailModule {
......
/********************************************************************************
* Copyright (c) 2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
import {EmailTextToArrayPipe} from "./email-text.pipe";
describe("EmailTextToArrayPipe", () => {
const pipe = new EmailTextToArrayPipe();
describe("transform", () => {
it("should convert the input text to an array of the text divided by the newlines", () => {
const inputText = `This is a test text \n\n and this is another row \n last row.`;
const result: string[] = pipe.transform(inputText);
expect(result.length).toBe(4);
expect(result).toEqual(
[
"This is a test text ",
"",
" and this is another row ",
" last row."
]
);
});
});
});
/********************************************************************************
* Copyright (c) 2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
import {Pipe, PipeTransform} from "@angular/core";
@Pipe({
name: "appEmailTextToArray"
})
export class EmailTextToArrayPipe implements PipeTransform {
public transform(text: string): Array<string> {
const textAsArray = text.replace("\r", "").split("\n");
return textAsArray.filter((el, index) => el.trim() !== "" || (index > 0 && index < textAsArray.length - 1));
}
}
/********************************************************************************
* Copyright (c) 2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
import {ExtractMailAddressPipe} from "./extract-mail-address.pipe";
describe("ExtractMailAddressPipe", () => {
const pipe = new ExtractMailAddressPipe();
describe("transform", () => {
it("should extract the email from between the <> tags", () => {
const inputText = "Max Mustermann <max@mustermann.muster>";
const result = pipe.transform(inputText);
expect(result).toBe("max@mustermann.muster");
});
it("should return the input text unchanged if no <> tags are present", () => {
const inputText = "Max Mustermann max@mustermann.muster";
const result = pipe.transform(inputText);
expect(result).toBe("Max Mustermann max@mustermann.muster");
});
});
});
/********************************************************************************
* Copyright (c) 2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
import {Pipe, PipeTransform} from "@angular/core";
import {arrayJoin} from "../../../util/store";
@Pipe({
name: "appExtractMailAddress"
})
export class ExtractMailAddressPipe implements PipeTransform {
public transform(text: string): string {
const splitText = text?.split("<");
return arrayJoin(splitText)[1] ? arrayJoin(splitText)[1].replace(">", "") : text;
}
}
/********************************************************************************
* Copyright (c) 2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
import {SenderSplitNameMailPipe} from "./sender-split-name-mail.pipe";
describe("SenderSplitNameMailPipe", () => {
const pipe = new SenderSplitNameMailPipe();
describe("transform", () => {
it("should split the input text at the first occurance of <", () => {
const inputText = "Max Mustermann <max@mustermann.muster> <example@example.example>";
const result: string[] = pipe.transform(inputText);
expect(result).toEqual([
"Max Mustermann ",
"<max@mustermann.muster> <example@example.example>"
]);
});
it("should return the text unchanged if no < delimter is present", () => {
const inputText = "Max Mustermann max@mustermann.muster example@example.example";
const result: string[] = pipe.transform(inputText);
console.log(result);
expect(result).toEqual([
"Max Mustermann max@mustermann.muster example@example.example"
]);
});
it("should return empty array for no value supplied", () => {
const inputText = null;
const result: string[] = pipe.transform(inputText);
expect(result).toEqual([]);
});
});
});
/********************************************************************************
* Copyright (c) 2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
import {Pipe, PipeTransform} from "@angular/core";
import {arrayJoin} from "../../../util/store";
@Pipe({
name: "appSenderSplitNameMail"
})
export class SenderSplitNameMailPipe implements PipeTransform {
public transform(text: string): Array<string> {
const textAsArray = arrayJoin(text?.split(/(?=<)/g));
return [textAsArray[0], textAsArray.slice(1).join("")].filter(x => x);
// return text.split(/(?=<)(.+)/);
}
}
......@@ -17,6 +17,8 @@ import {NgModule} from "@angular/core";
import {MatIconModule} from "@angular/material/icon";
import {RouterModule} from "@angular/router";
import {TranslateModule} from "@ngx-translate/core";
import {ButtonModule} from "primeng/button";
import {ToastModule} from "primeng/toast";
import {DropDownModule} from "../../shared/layout/drop-down";
import {SideMenuModule} from "../../shared/layout/side-menu";
import {ExitPageComponent, NavDropDownComponent, NavFrameComponent, NavHeaderComponent, NavigationComponent} from "./components";
......@@ -30,7 +32,9 @@ import {ExitPageComponent, NavDropDownComponent, NavFrameComponent, NavHeaderCom
DropDownModule,
ScrollingModule,
SideMenuModule
SideMenuModule,
ToastModule,
ButtonModule
],
declarations: [
ExitPageComponent,
......
......@@ -13,8 +13,11 @@
<button #appDropDown="appDropDown"
(click)="appDropDown.toggle()"
(appClose)="isOpen = false"
[appDropDown]="dropDownTemplate"
[class.nav-drop-down-button-opened]="appDropDown.isOpen"
(appOpen)="isOpen = true"
[appConnectedPositions]="connectedPositions"
[class.nav-drop-down-button-opened]="isOpen"
class="nav-drop-down-button nav-drop-down-toggle cursor-pointer user-select-none">
<span class="nav-drop-down-button-label">
......
......@@ -97,6 +97,7 @@ $nav-drop-down-menu-background: get-color($openk-info-palette, A100);
display: flex;
flex-flow: column;
width: 100%;
box-sizing: border-box;
background-color: $nav-drop-down-menu-background;
color: $nav-drop-down-menu-contrast;
border: 1px solid $nav-drop-down-menu-toggle-border;
......
......@@ -11,6 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
import {ConnectedPosition} from "@angular/cdk/overlay";
import {Component, EventEmitter, Output} from "@angular/core";
@Component({
......@@ -20,6 +21,18 @@ import {Component, EventEmitter, Output} from "@angular/core";
})
export class NavDropDownComponent {
public isOpen: boolean;
public connectedPositions: ConnectedPosition[] = [
{
originX: "center",
originY: "bottom",
overlayX: "center",
overlayY: "top",
panelClass: "bottom"
}
];
@Output()
public appLogOut = new EventEmitter<void>();
......
......@@ -19,7 +19,10 @@
[appUserRoles]="appUserRoles">
</app-nav-header>
<app-side-menu-container cdk-scrollable class="nav-frame-content">
<app-side-menu-container
[appHideSideMenu]="appExitCode != null"
cdk-scrollable
class="nav-frame-content">
<div class="nav-frame-content-main">
......
......@@ -39,6 +39,7 @@
[class.openk-primary]="isLinkActive(route.link, route.exact)"
[routerLink]="route.link"
[target]="route.target"
[title]="route.tooltip?.length > 0 ? (route.tooltip | translate) : ''"
class="openk-button openk-button-rounded nav-header-menu-anchor">
<mat-icon>{{route.icon}}</mat-icon>
</a>
......
......@@ -13,7 +13,7 @@
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
import {RouterTestingModule} from "@angular/router/testing";
import {EAPIUserRoles} from "../../../../core";
import {EAPIUserRoles, I18nModule} from "../../../../core";
import {AppNavigationFrameModule} from "../../app-navigation-frame.module";
import {NavHeaderComponent} from "./nav-header.component";
......@@ -23,9 +23,12 @@ describe("NavHeaderComponent", () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule, AppNavigationFrameModule]
})
.compileComponents();
imports: [
RouterTestingModule,
AppNavigationFrameModule,
I18nModule
]
}).compileComponents();
}));
beforeEach(() => {
......
......@@ -18,6 +18,7 @@ import {arrayJoin} from "../../../../util/store";
export interface INavHeaderRoute {
icon: string;
link: string;
tooltip?: string;
exact?: boolean;
roles?: EAPIUserRoles[];
target?: string;
......@@ -51,30 +52,36 @@ export class NavHeaderComponent {
{
icon: "home",
link: "/",
tooltip: "core.header.home",
exact: true
},
{
icon: "find_in_page",
link: "/search"
link: "/search",
tooltip: "core.header.search"
},
{
icon: "email",
link: "/mail",
tooltip: "core.header.mail",
roles: [EAPIUserRoles.SPA_OFFICIAL_IN_CHARGE]
},
{
icon: "note_add",
link: "/new",
tooltip: "core.header.new",
roles: [EAPIUserRoles.SPA_OFFICIAL_IN_CHARGE]
},
{
icon: "settings",
link: "/settings",
roles: [EAPIUserRoles.SPA_OFFICIAL_IN_CHARGE]
tooltip: "core.header.settings",
roles: [EAPIUserRoles.SPA_OFFICIAL_IN_CHARGE, EAPIUserRoles.SPA_ADMIN]
},
{
icon: "help_outline",
link: "/help",
tooltip: "core.header.help",
target: "_blank",
largeIcon: true
}
......
......@@ -11,6 +11,8 @@
* SPDX-License-Identifier: EPL-2.0
-------------------------------------------------------------------------------->
<p-toast *ngIf="(exitCode$ | async) == null" [preventOpenDuplicates]="true"></p-toast>
<app-nav-frame
(appCloseWindow)="closeWindow()"
(appLogout)="logOut()"
......
......@@ -15,6 +15,8 @@ import {ComponentFixture, TestBed} from "@angular/core/testing";
import {RouterTestingModule} from "@angular/router/testing";
import {StoreModule} from "@ngrx/store";
import {MockStore, provideMockStore} from "@ngrx/store/testing";
import {MessageService} from "primeng/api";
import {ToastModule} from "primeng/toast";
import {I18nModule, WINDOW} from "../../../../core";
import {logOutAction} from "../../../../store";
import {AppNavigationFrameModule} from "../../app-navigation-frame.module";
......@@ -22,7 +24,9 @@ import {NavigationComponent} from "./navigation.component";
class WindowMock {
public opener: any;
public close() {}
public close() {
}
}
describe("NavigationComponent", () => {
......@@ -38,12 +42,14 @@ describe("NavigationComponent", () => {
I18nModule,
RouterTestingModule,
StoreModule,
ToastModule
],
providers:
[