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
...@@ -47,7 +47,7 @@ The project maintains the following source code repositories: ...@@ -47,7 +47,7 @@ The project maintains the following source code repositories:
## Third-party Content ## Third-party Content
@angular-devkit/build-angular (0.901.11) @angular-devkit/build-angular (0.901.12)
* License: MIT * License: MIT
* Homepage: https://github.com/angular/angular-cli * Homepage: https://github.com/angular/angular-cli
......
...@@ -22,10 +22,21 @@ the `./package.json`. The following options are available: ...@@ -22,10 +22,21 @@ the `./package.json`. The following options are available:
* `routes.spaFrontend`: Route on which the website is served * `routes.spaFrontend`: Route on which the website is served
* `routes.spaBackend`: Route on which the website's backend is served * `routes.spaBackend`: Route on which the website's backend is served
* `routes.portal`: Route on which the main portal is served * `routes.portal`: Route on which the main portal is served
* `routes.contactDataBase`: Route on which the contact data base module is served
Changes to these properties take only effect after rebuilding the Changes to these properties take only effect after rebuilding the
application. application.
Additionally, the following options can be used to configure all map views based
on [Leaflet](https://leafletjs.com):
* `leaflet.templateUrl`: Route to the map tile server required by leaflet
* `leaflet.attribution`: Attribution which is added to the leaflet map, e.g.
`&copy; <a>TileServer</a> contributors`
* `leaflet.gis`: Route to a GIS system
* `leaflet.lat`/`leaflet.lng`/`leaflet.zoom`: Default coordinates and zoom
level to which all leaflet maps are initially configured
## Build ## Build
Building the application is done via the Angular CLI or by the Building the application is done via the Angular CLI or by the
......
{ {
"name": "openkonsequenz-statement-public-affairs", "name": "openkonsequenz-statement-public-affairs",
"version": "0.6.0", "version": "0.8.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
......
{ {
"name": "openkonsequenz-statement-public-affairs", "name": "openkonsequenz-statement-public-affairs",
"version": "0.7.0", "version": "0.8.0",
"description": "Statement Public Affairs", "description": "Statement Public Affairs",
"license": "Eclipse Public License - v 2.0", "license": "Eclipse Public License - v 2.0",
"repository": { "repository": {
...@@ -13,6 +13,14 @@ ...@@ -13,6 +13,14 @@
"portal": "/portalFE", "portal": "/portalFE",
"contactDataBase": "/contactdatabase" "contactDataBase": "/contactdatabase"
}, },
"leaflet": {
"urlTemplate": "https://localhost:4200/{s}/{z}/{x}/{y}.png",
"attribution": "&copy;",
"gis": "http://localhost:4200?X=##C_X##&Y=##C_Y##pLLX=##LL_X##&pLLY=##LL_Y##&pURX=##UR_X##&pURY=##UR_Y##&user=##OS_USER##",
"lat": 49.87282103349044,
"lng": 8.651196956634523,
"zoom": 12
},
"scripts": { "scripts": {
"-- Build ----------------": "", "-- Build ----------------": "",
"build": "ng build --prod --base-href /statementpaFE/", "build": "ng build --prod --base-href /statementpaFE/",
......
...@@ -14,18 +14,12 @@ ...@@ -14,18 +14,12 @@
import {Location} from "@angular/common"; import {Location} from "@angular/common";
import {NgZone} from "@angular/core"; import {NgZone} from "@angular/core";
import {async, TestBed} from "@angular/core/testing"; import {async, TestBed} from "@angular/core/testing";
import {CanActivate, Router} from "@angular/router"; import {Router} from "@angular/router";
import {RouterTestingModule} from "@angular/router/testing"; import {RouterTestingModule} from "@angular/router/testing";
import {provideMockStore} from "@ngrx/store/testing";
import {appRoutes} from "./app-routing.module"; import {appRoutes} from "./app-routing.module";
import {NewStatementRouteGuardService} from "./features/new/services/new-statement-route-guard.service"; import {ALL_NON_TRIVIAL_USER_ROLES} from "./core/api/core";
import {userRolesSelector} from "./store/root/selectors";
class RouteGuardMock implements CanActivate {
public canActivate() {
return true;
}
}
describe("AppRoutingModule", () => { describe("AppRoutingModule", () => {
let router: Router; let router: Router;
...@@ -44,10 +38,12 @@ describe("AppRoutingModule", () => { ...@@ -44,10 +38,12 @@ describe("AppRoutingModule", () => {
RouterTestingModule.withRoutes(appRoutes) RouterTestingModule.withRoutes(appRoutes)
], ],
providers: [ providers: [
{ provideMockStore({
provide: NewStatementRouteGuardService, selectors: [{
useClass: RouteGuardMock selector: userRolesSelector,
} value: [...ALL_NON_TRIVIAL_USER_ROLES]
}]
})
] ]
}).compileComponents(); }).compileComponents();
router = TestBed.inject(Router); router = TestBed.inject(Router);
......
...@@ -20,6 +20,8 @@ import {AppRoutingModule} from "./app-routing.module"; ...@@ -20,6 +20,8 @@ import {AppRoutingModule} from "./app-routing.module";
import {AppComponent} from "./app.component"; import {AppComponent} from "./app.component";
import {CoreModule} from "./core"; import {CoreModule} from "./core";
import {AppNavigationFrameModule} from "./features/navigation"; import {AppNavigationFrameModule} from "./features/navigation";
import {LeafletModule} from "./shared/layout/leaflet";
import {SideMenuRegistrationService} from "./shared/layout/side-menu/services";
import {AppStoreModule} from "./store"; import {AppStoreModule} from "./store";
@NgModule({ @NgModule({
...@@ -36,6 +38,8 @@ import {AppStoreModule} from "./store"; ...@@ -36,6 +38,8 @@ import {AppStoreModule} from "./store";
AppStoreModule, AppStoreModule,
AppNavigationFrameModule, AppNavigationFrameModule,
LeafletModule.for(SideMenuRegistrationService),
// This import is only important for development; in production, nothing is imported. // This import is only important for development; in production, nothing is imported.
// ! This import must come after AppStoreModule in order make the NGRX Store Devtools available. ! // ! This import must come after AppStoreModule in order make the NGRX Store Devtools available. !
...environment.imports ...environment.imports
......
...@@ -15,11 +15,13 @@ export enum EAPIUserRoles { ...@@ -15,11 +15,13 @@ export enum EAPIUserRoles {
DIVISION_MEMBER = "ROLE_SPA_DIVISION_MEMBER", DIVISION_MEMBER = "ROLE_SPA_DIVISION_MEMBER",
ROLE_SPA_ACCESS = "ROLE_SPA_ACCESS", ROLE_SPA_ACCESS = "ROLE_SPA_ACCESS",
SPA_APPROVER = "ROLE_SPA_APPROVER", SPA_APPROVER = "ROLE_SPA_APPROVER",
SPA_OFFICIAL_IN_CHARGE = "ROLE_SPA_OFFICIAL_IN_CHARGE" SPA_OFFICIAL_IN_CHARGE = "ROLE_SPA_OFFICIAL_IN_CHARGE",
SPA_ADMIN = "ROLE_SPA_ADMIN"
} }
export const ALL_NON_TRIVIAL_USER_ROLES = [ export const ALL_NON_TRIVIAL_USER_ROLES = [
EAPIUserRoles.DIVISION_MEMBER, EAPIUserRoles.DIVISION_MEMBER,
EAPIUserRoles.SPA_OFFICIAL_IN_CHARGE, EAPIUserRoles.SPA_OFFICIAL_IN_CHARGE,
EAPIUserRoles.SPA_APPROVER EAPIUserRoles.SPA_APPROVER,
EAPIUserRoles.SPA_ADMIN
]; ];
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
export * from "./attachments"; export * from "./attachments";
export * from "./contacts"; export * from "./contacts";
export * from "./core"; export * from "./core";
export * from "./mail";
export * from "./process"; export * from "./process";
export * from "./settings"; export * from "./settings";
export * from "./shared"; export * from "./shared";
......
...@@ -11,19 +11,8 @@ ...@@ -11,19 +11,8 @@
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
********************************************************************************/ ********************************************************************************/
@import "openk.styles"; export interface IAPIEmailAttachmentModel {
name: string;
.dashboard-item-header-actions { size: string;
display: inline-flex; type: string;
margin-left: auto;
& > * {
margin-left: 0.5em;
}
}
.dashboard-item-body {
padding: 1em;
display: flex;
flex-flow: column;
} }
...@@ -11,16 +11,14 @@ ...@@ -11,16 +11,14 @@
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
********************************************************************************/ ********************************************************************************/
import {Component, Input} from "@angular/core"; import {IAPIEmailAttachmentModel} from "./IAPIEmailAttachmentModel";
@Component({
selector: "app-dashboard-item",
templateUrl: "./dashboard-item.component.html",
styleUrls: ["./dashboard-item.component.scss"]
})
export class DashboardItemComponent {
@Input()
public appItem: any;
export interface IAPIEmailModel {
identifier: string;
subject: string;
date: string;
from: string;
textPlain: string;
textHtml: string;
attachments: IAPIEmailAttachmentModel[];
} }
/********************************************************************************
* 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-api.service";
export * from "./IAPIEmailAttachmentModel";
export * from "./IAPIEmailModel";
/********************************************************************************
* 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 {HttpClient} from "@angular/common/http";
import {Inject, Injectable} from "@angular/core";
import {Observable} from "rxjs";
import {urlJoin} from "../../../util/http";
import {SPA_BACKEND_ROUTE} from "../../external-routes";
import {IAPIAttachmentModel} from "../attachments";
import {IAPIEmailModel} from "./IAPIEmailModel";
@Injectable({providedIn: "root"})
export class MailApiService {
public constructor(
protected readonly httpClient: HttpClient,
@Inject(SPA_BACKEND_ROUTE) protected readonly baseUrl: string
) {
}
/**
* Fetches a list of all emails in the module's email inbox.
*/
public getInbox() {
const endPoint = `/mail/inbox`;
return this.httpClient.get<IAPIEmailModel[]>(urlJoin(this.baseUrl, endPoint));
}
/**
* Deletes a specific email from the module's email inbox.
*/
public deleteInboxEmail(mailId: string) {
const endPoint = `/mail/inbox/${mailId}`;
return this.httpClient.delete(urlJoin(this.baseUrl, endPoint));
}
/**
* Fetches a specific email from the module's email inbox.
*/
public getEmail(mailId: string) {
const endPoint = `/mail/identifier/${mailId}`;
return this.httpClient.get<IAPIEmailModel>(urlJoin(this.baseUrl, endPoint));
}
/**
* Transfers the linked email's body of a statement to its attachments.
*/
public transferMailText(statementId: number, taskId: string) {
const endPoint = `/process/statements/${statementId}/task/${taskId}/transfermailtext`;
return this.httpClient.post<IAPIAttachmentModel>(urlJoin(this.baseUrl, endPoint), null);
}
/**
* Transfers a list of email attachments for a statement to its attachments.
*/
public transferMailAttachment(statementId: number, taskId: string, body: Array<{ name: string, tagIds: string[] }>)
: Observable<IAPIAttachmentModel[]> {
const endPoint = `/process/statements/${statementId}/task/${taskId}/transfermailattachments`;
return this.httpClient.post<IAPIAttachmentModel[]>(urlJoin(this.baseUrl, endPoint), body);
}
/**
* Re-sends the outgoing email for a statement.
*/
public dispatchStatement(statementId: number, taskId: string) {
const endPoint = `/process/statements/${statementId}/task/${taskId}/maildispatch`;
return this.httpClient.post(urlJoin(this.baseUrl, endPoint), null);
}
}
...@@ -28,6 +28,10 @@ export interface IAPIProcessTask { ...@@ -28,6 +28,10 @@ export interface IAPIProcessTask {
assignee: string; assignee: string;
authorized: boolean;
name?: string;
requiredVariables: { requiredVariables: {
[key: string]: string [key: string]: string
}; };
......
/********************************************************************************
* 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 {IAPIProcessTask} from "../process";
import {IAPIStatementModel} from "./IAPIStatementModel";
export interface IAPIDashboardStatementModel {
info: IAPIStatementModel;
tasks: IAPIProcessTask[];
editedByMe: boolean;
mandatoryDepartmentsCount: number;
mandatoryContributionsCount: number;
optionalForMyDepartment: boolean;
completedForMyDepartment: boolean;
}
...@@ -38,4 +38,10 @@ export interface IAPIPartialStatementModel { ...@@ -38,4 +38,10 @@ export interface IAPIPartialStatementModel {
contactId: string; contactId: string;
sourceMailId: string;
creationDate: string;
customerReference?: string;
} }
...@@ -18,6 +18,7 @@ import {SPA_BACKEND_ROUTE} from "../../external-routes"; ...@@ -18,6 +18,7 @@ import {SPA_BACKEND_ROUTE} from "../../external-routes";
import {IAPIDepartmentGroups} from "../settings"; import {IAPIDepartmentGroups} from "../settings";
import {IAPIPaginationResponse, IAPISearchOptions} from "../shared"; import {IAPIPaginationResponse, IAPISearchOptions} from "../shared";
import {IAPICommentModel} from "./IAPICommentModel"; import {IAPICommentModel} from "./IAPICommentModel";
import {IAPIDashboardStatementModel} from "./IAPIDashboardStatementModel";
import {IAPISectorsModel} from "./IAPISectorsModel"; import {IAPISectorsModel} from "./IAPISectorsModel";
import {IAPIPartialStatementModel, IAPIStatementModel} from "./IAPIStatementModel"; import {IAPIPartialStatementModel, IAPIStatementModel} from "./IAPIStatementModel";
import {IAPIWorkflowData} from "./IAPIWorkflowData"; import {IAPIWorkflowData} from "./IAPIWorkflowData";
...@@ -167,4 +168,12 @@ export class StatementsApiService { ...@@ -167,4 +168,12 @@ export class StatementsApiService {
return this.httpClient.patch(urlJoin(this.baseUrl, endPoint), null); return this.httpClient.patch(urlJoin(this.baseUrl, endPoint), null);
} }
/**
* Fetches the list of statements to display in the dashboard.
*/
public getDashboardStatements() {
const endPoint = `/dashboard/statements`;
return this.httpClient.get<IAPIDashboardStatementModel[]>(urlJoin(this.baseUrl, endPoint));
}
} }
...@@ -30,4 +30,9 @@ export interface IAPITextBlockConfigurationModel { ...@@ -30,4 +30,9 @@ export interface IAPITextBlockConfigurationModel {
*/ */
groups: IAPITextBlockGroupModel[]; groups: IAPITextBlockGroupModel[];
/**
* List of all available text block template groups for a negative response.
*/
negativeGroups: IAPITextBlockGroupModel[];
} }
...@@ -11,14 +11,20 @@ ...@@ -11,14 +11,20 @@
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
--------------------------------------------------------------------------------> -------------------------------------------------------------------------------->
<div class="dashboard-list-title"> <span *ngIf="appCaption" class="dashboard-list--caption">
<span class="dashboard-list-title-text"> {{appCaption}}
{{appTitle}} </span>
</span>
</div>
<app-dashboard-item <app-statement-table
*ngFor="let item of appItems" [appColumns]="(large$ | async)?.matches ? columns : columnsShort"
[appItem]="item" [appEntries]="appEntries"
class="dashboard-list-item"> [appShowAlert]="true"
</app-dashboard-item> [appShowContributionStatusForMyDepartment]="appShowContributionStatusForMyDepartment"
[appStatementTypeOptions]="appStatementTypeOptions"
class="openk-table---last-row-without-border dashboard-list--table">
</app-statement-table>
<span *ngIf="appShowSubCaption"
class="dashboard-list--sub-caption">
<ng-content></ng-content>
</span>
...@@ -11,23 +11,28 @@ ...@@ -11,23 +11,28 @@
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
********************************************************************************/ ********************************************************************************/
@import "openk.styles";
:host { :host {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
} }
.dashboard-list-title { .dashboard-list--caption {
margin: 0 auto 0.5em 0; margin-left: 1em;
font-style: italic;
font-weight: 600;
font-size: small;
} }
.dashboard-list-title-text { .dashboard-list--sub-caption {
font-size: large; font-style: italic;
font-size: small;
margin-right: 1em;
margin-left: auto;
} }
.dashboard-list-item { .dashboard-list--table {
width: 100%; min-height: 5.3125em;
background: get-color($openk-default-palette);
&:not(:last-child) {
margin-bottom: 1em;
}
} }
...@@ -12,6 +12,9 @@ ...@@ -12,6 +12,9 @@
********************************************************************************/ ********************************************************************************/
import {async, ComponentFixture, TestBed} from "@angular/core/testing"; import {async, ComponentFixture, TestBed} from "@angular/core/testing";
import {RouterTestingModule} from "@angular/router/testing";
import {I18nModule} from "../../../../core/i18n";
import {DashboardModule} from "../../dashboard.module";
import {DashboardListComponent} from "./dashboard-list.component"; import {DashboardListComponent} from "./dashboard-list.component";
describe("DashboardListComponent", () => { describe("DashboardListComponent", () => {
...@@ -20,9 +23,12 @@ describe("DashboardListComponent", () => { ...@@ -20,9 +23,12 @@ describe("DashboardListComponent", () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [DashboardListComponent] imports: [
}) DashboardModule,
.compileComponents(); RouterTestingModule,
I18nModule
]
}).compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
...@@ -34,4 +40,5 @@ describe("DashboardListComponent", () => { ...@@ -34,4 +40,5 @@ describe("DashboardListComponent", () => {
it("should create", () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
});