[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 <keim@develop-group.de>
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..2cf5c82
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,19 @@
+# ******************************************************************************
+# 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 v. 2.0 which is available at
+# http://www.eclipse.org/legal/epl-2.0.
+#
+# SPDX-License-Identifier: EPL-2.0
+# ******************************************************************************
+
+FROM nginx:1.18-alpine
+
+COPY dist/statement-public-affairs /html-root
+
+COPY docker/buildDocker/default.conf /etc/nginx/conf.d/default.conf
+
diff --git a/README.md b/README.md
index 00f4205..82712e5 100644
--- a/README.md
+++ b/README.md
@@ -17,25 +17,42 @@
## Configuration
The whole application can be configured via certain properties in
-the `./package.json`. The following options are available:
+file `./app.config.json`. Changes to these properties take only
+effect after (re-)building the application.
-* `routes.spaFrontend`: Route on which the website is served
-* `routes.spaBackend`: Route on which the website's backend is served
-* `routes.portal`: Route on which the main portal is served
-* `routes.contactDataBase`: Route on which the contact data base module is served
+* `routes.spaBackend`: Route on which the website's backend is
+served
+* `routes.portal`: Route on which the main portal is served
+* `routes.contactDataBase`: Route on which the contact data base
+module is served
+* `leaflet.urlTemplate`: URL template to the map tile server
+required by [Leaflet](https://leafletjs.com)
+* `leaflet.attribution`: Attribution which is added to all
+Leaflet maps
+* `leaflet.lat`/`leaflet.lng`/`leaflet.zoom`: Default coordinates
+and zoom level to which all leaflet maps are initially configured
+* `gis.urlTemplate`: URL template to the geographic information
+system (GIS)
+* `gis.projectionFrom`: Coordinate projection used by the
+configured Leaflet tile server
+* `gis.projectionTo`: Coordinate projection used in the
+configured GIS
+* `nominatim.url`: URL to a [Nominatim](https://nominatim.org)
+geocoding service
+* `nominatim.searchQueryPrefix`: Prefix which is added automatically
+to every [Nominatim](https://nominatim.org) search query
-Changes to these properties take only effect after rebuilding the
-application.
+The following key words are replaced in the given GIS URL template
+for each map view:
+* `{centerX}`, `{centerY}`: Center coordinates
+* `{northEastX}`, `{northEastY}`: North east boundary coordinates
+* `{northWestX}`, `{northWestY}`: North west boundary coordinates
+* `{southEastX}`, `{southEastY}`: South east boundary coordinates
+* `{southWestX}`, `{southWestY}`: South west boundary coordinates
+* `{user}`: User name
-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.
-`© <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
+All coordinates are transformed via a HTTP call to the back end.
+For all available projections, please visit [Proj4j](https://trac.osgeo.org/proj4j/).
## Build
diff --git a/app.config.json b/app.config.json
new file mode 100644
index 0000000..e1ecf44
--- /dev/null
+++ b/app.config.json
@@ -0,0 +1,23 @@
+{
+ "gis": {
+ "urlTemplate": "http://localhost/Ext2GWS/GinPrjExt2GService.asmx/Coordinates?pLLX={southWestX}&pLLY={southWestY}&pURX={northEastX}&pURY={northEastY}&pReportName=TestName&pOSUser={user}&pLSNO=900",
+ "projectionFrom": "EPSG:4326",
+ "projectionTo": "EPSG:25832"
+ },
+ "leaflet": {
+ "urlTemplate": "http://localhost:4201/{s}/{z}/{x}/{y}.png",
+ "attribution": "© <a href=\"http://localhost:4201/copyright\">Tile Server</a> contributors",
+ "lat": 49.87282103349044,
+ "lng": 8.651196956634523,
+ "zoom": 12
+ },
+ "nominatim": {
+ "url": "http://localhost:4202",
+ "searchQueryPrefix": "Germany"
+ },
+ "routes": {
+ "spaBackend": "/statementpaBE",
+ "portal": "/portalFE",
+ "contactDataBase": "/contactdatabase"
+ }
+}
diff --git a/docker/buildDocker/README.md b/docker/buildDocker/README.md
new file mode 100644
index 0000000..592b72e
--- /dev/null
+++ b/docker/buildDocker/README.md
@@ -0,0 +1,24 @@
+*******************************************************************************
+ Copyright (c) 2019 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 v. 2.0 which is available at
+ http://www.eclipse.org/legal/epl-2.0.
+
+ SPDX-License-Identifier: EPL-2.0
+*******************************************************************************
+
+# Nginx based docker image that contains the statement module frontend
+
+This docker configuration creates a nginx based reverse-proxy.
+It provides the statement module frontend at port 80.
+
+Please go to the root folder to build this docker image.
+
+## Configuration
+
+The nginx configuration can be found in the default.conf file.
+
diff --git a/docker/buildDocker/default.conf b/docker/buildDocker/default.conf
new file mode 100644
index 0000000..3ef2fa6
--- /dev/null
+++ b/docker/buildDocker/default.conf
@@ -0,0 +1,19 @@
+server {
+ listen 80 default_server;
+ listen [::]:80 default_server;
+
+ root /html-root;
+
+ index index.html;
+
+ server_name you.server.com;
+
+ location / {
+ try_files $uri $uri/ @rewrites;
+ }
+
+ location @rewrites {
+ rewrite ^(.+)$ /index.html last;
+ }
+
+}
diff --git a/docker/buildenv/Dockerfile b/docker/buildenv/Dockerfile
new file mode 100644
index 0000000..affd89b
--- /dev/null
+++ b/docker/buildenv/Dockerfile
@@ -0,0 +1,22 @@
+# ******************************************************************************
+# 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 v. 2.0 which is available at
+# http://www.eclipse.org/legal/epl-2.0.
+#
+# SPDX-License-Identifier: EPL-2.0
+# ******************************************************************************
+
+FROM node:12-alpine
+
+RUN mkdir -p /buildenv/node_modules
+RUN mkdir -p /buildenv/root
+RUN npm install -g --silent @angular/cli
+RUN apk add openjdk8
+RUN ln -s /usr/lib/jvm/java-1.8-openjdk/bin/jar /usr/local/bin
+
+
diff --git a/docker/buildenv/README.md b/docker/buildenv/README.md
new file mode 100644
index 0000000..b0f7775
--- /dev/null
+++ b/docker/buildenv/README.md
@@ -0,0 +1,27 @@
+*******************************************************************************
+ Copyright (c) 2019 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 v. 2.0 which is available at
+ http://www.eclipse.org/legal/epl-2.0.
+
+ SPDX-License-Identifier: EPL-2.0
+*******************************************************************************
+
+# Build environment
+
+The 'build' script creates reproducable artifact builds.
+It uses an node based docker image.
+
+## Usage
+
+To build the artifact, run the compile script from the project base folder (where the package.json is located).
+
+The script deletes the target folder and then runs node build commands.
+
+After a successful build, the artifact is located in the 'dist' folder.
+
+
diff --git a/docker/buildenv/build b/docker/buildenv/build
new file mode 100755
index 0000000..e8ede8f
--- /dev/null
+++ b/docker/buildenv/build
@@ -0,0 +1,4 @@
+docker volume create --name node-modules
+
+docker run -it -v "$PWD":/buildenv/root -v node-modules:/buildenv/root/node_modules -w /buildenv/root openk-node:12-alpine sh -c "npm install --silent && npm run build && npm run build:archive"
+
diff --git a/package-lock.json b/package-lock.json
index d972f7e..c1b2c07 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "openkonsequenz-statement-public-affairs",
- "version": "0.8.0",
+ "version": "0.9.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index d4d2add..2d0c3be 100644
--- a/package.json
+++ b/package.json
@@ -1,26 +1,12 @@
{
"name": "openkonsequenz-statement-public-affairs",
- "version": "0.8.0",
+ "version": "0.9.0",
"description": "Statement Public Affairs",
"license": "Eclipse Public License - v 2.0",
"repository": {
"type": "git",
"url": "https://git.eclipse.org/r/plugins/gitiles/openk-usermodules/org.eclipse.openk-usermodules.statementPublicAffairs.frontend"
},
- "routes": {
- "spaFrontend": "/statementpaFE",
- "spaBackend": "/statementpaBE",
- "portal": "/portalFE",
- "contactDataBase": "/contactdatabase"
- },
- "leaflet": {
- "urlTemplate": "https://localhost:4200/{s}/{z}/{x}/{y}.png",
- "attribution": "©",
- "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": {
"-- Build ----------------": "",
"build": "ng build --prod --base-href /statementpaFE/",
diff --git a/src/app/app-routing.module.spec.ts b/src/app/app-routing.module.spec.ts
index c9c28c3..aab37b5 100644
--- a/src/app/app-routing.module.spec.ts
+++ b/src/app/app-routing.module.spec.ts
@@ -85,7 +85,20 @@
it("should navigate to /search", async () => {
const isRoutingSuccessful = await callInZone(() => router.navigate(["search"]));
expect(isRoutingSuccessful).toBeTruthy();
- expect(location.path()).toBe("/search");
+ await Promise.resolve();
+ expect(location.path()).toBe("/search/list");
+ });
+
+ it("should navigate to /search/list", async () => {
+ const isRoutingSuccessful = await callInZone(() => router.navigate(["search", "list"]));
+ expect(isRoutingSuccessful).toBeTruthy();
+ expect(location.path()).toBe("/search/list");
+ });
+
+ it("should navigate to /search/map", async () => {
+ const isRoutingSuccessful = await callInZone(() => router.navigate(["search", "map"]));
+ expect(isRoutingSuccessful).toBeTruthy();
+ expect(location.path()).toBe("/search/map");
});
it("should navigate to /settings", async () => {
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index d88839b..8ac6768 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -20,8 +20,6 @@
import {AppComponent} from "./app.component";
import {CoreModule} from "./core";
import {AppNavigationFrameModule} from "./features/navigation";
-import {LeafletModule} from "./shared/layout/leaflet";
-import {SideMenuRegistrationService} from "./shared/layout/side-menu/services";
import {AppStoreModule} from "./store";
@NgModule({
@@ -38,8 +36,6 @@
AppStoreModule,
AppNavigationFrameModule,
- LeafletModule.for(SideMenuRegistrationService),
-
// 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. !
...environment.imports
diff --git a/src/app/core/api/attachments/attachments-api.service.ts b/src/app/core/api/attachments/attachments-api.service.ts
index e0c2709..e6d4835 100644
--- a/src/app/core/api/attachments/attachments-api.service.ts
+++ b/src/app/core/api/attachments/attachments-api.service.ts
@@ -48,6 +48,16 @@
}
/**
+ * Uploads a new file for the statement with the given id and automatically sets the consideration tag on the attachment.
+ */
+ public postConsideration(statementId: number, file: File) {
+ const endPoint = `/statements/${statementId}/consideration`;
+ const formData = new FormData();
+ formData.append("attachment", file, file.name);
+ return this.httpClient.post<IAPIAttachmentModel>(urlJoin(this.baseUrl, endPoint), formData);
+ }
+
+ /**
* Uploads a new file to the back end linked to a specific statement.
*/
public deleteAttachment(statementId: number, taskId: string, attachmentId: number) {
diff --git a/src/app/features/search/components/search/index.ts b/src/app/core/api/geo/IAPIGeographicPositions.ts
similarity index 82%
copy from src/app/features/search/components/search/index.ts
copy to src/app/core/api/geo/IAPIGeographicPositions.ts
index 5eb9c7f..687b05a 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/core/api/geo/IAPIGeographicPositions.ts
@@ -11,4 +11,9 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export interface IAPIGeographicPositions {
+ [key: string]: {
+ x: number;
+ y: number;
+ };
+}
diff --git a/src/app/core/api/geo/geo-api.service.ts b/src/app/core/api/geo/geo-api.service.ts
new file mode 100644
index 0000000..700163b
--- /dev/null
+++ b/src/app/core/api/geo/geo-api.service.ts
@@ -0,0 +1,37 @@
+/********************************************************************************
+ * 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 {objectToHttpParams, urlJoin} from "../../../util/http";
+import {SPA_BACKEND_ROUTE} from "../../external-routes";
+import {IAPIGeographicPositions} from "./IAPIGeographicPositions";
+
+@Injectable({providedIn: "root"})
+export class GeoApiService {
+
+ public constructor(
+ protected readonly httpClient: HttpClient,
+ @Inject(SPA_BACKEND_ROUTE) protected readonly baseUrl: string
+ ) {
+
+ }
+
+ public transform(geographicPositions: IAPIGeographicPositions, from: string, to: string): Observable<IAPIGeographicPositions> {
+ const params = objectToHttpParams({from, to});
+ const endPoint = `/geo-coordinate-transform`;
+ return this.httpClient.post<IAPIGeographicPositions>(urlJoin(this.baseUrl, endPoint), geographicPositions, {params});
+ }
+
+}
diff --git a/src/app/features/search/components/search/index.ts b/src/app/core/api/geo/index.ts
similarity index 87%
copy from src/app/features/search/components/search/index.ts
copy to src/app/core/api/geo/index.ts
index 5eb9c7f..9f1b524 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/core/api/geo/index.ts
@@ -11,4 +11,5 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./IAPIGeographicPositions";
+export * from "./geo-api.service";
diff --git a/src/app/core/api/index.ts b/src/app/core/api/index.ts
index ccbbf5f..491f529 100644
--- a/src/app/core/api/index.ts
+++ b/src/app/core/api/index.ts
@@ -14,6 +14,7 @@
export * from "./attachments";
export * from "./contacts";
export * from "./core";
+export * from "./geo";
export * from "./mail";
export * from "./process";
export * from "./settings";
diff --git a/src/app/core/api/mail/mail-api.service.ts b/src/app/core/api/mail/mail-api.service.ts
index 90962d3..a4654cd 100644
--- a/src/app/core/api/mail/mail-api.service.ts
+++ b/src/app/core/api/mail/mail-api.service.ts
@@ -70,12 +70,4 @@
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);
- }
-
}
diff --git a/src/app/core/api/process/process-api.service.ts b/src/app/core/api/process/process-api.service.ts
index 78e1134..b57dced 100644
--- a/src/app/core/api/process/process-api.service.ts
+++ b/src/app/core/api/process/process-api.service.ts
@@ -80,4 +80,12 @@
return this.httpClient.get(urlJoin(this.baseUrl, endPoint), {responseType: "text"});
}
+ /**
+ * Re-sends the outgoing email for a statement.
+ */
+ public dispatchStatement(statementId: number, taskId: string) {
+ const endPoint = `/process/statements/${statementId}/task/${taskId}/mailandcomplete`;
+ return this.httpClient.post(urlJoin(this.baseUrl, endPoint), null);
+ }
+
}
diff --git a/src/app/core/api/shared/IAPIPositionSearchOptions.ts b/src/app/core/api/shared/IAPIPositionSearchOptions.ts
new file mode 100644
index 0000000..51d1484
--- /dev/null
+++ b/src/app/core/api/shared/IAPIPositionSearchOptions.ts
@@ -0,0 +1,35 @@
+/********************************************************************************
+ * 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
+ ********************************************************************************/
+
+/**
+ * Interface which represents the options for a paginated search in the back end data base.
+ */
+export interface IAPIPositionSearchOptions {
+
+ /**
+ * Key to filter the search by type. Only show results of chosen type.
+ */
+ typeId?: number;
+
+ /**
+ * Key for filtering by due date.
+ */
+ dueDateFrom?: string;
+
+ /**
+ * Key for filtering by due date.
+ */
+ dueDateTo?: string;
+
+}
+
diff --git a/src/app/core/api/shared/IAPISearchOptions.ts b/src/app/core/api/shared/IAPISearchOptions.ts
index 3489b18..93d07f8 100644
--- a/src/app/core/api/shared/IAPISearchOptions.ts
+++ b/src/app/core/api/shared/IAPISearchOptions.ts
@@ -36,5 +36,50 @@
*/
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;
+
}
diff --git a/src/app/core/api/shared/index.ts b/src/app/core/api/shared/index.ts
index 12dd624..e23cace 100644
--- a/src/app/core/api/shared/index.ts
+++ b/src/app/core/api/shared/index.ts
@@ -12,4 +12,5 @@
********************************************************************************/
export * from "./IAPIPaginationResponse";
+export * from "./IAPIPositionSearchOptions";
export * from "./IAPISearchOptions";
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss b/src/app/core/api/statements/IAPIPositionSearchStatementModel.ts
similarity index 76%
copy from src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
copy to src/app/core/api/statements/IAPIPositionSearchStatementModel.ts
index 4d2360d..8de60b7 100644
--- a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
+++ b/src/app/core/api/statements/IAPIPositionSearchStatementModel.ts
@@ -11,17 +11,13 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "../../../../styles/openk.styles";
+export interface IAPIPositionSearchStatementModel {
-:host {
- width: 100%;
-}
+ id: number;
+ title: string;
+ position: string;
+ typeId: string;
+ dueDate: string;
+ finished: boolean;
-.statements {
- margin-bottom: 1em;
- display: grid;
-}
-
-.statements--titlebar {
- margin-bottom: 0.5em;
}
diff --git a/src/app/core/api/statements/index.ts b/src/app/core/api/statements/index.ts
index 491e3ec..91527b0 100644
--- a/src/app/core/api/statements/index.ts
+++ b/src/app/core/api/statements/index.ts
@@ -14,5 +14,6 @@
export * from "./IAPICommentModel";
export * from "./IAPIStatementModel";
export * from "./IAPIWorkflowData";
+export * from "./IAPIPositionSearchStatementModel";
export * from "./statements-api.service";
diff --git a/src/app/core/api/statements/statements-api.service.ts b/src/app/core/api/statements/statements-api.service.ts
index 05cb28d..d611a35 100644
--- a/src/app/core/api/statements/statements-api.service.ts
+++ b/src/app/core/api/statements/statements-api.service.ts
@@ -17,8 +17,10 @@
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";
@@ -55,6 +57,15 @@
}
/**
+ *
+ */
+ 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 @@
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.
*/
diff --git a/src/app/core/configuration/app-configuration.token.ts b/src/app/core/configuration/app-configuration.token.ts
new file mode 100644
index 0000000..5b5eeba
--- /dev/null
+++ b/src/app/core/configuration/app-configuration.token.ts
@@ -0,0 +1,44 @@
+/********************************************************************************
+ * 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})
+});
diff --git a/src/app/features/search/components/search/index.ts b/src/app/core/configuration/index.ts
similarity index 92%
copy from src/app/features/search/components/search/index.ts
copy to src/app/core/configuration/index.ts
index 5eb9c7f..abeb879 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/core/configuration/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./app-configuration.token";
diff --git a/src/app/core/external-routes/contact-data-base-route.token.ts b/src/app/core/external-routes/contact-data-base-route.token.ts
index d0d9d47..33e992f 100644
--- a/src/app/core/external-routes/contact-data-base-route.token.ts
+++ b/src/app/core/external-routes/contact-data-base-route.token.ts
@@ -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
});
diff --git a/src/app/core/external-routes/nominatim-route.token.ts b/src/app/core/external-routes/nominatim-route.token.ts
new file mode 100644
index 0000000..06202c8
--- /dev/null
+++ b/src/app/core/external-routes/nominatim-route.token.ts
@@ -0,0 +1,24 @@
+/********************************************************************************
+ * 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
+});
diff --git a/src/app/core/external-routes/portal-route.token.ts b/src/app/core/external-routes/portal-route.token.ts
index d79a806..d6379e3 100644
--- a/src/app/core/external-routes/portal-route.token.ts
+++ b/src/app/core/external-routes/portal-route.token.ts
@@ -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
});
diff --git a/src/app/core/external-routes/spa-backend-route.token.ts b/src/app/core/external-routes/spa-backend-route.token.ts
index 578c2aa..fc14abb 100644
--- a/src/app/core/external-routes/spa-backend-route.token.ts
+++ b/src/app/core/external-routes/spa-backend-route.token.ts
@@ -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
});
diff --git a/src/app/core/index.ts b/src/app/core/index.ts
index c5164a0..ecefda5 100644
--- a/src/app/core/index.ts
+++ b/src/app/core/index.ts
@@ -13,6 +13,7 @@
export * from "./api";
export * from "./auth";
+export * from "./configuration";
export * from "./dom";
export * from "./download";
export * from "./external-routes";
diff --git a/src/app/features/search/components/search/index.ts b/src/app/features/details/components/attachments/index.ts
similarity index 90%
copy from src/app/features/search/components/search/index.ts
copy to src/app/features/details/components/attachments/index.ts
index 5eb9c7f..f44ee5e 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/features/details/components/attachments/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./statement-details-attachments.component";
diff --git a/src/app/features/details/components/attachments/statement-details-attachments.component.html b/src/app/features/details/components/attachments/statement-details-attachments.component.html
new file mode 100644
index 0000000..2b66156
--- /dev/null
+++ b/src/app/features/details/components/attachments/statement-details-attachments.component.html
@@ -0,0 +1,77 @@
+<!-------------------------------------------------------------------------------
+ * 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>
diff --git a/src/app/features/details/components/attachments/statement-details-attachments.component.scss b/src/app/features/details/components/attachments/statement-details-attachments.component.scss
new file mode 100644
index 0000000..d100724
--- /dev/null
+++ b/src/app/features/details/components/attachments/statement-details-attachments.component.scss
@@ -0,0 +1,75 @@
+/********************************************************************************
+ * 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;
+}
diff --git a/src/app/features/details/components/attachments/statement-details-attachments.component.spec.ts b/src/app/features/details/components/attachments/statement-details-attachments.component.spec.ts
new file mode 100644
index 0000000..78958ea
--- /dev/null
+++ b/src/app/features/details/components/attachments/statement-details-attachments.component.spec.ts
@@ -0,0 +1,118 @@
+/********************************************************************************
+ * 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[]);
+ });
+});
diff --git a/src/app/features/details/components/attachments/statement-details-attachments.component.ts b/src/app/features/details/components/attachments/statement-details-attachments.component.ts
new file mode 100644
index 0000000..026f1d9
--- /dev/null
+++ b/src/app/features/details/components/attachments/statement-details-attachments.component.ts
@@ -0,0 +1,139 @@
+/********************************************************************************
+ * 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, Input, OnDestroy, OnInit} from "@angular/core";
+import {select, Store} from "@ngrx/store";
+import {combineLatest, Subject} from "rxjs";
+import {filter, take, takeUntil} from "rxjs/operators";
+import {AUTO_SELECTED_TAGS, IAPIAttachmentModel} from "../../../../core/api/attachments";
+import {startAttachmentDownloadAction} from "../../../../store/attachments/actions";
+import {IAttachmentControlValue} from "../../../../store/attachments/model";
+import {
+ getAllStatementAttachments,
+ getAttachmentControlValueSelector,
+ getFilteredAttachmentTagsSelector
+} from "../../../../store/attachments/selectors";
+import {queryParamsIdSelector} from "../../../../store/root/selectors";
+import {filterDistinctValues} from "../../../../util/store";
+import {getMailAttachment} from "../../../forms/attachments/util/mail-attachments.util";
+
+/**
+ * This component displays the list of attachments saved with the statement.
+ * It's split up into 3 different categories:
+ * mail-text, attachments manually added to the statement and the attachments copied from the initial email.
+ *
+ * The attachment lists can be filtered to only show attachments with specific tags.
+ * The available tags for filtering are all the used tags on the statements attachments.
+ */
+
+@Component({
+ selector: "app-statement-details-attachments",
+ templateUrl: "./statement-details-attachments.component.html",
+ styleUrls: ["./statement-details-attachments.component.scss"]
+})
+export class StatementDetailsAttachmentsComponent implements OnInit, OnDestroy {
+
+ @Input()
+ public appCollapsed = false;
+
+ public allAttachments$ = this.store.pipe(select(getAllStatementAttachments));
+
+ public allAttachments: IAttachmentControlValue[] = [];
+
+ public statementAttachments$ = this.store.pipe(select(getAttachmentControlValueSelector, {forbiddenTagIds: AUTO_SELECTED_TAGS}));
+
+ public statementAttachments: IAttachmentControlValue[] = [];
+
+ public availableTags: { label: string, id: string, isSelected: boolean }[] = [];
+
+ public tags$ = this.store.pipe(select(getFilteredAttachmentTagsSelector, {without: AUTO_SELECTED_TAGS}));
+
+ public statementId$ = this.store.pipe(select(queryParamsIdSelector));
+
+ public mailTextAttachment: IAPIAttachmentModel;
+
+ private destroy$ = new Subject();
+
+ public constructor(public store: Store) {
+ }
+
+ public ngOnInit() {
+ combineLatest([this.allAttachments$, this.tags$]).pipe(
+ takeUntil(this.destroy$)
+ ).subscribe(([attachments, tags]) => {
+ const allTags = filterDistinctValues(attachments.map((attachment) => attachment.tagIds).reduce((_, x) => x.concat(_), []));
+ this.availableTags = tags.filter((_) => allTags.find((a) => a === _.id))
+ .map((_) => ({..._, isSelected: false}));
+ });
+ this.statementAttachments$.pipe(
+ takeUntil(this.destroy$)
+ ).subscribe((attachments) => {
+ this.statementAttachments = this.filterBySelectedTags(attachments);
+ });
+ this.allAttachments$.pipe(
+ filter((attachments) => attachments != null)
+ ).subscribe(async (attachments) => {
+ const mailAttachments = this.filterForEmailAttachments(attachments);
+ this.mailTextAttachment = getMailAttachment(attachments);
+ this.allAttachments = this.filterBySelectedTags(mailAttachments);
+ });
+ }
+
+ public ngOnDestroy() {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ public async toggleTag(tag, state?: boolean) {
+ tag.isSelected = state != null ? state : !tag.isSelected;
+ const sAttachments = await this.statementAttachments$.pipe(take(1)).toPromise();
+ this.statementAttachments = this.filterBySelectedTags(sAttachments);
+ const aAttachments = await this.allAttachments$.pipe(take(1)).toPromise();
+ const mailAttachments = this.filterForEmailAttachments(aAttachments);
+ this.allAttachments = this.filterBySelectedTags(mailAttachments);
+ }
+
+ public filterForEmailAttachments(attachments: IAPIAttachmentModel[]) {
+ return attachments.filter((attachment) => {
+ return attachment.tagIds.find((_) => _ === "email") != null &&
+ attachment.tagIds.find((_) => _ === "email-text") == null;
+ });
+ }
+
+ public deselectAllTags() {
+ for (const tag of this.availableTags) {
+ this.toggleTag(tag, false);
+ }
+ }
+
+ public async downloadAttachment(attachmentId: number) {
+ const statementId = await this.statementId$.pipe(take(1)).toPromise();
+ this.store.dispatch(startAttachmentDownloadAction({statementId, attachmentId}));
+ }
+
+ public filterBySelectedTags(attachments: IAttachmentControlValue[]) {
+ return attachments.filter((attachment) => {
+ const selectedTags = this.availableTags?.filter((_) => _.isSelected);
+ if (selectedTags?.length === 0) {
+ return true;
+ }
+ for (const tagId of attachment?.tagIds) {
+ const tagIsSelected = this.availableTags.find((_) => _.id === tagId)?.isSelected;
+ if (tagIsSelected) {
+ return true;
+ }
+ }
+ return false;
+ });
+ }
+}
diff --git a/src/app/features/search/components/search/index.ts b/src/app/features/details/components/considerations/index.ts
similarity index 89%
copy from src/app/features/search/components/search/index.ts
copy to src/app/features/details/components/considerations/index.ts
index 5eb9c7f..8dc78de 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/features/details/components/considerations/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./statement-details-considerations.component";
diff --git a/src/app/features/details/components/considerations/statement-details-considerations.component.html b/src/app/features/details/components/considerations/statement-details-considerations.component.html
new file mode 100644
index 0000000..5ecd60c
--- /dev/null
+++ b/src/app/features/details/components/considerations/statement-details-considerations.component.html
@@ -0,0 +1,45 @@
+<!-------------------------------------------------------------------------------
+ * 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="appFinished"
+ [(appCollapsed)]="appCollapsed"
+ [appTitle]="'details.considerations.title' | translate">
+
+ <div *ngIf="((considerations$ | async)?.length > 0) || (isOfficialInCharge$ | async)"
+ class="attachments--files">
+ <app-attachment-display-list
+ (appDownload)="downloadAttachment($event)"
+ *ngIf="(considerations$ | async)?.length > 0"
+ [appAttachments]="considerations$ | async"
+ [appTitle]="'details.considerations.attachments' | translate"
+ class="attachments--document--lists---half-size">
+ </app-attachment-display-list>
+
+ <app-attachment-file-drop-form
+ *ngIf="isOfficialInCharge$ | async"
+ [appFormArrayName]="'considerations'"
+ [appFormGroup]="appFormGroup"
+ [appTitle]="'attachments.add' | translate"
+ class="attachments--container">
+ <button (click)="submit()" class="openk-button openk-success"
+ style="margin-left: 0.25em;">{{'details.considerations.upload' | translate}}</button>
+ </app-attachment-file-drop-form>
+ </div>
+
+ <div *ngIf="!(((considerations$ | async)?.length > 0) || (isOfficialInCharge$ | async))"
+ class="placeholder">
+ {{"details.considerations.placeholder" | translate}}
+ </div>
+
+</app-collapsible>
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss b/src/app/features/details/components/considerations/statement-details-considerations.component.scss
similarity index 62%
copy from src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
copy to src/app/features/details/components/considerations/statement-details-considerations.component.scss
index 4d2360d..6bc3cc8 100644
--- a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
+++ b/src/app/features/details/components/considerations/statement-details-considerations.component.scss
@@ -11,17 +11,34 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "../../../../styles/openk.styles";
+@import "openk.styles";
:host {
+ display: block;
width: 100%;
+
+ &:empty {
+ display: none;
+ }
}
-.statements {
- margin-bottom: 1em;
- display: grid;
+.attachments--container {
+ min-height: 10em;
+ flex: 1;
}
-.statements--titlebar {
- margin-bottom: 0.5em;
+.attachments--document--lists---half-size {
+ flex: 1;
+ display: inline-block;
+ margin-right: 0.5em;
+}
+
+.attachments--files {
+ padding: 1em;
+ display: flex;
+}
+
+.placeholder {
+ padding: 1em;
+ font-style: italic;
}
diff --git a/src/app/features/details/components/considerations/statement-details-considerations.component.spec.ts b/src/app/features/details/components/considerations/statement-details-considerations.component.spec.ts
new file mode 100644
index 0000000..3eadbdc
--- /dev/null
+++ b/src/app/features/details/components/considerations/statement-details-considerations.component.spec.ts
@@ -0,0 +1,78 @@
+/********************************************************************************
+ * 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} from "../../../../core/i18n";
+import {startAttachmentDownloadAction} from "../../../../store/attachments/actions";
+import {IAttachmentControlValue} from "../../../../store/attachments/model";
+import {setErrorAction} from "../../../../store/root/actions";
+import {queryParamsIdSelector} from "../../../../store/root/selectors";
+import {submitConsiderationFilesAction} from "../../../../store/statements/actions";
+import {StatementDetailsModule} from "../../statement-details.module";
+import {StatementDetailsConsiderationsComponent} from "./statement-details-considerations.component";
+
+describe("StatementDetailsConsiderationsComponent", () => {
+ let component: StatementDetailsConsiderationsComponent;
+ let fixture: ComponentFixture<StatementDetailsConsiderationsComponent>;
+ let store: Store;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ StatementDetailsModule,
+ I18nModule
+ ],
+ providers: [
+ provideMockStore({
+ initialState: {},
+ selectors: [
+ {
+ selector: queryParamsIdSelector,
+ value: 19
+ }
+ ]
+ })
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StatementDetailsConsiderationsComponent);
+ component = fixture.componentInstance;
+ store = fixture.componentRef.injector.get(Store);
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should dispatch submitConsiderationFilesAction on submitting with the current files from cache", async () => {
+ const dispatchSpy = spyOn(store, "dispatch");
+ const file: IAttachmentControlValue = {name: "file1", tagIds: []};
+ component.setValueForArray([file], "considerations");
+ await component.submit();
+ expect(dispatchSpy).toHaveBeenCalledWith(setErrorAction({statementId: 19, error: null}));
+ expect(dispatchSpy).toHaveBeenCalledWith(submitConsiderationFilesAction({
+ statementId: 19, value: [file]
+ }));
+ });
+
+ 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}));
+ });
+});
diff --git a/src/app/features/details/components/considerations/statement-details-considerations.component.ts b/src/app/features/details/components/considerations/statement-details-considerations.component.ts
new file mode 100644
index 0000000..a79cd2e
--- /dev/null
+++ b/src/app/features/details/components/considerations/statement-details-considerations.component.ts
@@ -0,0 +1,89 @@
+/********************************************************************************
+ * 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, Input, OnDestroy, OnInit} from "@angular/core";
+import {FormArray} from "@angular/forms";
+import {select, Store} from "@ngrx/store";
+import {delay, take, takeUntil} from "rxjs/operators";
+import {startAttachmentDownloadAction} from "../../../../store/attachments/actions";
+import {IAttachmentControlValue} from "../../../../store/attachments/model";
+import {getConsiderationAttachments, getStatementAttachmentCacheSelector} from "../../../../store/attachments/selectors";
+import {setErrorAction} from "../../../../store/root/actions";
+import {isOfficialInChargeSelector, queryParamsIdSelector} from "../../../../store/root/selectors";
+import {submitConsiderationFilesAction} from "../../../../store/statements/actions";
+import {statementLoadingSelector} from "../../../../store/statements/selectors";
+import {createFormGroup} from "../../../../util/forms";
+import {AbstractReactiveFormComponent} from "../../../forms/abstract";
+
+@Component({
+ selector: "app-statement-details-considerations",
+ templateUrl: "./statement-details-considerations.component.html",
+ styleUrls: ["./statement-details-considerations.component.scss"]
+})
+export class StatementDetailsConsiderationsComponent
+ extends AbstractReactiveFormComponent<{ considerations: IAttachmentControlValue[] }> implements OnInit, OnDestroy {
+
+ @Input()
+ public appCollapsed = false;
+
+ @Input()
+ public appFinished: boolean;
+
+ @Input()
+ public appFormGroup = createFormGroup<{ considerations: IAttachmentControlValue[] }>({
+ considerations: new FormArray([])
+ });
+
+ public isOfficialInCharge$ = this.store.pipe(select(isOfficialInChargeSelector));
+
+ public statementId$ = this.store.pipe(select(queryParamsIdSelector));
+
+ public considerations$ = this.store.pipe(select(getConsiderationAttachments));
+
+ public fileCache$ = this.store.pipe(select(getStatementAttachmentCacheSelector));
+
+ public isStatementLoading$ = this.store.pipe(select(statementLoadingSelector));
+
+ public constructor(public readonly store: Store) {
+ super();
+ }
+
+ public ngOnInit() {
+ this.fileCache$.pipe(delay(0), takeUntil(this.destroy$))
+ .subscribe((values) => this.setValueForArray(values, "considerations"));
+ }
+
+ public async submit() {
+ await this.clearErrors();
+ const statementId = await this.statementId$.pipe(take(1)).toPromise();
+ const value = this.getValue("considerations");
+ this.store.dispatch(submitConsiderationFilesAction({
+ statementId,
+ value
+ }));
+ }
+
+ public async downloadAttachment(attachmentId: number) {
+ const statementId = await this.statementId$.pipe(take(1)).toPromise();
+ this.store.dispatch(startAttachmentDownloadAction({statementId, attachmentId}));
+ }
+
+ 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}));
+ }
+ }
+
+}
diff --git a/src/app/features/search/components/search/index.ts b/src/app/features/details/components/contributions/index.ts
similarity index 89%
copy from src/app/features/search/components/search/index.ts
copy to src/app/features/details/components/contributions/index.ts
index 5eb9c7f..42d5307 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/features/details/components/contributions/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./statement-details-contributions.component";
diff --git a/src/app/features/details/components/contributions/statement-details-contributions.component.html b/src/app/features/details/components/contributions/statement-details-contributions.component.html
new file mode 100644
index 0000000..77ada97
--- /dev/null
+++ b/src/app/features/details/components/contributions/statement-details-contributions.component.html
@@ -0,0 +1,34 @@
+<!-------------------------------------------------------------------------------
+ * 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]="('statementEditorForm.container.contributionStatus' | translate)
+ + ' (' + (contributions$ | async)?.selected?.length + '/' + (requiredContributionOptions$ | async).length + ')'"
+ class="statement-details">
+
+ <app-select-group
+ *ngIf="(requiredContributionOptions$ | async)?.length > 0"
+ [appGroups]="requiredContributionGroups$ | async"
+ [appHideControls]="true"
+ [appOptions]="requiredContributionOptions$ | async"
+ [appValue]="contributions$ | async"
+ style="padding: 1em;">
+ </app-select-group>
+
+ <div *ngIf="!((requiredContributionOptions$ | async)?.length > 0)"
+ class="placeholder">
+ {{"details.contributions.placeholder" | translate}}
+ </div>
+
+</app-collapsible>
diff --git a/src/app/features/search/components/search/index.ts b/src/app/features/details/components/contributions/statement-details-contributions.component.scss
similarity index 80%
copy from src/app/features/search/components/search/index.ts
copy to src/app/features/details/components/contributions/statement-details-contributions.component.scss
index 5eb9c7f..8e1723d 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/features/details/components/contributions/statement-details-contributions.component.scss
@@ -11,4 +11,16 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+:host {
+ display: block;
+ width: 100%;
+
+ &:empty {
+ display: none;
+ }
+}
+
+.placeholder {
+ padding: 1em;
+ font-style: italic;
+}
diff --git a/src/app/features/details/components/contributions/statement-details-contributions.component.spec.ts b/src/app/features/details/components/contributions/statement-details-contributions.component.spec.ts
new file mode 100644
index 0000000..65b5fc2
--- /dev/null
+++ b/src/app/features/details/components/contributions/statement-details-contributions.component.spec.ts
@@ -0,0 +1,43 @@
+/********************************************************************************
+ * 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 {provideMockStore} from "@ngrx/store/testing";
+import {I18nModule} from "../../../../core/i18n";
+import {StatementDetailsModule} from "../../statement-details.module";
+import {StatementDetailsContributionsComponent} from "./statement-details-contributions.component";
+
+describe("StatementDetailsContributionsComponent", () => {
+ let component: StatementDetailsContributionsComponent;
+ let fixture: ComponentFixture<StatementDetailsContributionsComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ StatementDetailsModule,
+ I18nModule
+ ],
+ providers: [provideMockStore()]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StatementDetailsContributionsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/features/details/components/contributions/statement-details-contributions.component.ts b/src/app/features/details/components/contributions/statement-details-contributions.component.ts
new file mode 100644
index 0000000..68a0f83
--- /dev/null
+++ b/src/app/features/details/components/contributions/statement-details-contributions.component.ts
@@ -0,0 +1,46 @@
+/********************************************************************************
+ * 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, Input} from "@angular/core";
+import {select, Store} from "@ngrx/store";
+import {
+ getContributionsSelector,
+ requiredContributionsGroupsSelector,
+ requiredContributionsOptionsSelector
+} from "../../../../store/statements/selectors";
+
+/**
+ * This component shows the current state of contributions of the statement. This display is purely visual. No editing possible.
+ */
+
+@Component({
+ selector: "app-statement-details-contributions",
+ templateUrl: "./statement-details-contributions.component.html",
+ styleUrls: ["./statement-details-contributions.component.scss"]
+})
+export class StatementDetailsContributionsComponent {
+
+ @Input()
+ public appCollapsed = false;
+
+ public requiredContributionOptions$ = this.store.pipe(select(requiredContributionsOptionsSelector));
+
+ public requiredContributionGroups$ = this.store.pipe(select(requiredContributionsGroupsSelector));
+
+ public contributions$ = this.store.pipe(select(getContributionsSelector));
+
+ public constructor(public store: Store) {
+
+ }
+
+}
diff --git a/src/app/features/search/components/search/index.ts b/src/app/features/details/components/geographic-position/index.ts
similarity index 89%
copy from src/app/features/search/components/search/index.ts
copy to src/app/features/details/components/geographic-position/index.ts
index 5eb9c7f..fc00319 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/features/details/components/geographic-position/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./statement-details-geographic-position.component";
diff --git a/src/app/features/details/components/geographic-position/statement-details-geographic-position.component.html b/src/app/features/details/components/geographic-position/statement-details-geographic-position.component.html
new file mode 100644
index 0000000..928dde4
--- /dev/null
+++ b/src/app/features/details/components/geographic-position/statement-details-geographic-position.component.html
@@ -0,0 +1,37 @@
+<!-------------------------------------------------------------------------------
+ * 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]="'Geographische Position'">
+
+ <div *ngIf="(geographicPosition$ | async) != null"
+ class="map">
+
+ <app-leaflet-map
+ (appOpenGis)="openGis($event)"
+ [appCenter]="geographicPosition$ | async">
+
+ <ng-container [appLeafletMarker]="geographicPosition$ | async">
+ </ng-container>
+
+ </app-leaflet-map>
+
+ </div>
+
+ <div *ngIf="!((geographicPosition$ | async) != null)"
+ class="placeholder">
+ {{"details.geoPositions.placeholder" | translate}}
+ </div>
+
+</app-collapsible>
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss b/src/app/features/details/components/geographic-position/statement-details-geographic-position.component.scss
similarity index 73%
copy from src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
copy to src/app/features/details/components/geographic-position/statement-details-geographic-position.component.scss
index 4d2360d..d94ab9c 100644
--- a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
+++ b/src/app/features/details/components/geographic-position/statement-details-geographic-position.component.scss
@@ -11,17 +11,25 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "../../../../styles/openk.styles";
+@import "openk.styles";
:host {
+ display: block;
width: 100%;
+
+ &:empty {
+ display: none;
+ }
}
-.statements {
- margin-bottom: 1em;
- display: grid;
+.placeholder {
+ padding: 1em;
+ font-style: italic;
}
-.statements--titlebar {
- margin-bottom: 0.5em;
+.map {
+ width: 100%;
+ height: 30em;
+ padding: 1em;
+ box-sizing: border-box;
}
diff --git a/src/app/features/details/components/geographic-position/statement-details-geographic-position.component.spec.ts b/src/app/features/details/components/geographic-position/statement-details-geographic-position.component.spec.ts
new file mode 100644
index 0000000..9be77b7
--- /dev/null
+++ b/src/app/features/details/components/geographic-position/statement-details-geographic-position.component.spec.ts
@@ -0,0 +1,62 @@
+/********************************************************************************
+ * 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 {MockStore, provideMockStore} from "@ngrx/store/testing";
+import {I18nModule} from "../../../../core/i18n";
+import {ILeafletBounds} from "../../../../shared/leaflet/directives/leaflet";
+import {openGisAction} from "../../../../store/geo/actions";
+import {userNameSelector} from "../../../../store/root/selectors";
+import {StatementDetailsModule} from "../../statement-details.module";
+import {StatementDetailsGeographicPositionComponent} from "./statement-details-geographic-position.component";
+
+describe("StatementDetailsGeographicPositionComponent", () => {
+ const user = "userName";
+
+ let component: StatementDetailsGeographicPositionComponent;
+ let fixture: ComponentFixture<StatementDetailsGeographicPositionComponent>;
+ let store: MockStore;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ StatementDetailsModule,
+ I18nModule
+ ],
+ providers: [
+ provideMockStore({
+ selectors: [{selector: userNameSelector, value: user}]
+ })
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StatementDetailsGeographicPositionComponent);
+ store = TestBed.inject(MockStore);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should open GIS", () => {
+ spyOn(store, "dispatch");
+ const bounds = {} as ILeafletBounds;
+ component.openGis(bounds);
+ expect(store.dispatch).toHaveBeenCalledWith(openGisAction({bounds, user}));
+ });
+
+});
diff --git a/src/app/features/details/components/geographic-position/statement-details-geographic-position.component.ts b/src/app/features/details/components/geographic-position/statement-details-geographic-position.component.ts
new file mode 100644
index 0000000..d249aaa
--- /dev/null
+++ b/src/app/features/details/components/geographic-position/statement-details-geographic-position.component.ts
@@ -0,0 +1,49 @@
+/********************************************************************************
+ * 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, Input} from "@angular/core";
+import {select, Store} from "@ngrx/store";
+import {take} from "rxjs/operators";
+import {ILeafletBounds} from "../../../../shared/leaflet";
+import {openGisAction, statementGeographicPositionSelector, userNameSelector} from "../../../../store";
+
+/**
+ * This component displays the statements coordinates on a map using leaflet.
+ * No editing of the set coordinates is possible.
+ */
+
+@Component({
+ selector: "app-statement-details-geographic-position",
+ templateUrl: "./statement-details-geographic-position.component.html",
+ styleUrls: ["./statement-details-geographic-position.component.scss"]
+})
+export class StatementDetailsGeographicPositionComponent {
+
+ @Input()
+ public appCollapsed = false;
+
+ public geographicPosition$ = this.store.pipe(select(statementGeographicPositionSelector));
+
+ public userName$ = this.store.pipe(select(userNameSelector));
+
+ public constructor(public store: Store) {
+
+ }
+
+ public openGis(bounds: ILeafletBounds) {
+ this.userName$.pipe(take(1)).subscribe((user) => {
+ this.store.dispatch(openGisAction({bounds, user}));
+ });
+ }
+
+}
diff --git a/src/app/features/details/components/index.ts b/src/app/features/details/components/index.ts
index afadae7..d62a2b3 100644
--- a/src/app/features/details/components/index.ts
+++ b/src/app/features/details/components/index.ts
@@ -11,6 +11,12 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+export * from "./attachments";
+export * from "./contributions";
+export * from "./geographic-position";
+export * from "./information";
+export * from "./linked-statements";
+export * from "./process-information";
+
export * from "./statement-details";
export * from "./side-menu";
-export * from "./process-information";
diff --git a/src/app/features/search/components/search/index.ts b/src/app/features/details/components/information/index.ts
similarity index 90%
copy from src/app/features/search/components/search/index.ts
copy to src/app/features/details/components/information/index.ts
index 5eb9c7f..b5b3679 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/features/details/components/information/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./statement-details-information.component";
diff --git a/src/app/features/details/components/information/statement-details-information.component.html b/src/app/features/details/components/information/statement-details-information.component.html
new file mode 100644
index 0000000..a94d7a7
--- /dev/null
+++ b/src/app/features/details/components/information/statement-details-information.component.html
@@ -0,0 +1,60 @@
+<!-------------------------------------------------------------------------------
+ * 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]="'Allgemeine Informationen'">
+
+ <div class="information">
+
+ <div class="information-display-container">
+ <span class="information--identifiers--key">{{"statementInformationForm.controls.title" | translate}}</span>
+ <span>{{appStatementInfo?.title}}</span>
+ <span class="information--identifiers--key">{{"statementInformationForm.controls.typeId" | translate}}</span>
+ <span>{{statementType}}</span>
+ <span
+ class="information--identifiers--key">{{"statementInformationForm.controls.creationDate" | translate}}</span>
+ <span>{{(appStatementInfo?.creationDate | appMomentPipe)?.format(timeFormat)}}</span>
+ <span class="information--identifiers--key">{{"statementInformationForm.controls.receiptDate" | translate}}</span>
+ <span>{{(appStatementInfo?.receiptDate | appMomentPipe)?.format(timeFormat)}}</span>
+ <span class="information--identifiers--key">{{"statementInformationForm.controls.dueDate" | translate}}</span>
+ <span>{{(appStatementInfo?.dueDate | appMomentPipe)?.format(timeFormat)}}</span>
+ <span class="information--identifiers--key">{{"statementInformationForm.controls.city" | translate}}</span>
+ <span>{{appStatementInfo?.city}}</span>
+ <span class="information--identifiers--key">{{"statementInformationForm.controls.district" | translate}}</span>
+ <span>{{appStatementInfo?.district}}</span>
+ <span class="information--identifiers--key">{{"statementInformationForm.controls.sectors" | translate}}</span>
+ <span>
+ {{(sectors$ | async | sector : appStatementInfo?.city : appStatementInfo?.district) ?
+ (sectors$ | async | sector : appStatementInfo?.city : appStatementInfo?.district) :
+ "shared.sectors.none" | translate}}
+ </span>
+ <span *ngIf="appStatementInfo?.customerReference"
+ class="information--identifiers--key">{{"statementInformationForm.controls.customerReference" | translate}}</span>
+ <span *ngIf="appStatementInfo?.customerReference">{{appStatementInfo?.customerReference}}</span>
+ </div>
+
+ <div class="information--contact-details">
+ <span class="information--identifiers--key">{{"statementInformationForm.container.contact" | translate}}</span>
+ <div class="information--identifiers">
+ <span>{{appContactInfo?.company}}</span>
+ <span>{{appContactInfo?.firstName}} {{appContactInfo?.lastName}}</span>
+ <span>{{appContactInfo?.street}} {{appContactInfo?.houseNumber}}</span>
+ <span>{{appContactInfo?.postCode}} {{appContactInfo?.community}} {{appContactInfo?.communitySuffix}}</span>
+ <span>{{appContactInfo?.email}}</span>
+ </div>
+ </div>
+
+ </div>
+
+</app-collapsible>
diff --git a/src/app/features/details/components/information/statement-details-information.component.scss b/src/app/features/details/components/information/statement-details-information.component.scss
new file mode 100644
index 0000000..535cd5c
--- /dev/null
+++ b/src/app/features/details/components/information/statement-details-information.component.scss
@@ -0,0 +1,63 @@
+/********************************************************************************
+ * 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
+ ********************************************************************************/
+
+:host {
+ display: block;
+ width: 100%;
+
+ &:empty {
+ display: none;
+ }
+}
+
+.information {
+ display: flex;
+ flex-direction: row;
+ padding: 1em;
+ overflow: auto;
+}
+
+.information-display-container {
+ display: grid;
+ grid-template-columns: max-content auto;
+ grid-column-gap: 0.5em;
+ margin-right: 0.5em;
+ min-width: 20em;
+ flex: 1;
+
+ > * {
+ margin-bottom: 0.1em;
+ }
+}
+
+.information--identifiers {
+ display: flex;
+ flex-direction: column;
+ margin-right: 1em;
+ margin-left: 1em;
+
+ > * {
+ margin-bottom: 0.1em;
+ }
+}
+
+.information--identifiers--key {
+ font-weight: 600;
+ margin-bottom: 0.1em;
+}
+
+.information--contact-details {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+}
diff --git a/src/app/features/details/components/information/statement-details-information.component.spec.ts b/src/app/features/details/components/information/statement-details-information.component.spec.ts
new file mode 100644
index 0000000..dcf161f
--- /dev/null
+++ b/src/app/features/details/components/information/statement-details-information.component.spec.ts
@@ -0,0 +1,43 @@
+/********************************************************************************
+ * 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 {provideMockStore} from "@ngrx/store/testing";
+import {I18nModule} from "../../../../core/i18n";
+import {StatementDetailsModule} from "../../statement-details.module";
+import {StatementDetailsInformationComponent} from "./statement-details-information.component";
+
+describe("StatementDetailsInformationComponent", () => {
+ let component: StatementDetailsInformationComponent;
+ let fixture: ComponentFixture<StatementDetailsInformationComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ StatementDetailsModule,
+ I18nModule
+ ],
+ providers: [provideMockStore()]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StatementDetailsInformationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/features/details/components/information/statement-details-information.component.stories.ts b/src/app/features/details/components/information/statement-details-information.component.stories.ts
new file mode 100644
index 0000000..cf14a38
--- /dev/null
+++ b/src/app/features/details/components/information/statement-details-information.component.stories.ts
@@ -0,0 +1,38 @@
+/********************************************************************************
+ * 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 {provideMockStore} from "@ngrx/store/testing";
+import {withKnobs} from "@storybook/addon-knobs";
+import {moduleMetadata, storiesOf} from "@storybook/angular";
+import {I18nModule} from "../../../../core/i18n";
+import {StatementDetailsModule} from "../../statement-details.module";
+
+storiesOf("Features / Forms / Details", module)
+ .addDecorator(withKnobs)
+ .addDecorator(moduleMetadata({
+ providers: [
+ provideMockStore()
+ ],
+ imports: [
+ I18nModule,
+ StatementDetailsModule
+ ]
+ }))
+ .add("StatementDetailsInformationComponent", () => ({
+ template: `
+ <app-statement-details-information>
+ </app-statement-details-information>
+ `,
+ props: {}
+ }));
diff --git a/src/app/features/details/components/information/statement-details-information.component.ts b/src/app/features/details/components/information/statement-details-information.component.ts
new file mode 100644
index 0000000..1b5d600
--- /dev/null
+++ b/src/app/features/details/components/information/statement-details-information.component.ts
@@ -0,0 +1,72 @@
+/********************************************************************************
+ * 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, Input, OnDestroy, OnInit} from "@angular/core";
+import {select, Store} from "@ngrx/store";
+import {Observable, Subject} from "rxjs";
+import {takeUntil} from "rxjs/operators";
+import {IAPIContactPersonDetails} from "../../../../core/api/contacts/IAPIContactPersonDetails";
+import {IAPIStatementModel} from "../../../../core/api/statements";
+import {IAPISectorsModel} from "../../../../core/api/statements/IAPISectorsModel";
+import {statementTypesSelector} from "../../../../store/settings/selectors";
+import {getStatementSectorsSelector} from "../../../../store/statements/selectors";
+import {momentFormatDisplayNumeric} from "../../../../util/moment";
+
+/**
+ * This component displays the general information saved to a statement.
+ */
+
+@Component({
+ selector: "app-statement-details-information",
+ templateUrl: "./statement-details-information.component.html",
+ styleUrls: ["./statement-details-information.component.scss"]
+})
+export class StatementDetailsInformationComponent implements OnInit, OnDestroy {
+
+ @Input()
+ public appCollapsed = false;
+
+ @Input()
+ public appStatementInfo: IAPIStatementModel;
+
+ @Input()
+ public appContactInfo: IAPIContactPersonDetails;
+
+ public statementTypeOptions$ = this.store.pipe(select(statementTypesSelector));
+
+ public statementType: string;
+
+ public sectors$: Observable<IAPISectorsModel> = this.store.pipe(select(getStatementSectorsSelector));
+
+ public timeFormat = momentFormatDisplayNumeric;
+
+ private destroy$ = new Subject();
+
+ public constructor(public store: Store) {
+
+ }
+
+ public ngOnInit() {
+ this.statementTypeOptions$.pipe(
+ takeUntil(this.destroy$)
+ ).subscribe((typeOptions) => {
+ this.statementType = typeOptions.find((_) => _?.value === this.appStatementInfo?.typeId)?.label;
+ });
+ }
+
+ public ngOnDestroy() {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+}
diff --git a/src/app/features/search/components/search/index.ts b/src/app/features/details/components/linked-statements/index.ts
similarity index 89%
copy from src/app/features/search/components/search/index.ts
copy to src/app/features/details/components/linked-statements/index.ts
index 5eb9c7f..7657824 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/features/details/components/linked-statements/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./statement-details-linked-statements.component";
diff --git a/src/app/shared/linked-statements/linked-statements/index.ts b/src/app/features/details/components/linked-statements/linked-statements/index.ts
similarity index 100%
rename from src/app/shared/linked-statements/linked-statements/index.ts
rename to src/app/features/details/components/linked-statements/linked-statements/index.ts
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.html b/src/app/features/details/components/linked-statements/linked-statements/linked-statements.component.html
similarity index 86%
rename from src/app/shared/linked-statements/linked-statements/linked-statements.component.html
rename to src/app/features/details/components/linked-statements/linked-statements/linked-statements.component.html
index 680e9c6..fdbc353 100644
--- a/src/app/shared/linked-statements/linked-statements/linked-statements.component.html
+++ b/src/app/features/details/components/linked-statements/linked-statements/linked-statements.component.html
@@ -11,22 +11,22 @@
* SPDX-License-Identifier: EPL-2.0
-------------------------------------------------------------------------------->
-<div *ngIf="appPredecessors?.length > 0" class="statements">
+<div *ngIf="appParents?.length > 0" class="statements">
<span class="statements--titlebar">{{"shared.linkedStatements.precedingStatements" | translate}}</span>
<app-statement-table
[appColumns]="columns"
- [appEntries]="appSuccessors"
+ [appEntries]="appParents"
[appOpenStatementInNewTab]="true"
[appStatementTypeOptions]="appStatementTypeOptions"
class="openk-table---last-row-without-border">
</app-statement-table>
</div>
-<div *ngIf="appSuccessors?.length > 0" class="statements">
+<div *ngIf="appChildren?.length > 0" class="statements">
<span class="statements--titlebar">{{"shared.linkedStatements.successiveStatements" | translate}}</span>
<app-statement-table
[appColumns]="columns"
- [appEntries]="appSuccessors"
+ [appEntries]="appChildren"
[appOpenStatementInNewTab]="true"
[appStatementTypeOptions]="appStatementTypeOptions"
class="openk-table---last-row-without-border">
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss b/src/app/features/details/components/linked-statements/linked-statements/linked-statements.component.scss
similarity index 90%
rename from src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
rename to src/app/features/details/components/linked-statements/linked-statements/linked-statements.component.scss
index 4d2360d..82a69c6 100644
--- a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
+++ b/src/app/features/details/components/linked-statements/linked-statements/linked-statements.component.scss
@@ -11,10 +11,13 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "../../../../styles/openk.styles";
-
:host {
+ display: block;
width: 100%;
+
+ &:empty {
+ display: none;
+ }
}
.statements {
@@ -24,4 +27,5 @@
.statements--titlebar {
margin-bottom: 0.5em;
+ font-weight: 600;
}
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.spec.ts b/src/app/features/details/components/linked-statements/linked-statements/linked-statements.component.spec.ts
similarity index 88%
rename from src/app/shared/linked-statements/linked-statements/linked-statements.component.spec.ts
rename to src/app/features/details/components/linked-statements/linked-statements/linked-statements.component.spec.ts
index 9c7ed3b..4b27146 100644
--- a/src/app/shared/linked-statements/linked-statements/linked-statements.component.spec.ts
+++ b/src/app/features/details/components/linked-statements/linked-statements/linked-statements.component.spec.ts
@@ -13,8 +13,7 @@
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
import {RouterTestingModule} from "@angular/router/testing";
-import {I18nModule} from "../../../core/i18n";
-import {LinkedStatementsModule} from "../linked-statements.module";
+import {I18nModule} from "../../../../../core/i18n";
import {LinkedStatementsComponent} from "./linked-statements.component";
describe("LinkedStatementsComponent", () => {
@@ -25,8 +24,7 @@
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
- I18nModule,
- LinkedStatementsModule
+ I18nModule
]
}).compileComponents();
}));
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.stories.ts b/src/app/features/details/components/linked-statements/linked-statements/linked-statements.component.stories.ts
similarity index 75%
rename from src/app/shared/linked-statements/linked-statements/linked-statements.component.stories.ts
rename to src/app/features/details/components/linked-statements/linked-statements/linked-statements.component.stories.ts
index f86ce60..d2bfda1 100644
--- a/src/app/shared/linked-statements/linked-statements/linked-statements.component.stories.ts
+++ b/src/app/features/details/components/linked-statements/linked-statements/linked-statements.component.stories.ts
@@ -14,10 +14,10 @@
import {RouterTestingModule} from "@angular/router/testing";
import {number, withKnobs} from "@storybook/addon-knobs";
import {moduleMetadata, storiesOf} from "@storybook/angular";
-import {I18nModule} from "../../../core/i18n";
-import {createStatementModelMock} from "../../../test";
-import {createSelectOptionsMock} from "../../../test/create-select-options.spec";
-import {LinkedStatementsModule} from "../linked-statements.module";
+import {I18nModule} from "../../../../../core/i18n";
+import {createSelectOptionsMock, createStatementModelMock} from "../../../../../test";
+import {StatementDetailsModule} from "../../../statement-details.module";
+
storiesOf("Shared", module)
.addDecorator(withKnobs)
@@ -25,15 +25,15 @@
imports: [
RouterTestingModule,
I18nModule,
- LinkedStatementsModule
+ StatementDetailsModule
]
}))
- .add("LinkedStatementsComponent", () => ({
+ .add("StatementDetailsLinkedStatementsComponent", () => ({
template: `
<div style="padding: 1em;">
<app-linked-statements
- [appPredecessors]="entries.slice(0, numberOfRowsToDisplay)"
- [appSuccessors]="entries.slice(0, numberOfRowsToDisplay)"
+ [appParents]="entries.slice(0, numberOfRowsToDisplay)"
+ [appChildren]="entries.slice(0, numberOfRowsToDisplay)"
[appStatementTypeOptions]="appStatementTypeOptions">
</app-linked-statements>
</div>
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.ts b/src/app/features/details/components/linked-statements/linked-statements/linked-statements.component.ts
similarity index 72%
rename from src/app/shared/linked-statements/linked-statements/linked-statements.component.ts
rename to src/app/features/details/components/linked-statements/linked-statements/linked-statements.component.ts
index e46b191..00f8705 100644
--- a/src/app/shared/linked-statements/linked-statements/linked-statements.component.ts
+++ b/src/app/features/details/components/linked-statements/linked-statements/linked-statements.component.ts
@@ -12,8 +12,9 @@
********************************************************************************/
import {Component, Input} from "@angular/core";
-import {ISelectOption} from "../../controls/select/model";
-import {IStatementTableEntry, StatementTableComponent, TStatementTableColumns} from "../../layout/statement-table";
+import {ISelectOption} from "../../../../../shared/controls/select/model";
+import {StatementTableComponent, TStatementTableColumns} from "../../../../../shared/layout/statement-table/components";
+import {IStatementTableEntry} from "../../../../../shared/layout/statement-table/model";
@Component({
selector: "app-linked-statements",
@@ -23,10 +24,10 @@
export class LinkedStatementsComponent {
@Input()
- public appPredecessors: Array<IStatementTableEntry>;
+ public appParents: Array<IStatementTableEntry>;
@Input()
- public appSuccessors: Array<IStatementTableEntry>;
+ public appChildren: Array<IStatementTableEntry>;
@Input()
public appStatementTypeOptions: ISelectOption<number>[];
diff --git a/src/app/features/details/components/linked-statements/statement-details-linked-statements.component.html b/src/app/features/details/components/linked-statements/statement-details-linked-statements.component.html
new file mode 100644
index 0000000..0489be5
--- /dev/null
+++ b/src/app/features/details/components/linked-statements/statement-details-linked-statements.component.html
@@ -0,0 +1,32 @@
+<!-------------------------------------------------------------------------------
+ * 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.linkedStatements.title' | translate">
+
+ <div *ngIf="(parents$ | async)?.length > 0 || (children$ | async)?.length > 0"
+ style="padding: 1em;">
+ <app-linked-statements
+ [appChildren]="children$ | async"
+ [appParents]="parents$ | async"
+ [appStatementTypeOptions]="statementTypeOptions$ | async">
+ </app-linked-statements>
+ </div>
+
+ <div *ngIf="!((parents$ | async)?.length > 0 || (children$ | async)?.length > 0)"
+ class="placeholder">
+ {{"details.linkedStatements.placeholder" | translate}}
+ </div>
+
+</app-collapsible>
diff --git a/src/app/features/search/components/search/index.ts b/src/app/features/details/components/linked-statements/statement-details-linked-statements.component.scss
similarity index 80%
copy from src/app/features/search/components/search/index.ts
copy to src/app/features/details/components/linked-statements/statement-details-linked-statements.component.scss
index 5eb9c7f..8e1723d 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/features/details/components/linked-statements/statement-details-linked-statements.component.scss
@@ -11,4 +11,16 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+:host {
+ display: block;
+ width: 100%;
+
+ &:empty {
+ display: none;
+ }
+}
+
+.placeholder {
+ padding: 1em;
+ font-style: italic;
+}
diff --git a/src/app/features/details/components/linked-statements/statement-details-linked-statements.component.spec.ts b/src/app/features/details/components/linked-statements/statement-details-linked-statements.component.spec.ts
new file mode 100644
index 0000000..49ad5b8
--- /dev/null
+++ b/src/app/features/details/components/linked-statements/statement-details-linked-statements.component.spec.ts
@@ -0,0 +1,43 @@
+/********************************************************************************
+ * 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 {provideMockStore} from "@ngrx/store/testing";
+import {I18nModule} from "../../../../core/i18n";
+import {StatementDetailsModule} from "../../statement-details.module";
+import {StatementDetailsLinkedStatementsComponent} from "./statement-details-linked-statements.component";
+
+describe("StatementDetailsLinkedStatementsComponent", () => {
+ let component: StatementDetailsLinkedStatementsComponent;
+ let fixture: ComponentFixture<StatementDetailsLinkedStatementsComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ StatementDetailsModule,
+ I18nModule
+ ],
+ providers: [provideMockStore()]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StatementDetailsLinkedStatementsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/features/details/components/linked-statements/statement-details-linked-statements.component.ts b/src/app/features/details/components/linked-statements/statement-details-linked-statements.component.ts
new file mode 100644
index 0000000..ef15d54
--- /dev/null
+++ b/src/app/features/details/components/linked-statements/statement-details-linked-statements.component.ts
@@ -0,0 +1,44 @@
+/********************************************************************************
+ * 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, Input} from "@angular/core";
+import {select, Store} from "@ngrx/store";
+import {statementTypesSelector} from "../../../../store/settings/selectors";
+import {childrenStatementListSelector, parentStatementListSelector} from "../../../../store/statements/selectors";
+
+/**
+ * This component the connected statements split up into two categories:
+ * Parents, meaning preceding statements and children, meaning following statements.
+ * Those lists cannot be modified in this display.
+ */
+
+@Component({
+ selector: "app-statement-details-linked-statements",
+ templateUrl: "./statement-details-linked-statements.component.html",
+ styleUrls: ["./statement-details-linked-statements.component.scss"]
+})
+export class StatementDetailsLinkedStatementsComponent {
+
+ @Input()
+ public appCollapsed = false;
+
+ public statementTypeOptions$ = this.store.pipe(select(statementTypesSelector));
+
+ public parents$ = this.store.pipe(select(parentStatementListSelector));
+
+ public children$ = this.store.pipe(select(childrenStatementListSelector));
+
+ public constructor(public store: Store) {
+ }
+
+}
diff --git a/src/app/features/details/components/outbox/statement-details-outbox.component.html b/src/app/features/details/components/outbox/statement-details-outbox.component.html
new file mode 100644
index 0000000..8b163f0
--- /dev/null
+++ b/src/app/features/details/components/outbox/statement-details-outbox.component.html
@@ -0,0 +1,48 @@
+<!-------------------------------------------------------------------------------
+ * 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.outbox.title' | translate">
+
+ <div *ngIf="(statementAttachment$ | async)?.length > 0 || (outboxAttachments$ | async)?.length > 0"
+ class="outbox-attachments">
+ <div *ngIf="(statementAttachment$ | async)?.length > 0"
+ class="outbox-attachments--statement"
+ style="margin-right: 0.5em;">
+ <app-attachment-display-list
+ (appDownload)="downloadAttachment($event)"
+ [appAttachments]="statementAttachment$ | async"
+ [appTagList]="[]"
+ [appTitle]="'details.outbox.statement' | translate"
+ class="outbox-attachments--statement">
+ </app-attachment-display-list>
+ </div>
+
+ <div class="outbox-attachments--statement">
+ <app-attachment-display-list
+ (appDownload)="downloadAttachment($event)"
+ [appAttachments]="outboxAttachments$ | async"
+ [appTagList]="[]"
+ [appTitle]="'details.outbox.attachments' | translate"
+ class="outbox-attachments--attachments">
+ </app-attachment-display-list>
+ </div>
+ </div>
+
+ <div *ngIf="!((statementAttachment$ | async)?.length > 0 || (outboxAttachments$ | async)?.length > 0)"
+ class="placeholder">
+ {{"details.outbox.placeholder" | translate}}
+ </div>
+
+</app-collapsible>
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss b/src/app/features/details/components/outbox/statement-details-outbox.component.scss
similarity index 63%
copy from src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
copy to src/app/features/details/components/outbox/statement-details-outbox.component.scss
index 4d2360d..aa3464f 100644
--- a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
+++ b/src/app/features/details/components/outbox/statement-details-outbox.component.scss
@@ -11,17 +11,34 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "../../../../styles/openk.styles";
+@import "openk.styles";
:host {
+ display: block;
width: 100%;
+
+ &:empty {
+ display: none;
+ }
}
-.statements {
- margin-bottom: 1em;
- display: grid;
+.placeholder {
+ padding: 1em;
+ font-style: italic;
}
-.statements--titlebar {
- margin-bottom: 0.5em;
+
+.outbox-attachments {
+ padding: 1em;
+ display: flex;
+ flex-direction: row;
+}
+
+.outbox-attachments--statement {
+ display: inline-block;
+ flex: 1;
+}
+
+.outbox-attachments--attachments {
+ display: inline-block;
}
diff --git a/src/app/features/details/components/outbox/statement-details-outbox.component.spec.ts b/src/app/features/details/components/outbox/statement-details-outbox.component.spec.ts
new file mode 100644
index 0000000..346d190
--- /dev/null
+++ b/src/app/features/details/components/outbox/statement-details-outbox.component.spec.ts
@@ -0,0 +1,64 @@
+/********************************************************************************
+ * 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} from "../../../../core/i18n";
+import {startAttachmentDownloadAction} from "../../../../store/attachments/actions";
+import {queryParamsIdSelector} from "../../../../store/root/selectors";
+import {StatementDetailsModule} from "../../statement-details.module";
+import {StatementDetailsOutboxComponent} from "./statement-details-outbox.component";
+
+describe("StatementDetailsOutboxComponent", () => {
+ let component: StatementDetailsOutboxComponent;
+ let fixture: ComponentFixture<StatementDetailsOutboxComponent>;
+ let store: Store;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ StatementDetailsModule,
+ I18nModule
+ ],
+ providers: [
+ provideMockStore({
+ initialState: {},
+ selectors: [
+ {
+ selector: queryParamsIdSelector,
+ value: 19
+ }
+ ]
+ })
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StatementDetailsOutboxComponent);
+ component = fixture.componentInstance;
+ store = fixture.componentRef.injector.get(Store);
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ 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}));
+ });
+});
diff --git a/src/app/features/details/components/outbox/statement-details-outbox.component.ts b/src/app/features/details/components/outbox/statement-details-outbox.component.ts
new file mode 100644
index 0000000..a2e3b12
--- /dev/null
+++ b/src/app/features/details/components/outbox/statement-details-outbox.component.ts
@@ -0,0 +1,51 @@
+/********************************************************************************
+ * 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, Input} from "@angular/core";
+import {select, Store} from "@ngrx/store";
+import {take} from "rxjs/operators";
+import {startAttachmentDownloadAction} from "../../../../store/attachments/actions";
+import {getOutboxAttachments, getStatementPdfAttachment} from "../../../../store/attachments/selectors";
+import {queryParamsIdSelector} from "../../../../store/root/selectors";
+
+/**
+ * This component displays the statements attachments that are meant for the statement response (outbox)
+ * and the generated Statement.pdf that is also sent in the response,
+ * The files can be downloaded by pressing a button.
+ */
+
+@Component({
+ selector: "app-statement-details-outbox",
+ templateUrl: "./statement-details-outbox.component.html",
+ styleUrls: ["./statement-details-outbox.component.scss"]
+})
+export class StatementDetailsOutboxComponent {
+
+ @Input()
+ public appCollapsed = false;
+
+ public statementId$ = this.store.pipe(select(queryParamsIdSelector));
+
+ public statementAttachment$ = this.store.pipe(select(getStatementPdfAttachment));
+
+ public outboxAttachments$ = this.store.pipe(select(getOutboxAttachments));
+
+ public constructor(public store: Store) {
+ }
+
+ public async downloadAttachment(attachmentId: number) {
+ const statementId = await this.statementId$.pipe(take(1)).toPromise();
+ this.store.dispatch(startAttachmentDownloadAction({statementId, attachmentId}));
+ }
+
+}
diff --git a/src/app/features/details/components/side-menu/statement-details-side-menu.component.ts b/src/app/features/details/components/side-menu/statement-details-side-menu.component.ts
index d5fcb0e..9988172 100644
--- a/src/app/features/details/components/side-menu/statement-details-side-menu.component.ts
+++ b/src/app/features/details/components/side-menu/statement-details-side-menu.component.ts
@@ -89,7 +89,7 @@
cssClass: "openk-danger"
},
{
- emit: this.emitClaimTaskFatory(),
+ emit: this.emitClaimTaskFactory(),
label: "details.sideMenu.editInfoData",
icon: "subject",
cssClass: "openk-success"
@@ -106,7 +106,7 @@
cssClass: "openk-info"
},
{
- emit: this.emitClaimTaskFatory(),
+ emit: this.emitClaimTaskFactory(),
label: "details.sideMenu.createNegativeStatement",
icon: "edit",
cssClass: "openk-info"
@@ -114,7 +114,7 @@
],
[EAPIProcessTaskDefinitionKey.ADD_WORK_FLOW_DATA]: [
{
- emit: this.emitClaimTaskFatory(),
+ emit: this.emitClaimTaskFactory(),
label: "details.sideMenu.editWorkflowData",
icon: "subject",
cssClass: "openk-info"
@@ -122,7 +122,7 @@
],
[EAPIProcessTaskDefinitionKey.CREATE_DRAFT]: [
{
- emit: this.emitClaimTaskFatory(),
+ emit: this.emitClaimTaskFactory(),
label: "details.sideMenu.createDraft",
icon: "edit",
cssClass: "openk-info"
@@ -138,7 +138,7 @@
],
[EAPIProcessTaskDefinitionKey.CHECK_AND_FORMULATE_RESPONSE]: [
{
- emit: this.emitClaimTaskFatory(),
+ emit: this.emitClaimTaskFactory(),
label: "details.sideMenu.completeDraft",
icon: "description",
cssClass: "openk-info"
@@ -165,7 +165,7 @@
[EAPIUserRoles.DIVISION_MEMBER]: {
[EAPIProcessTaskDefinitionKey.ENRICH_DRAFT]: [
{
- emit: this.emitClaimTaskFatory(),
+ emit: this.emitClaimTaskFactory(),
label: "details.sideMenu.editDraft",
icon: "description",
cssClass: "openk-info"
@@ -219,7 +219,7 @@
}));
}
- private emitClaimTaskFatory() {
+ private emitClaimTaskFactory() {
return (task: IAPIProcessTask) => this.appDispatch.emit(claimTaskAction({
statementId: task?.statementId,
taskId: task?.taskId
diff --git a/src/app/features/details/components/statement-details/statement-details.component.html b/src/app/features/details/components/statement-details/statement-details.component.html
index 0a0b6d9..00a2a34 100644
--- a/src/app/features/details/components/statement-details/statement-details.component.html
+++ b/src/app/features/details/components/statement-details/statement-details.component.html
@@ -20,7 +20,7 @@
</div>
<app-statement-details-side-menu
- (appDispatch)="store.dispatch($event)"
+ (appDispatch)="clearErrors(); store.dispatch($event)"
[appUserName]="userName$ | async"
[appLoading]="isStatementLoading$ | async"
[appTasks]="tasks$ | async"
@@ -29,42 +29,41 @@
</app-statement-details-side-menu>
<ng-container *ngIf="(statement$ | async) != null">
- <app-collapsible
- [appCollapsed]="true"
- [appTitle]="'Allgemeine Informationen'"
- class="statement-details">
- <div class="statement-details-content">
- <div *ngFor="let obj of (statement$ | async) | objToArray">
- <div *ngFor="let detail of obj.value | objToArray">
- {{detail?.key}}: {{detail?.value | json}}
- </div>
- </div>
- </div>
- </app-collapsible>
-
- <app-collapsible
- [appCollapsed]="true"
- [appTitle]="'Eingangsdokumente'"
+ <app-statement-details-information
+ [appContactInfo]="(statement$ | async)?.contactInfo"
+ [appStatementInfo]="(statement$ | async)?.info"
class="statement-details">
- <div class="placeholder">Not yet implemented.</div>
- </app-collapsible>
+ </app-statement-details-information>
- <app-collapsible
- [appCollapsed]="true"
- [appTitle]="'Betroffene Fachbereiche'"
+ <app-statement-details-geographic-position
class="statement-details">
- <div class="placeholder">Not yet implemented.</div>
- </app-collapsible>
+ </app-statement-details-geographic-position>
+
+ <app-statement-details-attachments
+ class="statement-details">
+ </app-statement-details-attachments>
+
+ <app-statement-details-contributions
+ class="statement-details">
+ </app-statement-details-contributions>
+
+ <app-statement-details-outbox
+ class="statement-details">
+ </app-statement-details-outbox>
+
+ <app-statement-details-considerations
+ [appFinished]="(statement$ | async)?.info?.finished"
+ class="statement-details">
+ </app-statement-details-considerations>
<app-comments-form
class="statement-details"
[appCollapsed]="true">
-
</app-comments-form>
<app-collapsible
- [appCollapsed]="true"
+ [appCollapsed]="false"
[appTitle]="'Prozessinformationen'"
class="statement-details">
<app-process-information
@@ -78,11 +77,8 @@
</app-process-information>
</app-collapsible>
- <app-collapsible
- [appCollapsed]="true"
- [appTitle]="'Verknüpfte Vorgänge'"
+ <app-statement-details-linked-statements
class="statement-details">
- <div class="placeholder"> Not yet implemented.</div>
- </app-collapsible>
+ </app-statement-details-linked-statements>
</ng-container>
diff --git a/src/app/features/details/components/statement-details/statement-details.component.spec.ts b/src/app/features/details/components/statement-details/statement-details.component.spec.ts
index 3232592..8aed7a5 100644
--- a/src/app/features/details/components/statement-details/statement-details.component.spec.ts
+++ b/src/app/features/details/components/statement-details/statement-details.component.spec.ts
@@ -17,13 +17,7 @@
import {MockStore, provideMockStore} from "@ngrx/store/testing";
import {I18nModule} from "../../../../core/i18n";
import {CardModule} from "../../../../shared/layout/card";
-import {
- addCommentAction,
- claimTaskAction,
- deleteCommentAction,
- fetchStatementDetailsAction,
- queryParamsIdSelector
-} from "../../../../store";
+import {addCommentAction, deleteCommentAction, fetchStatementDetailsAction, queryParamsIdSelector} from "../../../../store";
import {StatementDetailsComponent} from "./statement-details.component";
describe("StatementDetailsComponent", () => {
@@ -56,16 +50,6 @@
component.ngOnDestroy();
});
- it("should dispatch edit action", () => {
- const dispatchSpy = spyOn(mockStore, "dispatch");
- component.editTask({statementId: 1, taskId: "9"} as any, {negative: true});
- expect(dispatchSpy).toHaveBeenCalledWith(claimTaskAction({
- statementId: 1,
- taskId: "9",
- options: {negative: true}
- }));
- });
-
it("should dispatch add comment action", async () => {
const dispatchSpy = spyOn(mockStore, "dispatch");
diff --git a/src/app/features/details/components/statement-details/statement-details.component.ts b/src/app/features/details/components/statement-details/statement-details.component.ts
index 322432a..b43fe9c 100644
--- a/src/app/features/details/components/statement-details/statement-details.component.ts
+++ b/src/app/features/details/components/statement-details/statement-details.component.ts
@@ -13,19 +13,19 @@
import {Component, OnDestroy, OnInit} from "@angular/core";
import {select, Store} from "@ngrx/store";
-import {Observable, Subscription} from "rxjs";
-import {filter, take} from "rxjs/operators";
-import {IAPIProcessTask} from "../../../../core/api/process";
+import {combineLatest, Observable, Subject} from "rxjs";
+import {filter, take, takeUntil, withLatestFrom} from "rxjs/operators";
import {
addCommentAction,
- claimTaskAction,
currentActivityIds,
deleteCommentAction,
fetchStatementDetailsAction,
getStatementErrorSelector,
historySelector,
+ IStatementEntity,
IStatementErrorEntity,
processDiagramSelector,
+ processLoadingSelector,
processNameSelector,
processVersionSelector,
queryParamsIdSelector,
@@ -47,7 +47,7 @@
public statementId$ = this.store.pipe(select(queryParamsIdSelector));
- public statement$ = this.store.pipe(select(statementSelector));
+ public statement$: Observable<IStatementEntity> = this.store.pipe(select(statementSelector));
public title$ = this.store.pipe(select(statementTitleSelector));
@@ -71,34 +71,37 @@
public appError$: Observable<IStatementErrorEntity> = this.store.pipe(select(getStatementErrorSelector));
- private subscription: Subscription;
+ public tasksLoading$ = this.store.pipe(select(processLoadingSelector));
+
+ private destroy$ = new Subject();
public constructor(public readonly store: Store) {
}
public ngOnInit(): void {
- this.subscription = this.statementId$
- .pipe(filter((statementId) => statementId != null))
+ this.statementId$
+ .pipe(
+ filter((statementId) => statementId != null),
+ takeUntil(this.destroy$)
+ )
.subscribe((statementId) => this.store.dispatch(fetchStatementDetailsAction({statementId})));
+
+ this.tasksLoading$.pipe(
+ filter((tasks) => tasks === false),
+ withLatestFrom(this.statementId$),
+ takeUntil(this.destroy$)
+ ).subscribe(([loading, statementId]) => {
+ this.store.dispatch(fetchStatementDetailsAction({statementId}));
+ });
}
public async ngOnDestroy() {
- if (this.subscription != null) {
- this.subscription.unsubscribe();
- this.subscription = null;
- }
+ this.destroy$.next();
+ this.destroy$.complete();
await this.clearErrors();
}
- public editTask(task: IAPIProcessTask, options?: any) {
- this.store.dispatch(claimTaskAction({
- statementId: task?.statementId,
- taskId: task?.taskId,
- options
- }));
- }
-
public async addComment(text: string) {
const statementId = await this.statementId$.pipe(take(1)).toPromise();
this.store.dispatch(addCommentAction({statementId, text}));
@@ -109,12 +112,11 @@
this.store.dispatch(deleteCommentAction({statementId, commentId}));
}
- 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}));
- }
+ public clearErrors() {
+ combineLatest([this.statementId$, this.isStatementLoading$]).pipe(
+ take(1),
+ filter(([statementId, loading]) => statementId != null && !loading)
+ ).subscribe(([statementId]) => this.store.dispatch(setErrorAction({statementId, error: null})));
}
}
diff --git a/src/app/features/details/statement-details.module.ts b/src/app/features/details/statement-details.module.ts
index 13a1b6f..f370e1b 100644
--- a/src/app/features/details/statement-details.module.ts
+++ b/src/app/features/details/statement-details.module.ts
@@ -19,19 +19,32 @@
import {RouterModule} from "@angular/router";
import {TranslateModule} from "@ngx-translate/core";
import {DateControlModule} from "../../shared/controls/date-control";
+import {SelectModule} from "../../shared/controls/select";
import {ActionButtonModule} from "../../shared/layout/action-button";
import {CardModule} from "../../shared/layout/card";
import {CollapsibleModule} from "../../shared/layout/collapsible";
import {SideMenuModule} from "../../shared/layout/side-menu";
+import {StatementTableModule} from "../../shared/layout/statement-table";
+import {LeafletModule} from "../../shared/leaflet";
import {SharedPipesModule} from "../../shared/pipes";
+import {AttachmentsFormModule} from "../forms/attachments";
import {CommentsFormModule} from "../forms/comments";
+import {StatementInformationFormModule} from "../forms/statement-information";
import {
ProcessDiagramComponent,
ProcessHistoryComponent,
ProcessInformationComponent,
+ StatementDetailsAttachmentsComponent,
StatementDetailsComponent,
+ StatementDetailsContributionsComponent,
+ StatementDetailsGeographicPositionComponent,
+ StatementDetailsInformationComponent,
+ StatementDetailsLinkedStatementsComponent,
StatementDetailsSideMenuComponent
} from "./components";
+import {StatementDetailsConsiderationsComponent} from "./components/considerations";
+import {LinkedStatementsComponent} from "./components/linked-statements/linked-statements";
+import {StatementDetailsOutboxComponent} from "./components/outbox/statement-details-outbox.component";
import {BpmnDirective} from "./directives";
import {GetProcessHistoryEntriesPipe} from "./pipes";
@@ -50,7 +63,12 @@
CollapsibleModule,
CommentsFormModule,
SideMenuModule,
- ActionButtonModule
+ ActionButtonModule,
+ LeafletModule,
+ AttachmentsFormModule,
+ StatementInformationFormModule,
+ SelectModule,
+ StatementTableModule
],
declarations: [
StatementDetailsComponent,
@@ -59,7 +77,15 @@
ProcessInformationComponent,
BpmnDirective,
StatementDetailsSideMenuComponent,
- GetProcessHistoryEntriesPipe
+ GetProcessHistoryEntriesPipe,
+ StatementDetailsAttachmentsComponent,
+ StatementDetailsContributionsComponent,
+ StatementDetailsGeographicPositionComponent,
+ StatementDetailsInformationComponent,
+ StatementDetailsLinkedStatementsComponent,
+ StatementDetailsOutboxComponent,
+ LinkedStatementsComponent,
+ StatementDetailsConsiderationsComponent
],
exports: [
StatementDetailsComponent,
@@ -68,7 +94,15 @@
ProcessInformationComponent,
BpmnDirective,
StatementDetailsSideMenuComponent,
- GetProcessHistoryEntriesPipe
+ GetProcessHistoryEntriesPipe,
+ StatementDetailsAttachmentsComponent,
+ StatementDetailsContributionsComponent,
+ StatementDetailsGeographicPositionComponent,
+ StatementDetailsInformationComponent,
+ StatementDetailsLinkedStatementsComponent,
+ StatementDetailsOutboxComponent,
+ LinkedStatementsComponent,
+ StatementDetailsConsiderationsComponent
]
})
export class StatementDetailsModule {
diff --git a/src/app/features/forms/attachments/attachments-form.module.ts b/src/app/features/forms/attachments/attachments-form.module.ts
index 3d0cd18..fdab4e6 100644
--- a/src/app/features/forms/attachments/attachments-form.module.ts
+++ b/src/app/features/forms/attachments/attachments-form.module.ts
@@ -21,7 +21,11 @@
import {FileDropModule} from "../../../shared/layout/file-drop";
import {SharedPipesModule} from "../../../shared/pipes";
import {AttachmentControlComponent, AttachmentListFormComponent, AttachmentsFormGroupComponent} from "./components";
+import {AttachmentDisplayListComponent} from "./components/attachment-display";
import {AttachmentFileDropFormComponent} from "./components/attachment-file-drop-form";
+import {GetEmailTextAttachmentPipe} from "./pipes/get-email-text-attachment.pipe";
+import {GetMailAttachmentsPipe} from "./pipes/get-mail-attachments.pipe";
+
@NgModule({
imports: [
@@ -39,13 +43,19 @@
AttachmentsFormGroupComponent,
AttachmentControlComponent,
AttachmentFileDropFormComponent,
- AttachmentListFormComponent
+ AttachmentListFormComponent,
+ GetEmailTextAttachmentPipe,
+ AttachmentDisplayListComponent,
+ GetMailAttachmentsPipe
],
exports: [
AttachmentsFormGroupComponent,
AttachmentControlComponent,
AttachmentFileDropFormComponent,
- AttachmentListFormComponent
+ AttachmentListFormComponent,
+ AttachmentDisplayListComponent,
+ GetEmailTextAttachmentPipe,
+ GetMailAttachmentsPipe
]
})
export class AttachmentsFormModule {
diff --git a/src/app/features/forms/attachments/components/attachment-control/attachment-control.component.html b/src/app/features/forms/attachments/components/attachment-control/attachment-control.component.html
index dae9ae5..74a3bb7 100644
--- a/src/app/features/forms/attachments/components/attachment-control/attachment-control.component.html
+++ b/src/app/features/forms/attachments/components/attachment-control/attachment-control.component.html
@@ -57,14 +57,14 @@
<button (click)="toggleTag(tag?.id, appValue?.tagIds?.indexOf(tag?.id) > - 1)"
*ngIf="!appHideNotUsedTags || appValue?.tagIds?.length > 0 && appValue.tagIds.indexOf(tag?.id) > -1"
[class.openk-info]="appValue?.tagIds?.length > 0 && appValue.tagIds.indexOf(tag?.id) > -1"
- [disabled]="appDisabled"
+ [disabled]="appDisabled || !appEditableTags"
class="openk-button openk-chip">
{{tag?.label}}
</button>
</ng-container>
<button (click)="appHideNotUsedTags = !appHideNotUsedTags"
- *ngIf="appTagList?.length > 0"
+ *ngIf="appTagList?.length > 0 && appEditableTags"
[class.tag-toggle---plus]="appHideNotUsedTags"
[disabled]="appDisabled"
class="openk-button openk-button-rounded tag-toggle"
diff --git a/src/app/features/forms/attachments/components/attachment-control/attachment-control.component.ts b/src/app/features/forms/attachments/components/attachment-control/attachment-control.component.ts
index dbd4e7b..926da4f 100644
--- a/src/app/features/forms/attachments/components/attachment-control/attachment-control.component.ts
+++ b/src/app/features/forms/attachments/components/attachment-control/attachment-control.component.ts
@@ -58,6 +58,9 @@
@Input()
public appHideNotUsedTags: boolean;
+ @Input()
+ public appEditableTags = true;
+
public toggleSelection(isSelected: boolean) {
if (this.appDisabled) {
return;
diff --git a/src/app/features/forms/attachments/components/attachment-display/attachment-display-list.component.html b/src/app/features/forms/attachments/components/attachment-display/attachment-display-list.component.html
new file mode 100644
index 0000000..aa911c3
--- /dev/null
+++ b/src/app/features/forms/attachments/components/attachment-display/attachment-display-list.component.html
@@ -0,0 +1,31 @@
+<!-------------------------------------------------------------------------------
+ * 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
+ -------------------------------------------------------------------------------->
+
+<span class="title">
+ {{appTitle}}
+</span>
+<div class="attachment-list">
+ <app-attachment-control
+ (appDownloadAttachment)="appDownload.emit(att?.id)"
+ *ngFor="let att of appAttachments"
+ [appEditableTags]="false"
+ [appHideNotUsedTags]="true"
+ [appIsDownloadable]="true"
+ [appIsSelectable]="false"
+ [appTagList]="appTagList"
+ [appValue]="att"
+ class="attachment-list--control">
+ </app-attachment-control>
+ <span *ngIf="appAttachments?.length <= 0"
+ class="attachment-list--placeholder">{{"details.attachments.noResult" | translate}}</span>
+</div>
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss b/src/app/features/forms/attachments/components/attachment-display/attachment-display-list.component.scss
similarity index 71%
copy from src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
copy to src/app/features/forms/attachments/components/attachment-display/attachment-display-list.component.scss
index 4d2360d..bc6c2e4 100644
--- a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
+++ b/src/app/features/forms/attachments/components/attachment-display/attachment-display-list.component.scss
@@ -11,17 +11,21 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "../../../../styles/openk.styles";
+@import "openk.styles";
-:host {
- width: 100%;
+.title {
+ font-weight: 600;
}
-.statements {
- margin-bottom: 1em;
- display: grid;
+.attachment-list {
+ margin-top: 0.25em;
}
-.statements--titlebar {
- margin-bottom: 0.5em;
+.attachment-list--placeholder {
+ font-size: smaller;
+}
+
+.attachment-list--control {
+ box-sizing: border-box;
+ width: fit-content;
}
diff --git a/src/app/features/search/components/search/search.component.spec.ts b/src/app/features/forms/attachments/components/attachment-display/attachment-display-list.component.spec.ts
similarity index 62%
copy from src/app/features/search/components/search/search.component.spec.ts
copy to src/app/features/forms/attachments/components/attachment-display/attachment-display-list.component.spec.ts
index ec7b1b3..7db6bac 100644
--- a/src/app/features/search/components/search/search.component.spec.ts
+++ b/src/app/features/forms/attachments/components/attachment-display/attachment-display-list.component.spec.ts
@@ -12,20 +12,25 @@
********************************************************************************/
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
-import {SearchComponent} from "./search.component";
+import {I18nModule} from "../../../../../core/i18n";
+import {AttachmentsFormModule} from "../../attachments-form.module";
+import {AttachmentDisplayListComponent} from "./attachment-display-list.component";
-describe("SearchComponent", () => {
- let component: SearchComponent;
- let fixture: ComponentFixture<SearchComponent>;
+describe("AttachmentDisplayListComponent", () => {
+ let component: AttachmentDisplayListComponent;
+ let fixture: ComponentFixture<AttachmentDisplayListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
- declarations: [SearchComponent]
+ imports: [
+ I18nModule,
+ AttachmentsFormModule
+ ]
}).compileComponents();
}));
beforeEach(() => {
- fixture = TestBed.createComponent(SearchComponent);
+ fixture = TestBed.createComponent(AttachmentDisplayListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -33,4 +38,5 @@
it("should create", () => {
expect(component).toBeTruthy();
});
+
});
diff --git a/src/app/features/forms/attachments/components/attachment-display/attachment-display-list.component.stories.ts b/src/app/features/forms/attachments/components/attachment-display/attachment-display-list.component.stories.ts
new file mode 100644
index 0000000..5c0eeff
--- /dev/null
+++ b/src/app/features/forms/attachments/components/attachment-display/attachment-display-list.component.stories.ts
@@ -0,0 +1,33 @@
+/********************************************************************************
+ * 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 {withKnobs} from "@storybook/addon-knobs";
+import {moduleMetadata, storiesOf} from "@storybook/angular";
+import {I18nModule} from "../../../../../core/i18n";
+import {AttachmentsFormModule} from "../../attachments-form.module";
+
+storiesOf("Features / Forms / Attachments", module)
+ .addDecorator(withKnobs)
+ .addDecorator(moduleMetadata({
+ imports: [
+ I18nModule,
+ AttachmentsFormModule
+ ]
+ }))
+ .add("AttachmentDisplayListComponent", () => ({
+ template: `
+ <app-attachment-display-list style="margin: 1em; box-sizing: border-box">
+ </app-attachment-display-list>
+ `,
+ props: {}
+ }));
diff --git a/src/app/features/forms/attachments/components/attachment-display/attachment-display-list.component.ts b/src/app/features/forms/attachments/components/attachment-display/attachment-display-list.component.ts
new file mode 100644
index 0000000..fe9ca5d
--- /dev/null
+++ b/src/app/features/forms/attachments/components/attachment-display/attachment-display-list.component.ts
@@ -0,0 +1,37 @@
+/********************************************************************************
+ * 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 {IAPIAttachmentTag} from "../../../../../core/api/attachments";
+import {IAttachmentControlValue} from "../../../../../store/attachments/model";
+
+@Component({
+ selector: "app-attachment-display-list",
+ templateUrl: "./attachment-display-list.component.html",
+ styleUrls: ["./attachment-display-list.component.scss"]
+})
+export class AttachmentDisplayListComponent {
+
+ @Input()
+ public appTitle: string;
+
+ @Input()
+ public appAttachments: IAttachmentControlValue[] = [];
+
+ @Input()
+ public appTagList: IAPIAttachmentTag[] = [];
+
+ @Output()
+ public appDownload = new EventEmitter<number>();
+
+}
diff --git a/src/app/features/search/components/search/index.ts b/src/app/features/forms/attachments/components/attachment-display/index.ts
similarity index 91%
copy from src/app/features/search/components/search/index.ts
copy to src/app/features/forms/attachments/components/attachment-display/index.ts
index 5eb9c7f..497d713 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/features/forms/attachments/components/attachment-display/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./attachment-display-list.component";
diff --git a/src/app/features/forms/attachments/components/attachment-file-drop-form/attachment-file-drop-form.component.html b/src/app/features/forms/attachments/components/attachment-file-drop-form/attachment-file-drop-form.component.html
index 004e315..7013ed7 100644
--- a/src/app/features/forms/attachments/components/attachment-file-drop-form/attachment-file-drop-form.component.html
+++ b/src/app/features/forms/attachments/components/attachment-file-drop-form/attachment-file-drop-form.component.html
@@ -13,7 +13,7 @@
<ng-container [formGroup]="appFormGroup">
- <label>
+ <label class="file-drop--title">
{{appTitle}}
</label>
@@ -32,17 +32,19 @@
[appTagList]="appTagList"
[formControlName]="i"
class="file-drop--control">
-
</app-attachment-control>
</app-file-drop>
- <button (click)="fileDropComponent.openDialog()"
- [disabled]="fileDropComponent.appDisabled"
- class="openk-button attachments--select-file-button">
- <mat-icon>attach_file</mat-icon>
- {{"attachments.selectFile" | translate }}
- </button>
+ <div class="attachments--select-file-button">
+ <button (click)="fileDropComponent.openDialog()"
+ [disabled]="fileDropComponent.appDisabled"
+ class="openk-button">
+ <mat-icon class="attachments--select-file-button--icon">attach_file</mat-icon>
+ {{"attachments.selectFile" | translate }}
+ </button>
+ <ng-content></ng-content>
+ </div>
</ng-container>
diff --git a/src/app/features/forms/attachments/components/attachment-file-drop-form/attachment-file-drop-form.component.scss b/src/app/features/forms/attachments/components/attachment-file-drop-form/attachment-file-drop-form.component.scss
index 0383056..ff18d7d 100644
--- a/src/app/features/forms/attachments/components/attachment-file-drop-form/attachment-file-drop-form.component.scss
+++ b/src/app/features/forms/attachments/components/attachment-file-drop-form/attachment-file-drop-form.component.scss
@@ -23,6 +23,10 @@
margin: 0.25em 0 0.5em 0;
}
+.file-drop--title {
+ font-weight: 600;
+}
+
.file-drop--control {
font-size: small;
width: 100%;
@@ -40,3 +44,9 @@
.attachments--select-file-button {
margin-left: auto;
}
+
+.attachments--select-file-button--icon {
+ height: initial;
+ width: initial;
+ font-size: 1em;
+}
diff --git a/src/app/features/forms/attachments/components/attachment-file-drop-form/attachment-file-drop-form.component.ts b/src/app/features/forms/attachments/components/attachment-file-drop-form/attachment-file-drop-form.component.ts
index 50aba7b..b400e9c 100644
--- a/src/app/features/forms/attachments/components/attachment-file-drop-form/attachment-file-drop-form.component.ts
+++ b/src/app/features/forms/attachments/components/attachment-file-drop-form/attachment-file-drop-form.component.ts
@@ -37,7 +37,7 @@
public appFormGroup = createAttachmentForm();
@Input()
- public appFormArrayName: keyof IAttachmentFormValue = "add";
+ public appFormArrayName: keyof IAttachmentFormValue | "considerations" = "add";
public addControlForFiles(files: File[]) {
arrayJoin(files).forEach((file) => {
diff --git a/src/app/features/forms/attachments/components/attachments-form-group.component.scss b/src/app/features/forms/attachments/components/attachments-form-group.component.scss
index 6713a69..5ba0f61 100644
--- a/src/app/features/forms/attachments/components/attachments-form-group.component.scss
+++ b/src/app/features/forms/attachments/components/attachments-form-group.component.scss
@@ -23,19 +23,19 @@
.attachments {
display: flex;
flex-direction: row;
+ padding: 1em;
}
.attachments--files {
- padding: 0.5em;
box-sizing: border-box;
overflow: auto;
flex: 1;
}
.attachments--email {
- padding: 0.5em;
box-sizing: border-box;
flex: 1;
+ margin-right: 0.5em;
}
.attachments---half-size {
diff --git a/src/app/features/forms/attachments/components/attachments-form-group.component.ts b/src/app/features/forms/attachments/components/attachments-form-group.component.ts
index 69753f1..5508298 100644
--- a/src/app/features/forms/attachments/components/attachments-form-group.component.ts
+++ b/src/app/features/forms/attachments/components/attachments-form-group.component.ts
@@ -11,10 +11,10 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges} from "@angular/core";
+import {Component, Input, OnDestroy, OnInit} from "@angular/core";
import {select, Store} from "@ngrx/store";
-import {BehaviorSubject, combineLatest} from "rxjs";
-import {delay, filter, switchMap, take, takeUntil} from "rxjs/operators";
+import {combineLatest, Observable} from "rxjs";
+import {delay, filter, take, takeUntil} from "rxjs/operators";
import {AUTO_SELECTED_TAGS, IAPIAttachmentModel} from "../../../../core/api/attachments";
import {IAPIEmailAttachmentModel, IAPIEmailModel} from "../../../../core/api/mail";
import {
@@ -25,6 +25,7 @@
getAttachmentControlValueSelector,
getFilteredAttachmentTagsSelector,
getStatementAttachmentCacheSelector,
+ IAttachmentControlValue,
IAttachmentFormValue,
queryParamsIdSelector,
startAttachmentDownloadAction
@@ -33,6 +34,7 @@
import {getSelectedEmailSelector, getStatementMailSelector} from "../../../../store/mail/selectors";
import {arrayJoin} from "../../../../util/store";
import {AbstractReactiveFormComponent} from "../../abstract";
+import {getMailAttachment, getMailAttachments} from "../util/mail-attachments.util";
@Component({
selector: "app-attachments-form-group",
@@ -41,16 +43,16 @@
})
export class AttachmentsFormGroupComponent
extends AbstractReactiveFormComponent<IAttachmentFormValue>
- implements OnInit, OnChanges, OnDestroy {
+ implements OnInit, OnDestroy {
@Input()
public appAutoTagIds: string[];
@Input()
- public appForbiddenTagIds: string[];
+ public appForbiddenTagIds: string[] = [];
@Input()
- public appRestrictedTagIds: string[];
+ public appRestrictedTagIds: string[] = [];
@Input()
public appWithoutTagControl: boolean;
@@ -78,7 +80,7 @@
public statementMail$ = this.store.pipe(select(getStatementMailSelector));
- public attachments$ = this.store.pipe(select(getAttachmentControlValueSelector, {}));
+ public attachments$: Observable<IAttachmentControlValue[]>;
public allAttachments$ = this.store.pipe(select(getAllStatementAttachments));
@@ -86,20 +88,15 @@
public tagList$ = this.store.pipe(select(getFilteredAttachmentTagsSelector, {without: AUTO_SELECTED_TAGS}));
- private attachmentProps$ = new BehaviorSubject<{ restrictedTagIds: string[], forbiddenTagIds: string[] }>({
- restrictedTagIds: [],
- forbiddenTagIds: []
- });
-
public constructor(public store: Store) {
super();
}
public ngOnInit() {
- this.attachments$ = this.attachmentProps$.pipe(
- switchMap((props) => this.store.pipe(
- select(getAttachmentControlValueSelector, props)
- ))
+
+ this.attachments$ = this.store.pipe(
+ select(getAttachmentControlValueSelector,
+ {forbiddenTagIds: this.appForbiddenTagIds, restrictedTagIds: this.appRestrictedTagIds})
);
this.attachments$.pipe(delay(0), takeUntil(this.destroy$))
@@ -121,16 +118,6 @@
this.store.dispatch(fetchAttachmentTagsAction());
}
- public ngOnChanges(changes: SimpleChanges) {
- const keys: Array<keyof AttachmentsFormGroupComponent> = ["appRestrictedTagIds", "appForbiddenTagIds"];
- if (keys.some((_) => changes[_] != null)) {
- this.attachmentProps$.next({
- restrictedTagIds: this.appRestrictedTagIds,
- forbiddenTagIds: this.appForbiddenTagIds
- });
- }
- }
-
public ngOnDestroy() {
this.statementId$.pipe(take(1))
.subscribe((statementId) => this.store.dispatch(clearAttachmentCacheAction({statementId})));
@@ -151,28 +138,13 @@
}
public setMailTextAttachmentValue(attachments: IAPIAttachmentModel[], isNewStatement: boolean) {
- const mailTextAttachmentId = attachments.find((_) =>
- _.name === "mailText.txt" && _.tagIds && _.tagIds.length === 2
- && _.tagIds[0] === "email" && _.tagIds[1] === "email-text")?.id;
+ const mailTextAttachmentId = getMailAttachment(attachments)?.id;
this.appFormGroup.patchValue({mailTextAttachmentId, transferMailText: mailTextAttachmentId != null || isNewStatement});
}
public setMailAttachmentValues(attachments: IAPIAttachmentModel[], emailAttachments: IAPIEmailAttachmentModel[]) {
- const mergedAttachmentLists = emailAttachments.map((emailAttachment) => {
- const emailAttachmentFromAttachmentsArray = attachments.find((attachment) =>
- attachment.name === emailAttachment.name && attachment.tagIds.find((_) => _ === "email") != null);
-
- const isSelected = emailAttachmentFromAttachmentsArray != null || this.appForNewStatement;
-
- const tagIds = emailAttachmentFromAttachmentsArray?.tagIds;
-
- return {
- ...(emailAttachmentFromAttachmentsArray ? emailAttachmentFromAttachmentsArray : emailAttachment),
- isSelected,
- tagIds: arrayJoin(tagIds)
- };
- });
+ const mergedAttachmentLists = getMailAttachments(attachments, emailAttachments, this.appForNewStatement);
this.setValueForArray(mergedAttachmentLists, "email");
}
diff --git a/src/app/features/forms/attachments/pipes/get-email-text-attachment.pipe.spec.ts b/src/app/features/forms/attachments/pipes/get-email-text-attachment.pipe.spec.ts
new file mode 100644
index 0000000..f5c3890
--- /dev/null
+++ b/src/app/features/forms/attachments/pipes/get-email-text-attachment.pipe.spec.ts
@@ -0,0 +1,50 @@
+/********************************************************************************
+ * 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 {IAPIAttachmentModel} from "../../../../core/api/attachments";
+import {IAPIEmailModel} from "../../../../core/api/mail";
+import {GetEmailTextAttachmentPipe} from "./get-email-text-attachment.pipe";
+
+describe("GetEmailTextAttachmentPipe", () => {
+
+ const pipe = new GetEmailTextAttachmentPipe();
+
+ describe("transform", () => {
+
+ it("should return the mail text attachment with name set to mail subject", () => {
+
+ const mail: IAPIEmailModel = {
+ subject: "subject",
+ from: "from"
+ } as IAPIEmailModel;
+ const attachments: IAPIAttachmentModel[] = [
+ {name: "attachment1", tagIds: []},
+ {name: "attachment2", tagIds: []},
+ {name: "mailText.txt", tagIds: ["email", "email-text"]}
+ ] as IAPIAttachmentModel[];
+
+ const mailTextAttachment = pipe.transform(attachments, mail);
+ expect(mailTextAttachment).toEqual({...attachments[2], name: `"${mail.subject}" (${mail.from})`});
+
+ });
+
+ it("should return null if no mail text attachment was found", () => {
+ let mailTextAttachment = pipe.transform(null, null);
+ expect(mailTextAttachment).toEqual(null);
+
+ mailTextAttachment = pipe.transform([], null);
+ expect(mailTextAttachment).toEqual(null);
+ });
+ });
+});
+
diff --git a/src/app/features/forms/attachments/pipes/get-email-text-attachment.pipe.ts b/src/app/features/forms/attachments/pipes/get-email-text-attachment.pipe.ts
new file mode 100644
index 0000000..28beec6
--- /dev/null
+++ b/src/app/features/forms/attachments/pipes/get-email-text-attachment.pipe.ts
@@ -0,0 +1,37 @@
+/********************************************************************************
+ * 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 {IAPIAttachmentModel} from "../../../../core/api/attachments";
+import {IAPIEmailModel} from "../../../../core/api/mail";
+import {getMailAttachment} from "../util/mail-attachments.util";
+
+@Pipe({
+ name: "appGetEmailTextAttachment"
+})
+export class GetEmailTextAttachmentPipe implements PipeTransform {
+
+ public transform(attachments: IAPIAttachmentModel[], mail: IAPIEmailModel): IAPIAttachmentModel {
+ const mailTextAttachment = getMailAttachment(attachments);
+
+ if (!mailTextAttachment) {
+ return null;
+ }
+ const name = mail ? `"${mail.subject}" (${mail.from})` : mailTextAttachment.name;
+ return {
+ ...mailTextAttachment,
+ name
+ };
+ }
+
+}
diff --git a/src/app/features/forms/attachments/pipes/get-mail-attachments.pipe.spec.ts b/src/app/features/forms/attachments/pipes/get-mail-attachments.pipe.spec.ts
new file mode 100644
index 0000000..e4057fd
--- /dev/null
+++ b/src/app/features/forms/attachments/pipes/get-mail-attachments.pipe.spec.ts
@@ -0,0 +1,50 @@
+/********************************************************************************
+ * 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 {IAPIAttachmentModel} from "../../../../core/api/attachments";
+import {IAPIEmailAttachmentModel} from "../../../../core/api/mail";
+import {GetMailAttachmentsPipe} from "./get-mail-attachments.pipe";
+
+describe("GetMailAttachmentsPipe", () => {
+
+ const pipe = new GetMailAttachmentsPipe();
+
+ describe("transform", () => {
+
+ it("should return an empty array if called with empty lists", () => {
+ let result = pipe.transform(null, null);
+ expect(result).toEqual([]);
+ result = pipe.transform([], []);
+ expect(result).toEqual([]);
+ });
+
+ it("should return list of mail attachments with selected being the ones already in statement attachments", () => {
+ const attachments: IAPIAttachmentModel[] = [
+ {name: "attachment1", tagIds: []},
+ {name: "attachment2", tagIds: ["email"]},
+ {name: "mailText.txt", tagIds: ["email", "email-text"]}
+ ] as IAPIAttachmentModel[];
+ const emailAttachments: IAPIEmailAttachmentModel[] = [
+ {name: "attachment2"},
+ {name: "attachment3"}
+ ] as IAPIEmailAttachmentModel[];
+ const result = pipe.transform(attachments, emailAttachments);
+ expect(result).toEqual([
+ {name: "attachment2", tagIds: ["email"], isSelected: true},
+ {name: "attachment3", tagIds: [], isSelected: false}
+ ]);
+ });
+ });
+});
+
diff --git a/src/app/features/forms/attachments/pipes/get-mail-attachments.pipe.ts b/src/app/features/forms/attachments/pipes/get-mail-attachments.pipe.ts
new file mode 100644
index 0000000..6da6c31
--- /dev/null
+++ b/src/app/features/forms/attachments/pipes/get-mail-attachments.pipe.ts
@@ -0,0 +1,28 @@
+/********************************************************************************
+ * 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 {IAPIAttachmentModel} from "../../../../core/api/attachments";
+import {IAPIEmailAttachmentModel} from "../../../../core/api/mail";
+import {getMailAttachments} from "../util/mail-attachments.util";
+
+@Pipe({
+ name: "appGetMailAttachments"
+})
+export class GetMailAttachmentsPipe implements PipeTransform {
+
+ public transform(attachments: IAPIAttachmentModel[], emailAttachments: IAPIEmailAttachmentModel[], isNewStatement?: boolean) {
+ return getMailAttachments(attachments, emailAttachments, isNewStatement);
+ }
+
+}
diff --git a/src/app/features/forms/attachments/util/mail-attachments.util.ts b/src/app/features/forms/attachments/util/mail-attachments.util.ts
new file mode 100644
index 0000000..9836228
--- /dev/null
+++ b/src/app/features/forms/attachments/util/mail-attachments.util.ts
@@ -0,0 +1,52 @@
+/********************************************************************************
+ * 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 {IAPIAttachmentModel} from "../../../../core/api/attachments";
+import {IAPIEmailAttachmentModel} from "../../../../core/api/mail";
+import {IAttachmentControlValue} from "../../../../store/attachments/model";
+import {arrayJoin} from "../../../../util/store";
+
+/**
+ * Given the list of statement attachments and the list of the statement attachments of an email,
+ * this function returns a list of the attachment objects to the email attachment with tag "email".
+ * isSelected is set if email attachment is already in the list of statement attachments or for a new statement. (default value)
+ */
+export function getMailAttachments(
+ statementAttachments: IAttachmentControlValue[], emailAttachments: IAPIEmailAttachmentModel[], isNewStatement: boolean = false) {
+ return arrayJoin(emailAttachments).map((emailAttachment) => {
+ const emailAttachmentFromAttachmentsArray = arrayJoin(statementAttachments).find((attachment) =>
+ attachment.name === emailAttachment.name && attachment.tagIds.find((_) => _ === "email") != null);
+
+ const isSelected = (emailAttachmentFromAttachmentsArray != null) || isNewStatement;
+
+ const tagIds = emailAttachmentFromAttachmentsArray?.tagIds;
+
+ return {
+ ...(emailAttachmentFromAttachmentsArray ? emailAttachmentFromAttachmentsArray : emailAttachment),
+ isSelected,
+ tagIds: arrayJoin(tagIds)
+ };
+ });
+}
+
+/**
+ * Returns the email text attachment from the list of attachments.
+ */
+export function getMailAttachment(attachments: IAPIAttachmentModel[]): IAPIAttachmentModel {
+ return arrayJoin(attachments).find((_) =>
+ _.name === "mailText.txt" &&
+ _.tagIds.length === 2 &&
+ _.tagIds.find((tagId) => tagId === "email-text") != null &&
+ _.tagIds.find((tagId) => tagId === "email") != null
+ );
+}
diff --git a/src/app/features/forms/comments/components/comments-form/comments-form.component.ts b/src/app/features/forms/comments/components/comments-form/comments-form.component.ts
index a5a380c..af647cd 100644
--- a/src/app/features/forms/comments/components/comments-form/comments-form.component.ts
+++ b/src/app/features/forms/comments/components/comments-form/comments-form.component.ts
@@ -28,7 +28,7 @@
export class CommentsFormComponent {
@Input()
- public appCollapsed: boolean;
+ public appCollapsed = false;
@Input()
public appCommentsToShow = 5;
diff --git a/src/app/features/forms/statement-editor/components/arrangement-form-group/arrangement-form-group.component.html b/src/app/features/forms/statement-editor/components/arrangement-form-group/arrangement-form-group.component.html
index 6e8ffc0..774dc97 100644
--- a/src/app/features/forms/statement-editor/components/arrangement-form-group/arrangement-form-group.component.html
+++ b/src/app/features/forms/statement-editor/components/arrangement-form-group/arrangement-form-group.component.html
@@ -14,7 +14,7 @@
<mat-drawer-container [formGroup]="appFormGroup"
[hasBackdrop]="false"
class="editor--arrangement">
- <mat-drawer-content class="editor--arangement--content">
+ <mat-drawer-content cdkScrollable class="editor--arangement--content">
<div #cdkDropListRef="cdkDropList"
(cdkDropListDropped)="drop($event)"
[cdkDropListConnectedTo]="textBlockSelectComponent.dropList"
diff --git a/src/app/features/forms/statement-editor/components/statement-editor-form/statement-editor-form.component.html b/src/app/features/forms/statement-editor/components/statement-editor-form/statement-editor-form.component.html
index 754c9a8..fa2ac29 100644
--- a/src/app/features/forms/statement-editor/components/statement-editor-form/statement-editor-form.component.html
+++ b/src/app/features/forms/statement-editor/components/statement-editor-form/statement-editor-form.component.html
@@ -32,19 +32,36 @@
[appUserRoles]="userRoles$ | async">
</app-statement-editor-side-menu>
+<app-statement-details-information
+ [appContactInfo]="(statement$ | async)?.contactInfo"
+ [appStatementInfo]="(statement$ | async)?.info"
+ [appCollapsed]="true"
+ class="statement-details">
+</app-statement-details-information>
+
+<app-statement-details-attachments
+ [appCollapsed]="true"
+ class="statement-details">
+</app-statement-details-attachments>
+
<app-collapsible
- *ngIf="showContributions$ | async"
[appTitle]="('statementEditorForm.container.contributionStatus' | translate)
- + ' (' + (selectedContributionsCount$ | async) + '/' + (requiredContributionOptions$ | async).length + ')'"
+ + ' (' + (selectedContributionsCount$ | async) + '/' + (requiredContributionOptions$ | async)?.length + ')'"
[formGroup]="appFormGroup">
<app-select-group
+ *ngIf="(showContributions$ | async) && (requiredContributionOptions$ | async)?.length > 0"
[appGroups]="requiredContributionGroups$ | async"
[appOptions]="requiredContributionOptions$ | async"
[formControlName]="'contributions'"
style="padding: 1em;">
</app-select-group>
+ <div *ngIf="!((showContributions$ | async) && (requiredContributionOptions$ | async)?.length > 0)"
+ class="placeholder">
+ {{"statementEditorForm.contributions.placeholder" | translate}}
+ </div>
+
</app-collapsible>
<app-collapsible
diff --git a/src/app/features/forms/statement-editor/components/statement-editor-form/statement-editor-form.component.scss b/src/app/features/forms/statement-editor/components/statement-editor-form/statement-editor-form.component.scss
index a0ee48e..29e28d3 100644
--- a/src/app/features/forms/statement-editor/components/statement-editor-form/statement-editor-form.component.scss
+++ b/src/app/features/forms/statement-editor/components/statement-editor-form/statement-editor-form.component.scss
@@ -16,12 +16,19 @@
:host {
display: flex;
flex-flow: column;
+ max-width: 70em;
+ margin: 0 auto;
& > * {
margin-bottom: 1em;
}
}
+.placeholder {
+ padding: 1em;
+ font-style: italic;
+}
+
.pdf-overlay {
position: absolute;
top: 0;
diff --git a/src/app/features/forms/statement-editor/components/statement-editor-form/statement-editor-form.component.ts b/src/app/features/forms/statement-editor/components/statement-editor-form/statement-editor-form.component.ts
index 8919b3a..683b3b7 100644
--- a/src/app/features/forms/statement-editor/components/statement-editor-form/statement-editor-form.component.ts
+++ b/src/app/features/forms/statement-editor/components/statement-editor-form/statement-editor-form.component.ts
@@ -14,7 +14,7 @@
import {Component, OnDestroy, OnInit} from "@angular/core";
import {FormArray} from "@angular/forms";
import {select, Store} from "@ngrx/store";
-import {defer, Observable} from "rxjs";
+import {combineLatest, defer, Observable} from "rxjs";
import {filter, map, skip, switchMap, take, takeUntil} from "rxjs/operators";
import {
EAPIProcessTaskDefinitionKey,
@@ -34,6 +34,7 @@
getStatementStaticTextReplacementsSelector,
getStatementTextBlockGroupsForCurrentTaskSelector,
IStatementEditorFormValue,
+ IStatementEntity,
IStatementErrorEntity,
queryParamsIdSelector,
requiredContributionsGroupsSelector,
@@ -41,6 +42,7 @@
setErrorAction,
statementFileSelector,
statementLoadingSelector,
+ statementSelector,
submitStatementEditorFormAction,
taskSelector,
updateStatementEntityAction,
@@ -71,6 +73,8 @@
public statementId$ = this.store.pipe(select(queryParamsIdSelector));
+ public statement$: Observable<IStatementEntity> = this.store.pipe(select(statementSelector));
+
public showContributions$ = defer(() => this.task$).pipe(
map((task) => task?.taskDefinitionKey),
map((taskDefinitionKey) => {
@@ -123,13 +127,13 @@
public async ngOnInit() {
this.updateForm();
this.fetchTextArrangement();
- await this.deleteStatementFile();
+ this.deleteStatementFile();
}
- public async ngOnDestroy() {
+ public ngOnDestroy() {
super.ngOnDestroy();
- await this.deleteStatementFile();
- await this.clearErrors();
+ this.deleteStatementFile();
+ this.clearErrors();
}
public setArrangementErrors(errors: IAPITextArrangementErrorModel[]) {
@@ -199,12 +203,13 @@
}
}
- private async deleteStatementFile() {
- const file = await this.file$.pipe(take(1)).toPromise();
- if (file != null) {
- const statementId = await this.statementId$.pipe(take(1)).toPromise();
+ private deleteStatementFile() {
+ combineLatest([this.statementId$, this.file$]).pipe(
+ take(1),
+ filter(([statementId, file]) => statementId != null && file != null)
+ ).subscribe(([statementId]) => {
this.store.dispatch(updateStatementEntityAction({statementId, entity: {file: null}}));
- }
+ });
}
private fetchTextArrangement() {
@@ -233,12 +238,16 @@
).subscribe((errors) => this.setArrangementErrors(errors));
}
- 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 clearErrors() {
+ combineLatest([this.task$, this.isStatementLoading$]).pipe(
+ take(1),
+ filter(([task, loading]) => task?.statementId != null && !loading)
+ ).subscribe(([task]) => {
+ this.store.dispatch(setErrorAction({
+ statementId: task.statementId,
+ error: null
+ }));
+ });
}
}
diff --git a/src/app/features/forms/statement-editor/statement-editor.module.ts b/src/app/features/forms/statement-editor/statement-editor.module.ts
index 31cd96b..2e14aea 100644
--- a/src/app/features/forms/statement-editor/statement-editor.module.ts
+++ b/src/app/features/forms/statement-editor/statement-editor.module.ts
@@ -27,6 +27,7 @@
import {SharedPipesModule} from "../../../shared/pipes";
import {TextBlockModule} from "../../../shared/text-block";
import {CombineBlockdataTextPipe} from "../../../shared/text-block/pipes/combine-blockdata-text/combine-blockdata-text.pipe";
+import {StatementDetailsModule} from "../../details";
import {AttachmentsFormModule} from "../attachments";
import {
ArrangementFormGroupComponent,
@@ -54,7 +55,8 @@
ActionButtonModule,
TranslateModule,
FilePreviewModule,
- SelectModule
+ SelectModule,
+ StatementDetailsModule
],
declarations: [
StatementEditorFormComponent,
diff --git a/src/app/features/forms/statement-information/components/general-information-form-group/general-information-form-group.component.html b/src/app/features/forms/statement-information/components/general-information-form-group/general-information-form-group.component.html
index b1c2361..4930e22 100644
--- a/src/app/features/forms/statement-information/components/general-information-form-group/general-information-form-group.component.html
+++ b/src/app/features/forms/statement-information/components/general-information-form-group/general-information-form-group.component.html
@@ -71,15 +71,15 @@
<div class="form-group-container form-group-container--sectors">
<ng-container
- *ngIf="(appSectors | sector: appFormGroup.value) != null">
+ *ngIf="(appSectors | sector: appFormGroup.value?.city : appFormGroup.value.district) != null">
<span class="form-group-container--sectors--label">
{{("shared.sectors.available" | translate)}}
</span>
<span class="form-group-container--sectors--label---italic">
- {{(appSectors | sector: appFormGroup.value)}}
+ {{(appSectors | sector: appFormGroup.value?.city : appFormGroup.value.district)}}
</span>
</ng-container>
- <span *ngIf="(appSectors | sector: appFormGroup.value ) == null"
+ <span *ngIf="(appSectors | sector: appFormGroup.value?.city : appFormGroup.value.district ) == null"
class="form-group-container--sectors--label">
{{"shared.sectors.none" | translate}}
</span>
diff --git a/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.html b/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.html
index a9dc6a3..9892565 100644
--- a/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.html
+++ b/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.html
@@ -45,7 +45,8 @@
[appEntries]="contactSearchContent$ | async"
[appIsLoading]="(contactLoading$ | async)?.searching || (contactLoading$ | async)?.fetching"
[appMessage]="'contacts.selectContact' | translate"
- [appPageSize]="(contactSearch$ | async)?.totalPages"
+ [appPageSize]="(contactSearch$ | async)?.size"
+ [appTotalPages]="(contactSearch$ | async)?.totalPages"
[appPage]="(contactSearch$ | async)?.number"
[formControlName]="'contactId'"
[appSearch]="initialSearchText"
diff --git a/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.spec.ts b/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.spec.ts
index b4429f0..dc19e55 100644
--- a/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.spec.ts
+++ b/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.spec.ts
@@ -192,7 +192,7 @@
expect(dispatchSpy).toHaveBeenCalledWith(startContactSearchAction({options}));
options.page = 19;
- component.changePage(options.page);
+ component.changePage({page: options.page, size: options.size});
expect(dispatchSpy).toHaveBeenCalledWith(startContactSearchAction({options}));
});
diff --git a/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.ts b/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.ts
index 34c88c6..cb7e40d 100644
--- a/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.ts
+++ b/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.ts
@@ -115,6 +115,13 @@
this.appError$ = this.store.pipe(select(this.appForNewStatement ? getStatementErrorForNewSelector : getStatementErrorSelector));
+ this.statementMailId$.pipe(
+ takeUntil(this.destroy$)
+ ).subscribe(async (mId) => {
+ const statementId = (await this.task$.pipe(take(1)).toPromise())?.statementId;
+ this.store.dispatch(fetchEmailAction({mailId: mId, statementId}));
+ });
+
let mailId = await this.queryParamsMailId$.pipe(take(1)).toPromise();
if (!mailId) {
mailId = await this.statementMailId$.pipe(take(1)).toPromise();
@@ -122,17 +129,18 @@
this.mailId = mailId;
if (this.appForNewStatement) {
- await this.clearErrors();
+ this.clearErrors(true);
await this.setInitialValue();
this.store.dispatch(fetchSettingsAction());
- } else {
- this.appFormGroup.markAllAsTouched();
- }
-
- if (this.mailId) {
- await this.setEmailValues(mailId);
+ if (this.mailId) {
+ await this.setEmailValues(mailId);
+ this.appFormGroup.markAllAsTouched();
+ } else {
+ this.search("");
+ }
} else {
this.search("");
+ this.appFormGroup.markAllAsTouched();
}
this.updateForm();
@@ -141,15 +149,13 @@
this.value$.pipe(takeUntil(this.destroy$)).subscribe(async () => {
const errorMessage = await this.appError$.pipe(take(1)).toPromise();
if (this.appFormGroup.valid && errorMessage === EErrorCode.MISSING_FORM_DATA) {
- return this.clearErrors();
+ return this.clearErrors(true);
}
});
}
- public async ngOnDestroy() {
- if (!await this.isStatementLoading$.pipe(take(1)).toPromise()) {
- await this.clearErrors();
- }
+ public ngOnDestroy() {
+ this.clearErrors();
}
public openContactDataBaseModule() {
@@ -158,24 +164,30 @@
public search(searchText?: string) {
this.searchText = searchText;
- this.changePage(0);
+ this.changePage({page: 0, size: this.searchSize});
}
- public changePage(page: number) {
+ public changePage(newPage: { page: number, size: number }) {
+ this.searchSize = newPage?.size ? newPage.size : 10;
const options: IAPISearchOptions = {
q: this.searchText == null ? "" : this.searchText,
- page: page == null ? 0 : page,
+ page: newPage?.page == null ? 0 : newPage.page,
size: this.searchSize
};
this.store.dispatch(startContactSearchAction({options}));
}
- public async clearErrors() {
- const task = await this.task$.pipe(take(1)).toPromise();
- this.store.dispatch(setErrorAction({
- statementId: this.appForNewStatement ? "new" : task?.statementId,
- error: null
- }));
+ public clearErrors(force?: boolean) {
+ combineLatest([this.task$, this.isStatementLoading$]).pipe(
+ take(1),
+ filter(([task]) => this.appForNewStatement || task?.statementId != null),
+ filter(([task, loading]) => force || !loading)
+ ).subscribe(([task]) => {
+ this.store.dispatch(setErrorAction({
+ statementId: this.appForNewStatement ? "new" : task?.statementId,
+ error: null
+ }));
+ });
}
public async submit(responsible?: boolean) {
@@ -188,7 +200,7 @@
error: EErrorCode.MISSING_FORM_DATA
}));
} else {
- await this.clearErrors();
+ this.clearErrors(true);
return this.store.dispatch(submitStatementInformationFormAction(
this.appForNewStatement ? {
new: true,
@@ -282,7 +294,7 @@
).subscribe(async ([contactId, task]) => {
const errorMessage = (await this.appError$.pipe(take(1)).toPromise())?.errorMessage;
if (errorMessage === EErrorCode.FAILED_LOADING_CONTACT) {
- await this.clearErrors();
+ this.clearErrors(true);
}
const statementId = this.appForNewStatement ? "new" : task?.statementId;
this.store.dispatch(fetchContactDetailsAction({contactId, statementId}));
diff --git a/src/app/features/forms/statement-information/pipes/sector.pipe.spec.ts b/src/app/features/forms/statement-information/pipes/sector.pipe.spec.ts
index e53135a..d6be931 100644
--- a/src/app/features/forms/statement-information/pipes/sector.pipe.spec.ts
+++ b/src/app/features/forms/statement-information/pipes/sector.pipe.spec.ts
@@ -27,19 +27,19 @@
]
};
- let result = pipe.transform(sectors, {city: "", district: ""});
+ let result = pipe.transform(sectors, "", "");
expect(result).toEqual(undefined);
expect(result).toBeFalsy();
- result = pipe.transform(sectors, null);
+ result = pipe.transform(sectors, null, null);
expect(result).toEqual(undefined);
expect(result).toBeFalsy();
- result = pipe.transform(sectors, {city: "Stadt", district: "Straße"});
+ result = pipe.transform(sectors, "Stadt", "Straße");
expect(result).toEqual(undefined);
expect(result).toBeFalsy();
- result = pipe.transform(sectors, {city: "Ort", district: "Ortsteil"});
+ result = pipe.transform(sectors, "Ort", "Ortsteil");
expect(result).toEqual(" Strom, Gas, Beleuchtung");
});
});
diff --git a/src/app/features/forms/statement-information/pipes/sector.pipe.ts b/src/app/features/forms/statement-information/pipes/sector.pipe.ts
index b0d72f9..4ba7481 100644
--- a/src/app/features/forms/statement-information/pipes/sector.pipe.ts
+++ b/src/app/features/forms/statement-information/pipes/sector.pipe.ts
@@ -13,16 +13,15 @@
import {Pipe, PipeTransform} from "@angular/core";
import {IAPISectorsModel} from "../../../../core/api/statements/IAPISectorsModel";
-import {IStatementInformationFormValue} from "../../../../store/statements/model";
@Pipe({
name: "sector"
})
export class SectorPipe implements PipeTransform {
- transform(sectors: IAPISectorsModel, args?: Partial<IStatementInformationFormValue>): any {
+ transform(sectors: IAPISectorsModel, city: string, district: string): any {
- if (sectors && args?.city && args.district) {
- return sectors[args.city + "#" + args.district]?.map((_) => " " + _).toString();
+ if (sectors && city && district) {
+ return sectors[city + "#" + district]?.map((_) => " " + _).toString();
} else {
return undefined;
}
diff --git a/src/app/features/forms/workflow-data/components/workflow-data-form.component.html b/src/app/features/forms/workflow-data/components/workflow-data-form.component.html
index 657543a..d7c7f3b 100644
--- a/src/app/features/forms/workflow-data/components/workflow-data-form.component.html
+++ b/src/app/features/forms/workflow-data/components/workflow-data-form.component.html
@@ -22,27 +22,23 @@
<ng-container [formGroup]="appFormGroup">
- <app-collapsible
+ <app-statement-details-information
[appCollapsed]="true"
- [appTitle]="'workflowDataForm.container.general' | translate">
+ [appContactInfo]="(statement$ | async)?.contactInfo"
+ [appStatementInfo]="(statement$ | async)?.info"
+ class="statement-details">
+ </app-statement-details-information>
- <div style="padding: 1em;"> Not yet implemented.</div>
-
- </app-collapsible>
-
- <app-collapsible
+ <app-statement-details-attachments
[appCollapsed]="true"
- [appTitle]="'workflowDataForm.container.inboxAttachments' | translate">
-
- <div style="padding: 1em;"> Not yet implemented.</div>
-
- </app-collapsible>
+ class="statement-details">
+ </app-statement-details-attachments>
<app-collapsible
[appTitle]="'workflowDataForm.container.geographicPosition' | translate">
<app-map-select
- [appActionButtonLabel]="'shared.map.openGIS' | translate"
+ (appOpenGis)="openGis($event)"
[appCenter]="'leaflet.defaultView' | translate"
[formControlName]="'geographicPosition'"
class="geographic-position">
diff --git a/src/app/features/forms/workflow-data/components/workflow-data-form.component.scss b/src/app/features/forms/workflow-data/components/workflow-data-form.component.scss
index 467dba9..d14e2ee 100644
--- a/src/app/features/forms/workflow-data/components/workflow-data-form.component.scss
+++ b/src/app/features/forms/workflow-data/components/workflow-data-form.component.scss
@@ -16,6 +16,8 @@
:host {
display: block;
width: 100%;
+ max-width: 70em;
+ margin: 0 auto;
& > * {
margin-bottom: 1em;
@@ -31,6 +33,7 @@
}
.geographic-position {
+ box-sizing: border-box;
padding: 1em;
height: 30em;
}
diff --git a/src/app/features/forms/workflow-data/components/workflow-data-form.component.spec.ts b/src/app/features/forms/workflow-data/components/workflow-data-form.component.spec.ts
index 7964991..cedcdd3 100644
--- a/src/app/features/forms/workflow-data/components/workflow-data-form.component.spec.ts
+++ b/src/app/features/forms/workflow-data/components/workflow-data-form.component.spec.ts
@@ -16,19 +16,29 @@
import {RouterTestingModule} from "@angular/router/testing";
import {MockStore, provideMockStore} from "@ngrx/store/testing";
import {I18nModule, IAPISearchOptions} from "../../../../core";
-import {IWorkflowFormValue, startStatementSearchAction, submitWorkflowDataFormAction, taskSelector} from "../../../../store";
+import {ILeafletBounds} from "../../../../shared/leaflet";
+import {
+ IWorkflowFormValue,
+ openGisAction,
+ startStatementSearchAction,
+ submitWorkflowDataFormAction,
+ taskSelector,
+ userNameSelector
+} from "../../../../store";
import {WorkflowDataFormModule} from "../workflow-data-form.module";
import {WorkflowDataFormComponent} from "./workflow-data-form.component";
describe("WorkflowDataFormComponent", () => {
+ const user = "userName";
+
const initialState = {
statements: {},
process: {},
settings: {}
};
- let mockStore: MockStore;
+ let store: MockStore;
let component: WorkflowDataFormComponent;
let fixture: ComponentFixture<WorkflowDataFormComponent>;
@@ -40,14 +50,17 @@
RouterTestingModule
],
providers: [
- provideMockStore({initialState})
+ provideMockStore({
+ initialState,
+ selectors: [{selector: userNameSelector, value: user}]
+ })
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(WorkflowDataFormComponent);
- mockStore = fixture.componentRef.injector.get(MockStore);
+ store = fixture.componentRef.injector.get(MockStore);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -57,8 +70,8 @@
});
it("should dispatch submit workflow form action", async () => {
- mockStore.overrideSelector(taskSelector, {statementId: 1, taskId: "19"} as any);
- const dispatchSpy = spyOn(mockStore, "dispatch");
+ store.overrideSelector(taskSelector, {statementId: 1, taskId: "19"} as any);
+ const dispatchSpy = spyOn(store, "dispatch");
const value: IWorkflowFormValue = {
geographicPosition: "1919",
departments: {
@@ -86,10 +99,17 @@
});
it("should dispatch search statements action", () => {
- const dispatchSpy = spyOn(mockStore, "dispatch");
+ const dispatchSpy = spyOn(store, "dispatch");
const options: IAPISearchOptions = {q: ""};
component.search(options);
expect(dispatchSpy).toHaveBeenCalledWith(startStatementSearchAction({options}));
});
+ it("should open GIS", () => {
+ spyOn(store, "dispatch");
+ const bounds = {} as ILeafletBounds;
+ component.openGis(bounds);
+ expect(store.dispatch).toHaveBeenCalledWith(openGisAction({bounds, user}));
+ });
+
});
diff --git a/src/app/features/forms/workflow-data/components/workflow-data-form.component.ts b/src/app/features/forms/workflow-data/components/workflow-data-form.component.ts
index f119b47..e0b4122 100644
--- a/src/app/features/forms/workflow-data/components/workflow-data-form.component.ts
+++ b/src/app/features/forms/workflow-data/components/workflow-data-form.component.ts
@@ -11,26 +11,32 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {Component, OnDestroy, OnInit} from "@angular/core";
+import {Component, Inject, 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 {combineLatest, Observable, Subscription} from "rxjs";
+import {distinctUntilChanged, filter, take, takeUntil} from "rxjs/operators";
+import {APP_CONFIGURATION, IAPISearchOptions, IAppConfiguration} from "../../../../core";
+import {ILeafletBounds, latLngZoomToString} from "../../../../shared/leaflet";
import {
createWorkflowForm,
departmentGroupsSelector,
departmentOptionsSelector,
+ fetchStatementDetailsAction,
getSearchContentStatementsSelector,
getStatementErrorSelector,
+ IStatementEntity,
IStatementErrorEntity,
IWorkflowFormValue,
+ openGisAction,
queryParamsIdSelector,
setErrorAction,
startStatementSearchAction,
statementLoadingSelector,
+ statementSelector,
statementTypesSelector,
submitWorkflowDataFormAction,
taskSelector,
+ userNameSelector,
workflowFormValueSelector
} from "../../../../store";
import {AbstractReactiveFormComponent} from "../../abstract";
@@ -62,22 +68,37 @@
private form$ = this.store.pipe(select(workflowFormValueSelector));
- public constructor(public store: Store) {
+ public statement$: Observable<IStatementEntity> = this.store.pipe(select(statementSelector));
+
+ public userName$ = this.store.pipe(select(userNameSelector));
+
+ public subscription: Subscription;
+
+ private defaultGeographicPosition = latLngZoomToString(this.configuration.leaflet, this.configuration.leaflet.zoom);
+
+ public constructor(public store: Store, @Inject(APP_CONFIGURATION) public configuration: IAppConfiguration) {
super();
}
public async ngOnInit() {
this.updateForm();
this.task$.pipe(takeUntil(this.destroy$)).subscribe(() => this.search({q: ""}));
+ this.subscription = this.statementId$
+ .pipe(filter((statementId) => statementId != null))
+ .subscribe((statementId) => this.store.dispatch(fetchStatementDetailsAction({statementId})));
}
- public async ngOnDestroy() {
+ public ngOnDestroy() {
super.ngOnDestroy();
+ if (this.subscription != null) {
+ this.subscription.unsubscribe();
+ this.subscription = null;
+ }
return this.clearErrors();
}
public async submit(completeTask?: boolean) {
- await this.clearErrors();
+ this.clearErrors();
const task = await this.task$.pipe(take(1)).toPromise();
this.store.dispatch(submitWorkflowDataFormAction({
statementId: task.statementId,
@@ -91,18 +112,33 @@
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}));
- }
+ public openGis(bounds: ILeafletBounds) {
+ this.userName$.pipe(take(1)).subscribe((user) => {
+ this.store.dispatch(openGisAction({bounds, user}));
+ });
+ }
+
+ public clearErrors() {
+ combineLatest([this.task$, this.isStatementLoading$]).pipe(
+ take(1),
+ filter(([task, loading]) => task?.statementId != null && !loading)
+ ).subscribe(([task]) => {
+ this.store.dispatch(setErrorAction({
+ statementId: task.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));
+ this.form$.pipe(takeUntil(this.destroy$)).subscribe((value) => {
+ this.patchValue({
+ ...value,
+ geographicPosition: value.geographicPosition == null ? this.defaultGeographicPosition : value.geographicPosition,
+ });
+ });
}
diff --git a/src/app/features/forms/workflow-data/workflow-data-form.module.ts b/src/app/features/forms/workflow-data/workflow-data-form.module.ts
index d9c9f43..b03fc0e 100644
--- a/src/app/features/forms/workflow-data/workflow-data-form.module.ts
+++ b/src/app/features/forms/workflow-data/workflow-data-form.module.ts
@@ -22,6 +22,7 @@
import {ActionButtonModule} from "../../../shared/layout/action-button";
import {CollapsibleModule} from "../../../shared/layout/collapsible";
import {SideMenuModule} from "../../../shared/layout/side-menu";
+import {StatementDetailsModule} from "../../details";
import {WorkflowDataFormComponent, WorkflowDataSideMenuComponent} from "./components";
@NgModule({
@@ -36,7 +37,8 @@
StatementSelectModule,
SideMenuModule,
ActionButtonModule,
- MapSelectModule
+ MapSelectModule,
+ StatementDetailsModule
],
declarations: [
WorkflowDataFormComponent,
diff --git a/src/app/features/mail/components/mail-details/mail-details.component.scss b/src/app/features/mail/components/mail-details/mail-details.component.scss
index 4485712..7d5e8b8 100644
--- a/src/app/features/mail/components/mail-details/mail-details.component.scss
+++ b/src/app/features/mail/components/mail-details/mail-details.component.scss
@@ -44,7 +44,7 @@
.email--content--attachments--btn {
border: 0;
- padding: 0.1em 0 0.1em 0.5em;
+ margin: 0.1em 0 0.1em 0.5em;
&:not(.openk-info) {
background-color: transparent;
diff --git a/src/app/features/mail/components/mail/mail.component.html b/src/app/features/mail/components/mail/mail.component.html
index 0735fcf..c2ba35c 100644
--- a/src/app/features/mail/components/mail/mail.component.html
+++ b/src/app/features/mail/components/mail/mail.component.html
@@ -14,7 +14,7 @@
<app-mail-inbox
(appFetch)="fetchInbox()"
*appSideMenu="'top'; left: true; style: { padding: '0' }"
- [appLoading]="(loading$ | async)?.fetchingInbox"
+ [appLoading]="(loading$ | async) != null"
[appMails]="(emailInbox$ | async)"
[appSelectedMailId]="(selectedEmailId$ | async)">
</app-mail-inbox>
@@ -23,6 +23,7 @@
(appDownloadAttachment)="downloadAttachment($event?.mailId, $event?.name)"
(appRemoveFromInbox)="remove($event)"
[appEmail]="selectedEmail$ | async"
- [appDeleting]="(loading$ | async)?.deleting"
[appHideControls]="(loading$ | async) != null || (emailInbox$ | async)?.indexOf(selectedEmail$ | async) === -1">
</app-mail-details>
+
+
diff --git a/src/app/features/mail/components/mail/mail.component.ts b/src/app/features/mail/components/mail/mail.component.ts
index edd300e..714793f 100644
--- a/src/app/features/mail/components/mail/mail.component.ts
+++ b/src/app/features/mail/components/mail/mail.component.ts
@@ -14,8 +14,8 @@
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 {combineLatest, Subject} from "rxjs";
+import {distinctUntilChanged, filter, map, takeUntil, withLatestFrom} from "rxjs/operators";
import {
deleteEmailFromInboxAction,
downloadEmailAttachmentAction,
@@ -24,7 +24,6 @@
} 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",
@@ -37,7 +36,7 @@
public emailInbox$ = this.store.pipe(select(getEmailInboxSelector));
- public selectedEmailId$ = this.store.pipe(select(queryParamsMailIdSelector));
+ public selectedEmailId$ = this.store.pipe(select(queryParamsMailIdSelector)).pipe(distinctUntilChanged());
public selectedEmail$ = this.store.pipe(select(getSelectedEmailSelector));
@@ -52,36 +51,22 @@
public ngOnInit() {
this.fetchInbox();
+
+ // Automatically fetch selected emails which are not part of the inbox
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;
- })
- );
- }),
+ filter((mailId) => mailId != null),
+ withLatestFrom(this.emailInbox$),
+ filter(([mailId, inbox]) => !inbox.some((mail) => mail.identifier === mailId)),
takeUntil(this.destroy$)
- ).subscribe((mailId) => this.fetch(mailId));
+ ).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}});
- });
-
+ // Automatically navigate to the first inbox item if no email is selected
+ combineLatest([this.selectedEmailId$, this.emailInbox$]).pipe(
+ filter(([mailId]) => mailId == null),
+ map(([_, inbox]) => inbox[0]?.identifier),
+ filter((firstMailId) => firstMailId != null),
+ takeUntil(this.destroy$)
+ ).subscribe((firstMailId) => this.selectMail(firstMailId));
}
public ngOnDestroy() {
@@ -89,7 +74,7 @@
this.destroy$.complete();
}
- public async fetchInbox() {
+ public fetchInbox() {
this.store.dispatch(fetchEmailInboxAction());
}
@@ -102,7 +87,11 @@
}
public remove(mailId: string) {
- this.store.dispatch(deleteEmailFromInboxAction({mailId, navigateTo: "mail"}));
+ this.store.dispatch(deleteEmailFromInboxAction({mailId}));
+ }
+
+ public selectMail(mailId: string) {
+ return this.router.navigate([], {queryParams: {mailId}, replaceUrl: true});
}
}
diff --git a/src/app/features/navigation/components/nav-header/nav-header.component.ts b/src/app/features/navigation/components/nav-header/nav-header.component.ts
index b6c7282..9a927cd 100644
--- a/src/app/features/navigation/components/nav-header/nav-header.component.ts
+++ b/src/app/features/navigation/components/nav-header/nav-header.component.ts
@@ -57,10 +57,15 @@
},
{
icon: "find_in_page",
- link: "/search",
+ link: "/search/list",
tooltip: "core.header.search"
},
{
+ icon: "map",
+ link: "/search/map",
+ tooltip: "core.header.searchMap"
+ },
+ {
icon: "email",
link: "/mail",
tooltip: "core.header.mail",
diff --git a/src/app/features/search/components/date-filter/date-filter.component.html b/src/app/features/search/components/date-filter/date-filter.component.html
new file mode 100644
index 0000000..76fb14e
--- /dev/null
+++ b/src/app/features/search/components/date-filter/date-filter.component.html
@@ -0,0 +1,28 @@
+<!-------------------------------------------------------------------------------
+ * 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="date-filter">
+ <button
+ (click)="appActive = !appActive; appValueChange.emit(dueDateFromSelect.value)"
+ [class.openk-info]="appActive"
+ class="openk-button openk-chip date-filter--button">
+ {{appTitle}}
+ </button>
+ <div>
+ <app-date-control
+ #dueDateFromSelect
+ (appValueChange)="emitNewValue(dueDateFromSelect.value)"
+ class="openk-info">
+ </app-date-control>
+ </div>
+</div>
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss b/src/app/features/search/components/date-filter/date-filter.component.scss
similarity index 71%
copy from src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
copy to src/app/features/search/components/date-filter/date-filter.component.scss
index 4d2360d..1230d15 100644
--- a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
+++ b/src/app/features/search/components/date-filter/date-filter.component.scss
@@ -11,17 +11,17 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "../../../../styles/openk.styles";
+@import "openk.styles";
-:host {
- width: 100%;
+.date-filter {
+ display: flex;
+ align-items: center;
}
-.statements {
- margin-bottom: 1em;
- display: grid;
-}
-
-.statements--titlebar {
- margin-bottom: 0.5em;
+.date-filter--button {
+ margin-right: 0.5em;
+ height: 2em;
+ width: 11em;
+ font-size: small;
+ border-color: get-color($openk-info-palette, 500);
}
diff --git a/src/app/features/search/components/date-filter/date-filter.component.spec.ts b/src/app/features/search/components/date-filter/date-filter.component.spec.ts
new file mode 100644
index 0000000..af52b2a
--- /dev/null
+++ b/src/app/features/search/components/date-filter/date-filter.component.spec.ts
@@ -0,0 +1,51 @@
+/********************************************************************************
+ * 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 {I18nModule} from "../../../../core/i18n";
+import {SearchModule} from "../../search.module";
+import {DateFilterComponent} from "./date-filter.component";
+
+describe("DateFilterComponent", () => {
+ let component: DateFilterComponent;
+ let fixture: ComponentFixture<DateFilterComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ SearchModule,
+ I18nModule
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DateFilterComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should emit appValueChange only if filter is active", () => {
+ spyOn(component.appValueChange, "emit").and.callThrough();
+ const date = new Date();
+ component.emitNewValue(date);
+ expect(component.appValueChange.emit).not.toHaveBeenCalled();
+ component.appActive = true;
+ component.emitNewValue(date);
+ expect(component.appValueChange.emit).toHaveBeenCalledWith(date);
+ });
+});
diff --git a/src/app/features/search/components/date-filter/date-filter.component.ts b/src/app/features/search/components/date-filter/date-filter.component.ts
new file mode 100644
index 0000000..569c760
--- /dev/null
+++ b/src/app/features/search/components/date-filter/date-filter.component.ts
@@ -0,0 +1,38 @@
+/********************************************************************************
+ * 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";
+
+@Component({
+ selector: "app-date-filter",
+ templateUrl: "./date-filter.component.html",
+ styleUrls: ["./date-filter.component.scss"]
+})
+export class DateFilterComponent {
+
+ @Input()
+ public appActive: boolean;
+
+ @Input()
+ public appTitle: string;
+
+ @Output()
+ public appValueChange = new EventEmitter<Date>();
+
+ public emitNewValue(value: Date) {
+ if (this.appActive) {
+ this.appValueChange.emit(value);
+ }
+ }
+
+}
diff --git a/src/app/features/search/components/search/index.ts b/src/app/features/search/components/date-filter/index.ts
similarity index 93%
copy from src/app/features/search/components/search/index.ts
copy to src/app/features/search/components/date-filter/index.ts
index 5eb9c7f..8d5201f 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/features/search/components/date-filter/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./date-filter.component";
diff --git a/src/app/features/search/components/index.ts b/src/app/features/search/components/index.ts
index 990bb42..f868c39 100644
--- a/src/app/features/search/components/index.ts
+++ b/src/app/features/search/components/index.ts
@@ -11,4 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./search-filter";
+export * from "./search-statements";
+export * from "./position-search";
+export * from "./date-filter";
diff --git a/src/app/features/search/components/search/index.ts b/src/app/features/search/components/position-search/index.ts
similarity index 92%
copy from src/app/features/search/components/search/index.ts
copy to src/app/features/search/components/position-search/index.ts
index 5eb9c7f..16b6eaa 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/features/search/components/position-search/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./position-search.component";
diff --git a/src/app/features/search/components/position-search/position-search.component.html b/src/app/features/search/components/position-search/position-search.component.html
new file mode 100644
index 0000000..a934642
--- /dev/null
+++ b/src/app/features/search/components/position-search/position-search.component.html
@@ -0,0 +1,47 @@
+<!-------------------------------------------------------------------------------
+ * 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="search-filters">
+ <app-search-filter
+ (appValueChange)="search($event)"
+ [appFilters]="filtersToShow"
+ [appLoading]="statementLoading$ | async"
+ [appShowSearch]="false"
+ [appStatementTypeOptions]="statementTypeOptions$ | async">
+ </app-search-filter>
+</div>
+
+<app-leaflet-map (appCenterChange)="changeCenter($event)"
+ (appClick)="selected = null"
+ (appOpenGis)="openGis($event)"
+ (appPopupClose)="selected = null"
+ [appCenter]="coord$ | async"
+ class="search-map">
+
+ <ng-container *ngFor="let entry of searchContent$ | async; trackBy: trackById">
+ <ng-container *ngIf="entry?.position != null"
+ (appClick)="selected = entry"
+ [appLeafletMarker]="entry.position">
+ </ng-container>
+ </ng-container>
+
+ <a *appLeafletPopup="selected?.position; data: selected"
+ [queryParams]="{ id: selected?.id }"
+ [routerLink]="'/details'"
+ class="popup">
+ <ng-container *ngIf="selected != null">
+ ID {{selected.id}}: {{selected.title}}
+ </ng-container>
+ </a>
+
+</app-leaflet-map>
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss b/src/app/features/search/components/position-search/position-search.component.scss
similarity index 68%
copy from src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
copy to src/app/features/search/components/position-search/position-search.component.scss
index 4d2360d..155644f 100644
--- a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
+++ b/src/app/features/search/components/position-search/position-search.component.scss
@@ -11,17 +11,28 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "../../../../styles/openk.styles";
+@import "openk.styles";
:host {
- width: 100%;
+ display: flex;
+ flex-flow: column;
+ padding: 1em 1em 0.5em 1em;
+ min-height: 100%;
+ box-sizing: border-box;
}
-.statements {
- margin-bottom: 1em;
- display: grid;
-}
-
-.statements--titlebar {
+.search-filters {
+ display: flex;
+ flex-flow: row;
margin-bottom: 0.5em;
}
+
+.search-map {
+ width: 100%;
+ flex: 1 1 100%;
+}
+
+.popup {
+ line-height: 1;
+ display: inline-block;
+}
diff --git a/src/app/features/search/components/position-search/position-search.component.spec.ts b/src/app/features/search/components/position-search/position-search.component.spec.ts
new file mode 100644
index 0000000..eeec9c8
--- /dev/null
+++ b/src/app/features/search/components/position-search/position-search.component.spec.ts
@@ -0,0 +1,82 @@
+/********************************************************************************
+ * 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 {Router} from "@angular/router";
+import {RouterTestingModule} from "@angular/router/testing";
+import {MockStore, provideMockStore} from "@ngrx/store/testing";
+import {IAPIPositionSearchStatementModel} from "../../../../core";
+import {I18nModule} from "../../../../core/i18n";
+import {ILeafletBounds} from "../../../../shared/leaflet";
+import {openGisAction, startStatementPositionSearchAction, userNameSelector} from "../../../../store";
+import {SearchModule} from "../../search.module";
+import {PositionSearchComponent} from "./position-search.component";
+
+describe("PositionSearchComponent", () => {
+
+ const user = "userName";
+
+ let component: PositionSearchComponent;
+ let store: MockStore;
+ let router: Router;
+ let fixture: ComponentFixture<PositionSearchComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ SearchModule,
+ I18nModule,
+ RouterTestingModule
+ ],
+ providers: [provideMockStore({
+ selectors: [{selector: userNameSelector, value: user}]
+ })]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PositionSearchComponent);
+ component = fixture.componentInstance;
+ store = TestBed.inject(MockStore);
+ spyOn(store, "dispatch").and.callThrough();
+ router = TestBed.inject(Router);
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should start search after init", () => {
+ expect(store.dispatch).toHaveBeenCalledWith(startStatementPositionSearchAction({options: {}}));
+ });
+
+ it("should track by ID", () => {
+ expect(component.trackById(0, {...{} as IAPIPositionSearchStatementModel, id: 19})).toBe(19);
+ expect(component.trackById(0, {} as IAPIPositionSearchStatementModel)).not.toBeDefined();
+ expect(component.trackById(0, null)).not.toBeDefined();
+ });
+
+ it("should change center coordinates in url", () => {
+ spyOn(router, "navigate");
+ component.changeCenter("1,2,3");
+ expect(router.navigate).toHaveBeenCalledWith([], {queryParams: {coord: "1,2,3"}, replaceUrl: true});
+ });
+
+ it("should open GIS", () => {
+ const bounds = {} as ILeafletBounds;
+ component.openGis(bounds);
+ expect(store.dispatch).toHaveBeenCalledWith(openGisAction({bounds, user}));
+ });
+
+});
diff --git a/src/app/features/search/components/position-search/position-search.component.ts b/src/app/features/search/components/position-search/position-search.component.ts
new file mode 100644
index 0000000..9a4d1f1
--- /dev/null
+++ b/src/app/features/search/components/position-search/position-search.component.ts
@@ -0,0 +1,85 @@
+/********************************************************************************
+ * 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, OnInit} from "@angular/core";
+import {Router} from "@angular/router";
+import {select, Store} from "@ngrx/store";
+import {take} from "rxjs/operators";
+import {IAPIPositionSearchOptions, IAPIPositionSearchStatementModel} from "../../../../core";
+import {ILeafletBounds} from "../../../../shared/leaflet";
+import {
+ getStatementPositionSearchSelector,
+ openGisAction,
+ queryParamsCoordSelector,
+ startStatementPositionSearchAction,
+ statementLoadingSelector,
+ statementTypesSelector,
+ userNameSelector
+} from "../../../../store";
+import {IFilterToDisplay} from "../search-filter";
+
+@Component({
+ selector: "app-position-search",
+ templateUrl: "./position-search.component.html",
+ styleUrls: ["./position-search.component.scss"]
+})
+export class PositionSearchComponent implements OnInit {
+
+ public coord$ = this.store.pipe(select(queryParamsCoordSelector), take(1));
+
+ public searchContent$ = this.store.pipe(select(getStatementPositionSearchSelector));
+
+ public statementLoading$ = this.store.pipe(select(statementLoadingSelector));
+
+ public statementTypeOptions$ = this.store.pipe(select(statementTypesSelector));
+
+ public userName$ = this.store.pipe(select(userNameSelector));
+
+
+ public filtersToShow: IFilterToDisplay = {
+ filterForType: false,
+ dueDateFrom: false,
+ dueDateTo: false
+ };
+
+ public selected: IAPIPositionSearchStatementModel;
+
+ public trackById = trackById;
+
+ public constructor(public store: Store, public router: Router) {
+
+ }
+
+ public ngOnInit() {
+ this.search({});
+ }
+
+ public search(options: IAPIPositionSearchOptions) {
+ this.store.dispatch(startStatementPositionSearchAction({options}));
+ }
+
+ public changeCenter(latLngZoom: string) {
+ this.router.navigate([], {queryParams: {coord: latLngZoom}, replaceUrl: true});
+ }
+
+ public openGis(bounds: ILeafletBounds) {
+ this.userName$.pipe(take(1)).subscribe((user) => {
+ this.store.dispatch(openGisAction({bounds, user}));
+ });
+ }
+
+}
+
+function trackById(index, entry: IAPIPositionSearchStatementModel) {
+ return entry?.id;
+}
diff --git a/src/app/features/search/components/search/search.component.ts b/src/app/features/search/components/search-filter/IFilterToDisplay.ts
similarity index 66%
rename from src/app/features/search/components/search/search.component.ts
rename to src/app/features/search/components/search-filter/IFilterToDisplay.ts
index 27f29f1..203b033 100644
--- a/src/app/features/search/components/search/search.component.ts
+++ b/src/app/features/search/components/search-filter/IFilterToDisplay.ts
@@ -11,13 +11,14 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {Component} from "@angular/core";
-
-@Component({
- selector: "app-search",
- templateUrl: "./search.component.html",
- styleUrls: ["./search.component.scss"]
-})
-export class SearchComponent {
-
+export interface IFilterToDisplay {
+ filterForType?: boolean;
+ status?: boolean;
+ editedByMe?: boolean;
+ dueDateFrom?: boolean;
+ dueDateTo?: boolean;
+ creationDateFrom?: boolean;
+ creationDateTo?: boolean;
+ receiptDateFrom?: boolean;
+ receiptDateTo?: boolean;
}
diff --git a/src/app/features/search/components/search/index.ts b/src/app/features/search/components/search-filter/index.ts
similarity index 87%
copy from src/app/features/search/components/search/index.ts
copy to src/app/features/search/components/search-filter/index.ts
index 5eb9c7f..da57faa 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/features/search/components/search-filter/index.ts
@@ -11,4 +11,5 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./search-filter.component";
+export * from "./IFilterToDisplay";
diff --git a/src/app/features/search/components/search-filter/search-filter.component.html b/src/app/features/search/components/search-filter/search-filter.component.html
new file mode 100644
index 0000000..37c2735
--- /dev/null
+++ b/src/app/features/search/components/search-filter/search-filter.component.html
@@ -0,0 +1,147 @@
+<!-------------------------------------------------------------------------------
+ * 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 *ngIf="appShowSearch" class="filters--searchbar">
+ <span class="filters--searchbar--text">{{"search.title" | translate}}</span>
+ <app-searchbar
+ (appSearch)="searchByString($event)"
+ [appIsLoading]="appLoading"
+ [appPlaceholder]="'search.executeSearch' | translate"
+ class="filters--searchbar--input">
+ </app-searchbar>
+
+ <ng-content></ng-content>
+</div>
+
+<div class="filters">
+
+ <button (click)="disableAllFilters()"
+ class="openk-button openk-button-rounded"
+ type="button">
+ <mat-icon>filter_list</mat-icon>
+ </button>
+
+ <div class="all-filters">
+ <div *ngIf="appFilters.filterForType != null || appFilters.editedByMe != null || appFilters.status != null"
+ class="filters--row">
+ <div class="filter-group filter-group---stacked">
+ <button
+ (click)="appFilters.filterForType = !appFilters.filterForType; setSearchParams('typeId', typeSelect.appValue, appFilters.filterForType); emitSearch()"
+ [class.openk-info]="appFilters.filterForType"
+ class="openk-button openk-chip filters--btn filters--btn---margin">
+ {{"search.type" | translate}}
+ </button>
+ <div class="filters--type-select-width">
+ <app-select #typeSelect
+ (appValueChange)="setSearchParams('typeId', typeSelect.appValue, appFilters.filterForType);
+ appFilters.filterForType ? emitSearch() : null"
+ [appDisabled]="appStatementTypeOptions?.length == null || appStatementTypeOptions.length === 0"
+ [appOptions]="appStatementTypeOptions"
+ [appPlaceholder]="typeSelect.appDisabled ? ('search.noData' | translate) : ''"
+ [appValue]="appStatementTypeOptions ? appStatementTypeOptions[0]?.value : undefined"
+ class="openk-info filters--select--input-width">
+ </app-select>
+ </div>
+ </div>
+ <div *ngIf="appFilters.status != null"
+ class="filter-group filter-group---stacked">
+ <button
+ (click)="appFilters.status = !appFilters.status;
+ setSearchParams('finished', statusSelect.appValue, appFilters.status); emitSearch()"
+ [class.openk-info]="appFilters.status"
+ class="openk-button openk-chip filters--btn filters--btn---margin">
+ Status
+ </button>
+ <div class="filters--finished-select-width">
+ <app-select #statusSelect
+ (appValueChange)="setSearchParams('finished', statusSelect.appValue, appFilters.status);
+ appFilters.status ? emitSearch() : null"
+ [appOptions]="finishedOptions$ | async"
+ [appValue]="(finishedOptions$ | async)?.length > 0 ? (finishedOptions$ | async)[0].value : statusSelect.appValue"
+ class="openk-info filters--select--input-width">
+ </app-select>
+ </div>
+ </div>
+ <div *ngIf="appFilters.editedByMe != null"
+ class="filter-group filter-group---stacked">
+ <button
+ (click)="appFilters.editedByMe = !appFilters.editedByMe; setParamsBoolean('editedByMe', appFilters.editedByMe ? appFilters.editedByMe : undefined)"
+ *ngIf="appFilters.editedByMe != null"
+ [class.openk-info]="appFilters.editedByMe"
+ class="openk-button openk-chip filters--btn">
+ {{"search.editedByMe" | translate}}
+ </button>
+ </div>
+ </div>
+ <div class="filters--date-selects">
+ <div *ngIf="appFilters.creationDateFrom != null || appFilters.creationDateTo != null"
+ class="filter-group filter-group---stacked">
+ <div class="filters--date-selects--block">
+ <app-date-filter
+ #creationDateFrom
+ (appValueChange)="setSearchParamsDate('creationDateFrom', $event, creationDateFrom.appActive)"
+ *ngIf="appFilters.creationDateFrom != null"
+ [appActive]="appFilters.creationDateFrom"
+ [appTitle]="'search.creationDateFrom' | translate">
+ </app-date-filter>
+ <app-date-filter
+ #creationDateTo
+ (appValueChange)="setSearchParamsDate('creationDateTo', $event, creationDateTo.appActive)"
+ *ngIf="appFilters.creationDateTo != null"
+ [appActive]="appFilters.creationDateTo"
+ [appTitle]="'search.creationDateTo' | translate">
+ </app-date-filter>
+ </div>
+ </div>
+ <div *ngIf="appFilters.dueDateFrom != null || appFilters.dueDateTo != null"
+ class="filter-group filter-group---stacked">
+ <div class="filters--date-selects--block">
+ <app-date-filter
+ #dueDateFrom
+ (appValueChange)="setSearchParamsDate('dueDateFrom', $event, dueDateFrom.appActive)"
+ *ngIf="appFilters.dueDateFrom != null"
+ [appActive]="appFilters.dueDateFrom"
+ [appTitle]="'search.dueDateFrom' | translate">
+ </app-date-filter>
+ <app-date-filter
+ #dueDateTo
+ (appValueChange)="setSearchParamsDate('dueDateTo', $event, dueDateTo.appActive)"
+ *ngIf="appFilters.dueDateTo != null"
+ [appActive]="appFilters.dueDateTo"
+ [appTitle]="'search.dueDateTo' | translate">
+ </app-date-filter>
+ </div>
+ </div>
+ <div *ngIf="appFilters.receiptDateFrom != null || appFilters.receiptDateTo != null"
+ class="filter-group filter-group---stacked">
+ <div class="filters--date-selects--block">
+ <app-date-filter
+ #receiptDateFrom
+ (appValueChange)="setSearchParamsDate('receiptDateFrom', $event, receiptDateFrom.appActive)"
+ *ngIf="appFilters.receiptDateFrom != null"
+ [appActive]="appFilters.receiptDateFrom"
+ [appTitle]="'search.receiptDateFrom' | translate">
+ </app-date-filter>
+ <app-date-filter
+ #receiptDateTo
+ (appValueChange)="setSearchParamsDate('receiptDateTo', $event, receiptDateTo.appActive)"
+ *ngIf="appFilters.receiptDateTo != null"
+ [appActive]="appFilters.receiptDateTo"
+ [appTitle]="'search.receiptDateTo' | translate">
+ </app-date-filter>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
diff --git a/src/app/features/search/components/search-filter/search-filter.component.scss b/src/app/features/search/components/search-filter/search-filter.component.scss
new file mode 100644
index 0000000..6dae64d
--- /dev/null
+++ b/src/app/features/search/components/search-filter/search-filter.component.scss
@@ -0,0 +1,133 @@
+/********************************************************************************
+ * 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%;
+}
+
+.filters--searchbar {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ margin-bottom: 1em;
+ width: 100%;
+}
+
+.filters--searchbar--text {
+ margin-right: 0.5em;
+ min-width: 2.9em;
+}
+
+.filters--searchbar--input {
+ flex: 1;
+ max-width: 42.85em;
+}
+
+.filters {
+ display: flex;
+ flex-direction: row;
+}
+
+.filters--row {
+ display: inline-flex;
+ align-items: center;
+ box-sizing: border-box;
+ flex-wrap: wrap;
+}
+
+.filters--btn {
+ min-width: 6em;
+ font-size: small;
+ border-color: get-color($openk-info-palette, 500);
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+.filters--btn---margin {
+ margin-right: 0.5em;
+}
+
+.filters--type-select-width {
+ width: 12em;
+}
+
+.filters--finished-select-width {
+ width: 8em;
+}
+
+.filters--select--input-width {
+ width: 100%;
+}
+
+.filters--date-selects {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+}
+
+.filters--date-selects--block {
+ display: flex;
+ flex-direction: column;
+
+ :first-child {
+ margin-bottom: 0.2em;
+ }
+}
+
+.openk-button-rounded {
+ font-size: 1.5em;
+ border: 0;
+ color: get-color($openk-info-palette);
+ margin-right: 0.25em;
+ min-width: 2em;
+
+ &: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 {
+ height: 2em;
+}
+
+.filter-group {
+ background-color: $openk-background-highlight;
+ padding: 0.3em;
+ border-radius: 6px;
+ border: 1px solid $openk-form-border;
+ display: inline-flex;
+ align-items: center;
+ box-sizing: border-box;
+ height: 100%;
+ margin-right: 0.5em;
+}
+
+.filter-group---stacked {
+ height: initial;
+ margin-bottom: 0.5em;
+}
+
+.all-filters {
+ display: flex;
+ flex-direction: column;
+
+}
diff --git a/src/app/features/search/components/search-filter/search-filter.component.spec.ts b/src/app/features/search/components/search-filter/search-filter.component.spec.ts
new file mode 100644
index 0000000..ac06435
--- /dev/null
+++ b/src/app/features/search/components/search-filter/search-filter.component.spec.ts
@@ -0,0 +1,94 @@
+/********************************************************************************
+ * 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} from "../../../../core/i18n";
+import {queryParamsIdSelector} from "../../../../store/root/selectors";
+import {SearchModule} from "../../search.module";
+import {SearchFilterComponent} from "./search-filter.component";
+
+describe("SearchFilterComponent", () => {
+ let component: SearchFilterComponent;
+ let fixture: ComponentFixture<SearchFilterComponent>;
+ let store: Store;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ SearchModule,
+ I18nModule
+ ],
+ providers: [
+ provideMockStore({
+ initialState: {},
+ selectors: [
+ {
+ selector: queryParamsIdSelector,
+ value: 19
+ }
+ ]
+ })
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SearchFilterComponent);
+ component = fixture.componentInstance;
+ store = fixture.componentRef.injector.get(Store);
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should set all filter active status to false on disableAllFilters", () => {
+ component.appFilters = {
+ status: false,
+ editedByMe: true,
+ dueDateFrom: false,
+ dueDateTo: false
+ };
+ component.disableAllFilters();
+ expect(component.appFilters).toEqual({
+ status: false,
+ editedByMe: false,
+ dueDateFrom: false,
+ dueDateTo: false
+ });
+ });
+
+ it("should set the search parameter q and emit onValueChange", () => {
+ spyOn(component.appValueChange, "emit").and.callThrough();
+ component.searchByString("test");
+ expect(component.appValueChange.emit).toHaveBeenCalledWith({q: "test"});
+ });
+
+ it("should set search parameter and not emit", () => {
+ component.appFilters = {
+ filterForType: false
+ };
+ spyOn(component.appValueChange, "emit").and.callThrough();
+ component.setSearchParams("typeId", 2, true);
+ expect(component.appValueChange.emit).not.toHaveBeenCalled();
+ expect(component.appValue).toEqual({
+ q: "",
+ typeId: 2
+ });
+ component.setSearchParams("typeId", 2, false);
+ expect(component.appValue).toEqual({q: "", typeId: undefined});
+ });
+});
diff --git a/src/app/features/search/components/search-filter/search-filter.component.stories.ts b/src/app/features/search/components/search-filter/search-filter.component.stories.ts
new file mode 100644
index 0000000..6b9e6ba
--- /dev/null
+++ b/src/app/features/search/components/search-filter/search-filter.component.stories.ts
@@ -0,0 +1,32 @@
+/********************************************************************************
+ * 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 {withKnobs} from "@storybook/addon-knobs";
+import {moduleMetadata, storiesOf} from "@storybook/angular";
+import {I18nModule} from "../../../../core";
+import {SearchModule} from "../../search.module";
+import {SearchFilterComponent} from "./search-filter.component";
+
+storiesOf("Features / search", module)
+ .addDecorator(withKnobs)
+ .addDecorator(moduleMetadata({
+ declarations: [],
+ imports: [
+ I18nModule,
+ SearchModule
+ ]
+ }))
+ .add("SearchFilterComponent", () => ({
+ component: SearchFilterComponent,
+ props: {}
+ }));
diff --git a/src/app/features/search/components/search-filter/search-filter.component.ts b/src/app/features/search/components/search-filter/search-filter.component.ts
new file mode 100644
index 0000000..d407dcc
--- /dev/null
+++ b/src/app/features/search/components/search-filter/search-filter.component.ts
@@ -0,0 +1,130 @@
+/********************************************************************************
+ * 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, OnInit, Output} from "@angular/core";
+import {TranslateService} from "@ngx-translate/core";
+import {Observable} from "rxjs";
+import {map} from "rxjs/operators";
+import {IAPISearchOptions} from "../../../../core/api/shared";
+import {AbstractControlValueAccessorComponent} from "../../../../shared/controls/common";
+import {ISelectOption} from "../../../../shared/controls/select/model";
+import {momentFormatInternal, parseMomentToString} from "../../../../util/moment";
+import {IFilterToDisplay} from "./IFilterToDisplay";
+
+/**
+ * This component displays a selection of filters and generates search parameters.
+ * Those search parameters can be used in the backend calls to filter the list of all statements for e.g. specific keywords or date ranges.
+ * The displayed filters and their state are set by the input property appFilters.
+ * E.g.
+ * {
+ * status: true,
+ * editedByMe: false
+ * }
+ * only displays the filter dropdown for status (finished/not finished) and the toggle for editedByMe state.
+ * The initial value for the filter button for status is true.
+ * That means the filter is active with the default value (first value in drop down)
+ */
+
+@Component({
+ selector: "app-search-filter",
+ templateUrl: "./search-filter.component.html",
+ styleUrls: ["./search-filter.component.scss"]
+})
+export class SearchFilterComponent extends AbstractControlValueAccessorComponent<IAPISearchOptions> implements OnInit {
+
+ @Input()
+ public appLoading: boolean;
+
+ @Input()
+ public appStatementTypeOptions: ISelectOption<number>[];
+
+ @Input()
+ public appShowSearch = true;
+
+ @Output()
+ public appFilterParams = new EventEmitter<IAPISearchOptions>();
+
+ @Input()
+ public appFilters: IFilterToDisplay = {
+ filterForType: false,
+ status: false,
+ editedByMe: false,
+ dueDateFrom: false,
+ dueDateTo: false,
+ creationDateFrom: false,
+ creationDateTo: false,
+ receiptDateFrom: false,
+ receiptDateTo: false
+ };
+
+ public finishedOptions$: Observable<ISelectOption[]> = this.translateService.get(["search.open", "search.finished"]).pipe(
+ map((value) => {
+ return [
+ {
+ label: value["search.open"],
+ value: false
+ },
+ {
+ label: value["search.finished"],
+ value: true
+ }
+ ];
+ })
+ );
+
+ public constructor(public readonly translateService: TranslateService) {
+ super();
+ }
+
+ public ngOnInit() {
+ this.writeValue({
+ q: this.appShowSearch ? "" : undefined
+ }, true);
+ }
+
+ public searchByString(q: string) {
+ this.writeValue({...this.appValue, q}, true);
+ }
+
+ public setSearchParams(label: string, value: string | number, filterActive: boolean) {
+ const newValue = {...this.appValue};
+ newValue[label] = filterActive ? value : undefined;
+ this.writeValue(newValue);
+ }
+
+ public setSearchParamsDate(label: string, value: Date, filterActive: boolean) {
+ const newValue = {...this.appValue};
+ newValue[label] = filterActive ? parseMomentToString(value, momentFormatInternal, momentFormatInternal) : undefined;
+ this.appFilters[label] = filterActive;
+ this.writeValue(newValue, true);
+ }
+
+ public setParamsBoolean(label: string, value: boolean) {
+ const newValue = {...this.appValue};
+ newValue[label] = value;
+ this.writeValue(newValue, true);
+ }
+
+ public emitSearch() {
+ this.appValueChange.emit(this.appValue);
+ }
+
+ public disableAllFilters() {
+ for (const key of Object.keys(this.appFilters)) {
+ this.appFilters[key] = false;
+ }
+ this.writeValue({q: undefined}, true);
+ }
+
+}
+
diff --git a/src/app/features/search/components/search/index.ts b/src/app/features/search/components/search-statements/index.ts
similarity index 92%
copy from src/app/features/search/components/search/index.ts
copy to src/app/features/search/components/search-statements/index.ts
index 5eb9c7f..aa947a3 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/features/search/components/search-statements/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./search-statements.component";
diff --git a/src/app/features/search/components/search-statements/search-statements.component.html b/src/app/features/search/components/search-statements/search-statements.component.html
new file mode 100644
index 0000000..518a4a0
--- /dev/null
+++ b/src/app/features/search/components/search-statements/search-statements.component.html
@@ -0,0 +1,37 @@
+<!-------------------------------------------------------------------------------
+ * 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="search-filters">
+ <app-search-filter
+ (appValueChange)="addToSearchParams($event)"
+ [appLoading]="statementLoading$ | async"
+ [appStatementTypeOptions]="statementTypeOptions$ | async">
+ </app-search-filter>
+</div>
+
+<app-statement-table
+ (appSort)="sort($event?.label, $event?.direction)"
+ [appColumns]="columns"
+ [appEntries]="searchContent$ | async"
+ [appIsSortable]="true"
+ [appStatementTypeOptions]="statementTypeOptions$ | async"
+ class="openk-table---last-row-without-border search-list">
+</app-statement-table>
+
+<app-pagination-counter
+ (appPageChange)="changePage($event?.page, $event?.size)"
+ [appDisabled]="statementLoading$ | async"
+ [appPageSize]="(searchInfo$ | async)?.size"
+ [appPage]="(searchInfo$ | async)?.currentPage"
+ [appTotalPages]="(searchInfo$ | async)?.totalPages">
+</app-pagination-counter>
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss b/src/app/features/search/components/search-statements/search-statements.component.scss
similarity index 72%
copy from src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
copy to src/app/features/search/components/search-statements/search-statements.component.scss
index 4d2360d..2d673ee 100644
--- a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
+++ b/src/app/features/search/components/search-statements/search-statements.component.scss
@@ -11,17 +11,23 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "../../../../styles/openk.styles";
+@import "openk.styles";
+
:host {
+ position: relative;
+ display: block;
width: 100%;
+ box-sizing: border-box;
+ padding: 1em;
}
-.statements {
- margin-bottom: 1em;
- display: grid;
+.search-list {
+ min-height: 5.3125em;
+ background: get-color($openk-default-palette);
}
-.statements--titlebar {
+.search-filters {
margin-bottom: 0.5em;
+ display: flex;
}
diff --git a/src/app/features/search/components/search-statements/search-statements.component.spec.ts b/src/app/features/search/components/search-statements/search-statements.component.spec.ts
new file mode 100644
index 0000000..c9f3032
--- /dev/null
+++ b/src/app/features/search/components/search-statements/search-statements.component.spec.ts
@@ -0,0 +1,75 @@
+/********************************************************************************
+ * 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 {MockStore, provideMockStore} from "@ngrx/store/testing";
+import {I18nModule} from "../../../../core/i18n";
+import {startStatementSearchAction} from "../../../../store/statements/actions";
+import {SearchModule} from "../../search.module";
+import {SearchStatementsComponent} from "./search-statements.component";
+
+describe("SearchComponent", () => {
+ let component: SearchStatementsComponent;
+ let fixture: ComponentFixture<SearchStatementsComponent>;
+ let store: MockStore;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ SearchModule,
+ I18nModule,
+ RouterTestingModule
+ ],
+ providers: [provideMockStore()]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SearchStatementsComponent);
+ component = fixture.componentInstance;
+ store = TestBed.inject(MockStore);
+ spyOn(store, "dispatch").and.callThrough();
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should start search after init", () => {
+ expect(store.dispatch).toHaveBeenCalledWith(startStatementSearchAction({options: component.searchParams}));
+ });
+
+ it("should add parameters to search", () => {
+ component.addToSearchParams({q: "", typeId: 19});
+ expect(store.dispatch).toHaveBeenCalledWith(startStatementSearchAction({
+ options: {...component.searchParams, q: "", typeId: 19}
+ }));
+ });
+
+ it("should sort list", () => {
+ component.sort("id", "asc");
+ expect(store.dispatch).toHaveBeenCalledWith(startStatementSearchAction({
+ options: {...component.searchParams, sort: "id,asc"}
+ }));
+ });
+
+ it("should change page", () => {
+ component.changePage(19, 1919);
+ expect(store.dispatch).toHaveBeenCalledWith(startStatementSearchAction({
+ options: {...component.searchParams, page: 19, size: 1919}
+ }));
+ });
+
+});
diff --git a/src/app/features/search/components/search-statements/search-statements.component.ts b/src/app/features/search/components/search-statements/search-statements.component.ts
new file mode 100644
index 0000000..90579db
--- /dev/null
+++ b/src/app/features/search/components/search-statements/search-statements.component.ts
@@ -0,0 +1,86 @@
+/********************************************************************************
+ * 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, OnInit} from "@angular/core";
+import {select, Store} from "@ngrx/store";
+import {IAPISearchOptions} from "../../../../core/api/shared";
+import {StatementTableComponent} from "../../../../shared/layout/statement-table/components";
+import {statementTypesSelector} from "../../../../store/settings/selectors";
+import {startStatementSearchAction} from "../../../../store/statements/actions";
+import {statementLoadingSelector} from "../../../../store/statements/selectors";
+import {getSearchContentInfoSelector, getSearchContentStatementsSelector} from "../../../../store/statements/selectors/search";
+
+/**
+ * This component display a list of statements. A keyword search and filters can be applied to the list of statements.
+ * The list can also be sorted by clicking on specific header column to sort for.
+ * Pagination and sorting will be added to the query parameters for the search calls to the backend.
+ */
+
+@Component({
+ selector: "app-search-statements",
+ templateUrl: "./search-statements.component.html",
+ styleUrls: ["./search-statements.component.scss"]
+})
+export class SearchStatementsComponent implements OnInit {
+
+ public columns = [...StatementTableComponent.SEARCH_COLUMNS];
+
+ public statementTypeOptions$ = this.store.pipe(select(statementTypesSelector));
+
+ public searchContent$ = this.store.pipe(select(getSearchContentStatementsSelector));
+
+ public searchInfo$ = this.store.pipe(select(getSearchContentInfoSelector));
+
+ public statementLoading$ = this.store.pipe(select(statementLoadingSelector));
+
+ public pageSize = 10;
+
+ public baseSearchParams: IAPISearchOptions = {
+ q: "",
+ size: this.pageSize,
+ page: 0
+ };
+
+ public searchParams: IAPISearchOptions = {...this.baseSearchParams};
+
+ public sortParam: string;
+
+ public constructor(public store: Store) {
+
+ }
+
+ public ngOnInit() {
+ this.search();
+ }
+
+ public search() {
+ this.store.dispatch(startStatementSearchAction({options: this.searchParams}));
+ }
+
+ public sort(label: string, direction: "asc" | "desc") {
+ this.sortParam = `${label},${direction}`;
+ this.searchParams = {...this.searchParams, sort: this.sortParam};
+ this.search();
+ }
+
+ public addToSearchParams(additionalParams: IAPISearchOptions) {
+ this.searchParams = {...this.baseSearchParams, ...additionalParams, sort: this.sortParam, size: this.pageSize};
+ this.search();
+ }
+
+ public changePage(page: number, size: number) {
+ this.searchParams = {...this.searchParams, page, size};
+ this.pageSize = size;
+ this.search();
+ }
+}
diff --git a/src/app/features/search/components/search/search.component.html b/src/app/features/search/components/search/search.component.html
deleted file mode 100644
index 0bc2dce..0000000
--- a/src/app/features/search/components/search/search.component.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<!-------------------------------------------------------------------------------
- * 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 style="padding: 1em;">
- Not yet implemented.
-</div>
diff --git a/src/app/features/search/index.ts b/src/app/features/search/index.ts
index cfd6650..5ea4030 100644
--- a/src/app/features/search/index.ts
+++ b/src/app/features/search/index.ts
@@ -12,3 +12,4 @@
********************************************************************************/
export * from "./search.module";
+export * from "./components";
diff --git a/src/app/features/search/search-routing.module.ts b/src/app/features/search/search-routing.module.ts
index 6d938a9..e3d5b7c 100644
--- a/src/app/features/search/search-routing.module.ts
+++ b/src/app/features/search/search-routing.module.ts
@@ -13,14 +13,24 @@
import {NgModule} from "@angular/core";
import {RouterModule, Routes} from "@angular/router";
-import {SearchComponent} from "./components";
+import {PositionSearchComponent, SearchStatementsComponent} from "./components";
import {SearchModule} from "./search.module";
const routes: Routes = [
{
path: "",
pathMatch: "full",
- component: SearchComponent
+ redirectTo: "list"
+ },
+ {
+ path: "list",
+ pathMatch: "full",
+ component: SearchStatementsComponent
+ },
+ {
+ path: "map",
+ pathMatch: "full",
+ component: PositionSearchComponent
}
];
diff --git a/src/app/features/search/search.module.ts b/src/app/features/search/search.module.ts
index 9e594b1..937ca93 100644
--- a/src/app/features/search/search.module.ts
+++ b/src/app/features/search/search.module.ts
@@ -13,17 +13,44 @@
import {CommonModule} from "@angular/common";
import {NgModule} from "@angular/core";
-import {SearchComponent} from "./components";
+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 {SelectModule} from "../../shared/controls/select";
+import {PaginationCounterModule} from "../../shared/layout/pagination-counter";
+import {SearchbarModule} from "../../shared/layout/searchbar";
+import {StatementTableModule} from "../../shared/layout/statement-table";
+import {LeafletModule} from "../../shared/leaflet";
+import {DateFilterComponent} from "./components/date-filter";
+import {PositionSearchComponent} from "./components/position-search";
+import {SearchFilterComponent} from "./components/search-filter";
+import {SearchStatementsComponent} from "./components/search-statements";
@NgModule({
imports: [
- CommonModule
+ CommonModule,
+ StatementTableModule,
+ PaginationCounterModule,
+ SearchbarModule,
+ SelectModule,
+ DateControlModule,
+ TranslateModule,
+ MatIconModule,
+ RouterModule,
+ LeafletModule
],
declarations: [
- SearchComponent
+ SearchFilterComponent,
+ DateFilterComponent,
+ SearchStatementsComponent,
+ PositionSearchComponent
],
exports: [
- SearchComponent
+ SearchFilterComponent,
+ DateFilterComponent,
+ SearchStatementsComponent,
+ PositionSearchComponent
]
})
export class SearchModule {
diff --git a/src/app/shared/controls/contact-select/contact-select.component.html b/src/app/shared/controls/contact-select/contact-select.component.html
index a045f94..84f4d4a 100644
--- a/src/app/shared/controls/contact-select/contact-select.component.html
+++ b/src/app/shared/controls/contact-select/contact-select.component.html
@@ -42,9 +42,9 @@
<app-pagination-counter
(appPageChange)="appPageChange.emit($event)"
- *ngIf="appPageSize >= 2"
[appDisabled]="appDisabled || appIsLoading"
[appPageSize]="appPageSize"
+ [appTotalPages]="appTotalPages"
[appPage]="appPage">
</app-pagination-counter>
</div>
diff --git a/src/app/shared/controls/contact-select/contact-select.component.scss b/src/app/shared/controls/contact-select/contact-select.component.scss
index a28ef31..7df646c 100644
--- a/src/app/shared/controls/contact-select/contact-select.component.scss
+++ b/src/app/shared/controls/contact-select/contact-select.component.scss
@@ -56,10 +56,11 @@
display: flex;
flex-direction: row;
justify-content: space-between;
+ align-items: flex-start;
}
.contacts--selection--list--box--info--button {
- margin-top: 1em;
+ margin-top: 0.5em;
}
.contacts--details {
diff --git a/src/app/shared/controls/contact-select/contact-select.component.ts b/src/app/shared/controls/contact-select/contact-select.component.ts
index e916aae..3dcc299 100644
--- a/src/app/shared/controls/contact-select/contact-select.component.ts
+++ b/src/app/shared/controls/contact-select/contact-select.component.ts
@@ -47,14 +47,17 @@
public appMessage: string;
@Output()
- public appPageChange = new EventEmitter<number>();
+ public appPageChange = new EventEmitter<{ page: number, size: number }>();
@Input()
- public appPageSize: number;
+ public appTotalPages: number;
@Input()
public appSearch: string;
+ @Input()
+ public appPageSize: number;
+
@Output()
public appSearchChange = new EventEmitter<string>();
diff --git a/src/app/shared/controls/map-select/components/map-select.component.html b/src/app/shared/controls/map-select/components/map-select.component.html
index 0b9644a..21b71a7 100644
--- a/src/app/shared/controls/map-select/components/map-select.component.html
+++ b/src/app/shared/controls/map-select/components/map-select.component.html
@@ -11,29 +11,15 @@
* SPDX-License-Identifier: EPL-2.0
-------------------------------------------------------------------------------->
-<div class="map">
- <div #appLeaflet="appLeaflet"
- (appLatLngZoomChange)="select($event)"
- [appDisabled]="appDisabled"
- [appView]="appValue | stringToLatLngZoom"
- appLeaflet
- class="map--leaflet">
+<app-leaflet-map
+ (appCenterChange)="select($event)"
+ (appOpenGis)="appOpenGis.emit($event)"
+ [appCenter]="appValue"
+ [appDisabled]="appDisabled"
+ [appSubCaption]="appSubCaption"
+ class="map">
- <ng-container appLeafletCenterMarker>
- </ng-container>
+ <ng-container appLeafletCenterMarker>
+ </ng-container>
- </div>
-
- <app-action-button
- (appClick)="appActionButtonClick.emit(appLeaflet.getBounds())"
- *ngIf="appActionButtonLabel"
- [appDisabled]="appDisabled"
- [appIcon]="'my_location'"
- class="map--action-button openk-info">
- {{appActionButtonLabel}}
- </app-action-button>
-</div>
-
-<label *ngIf="appSubCaption" class="sub-caption">
- {{appSubCaption}}
-</label>
+</app-leaflet-map>
diff --git a/src/app/shared/controls/map-select/components/map-select.component.scss b/src/app/shared/controls/map-select/components/map-select.component.scss
index d45e610..7e59f1f 100644
--- a/src/app/shared/controls/map-select/components/map-select.component.scss
+++ b/src/app/shared/controls/map-select/components/map-select.component.scss
@@ -14,42 +14,7 @@
@import "openk.styles";
:host {
- width: 100%;
- height: 100%;
- box-sizing: border-box;
-
- display: flex;
- flex-flow: column;
-}
-
-.map {
- flex: 1 1 100%;
- width: 100%;
- box-sizing: border-box;
- border: 1px solid $openk-form-border;
- position: relative;
- overflow: hidden;
-}
-
-.map--leaflet {
- width: 100%;
- height: 100%;
-}
-
-.map--action-button {
display: block;
- width: fit-content;
- height: fit-content;
- position: absolute;
- bottom: 10px;
- left: 10px;
- z-index: 1000;
-}
-
-.sub-caption {
- color: $openk-form-border;
- margin-top: 0.5em;
- margin-left: auto;
- font-size: smaller;
- font-style: italic;
+ width: 100%;
+ height: 100%;
}
diff --git a/src/app/shared/controls/map-select/components/map-select.component.spec.ts b/src/app/shared/controls/map-select/components/map-select.component.spec.ts
index f57df21..dd5e766 100644
--- a/src/app/shared/controls/map-select/components/map-select.component.spec.ts
+++ b/src/app/shared/controls/map-select/components/map-select.component.spec.ts
@@ -12,6 +12,7 @@
********************************************************************************/
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {I18nModule} from "../../../../core";
import {MapSelectModule} from "../map-select.module";
import {MapSelectComponent} from "./map-select.component";
@@ -21,7 +22,7 @@
beforeEach(async(() => {
TestBed.configureTestingModule({
- imports: [MapSelectModule]
+ imports: [MapSelectModule, I18nModule]
}).compileComponents();
}));
diff --git a/src/app/shared/controls/map-select/components/map-select.component.ts b/src/app/shared/controls/map-select/components/map-select.component.ts
index f408006..9c8759d 100644
--- a/src/app/shared/controls/map-select/components/map-select.component.ts
+++ b/src/app/shared/controls/map-select/components/map-select.component.ts
@@ -13,7 +13,7 @@
import {Component, EventEmitter, forwardRef, Input, Output} from "@angular/core";
import {NG_VALUE_ACCESSOR} from "@angular/forms";
-import {ILeafletBounds} from "../../../layout/leaflet";
+import {ILeafletBounds} from "../../../leaflet";
import {AbstractControlValueAccessorComponent} from "../../common";
@Component({
@@ -36,11 +36,8 @@
@Input()
public appSubCaption: string;
- @Input()
- public appActionButtonLabel: string;
-
@Output()
- public appActionButtonClick = new EventEmitter<ILeafletBounds>();
+ public appOpenGis = new EventEmitter<ILeafletBounds>();
public select(value: string) {
// Note that this.appValue should not be changed here:
diff --git a/src/app/shared/controls/map-select/map-select.module.ts b/src/app/shared/controls/map-select/map-select.module.ts
index 8a22296..278693d 100644
--- a/src/app/shared/controls/map-select/map-select.module.ts
+++ b/src/app/shared/controls/map-select/map-select.module.ts
@@ -14,7 +14,7 @@
import {CommonModule} from "@angular/common";
import {NgModule} from "@angular/core";
import {ActionButtonModule} from "../../layout/action-button";
-import {LeafletModule} from "../../layout/leaflet";
+import {LeafletModule} from "../../leaflet";
import {MapSelectComponent} from "./components";
@NgModule({
diff --git a/src/app/shared/controls/select/components/select-group/select-group.component.scss b/src/app/shared/controls/select/components/select-group/select-group.component.scss
index b07ef7e..40af940 100644
--- a/src/app/shared/controls/select/components/select-group/select-group.component.scss
+++ b/src/app/shared/controls/select/components/select-group/select-group.component.scss
@@ -18,7 +18,7 @@
}
.select-group-title {
- font-weight: bold;
+ font-weight: 600;
margin-top: 0.5em;
&:first-child {
diff --git a/src/app/shared/controls/select/components/select/select.component.scss b/src/app/shared/controls/select/components/select/select.component.scss
index 305eef4..307fa14 100644
--- a/src/app/shared/controls/select/components/select/select.component.scss
+++ b/src/app/shared/controls/select/components/select/select.component.scss
@@ -103,6 +103,13 @@
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1);
}
+@supports (-moz-appearance:none) {
+
+ .select-options {
+ overflow-y: scroll;
+ }
+}
+
.select-options---static-size {
display: flex;
flex-flow: column nowrap;
diff --git a/src/app/shared/layout/collapsible/collapsible.component.ts b/src/app/shared/layout/collapsible/collapsible.component.ts
index 6e3e19d..72fef31 100644
--- a/src/app/shared/layout/collapsible/collapsible.component.ts
+++ b/src/app/shared/layout/collapsible/collapsible.component.ts
@@ -48,7 +48,7 @@
public appTitle: string;
/**
- * If set, the template is rendered is rendered in the header part of the container
+ * If set, the template is rendered in the header part of the container
*/
@Input()
public appHeaderTemplateRef: TemplateRef<any>;
diff --git a/src/app/shared/layout/leaflet/directives/leaflet/leaflet.directive.ts b/src/app/shared/layout/leaflet/directives/leaflet/leaflet.directive.ts
deleted file mode 100644
index 62fcaae..0000000
--- a/src/app/shared/layout/leaflet/directives/leaflet/leaflet.directive.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-/********************************************************************************
- * 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 {AfterViewInit, Directive, ElementRef, HostBinding, Inject, Input, NgZone, OnDestroy, OnInit, Optional, Output} from "@angular/core";
-import {InvalidateSizeOptions, LatLngLiteral, LeafletEvent, LeafletMouseEvent, Map, TileLayer} from "leaflet";
-import {defer, Observable, of, pipe} from "rxjs";
-import {debounceTime, distinctUntilChanged, map, switchMap, takeUntil, throttleTime} from "rxjs/operators";
-import {runInZone, runOutsideZone} from "../../../../../util/rxjs";
-import {ILeafletConfiguration, LEAFLET_CONFIGURATION_TOKEN, LEAFLET_RESIZE_TOKEN} from "../../leaflet-configuration.token";
-import {fromLeafletEvent, latLngZoomToString} from "../../util";
-
-export interface ILeafletBounds {
- northWest: LatLngLiteral;
- northEast: LatLngLiteral;
- southEast: LatLngLiteral;
- southWest: LatLngLiteral;
- center: LatLngLiteral;
- zoom: number;
-}
-
-@Directive({
- selector: "[appLeaflet]",
- exportAs: "appLeaflet"
-})
-export class LeafletDirective implements OnInit, OnDestroy, AfterViewInit {
-
- @HostBinding("class.no-pointer-events")
- @HostBinding("class.disable")
- @Input()
- public appDisabled: boolean;
-
- @Output()
- public appLatLngZoomChange = defer(() => this.on("move zoom", true)).pipe(
- debounceTime(10),
- map(() => latLngZoomToString(this.leaflet.getCenter(), this.leaflet.getZoom())),
- distinctUntilChanged(),
- runInZone(this.ngZone)
- );
-
- @Output()
- public appClick = defer(() => this.on<LeafletMouseEvent>("click"));
-
- @Output()
- public appUnload$ = defer(() => this.on("unload"));
-
- public readonly leaflet: Map;
-
- public constructor(
- public readonly elementRef: ElementRef<HTMLElement>,
- public readonly ngZone: NgZone,
- @Inject(LEAFLET_CONFIGURATION_TOKEN) public readonly configuration: ILeafletConfiguration,
- @Optional() @Inject(LEAFLET_RESIZE_TOKEN) public resize$: Observable<any>
- ) {
- this.leaflet = this.ngZone.runOutsideAngular(() => {
- const tileLayer = new TileLayer(this.configuration.urlTemplate, {
- attribution: this.configuration.attribution
- });
- const result = new Map(this.elementRef.nativeElement, {layers: [tileLayer]});
- result.setView({lat: configuration.lat, lng: configuration.lng}, configuration.zoom);
- return result;
- });
- }
-
- @Input()
- public set appView(value: { lat: number, lng: number, zoom: number }) {
- if (value != null) {
- this.ngZone.runOutsideAngular(() => this.leaflet.setView(value, value.zoom));
- }
- }
-
- public ngOnInit() {
- if (this.resize$ instanceof Observable) {
- this.resize$.pipe(throttleTime(10), takeUntil(this.appUnload$))
- .subscribe((_) => this.invalidateSize());
- }
- }
-
- public ngAfterViewInit() {
- setTimeout(() => this.invalidateSize());
- }
-
- public ngOnDestroy() {
- this.ngZone.runOutsideAngular(() => this.leaflet.remove());
- }
-
- public getBounds(): ILeafletBounds {
- return this.ngZone.runOutsideAngular(() => {
- const bounds = this.leaflet.getBounds();
- const zoom = this.leaflet.getZoom();
- const result = {
- northWest: bounds.getNorthWest(),
- northEast: bounds.getNorthEast(),
- southEast: bounds.getSouthEast(),
- southWest: bounds.getSouthWest(),
- center: bounds.getCenter(),
- zoom
- };
- return this.ngZone.run(() => result);
- });
- }
-
- public invalidateSize(options?: boolean | InvalidateSizeOptions) {
- this.ngZone.runOutsideAngular(() => this.leaflet.invalidateSize(options));
- }
-
- public on<T extends LeafletEvent>(type: string, outsideZone?: boolean): Observable<T> {
- return of(type).pipe(
- runOutsideZone(this.ngZone),
- switchMap((_) => fromLeafletEvent<T>(this.leaflet, type)),
- outsideZone ? pipe() : runInZone(this.ngZone)
- );
- }
-
-}
diff --git a/src/app/shared/layout/leaflet/leaflet.module.ts b/src/app/shared/layout/leaflet/leaflet.module.ts
deleted file mode 100644
index f817b70..0000000
--- a/src/app/shared/layout/leaflet/leaflet.module.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/********************************************************************************
- * 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 {CommonModule} from "@angular/common";
-import {ModuleWithProviders, NgModule, Type} from "@angular/core";
-import {Observable} from "rxjs";
-import {LeafletCenterMarkerDirective, LeafletDirective, LeafletMarkerDirective} from "./directives";
-import {LEAFLET_RESIZE_TOKEN} from "./leaflet-configuration.token";
-import {StringToLatLngZoomPipe} from "./pipes";
-
-@NgModule({
- imports: [
- CommonModule
- ],
- declarations: [
- LeafletCenterMarkerDirective,
- LeafletDirective,
- LeafletMarkerDirective,
- StringToLatLngZoomPipe
- ],
- exports: [
- LeafletCenterMarkerDirective,
- LeafletDirective,
- LeafletMarkerDirective,
- StringToLatLngZoomPipe
- ]
-})
-export class LeafletModule {
-
- public static for<T extends { resize$: Observable<any> }>(resizeService: Type<T>): ModuleWithProviders {
- return {
- ngModule: LeafletModule,
- providers: [
- {
- provide: LEAFLET_RESIZE_TOKEN,
- useFactory: (service: T) => service.resize$,
- deps: [resizeService]
- }
- ]
- };
- }
-
-}
diff --git a/src/app/shared/layout/pagination-counter/pagination-counter.component.html b/src/app/shared/layout/pagination-counter/pagination-counter.component.html
index c599d92..c001e58 100644
--- a/src/app/shared/layout/pagination-counter/pagination-counter.component.html
+++ b/src/app/shared/layout/pagination-counter/pagination-counter.component.html
@@ -11,21 +11,30 @@
* SPDX-License-Identifier: EPL-2.0
-------------------------------------------------------------------------------->
-<div *ngIf="appPageSize > 0" class="page-info">
- <button (click)="getPreviousPage()"
- [class.openk-button-disabled]="!(appPage > 0)"
- [disabled]="appDisabled || !(appPage > 0)"
- class="openk-button openk-info page-info--button">
- <mat-icon class="page-info--button--icon">keyboard_arrow_left</mat-icon>
- </button>
- <span
- [class.disabled]="appDisabled"
- class="page-info--text">
- {{appPage == null ? 1 : appPage + 1}} / {{appPageSize}}
- </span>
- <button (click)="getNextPage()"
- [disabled]="appDisabled || appPage >= appPageSize - 1"
- class="openk-button openk-info page-info--button">
- <mat-icon class="page-info--button--icon">keyboard_arrow_right</mat-icon>
- </button>
-</div>
+<app-select #pageSizeSelect
+ (appValueChange)="appPageSize = $event; appPageChange.emit({page: 0, size: $event});"
+ [appDisabled]="appDisabled"
+ [appOptions]="appPageSizeOptions"
+ [appValue]="appPageSize"
+ [title]="'shared.pagination.size' | translate"
+ class="page-size-select">
+</app-select>
+
+<button (click)="getPreviousPage(pageSizeSelect.appValue)"
+ [class.openk-button-disabled]="!(appPage > 0)"
+ [disabled]="appDisabled || !(appPage > 0)"
+ class="openk-button openk-info page-info--button">
+ <mat-icon class="page-info--button--icon">keyboard_arrow_left</mat-icon>
+</button>
+
+<span
+ [class.disabled]="appDisabled"
+ class="page-info--text">
+ {{appPage == null ? 1 : appPage + 1}} / {{appTotalPages === 0 ? 1 : appTotalPages}}
+</span>
+
+<button (click)="getNextPage(pageSizeSelect.appValue)"
+ [disabled]="appDisabled || appPage >= appTotalPages - 1"
+ class="openk-button openk-info page-info--button">
+ <mat-icon class="page-info--button--icon">keyboard_arrow_right</mat-icon>
+</button>
diff --git a/src/app/shared/layout/pagination-counter/pagination-counter.component.scss b/src/app/shared/layout/pagination-counter/pagination-counter.component.scss
index e6268bb..134a731 100644
--- a/src/app/shared/layout/pagination-counter/pagination-counter.component.scss
+++ b/src/app/shared/layout/pagination-counter/pagination-counter.component.scss
@@ -11,9 +11,11 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-.page-info {
+:host {
display: flex;
justify-content: flex-end;
+ align-items: center;
+ padding-top: 0.2em;
}
.page-info--button {
@@ -33,3 +35,9 @@
pointer-events: none;
opacity: 0.6;
}
+
+.page-size-select {
+ width: 5.5em;
+ font-size: 0.66em;
+}
+
diff --git a/src/app/shared/layout/pagination-counter/pagination-counter.component.spec.ts b/src/app/shared/layout/pagination-counter/pagination-counter.component.spec.ts
index 464321f..71b2a87 100644
--- a/src/app/shared/layout/pagination-counter/pagination-counter.component.spec.ts
+++ b/src/app/shared/layout/pagination-counter/pagination-counter.component.spec.ts
@@ -12,6 +12,7 @@
********************************************************************************/
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {I18nModule} from "../../../core/i18n";
import {PaginationCounterComponent} from "./pagination-counter.component";
import {PaginationCounterModule} from "./pagination-counter.module";
@@ -22,7 +23,8 @@
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
- PaginationCounterModule
+ PaginationCounterModule,
+ I18nModule
]
}).compileComponents();
}));
@@ -40,9 +42,9 @@
it("should emit appPageChange with the correct page number", () => {
component.appPage = 3;
spyOn(component.appPageChange, "emit").and.callThrough();
- component.getNextPage();
- expect(component.appPageChange.emit).toHaveBeenCalledWith(4);
- component.getPreviousPage();
- expect(component.appPageChange.emit).toHaveBeenCalledWith(2);
+ component.getNextPage(10);
+ expect(component.appPageChange.emit).toHaveBeenCalledWith({page: 4, size: 10});
+ component.getPreviousPage(10);
+ expect(component.appPageChange.emit).toHaveBeenCalledWith({page: 2, size: 10});
});
});
diff --git a/src/app/shared/layout/pagination-counter/pagination-counter.component.ts b/src/app/shared/layout/pagination-counter/pagination-counter.component.ts
index 0e89738..c502026 100644
--- a/src/app/shared/layout/pagination-counter/pagination-counter.component.ts
+++ b/src/app/shared/layout/pagination-counter/pagination-counter.component.ts
@@ -12,6 +12,7 @@
********************************************************************************/
import {Component, EventEmitter, Input, Output} from "@angular/core";
+import {ISelectOption} from "../../controls/select/model";
@Component({
selector: "app-pagination-counter",
@@ -27,17 +28,29 @@
public appPage: number;
@Input()
- public appPageSize: number;
+ public appPageSizeOptions: ISelectOption[] = [
+ {label: "10", value: 10},
+ {label: "25", value: 25},
+ {label: "50", value: 50}
+ ];
+
+ @Input()
+ public appPageSize = this.appPageSizeOptions[0].value;
+
+ @Input()
+ public appTotalPages: number;
@Output()
- public appPageChange = new EventEmitter<number>();
+ public appPageChange = new EventEmitter<{ page: number, size: number }>();
- public getPreviousPage() {
- this.appPageChange.emit(this.appPage - 1);
+ public getPreviousPage(pageSize: number) {
+ this.appPageSize = pageSize;
+ this.appPageChange.emit({page: this.appPage - 1, size: this.appPageSize});
}
- public getNextPage() {
- this.appPageChange.emit(this.appPage + 1);
+ public getNextPage(pageSize: number) {
+ this.appPageSize = pageSize;
+ this.appPageChange.emit({page: this.appPage + 1, size: this.appPageSize});
}
}
diff --git a/src/app/shared/layout/pagination-counter/pagination-counter.module.ts b/src/app/shared/layout/pagination-counter/pagination-counter.module.ts
index c440039..15adbe7 100644
--- a/src/app/shared/layout/pagination-counter/pagination-counter.module.ts
+++ b/src/app/shared/layout/pagination-counter/pagination-counter.module.ts
@@ -14,12 +14,16 @@
import {CommonModule} from "@angular/common";
import {NgModule} from "@angular/core";
import {MatIconModule} from "@angular/material/icon";
+import {TranslateModule} from "@ngx-translate/core";
+import {SelectModule} from "../../controls/select";
import {PaginationCounterComponent} from "./pagination-counter.component";
@NgModule({
imports: [
CommonModule,
- MatIconModule
+ MatIconModule,
+ SelectModule,
+ TranslateModule
],
exports: [
PaginationCounterComponent
diff --git a/src/app/shared/layout/side-menu/components/side-menu-container/side-menu-container.component.html b/src/app/shared/layout/side-menu/components/side-menu-container/side-menu-container.component.html
index 98df1f0..59bb656 100644
--- a/src/app/shared/layout/side-menu/components/side-menu-container/side-menu-container.component.html
+++ b/src/app/shared/layout/side-menu/components/side-menu-container/side-menu-container.component.html
@@ -60,7 +60,7 @@
</ng-container>
<div class="main-content main-content---without-scroll">
- <div class="main-content">
+ <div cdkScrollable class="main-content">
<ng-content></ng-content>
</div>
</div>
diff --git a/src/app/shared/layout/side-menu/side-menu.module.ts b/src/app/shared/layout/side-menu/side-menu.module.ts
index 778b5aa..bbb5ec4 100644
--- a/src/app/shared/layout/side-menu/side-menu.module.ts
+++ b/src/app/shared/layout/side-menu/side-menu.module.ts
@@ -11,6 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+import {CdkScrollableModule} from "@angular/cdk/scrolling";
import {CommonModule} from "@angular/common";
import {NgModule} from "@angular/core";
import {MatIconModule} from "@angular/material/icon";
@@ -24,7 +25,8 @@
CommonModule,
ProgressSpinnerModule,
MatIconModule,
- TranslateModule
+ TranslateModule,
+ CdkScrollableModule
],
declarations: [
SideMenuContainerComponent,
diff --git a/src/app/shared/layout/statement-table/components/statement-table.component.html b/src/app/shared/layout/statement-table/components/statement-table.component.html
index 6e3b9bb..55a9f89 100644
--- a/src/app/shared/layout/statement-table/components/statement-table.component.html
+++ b/src/app/shared/layout/statement-table/components/statement-table.component.html
@@ -46,12 +46,22 @@
</td>
</ng-container>
- <ng-container cdkColumnDef="id">
+ <ng-container *ngIf="'id'; let label" cdkColumnDef="id">
<th *cdkHeaderCellDef="let statementHeader"
+ (click)="click(label, sorting[label])"
+ [class.cursor-pointer]="appIsSortable"
cdk-header-cell
class="statement-table--id"
scope="col">
- {{"shared.statementTable.id" | translate}}
+ <div class="statement-table--header statement-table--header---right">
+ {{"shared.statementTable.id" | translate}}
+ <mat-icon
+ *ngIf="sorting[label]"
+ [class.statement-table--header--icon---rotate]="sorting[label] ==='asc'"
+ class="statement-table--header--icon">
+ play_arrow
+ </mat-icon>
+ </div>
</th>
<td *cdkCellDef="let statement" cdk-cell
class="openk-no-whitespace-wrap statement-table--id">
@@ -65,12 +75,22 @@
</td>
</ng-container>
- <ng-container cdkColumnDef="title">
+ <ng-container *ngIf="'title'; let label" cdkColumnDef="title">
<th *cdkHeaderCellDef="let statementHeader"
+ (click)="click(label, sorting[label])"
+ [class.cursor-pointer]="appIsSortable"
cdk-header-cell
class="statement-table--title"
scope="col">
- {{"shared.statementTable.title" | translate}}
+ <div class="statement-table--header">
+ {{"shared.statementTable.title" | translate}}
+ <mat-icon
+ *ngIf="sorting[label]"
+ [class.statement-table--header--icon---rotate]="sorting[label] ==='asc'"
+ class="statement-table--header--icon">
+ play_arrow
+ </mat-icon>
+ </div>
</th>
<td *cdkCellDef="let statement" cdk-cell
class="statement-table--title">
@@ -78,12 +98,22 @@
</td>
</ng-container>
- <ng-container cdkColumnDef="city">
+ <ng-container *ngIf="'city'; let label" cdkColumnDef="city">
<th *cdkHeaderCellDef="let statementHeader"
+ (click)="click(label, sorting[label])"
+ [class.cursor-pointer]="appIsSortable"
cdk-header-cell
class="statement-table--city"
scope="col">
- {{"shared.statementTable.city" | translate}}
+ <div class="statement-table--header">
+ {{"shared.statementTable.city" | translate}}
+ <mat-icon
+ *ngIf="sorting[label]"
+ [class.statement-table--header--icon---rotate]="sorting[label] ==='asc'"
+ class="statement-table--header--icon">
+ play_arrow
+ </mat-icon>
+ </div>
</th>
<td *cdkCellDef="let statement" cdk-cell
class="statement-table--city">
@@ -91,12 +121,22 @@
</td>
</ng-container>
- <ng-container cdkColumnDef="district">
+ <ng-container *ngIf="'district'; let label" cdkColumnDef="district">
<th *cdkHeaderCellDef="let statementHeader"
+ (click)="click(label, sorting[label])"
+ [class.cursor-pointer]="appIsSortable"
cdk-header-cell
class="statement-table--district"
scope="col">
- {{"shared.statementTable.district" | translate}}
+ <div class="statement-table--header">
+ {{"shared.statementTable.district" | translate}}
+ <mat-icon
+ *ngIf="sorting[label]"
+ [class.statement-table--header--icon---rotate]="sorting[label] ==='asc'"
+ class="statement-table--header--icon">
+ play_arrow
+ </mat-icon>
+ </div>
</th>
<td *cdkCellDef="let statement" cdk-cell
class="statement-table--district">
@@ -104,12 +144,22 @@
</td>
</ng-container>
- <ng-container cdkColumnDef="type">
- <th *cdkHeaderCellDef="let statementHeader"
+ <ng-container *ngIf="'typeId'; let label" cdkColumnDef="type">
+ <th (click)="click(label, sorting[label])"
+ *cdkHeaderCellDef="let statementHeader;"
+ [class.cursor-pointer]="appIsSortable"
cdk-header-cell
class="statement-table--type"
scope="col">
- {{"shared.statementTable.statementType" | translate}}
+ <div class="statement-table--header">
+ {{"shared.statementTable.statementType" | translate}}
+ <mat-icon
+ *ngIf="sorting[label]"
+ [class.statement-table--header--icon---rotate]="sorting[label] ==='asc'"
+ class="statement-table--header--icon">
+ play_arrow
+ </mat-icon>
+ </div>
</th>
<td *cdkCellDef="let statement" cdk-cell
class="openk-no-whitespace-wrap statement-table--type">
@@ -117,12 +167,22 @@
</td>
</ng-container>
- <ng-container cdkColumnDef="creationDate">
+ <ng-container *ngIf="'creationDate'; let label" cdkColumnDef="creationDate">
<th *cdkHeaderCellDef="let statementHeader"
+ (click)="click(label, sorting[label])"
+ [class.cursor-pointer]="appIsSortable"
cdk-header-cell
class="statement-table--date"
scope="col">
- {{"shared.statementTable.creationDate" | translate}}
+ <div class="statement-table--header">
+ {{"shared.statementTable.creationDate" | translate}}
+ <mat-icon
+ *ngIf="sorting[label]"
+ [class.statement-table--header--icon---rotate]="sorting[label] ==='asc'"
+ class="statement-table--header--icon">
+ play_arrow
+ </mat-icon>
+ </div>
</th>
<td *cdkCellDef="let statement" cdk-cell
class="openk-no-whitespace-wrap statement-table--date">
@@ -130,12 +190,22 @@
</td>
</ng-container>
- <ng-container cdkColumnDef="receiptDate">
+ <ng-container *ngIf="'receiptDate'; let label" cdkColumnDef="receiptDate">
<th *cdkHeaderCellDef="let statementHeader"
+ (click)="click(label, sorting[label])"
+ [class.cursor-pointer]="appIsSortable"
cdk-header-cell
class="statement-table--date"
scope="col">
- {{"shared.statementTable.receiptDate" | translate}}
+ <div class="statement-table--header">
+ {{"shared.statementTable.receiptDate" | translate}}
+ <mat-icon
+ *ngIf="sorting[label]"
+ [class.statement-table--header--icon---rotate]="sorting[label] ==='asc'"
+ class="statement-table--header--icon">
+ play_arrow
+ </mat-icon>
+ </div>
</th>
<td *cdkCellDef="let statement" cdk-cell
class="openk-no-whitespace-wrap statement-table--date">
@@ -143,12 +213,22 @@
</td>
</ng-container>
- <ng-container cdkColumnDef="dueDate">
+ <ng-container *ngIf="'dueDate'; let label" cdkColumnDef="dueDate">
<th *cdkHeaderCellDef="let statementHeader"
+ (click)="click(label, sorting[label])"
+ [class.cursor-pointer]="appIsSortable"
cdk-header-cell
class="statement-table--date"
scope="col">
- {{"shared.statementTable.dueDate" | translate}}
+ <div class="statement-table--header">
+ {{"shared.statementTable.dueDate" | translate}}
+ <mat-icon
+ *ngIf="sorting[label]"
+ [class.statement-table--header--icon---rotate]="sorting[label] ==='asc'"
+ class="statement-table--header--icon">
+ play_arrow
+ </mat-icon>
+ </div>
</th>
<td *cdkCellDef="let statement" cdk-cell
class="openk-no-whitespace-wrap statement-table--date">
diff --git a/src/app/shared/layout/statement-table/components/statement-table.component.scss b/src/app/shared/layout/statement-table/components/statement-table.component.scss
index 9304f8d..50c12ba 100644
--- a/src/app/shared/layout/statement-table/components/statement-table.component.scss
+++ b/src/app/shared/layout/statement-table/components/statement-table.component.scss
@@ -73,7 +73,7 @@
}
.statement-table--id {
- width: 1em;
+ width: 2em;
text-align: right;
}
@@ -132,3 +132,26 @@
.statement-table--button {
font-size: 0.7em;
}
+
+.statement-table--header {
+ display: flex;
+ align-items: center;
+ line-height: 1;
+}
+
+.statement-table--header---right {
+ justify-content: flex-end;
+}
+
+.statement-table--header--icon {
+ width: initial;
+ height: initial;
+ font-size: 1em;
+ transform: rotate(90deg);
+ line-height: 1;
+ transition: transform 100ms ease-in-out;
+}
+
+.statement-table--header--icon---rotate {
+ transform: rotate(270deg);
+}
diff --git a/src/app/shared/layout/statement-table/components/statement-table.component.spec.ts b/src/app/shared/layout/statement-table/components/statement-table.component.spec.ts
index 8e675ec..d68eddd 100644
--- a/src/app/shared/layout/statement-table/components/statement-table.component.spec.ts
+++ b/src/app/shared/layout/statement-table/components/statement-table.component.spec.ts
@@ -54,4 +54,5 @@
expect(component.trackBy(1919, {...{} as IStatementTableEntry})).not.toBeDefined();
expect(component.trackBy(1919, {...{} as IStatementTableEntry, id: 19})).toBe(19);
});
+
});
diff --git a/src/app/shared/layout/statement-table/components/statement-table.component.ts b/src/app/shared/layout/statement-table/components/statement-table.component.ts
index c569bd4..5a89f2b 100644
--- a/src/app/shared/layout/statement-table/components/statement-table.component.ts
+++ b/src/app/shared/layout/statement-table/components/statement-table.component.ts
@@ -101,9 +101,17 @@
@Input()
public appTimeDisplayFormat: string = momentFormatDisplayNumeric;
+ @Input()
+ public appIsSortable: boolean;
+
+ @Output()
+ public appSort: EventEmitter<{ label: string, direction: "asc" | "desc" }> = new EventEmitter();
+
@Output()
public appSelect: EventEmitter<{ id: number, value: boolean }> = new EventEmitter();
+ public sorting: { [x: string]: "asc" | "desc" } = {};
+
public trackBy(index: number, entry: IStatementTableEntry) {
return entry?.id;
}
@@ -116,4 +124,13 @@
this.appSelect.emit({id, value: false});
}
+ public click(label: string, direction: "asc" | "desc") {
+ if (this.appIsSortable) {
+ direction = direction == null ? "asc" : (direction === "asc" ? "desc" : "asc");
+ this.appSort.emit({label, direction});
+ this.sorting = {};
+ this.sorting[label] = direction;
+ }
+ }
+
}
diff --git a/src/app/features/search/components/search/index.ts b/src/app/shared/leaflet/components/index.ts
similarity index 92%
copy from src/app/features/search/components/search/index.ts
copy to src/app/shared/leaflet/components/index.ts
index 5eb9c7f..9f971c2 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/shared/leaflet/components/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./leaflet-map.component";
diff --git a/src/app/shared/leaflet/components/leaflet-map.component.html b/src/app/shared/leaflet/components/leaflet-map.component.html
new file mode 100644
index 0000000..d71960e
--- /dev/null
+++ b/src/app/shared/leaflet/components/leaflet-map.component.html
@@ -0,0 +1,38 @@
+<!-------------------------------------------------------------------------------
+ * 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="map">
+ <div #appLeaflet="appLeaflet"
+ (appClick)="appClick.emit($event)"
+ (appPopupClose)="appPopupClose.emit($event)"
+ (appLatLngZoomChange)="appCenterChange.emit($event)"
+ [appCenter]="appCenter | stringToLatLngZoom"
+ [appDisabled]="appDisabled"
+ appLeaflet
+ class="map--leaflet">
+
+ <ng-content></ng-content>
+
+ </div>
+
+ <app-action-button
+ (appClick)="appOpenGis.emit(appLeaflet.getBounds())"
+ [appIcon]="'my_location'"
+ class="map--button openk-info">
+ {{'shared.map.openGIS' | translate}}
+ </app-action-button>
+</div>
+
+<label *ngIf="appSubCaption" class="sub-caption">
+ {{appSubCaption}}
+</label>
diff --git a/src/app/shared/leaflet/components/leaflet-map.component.scss b/src/app/shared/leaflet/components/leaflet-map.component.scss
new file mode 100644
index 0000000..e8e1a36
--- /dev/null
+++ b/src/app/shared/leaflet/components/leaflet-map.component.scss
@@ -0,0 +1,53 @@
+/********************************************************************************
+ * 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%;
+
+ display: flex;
+ flex-flow: column;
+}
+
+.map {
+ height: 100%;
+ width: 100%;
+ position: relative;
+ overflow: hidden;
+ box-sizing: border-box;
+ border: 1px solid $openk-form-border;
+}
+
+.map--leaflet {
+ width: 100%;
+ height: 100%;
+}
+
+.map--button {
+ display: block;
+ width: fit-content;
+ height: fit-content;
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+ z-index: 1000;
+}
+
+.sub-caption {
+ color: $openk-form-border;
+ margin-left: auto;
+ font-size: smaller;
+ font-style: italic;
+}
diff --git a/src/app/features/search/components/search/search.component.spec.ts b/src/app/shared/leaflet/components/leaflet-map.component.spec.ts
similarity index 69%
rename from src/app/features/search/components/search/search.component.spec.ts
rename to src/app/shared/leaflet/components/leaflet-map.component.spec.ts
index ec7b1b3..f839910 100644
--- a/src/app/features/search/components/search/search.component.spec.ts
+++ b/src/app/shared/leaflet/components/leaflet-map.component.spec.ts
@@ -12,20 +12,22 @@
********************************************************************************/
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
-import {SearchComponent} from "./search.component";
+import {I18nModule} from "../../../core/i18n";
+import {LeafletModule} from "../leaflet.module";
+import {LeafletMapComponent} from "./leaflet-map.component";
-describe("SearchComponent", () => {
- let component: SearchComponent;
- let fixture: ComponentFixture<SearchComponent>;
+describe("LeafletMapComponent", () => {
+ let component: LeafletMapComponent;
+ let fixture: ComponentFixture<LeafletMapComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
- declarations: [SearchComponent]
+ imports: [LeafletModule, I18nModule]
}).compileComponents();
}));
beforeEach(() => {
- fixture = TestBed.createComponent(SearchComponent);
+ fixture = TestBed.createComponent(LeafletMapComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
diff --git a/src/app/shared/leaflet/components/leaflet-map.component.ts b/src/app/shared/leaflet/components/leaflet-map.component.ts
new file mode 100644
index 0000000..53e8e48
--- /dev/null
+++ b/src/app/shared/leaflet/components/leaflet-map.component.ts
@@ -0,0 +1,63 @@
+/********************************************************************************
+ * 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, forwardRef, Input, NgZone, Output, ViewChild} from "@angular/core";
+import {LeafletMouseEvent, PopupEvent} from "leaflet";
+import {ILeafletBounds, LeafletDirective, LeafletHandler} from "../directives";
+
+@Component({
+ selector: "app-leaflet-map",
+ templateUrl: "./leaflet-map.component.html",
+ styleUrls: ["./leaflet-map.component.scss"],
+ providers: [
+ {
+ provide: LeafletHandler,
+ useExisting: forwardRef(() => LeafletMapComponent)
+ }
+ ]
+})
+export class LeafletMapComponent extends LeafletHandler {
+
+ @Input()
+ public appDisabled: boolean;
+
+ @Input()
+ public appCenter: string;
+
+ @Output()
+ public appClick = new EventEmitter<LeafletMouseEvent>();
+
+ @Output()
+ public appPopupClose = new EventEmitter<PopupEvent>();
+
+ @Output()
+ public appCenterChange = new EventEmitter<string>();
+
+ @Input()
+ public appSubCaption: string;
+
+ @Output()
+ public appOpenGis = new EventEmitter<ILeafletBounds>();
+
+ @ViewChild(LeafletDirective, {static: true})
+ public leafletDirective: LeafletDirective;
+
+ public constructor(public readonly ngZone: NgZone) {
+ super();
+ }
+
+ public get instance() {
+ return this.leafletDirective.instance;
+ }
+
+}
diff --git a/src/app/shared/layout/leaflet/directives/center-marker/index.ts b/src/app/shared/leaflet/directives/center-marker/index.ts
similarity index 100%
rename from src/app/shared/layout/leaflet/directives/center-marker/index.ts
rename to src/app/shared/leaflet/directives/center-marker/index.ts
diff --git a/src/app/shared/layout/leaflet/directives/center-marker/leaflet-center-marker.directive.spec.ts b/src/app/shared/leaflet/directives/center-marker/leaflet-center-marker.directive.spec.ts
similarity index 96%
rename from src/app/shared/layout/leaflet/directives/center-marker/leaflet-center-marker.directive.spec.ts
rename to src/app/shared/leaflet/directives/center-marker/leaflet-center-marker.directive.spec.ts
index 1da4d1b..6922af1 100644
--- a/src/app/shared/layout/leaflet/directives/center-marker/leaflet-center-marker.directive.spec.ts
+++ b/src/app/shared/leaflet/directives/center-marker/leaflet-center-marker.directive.spec.ts
@@ -47,7 +47,7 @@
const center = new LatLng(latLng.lat, latLng.lng);
const move$ = new Subject<any>();
const setLatLngSpy = spyOn(directive.marker, "setLatLng").and.callThrough();
- spyOn(directive.leafletDirective.leaflet, "getCenter").and.returnValue(center);
+ spyOn(directive.leafletHandler.instance, "getCenter").and.returnValue(center);
spyOn(directive, "on").and.returnValue(move$.asObservable());
directive.ngOnInit();
move$.next();
diff --git a/src/app/shared/layout/leaflet/directives/center-marker/leaflet-center-marker.directive.ts b/src/app/shared/leaflet/directives/center-marker/leaflet-center-marker.directive.ts
similarity index 80%
rename from src/app/shared/layout/leaflet/directives/center-marker/leaflet-center-marker.directive.ts
rename to src/app/shared/leaflet/directives/center-marker/leaflet-center-marker.directive.ts
index 9fa22aa..9db8965 100644
--- a/src/app/shared/layout/leaflet/directives/center-marker/leaflet-center-marker.directive.ts
+++ b/src/app/shared/leaflet/directives/center-marker/leaflet-center-marker.directive.ts
@@ -14,9 +14,9 @@
import {Directive, Inject, NgZone, OnDestroy, OnInit} from "@angular/core";
import {merge, of, Subject} from "rxjs";
import {takeUntil} from "rxjs/operators";
-import {runOutsideZone} from "../../../../../util/rxjs";
+import {runOutsideZone} from "../../../../util/rxjs";
import {ILeafletConfiguration, LEAFLET_CONFIGURATION_TOKEN} from "../../leaflet-configuration.token";
-import {LeafletDirective} from "../leaflet";
+import {LeafletHandler} from "../leaflet";
import {AbstractLeafletMarkerDirective} from "../marker/abstract-leaflet-marker.directive";
@Directive({
@@ -29,16 +29,16 @@
public constructor(
ngZone: NgZone,
- leafletDirective: LeafletDirective,
+ leafletHandler: LeafletHandler,
@Inject(LEAFLET_CONFIGURATION_TOKEN) configuration: ILeafletConfiguration,
) {
- super(ngZone, leafletDirective, configuration);
+ super(ngZone, leafletHandler, configuration);
}
- public ngOnInit() {
- merge(of(null), this.leafletDirective.on("move", true))
+ public async ngOnInit() {
+ merge(of(null), this.leafletHandler.on("move", true))
.pipe(takeUntil(this.destroy$), runOutsideZone(this.ngZone))
- .subscribe(() => this.setLatLng(this.leafletDirective.leaflet.getCenter()));
+ .subscribe(() => this.setLatLng(this.leafletHandler.instance.getCenter()));
}
public ngOnDestroy() {
diff --git a/src/app/shared/layout/leaflet/directives/index.ts b/src/app/shared/leaflet/directives/index.ts
similarity index 96%
rename from src/app/shared/layout/leaflet/directives/index.ts
rename to src/app/shared/leaflet/directives/index.ts
index 34d7752..d2be228 100644
--- a/src/app/shared/layout/leaflet/directives/index.ts
+++ b/src/app/shared/leaflet/directives/index.ts
@@ -14,3 +14,4 @@
export * from "./center-marker";
export * from "./leaflet";
export * from "./marker";
+export * from "./popup";
diff --git a/src/app/shared/leaflet/directives/leaflet/LeafletHandler.ts b/src/app/shared/leaflet/directives/leaflet/LeafletHandler.ts
new file mode 100644
index 0000000..3b55f0e
--- /dev/null
+++ b/src/app/shared/leaflet/directives/leaflet/LeafletHandler.ts
@@ -0,0 +1,35 @@
+/********************************************************************************
+ * 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 {NgZone} from "@angular/core";
+import {LeafletEvent, Map} from "leaflet";
+import {Observable, of, pipe} from "rxjs";
+import {switchMap} from "rxjs/operators";
+import {runInZone, runOutsideZone} from "../../../../util/rxjs";
+import {fromLeafletEvent} from "../../util";
+
+export abstract class LeafletHandler {
+
+ public abstract readonly instance: Map;
+
+ public abstract readonly ngZone: NgZone;
+
+ public on<T extends LeafletEvent>(type: string, outsideZone?: boolean): Observable<T> {
+ return of(type).pipe(
+ runOutsideZone(this.ngZone),
+ switchMap((_) => fromLeafletEvent<T>(this.instance, type)),
+ outsideZone ? pipe() : runInZone(this.ngZone)
+ );
+ }
+
+}
diff --git a/src/app/shared/layout/leaflet/directives/leaflet/index.ts b/src/app/shared/leaflet/directives/leaflet/index.ts
similarity index 94%
rename from src/app/shared/layout/leaflet/directives/leaflet/index.ts
rename to src/app/shared/leaflet/directives/leaflet/index.ts
index 6ecc261..d6c1b2f 100644
--- a/src/app/shared/layout/leaflet/directives/leaflet/index.ts
+++ b/src/app/shared/leaflet/directives/leaflet/index.ts
@@ -12,3 +12,4 @@
********************************************************************************/
export * from "./leaflet.directive";
+export * from "./LeafletHandler";
diff --git a/src/app/shared/layout/leaflet/directives/leaflet/leaflet.directive.spec.ts b/src/app/shared/leaflet/directives/leaflet/leaflet.directive.spec.ts
similarity index 72%
rename from src/app/shared/layout/leaflet/directives/leaflet/leaflet.directive.spec.ts
rename to src/app/shared/leaflet/directives/leaflet/leaflet.directive.spec.ts
index 3e85b17..ddf71fc 100644
--- a/src/app/shared/layout/leaflet/directives/leaflet/leaflet.directive.spec.ts
+++ b/src/app/shared/leaflet/directives/leaflet/leaflet.directive.spec.ts
@@ -45,18 +45,37 @@
directive = component.directive;
});
- it("should", () => {
+ it("should set center and zoom", () => {
const latLngZoom: LatLngLiteral & { zoom: number } = {
lat: 52.520008,
lng: 13.404954,
zoom: 13
};
- directive.appView = null;
- directive.appView = latLngZoom;
- expect(directive.leaflet.getCenter().lat).toBe(latLngZoom.lat);
- expect(directive.leaflet.getCenter().lng).toBe(latLngZoom.lng);
- expect(directive.leaflet.getZoom()).toBe(latLngZoom.zoom);
+ directive.appCenter = null;
+ directive.appCenter = latLngZoom;
+ expect(directive.instance.getCenter().lat).toBe(latLngZoom.lat);
+ expect(directive.instance.getCenter().lng).toBe(latLngZoom.lng);
+ expect(directive.instance.getZoom()).toBe(latLngZoom.zoom);
+ });
+
+ it("should disable/enable all leaflet controls", () => {
+ const handlers = [
+ directive.instance.dragging,
+ directive.instance.touchZoom,
+ directive.instance.doubleClickZoom,
+ directive.instance.scrollWheelZoom,
+ directive.instance.boxZoom,
+ directive.instance.keyboard
+ ];
+ handlers.forEach((handler) => spyOn(handler, "disable"));
+ handlers.forEach((handler) => spyOn(handler, "enable"));
+
+ directive.appDisabled = true;
+ handlers.forEach((handler) => expect(handler.disable).toHaveBeenCalled());
+
+ directive.appDisabled = false;
+ handlers.forEach((handler) => expect(handler.enable).toHaveBeenCalled());
});
it("should invalidate size on resize", () => {
@@ -85,12 +104,19 @@
onSpy.calls.reset();
subscription = directive.appLatLngZoomChange.subscribe();
- expect(onSpy).toHaveBeenCalledWith("move zoom", true);
+ expect(onSpy).toHaveBeenCalledWith("moveend zoomend", true);
event$.next();
tick(10);
subscription.unsubscribe();
onSpy.calls.reset();
+ subscription = directive.appPopupClose.subscribe();
+ expect(onSpy).toHaveBeenCalledWith("popupclose");
+ event$.next();
+ tick(0);
+ subscription.unsubscribe();
+
+ onSpy.calls.reset();
subscription = directive.appClick.subscribe();
expect(onSpy).toHaveBeenCalledWith("click");
subscription.unsubscribe();
diff --git a/src/app/shared/leaflet/directives/leaflet/leaflet.directive.ts b/src/app/shared/leaflet/directives/leaflet/leaflet.directive.ts
new file mode 100644
index 0000000..bbcc73d
--- /dev/null
+++ b/src/app/shared/leaflet/directives/leaflet/leaflet.directive.ts
@@ -0,0 +1,135 @@
+/********************************************************************************
+ * 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 {AfterViewInit, Directive, ElementRef, forwardRef, Inject, Input, NgZone, OnDestroy, OnInit, Optional, Output} from "@angular/core";
+import {InvalidateSizeOptions, LatLngLiteral, LeafletMouseEvent, Map, PopupEvent, TileLayer} from "leaflet";
+import {defer, Observable} from "rxjs";
+import {debounceTime, delay, map, takeUntil, throttleTime} from "rxjs/operators";
+import {runInZone} from "../../../../util/rxjs";
+import {ILeafletConfiguration, LEAFLET_CONFIGURATION_TOKEN, LEAFLET_RESIZE_TOKEN} from "../../leaflet-configuration.token";
+import {latLngZoomToString} from "../../util";
+import {LeafletHandler} from "./LeafletHandler";
+
+export interface ILeafletBounds {
+ northWest: LatLngLiteral;
+ northEast: LatLngLiteral;
+ southEast: LatLngLiteral;
+ southWest: LatLngLiteral;
+ center: LatLngLiteral;
+ zoom: number;
+}
+
+@Directive({
+ selector: "[appLeaflet]",
+ exportAs: "appLeaflet",
+ providers: [
+ {
+ provide: LeafletHandler,
+ useExisting: forwardRef(() => LeafletDirective)
+ }
+ ]
+})
+export class LeafletDirective extends LeafletHandler implements OnInit, OnDestroy, AfterViewInit {
+
+ public readonly instance: Map;
+
+ @Output()
+ public appClick = defer(() => this.on<LeafletMouseEvent>("click"));
+
+ @Output()
+ public appPopupClose = defer(() => this.on<PopupEvent>("popupclose")).pipe(delay(0));
+
+ @Output()
+ public appUnload$ = defer(() => this.on("unload"));
+
+ @Output()
+ public appLatLngZoomChange = defer(() => this.on("moveend zoomend", true)).pipe(
+ debounceTime(10),
+ map(() => latLngZoomToString(this.instance.getCenter(), this.instance.getZoom())),
+ runInZone(this.ngZone)
+ );
+
+ public constructor(
+ public readonly elementRef: ElementRef<HTMLElement>,
+ public readonly ngZone: NgZone,
+ @Inject(LEAFLET_CONFIGURATION_TOKEN) public readonly configuration: ILeafletConfiguration,
+ @Optional() @Inject(LEAFLET_RESIZE_TOKEN) public resize$: Observable<any>
+ ) {
+ super();
+ this.instance = this.ngZone.runOutsideAngular(() => {
+ const tileLayer = new TileLayer(this.configuration.urlTemplate, {
+ attribution: this.configuration.attribution
+ });
+ const result = new Map(this.elementRef.nativeElement, {layers: [tileLayer]});
+ result.setView({lat: configuration.lat, lng: configuration.lng}, configuration.zoom);
+ return result;
+ });
+ }
+
+ @Input()
+ public set appCenter(value: { lat: number, lng: number, zoom: number }) {
+ if (value != null) {
+ this.ngZone.runOutsideAngular(() => this.instance.setView(value, value.zoom));
+ }
+ }
+
+ @Input()
+ public set appDisabled(value: boolean) {
+ const handlers = [
+ this.instance.dragging,
+ this.instance.touchZoom,
+ this.instance.doubleClickZoom,
+ this.instance.scrollWheelZoom,
+ this.instance.boxZoom,
+ this.instance.keyboard
+ ];
+ handlers.forEach((handler) => value ? handler.disable() : handler.enable());
+ value ? this.instance.removeControl(this.instance.zoomControl) : this.instance.addControl(this.instance.zoomControl);
+ }
+
+ public ngOnInit() {
+ if (this.resize$ instanceof Observable) {
+ this.resize$.pipe(throttleTime(10), takeUntil(this.appUnload$))
+ .subscribe((_) => this.invalidateSize());
+ }
+ }
+
+ public ngAfterViewInit() {
+ setTimeout(() => this.invalidateSize());
+ }
+
+ public ngOnDestroy() {
+ this.ngZone.runOutsideAngular(() => this.instance.remove());
+ }
+
+ public getBounds(): ILeafletBounds {
+ return this.ngZone.runOutsideAngular(() => {
+ const bounds = this.instance.getBounds();
+ const zoom = this.instance.getZoom();
+ const result: ILeafletBounds = {
+ northWest: bounds.getNorthWest(),
+ northEast: bounds.getNorthEast(),
+ southEast: bounds.getSouthEast(),
+ southWest: bounds.getSouthWest(),
+ center: bounds.getCenter(),
+ zoom
+ };
+ return this.ngZone.run(() => result);
+ });
+ }
+
+ public invalidateSize(options?: boolean | InvalidateSizeOptions) {
+ this.ngZone.runOutsideAngular(() => this.instance.invalidateSize(options));
+ }
+
+}
diff --git a/src/app/shared/layout/leaflet/directives/marker/abstract-leaflet-marker.directive.ts b/src/app/shared/leaflet/directives/marker/abstract-leaflet-marker.directive.ts
similarity index 87%
rename from src/app/shared/layout/leaflet/directives/marker/abstract-leaflet-marker.directive.ts
rename to src/app/shared/leaflet/directives/marker/abstract-leaflet-marker.directive.ts
index e51a197..b2ed655 100644
--- a/src/app/shared/layout/leaflet/directives/marker/abstract-leaflet-marker.directive.ts
+++ b/src/app/shared/leaflet/directives/marker/abstract-leaflet-marker.directive.ts
@@ -15,10 +15,10 @@
import {DivIcon, LatLngLiteral, LeafletEvent, LeafletMouseEvent, Marker} from "leaflet";
import {defer, Observable, of, pipe} from "rxjs";
import {switchMap} from "rxjs/operators";
-import {runInZone, runOutsideZone} from "../../../../../util/rxjs";
+import {runInZone, runOutsideZone} from "../../../../util/rxjs";
import {ILeafletConfiguration, LEAFLET_CONFIGURATION_TOKEN} from "../../leaflet-configuration.token";
import {fromLeafletEvent} from "../../util";
-import {LeafletDirective} from "../leaflet";
+import {LeafletHandler} from "../leaflet";
@Directive({})
@@ -33,7 +33,7 @@
public constructor(
public readonly ngZone: NgZone,
- public readonly leafletDirective: LeafletDirective,
+ public readonly leafletHandler: LeafletHandler,
@Inject(LEAFLET_CONFIGURATION_TOKEN) public readonly configuration: ILeafletConfiguration,
) {
@@ -53,9 +53,9 @@
protected setLatLng(value?: LatLngLiteral): boolean {
return this.ngZone.runOutsideAngular(() => {
- if (Number.isFinite(value?.lat) && Number.isFinite(value?.lng)) {
+ if (value != null && Number.isFinite(value.lat) && Number.isFinite(value.lng)) {
this.marker.setLatLng(value);
- this.marker.addTo(this.leafletDirective.leaflet);
+ this.marker.addTo(this.leafletHandler.instance);
return true;
}
return false;
diff --git a/src/app/shared/layout/leaflet/directives/marker/index.ts b/src/app/shared/leaflet/directives/marker/index.ts
similarity index 100%
rename from src/app/shared/layout/leaflet/directives/marker/index.ts
rename to src/app/shared/leaflet/directives/marker/index.ts
diff --git a/src/app/shared/layout/leaflet/directives/marker/leaflet-marker.directive.spec.ts b/src/app/shared/leaflet/directives/marker/leaflet-marker.directive.spec.ts
similarity index 94%
rename from src/app/shared/layout/leaflet/directives/marker/leaflet-marker.directive.spec.ts
rename to src/app/shared/leaflet/directives/marker/leaflet-marker.directive.spec.ts
index c5b474c..24f654f 100644
--- a/src/app/shared/layout/leaflet/directives/marker/leaflet-marker.directive.spec.ts
+++ b/src/app/shared/leaflet/directives/marker/leaflet-marker.directive.spec.ts
@@ -50,6 +50,10 @@
expect(directive.marker.getLatLng().lat).toEqual(latLng.lat);
expect(directive.marker.getLatLng().lng).toEqual(latLng.lng);
+ directive.appLeafletMarker = "1,2,12";
+ expect(directive.marker.getLatLng().lat).toEqual(1);
+ expect(directive.marker.getLatLng().lng).toEqual(2);
+
directive.appLeafletMarker = {lat: 1, lng: null};
expect(removeSpy).toHaveBeenCalled();
directive.appLeafletMarker = {lat: null, lng: 2};
diff --git a/src/app/shared/layout/leaflet/directives/marker/leaflet-marker.directive.ts b/src/app/shared/leaflet/directives/marker/leaflet-marker.directive.ts
similarity index 78%
rename from src/app/shared/layout/leaflet/directives/marker/leaflet-marker.directive.ts
rename to src/app/shared/leaflet/directives/marker/leaflet-marker.directive.ts
index 9ea0814..8b40418 100644
--- a/src/app/shared/layout/leaflet/directives/marker/leaflet-marker.directive.ts
+++ b/src/app/shared/leaflet/directives/marker/leaflet-marker.directive.ts
@@ -14,7 +14,8 @@
import {Directive, Inject, Input, NgZone} from "@angular/core";
import {LatLngLiteral} from "leaflet";
import {ILeafletConfiguration, LEAFLET_CONFIGURATION_TOKEN} from "../../leaflet-configuration.token";
-import {LeafletDirective} from "../leaflet";
+import {stringToLatLngZoom} from "../../util";
+import {LeafletHandler} from "../leaflet";
import {AbstractLeafletMarkerDirective} from "./abstract-leaflet-marker.directive";
@Directive({
@@ -25,15 +26,15 @@
public constructor(
ngZone: NgZone,
- leafletDirective: LeafletDirective,
+ leafletHandler: LeafletHandler,
@Inject(LEAFLET_CONFIGURATION_TOKEN) configuration: ILeafletConfiguration,
) {
- super(ngZone, leafletDirective, configuration);
+ super(ngZone, leafletHandler, configuration);
}
@Input()
- public set appLeafletMarker(value: LatLngLiteral) {
- const isSet = this.setLatLng(value);
+ public set appLeafletMarker(value: LatLngLiteral | string) {
+ const isSet = this.setLatLng(typeof value === "string" ? stringToLatLngZoom(value) : value);
if (!isSet) {
this.remove();
}
diff --git a/src/app/features/search/components/search/index.ts b/src/app/shared/leaflet/directives/popup/index.ts
similarity index 92%
copy from src/app/features/search/components/search/index.ts
copy to src/app/shared/leaflet/directives/popup/index.ts
index 5eb9c7f..09e6c2a 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/shared/leaflet/directives/popup/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./leaflet-popup.directive";
diff --git a/src/app/shared/leaflet/directives/popup/leaflet-popup.directive.spec.ts b/src/app/shared/leaflet/directives/popup/leaflet-popup.directive.spec.ts
new file mode 100644
index 0000000..fc8d984
--- /dev/null
+++ b/src/app/shared/leaflet/directives/popup/leaflet-popup.directive.spec.ts
@@ -0,0 +1,109 @@
+/********************************************************************************
+ * 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
+ ********************************************************************************/
+
+/********************************************************************************
+ * 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, ViewChild} from "@angular/core";
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {LeafletModule} from "../../leaflet.module";
+import {LeafletPopupDirective} from "./leaflet-popup.directive";
+
+describe("LeafletPopupDirective", () => {
+
+ const latLngZoom = "49.87774673189807,8.651438355445864,17";
+
+ let component: LeafletPopupSpecComponent;
+ let fixture: ComponentFixture<LeafletPopupSpecComponent>;
+ let directive: LeafletPopupDirective;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [LeafletModule],
+ declarations: [LeafletPopupSpecComponent]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LeafletPopupSpecComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ directive = component.directive;
+ });
+
+ it("should create", () => {
+ expect(directive).toBeDefined();
+ });
+
+ it("should open/close popup on input", () => {
+ const openOnSpy = spyOn(directive.popup, "openOn");
+ const removeFromSpy = spyOn(directive.popup, "removeFrom");
+
+ openOnSpy.calls.reset();
+ removeFromSpy.calls.reset();
+ directive.appLeafletPopup = latLngZoom;
+ fixture.detectChanges();
+ expect(openOnSpy).toHaveBeenCalledWith(directive.leafletHandler.instance);
+ expect(removeFromSpy).not.toHaveBeenCalled();
+
+ openOnSpy.calls.reset();
+ removeFromSpy.calls.reset();
+ directive.appLeafletPopup = null;
+ fixture.detectChanges();
+ expect(removeFromSpy).toHaveBeenCalledWith(directive.leafletHandler.instance);
+ expect(openOnSpy).not.toHaveBeenCalled();
+ });
+
+ it("should update popup on data changes", () => {
+ const updateViewSpy = spyOn(directive, "updateView");
+ updateViewSpy.calls.reset();
+ component.data = {id: 19};
+ fixture.detectChanges();
+ expect(updateViewSpy).toHaveBeenCalled();
+
+ updateViewSpy.calls.reset();
+ component.coordinates = latLngZoom;
+ fixture.detectChanges();
+ expect(updateViewSpy).not.toHaveBeenCalled();
+ });
+
+});
+
+@Component({
+ selector: "app-leaflet-popup-spec",
+ template: `
+ <div appLeaflet>
+ <div *appLeafletPopup="coordinates; data: data"></div>
+ </div>
+ `
+})
+class LeafletPopupSpecComponent {
+
+ public coordinates: string;
+
+ public data: any = {};
+
+ @ViewChild(LeafletPopupDirective, {static: true})
+ public directive: LeafletPopupDirective;
+
+}
diff --git a/src/app/shared/leaflet/directives/popup/leaflet-popup.directive.ts b/src/app/shared/leaflet/directives/popup/leaflet-popup.directive.ts
new file mode 100644
index 0000000..e02030c
--- /dev/null
+++ b/src/app/shared/leaflet/directives/popup/leaflet-popup.directive.ts
@@ -0,0 +1,97 @@
+/********************************************************************************
+ * 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 {Directive, EmbeddedViewRef, Input, NgZone, OnChanges, OnDestroy, SimpleChanges, TemplateRef} from "@angular/core";
+import {LatLngExpression, Popup} from "leaflet";
+import {stringToLatLngZoom} from "../../util";
+import {LeafletHandler} from "../leaflet";
+
+/**
+ * This structural directive renders an Angular template as a popup in a leaflet map instance.
+ * Since leaflet is running outside Angular's zone, change detection must be either triggered manually via the updateView method or
+ * by providing an input for appLeafletPopupData which is then tracked by Angular itself.
+ *
+ * The popup is only rendered if proper coordinates are provided via the input appLeafletPopup.
+ */
+@Directive({selector: "[appLeafletPopup]"})
+export class LeafletPopupDirective implements OnChanges, OnDestroy {
+
+ /**
+ * Data object which is tracked by Angular and updates the view.
+ */
+ @Input()
+ public appLeafletPopupData: any;
+
+ public readonly embeddedViewRef: EmbeddedViewRef<any>;
+
+ public readonly popup: Popup = new Popup({closeOnClick: false, offset: [0.25, -11]});
+
+ public constructor(
+ public readonly templateRef: TemplateRef<any>,
+ public readonly ngZone: NgZone,
+ public readonly leafletHandler: LeafletHandler
+ ) {
+ this.embeddedViewRef = this.templateRef.createEmbeddedView({});
+ this.embeddedViewRef.detectChanges();
+ const content = this.embeddedViewRef.rootNodes[0];
+ this.ngZone.run(() => this.popup.setContent(content));
+ }
+
+ /**
+ * If proper coordinates are provided, the leaflet popup instance is opened and set to the given coordinates.
+ * Otherwise, the popup is removed from the map.
+ */
+ @Input()
+ public set appLeafletPopup(latLngZoom: string) {
+ const position = stringToLatLngZoom(latLngZoom);
+ if (position == null) {
+ this.close();
+ } else {
+ this.setLatLng(position);
+ this.open();
+ }
+ }
+
+ public ngOnDestroy() {
+ this.close();
+ this.embeddedViewRef.destroy();
+ }
+
+ public ngOnChanges(changes: SimpleChanges) {
+ const updateOn: Array<keyof LeafletPopupDirective> = ["appLeafletPopupData"];
+ if (updateOn.some((key) => changes[key] != null)) {
+ this.updateView();
+ }
+ }
+
+ /**
+ * Triggers the change detection for the embedded view provided by the template ref and updates also the leaflet popup instance.
+ */
+ public updateView() {
+ this.embeddedViewRef.detectChanges();
+ this.ngZone.runOutsideAngular(() => this.popup.update());
+ }
+
+ public close() {
+ this.ngZone.runOutsideAngular(() => this.popup.removeFrom(this.leafletHandler.instance));
+ }
+
+ public open() {
+ this.ngZone.runOutsideAngular(() => this.popup.openOn(this.leafletHandler.instance));
+ }
+
+ public setLatLng(latLng: LatLngExpression) {
+ this.ngZone.runOutsideAngular(() => this.popup.setLatLng(latLng));
+ }
+
+}
diff --git a/src/app/shared/layout/leaflet/index.ts b/src/app/shared/leaflet/index.ts
similarity index 100%
rename from src/app/shared/layout/leaflet/index.ts
rename to src/app/shared/leaflet/index.ts
diff --git a/src/app/shared/layout/leaflet/leaflet-configuration.token.ts b/src/app/shared/leaflet/leaflet-configuration.token.ts
similarity index 70%
rename from src/app/shared/layout/leaflet/leaflet-configuration.token.ts
rename to src/app/shared/leaflet/leaflet-configuration.token.ts
index ea60f4b..0ef5cd1 100644
--- a/src/app/shared/layout/leaflet/leaflet-configuration.token.ts
+++ b/src/app/shared/leaflet/leaflet-configuration.token.ts
@@ -13,27 +13,10 @@
import {InjectionToken} from "@angular/core";
import {Observable} from "rxjs";
-import {environment} from "../../../../environments/environment";
+import {IAppConfiguration} from "../../core";
-export interface ILeafletConfiguration {
-
- urlTemplate: string;
-
- attribution?: string;
-
- gis: string;
-
- lat: number;
-
- lng: number;
-
- zoom: number;
-
-}
+export type ILeafletConfiguration = IAppConfiguration["leaflet"];
export const LEAFLET_RESIZE_TOKEN = new InjectionToken<Observable<any>>("Leaflet resize observable");
-export const LEAFLET_CONFIGURATION_TOKEN = new InjectionToken<ILeafletConfiguration>("Leaflet configuration", {
- providedIn: "root",
- factory: () => environment.leaflet
-});
+export const LEAFLET_CONFIGURATION_TOKEN = new InjectionToken<ILeafletConfiguration>("Leaflet configuration");
diff --git a/src/app/shared/leaflet/leaflet.module.ts b/src/app/shared/leaflet/leaflet.module.ts
new file mode 100644
index 0000000..5625400
--- /dev/null
+++ b/src/app/shared/leaflet/leaflet.module.ts
@@ -0,0 +1,62 @@
+/********************************************************************************
+ * 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 {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {TranslateModule} from "@ngx-translate/core";
+import {APP_CONFIGURATION, IAppConfiguration} from "../../core/configuration";
+import {ActionButtonModule} from "../layout/action-button";
+import {SideMenuRegistrationService} from "../layout/side-menu/services";
+import {LeafletMapComponent} from "./components";
+import {LeafletCenterMarkerDirective, LeafletDirective, LeafletMarkerDirective, LeafletPopupDirective} from "./directives";
+import {LEAFLET_CONFIGURATION_TOKEN, LEAFLET_RESIZE_TOKEN} from "./leaflet-configuration.token";
+import {StringToLatLngZoomPipe} from "./pipes";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ ActionButtonModule,
+ TranslateModule
+ ],
+ declarations: [
+ LeafletCenterMarkerDirective,
+ LeafletDirective,
+ LeafletMarkerDirective,
+ LeafletPopupDirective,
+ LeafletMapComponent,
+ StringToLatLngZoomPipe
+ ],
+ exports: [
+ LeafletCenterMarkerDirective,
+ LeafletDirective,
+ LeafletMarkerDirective,
+ LeafletPopupDirective,
+ LeafletMapComponent,
+ StringToLatLngZoomPipe
+ ],
+ providers: [
+ {
+ provide: LEAFLET_CONFIGURATION_TOKEN,
+ useFactory: (config: IAppConfiguration) => config.leaflet,
+ deps: [APP_CONFIGURATION]
+ },
+ {
+ provide: LEAFLET_RESIZE_TOKEN,
+ useFactory: (service: SideMenuRegistrationService) => service.resize$,
+ deps: [SideMenuRegistrationService]
+ }
+ ]
+})
+export class LeafletModule {
+
+}
diff --git a/src/app/shared/layout/leaflet/pipes/index.ts b/src/app/shared/leaflet/pipes/index.ts
similarity index 100%
rename from src/app/shared/layout/leaflet/pipes/index.ts
rename to src/app/shared/leaflet/pipes/index.ts
diff --git a/src/app/shared/layout/leaflet/pipes/string-to-lat-lng-zoom.pipe.spec.ts b/src/app/shared/leaflet/pipes/string-to-lat-lng-zoom.pipe.spec.ts
similarity index 100%
rename from src/app/shared/layout/leaflet/pipes/string-to-lat-lng-zoom.pipe.spec.ts
rename to src/app/shared/leaflet/pipes/string-to-lat-lng-zoom.pipe.spec.ts
diff --git a/src/app/shared/layout/leaflet/pipes/string-to-lat-lng-zoom.pipe.ts b/src/app/shared/leaflet/pipes/string-to-lat-lng-zoom.pipe.ts
similarity index 100%
rename from src/app/shared/layout/leaflet/pipes/string-to-lat-lng-zoom.pipe.ts
rename to src/app/shared/leaflet/pipes/string-to-lat-lng-zoom.pipe.ts
diff --git a/src/app/shared/layout/leaflet/util/index.ts b/src/app/shared/leaflet/util/index.ts
similarity index 100%
rename from src/app/shared/layout/leaflet/util/index.ts
rename to src/app/shared/leaflet/util/index.ts
diff --git a/src/app/shared/layout/leaflet/util/leaflet.util.spec.ts b/src/app/shared/leaflet/util/leaflet.util.spec.ts
similarity index 100%
rename from src/app/shared/layout/leaflet/util/leaflet.util.spec.ts
rename to src/app/shared/leaflet/util/leaflet.util.spec.ts
diff --git a/src/app/shared/layout/leaflet/util/leaflet.util.ts b/src/app/shared/leaflet/util/leaflet.util.ts
similarity index 79%
rename from src/app/shared/layout/leaflet/util/leaflet.util.ts
rename to src/app/shared/leaflet/util/leaflet.util.ts
index 2df3ef7..eff0886 100644
--- a/src/app/shared/layout/leaflet/util/leaflet.util.ts
+++ b/src/app/shared/leaflet/util/leaflet.util.ts
@@ -12,23 +12,19 @@
********************************************************************************/
import {Evented, LatLngLiteral, LeafletEvent} from "leaflet";
-import {Observable} from "rxjs";
+import {EMPTY, fromEventPattern, Observable} from "rxjs";
+import {NodeEventHandler} from "rxjs/internal/observable/fromEvent";
/**
* Creates an observable for specific leaflet map events.
*/
export function fromLeafletEvent<T extends LeafletEvent>(leafletMap: Evented, type: string): Observable<T> {
- return new Observable<T>((subscriber) => {
- if (leafletMap == null) {
- subscriber.complete();
- return;
- }
- const next = (e) => subscriber.next(e);
- leafletMap.on(type, next);
- subscriber.add(() => {
- leafletMap.off(type, next);
- });
- });
+ if (leafletMap == null) {
+ return EMPTY;
+ }
+ const addHandler = (handler: NodeEventHandler) => leafletMap.on(type, handler);
+ const removeHandler = (handler: NodeEventHandler) => leafletMap.off(type, handler);
+ return fromEventPattern<T>(addHandler, removeHandler);
}
/**
diff --git a/src/app/shared/linked-statements/index.ts b/src/app/shared/linked-statements/index.ts
deleted file mode 100644
index ef4c339..0000000
--- a/src/app/shared/linked-statements/index.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/********************************************************************************
- * 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 "./linked-statements.module";
diff --git a/src/app/shared/linked-statements/linked-statements.module.ts b/src/app/shared/linked-statements/linked-statements.module.ts
deleted file mode 100644
index 3869a3f..0000000
--- a/src/app/shared/linked-statements/linked-statements.module.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/********************************************************************************
- * 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 {CommonModule} from "@angular/common";
-import {NgModule} from "@angular/core";
-import {MatIconModule} from "@angular/material/icon";
-import {TranslateModule} from "@ngx-translate/core";
-import {SearchbarModule} from "../layout/searchbar";
-import {StatementTableModule} from "../layout/statement-table";
-import {LinkedStatementsComponent} from "./linked-statements";
-
-@NgModule({
- imports: [
- CommonModule,
- MatIconModule,
- TranslateModule,
- StatementTableModule,
- SearchbarModule
- ],
- declarations: [
- LinkedStatementsComponent,
- ],
- exports: [
- LinkedStatementsComponent
- ]
-})
-export class LinkedStatementsModule {
-
-}
diff --git a/src/app/store/app-store.module.ts b/src/app/store/app-store.module.ts
index 013dafb..cc16b58 100644
--- a/src/app/store/app-store.module.ts
+++ b/src/app/store/app-store.module.ts
@@ -17,6 +17,7 @@
import {ToastModule} from "primeng/toast";
import {AttachmentsStoreModule} from "./attachments";
import {ContactsStoreModule} from "./contacts";
+import {GeoStoreModule} from "./geo";
import {MailStoreModule} from "./mail";
import {ProcessStoreModule} from "./process";
import {RootStoreModule} from "./root";
@@ -25,20 +26,20 @@
@NgModule({
imports: [
- AttachmentsStoreModule,
RootStoreModule,
- SettingsStoreModule,
- ProcessStoreModule,
- StatementsStoreModule,
+ AttachmentsStoreModule,
ContactsStoreModule,
+ GeoStoreModule,
+ MailStoreModule,
+ ProcessStoreModule,
+ SettingsStoreModule,
+ StatementsStoreModule,
+
ToastModule,
- ButtonModule,
- MailStoreModule
+ ButtonModule
],
providers: [
- MessageService,
- ContactsStoreModule,
- MailStoreModule
+ MessageService
]
})
export class AppStoreModule {
diff --git a/src/app/store/attachments/attachments-store.module.ts b/src/app/store/attachments/attachments-store.module.ts
index 895c2f9..8b95c07 100644
--- a/src/app/store/attachments/attachments-store.module.ts
+++ b/src/app/store/attachments/attachments-store.module.ts
@@ -17,6 +17,7 @@
import {ATTACHMENTS_NAME, ATTACHMENTS_REDUCER} from "./attachments-reducers.token";
import {FetchAttachmentsEffect, SubmitAttachmentsEffect} from "./effects";
import {AttachmentDownloadEffect} from "./effects/download";
+import {SubmitConsiderationsEffect} from "./effects/submit/submit-considerations.effect";
@NgModule({
imports: [
@@ -24,7 +25,8 @@
EffectsModule.forFeature([
AttachmentDownloadEffect,
FetchAttachmentsEffect,
- SubmitAttachmentsEffect
+ SubmitAttachmentsEffect,
+ SubmitConsiderationsEffect
])
]
})
diff --git a/src/app/store/attachments/effects/submit/submit-considerations.effect.ts b/src/app/store/attachments/effects/submit/submit-considerations.effect.ts
new file mode 100644
index 0000000..df309f5
--- /dev/null
+++ b/src/app/store/attachments/effects/submit/submit-considerations.effect.ts
@@ -0,0 +1,102 @@
+/********************************************************************************
+ * 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 {Injectable} from "@angular/core";
+import {Actions, createEffect, ofType} from "@ngrx/effects";
+import {Action} from "@ngrx/store";
+import {concat, EMPTY, Observable, of, throwError} from "rxjs";
+import {catchError, endWith, filter, map, mergeMap, startWith, switchMap} from "rxjs/operators";
+import {AttachmentsApiService} from "../../../../core/api/attachments";
+import {arrayJoin, endWithObservable, ignoreError} from "../../../../util";
+import {setErrorAction} from "../../../root/actions";
+import {EErrorCode} from "../../../root/model";
+import {setStatementLoadingAction, submitConsiderationFilesAction} from "../../../statements/actions";
+import {addAttachmentEntityAction, setAttachmentCacheAction} from "../../actions";
+import {IAttachmentControlValue, IAttachmentError} from "../../model";
+import {FetchAttachmentsEffect} from "../fetch";
+
+@Injectable({providedIn: "root"})
+export class SubmitConsiderationsEffect {
+
+ public submit$ = createEffect(() => this.actions.pipe(
+ ofType(submitConsiderationFilesAction),
+ switchMap((action) => {
+ return this.submit(action.statementId, action.value).pipe(
+ ignoreError()
+ );
+ })
+ ));
+
+ public constructor(
+ public readonly actions: Actions,
+ public readonly attachmentsApiService: AttachmentsApiService,
+ public readonly fetchAttachmentsEffect: FetchAttachmentsEffect
+ ) {
+
+ }
+
+
+ public submit(
+ statementId: number,
+ value: IAttachmentControlValue[]
+ ): Observable<Action> {
+ const errors: IAttachmentError[] = [];
+
+ return concat(
+ this.addConsiderations(statementId, value, errors),
+ this.fetchAttachmentsEffect.fetchAttachments(statementId)
+ ).pipe(
+ endWithObservable(() => {
+ const lastError = errors.reverse()[0];
+ return lastError == null ? EMPTY : concat(
+ of(setErrorAction({statementId, error: lastError.message})),
+ throwError(lastError.error)
+ );
+ }),
+ startWith(setStatementLoadingAction({loading: {submittingConsiderationFiles: true}})),
+ endWith(setStatementLoadingAction({loading: {submittingConsiderationFiles: false}}))
+ );
+ }
+
+ public addConsiderations(
+ statementId: number,
+ considerations: IAttachmentControlValue[],
+ errors: IAttachmentError[] = []
+ ): Observable<Action> {
+ const items: IAttachmentControlValue[] = [];
+ return of(...arrayJoin(considerations)).pipe(
+ filter((item) => item?.file instanceof File),
+ mergeMap((item) => {
+ return this.addSingleConsiderationFile(statementId, item.file).pipe(
+ catchError((error) => {
+ items.push(item);
+ errors.push({statementId, attachment: item, error, message: EErrorCode.FAILED_FILE_UPLOAD});
+ return EMPTY;
+ })
+ );
+ }, 2),
+ startWith(setAttachmentCacheAction({statementId, items: considerations})),
+ endWithObservable(() => of(setAttachmentCacheAction({statementId, items})))
+ );
+ }
+
+ private addSingleConsiderationFile(
+ statementId: number,
+ file: File
+ ): Observable<Action> {
+ return this.attachmentsApiService.postConsideration(statementId, file).pipe(
+ map((entity) => addAttachmentEntityAction({statementId, entity}))
+ );
+ }
+
+}
diff --git a/src/app/store/attachments/selectors/attachments.selectors.spec.ts b/src/app/store/attachments/selectors/attachments.selectors.spec.ts
index 5d7dab3..e53dc34 100644
--- a/src/app/store/attachments/selectors/attachments.selectors.spec.ts
+++ b/src/app/store/attachments/selectors/attachments.selectors.spec.ts
@@ -11,13 +11,15 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {IAPIAttachmentTag} from "../../../core/api/attachments";
+import {IAPIAttachmentModel, IAPIAttachmentTag} from "../../../core/api/attachments";
import {createAttachmentFileMock, createAttachmentModelMock} from "../../../test";
import {IAttachmentsStoreState} from "../model";
import {
getAttachmentControlValueSelector,
getFilteredAttachmentTagsSelector,
- getStatementAttachmentsSelector
+ getOutboxAttachments,
+ getStatementAttachmentsSelector,
+ getStatementPdfAttachment
} from "./attachments.selectors";
describe("attachmentsSelectors", () => {
@@ -66,4 +68,24 @@
.toEqual([{id: 19, name: "name", tagIds: [], isSelected: true}]);
});
+ it("getOutboxAttachments", () => {
+ const projector = getOutboxAttachments.projector;
+ const input: IAPIAttachmentModel[] = [
+ {id: 19, name: "name", tagIds: ["outbox"]}, {id: 20, name: "name", tagIds: []}
+ ] as IAPIAttachmentModel[];
+ const expectedResult = [{id: 19, name: "name", tagIds: ["outbox"]}] as IAPIAttachmentModel[];
+ expect(projector(input))
+ .toEqual(expectedResult);
+ });
+
+ it("getStatementPdfAttachment", () => {
+ const projector = getStatementPdfAttachment.projector;
+ const input: IAPIAttachmentModel[] = [
+ {id: 19, name: "name", tagIds: ["statement"]}, {id: 20, name: "name", tagIds: []}
+ ] as IAPIAttachmentModel[];
+ const expectedResult = [{id: 19, name: "name", tagIds: ["statement"]}] as IAPIAttachmentModel[];
+ expect(projector(input))
+ .toEqual(expectedResult);
+ });
+
});
diff --git a/src/app/store/attachments/selectors/attachments.selectors.ts b/src/app/store/attachments/selectors/attachments.selectors.ts
index 59ae2dc..db627c3 100644
--- a/src/app/store/attachments/selectors/attachments.selectors.ts
+++ b/src/app/store/attachments/selectors/attachments.selectors.ts
@@ -93,6 +93,29 @@
}
);
+export const getOutboxAttachments = createSelector(
+ getAllStatementAttachments,
+ (attachments) => {
+ return arrayJoin(attachments).filter((attachment) =>
+ attachment?.tagIds.some((_) => _ === "outbox") && attachment?.tagIds?.length === 1);
+ }
+);
+
+export const getConsiderationAttachments = createSelector(
+ getAllStatementAttachments,
+ (attachments) => {
+ return arrayJoin(attachments).filter((attachment) =>
+ attachment?.tagIds.some((_) => _ === "consideration") && attachment?.tagIds?.length === 1);
+ }
+);
+
+export const getStatementPdfAttachment = createSelector(
+ getAllStatementAttachments,
+ (attachments) => {
+ return arrayJoin(attachments).filter((attachment) => attachment?.tagIds.some((_) => _ === "statement"));
+ }
+);
+
export const getStatementAttachmentCacheSelector = createSelector(
getStatementAttachmentCacheEntitiesSelector,
queryParamsIdSelector,
diff --git a/src/app/features/search/components/search/index.ts b/src/app/store/geo/actions/geo.actions.ts
similarity index 70%
copy from src/app/features/search/components/search/index.ts
copy to src/app/store/geo/actions/geo.actions.ts
index 5eb9c7f..9c1a7f8 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/store/geo/actions/geo.actions.ts
@@ -11,4 +11,10 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+import {createAction, props} from "@ngrx/store";
+import {ILeafletBounds} from "../../../shared/leaflet";
+
+export const openGisAction = createAction(
+ "[Map] Open GIS",
+ props<{ bounds: ILeafletBounds, user: string }>()
+);
diff --git a/src/app/features/search/components/search/index.ts b/src/app/store/geo/actions/index.ts
similarity index 93%
copy from src/app/features/search/components/search/index.ts
copy to src/app/store/geo/actions/index.ts
index 5eb9c7f..df868cc 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/store/geo/actions/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./geo.actions";
diff --git a/src/app/features/search/components/search/search.component.scss b/src/app/store/geo/effects/index.ts
similarity index 94%
rename from src/app/features/search/components/search/search.component.scss
rename to src/app/store/geo/effects/index.ts
index 06db89a..1cfc850 100644
--- a/src/app/features/search/components/search/search.component.scss
+++ b/src/app/store/geo/effects/index.ts
@@ -11,3 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+export * from "./open-gis";
diff --git a/src/app/features/search/components/search/index.ts b/src/app/store/geo/effects/open-gis/index.ts
similarity index 93%
copy from src/app/features/search/components/search/index.ts
copy to src/app/store/geo/effects/open-gis/index.ts
index 5eb9c7f..66752fd 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/store/geo/effects/open-gis/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./open-gis.effect";
diff --git a/src/app/store/geo/effects/open-gis/open-gis.effect.spec.ts b/src/app/store/geo/effects/open-gis/open-gis.effect.spec.ts
new file mode 100644
index 0000000..c33cff7
--- /dev/null
+++ b/src/app/store/geo/effects/open-gis/open-gis.effect.spec.ts
@@ -0,0 +1,127 @@
+/********************************************************************************
+ * 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 {HttpClientTestingModule, HttpTestingController} from "@angular/common/http/testing";
+import {TestBed} from "@angular/core/testing";
+import {provideMockActions} from "@ngrx/effects/testing";
+import {Action} from "@ngrx/store";
+import {LatLngLiteral} from "leaflet";
+import {Observable, Subject, Subscription} from "rxjs";
+import {IAPIGeographicPositions, SPA_BACKEND_ROUTE, WINDOW} from "../../../../core";
+import {ILeafletBounds} from "../../../../shared/leaflet";
+import {openGisAction} from "../../actions";
+import {OpenGisEffect} from "./open-gis.effect";
+
+describe("OpenGisEffect", () => {
+ const bounds: ILeafletBounds = {
+ center: createLatLngMock(),
+ northEast: createLatLngMock(),
+ northWest: createLatLngMock(),
+ southEast: createLatLngMock(),
+ southWest: createLatLngMock(),
+ zoom: 13
+ };
+ const user = "userName";
+ const gisTemplateUrl = "http://localhost2200/Coordinates?" +
+ "pLLX={southWestX}&pLLY={southWestY}&" +
+ "pURX={northEastX}&pURY={northEastY}&" +
+ "pCX={centerX}&pCY={centerY}&" +
+ "user={user}";
+
+ const geographicPositions: IAPIGeographicPositions = {
+ southWest: {x: bounds.southWest.lng, y: bounds.southWest.lat},
+ northEast: {x: bounds.northEast.lng, y: bounds.northEast.lat},
+ center: {x: bounds.center.lng, y: bounds.center.lat}
+ };
+ const gisUrl = "http://localhost2200/Coordinates?" +
+ `pLLX=${geographicPositions.southWest.x}&pLLY=${geographicPositions.southWest.y}&` +
+ `pURX=${geographicPositions.northEast.x}&pURY=${geographicPositions.northEast.y}&` +
+ `pCX=${geographicPositions.center.x}&pCY=${geographicPositions.center.y}&` +
+ `user=${user}`;
+
+ let actions$: Observable<Action>;
+ let httpTestingController: HttpTestingController;
+ let effect: OpenGisEffect;
+ let subscription: Subscription;
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ imports: [
+ HttpClientTestingModule
+ ],
+ providers: [
+ provideMockActions(() => actions$),
+ {
+ provide: SPA_BACKEND_ROUTE,
+ useValue: "/"
+ },
+ {
+ provide: WINDOW,
+ useValue: ({
+ open(url?: string, target?: string, features?: string, replace?: boolean) {
+ }
+ })
+ }
+ ]
+ });
+ effect = TestBed.inject(OpenGisEffect);
+ effect.gisUrlTemplate = gisTemplateUrl;
+ httpTestingController = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ if (subscription != null) {
+ subscription.unsubscribe();
+ }
+ });
+
+ it("should open GIS in new window", () => {
+ const results: Action[] = [];
+ const spy = spyOn(effect.window, "open");
+ const actionSubject = new Subject<Action>();
+ actions$ = actionSubject;
+
+ subscription = effect.open$.subscribe((_) => results.push(_));
+
+ actionSubject.next(openGisAction({bounds, user}));
+ expectTransformRequest(geographicPositions);
+ expect(spy).toHaveBeenCalledWith(gisUrl, "_blank");
+ expect(results).toEqual([]);
+ httpTestingController.verify();
+ });
+
+ it("should not call back end if no transform is required", () => {
+ subscription = effect.transform({}).subscribe();
+ httpTestingController.verify();
+ });
+
+ it("should extract geographic positions from bounds", () => {
+ expect(effect.extractGeographicPositionFromBounds(bounds)).toEqual(geographicPositions);
+ expect(effect.extractGeographicPositionFromBounds({} as any)).toEqual({});
+ });
+
+ function expectTransformRequest(body: IAPIGeographicPositions) {
+ const endPoint = `/geo-coordinate-transform?from=${effect.projectionFrom}&to=${effect.projectionTo}`;
+ const request = httpTestingController.expectOne(endPoint);
+ expect(request.request.method).toBe("POST");
+ request.flush(body);
+ }
+
+});
+
+function createLatLngMock(): LatLngLiteral {
+ return {
+ lat: 4 + Math.random() * 10,
+ lng: 4 + Math.random() * 10
+ };
+}
diff --git a/src/app/store/geo/effects/open-gis/open-gis.effect.ts b/src/app/store/geo/effects/open-gis/open-gis.effect.ts
new file mode 100644
index 0000000..19da54f
--- /dev/null
+++ b/src/app/store/geo/effects/open-gis/open-gis.effect.ts
@@ -0,0 +1,118 @@
+/********************************************************************************
+ * 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 {Inject, Injectable} from "@angular/core";
+import {Actions, createEffect, ofType} from "@ngrx/effects";
+import {Action} from "@ngrx/store";
+import {EMPTY, Observable, of} from "rxjs";
+import {exhaustMap, ignoreElements, switchMap} from "rxjs/operators";
+import {APP_CONFIGURATION, GeoApiService, IAPIGeographicPositions, IAppConfiguration, WINDOW} from "../../../../core";
+import {ILeafletBounds} from "../../../../shared/leaflet";
+import {catchErrorTo} from "../../../../util/rxjs";
+import {EErrorCode, setErrorAction} from "../../../root";
+import {openGisAction} from "../../actions";
+
+@Injectable({providedIn: "root"})
+export class OpenGisEffect {
+
+ public open$ = createEffect(() => this.actions.pipe(
+ ofType(openGisAction),
+ exhaustMap((action) => this.openGis(action.bounds, action.user))
+ ));
+
+ /**
+ * URL Template to connect with GIS.
+ */
+ public gisUrlTemplate = this.configuration.gis.urlTemplate;
+
+ /**
+ * Expected projection format for the given Leaflet coordinates.
+ */
+ public projectionFrom = this.configuration.gis.projectionFrom;
+
+ /**
+ * Expected projection required for the GIS.
+ */
+ public projectionTo = this.configuration.gis.projectionTo;
+
+ /**
+ * Search parameter keys which are transformed in the GIS URL template.
+ */
+ public coordinateKeys: Array<keyof ILeafletBounds> = ["center", "northEast", "northWest", "southEast", "southWest"];
+
+ public constructor(
+ public actions: Actions,
+ public geoApiService: GeoApiService,
+ @Inject(WINDOW) public window: Window,
+ @Inject(APP_CONFIGURATION) public configuration: IAppConfiguration
+ ) {
+
+ }
+
+ public openGis(bounds: ILeafletBounds, user: string): Observable<Action> {
+ return of(bounds).pipe(
+ switchMap(() => this.transform(this.extractGeographicPositionFromBounds(bounds))),
+ switchMap((geographicPositions) => {
+ const gisUrl = this.generateUrl(geographicPositions, user);
+ this.window.open(gisUrl, "_blank");
+ return EMPTY;
+ }),
+ ignoreElements(),
+ catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED}))
+ );
+ }
+
+ /**
+ * Extract all required geographic positions from the given Leaflet bounds.
+ */
+ public extractGeographicPositionFromBounds(bounds: ILeafletBounds): IAPIGeographicPositions {
+ const geographicPositions: IAPIGeographicPositions = {};
+ this.coordinateKeys.forEach((key) => {
+ const value = bounds[key];
+ if (typeof value !== "object") {
+ return;
+ }
+ const tokens = [`{${key}X}`, `{${key}Y}`];
+ if (tokens.some((token) => this.gisUrlTemplate.indexOf(token) > -1)) {
+ geographicPositions[key] = {
+ x: value.lng,
+ y: value.lat
+ };
+ }
+ });
+ return geographicPositions;
+ }
+
+ /**
+ * Transforms a set of geographic positions via an API back end call.
+ */
+ public transform(geographicPositions: IAPIGeographicPositions): Observable<IAPIGeographicPositions> {
+ return Object.keys(geographicPositions).length > 0 ?
+ this.geoApiService.transform(geographicPositions, this.projectionFrom, this.projectionTo) :
+ of(geographicPositions);
+ }
+
+ /**
+ * Generate the URL to the GIS with a set of given geographic positions.
+ */
+ public generateUrl(geographicPositions: IAPIGeographicPositions, user: string) {
+ let result = this.gisUrlTemplate;
+ Object.entries(geographicPositions).forEach(([key, value]) => {
+ result = result.replace(new RegExp(`{${key}X}`, "g"), "" + value.x);
+ result = result.replace(new RegExp(`{${key}Y}`, "g"), "" + value.y);
+ });
+ result = result.replace(new RegExp(`{user}`, "g"), user);
+ return result;
+ }
+
+}
diff --git a/src/app/features/search/components/search/index.ts b/src/app/store/geo/geo-reducers.token.ts
similarity index 63%
copy from src/app/features/search/components/search/index.ts
copy to src/app/store/geo/geo-reducers.token.ts
index 5eb9c7f..a1453d3 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/store/geo/geo-reducers.token.ts
@@ -11,4 +11,13 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+import {InjectionToken} from "@angular/core";
+import {ActionReducerMap} from "@ngrx/store";
+import {IGeoStoreState} from "./model";
+
+export const GEO_NAME = "geo";
+
+export const GEO_REDUCERS = new InjectionToken<ActionReducerMap<IGeoStoreState>>("Geo store reducer", {
+ providedIn: "root",
+ factory: () => ({})
+});
diff --git a/src/app/store/geo/geo-store.module.ts b/src/app/store/geo/geo-store.module.ts
new file mode 100644
index 0000000..1253f7d
--- /dev/null
+++ b/src/app/store/geo/geo-store.module.ts
@@ -0,0 +1,30 @@
+/********************************************************************************
+ * 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 {NgModule} from "@angular/core";
+import {EffectsModule} from "@ngrx/effects";
+import {StoreModule} from "@ngrx/store";
+import {OpenGisEffect} from "./effects";
+import {GEO_NAME, GEO_REDUCERS} from "./geo-reducers.token";
+
+@NgModule({
+ imports: [
+ StoreModule.forFeature(GEO_NAME, GEO_REDUCERS),
+ EffectsModule.forFeature([
+ OpenGisEffect
+ ])
+ ]
+})
+export class GeoStoreModule {
+
+}
diff --git a/src/app/features/search/components/search/index.ts b/src/app/store/geo/index.ts
similarity index 86%
copy from src/app/features/search/components/search/index.ts
copy to src/app/store/geo/index.ts
index 5eb9c7f..5b5ab49 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/store/geo/index.ts
@@ -11,4 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./actions";
+export * from "./model";
+
+export * from "./geo-store.module";
diff --git a/src/app/features/search/components/search/index.ts b/src/app/store/geo/model/IGeoStoreState.ts
similarity index 90%
copy from src/app/features/search/components/search/index.ts
copy to src/app/store/geo/model/IGeoStoreState.ts
index 5eb9c7f..c4a0812 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/store/geo/model/IGeoStoreState.ts
@@ -11,4 +11,8 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export interface IGeoStoreState {
+
+ loading?: any;
+
+}
diff --git a/src/app/features/search/components/search/index.ts b/src/app/store/geo/model/index.ts
similarity index 93%
rename from src/app/features/search/components/search/index.ts
rename to src/app/store/geo/model/index.ts
index 5eb9c7f..ba18e63 100644
--- a/src/app/features/search/components/search/index.ts
+++ b/src/app/store/geo/model/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search.component";
+export * from "./IGeoStoreState";
diff --git a/src/app/store/index.ts b/src/app/store/index.ts
index 0044745..9de1c49 100644
--- a/src/app/store/index.ts
+++ b/src/app/store/index.ts
@@ -13,6 +13,8 @@
export * from "./attachments";
export * from "./contacts";
+export * from "./geo";
+export * from "./mail";
export * from "./process";
export * from "./root";
export * from "./settings";
diff --git a/src/app/store/mail/actions/mail.actions.ts b/src/app/store/mail/actions/mail.actions.ts
index 8bde645..31314b6 100644
--- a/src/app/store/mail/actions/mail.actions.ts
+++ b/src/app/store/mail/actions/mail.actions.ts
@@ -26,7 +26,7 @@
export const deleteEmailFromInboxAction = createAction(
"[Email] Delete email from inbox",
- props<{ mailId: string, navigateTo?: string }>()
+ props<{ mailId: string }>()
);
export const downloadEmailAttachmentAction = createAction(
diff --git a/src/app/store/mail/effects/delete/delete-email-from-inbox.effect.spec.ts b/src/app/store/mail/effects/delete/delete-email-from-inbox.effect.spec.ts
index 6abb45d..370af82 100644
--- a/src/app/store/mail/effects/delete/delete-email-from-inbox.effect.spec.ts
+++ b/src/app/store/mail/effects/delete/delete-email-from-inbox.effect.spec.ts
@@ -13,6 +13,7 @@
import {HttpClientTestingModule, HttpTestingController} from "@angular/common/http/testing";
import {TestBed} from "@angular/core/testing";
+import {ActivatedRoute, Params} from "@angular/router";
import {RouterTestingModule} from "@angular/router/testing";
import {provideMockActions} from "@ngrx/effects/testing";
import {Action} from "@ngrx/store";
@@ -24,6 +25,7 @@
describe("DeleteEmailFromInboxEffect", () => {
let actions$: Observable<Action>;
+ let queryParams: Params;
let httpTestingController: HttpTestingController;
let effect: DeleteEmailFromInboxEffect;
let subscription: Subscription;
@@ -39,11 +41,16 @@
{
provide: SPA_BACKEND_ROUTE,
useValue: "/"
+ },
+ {
+ provide: ActivatedRoute,
+ useValue: {snapshot: {queryParams: {}}}
}
]
});
effect = TestBed.inject(DeleteEmailFromInboxEffect);
httpTestingController = TestBed.inject(HttpTestingController);
+ queryParams = TestBed.inject(ActivatedRoute).snapshot.queryParams;
});
afterEach(() => {
@@ -61,7 +68,7 @@
subscription = effect.delete$.subscribe((_) => results.push(_));
actionSubject.next(deleteEmailFromInboxAction({mailId}));
- expect(spy).toHaveBeenCalledWith(mailId, undefined);
+ expect(spy).toHaveBeenCalledWith(mailId);
spy.calls.reset();
actionSubject.next(deleteEmailFromInboxAction({mailId: null}));
@@ -74,10 +81,17 @@
const mailId = "<Mail19>";
const results: Action[] = [];
+ queryParams.mailId = mailId;
+ const navigationPromise = Promise.resolve(true);
+ const navigateSpy = spyOn(effect.router, "navigate").and.returnValue(navigationPromise);
+ navigateSpy.calls.reset();
subscription = effect.delete(mailId).subscribe((_) => results.push(_));
+
expectDeleteMailFromInboxRequest(mailId);
+ await navigationPromise; // Wait for navigation to finish
expect(subscription.closed).toBeTrue();
+ expect(navigateSpy).toHaveBeenCalled();
expect(results).toEqual([
setEmailLoadingStateAction({loading: {deleting: true}}),
deleteEmailEntityAction({mailId}),
diff --git a/src/app/store/mail/effects/delete/delete-email-from-inbox.effect.ts b/src/app/store/mail/effects/delete/delete-email-from-inbox.effect.ts
index 2b937b3..b5fc123 100644
--- a/src/app/store/mail/effects/delete/delete-email-from-inbox.effect.ts
+++ b/src/app/store/mail/effects/delete/delete-email-from-inbox.effect.ts
@@ -12,13 +12,15 @@
********************************************************************************/
import {Injectable} from "@angular/core";
-import {Router} from "@angular/router";
+import {ActivatedRoute, Router} from "@angular/router";
import {Actions, createEffect, ofType} from "@ngrx/effects";
import {Action} from "@ngrx/store";
-import {EMPTY, Observable} from "rxjs";
-import {catchError, endWith, filter, map, mergeMap, startWith} from "rxjs/operators";
+import {concat, EMPTY, from, Observable} from "rxjs";
+import {endWith, filter, ignoreElements, map, mergeMap, startWith} from "rxjs/operators";
import {MailApiService} from "../../../../core";
-import {endWithObservable, ignoreError} from "../../../../util/rxjs";
+import {catchErrorTo} from "../../../../util/rxjs";
+import {setErrorAction} from "../../../root/actions";
+import {EErrorCode} from "../../../root/model";
import {deleteEmailEntityAction, deleteEmailFromInboxAction, setEmailLoadingStateAction} from "../../actions";
@Injectable({providedIn: "root"})
@@ -27,35 +29,45 @@
public delete$ = createEffect(() => this.actions.pipe(
ofType(deleteEmailFromInboxAction),
filter((action) => action.mailId != null),
- mergeMap((action) => this.delete(action.mailId, action.navigateTo))
+ mergeMap((action) => this.delete(action.mailId))
));
public constructor(
public readonly actions: Actions,
public readonly mailApiService: MailApiService,
+ public readonly activatedRoute: ActivatedRoute,
public readonly router: Router
) {
}
- public delete(mailId: string, navigateTo?: string): Observable<Action> {
- let error = false;
- return this.mailApiService.deleteInboxEmail(mailId).pipe(
- map(() => deleteEmailEntityAction({mailId})),
+ public delete(mailId: string): Observable<Action> {
+ return concat(
+ this.deleteEmailFromInbox(mailId),
+ this.removeQueryParam(mailId)
+ ).pipe(
+ catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
startWith(setEmailLoadingStateAction({loading: {deleting: true}})),
- catchError(() => {
- error = true;
- return EMPTY;
- }),
- ignoreError(),
- endWith((setEmailLoadingStateAction({loading: {deleting: false}}))),
- endWithObservable(() => {
- if (navigateTo && !error) {
- this.router.navigate(["mail"]);
- }
- return EMPTY;
- })
+ endWith((setEmailLoadingStateAction({loading: {deleting: false}})))
);
}
+ public deleteEmailFromInbox(mailId: string): Observable<Action> {
+ return this.mailApiService.deleteInboxEmail(mailId).pipe(
+ map(() => deleteEmailEntityAction({mailId}))
+ );
+ }
+
+ public removeQueryParam(mailId: string): Observable<never> {
+ if (this.activatedRoute.snapshot.queryParams.mailId !== mailId) {
+ return EMPTY;
+ }
+ const navigatePromise = this.router.navigate([], {
+ queryParams: {mailId: null},
+ queryParamsHandling: "merge",
+ replaceUrl: true
+ });
+ return from(navigatePromise).pipe(ignoreElements());
+ }
+
}
diff --git a/src/app/store/process/effects/process-task.effect.ts b/src/app/store/process/effects/process-task.effect.ts
index 2078a99..39cdc55 100644
--- a/src/app/store/process/effects/process-task.effect.ts
+++ b/src/app/store/process/effects/process-task.effect.ts
@@ -23,6 +23,7 @@
import {arrayJoin} from "../../../util/store";
import {setErrorAction} from "../../root/actions";
import {EErrorCode} from "../../root/model";
+import {sendStatementViaMailAction} from "../../statements/actions";
import {
claimAndCompleteTask,
claimTaskAction,
@@ -55,6 +56,15 @@
))
));
+ public claimAndSend$ = createEffect(() => this.actions.pipe(
+ ofType(sendStatementViaMailAction),
+ switchMap((action) => this.claimAndSend(action.statementId, action.taskId).pipe(
+ catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
+ startWith(setProcessLoadingAction({statementId: action.statementId, loading: true})),
+ endWith(setProcessLoadingAction({statementId: action.statementId, loading: false}))
+ ))
+ ));
+
public completeTask$ = createEffect(() => this.actions.pipe(
ofType(completeTaskAction),
exhaustMap((action) => this.completeTask(action.statementId, action.taskId, action.variables, action.claimNext).pipe(
@@ -103,6 +113,30 @@
);
}
+ public claimAndSend(
+ statementId: number,
+ taskId: string
+ ): Observable<Action> {
+ return this.claimTask(statementId, taskId).pipe(
+ throwAfterActionType(setErrorAction),
+ endWithObservable(() => concat(
+ this.sendMailAndComplete(statementId, taskId),
+ this.fetchTasks(statementId))
+ ),
+ ignoreError()
+ );
+ }
+
+ public sendMailAndComplete(statementId: number, taskId: string): Observable<Action> {
+ return this.processApiService.dispatchStatement(statementId, taskId).pipe(
+ map(() => deleteTaskAction({statementId, taskId})),
+ catchError(() => this.processApiService.unclaimStatementTask(statementId, taskId).pipe(
+ map(() => setErrorAction({statementId, error: EErrorCode.COULD_NOT_SEND_MAIL})),
+ catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED}))
+ ))
+ );
+ }
+
public completeTask(
statementId: number,
taskId: string,
@@ -171,7 +205,7 @@
): Observable<IAPIProcessTask> {
let taskId: string;
return claimNext == null || claimNext === false ? EMPTY : this.getNextTask(statementId, claimNext).pipe(
- switchMap((_taskId) => this.processApiService.claimStatementTask(statementId, taskId = _taskId)),
+ switchMap((_taskId) => _taskId ? this.processApiService.claimStatementTask(statementId, taskId = _taskId) : EMPTY),
endWithObservable(() => navigate ? this.navigateTo(statementId, taskId) : EMPTY)
);
}
diff --git a/src/app/store/root/model/EErrorCode.ts b/src/app/store/root/model/EErrorCode.ts
index f5cbecc..02a601c 100644
--- a/src/app/store/root/model/EErrorCode.ts
+++ b/src/app/store/root/model/EErrorCode.ts
@@ -21,5 +21,6 @@
FAILED_FILE_UPLOAD = "shared.errorMessages.failedFileUpload",
FAILED_MAIL_TRANSFER = "shared.errorMessages.failedMailTransfer",
INVALID_TEXT_ARRANGEMENT = "shared.errorMessages.invalidTextArrangement",
- COULD_NOT_LOAD_MAIL_DATA = "shared.errorMessages.couldNotLoadMailData"
+ COULD_NOT_LOAD_MAIL_DATA = "shared.errorMessages.couldNotLoadMailData",
+ COULD_NOT_SEND_MAIL = "shared.errorMessages.couldNotSendMail"
}
diff --git a/src/app/store/root/selectors/query-params.selectors.ts b/src/app/store/root/selectors/query-params.selectors.ts
index 242e71a..f547563 100644
--- a/src/app/store/root/selectors/query-params.selectors.ts
+++ b/src/app/store/root/selectors/query-params.selectors.ts
@@ -11,9 +11,25 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+import {Params} from "@angular/router";
import {createSelector} from "@ngrx/store";
import {rootStateSelector} from "./root.selectors";
+function selectQueryParamString(key: string) {
+ return (state: Params): string => state == null ? undefined : state[key];
+}
+
+function selectQueryParamInt(key: string) {
+ return (state: Params): number => {
+ if (state == null || state[key] == null) {
+ return undefined;
+ }
+ const result = typeof state[key] === "string" ? parseInt(state[key], 10) : state[key];
+ return Number.isInteger(result) ? result : undefined;
+ };
+}
+
+
export const queryParamsSelector = createSelector(
rootStateSelector,
(state) => state.queryParams
@@ -21,21 +37,20 @@
export const queryParamsIdSelector = createSelector(
queryParamsSelector,
- (state): number => {
- if (state == null || state.id == null) {
- return undefined;
- }
- const id = typeof state.id === "string" ? parseInt(state.id, 10) : state.id;
- return Number.isInteger(id) ? id : undefined;
- }
+ selectQueryParamInt("id")
);
export const queryParamsTaskIdSelector = createSelector(
queryParamsSelector,
- (state): string => state?.taskId
+ selectQueryParamString("taskId")
);
export const queryParamsMailIdSelector = createSelector(
queryParamsSelector,
- (state): string => state?.mailId
+ selectQueryParamString("mailId")
+);
+
+export const queryParamsCoordSelector = createSelector(
+ queryParamsSelector,
+ selectQueryParamString("coord")
);
diff --git a/src/app/store/statements/actions/fetch.actions.ts b/src/app/store/statements/actions/fetch.actions.ts
index ecbf621..d0d1b1a 100644
--- a/src/app/store/statements/actions/fetch.actions.ts
+++ b/src/app/store/statements/actions/fetch.actions.ts
@@ -20,6 +20,11 @@
props<{ statementId: number; withConfiguration?: boolean }>()
);
+export const fetchProcessStateInfo = createAction(
+ "[Details] Fetch process history",
+ props<{ statementId: number; }>()
+);
+
export const updateStatementEntityAction = createAction(
"[API] Update statement entity",
props<{ statementId: number, entity: IStatementEntity }>()
diff --git a/src/app/store/statements/actions/search.actions.ts b/src/app/store/statements/actions/search.actions.ts
index 2b51abe..1b1550a 100644
--- a/src/app/store/statements/actions/search.actions.ts
+++ b/src/app/store/statements/actions/search.actions.ts
@@ -12,7 +12,8 @@
********************************************************************************/
import {createAction, props} from "@ngrx/store";
-import {IAPIPaginationResponse, IAPISearchOptions, IAPIStatementModel} from "../../../core";
+import {IAPIPaginationResponse, IAPIPositionSearchStatementModel, IAPISearchOptions, IAPIStatementModel} from "../../../core";
+import {IAPIPositionSearchOptions} from "../../../core/api/shared/IAPIPositionSearchOptions";
export const startStatementSearchAction = createAction(
"[Dashboard/Edit/List] Start search for statements",
@@ -23,3 +24,13 @@
"[API] Set search results for statements",
props<{ results: IAPIPaginationResponse<IAPIStatementModel> }>()
);
+
+export const startStatementPositionSearchAction = createAction(
+ "[PositionSearch] Start search for statement positions",
+ props<{ options: IAPIPositionSearchOptions }>()
+);
+
+export const setStatementPositionSearchResultAction = createAction(
+ "[API] Set search results for statement positions",
+ props<{ results: IAPIPositionSearchStatementModel[] }>()
+);
diff --git a/src/app/store/statements/actions/submit.actions.ts b/src/app/store/statements/actions/submit.actions.ts
index 3975538..003026a 100644
--- a/src/app/store/statements/actions/submit.actions.ts
+++ b/src/app/store/statements/actions/submit.actions.ts
@@ -14,6 +14,7 @@
import {createAction, props} from "@ngrx/store";
import {EAPIProcessTaskDefinitionKey, TCompleteTaskVariable} from "../../../core/api/process";
import {IAPITextArrangementItemModel} from "../../../core/api/text";
+import {IAttachmentControlValue} from "../../attachments/model";
import {IStatementEditorFormValue, IStatementInformationFormValue, IWorkflowFormValue} from "../model";
/**
@@ -74,6 +75,13 @@
}>()
);
+export const submitConsiderationFilesAction = createAction(
+ "[Edit] Submit considerations",
+ props<{
+ statementId: number,
+ value: IAttachmentControlValue[]
+ }>()
+);
export const sendStatementViaMailAction = createAction(
"[Details] Resend statement via email",
diff --git a/src/app/store/statements/effects/fetch-statement-details/fetch-statement-details.effect.ts b/src/app/store/statements/effects/fetch-statement-details/fetch-statement-details.effect.ts
index e9c8d46..12646e4 100644
--- a/src/app/store/statements/effects/fetch-statement-details/fetch-statement-details.effect.ts
+++ b/src/app/store/statements/effects/fetch-statement-details/fetch-statement-details.effect.ts
@@ -15,8 +15,8 @@
import {Actions, createEffect, ofType} from "@ngrx/effects";
import {Action} from "@ngrx/store";
import {EMPTY, merge, Observable, of} from "rxjs";
-import {filter, map, retry, startWith, switchMap} from "rxjs/operators";
-import {ProcessApiService, SettingsApiService, StatementsApiService} from "../../../../core";
+import {endWith, filter, map, retry, startWith, switchMap} from "rxjs/operators";
+import {ContactsApiService, ProcessApiService, SettingsApiService, StatementsApiService} from "../../../../core";
import {catchErrorTo} from "../../../../util/rxjs";
import {arrayJoin} from "../../../../util/store";
import {fetchAttachmentsAction} from "../../../attachments/actions";
@@ -26,7 +26,9 @@
import {EErrorCode} from "../../../root/model";
import {
fetchCommentsAction,
+ fetchProcessStateInfo,
fetchStatementDetailsAction,
+ setStatementLoadingAction,
updateStatementConfigurationAction,
updateStatementEntityAction,
updateStatementInfoAction
@@ -38,7 +40,22 @@
public readonly fetchStatementDetails$ = createEffect(() => this.actions.pipe(
ofType(fetchStatementDetailsAction),
filter((action) => action.statementId != null),
- switchMap((action) => this.fetchStatementDetails(action.statementId))
+ switchMap((action) => this.fetchStatementDetails(action.statementId).pipe(
+ startWith(setStatementLoadingAction({loading: {fetchingStatementDetails: true}})),
+ endWith(setStatementLoadingAction({loading: {fetchingStatementDetails: false}})),
+ ))
+ ));
+
+ public readonly fetchStatementHistory$ = createEffect(() => this.actions.pipe(
+ ofType(fetchProcessStateInfo),
+ filter((action) => action.statementId != null),
+ switchMap((action) => merge<Action>(
+ this.fetchHistory(action.statementId),
+ this.fetchDiagram(action.statementId)
+ ).pipe(
+ startWith(setStatementLoadingAction({loading: {fetchingStatementDetails: true}})),
+ endWith(setStatementLoadingAction({loading: {fetchingStatementDetails: false}})),
+ ))
));
public constructor(
@@ -46,7 +63,8 @@
private readonly statementsApiService: StatementsApiService,
private readonly processApiService: ProcessApiService,
private readonly settingsApiService: SettingsApiService,
- private readonly taskEffect: ProcessTaskEffect
+ private readonly taskEffect: ProcessTaskEffect,
+ private readonly contactsApiService: ContactsApiService
) {
}
@@ -65,7 +83,9 @@
of(fetchAttachmentsAction({statementId})),
of(fetchCommentsAction({statementId})),
withoutConfiguration ? EMPTY : this.fetchConfiguration(statementId),
- this.fetchSectors(statementId)
+ this.fetchSectors(statementId),
+ this.fetchStatementContact(statementId),
+ this.fetchChildren(statementId)
).pipe(
startWith(updateStatementEntityAction({statementId, entity: {info}}))
);
@@ -78,20 +98,34 @@
return this.statementsApiService.getParentIds(statementId).pipe(
retry(2),
switchMap((parentIds) => {
- parentIds = arrayJoin(parentIds);
- const fetchParentsInfo$ = this.statementsApiService.getStatements(...parentIds).pipe(
- map((items) => updateStatementInfoAction({items})),
- retry(2),
- catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
- );
- return merge(
- of(updateStatementEntityAction({statementId, entity: {parentIds}})),
- parentIds.length === 0 ? EMPTY : fetchParentsInfo$
- );
+ return this.getStatementsByIds(parentIds, statementId, true);
})
);
}
+ public fetchChildren(statementId: number) {
+ return this.statementsApiService.getChildrenIds(statementId).pipe(
+ retry(2),
+ switchMap((parentIds) => {
+ return this.getStatementsByIds(parentIds, statementId, false);
+ })
+ );
+ }
+
+ public getStatementsByIds(ids: number[], statementId: number, parents: boolean) {
+ ids = arrayJoin(ids);
+ const fetchStatementInfo$ = this.statementsApiService.getStatements(...ids).pipe(
+ map((items) => updateStatementInfoAction({items})),
+ retry(2),
+ catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
+ );
+ return merge(
+ parents ? of(updateStatementEntityAction({statementId, entity: {parentIds: ids}})) :
+ of(updateStatementEntityAction({statementId, entity: {childrenIds: ids}})),
+ ids.length === 0 ? EMPTY : fetchStatementInfo$
+ );
+ }
+
public fetchWorkflowData(statementId: number) {
return this.statementsApiService.getWorkflowData(statementId).pipe(
retry(2),
@@ -140,4 +174,12 @@
);
}
+ public fetchStatementContact(statementId: number) {
+ return this.contactsApiService.getStatementsContact(statementId).pipe(
+ map((contactInfo) => updateStatementEntityAction({statementId, entity: {contactInfo}})),
+ retry(2),
+ catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
+ );
+ }
+
}
diff --git a/src/app/store/statements/effects/search/index.ts b/src/app/store/statements/effects/search/index.ts
index 589e408..6825d40 100644
--- a/src/app/store/statements/effects/search/index.ts
+++ b/src/app/store/statements/effects/search/index.ts
@@ -12,3 +12,4 @@
********************************************************************************/
export * from "./search-statements.effect";
+export * from "./search-statement-positions.effect";
diff --git a/src/app/store/statements/effects/search/search-statement-positions.effect.ts b/src/app/store/statements/effects/search/search-statement-positions.effect.ts
new file mode 100644
index 0000000..43987db
--- /dev/null
+++ b/src/app/store/statements/effects/search/search-statement-positions.effect.ts
@@ -0,0 +1,56 @@
+/********************************************************************************
+ * 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 {Injectable} from "@angular/core";
+import {Actions, createEffect, ofType} from "@ngrx/effects";
+import {Action} from "@ngrx/store";
+import {Observable, of} from "rxjs";
+import {concatMap, debounceTime, endWith, filter, startWith, switchMap} from "rxjs/operators";
+import {StatementsApiService} from "../../../../core";
+import {IAPIPositionSearchOptions} from "../../../../core/api/shared/IAPIPositionSearchOptions";
+import {catchErrorTo} from "../../../../util/rxjs";
+import {setErrorAction} from "../../../root/actions";
+import {EErrorCode} from "../../../root/model";
+import {setStatementLoadingAction, setStatementPositionSearchResultAction, startStatementPositionSearchAction} from "../../actions";
+
+@Injectable({providedIn: "root"})
+export class SearchStatementPositionsEffect {
+
+ public search$ = createEffect(() => this.actions.pipe(
+ ofType(startStatementPositionSearchAction),
+ debounceTime(200),
+ concatMap((action) => this.search(action.options))
+ ));
+
+ public constructor(
+ private readonly actions: Actions,
+ private readonly statementsApiService: StatementsApiService
+ ) {
+
+ }
+
+ public search(options: IAPIPositionSearchOptions): Observable<Action> {
+ return this.statementsApiService.getStatementPositionsSearch(options).pipe(
+ filter((results) => Array.isArray(results)),
+ switchMap((results) => {
+ return of(
+ setStatementPositionSearchResultAction({results})
+ );
+ }),
+ catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
+ startWith(setStatementLoadingAction({loading: {positionSearch: true}})),
+ endWith(setStatementLoadingAction({loading: {positionSearch: false}}))
+ );
+ }
+
+}
diff --git a/src/app/store/statements/model/IStatementEntity.ts b/src/app/store/statements/model/IStatementEntity.ts
index e614b23..1a654b9 100644
--- a/src/app/store/statements/model/IStatementEntity.ts
+++ b/src/app/store/statements/model/IStatementEntity.ts
@@ -11,6 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+import {IAPIContactPersonDetails} from "../../../core/api/contacts/IAPIContactPersonDetails";
import {IAPIProcessTask} from "../../../core/api/process";
import {IAPIDepartmentGroups} from "../../../core/api/settings";
import {IAPICommentModel, IAPIStatementModel, IAPIWorkflowData} from "../../../core/api/statements";
@@ -26,6 +27,8 @@
parentIds?: number[];
+ childrenIds?: number[];
+
comments?: IAPICommentModel[];
arrangement?: IAPITextArrangementItemModel[];
@@ -42,6 +45,8 @@
completedForMyDepartment?: boolean;
+ contactInfo?: IAPIContactPersonDetails;
+
}
export interface IStatementEntityWithTasks extends IStatementEntity {
diff --git a/src/app/store/statements/model/IStatementLoadingEntity.ts b/src/app/store/statements/model/IStatementLoadingEntity.ts
index 59343a7..e952f63 100644
--- a/src/app/store/statements/model/IStatementLoadingEntity.ts
+++ b/src/app/store/statements/model/IStatementLoadingEntity.ts
@@ -15,6 +15,8 @@
search?: boolean;
+ positionSearch?: boolean;
+
fetchingDashboardStatements?: boolean;
submittingStatementInformation?: boolean;
@@ -23,4 +25,8 @@
submittingStatementEditorForm?: boolean;
+ submittingConsiderationFiles?: boolean;
+
+ fetchingStatementDetails?: boolean;
+
}
diff --git a/src/app/store/statements/model/IStatementsStoreState.ts b/src/app/store/statements/model/IStatementsStoreState.ts
index 5eb56d8..b0f66bd 100644
--- a/src/app/store/statements/model/IStatementsStoreState.ts
+++ b/src/app/store/statements/model/IStatementsStoreState.ts
@@ -11,7 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {IAPIPaginationResponse} from "../../../core";
+import {IAPIPaginationResponse, IAPIPositionSearchStatementModel} from "../../../core";
import {TStoreEntities} from "../../../util/store";
import {IStatementConfigurationEntity} from "./IStatementConfigurationEntity";
import {IStatementEntity} from "./IStatementEntity";
@@ -30,4 +30,6 @@
loading?: IStatementLoadingEntity;
+ positions?: IAPIPositionSearchStatementModel[];
+
}
diff --git a/src/app/store/statements/reducers/search/statement-positions-search.reducer.ts b/src/app/store/statements/reducers/search/statement-positions-search.reducer.ts
new file mode 100644
index 0000000..d412157
--- /dev/null
+++ b/src/app/store/statements/reducers/search/statement-positions-search.reducer.ts
@@ -0,0 +1,25 @@
+/********************************************************************************
+ * 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 {createReducer, on} from "@ngrx/store";
+import {IAPIPositionSearchStatementModel} from "../../../../core/api/statements";
+import {arrayJoin} from "../../../../util/store";
+import {setStatementPositionSearchResultAction} from "../../actions";
+
+export const statementPositionsSearchReducer = createReducer<IAPIPositionSearchStatementModel[]>(
+ undefined,
+ on(setStatementPositionSearchResultAction, (state, action) => {
+ return arrayJoin(action.results)
+ .filter((statement: any) => statement?.id != null);
+ })
+);
diff --git a/src/app/store/statements/selectors/list/statement-list.selectors.ts b/src/app/store/statements/selectors/list/statement-list.selectors.ts
index d362118..187e2d2 100644
--- a/src/app/store/statements/selectors/list/statement-list.selectors.ts
+++ b/src/app/store/statements/selectors/list/statement-list.selectors.ts
@@ -17,7 +17,7 @@
import {processStateSelector} from "../../../process/selectors";
import {userRolesSelector} from "../../../root/selectors";
import {IStatementEntityWithTasks} from "../../model";
-import {statementEntitiesSelector} from "../statement.selectors";
+import {statementChildrenIdsSelector, statementEntitiesSelector, statementParentIdsSelector} from "../statement.selectors";
export const statementListSelector = createSelector(
statementEntitiesSelector,
@@ -28,6 +28,22 @@
}
);
+export const parentStatementListSelector = createSelector(
+ statementParentIdsSelector,
+ statementListSelector,
+ (ids, statements) => {
+ return arrayJoin(statements).filter((_) => arrayJoin(ids).find((id) => id === _.id) != null);
+ }
+);
+
+export const childrenStatementListSelector = createSelector(
+ statementChildrenIdsSelector,
+ statementListSelector,
+ (ids, statements) => {
+ return arrayJoin(statements).filter((_) => arrayJoin(ids).find((id) => id === _.id) != null);
+ }
+);
+
export const finishedStatementListSelector = createSelector(
statementListSelector,
(list) => list.filter((statement) => statement.finished)
diff --git a/src/app/store/statements/selectors/search/statement-search.selectors.ts b/src/app/store/statements/selectors/search/statement-search.selectors.ts
index d88147f..adc9428 100644
--- a/src/app/store/statements/selectors/search/statement-search.selectors.ts
+++ b/src/app/store/statements/selectors/search/statement-search.selectors.ts
@@ -27,3 +27,14 @@
return [];
}
);
+
+export const getSearchContentInfoSelector = createSelector(
+ getStatementSearchSelector,
+ (search) => {
+ return {
+ totalPages: search?.totalPages,
+ currentPage: search?.number,
+ size: search?.size
+ };
+ }
+);
diff --git a/src/app/store/statements/selectors/statement.selectors.ts b/src/app/store/statements/selectors/statement.selectors.ts
index 04810f8..3143a40 100644
--- a/src/app/store/statements/selectors/statement.selectors.ts
+++ b/src/app/store/statements/selectors/statement.selectors.ts
@@ -42,6 +42,11 @@
selectPropertyProjector("workflow")
);
+export const statementGeographicPositionSelector = createSelector(
+ statementWorkflowSelector,
+ selectPropertyProjector("geoPosition")
+);
+
export const statementContributionsSelector = createSelector(
statementSelector,
selectPropertyProjector("contributions")
@@ -52,6 +57,11 @@
selectArrayProjector("parentIds", [])
);
+export const statementChildrenIdsSelector = createSelector(
+ statementSelector,
+ selectArrayProjector("childrenIds", [])
+);
+
export const statementCommentsSelector = createSelector(
statementSelector,
selectArrayProjector("comments", [])
diff --git a/src/app/store/statements/selectors/statements-store-state.selectors.ts b/src/app/store/statements/selectors/statements-store-state.selectors.ts
index 746f49f..9dca721 100644
--- a/src/app/store/statements/selectors/statements-store-state.selectors.ts
+++ b/src/app/store/statements/selectors/statements-store-state.selectors.ts
@@ -25,6 +25,11 @@
selectPropertyProjector("search")
);
+export const getStatementPositionSearchSelector = createSelector(
+ statementsStoreStateSelector,
+ selectPropertyProjector("positions")
+);
+
export const getStatementLoadingSelector = createSelector(
statementsStoreStateSelector,
selectPropertyProjector("loading", {})
diff --git a/src/app/store/statements/statements-reducers.token.ts b/src/app/store/statements/statements-reducers.token.ts
index 5bf5ac6..071408f 100644
--- a/src/app/store/statements/statements-reducers.token.ts
+++ b/src/app/store/statements/statements-reducers.token.ts
@@ -21,6 +21,7 @@
statementLoadingReducer,
statementSearchReducer
} from "./reducers";
+import {statementPositionsSearchReducer} from "./reducers/search/statement-positions-search.reducer";
export const STATEMENTS_NAME = "statements";
@@ -31,6 +32,7 @@
entities: statementEntitiesReducer,
error: statementErrorReducer,
search: statementSearchReducer,
+ positions: statementPositionsSearchReducer,
loading: statementLoadingReducer
})
});
diff --git a/src/app/store/statements/statements-store.module.ts b/src/app/store/statements/statements-store.module.ts
index a8880fc..093e803 100644
--- a/src/app/store/statements/statements-store.module.ts
+++ b/src/app/store/statements/statements-store.module.ts
@@ -19,6 +19,7 @@
CompileStatementArrangementEffect,
FetchStatementDetailsEffect,
FetchTextArrangementEffect,
+ SearchStatementPositionsEffect,
SearchStatementsEffect,
SubmitStatementInformationFormEffect,
SubmitWorkflowFormEffect,
@@ -37,6 +38,7 @@
FetchStatementDetailsEffect,
FetchTextArrangementEffect,
SearchStatementsEffect,
+ SearchStatementPositionsEffect,
SubmitStatementEditorFormEffect,
SubmitStatementInformationFormEffect,
SubmitWorkflowFormEffect,
diff --git a/src/assets/i18n/de.i18.json b/src/assets/i18n/de.i18.json
index d12efee..3b1bd96 100644
--- a/src/assets/i18n/de.i18.json
+++ b/src/assets/i18n/de.i18.json
@@ -7,7 +7,8 @@
"submitting": "Daten werden übertragen...",
"header": {
"home": "Übersicht",
- "search": "Stellungnahme suchen",
+ "search": "Stellungnahme suchen (Liste)",
+ "searchMap": "Stellungnahme suchen (Karte)",
"mail": "Posteingang",
"new": "Neue Stellungnahme anlegen",
"settings": "Einstellungen",
@@ -82,6 +83,36 @@
"completeIssue": "Vorgang manuell abschließen",
"disapprove": "Nachbearbeitung anstoßen",
"approve": "Stellungnahme genehmigen"
+ },
+ "attachments": {
+ "title": "Eingangsdokumente",
+ "filter": "Filter",
+ "noResult": "Keine Anhänge vorhanden.",
+ "emailDocuments": "Email-Dokumente",
+ "email": "Email",
+ "placeholder": "Es sind keine Anhänge zu der Stellungnahme vorhanden."
+ },
+ "outbox": {
+ "title": "Ausgangsdokumente",
+ "statement": "Stellungnahme",
+ "attachments": "Anhänge",
+ "placeholder": "Es sind keine Anhänge zu der Stellungnahme vorhanden."
+ },
+ "considerations": {
+ "title": "Abwägungsergebnis",
+ "attachments": "Dokumente",
+ "upload": "Hochladen",
+ "placeholder": "Es sind keine Abwägungsergebnisse zu der Stellungnahme vorhanden."
+ },
+ "contributions": {
+ "placeholder": "Es wurden keine Fachbereiche zur Bearbeitung ausgewählt."
+ },
+ "geoPositions": {
+ "placeholder": "Es wurde keine Position zu der Stellungnahme hinterlegt."
+ },
+ "linkedStatements": {
+ "title": "Verknüpfte Vorgänge",
+ "placeholder": "Es gibt keine verlinkten Stellungnahmen."
}
},
"edit": {
@@ -152,7 +183,8 @@
"city": "Ort:",
"district": "Ortsteil:",
"typeId": "Art des Vorgangs:",
- "customerReference": "Referenzzeichen:"
+ "customerReference": "Referenzzeichen:",
+ "sectors": "Betroffene Sparten:"
}
},
"workflowDataForm": {
@@ -189,9 +221,15 @@
"contributionStatus": "Bearbeitungsstatus der Fachbereiche",
"draft": "Entwurf der Stellungnahme",
"attachments": "Anhänge für den Versand"
+ },
+ "contributions": {
+ "placeholder": "Es gibt keine verlinkten Stellungnahmen."
}
},
"shared": {
+ "pagination": {
+ "size": "Einträge pro Seite"
+ },
"map": {
"openGIS": "Im GIS öffnen"
},
@@ -234,6 +272,7 @@
"failedMailTransfer": "Beim Transferieren der Email ist ein Fehler aufgetreten. Bitte prüfen Sie Ihre Auswahl und versuchen Sie es erneut.",
"invalidTextArrangement": "Die Zusammenstellung der Stellungnahme weist Fehler auf. Bitte prüfen Sie die Auswahl an Textbausteinen.",
"couldNotLoadMailData": "Die Daten der ausgewählten Email konnten nicht geladen werden. Eventuell besteht ein Problem mit der Verbindung zum Mail-Server. Bitte versuchen Sie es nocheinmal oder kontaktieren Sie den Support.",
+ "couldNotSendMail": "Die Email konnte nicht an die hinterlegte Email-Adresse verschickt werden. Bitte prüfen Sie die Kontaktinformationen oder versenden Sie die Stellungnahme manuell.",
"noAccessToContactModule": "Der Zugriff auf das Kontaktstammdatenmodul ist nicht möglich. Bitte kontaktieren Sie den Support."
}
},
@@ -260,5 +299,20 @@
"at": "vom:",
"inbox": "Email Eingang",
"attachments": "Anhänge"
+ },
+ "search": {
+ "title": "Suche",
+ "executeSearch": "Nach Stellungnahmen suchen...",
+ "type": "Vorgangstyp",
+ "noData": "Keine Daten verfügbar...",
+ "creationDateFrom": "Erstellungsdatum von:",
+ "creationDateTo": "Erstellungsdatum bis:",
+ "receiptDateFrom": "Eingang von:",
+ "receiptDateTo": "Eingang bis:",
+ "dueDateFrom": "Frist von:",
+ "dueDateTo": "Frist bis:",
+ "finished": "Abgeschlossen",
+ "open": "Offen",
+ "editedByMe": "Eigene Vorgänge"
}
}
diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts
index 66dc98e..549464f 100644
--- a/src/environments/environment.prod.ts
+++ b/src/environments/environment.prod.ts
@@ -16,7 +16,5 @@
export const environment = {
production: true,
version: npm.version,
- routes: {...npm.routes},
- leaflet: {...npm.leaflet},
imports: []
};
diff --git a/src/environments/environment.ts b/src/environments/environment.ts
index 99596a3..2e28e37 100644
--- a/src/environments/environment.ts
+++ b/src/environments/environment.ts
@@ -21,8 +21,6 @@
export const environment = {
production: false,
version: npm.version + "dev",
- routes: {...npm.routes},
- leaflet: {...npm.leaflet},
imports: [
StoreDevtoolsModule.instrument({maxAge: 50, logOnly: false})
]
diff --git a/src/theme/leaflet/_leaflet.theme.scss b/src/theme/leaflet/_leaflet.theme.scss
index f810012..06feb1f 100644
--- a/src/theme/leaflet/_leaflet.theme.scss
+++ b/src/theme/leaflet/_leaflet.theme.scss
@@ -32,3 +32,7 @@
content: "place";
}
}
+
+.leaflet-popup-content-wrapper {
+ @include rounded-border-mixin();
+}