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

[TOB-127,21,406,74,315,52,92,354,391] feat: v0.9.0



[TOB-127] feat: Add components to display statement details

* Add back end calls for statement details
* Add component to display general statement information
* Add component to display geographic position
* Add component to display contribution status
* Add component to display statements inbox attachments
* Add component to display statements outbox attachments
* Add component to display linked statements
* Integrate details components in details page
* Integrate details components in form components
* Refresh process history on task change

[TOB-21] feat: Add functionality to manually redispatch statement email

* Add back end calls to redispatch statement email
* Add store actions and effect for redispatching statement email
* Integrate store into statement details page

[TOB-406] feat: Add statement search

* Extend back end API interface for search parameters
* Add component for search filters
* Add sorting buttons to statement table component
* Integrate statement search into search page

[TOB-74] feat: Add map to position search page

* Reorganize routing of search subpages
* Add directive to display leaflet popups
* Integrate leaflet map, markers and popups into position search page

[TOB-315] feat: Add search functionality to position search page

* Add back end calls for position search
* Add store effect for position search
* Integrate search component and store into position search page

[TOB-52] feat: Add GIS call to leaflet map

* Reorganize website configuration to separate config file
* Add back end calls to transform geographic positions
* Add store module for leaflet map
* Add store effecto to open GIS
* Integrate store module into map components
* Match leaflet styling to openk theme

[TOB-92] feat: Add upload functionality for consideration attachments

* Add component to display and upload consideration attachments
* Integrate component into details page

[TOB-354] feat: Add dockerfiles

[TOB-391] fix: Fix minor bugs

* Cancel rerouting of email when leaving mail page
* Prevent refetching of deleted emails
* Properly reset error messages in side menu
* Also perform contact search in new statement page when no mailid is provided
* Close drop downs on scroll
* Fix minor styling issues
Signed-off-by: Christopher Keim's avatarChristopher Keim <keim@develop-group.de>
parent c14146b0
......@@ -36,5 +36,50 @@ export interface IAPISearchOptions {
*/
sort?: string;
/**
* Key to filter the search by type. Only show results of chosen type.
*/
typeId?: number;
/**
* Key to filter the search by state. Show statements that are either finished or not.
*/
finished?: string;
/**
* Key to filter for statements that have been edited by the user.
*/
editedByMe?: string;
/**
* Key for filtering by creation date.
*/
creationDateFrom?: string;
/**
* Key for filtering by creation date.
*/
creationDateTo?: string;
/**
* Key for filtering by due date.
*/
dueDateFrom?: string;
/**
* Key for filtering by due date.
*/
dueDateTo?: string;
/**
* Key for filtering by receipt date.
*/
receiptDateFrom?: string;
/**
* Key for filtering by receipt date.
*/
receiptDateTo?: string;
}
......@@ -12,4 +12,5 @@
********************************************************************************/
export * from "./IAPIPaginationResponse";
export * from "./IAPIPositionSearchOptions";
export * from "./IAPISearchOptions";
/********************************************************************************
* 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 interface IAPIPositionSearchStatementModel {
id: number;
title: string;
position: string;
typeId: string;
dueDate: string;
finished: boolean;
}
......@@ -14,5 +14,6 @@
export * from "./IAPICommentModel";
export * from "./IAPIStatementModel";
export * from "./IAPIWorkflowData";
export * from "./IAPIPositionSearchStatementModel";
export * from "./statements-api.service";
......@@ -17,8 +17,10 @@ import {objectToHttpParams, urlJoin} from "../../../util";
import {SPA_BACKEND_ROUTE} from "../../external-routes";
import {IAPIDepartmentGroups} from "../settings";
import {IAPIPaginationResponse, IAPISearchOptions} from "../shared";
import {IAPIPositionSearchOptions} from "../shared/IAPIPositionSearchOptions";
import {IAPICommentModel} from "./IAPICommentModel";
import {IAPIDashboardStatementModel} from "./IAPIDashboardStatementModel";
import {IAPIPositionSearchStatementModel} from "./IAPIPositionSearchStatementModel";
import {IAPISectorsModel} from "./IAPISectorsModel";
import {IAPIPartialStatementModel, IAPIStatementModel} from "./IAPIStatementModel";
import {IAPIWorkflowData} from "./IAPIWorkflowData";
......@@ -54,6 +56,15 @@ export class StatementsApiService {
return this.httpClient.get<IAPIPaginationResponse<IAPIStatementModel>>(urlJoin(this.baseUrl, endPoint), {params});
}
/**
*
*/
public getStatementPositionsSearch(searchOptions: IAPIPositionSearchOptions) {
const endPoint = `statementpositionsearch`;
const params = objectToHttpParams({...searchOptions});
return this.httpClient.get<IAPIPositionSearchStatementModel[]>(urlJoin(this.baseUrl, endPoint), {params});
}
/**
* Fetches all basic details for a specific statement.
* @param id Id of the statement to fetch.
......@@ -119,6 +130,15 @@ export class StatementsApiService {
return this.httpClient.get<number[]>(urlJoin(this.baseUrl, endPoint));
}
/**
* Fetches the IDs of all children of a specific statement.
*/
public getChildrenIds(statementId: number) {
const endPoint = `/process/statements/${statementId}/workflow/children`;
return this.httpClient.get<number[]>(urlJoin(this.baseUrl, endPoint));
}
/**
* Updates the IDs of all parents to specific statement in the back end data base.
*/
......
/********************************************************************************
* 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 {InjectionToken} from "@angular/core";
import {gis, leaflet, nominatim, routes} from "../../../../app.config.json";
export interface IAppConfiguration {
gis: {
urlTemplate: string;
projectionFrom: string;
projectionTo: string;
};
leaflet: {
urlTemplate: string;
attribution?: string;
lat: number;
lng: number;
zoom: number;
};
nominatim: {
url: string;
searchQueryPrefix?: string;
};
routes: {
spaBackend: string;
portal: string;
contactDataBase: string;
};
}
export const APP_CONFIGURATION = new InjectionToken<IAppConfiguration>("App configuration object", {
providedIn: "root",
factory: () => ({gis, leaflet, nominatim, routes})
});
......@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
export * from "./linked-statements.module";
export * from "./app-configuration.token";
......@@ -12,12 +12,13 @@
********************************************************************************/
import {InjectionToken} from "@angular/core";
import {environment} from "../../../environments/environment";
import * as config from "../../../../app.config.json";
import {IAppConfiguration} from "../configuration";
/**
* Injection token for the external route to the contact base module.
*/
export const CONTACT_DATA_BASE_ROUTE = new InjectionToken<string>("External route to the contact data base module", {
providedIn: "root",
factory: () => environment.routes.contactDataBase
factory: () => (config as IAppConfiguration).routes.contactDataBase
});
/********************************************************************************
* 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 {InjectionToken} from "@angular/core";
import * as config from "../../../../app.config.json";
import {IAppConfiguration} from "../configuration";
/**
* Injection token for the external route to the backend server.
*/
export const NOMINATIM_ROUTE = new InjectionToken<string>("External route to nominatim geocoding service", {
providedIn: "root",
factory: () => (config as IAppConfiguration).nominatim.url
});
......@@ -12,12 +12,13 @@
********************************************************************************/
import {InjectionToken} from "@angular/core";
import {environment} from "../../../environments/environment";
import * as config from "../../../../app.config.json";
import {IAppConfiguration} from "../configuration";
/**
* Injection token for the external route to the main portal.
*/
export const PORTAL_ROUTE = new InjectionToken<string>("External route to entry portal", {
providedIn: "root",
factory: () => environment.routes.portal
factory: () => (config as IAppConfiguration).routes.portal
});
......@@ -12,12 +12,13 @@
********************************************************************************/
import {InjectionToken} from "@angular/core";
import {environment} from "../../../environments/environment";
import * as config from "../../../../app.config.json";
import {IAppConfiguration} from "../configuration";
/**
* Injection token for the external route to the backend server.
*/
export const SPA_BACKEND_ROUTE = new InjectionToken<string>("Base url for back end", {
providedIn: "root",
factory: () => environment.routes.spaBackend
factory: () => (config as IAppConfiguration).routes.spaBackend
});
......@@ -13,6 +13,7 @@
export * from "./api";
export * from "./auth";
export * from "./configuration";
export * from "./dom";
export * from "./download";
export * from "./external-routes";
......
/********************************************************************************
* 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 "./statement-details-attachments.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
[(appCollapsed)]="appCollapsed"
[appTitle]="'details.attachments.title' | translate">
<div *ngIf="(statementAttachments$| async)?.length > 0 || mailTextAttachment != null"
class="attachments">
<div *ngIf="mailTextAttachment"
class="attachments--email">
<app-attachment-display-list
(appDownload)="downloadAttachment($event)"
[appAttachments]="[mailTextAttachment]"
[appTagList]="[]"
[appTitle]="'details.attachments.email' | translate">
</app-attachment-display-list>
</div>
<div
*ngIf="(allAttachments$ | async)?.length > 0 || (statementAttachments$ | async)?.length > 0">
<div *ngIf="availableTags?.length > 0"
class="attachments--documents--filter-btns">
<button (click)="deselectAllTags()"
class="openk-button openk-button-rounded"
type="button">
<mat-icon>filter_list</mat-icon>
</button>
<ng-container *ngFor="let tag of availableTags">
<button (click)="toggleTag(tag)"
[class.openk-info]="tag?.isSelected"
class="openk-button openk-chip">
{{tag?.label}}
</button>
</ng-container>
</div>
<div class="attachments--documents--lists">
<app-attachment-display-list
(appDownload)="downloadAttachment($event)"
[appAttachments]="allAttachments"
[appTagList]="tags$ | async"
[appTitle]="'details.attachments.emailDocuments' | translate"
class="attachments--document--lists---half-size attachments--document--lists---space-between">
</app-attachment-display-list>
<app-attachment-display-list
(appDownload)="downloadAttachment($event)"
[appAttachments]="statementAttachments"
[appTagList]="tags$ | async"
[appTitle]="'Manuell hinzugefügte Anhänge'"
class="attachments--document--lists---half-size">
</app-attachment-display-list>
</div>
</div>
</div>
<div *ngIf="!((statementAttachments$| async)?.length > 0 || mailTextAttachment != null)"
class="placeholder">
{{"details.attachments.placeholder" | translate}}
</div>
</app-collapsible>
/********************************************************************************
* 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 {
display: block;
width: 100%;
&:empty {
display: none;
}
}
.placeholder {
padding: 1em;
font-style: italic;
}
.attachments {
padding: 1em;
}
.attachments--email {
display: inline-block;
margin-bottom: 0.5em;
}
.attachments--documents--filter-btns {
margin-bottom: 0.1em;
}
.attachments--documents--lists {
display: flex;
flex-direction: row;
}
.attachments--document--lists---half-size {
flex: 1;
}
.attachments--document--lists---space-between {
margin-right: 0.5em;
}
.openk-button-rounded {
font-size: 1.15em;
border: 0;
color: get-color($openk-info-palette);
margin-right: 0.25em;
&:not(.openk-info) {
background-color: transparent;
}
&:not(.openk-info):active,
&:not(.openk-info):focus,
&:not(.openk-info):hover {
background-color: $openk-background-highlight;
}
}
.openk-chip {
margin-right: 0.25em;
}
/********************************************************************************
* 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 {Store} from "@ngrx/store";
import {provideMockStore} from "@ngrx/store/testing";
import {I18nModule, IAPIAttachmentModel} from "../../../../core";
import {IAttachmentControlValue, queryParamsIdSelector, startAttachmentDownloadAction} from "../../../../store";
import {StatementDetailsModule} from "../../statement-details.module";
import {StatementDetailsAttachmentsComponent} from "./statement-details-attachments.component";
describe("StatementDetailsAttachmentsComponent", () => {
let component: StatementDetailsAttachmentsComponent;
let fixture: ComponentFixture<StatementDetailsAttachmentsComponent>;
let store: Store;
const attachments: IAttachmentControlValue[] = [
{name: "attachment1", tagIds: ["tag1", "email"]},
{name: "attachment2", tagIds: ["email", "tag3"]},
{name: "attachment3", tagIds: ["tag1", "tag2"]},
{name: "attachment4", tagIds: ["tag1", "tag2", "email"]}
];
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
StatementDetailsModule,
I18nModule
],
providers: [
provideMockStore({
initialState: {},
selectors: [
{
selector: queryParamsIdSelector,
value: 19
}
]
})
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(StatementDetailsAttachmentsComponent);
component = fixture.componentInstance;
store = fixture.componentRef.injector.get(Store);
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
it("should return only the attachments containing at least one of the selected tags", () => {
component.availableTags = [
{label: "tag1", id: "tag1", isSelected: true},
{label: "tag2", id: "tag2", isSelected: true},
{label: "tag3", id: "tag3", isSelected: false}
];
const result = component.filterBySelectedTags(attachments);
expect(result).toEqual([
{name: "attachment1", tagIds: ["tag1", "email"]},
{name: "attachment3", tagIds: ["tag1", "tag2"]},
{name: "attachment4", tagIds: ["tag1", "tag2", "email"]}
]);
});
it("should select all available tags", () => {
component.availableTags = [
{label: "tag1", id: "tag1", isSelected: true},
{label: "tag2", id: "tag2", isSelected: false},
{label: "tag3", id: "tag3", isSelected: false}
];
component.deselectAllTags();
expect(component.availableTags).toEqual([
{label: "tag1", id: "tag1", isSelected: false},
{label: "tag2", id: "tag2", isSelected: false},
{label: "tag3", id: "tag3", isSelected: false}
]);
});
it("should toggle the tag state or set to given value", () => {
const tag = {label: "tag1", id: "tag1", isSelected: true};
component.toggleTag(tag);
expect(tag.isSelected).toBeFalse();
component.toggleTag(tag);
expect(tag.isSelected).toBeTrue();
component.toggleTag(tag, true);
expect(tag.isSelected).toBeTrue();
});
it("should dispatch startAttachmentDownloadAction with correct attachmentId", async () => {
const dispatchSpy = spyOn(store, "dispatch");
await component.downloadAttachment(15);
expect(dispatchSpy).toHaveBeenCalledWith(startAttachmentDownloadAction({statementId: 19, attachmentId: 15}));
});
it("should only return the email attachments", () => {
const emailAttachments = component.filterForEmailAttachments(attachments as IAPIAttachmentModel[]);
expect(emailAttachments).toEqual([
{name: "attachment1", tagIds: ["tag1", "email"]},
{name: "attachment2", tagIds: ["email", "tag3"]},
{name: "attachment4", tagIds: ["tag1", "tag2", "email"]}
] as IAPIAttachmentModel[]);
});
});
/********************************************************************************
* 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