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

* Add abstract route guard service for user roles
* Add optional styling attribute to side menu
* Optionally hide toggle button in collapsible component
* Use general user role route guards
* Add back end calls for email endpoints
* Add store module for emails
* Add component for displaying email inbox
* Add component for displaying email details
* Automatically select values from email in statement info form
* Extend attachment form components for email information
* Extend attachment store module to handle email attachments

[TOB-350,351] feat: Add reference and creation date to statement model

 * Add additional properties to statement info model
 * Add additional input fields to statement info form

[TOB-26] feat: Add dashboard as general landing page

 * Add utility function to compute time diffs of dates
 * Add back end call for fetching dashboard statements
 * Add store functionality for dashboard
 * Add name attribute for process tasks
 * Add global styles for tables
 * Refactor statement table component
 * Reuse statement table component in dashboard component
 * Integrate store in dashboard component
 * Remove unused code

[TOB-23] feat: Adjust statement editor for negative statements

 * Use seperate text block groups for negative statements
 * Filter displayed arrangement for available text blocks

[TOB-358] fix: Fix minor bugs

 * Reword translations
 * Add tooltips to navigation header
 * Fix position and button style of navigation drop down
 * Fix resizing of side menu in Firefox
Signed-off-by: Christopher Keim's avatarChristopher Keim <keim@develop-group.de>
parent ebb6e8bb
......@@ -35,6 +35,7 @@
<app-side-menu-status
*appSideMenu="'center'"
[appLoadingMessage]="'core.submitting' | translate"
[appErrorMessage]="appErrorMessage"
[appLoading]="appLoading">
</app-side-menu-status>
......
......@@ -29,6 +29,9 @@ export class WorkflowDataSideMenuComponent {
@Input()
public appLoading: boolean;
@Input()
public appErrorMessage: string;
@Output()
public appSubmit = new EventEmitter<boolean>();
......
......@@ -15,8 +15,8 @@
(appSubmit)="submit($event)"
[appDisabled]="appFormGroup.disabled"
[appLoading]="isStatementLoading$ | async"
[appErrorMessage]="(appErrorMessage$ | async)?.errorMessage"
[appStatementId]="(task$ | async)?.statementId">
</app-workflow-data-side-menu>
......@@ -39,10 +39,14 @@
</app-collapsible>
<app-collapsible
[appCollapsed]="true"
[appTitle]="'workflowDataForm.container.geographicPosition' | translate">
<div style="padding: 1em;"> Not yet implemented.</div>
<app-map-select
[appActionButtonLabel]="'shared.map.openGIS' | translate"
[appCenter]="'leaflet.defaultView' | translate"
[formControlName]="'geographicPosition'"
class="geographic-position">
</app-map-select>
</app-collapsible>
......@@ -60,7 +64,6 @@
</app-collapsible>
<app-collapsible
[appCollapsed]="true"
[appTitle]="('workflowDataForm.container.linkedIssues' | translate) + ' (' + appFormGroup.value.parentIds?.length + ')'">
<app-statement-select
......
......@@ -31,8 +31,8 @@
}
.geographic-position {
box-sizing: border-box;
height: 3em;
padding: 1em;
height: 30em;
}
.departments {
......
......@@ -11,8 +11,9 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
import {Component, OnInit} from "@angular/core";
import {Component, OnDestroy, OnInit} from "@angular/core";
import {select, Store} from "@ngrx/store";
import {Observable} from "rxjs";
import {distinctUntilChanged, take, takeUntil} from "rxjs/operators";
import {IAPISearchOptions} from "../../../../core/api";
import {
......@@ -20,7 +21,11 @@ import {
departmentGroupsSelector,
departmentOptionsSelector,
getSearchContentStatementsSelector,
getStatementErrorSelector,
IStatementErrorEntity,
IWorkflowFormValue,
queryParamsIdSelector,
setErrorAction,
startStatementSearchAction,
statementLoadingSelector,
statementTypesSelector,
......@@ -35,10 +40,12 @@ import {AbstractReactiveFormComponent} from "../../abstract";
templateUrl: "./workflow-data-form.component.html",
styleUrls: ["./workflow-data-form.component.scss"]
})
export class WorkflowDataFormComponent extends AbstractReactiveFormComponent<IWorkflowFormValue> implements OnInit {
export class WorkflowDataFormComponent extends AbstractReactiveFormComponent<IWorkflowFormValue> implements OnInit, OnDestroy {
public task$ = this.store.pipe(select(taskSelector));
public statementId$ = this.store.pipe(select(queryParamsIdSelector));
public statementTypes$ = this.store.pipe(select(statementTypesSelector));
public searchContent$ = this.store.pipe(select(getSearchContentStatementsSelector));
......@@ -51,25 +58,26 @@ export class WorkflowDataFormComponent extends AbstractReactiveFormComponent<IWo
public appFormGroup = createWorkflowForm();
public appErrorMessage$: Observable<IStatementErrorEntity> = this.store.pipe(select(getStatementErrorSelector));
private form$ = this.store.pipe(select(workflowFormValueSelector));
public constructor(public store: Store) {
super();
}
public ngOnInit() {
this.patchValue({geographicPosition: "", departments: {selected: [], indeterminate: []}, parentIds: []});
this.isStatementLoading$.pipe(takeUntil(this.destroy$), distinctUntilChanged())
.subscribe((loading) => loading ? this.appFormGroup.disable() : this.appFormGroup.enable());
this.form$.pipe(takeUntil(this.destroy$))
.subscribe((value) => this.patchValue(value));
this.task$.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.search({q: ""});
});
public async ngOnInit() {
this.updateForm();
this.task$.pipe(takeUntil(this.destroy$)).subscribe(() => this.search({q: ""}));
}
public async ngOnDestroy() {
super.ngOnDestroy();
return this.clearErrors();
}
public async submit(completeTask?: boolean) {
await this.clearErrors();
const task = await this.task$.pipe(take(1)).toPromise();
this.store.dispatch(submitWorkflowDataFormAction({
statementId: task.statementId,
......@@ -83,4 +91,19 @@ export class WorkflowDataFormComponent extends AbstractReactiveFormComponent<IWo
this.store.dispatch(startStatementSearchAction({options}));
}
private async clearErrors() {
const statementId = await this.statementId$.pipe(take(1)).toPromise();
const loading = await this.isStatementLoading$.pipe(take(1)).toPromise();
if (statementId != null && !loading) {
this.store.dispatch(setErrorAction({statementId, error: null}));
}
}
private updateForm() {
this.isStatementLoading$.pipe(takeUntil(this.destroy$), distinctUntilChanged())
.subscribe((loading) => loading ? this.appFormGroup.disable() : this.appFormGroup.enable());
this.form$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.patchValue(value));
}
}
......@@ -16,6 +16,7 @@ import {NgModule} from "@angular/core";
import {ReactiveFormsModule} from "@angular/forms";
import {MatIconModule} from "@angular/material/icon";
import {TranslateModule} from "@ngx-translate/core";
import {MapSelectModule} from "../../../shared/controls/map-select/map-select.module";
import {SelectModule} from "../../../shared/controls/select";
import {StatementSelectModule} from "../../../shared/controls/statement-select";
import {ActionButtonModule} from "../../../shared/layout/action-button";
......@@ -34,7 +35,8 @@ import {WorkflowDataFormComponent, WorkflowDataSideMenuComponent} from "./compon
SelectModule,
StatementSelectModule,
SideMenuModule,
ActionButtonModule
ActionButtonModule,
MapSelectModule
],
declarations: [
WorkflowDataFormComponent,
......
......@@ -12,3 +12,5 @@
********************************************************************************/
export * from "./mail";
export * from "./mail-details";
export * from "./mail-inbox";
......@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
export * from "./dashboard-item.component";
export * from "./mail-details.component";
<!-------------------------------------------------------------------------------
* 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
-------------------------------------------------------------------------------->
<app-collapsible *ngIf="appEmail"
[appCollapsed]="false"
[appHeaderTemplateRef]="emailHeaderRef"
[appHideButton]="true"
[appTitle]="appEmail?.subject"
class="email">
<div class="email--content">
<div class="email--content--info">
<div class="email--content--info--text">
<span class="email--content--info--text---bold">{{"mails.sender" | translate}}</span>
<span class="email--content--info--text---bold">{{"mails.date" | translate}}</span>
</div>
<div class="email--content--info--text">
<a [href]="'mailto:' + appEmail.from" rel="noreferrer noopener" target="_blank">{{appEmail.from}}</a>
<span>{{(appEmail.date | appMomentPipe).format(dateFormat)}}</span>
</div>
</div>
<div class="email--content--body">
<div *ngFor="let text of (appEmail.textPlain | appEmailTextToArray), let i = index;">
{{text}} <br *ngIf="(appEmail.textPlain | appEmailTextToArray).length !== i">
</div>
<div *ngIf="appEmail?.textPlain === '\r\n'"
class="email--content--body--placeholder">{{"mails.noContent" | translate}}
</div>
</div>
<app-collapsible [appCollapsed]="false"
[appSimpleCollapsible]="true"
[appTitle]="('mails.attachments' | translate) + ' (' + (appEmail?.attachments ? appEmail.attachments.length.toString() : '0') +')'"
class="email--content--attachments">
<div *ngIf="appEmail?.attachments?.length > 0" class="email--content--attachments--row">
<button (click)="appDownloadAttachment.emit({ mailId: appEmail.identifier, name: attachment?.name })"
*ngFor="let attachment of appEmail?.attachments"
class="openk-button email--content--attachments--btn"
type="button">
{{attachment?.name}}
<mat-icon class="email--content--attachments--icon">get_app</mat-icon>
</button>
</div>
</app-collapsible>
</div>
</app-collapsible>
<div *ngIf="!appHideControls" class="email-controls">
<app-action-button
[appIcon]="'note_add'"
[appMailId]="appEmail?.identifier"
[appRouterLink]="'/new'"
class="openk-success">
{{"core.actions.createStatementFromEmail" | translate}}
</app-action-button>
</div>
<ng-template #emailHeaderRef>
<div class="email--header">
<app-progress-spinner
[class.progress-spinner---hidden]="!appDeleting">
</app-progress-spinner>
<button (click)="appRemoveFromInbox.emit(appEmail.identifier)"
*ngIf="!appHideControls && !appDeleting"
class="openk-button openk-button-rounded email--content--attachments--btn"
type="button">
<mat-icon>delete_forever</mat-icon>
</button>
</div>
</ng-template>
/********************************************************************************
* 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 "openk.styles";
:host {
width: 100%;
height: 100%;
padding: 1em;
box-sizing: border-box;
display: flex;
flex-flow: column;
}
.email-controls {
width: 100%;
margin-top: 1em;
display: flex;
justify-content: flex-end;
& > * {
width: initial;
margin-left: 0.5em;
}
}
.email--header {
display: flex;
justify-content: flex-end;
align-content: center;
margin-right: 0.125em;
}
.email--content--attachments--btn {
border: 0;
padding: 0.1em 0 0.1em 0.5em;
&:not(.openk-info) {
background-color: transparent;
}
&:not(.openk-info):active,
&:not(.openk-info):focus,
&:not(.openk-info):hover {
background-color: $openk-background-highlight;
}
}
.email--content--attachments--icon {
color: get-color($openk-info-palette);
font-size: 1em;
padding: 0;
}
.email--content--attachments {
border: initial;
background: initial;
font-size: smaller;
}
.email--content--attachments--row {
margin: 0 0.5em 0.5em 0.5em;
font-size: medium;
display: inline-block;
border-radius: 0.5em;
}
.email {
max-height: calc(100vh - 50px - 7.25em);
}
.email--content {
display: grid;
grid-template-rows: auto 1fr auto;
height: 100%;
}
.email--content--info {
display: flex;
padding: 0.5em;
border-bottom: 1px solid $openk-form-border;
}
.email--content--info--text {
display: flex;
flex-direction: column;
& > * {
margin-left: 0.25em;
}
}
.email--content--info--text---bold {
font-weight: 600;
}
.email--content--body {
overflow: auto;
padding: 0.5em 1em;
border-bottom: 1px solid $openk-form-border;
min-height: 5em;
}
.email--content--body--placeholder {
font-weight: 600;
width: 100%;
margin-top: 1.5em;
text-align: center;
}
.progress-spinner---hidden {
display: block;
font-size: 0;
height: 0;
width: 0;
overflow: hidden;
}
/********************************************************************************
* 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 {async, ComponentFixture, TestBed} from "@angular/core/testing";
import {RouterTestingModule} from "@angular/router/testing";
import {I18nModule} from "../../../../core/i18n";
import {MailModule} from "../../mail.module";
import {MailDetailsComponent} from "./mail-details.component";
describe("MailDetailsComponent", () => {
let component: MailDetailsComponent;
let fixture: ComponentFixture<MailDetailsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MailModule, RouterTestingModule, I18nModule]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MailDetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});
/********************************************************************************
* 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 {Component, EventEmitter, Input, Output} from "@angular/core";
import {IAPIEmailModel} from "../../../../core/api/mail";
import {momentFormatDisplayFullDateAndTime} from "../../../../util/moment";
@Component({
selector: "app-mail-details",
templateUrl: "./mail-details.component.html",
styleUrls: ["./mail-details.component.scss"]
})
export class MailDetailsComponent {
@Input()
public appEmail: IAPIEmailModel;
@Input()
public appHideControls: boolean;
@Input()
public appDeleting: boolean;
@Output()
public appDownloadAttachment = new EventEmitter<{ mailId: string, name: string }>();
@Output()
public appRemoveFromInbox = new EventEmitter<string>();
public dateFormat = momentFormatDisplayFullDateAndTime;
}
/********************************************************************************
* 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
********************************************************************************/
export * from "./mail-inbox.component";
<!-------------------------------------------------------------------------------
* 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
-------------------------------------------------------------------------------->
<div class="email-inbox--title">
<span class="email-inbox--title--text">
{{"mails.inbox" | translate}} {{"(" + (appMails?.length ? appMails.length : 0) + ")"}}
</span>
<app-progress-spinner
[class.progress-spinner---hidden]="!appLoading">
</app-progress-spinner>
<button (click)="appFetch.emit()"
*ngIf="!appLoading"
[disabled]="appLoading"
class="openk-button openk-button-rounded openk-info email-inbox--refresh-button">
<mat-icon>refresh</mat-icon>
</button>
</div>
<div class="email-inbox--list">
<a *ngFor="let item of appMails"
[class.email-inbox--list--element---active]="item.identifier === appSelectedMailId"
[queryParams]="{ mailId: item.identifier }"
[routerLink]="'/mail'"
class="email-inbox--list--element">
<span class="email-inbox--list--element--title">{{ item.subject }}</span>
<br>
<span class="email-inbox--list--element--date">
{{"mails.at" | translate}} {{(item.date | appMomentPipe).format(dateFormat)}}
</span>
<br>
<div class="email-inbox--list--element--sender">
<span class="email-inbox--list--element--sender--text">
{{"mails.from" | translate}}
</span>
<div class="email-inbox--list--element--sender--column">
<span *ngFor="let contact of (item.from | appSenderSplitNameMail)"
class="email-inbox--list--element--sender--text">
{{contact}}
</span>
</div>
</div>
</a>
</div>
......@@ -16,104 +16,87 @@
:host {
display: flex;
flex-flow: column;
height: 100%;
width: 100%;
overflow: auto;
box-sizing: border-box;
border: 1px solid $openk-form-border;