[TOB-241,38,20,62,396,397,399,400,73,458,460,425] feat: v1.0.0
[TOB-241] feat: Add configuration page for attachment tags
* Add basic settings routing
* Add back end calls to add tags
* Add component for list of items
* Integrate component into settings page
[TOB-38] feat: Add configuration page for departments
* Add util functions to parse CSV data
* Add back end calls for departments settings
* Add store effect for departments settings
* Add pipes to convert departments data
* Add component to show department table
* Integrate components into settings page
[TOB-20] feat: Add configuration page for text blocks
* Add back end calls for text block config
* Add store effect for text block config
* Add pipe to create select options from array
* Resize text areas after rendering
* Add class for text block configuration form
* Add component to edit text blocks
* Add component to edit text block groups
* Add component to edit select lists
* Integrate components into settings page
[TOB-62] feat: Add configuration page for user
* Add back end calls for user configuration
* Add user table component
* Add user settings form component
* Add pipes to transform and filter user data
* Integrate components into settings page
[TOB-396] feat: Select position on leaflet map only via click
[TOB-397] feat: Add location search for leaflet map
* Add back end calls for Nominatim search
* Add search bar to leaflet map component
* Reorganize map module as feature module
* Integrate map store into leaflet map component
[TOB-399] feat: Show username of task that is currently claimed
[TOB-400] feat: Show confirm dialog before delete operations
[TOB-73] feat: Add configurable help url
* Add help url to app.config.json
* Use help url in header bar
* Redirect to help url on help route
[TOB-458] refactor: Auto activate search filters on value changes
* Auto activate search filter on value change for statement search
* Auto activate search filter on value change for user search
[TOB-460] feat: Open GIS merely via GET call
[TOB-425] fix: Minor bug fixes
* Fix minor styling issues
* Hide contribution status in task create negative response
Signed-off-by: Christopher Keim <keim@develop-group.de>
diff --git a/README.md b/README.md
index 82712e5..80d921e 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@
* `routes.portal`: Route on which the main portal is served
* `routes.contactDataBase`: Route on which the contact data base
module is served
+* `routes.userDocu`: Route on which the user documentation 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
diff --git a/app.config.json b/app.config.json
index e1ecf44..8f73e86 100644
--- a/app.config.json
+++ b/app.config.json
@@ -18,6 +18,7 @@
"routes": {
"spaBackend": "/statementpaBE",
"portal": "/portalFE",
- "contactDataBase": "/contactdatabase"
+ "contactDataBase": "/contactdatabase",
+ "userDocu": "./assets/docu/userDocumentation.pdf"
}
}
diff --git a/package-lock.json b/package-lock.json
index c1b2c07..2e75d93 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "openkonsequenz-statement-public-affairs",
- "version": "0.9.0",
+ "version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -4649,9 +4649,9 @@
}
},
"bl": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz",
- "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz",
+ "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==",
"dev": true,
"requires": {
"buffer": "^5.5.0",
@@ -4660,13 +4660,13 @@
},
"dependencies": {
"buffer": {
- "version": "5.6.0",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz",
- "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==",
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"requires": {
- "base64-js": "^1.0.2",
- "ieee754": "^1.1.4"
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
}
},
"readable-stream": {
@@ -11789,9 +11789,9 @@
}
},
"node-fetch": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
- "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==",
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
+ "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
"dev": true
},
"node-fetch-npm": {
@@ -11806,9 +11806,9 @@
}
},
"node-forge": {
- "version": "0.9.0",
- "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz",
- "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==",
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
+ "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
"dev": true
},
"node-libs-browser": {
@@ -16121,12 +16121,12 @@
}
},
"selfsigned": {
- "version": "1.10.7",
- "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz",
- "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==",
+ "version": "1.10.8",
+ "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz",
+ "integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==",
"dev": true,
"requires": {
- "node-forge": "0.9.0"
+ "node-forge": "^0.10.0"
}
},
"semver": {
diff --git a/package.json b/package.json
index 2d0c3be..35ed20a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "openkonsequenz-statement-public-affairs",
- "version": "0.9.0",
+ "version": "1.0.0",
"description": "Statement Public Affairs",
"license": "Eclipse Public License - v 2.0",
"repository": {
diff --git a/src/app/app-routing.module.spec.ts b/src/app/app-routing.module.spec.ts
index aab37b5..4c18b95 100644
--- a/src/app/app-routing.module.spec.ts
+++ b/src/app/app-routing.module.spec.ts
@@ -104,7 +104,7 @@
it("should navigate to /settings", async () => {
const isRoutingSuccessful = await callInZone(() => router.navigate(["settings"]));
expect(isRoutingSuccessful).toBeTruthy();
- expect(location.path()).toBe("/settings");
+ expect(location.path()).toBe("/settings/text-blocks");
});
});
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index 7ebe298..455f6b6 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -13,6 +13,7 @@
import {NgModule} from "@angular/core";
import {PreloadAllModules, RouterModule, Routes} from "@angular/router";
+import {RedirectRouteGuard} from "./core";
export const appRoutes: Routes = [
{
@@ -50,6 +51,14 @@
loadChildren: () => import("./features/settings/settings-routing.module")
.then((m) => m.SettingsRoutingModule)
},
+ {
+ path: "help",
+ canActivate: [RedirectRouteGuard],
+ component: RedirectRouteGuard,
+ data: {
+ externalUrl: "help"
+ }
+ },
// The wildcard has to be placed as the last item (otherwise, its overriding routes)
{
path: "**",
diff --git a/src/app/core/api/attachments/EAPIStaticAttachmentTagIds.ts b/src/app/core/api/attachments/EAPIStaticAttachmentTagIds.ts
index e25c4b3..784fb61 100644
--- a/src/app/core/api/attachments/EAPIStaticAttachmentTagIds.ts
+++ b/src/app/core/api/attachments/EAPIStaticAttachmentTagIds.ts
@@ -11,16 +11,25 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+/**
+ * Attachment tags that cannot be set manually. Those are automatically set on specified task steps.
+ * E.g. attachments tranferred from a mail are automatically assigned the "email" tag.
+ */
export enum EAPIStaticAttachmentTagIds {
+ // Used for the attachment generated from the mail body.
EMAIL_TEXT = "email-text",
+ // Used for the transferred mail attachments.
EMAIL = "email",
+ // Used for all attachments that will be send with the generated statement pdf.
OUTBOX = "outbox",
+ // Used for the attachments uploaded after the statement has been finished.
CONSIDERATION = "consideration",
+ // Used for the generated statement pdf from text block arrangement.
STATEMENT = "statement",
COVER_LETTER = "cover-letter"
diff --git a/src/app/core/api/attachments/attachments-api.service.ts b/src/app/core/api/attachments/attachments-api.service.ts
index e6d4835..cd92f7e 100644
--- a/src/app/core/api/attachments/attachments-api.service.ts
+++ b/src/app/core/api/attachments/attachments-api.service.ts
@@ -85,8 +85,9 @@
* Creates a new tag in the back end data base.
*/
public addNewTag(label: string) {
+ const params = {label};
const endPoint = `/tags`;
- return this.httpClient.put(urlJoin(this.baseUrl, endPoint), {label});
+ return this.httpClient.put(urlJoin(this.baseUrl, endPoint), {}, {params});
}
}
diff --git a/src/app/core/api/contacts/IAPIContactPerson.ts b/src/app/core/api/contacts/IAPIContactPerson.ts
index d92f4c1..3dde31f 100644
--- a/src/app/core/api/contacts/IAPIContactPerson.ts
+++ b/src/app/core/api/contacts/IAPIContactPerson.ts
@@ -11,6 +11,9 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+/**
+ * Interface which represents the model of all basic information of a specific contact.
+ */
export interface IAPIContactPerson {
companyId: string;
companyName: string;
diff --git a/src/app/core/api/contacts/IAPIContactPersonDetails.ts b/src/app/core/api/contacts/IAPIContactPersonDetails.ts
index 4fcda36..5f5acb0 100644
--- a/src/app/core/api/contacts/IAPIContactPersonDetails.ts
+++ b/src/app/core/api/contacts/IAPIContactPersonDetails.ts
@@ -11,6 +11,10 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+/**
+ * Interface which represents all data accessible for a specific contact.
+ * This contains additional values to IAPIContactPerson.
+ */
export interface IAPIContactPersonDetails {
community: string;
communitySuffix: string;
diff --git a/src/app/core/api/core/EAPIUserRoles.ts b/src/app/core/api/core/EAPIUserRoles.ts
index 3e2e8d8..c78fe05 100644
--- a/src/app/core/api/core/EAPIUserRoles.ts
+++ b/src/app/core/api/core/EAPIUserRoles.ts
@@ -16,7 +16,8 @@
ROLE_SPA_ACCESS = "ROLE_SPA_ACCESS",
SPA_APPROVER = "ROLE_SPA_APPROVER",
SPA_OFFICIAL_IN_CHARGE = "ROLE_SPA_OFFICIAL_IN_CHARGE",
- SPA_ADMIN = "ROLE_SPA_ADMIN"
+ SPA_ADMIN = "ROLE_SPA_ADMIN",
+ SPA_CUSTOMER = "ROLE_SPA_CUSTOMER"
}
export const ALL_NON_TRIVIAL_USER_ROLES = [
diff --git a/src/app/core/api/core/IAPIUserInfo.ts b/src/app/core/api/core/IAPIUserInfo.ts
index 00eeedd..a1e3e86 100644
--- a/src/app/core/api/core/IAPIUserInfo.ts
+++ b/src/app/core/api/core/IAPIUserInfo.ts
@@ -13,6 +13,9 @@
import {EAPIUserRoles} from "./EAPIUserRoles";
+/**
+ * Interface which represents basic information of a specific user.
+ */
export interface IAPIUserInfo {
firstName: string;
lastName: string;
diff --git a/src/app/core/api/core/IAPIVersion.ts b/src/app/core/api/core/IAPIVersion.ts
index 81fed53..584ad0e 100644
--- a/src/app/core/api/core/IAPIVersion.ts
+++ b/src/app/core/api/core/IAPIVersion.ts
@@ -11,6 +11,9 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+/**
+ * Interface which represents version information.
+ */
export interface IAPIVersion {
buildVersion: string;
applicationName: string;
diff --git a/src/app/core/api/geo/IAPIGeographicPositions.ts b/src/app/core/api/geo/IAPIGeographicPositions.ts
index 687b05a..817167c 100644
--- a/src/app/core/api/geo/IAPIGeographicPositions.ts
+++ b/src/app/core/api/geo/IAPIGeographicPositions.ts
@@ -11,6 +11,9 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+/**
+ * Multiple geo positions as object properties.
+ */
export interface IAPIGeographicPositions {
[key: string]: {
x: number;
diff --git a/src/app/core/api/geo/IAPINominatimSearchResult.ts b/src/app/core/api/geo/IAPINominatimSearchResult.ts
new file mode 100644
index 0000000..abd3528
--- /dev/null
+++ b/src/app/core/api/geo/IAPINominatimSearchResult.ts
@@ -0,0 +1,39 @@
+/********************************************************************************
+ * 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
+ ********************************************************************************/
+
+/**
+ * Search result from nominatim server. Includes information about a specific place like coordinates and name/description.
+ */
+export interface IAPINominatimSearchResult {
+ place_id: number;
+ licence: string;
+ osm_type: string;
+ osm_id: number;
+ boundingbox: [string, string, string, string];
+ lat: string;
+ lon: string;
+ display_name: string;
+ place_rank: number;
+ category: string;
+ type: string;
+ importance: number;
+ geojson: {
+ type: string;
+ coordinates: [
+ [string, string],
+ [string, string],
+ [string, string],
+ [string, string]
+ ]
+ };
+}
diff --git a/src/app/core/api/geo/geo-api.service.ts b/src/app/core/api/geo/geo-api.service.ts
index 700163b..0e925f3 100644
--- a/src/app/core/api/geo/geo-api.service.ts
+++ b/src/app/core/api/geo/geo-api.service.ts
@@ -15,23 +15,38 @@
import {Inject, Injectable} from "@angular/core";
import {Observable} from "rxjs";
import {objectToHttpParams, urlJoin} from "../../../util/http";
+import {APP_CONFIGURATION, IAppConfiguration} from "../../configuration";
import {SPA_BACKEND_ROUTE} from "../../external-routes";
import {IAPIGeographicPositions} from "./IAPIGeographicPositions";
+import {IAPINominatimSearchResult} from "./IAPINominatimSearchResult";
@Injectable({providedIn: "root"})
export class GeoApiService {
public constructor(
protected readonly httpClient: HttpClient,
- @Inject(SPA_BACKEND_ROUTE) protected readonly baseUrl: string
+ @Inject(SPA_BACKEND_ROUTE) protected readonly baseUrl: string,
+ @Inject(APP_CONFIGURATION) protected readonly configuration: IAppConfiguration
) {
}
+ /**
+ * Sends geo positions and input + output format and receives the transformed positions back.
+ */
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});
}
+ /**
+ * Searches for a specific place to retrieve coordinates to display.
+ */
+ public search(searchQuery: string) {
+ const endPoint = `search`;
+ const params = objectToHttpParams({q: this.configuration.nominatim.searchQueryPrefix + " " + searchQuery, format: "json"});
+ return this.httpClient.get<IAPINominatimSearchResult[]>(urlJoin(this.configuration.nominatim.url, endPoint), {params});
+ }
+
}
diff --git a/src/app/core/api/geo/index.ts b/src/app/core/api/geo/index.ts
index 9f1b524..b40cc25 100644
--- a/src/app/core/api/geo/index.ts
+++ b/src/app/core/api/geo/index.ts
@@ -12,4 +12,5 @@
********************************************************************************/
export * from "./IAPIGeographicPositions";
+export * from "./IAPINominatimSearchResult";
export * from "./geo-api.service";
diff --git a/src/app/core/api/mail/IAPIEmailAttachmentModel.ts b/src/app/core/api/mail/IAPIEmailAttachmentModel.ts
index eeb188c..a6c8787 100644
--- a/src/app/core/api/mail/IAPIEmailAttachmentModel.ts
+++ b/src/app/core/api/mail/IAPIEmailAttachmentModel.ts
@@ -11,6 +11,9 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+/**
+ * Interface which represents the model of an email attachment.
+ */
export interface IAPIEmailAttachmentModel {
name: string;
size: string;
diff --git a/src/app/core/api/mail/IAPIEmailModel.ts b/src/app/core/api/mail/IAPIEmailModel.ts
index a087714..1b03290 100644
--- a/src/app/core/api/mail/IAPIEmailModel.ts
+++ b/src/app/core/api/mail/IAPIEmailModel.ts
@@ -13,6 +13,9 @@
import {IAPIEmailAttachmentModel} from "./IAPIEmailAttachmentModel";
+/**
+ * Interface which represents the model of an email. Contains meta data and mail content as text or html.
+ */
export interface IAPIEmailModel {
identifier: string;
subject: string;
diff --git a/src/app/core/api/process/IAPIStatementHistory.ts b/src/app/core/api/process/IAPIStatementHistory.ts
index 8177eb2..d4cac72 100644
--- a/src/app/core/api/process/IAPIStatementHistory.ts
+++ b/src/app/core/api/process/IAPIStatementHistory.ts
@@ -11,6 +11,9 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+/**
+ * Interface which represents the model of the statement history as saved in the back end database.
+ */
export interface IAPIStatementHistory {
processName: string;
processVersion: number;
@@ -18,6 +21,9 @@
currentProcessActivities: IAPIProcessActivity[];
}
+/**
+ * Interface which represents the model of a single process activity.
+ */
export interface IAPIProcessActivity {
id: string;
activityId: string;
diff --git a/src/app/core/api/process/process-api.service.ts b/src/app/core/api/process/process-api.service.ts
index b57dced..b8260fe 100644
--- a/src/app/core/api/process/process-api.service.ts
+++ b/src/app/core/api/process/process-api.service.ts
@@ -70,11 +70,17 @@
return this.httpClient.post<void>(urlJoin(this.baseUrl, endPoint), body);
}
+ /**
+ * Fetches the statement history.
+ */
public getStatementHistory(statementId: number) {
const endPoint = `/process/statements/${statementId}/history`;
return this.httpClient.get<IAPIStatementHistory>(urlJoin(this.baseUrl, endPoint));
}
+ /**
+ * Fetches the statement process diagram. Response is a xml sent as text.
+ */
public getStatementProcessDiagram(statementId: number) {
const endPoint = `/process/statements/${statementId}/workflowmodel`;
return this.httpClient.get(urlJoin(this.baseUrl, endPoint), {responseType: "text"});
diff --git a/src/app/core/api/settings/IAPIDepartmentsConfiguration.ts b/src/app/core/api/settings/IAPIDepartmentsConfiguration.ts
index 8a562ef..11cad75 100644
--- a/src/app/core/api/settings/IAPIDepartmentsConfiguration.ts
+++ b/src/app/core/api/settings/IAPIDepartmentsConfiguration.ts
@@ -39,3 +39,14 @@
[groupName: string]: string[];
}
+
+/**
+ * Interface which models the underlying table to assign city and district values to sectors and departments.
+ * Each key represents a pair of city and district concatenated with a #.
+ */
+export interface IAPIDepartmentTable {
+ [cityDistrict: string]: {
+ provides: string[];
+ departments: IAPIDepartmentGroups
+ };
+}
diff --git a/src/app/shared/leaflet/index.ts b/src/app/core/api/settings/IAPIUserInfoExtended.ts
similarity index 73%
copy from src/app/shared/leaflet/index.ts
copy to src/app/core/api/settings/IAPIUserInfoExtended.ts
index 849c149..31bfc5a 100644
--- a/src/app/shared/leaflet/index.ts
+++ b/src/app/core/api/settings/IAPIUserInfoExtended.ts
@@ -10,10 +10,10 @@
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+import {IAPIUserInfo} from "../core";
+import {IAPIUserSettings} from "./IAPIUserSettings";
-export * from "./directives";
-export * from "./pipes";
-export * from "./util";
-
-export * from "./leaflet.module";
-export * from "./leaflet-configuration.token";
+export interface IAPIUserInfoExtended extends IAPIUserInfo {
+ id: number;
+ settings: IAPIUserSettings;
+}
diff --git a/src/app/shared/controls/map-select/components/map-select.component.scss b/src/app/core/api/settings/IAPIUserSettings.ts
similarity index 81%
copy from src/app/shared/controls/map-select/components/map-select.component.scss
copy to src/app/core/api/settings/IAPIUserSettings.ts
index 7e59f1f..02edab5 100644
--- a/src/app/shared/controls/map-select/components/map-select.component.scss
+++ b/src/app/core/api/settings/IAPIUserSettings.ts
@@ -11,10 +11,10 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "openk.styles";
-
-:host {
- display: block;
- width: 100%;
- height: 100%;
+export interface IAPIUserSettings {
+ email?: string;
+ department?: {
+ group: string;
+ name: string;
+ };
}
diff --git a/src/app/core/api/settings/index.ts b/src/app/core/api/settings/index.ts
index 4137763..9940ee4 100644
--- a/src/app/core/api/settings/index.ts
+++ b/src/app/core/api/settings/index.ts
@@ -13,5 +13,7 @@
export * from "./IAPIDepartmentsConfiguration";
export * from "./IAPIStatementType";
+export * from "./IAPIUserInfoExtended";
+export * from "./IAPIUserSettings";
export * from "./settings-api.service";
diff --git a/src/app/core/api/settings/settings-api.service.ts b/src/app/core/api/settings/settings-api.service.ts
index e84168b..68c900c 100644
--- a/src/app/core/api/settings/settings-api.service.ts
+++ b/src/app/core/api/settings/settings-api.service.ts
@@ -15,9 +15,12 @@
import {Inject, Injectable} from "@angular/core";
import {urlJoin} from "../../../util";
import {SPA_BACKEND_ROUTE} from "../../external-routes";
-import {IAPISectorsModel} from "../statements/IAPISectorsModel";
-import {IAPIDepartmentsConfiguration} from "./IAPIDepartmentsConfiguration";
+import {IAPISectorsModel} from "../statements";
+import {IAPITextBlockConfigurationModel} from "../text";
+import {IAPIDepartmentsConfiguration, IAPIDepartmentTable} from "./IAPIDepartmentsConfiguration";
import {IAPIStatementType} from "./IAPIStatementType";
+import {IAPIUserInfoExtended} from "./IAPIUserInfoExtended";
+import {IAPIUserSettings} from "./IAPIUserSettings";
@Injectable({
providedIn: "root"
@@ -55,4 +58,61 @@
return this.httpClient.get<IAPISectorsModel>(urlJoin(this.baseUrl, endPoint));
}
+ /**
+ * Fetches the table with all departments and sectors from the back end.
+ */
+ public getDepartmentTable() {
+ const endPoint = `/admin/departments`;
+ return this.httpClient.get<IAPIDepartmentTable>(urlJoin(this.baseUrl, endPoint));
+ }
+
+ /**
+ * Sets the table of all department and sectors in the back end.
+ */
+ public putDepartmentTable(data: IAPIDepartmentTable) {
+ const endPoint = "/admin/departments";
+ return this.httpClient.put(urlJoin(this.baseUrl, endPoint), data);
+ }
+
+ /**
+ * Fetches the text block configuration for all newly created statements from the back end.
+ */
+ public getTextblockConfig() {
+ const endPoint = `/admin/textblockconfig`;
+ return this.httpClient.get<IAPITextBlockConfigurationModel>(urlJoin(this.baseUrl, endPoint));
+ }
+
+ /**
+ * Sets the text block configuration for all newly created statements in the back end.
+ */
+ public putTextblockConfig(data: IAPITextBlockConfigurationModel) {
+ const endPoint = "/admin/textblockconfig";
+ return this.httpClient.put(urlJoin(this.baseUrl, endPoint), data);
+ }
+
+ /**
+ * Triggers the backend to refresh user data with auth-n-auth module. User data is only refreshed after receiving this command, not
+ * automatically.
+ */
+ public syncUserData() {
+ const endPoint = "/admin/users-sync";
+ return this.httpClient.post(urlJoin(this.baseUrl, endPoint), {});
+ }
+
+ /**
+ * Fetches the list of users with assigned email addresses from the back end.
+ */
+ public fetchUsers() {
+ const endPoint = "/admin/users";
+ return this.httpClient.get<IAPIUserInfoExtended[]>(urlJoin(this.baseUrl, endPoint), {});
+ }
+
+ /**
+ * Sets settings like email address or department assignment for a specific user in the back end database.
+ */
+ public setUserData(userId: number, settings: IAPIUserSettings) {
+ const endPoint = `/admin/users/${userId}/settings`;
+ return this.httpClient.post(urlJoin(this.baseUrl, endPoint), {...settings});
+ }
+
}
diff --git a/src/app/core/api/shared/IAPISearchOptions.ts b/src/app/core/api/shared/IAPISearchOptions.ts
index 93d07f8..f406ef5 100644
--- a/src/app/core/api/shared/IAPISearchOptions.ts
+++ b/src/app/core/api/shared/IAPISearchOptions.ts
@@ -19,7 +19,7 @@
/**
* Search strings to find statements.
*/
- q: string;
+ q?: string;
/**
* Size of the loaded page.
diff --git a/src/app/core/api/statements/IAPIPositionSearchStatementModel.ts b/src/app/core/api/statements/IAPIPositionSearchStatementModel.ts
index 8de60b7..da6ab17 100644
--- a/src/app/core/api/statements/IAPIPositionSearchStatementModel.ts
+++ b/src/app/core/api/statements/IAPIPositionSearchStatementModel.ts
@@ -11,6 +11,10 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+/**
+ * Interface that represents the model of a statement from a position search call. Similiar to IAPIStatementModel but has information
+ * specifically for the position search such as position and dueDate.
+ */
export interface IAPIPositionSearchStatementModel {
id: number;
diff --git a/src/app/core/api/statements/IAPISectorsModel.ts b/src/app/core/api/statements/IAPISectorsModel.ts
index c6cffa9..b7ca879 100644
--- a/src/app/core/api/statements/IAPISectorsModel.ts
+++ b/src/app/core/api/statements/IAPISectorsModel.ts
@@ -11,6 +11,10 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+/**
+ * Interface that represents the model the sector information. Contains properties that describe which sectors are available at that
+ * location.
+ */
export interface IAPISectorsModel {
[sectorIdentifier: string]: string[];
diff --git a/src/app/core/api/statements/index.ts b/src/app/core/api/statements/index.ts
index 91527b0..ac56225 100644
--- a/src/app/core/api/statements/index.ts
+++ b/src/app/core/api/statements/index.ts
@@ -12,8 +12,10 @@
********************************************************************************/
export * from "./IAPICommentModel";
+export * from "./IAPIDashboardStatementModel";
+export * from "./IAPIPositionSearchStatementModel";
+export * from "./IAPISectorsModel";
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 d611a35..c798a47 100644
--- a/src/app/core/api/statements/statements-api.service.ts
+++ b/src/app/core/api/statements/statements-api.service.ts
@@ -16,8 +16,7 @@
import {objectToHttpParams, urlJoin} from "../../../util";
import {SPA_BACKEND_ROUTE} from "../../external-routes";
import {IAPIDepartmentGroups} from "../settings";
-import {IAPIPaginationResponse, IAPISearchOptions} from "../shared";
-import {IAPIPositionSearchOptions} from "../shared/IAPIPositionSearchOptions";
+import {IAPIPaginationResponse, IAPIPositionSearchOptions, IAPISearchOptions} from "../shared";
import {IAPICommentModel} from "./IAPICommentModel";
import {IAPIDashboardStatementModel} from "./IAPIDashboardStatementModel";
import {IAPIPositionSearchStatementModel} from "./IAPIPositionSearchStatementModel";
@@ -57,7 +56,7 @@
}
/**
- *
+ * Search for a list of statements in the back end data base.
*/
public getStatementPositionsSearch(searchOptions: IAPIPositionSearchOptions) {
const endPoint = `statementpositionsearch`;
diff --git a/src/app/core/api/text/IAPITextBlockModel.ts b/src/app/core/api/text/IAPITextBlockModel.ts
index 52b4f3b..79c301e 100644
--- a/src/app/core/api/text/IAPITextBlockModel.ts
+++ b/src/app/core/api/text/IAPITextBlockModel.ts
@@ -39,3 +39,5 @@
requires: IAPIRequireRuleModel[];
}
+
+export type TAPITextBlockRuleKey = keyof IAPITextBlockModel & ("requires" | "excludes");
diff --git a/src/app/core/auth/auth-interceptor.service.ts b/src/app/core/auth/auth-interceptor.service.ts
index 4e79d17..85686fb 100644
--- a/src/app/core/auth/auth-interceptor.service.ts
+++ b/src/app/core/auth/auth-interceptor.service.ts
@@ -19,6 +19,9 @@
import {SPA_BACKEND_ROUTE} from "../external-routes";
import {AuthService} from "./auth.service";
+/**
+ * This service adds the access token to the headers for all backend requests if needed.
+ */
@Injectable({providedIn: "root"})
export class AuthInterceptorService implements HttpInterceptor {
diff --git a/src/app/core/configuration/app-configuration.token.ts b/src/app/core/configuration/app-configuration.token.ts
index 5b5eeba..b36d46b 100644
--- a/src/app/core/configuration/app-configuration.token.ts
+++ b/src/app/core/configuration/app-configuration.token.ts
@@ -35,6 +35,7 @@
spaBackend: string;
portal: string;
contactDataBase: string;
+ userDocu: string;
};
}
diff --git a/src/app/core/confirm/confirm.service.ts b/src/app/core/confirm/confirm.service.ts
new file mode 100644
index 0000000..8f5fd82
--- /dev/null
+++ b/src/app/core/confirm/confirm.service.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 {Inject, Injectable} from "@angular/core";
+import {WINDOW} from "../dom";
+
+@Injectable({providedIn: "root"})
+export class ConfirmService {
+
+ public constructor(
+ @Inject(WINDOW) private readonly window: Window
+ ) {
+
+ }
+
+ /**
+ * Opens a confirmation dialog on the browser window and returns result. OK = true, Cancel = false.
+ */
+ public askForConfirmation(message: string) {
+ return this.window.confirm(message);
+ }
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/core/confirm/index.ts
similarity index 93%
copy from src/app/features/settings/components/index.ts
copy to src/app/core/confirm/index.ts
index 990bb42..4a397bd 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/core/confirm/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./confirm.service";
diff --git a/src/app/core/download/download.service.ts b/src/app/core/download/download.service.ts
index d69dbdd..68c2b37 100644
--- a/src/app/core/download/download.service.ts
+++ b/src/app/core/download/download.service.ts
@@ -20,6 +20,10 @@
}
+ /**
+ * To download an attachment the backend link to the attachment is put into an dom element, clicked and the value is removed again.
+ * This is done so the needed accessToken for the data access is not displayed on hovering the button used for the download.
+ */
public startDownload(url: string, token?: string) {
url += token == null ? "" : `?accessToken=${token}`;
const anchor = this.document.createElement("a");
diff --git a/src/app/core/external-routes/index.ts b/src/app/core/external-routes/index.ts
index a4576e0..2455a42 100644
--- a/src/app/core/external-routes/index.ts
+++ b/src/app/core/external-routes/index.ts
@@ -12,5 +12,8 @@
********************************************************************************/
export * from "./contact-data-base-route.token";
+export * from "./nominatim-route.token";
+export * from "./redirect-route-guard.service";
export * from "./portal-route.token";
export * from "./spa-backend-route.token";
+export * from "./user-docu-route.token";
diff --git a/src/app/core/external-routes/redirect-route-guard.service.ts b/src/app/core/external-routes/redirect-route-guard.service.ts
new file mode 100644
index 0000000..fbf5fb6
--- /dev/null
+++ b/src/app/core/external-routes/redirect-route-guard.service.ts
@@ -0,0 +1,36 @@
+/********************************************************************************
+ * 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 {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from "@angular/router";
+import {HELP_ROUTE} from "./user-docu-route.token";
+
+@Injectable({
+ providedIn: "root"
+})
+export class RedirectRouteGuard implements CanActivate {
+
+ public constructor(
+ @Inject(HELP_ROUTE) public helpRoute: string
+ ) {
+
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
+ if (route.data.externalUrl === "help") {
+ window.location.href = this.helpRoute;
+ }
+ return false;
+ }
+
+}
diff --git a/src/app/core/external-routes/user-docu-route.token.ts b/src/app/core/external-routes/user-docu-route.token.ts
new file mode 100644
index 0000000..43daa26
--- /dev/null
+++ b/src/app/core/external-routes/user-docu-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 main portal.
+ */
+export const HELP_ROUTE = new InjectionToken<string>("External route user documentation", {
+ providedIn: "root",
+ factory: () => (config as IAppConfiguration).routes.userDocu
+});
diff --git a/src/app/core/i18n/i18n.service.ts b/src/app/core/i18n/i18n.service.ts
index f5b5ab6..5d91b55 100644
--- a/src/app/core/i18n/i18n.service.ts
+++ b/src/app/core/i18n/i18n.service.ts
@@ -16,6 +16,11 @@
import * as moment from "moment";
import {WINDOW} from "../dom";
+/**
+ * This component is used to set the language to use for the app. The language is set to the language of the current browser window the app
+ * was opened in. If there is no language file for the browser language, the default specified for the translation service is used.
+ * The default language for momentjs is set to the value specified in the translation file for the selected display language.
+ */
@Injectable({providedIn: "root"})
export class I18nService {
diff --git a/src/app/core/index.ts b/src/app/core/index.ts
index ecefda5..623ee96 100644
--- a/src/app/core/index.ts
+++ b/src/app/core/index.ts
@@ -14,6 +14,7 @@
export * from "./api";
export * from "./auth";
export * from "./configuration";
+export * from "./confirm";
export * from "./dom";
export * from "./download";
export * from "./external-routes";
diff --git a/src/app/features/dashboard/components/dashboard/dashboard.component.ts b/src/app/features/dashboard/components/dashboard/dashboard.component.ts
index a45fb13..79dcb14 100644
--- a/src/app/features/dashboard/components/dashboard/dashboard.component.ts
+++ b/src/app/features/dashboard/components/dashboard/dashboard.component.ts
@@ -39,6 +39,12 @@
showSubCaption$?: Observable<boolean>;
}
+/**
+ * This component displayed multiple lists of statements that might be relevant to the logged in users.
+ * Different list for the various user roles are displayed. The goal is to show the most relevant statements for the user.
+ * The lists are sorted by due date to show the statements that need attention.
+ */
+
@Component({
selector: "app-dashboard",
templateUrl: "./dashboard.component.html",
diff --git a/src/app/features/dashboard/pipe/get-dashboard-entries.pipe.ts b/src/app/features/dashboard/pipe/get-dashboard-entries.pipe.ts
index 74ceb3d..46f6f76 100644
--- a/src/app/features/dashboard/pipe/get-dashboard-entries.pipe.ts
+++ b/src/app/features/dashboard/pipe/get-dashboard-entries.pipe.ts
@@ -16,6 +16,12 @@
import {IStatementEntityWithTasks} from "../../../store/statements/model";
import {arrayJoin} from "../../../util/store";
+
+/**
+ * This function converts a list of statement entities to a list of statement table entries.
+ * Which means the additional properties needed to display the statements in the dashboard lists are added to the list elements.
+ */
+
@Pipe({name: "getDashboardEntries"})
export class GetDashboardEntriesPipe implements PipeTransform {
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
index 2b66156..0c0fef5 100644
--- a/src/app/features/details/components/attachments/statement-details-attachments.component.html
+++ b/src/app/features/details/components/attachments/statement-details-attachments.component.html
@@ -61,7 +61,7 @@
(appDownload)="downloadAttachment($event)"
[appAttachments]="statementAttachments"
[appTagList]="tags$ | async"
- [appTitle]="'Manuell hinzugefügte Anhänge'"
+ [appTitle]="'details.attachments.added' | translate"
class="attachments--document--lists---half-size">
</app-attachment-display-list>
</div>
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
index 026f1d9..da72d14 100644
--- a/src/app/features/details/components/attachments/statement-details-attachments.component.ts
+++ b/src/app/features/details/components/attachments/statement-details-attachments.component.ts
@@ -67,6 +67,10 @@
public constructor(public store: Store) {
}
+ /**
+ * Subscribing to the attachment observables to apply the filtering for tags when changes to the statement attachments happen.
+ */
+
public ngOnInit() {
combineLatest([this.allAttachments$, this.tags$]).pipe(
takeUntil(this.destroy$)
@@ -121,6 +125,10 @@
this.store.dispatch(startAttachmentDownloadAction({statementId, attachmentId}));
}
+ /**
+ * Filters the input list of attachments to only return the ones that have the isSelected property set to true.
+ */
+
public filterBySelectedTags(attachments: IAttachmentControlValue[]) {
return attachments.filter((attachment) => {
const selectedTags = this.availableTags?.filter((_) => _.isSelected);
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
index 5ecd60c..270c495 100644
--- a/src/app/features/details/components/considerations/statement-details-considerations.component.html
+++ b/src/app/features/details/components/considerations/statement-details-considerations.component.html
@@ -33,7 +33,10 @@
[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>
+ style="margin-left: 0.25em;">
+ <mat-icon class="attachments--select-file-button--icon">publish</mat-icon>
+ {{'details.considerations.upload' | translate}}
+ </button>
</app-attachment-file-drop-form>
</div>
diff --git a/src/app/features/details/components/considerations/statement-details-considerations.component.scss b/src/app/features/details/components/considerations/statement-details-considerations.component.scss
index 6bc3cc8..caaac5e 100644
--- a/src/app/features/details/components/considerations/statement-details-considerations.component.scss
+++ b/src/app/features/details/components/considerations/statement-details-considerations.component.scss
@@ -42,3 +42,9 @@
padding: 1em;
font-style: italic;
}
+
+.attachments--select-file-button--icon {
+ height: initial;
+ width: initial;
+ font-size: 1em;
+}
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
index a79cd2e..9ddb07e 100644
--- a/src/app/features/details/components/considerations/statement-details-considerations.component.ts
+++ b/src/app/features/details/components/considerations/statement-details-considerations.component.ts
@@ -25,6 +25,11 @@
import {createFormGroup} from "../../../../util/forms";
import {AbstractReactiveFormComponent} from "../../../forms/abstract";
+/**
+ * This component displays the list of files saved as consideration result to the statement.
+ * For the role of official in charge files can be uploaded as consideration via the file drop component.
+ */
+
@Component({
selector: "app-statement-details-considerations",
templateUrl: "./statement-details-considerations.component.html",
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
index 928dde4..13b13bc 100644
--- 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
@@ -13,7 +13,7 @@
<app-collapsible
[(appCollapsed)]="appCollapsed"
- [appTitle]="'Geographische Position'">
+ [appTitle]="'details.geoPositions.title' | translate">
<div *ngIf="(geographicPosition$ | async) != null"
class="map">
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
index 9be77b7..6993752 100644
--- 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
@@ -14,9 +14,9 @@
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 {ILeafletBounds} from "../../../map/directives/leaflet";
import {StatementDetailsModule} from "../../statement-details.module";
import {StatementDetailsGeographicPositionComponent} from "./statement-details-geographic-position.component";
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
index d249aaa..ccb1861 100644
--- 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
@@ -14,8 +14,8 @@
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";
+import {ILeafletBounds} from "../../../map";
/**
* This component displays the statements coordinates on a map using leaflet.
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
index a94d7a7..57e27ac 100644
--- a/src/app/features/details/components/information/statement-details-information.component.html
+++ b/src/app/features/details/components/information/statement-details-information.component.html
@@ -13,7 +13,7 @@
<app-collapsible
[(appCollapsed)]="appCollapsed"
- [appTitle]="'Allgemeine Informationen'">
+ [appTitle]="'details.information.title' | translate">
<div class="information">
diff --git a/src/app/features/details/components/process-information/process-information/process-information.component.ts b/src/app/features/details/components/process-information/process-information/process-information.component.ts
index 15a449a..fd18582 100644
--- a/src/app/features/details/components/process-information/process-information/process-information.component.ts
+++ b/src/app/features/details/components/process-information/process-information/process-information.component.ts
@@ -14,6 +14,11 @@
import {Component, Input} from "@angular/core";
import {IProcessHistoryData} from "../process-history";
+/**
+ * This component displays the process diagram (bpmn) and marks the current state the statement is in.
+ * Also shows the process history as a table.
+ */
+
@Component({
selector: "app-process-information",
templateUrl: "./process-information.component.html",
diff --git a/src/app/features/details/components/side-menu/statement-details-side-menu.component.spec.ts b/src/app/features/details/components/side-menu/statement-details-side-menu.component.spec.ts
index 1a5afd1..bde6e26 100644
--- a/src/app/features/details/components/side-menu/statement-details-side-menu.component.spec.ts
+++ b/src/app/features/details/components/side-menu/statement-details-side-menu.component.spec.ts
@@ -13,6 +13,8 @@
import {SimpleChange} from "@angular/core";
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {TranslateService} from "@ngx-translate/core";
+import {take} from "rxjs/operators";
import {EAPIProcessTaskDefinitionKey, EAPIUserRoles, I18nModule, IAPIProcessTask} from "../../../../core";
import {EErrorCode} from "../../../../store/root/model";
import {StatementDetailsModule} from "../../statement-details.module";
@@ -21,10 +23,13 @@
describe("StatementDetailsSideMenuComponent", () => {
let component: StatementDetailsSideMenuComponent;
let fixture: ComponentFixture<StatementDetailsSideMenuComponent>;
+ let translateService: TranslateService;
beforeEach(async(() => {
TestBed.configureTestingModule({
- imports: [StatementDetailsModule, I18nModule]
+ imports: [
+ StatementDetailsModule, I18nModule
+ ]
}).compileComponents();
}));
@@ -32,6 +37,7 @@
fixture = TestBed.createComponent(StatementDetailsSideMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
+ translateService = TestBed.inject(TranslateService);
});
it("should create", () => {
@@ -80,8 +86,8 @@
expect(component.buttonLayout.length).toEqual(0);
});
- it("should update info message based upon current user name and tasks", () => {
- component.update();
+ it("should update info message based upon current user name and tasks", async () => {
+ await component.update();
expect(component.buttonLayout).toEqual([]);
component.appUserName = "hugo";
@@ -94,11 +100,13 @@
authorized: true,
assignee: "hugo"
}];
- component.update();
+ await component.update();
expect(component.infoMessage).not.toBeDefined();
component.appUserName = "noAssignee";
- component.update();
- expect(component.infoMessage).toEqual(EErrorCode.CLAIMED_BY_OTHER_USER);
+ await component.update();
+ const expectedInfoMessage = await translateService.get(EErrorCode.CLAIMED_BY_OTHER_USER, {user: "hugo"})
+ .pipe(take(1)).toPromise();
+ expect(component.infoMessage).toEqual(expectedInfoMessage);
});
});
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 9988172..501197a 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
@@ -13,6 +13,7 @@
import {Component, EventEmitter, Input, OnChanges, Output, SimpleChanges} from "@angular/core";
import {Action} from "@ngrx/store";
+import {TranslateService} from "@ngx-translate/core";
import {
ALL_NON_TRIVIAL_USER_ROLES,
EAPIProcessTaskDefinitionKey,
@@ -92,7 +93,7 @@
emit: this.emitClaimTaskFactory(),
label: "details.sideMenu.editInfoData",
icon: "subject",
- cssClass: "openk-success"
+ cssClass: "openk-info"
}
],
[EAPIProcessTaskDefinitionKey.CREATE_NEGATIVE_RESPONSE]: [
@@ -103,7 +104,7 @@
),
label: "details.sideMenu.backToInfoData",
icon: "subject",
- cssClass: "openk-info"
+ cssClass: "openk-danger"
},
{
emit: this.emitClaimTaskFactory(),
@@ -148,7 +149,8 @@
{
emit: (task) => this.appDispatch.emit(sendStatementViaMailAction({
statementId: task?.statementId,
- taskId: task?.taskId
+ taskId: task?.taskId,
+ assignee: task?.assignee
})),
label: "details.sideMenu.sendEmail",
icon: "send",
@@ -190,6 +192,11 @@
}
};
+ public constructor(
+ private translateService: TranslateService
+ ) {
+ }
+
public ngOnChanges(changes: SimpleChanges) {
const keys: Array<keyof StatementDetailsSideMenuComponent> = ["appUserRoles", "appTasks", "appUserName"];
if (keys.some((_) => changes[_] != null)) {
@@ -197,7 +204,7 @@
}
}
- public update() {
+ public async update() {
let roles = filterDistinctValues(this.appUserRoles);
roles = ALL_NON_TRIVIAL_USER_ROLES.filter((_) => roles.indexOf(_) > -1);
const tasks = filterDistinctValues(this.appTasks);
@@ -207,13 +214,14 @@
.map((task) => this.getLayoutForRoleAndTask(role, task));
});
this.buttonLayout = arrayJoin(...arrayJoin(...actionsForRoles));
- this.infoMessage = this.getInfoMessage();
+ this.infoMessage = await this.getInfoMessage();
}
private emitClaimAndCompleteFactory(variables: TCompleteTaskVariable, claimNext?: EAPIProcessTaskDefinitionKey) {
return (task: IAPIProcessTask) => this.appDispatch.emit(claimAndCompleteTask({
statementId: task?.statementId,
taskId: task?.taskId,
+ assignee: task?.assignee,
variables,
claimNext
}));
@@ -231,13 +239,12 @@
return userActions == null ? [] : arrayJoin(userActions[task.taskDefinitionKey]).map((_) => ({..._, task}));
}
- private getInfoMessage(): string {
- const isTaskClaimedByOtherUser = arrayJoin(this.appTasks)
- .map((task) => task.assignee)
- .some((assignee) => assignee != null && assignee !== this.appUserName);
+ private async getInfoMessage(): Promise<string> {
+ const assignees = arrayJoin(this.appTasks).map((task) => task.assignee);
+ const userTaskIsClaimedBy = assignees.some((assignee) => assignee != null && assignee !== this.appUserName) ? assignees[0] : null;
- if (isTaskClaimedByOtherUser) {
- return EErrorCode.CLAIMED_BY_OTHER_USER;
+ if (userTaskIsClaimedBy) {
+ return this.translateService.get(EErrorCode.CLAIMED_BY_OTHER_USER, {user: userTaskIsClaimedBy}).toPromise();
}
return;
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 00a2a34..64f57aa 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
@@ -64,7 +64,7 @@
<app-collapsible
[appCollapsed]="false"
- [appTitle]="'Prozessinformationen'"
+ [appTitle]="'details.processInformation.title' | translate"
class="statement-details">
<app-process-information
[appCurrentActivities]="processCurrentActivityIds$ | async"
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 b43fe9c..693c8e6 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
@@ -38,6 +38,12 @@
userRolesSelector
} from "../../../../store";
+
+/**
+ * This component displays all information saved for the selected statement.
+ * Data cannot be edited. Only comments can be added to the statement.
+ */
+
@Component({
selector: "app-statement-details",
templateUrl: "./statement-details.component.html",
diff --git a/src/app/features/details/directives/bpmn-directive.ts b/src/app/features/details/directives/bpmn-directive.ts
index 7c11c98..e3be829 100644
--- a/src/app/features/details/directives/bpmn-directive.ts
+++ b/src/app/features/details/directives/bpmn-directive.ts
@@ -14,6 +14,11 @@
import {Directive, ElementRef, Input, NgZone, OnDestroy} from "@angular/core";
import * as BpmnJS from "bpmn-js/dist/bpmn-navigated-viewer.production.min.js";
+
+/**
+ * This directive takes a bpmn diagram as xml input and attaches it to the dom node.
+ */
+
@Directive({
selector: "[appBpmn]"
})
diff --git a/src/app/features/details/pipes/get-process-history-entries.pipe.spec.ts b/src/app/features/details/pipes/get-process-history-entries.pipe.spec.ts
new file mode 100644
index 0000000..9ae280b
--- /dev/null
+++ b/src/app/features/details/pipes/get-process-history-entries.pipe.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 {IAPIProcessActivity, IAPIStatementHistory} from "../../../core/api/process";
+import {GetProcessHistoryEntriesPipe} from "./get-process-history-entries.pipe";
+
+describe("GetProcessHistoryEntriesPipe", () => {
+
+ const pipe = new GetProcessHistoryEntriesPipe();
+
+ describe("transform", () => {
+
+ const history: IAPIStatementHistory = {
+ processName: "processName",
+ processVersion: 0,
+ finishedProcessActivities: [
+ {
+ activityName: "activityName",
+ activityType: "userTask",
+ assignee: "assignee",
+ endTime: "endTime",
+ canceled: false
+ } as IAPIProcessActivity
+ ],
+ currentProcessActivities: [
+ {
+ activityName: "activityName1",
+ activityType: "serviceTask",
+ assignee: "assignee1",
+ endTime: "endTime1",
+ canceled: true
+ } as IAPIProcessActivity
+ ]
+ };
+
+ it("should return empty array for invalid input", () => {
+ let result = pipe.transform(null);
+ expect(result).toEqual([]);
+ result = pipe.transform(undefined);
+ expect(result).toEqual([]);
+ });
+
+ it("should combine active and finished activities and returns a list of all user and service task as process history data", () => {
+ const result = pipe.transform(history);
+ expect(result).toEqual([
+ {
+ icon: "account_circle",
+ activityName: "activityName",
+ assignee: "assignee",
+ endTime: "endTime",
+ cancelled: false
+ },
+ {
+ icon: "group_work",
+ activityName: "activityName1",
+ assignee: "assignee1",
+ endTime: "endTime1",
+ cancelled: true
+ }
+ ]);
+ });
+ });
+});
+
diff --git a/src/app/features/details/pipes/get-process-history-entries.pipe.ts b/src/app/features/details/pipes/get-process-history-entries.pipe.ts
index c35f0cc..6a9ff64 100644
--- a/src/app/features/details/pipes/get-process-history-entries.pipe.ts
+++ b/src/app/features/details/pipes/get-process-history-entries.pipe.ts
@@ -16,6 +16,11 @@
import {arrayJoin} from "../../../util/store";
import {IProcessHistoryData} from "../components/process-information/process-history";
+
+/**
+ * From the process history data combines finished and ongoing tasks, then filters for only user and service tasks.
+ */
+
@Pipe({name: "getProcessHistoryEntries"})
export class GetProcessHistoryEntriesPipe implements PipeTransform {
diff --git a/src/app/features/details/statement-details.module.ts b/src/app/features/details/statement-details.module.ts
index f370e1b..1b726a7 100644
--- a/src/app/features/details/statement-details.module.ts
+++ b/src/app/features/details/statement-details.module.ts
@@ -25,11 +25,11 @@
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 {MapModule} from "../map";
import {
ProcessDiagramComponent,
ProcessHistoryComponent,
@@ -64,7 +64,7 @@
CommentsFormModule,
SideMenuModule,
ActionButtonModule,
- LeafletModule,
+ MapModule,
AttachmentsFormModule,
StatementInformationFormModule,
SelectModule,
diff --git a/src/app/features/edit/components/edit-portal/statement-edit-portal.component.ts b/src/app/features/edit/components/edit-portal/statement-edit-portal.component.ts
index 64d4f56..95f9969 100644
--- a/src/app/features/edit/components/edit-portal/statement-edit-portal.component.ts
+++ b/src/app/features/edit/components/edit-portal/statement-edit-portal.component.ts
@@ -29,6 +29,11 @@
import {EStatementEditSites} from "../../model";
import {statementEditSiteSelector} from "../../selectors";
+/**
+ * This component displays the corresponding edit page to the selected task. The task is selected via task id in the url parameters.
+ * The task definition key determines the edit page to display.
+ */
+
@Component({
selector: "app-statement-edit-portal",
templateUrl: "./statement-edit-portal.component.html",
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 5508298..f534592 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
@@ -36,6 +36,12 @@
import {AbstractReactiveFormComponent} from "../../abstract";
import {getMailAttachment, getMailAttachments} from "../util/mail-attachments.util";
+/**
+ * This form component shows the list of statement attachments. The attachments are split into two categories.
+ * Mail attachments are displayed separately from the normal attachments.
+ * New attachments can be uploaded via the file drop component. Displayed attachments can be selected and downloaded.
+ */
+
@Component({
selector: "app-attachments-form-group",
templateUrl: "./attachments-form-group.component.html",
@@ -102,6 +108,9 @@
this.attachments$.pipe(delay(0), takeUntil(this.destroy$))
.subscribe((values) => this.setValueForArray(values, "edit"));
+ /**
+ * Reacts to changes on email and attachment data and sets the list of email attachments accordingly.
+ */
combineLatest([this.selectedMail$, this.statementMail$, this.allAttachments$]).pipe(
filter(([_, m, a]) => (_ != null || m != null) && a != null),
delay(0),
@@ -112,7 +121,6 @@
this.setMailAttachmentValues(attachments, arrayJoin(this.mail?.attachments));
});
-
this.fileCache$.pipe(delay(0), takeUntil(this.destroy$))
.subscribe((values) => this.setValueForArray(values, "add"));
this.store.dispatch(fetchAttachmentTagsAction());
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
index 28beec6..50f6505 100644
--- 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
@@ -21,6 +21,11 @@
})
export class GetEmailTextAttachmentPipe implements PipeTransform {
+
+ /**
+ * Given the mail data and attachments list, gets the mail text attachment and replaces the name with the mail subject.
+ */
+
public transform(attachments: IAPIAttachmentModel[], mail: IAPIEmailModel): IAPIAttachmentModel {
const mailTextAttachment = getMailAttachment(attachments);
diff --git a/src/app/features/forms/comments/components/comments-form/comments-form.component.spec.ts b/src/app/features/forms/comments/components/comments-form/comments-form.component.spec.ts
index 07b491b..e74c432 100644
--- a/src/app/features/forms/comments/components/comments-form/comments-form.component.spec.ts
+++ b/src/app/features/forms/comments/components/comments-form/comments-form.component.spec.ts
@@ -15,10 +15,8 @@
import {Store} from "@ngrx/store";
import {provideMockStore} from "@ngrx/store/testing";
import {take} from "rxjs/operators";
-import {I18nModule} from "../../../../../core/i18n";
-import {queryParamsIdSelector} from "../../../../../store/root/selectors";
-import {addCommentAction, deleteCommentAction} from "../../../../../store/statements/actions";
-import {statementCommentsSelector} from "../../../../../store/statements/selectors";
+import {ConfirmService, I18nModule} from "../../../../../core";
+import {addCommentAction, deleteCommentAction, queryParamsIdSelector, statementCommentsSelector} from "../../../../../store";
import {CommentsFormModule} from "../../comments-form.module";
import {CommentsFormComponent} from "./comments-form.component";
@@ -46,7 +44,8 @@
value: [{id: 1919} as any]
}
]
- })
+ }),
+ {provide: ConfirmService, useValue: {askForConfirmation: (message: string) => true}}
]
}).compileComponents();
}));
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 af647cd..6449e42 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
@@ -13,12 +13,19 @@
import {Component, Input} from "@angular/core";
import {select, Store} from "@ngrx/store";
+import {TranslateService} from "@ngx-translate/core";
import {defer} from "rxjs";
import {map, take} from "rxjs/operators";
-import {queryParamsIdSelector} from "../../../../../store/root/selectors";
-import {addCommentAction, deleteCommentAction} from "../../../../../store/statements/actions";
-import {statementCommentsSelector} from "../../../../../store/statements/selectors";
-import {arrayJoin} from "../../../../../util/store";
+import {ConfirmService} from "../../../../../core";
+import {addCommentAction, deleteCommentAction, queryParamsIdSelector, statementCommentsSelector} from "../../../../../store";
+import {arrayJoin} from "../../../../../util";
+
+
+/**
+ * This component displays all saved comments to the statement.
+ * Also makes it possible to input text for a new comment in a text field. New comment can be added to the statement.
+ * Comments can be deleted by the user that created the comment.
+ */
@Component({
selector: "app-comments-form",
@@ -41,7 +48,10 @@
map((comments) => arrayJoin(comments).length)
);
- public constructor(public store: Store) {
+ public constructor(
+ public store: Store,
+ public confirmService: ConfirmService,
+ private translationService: TranslateService) {
}
@@ -51,8 +61,11 @@
}
public async deleteComment(commentId: number) {
- const statementId = await this.statementId$.pipe(take(1)).toPromise();
- this.store.dispatch(deleteCommentAction({statementId, commentId}));
+ const confirmationText = await this.translationService.get("comments.confirmDelete").toPromise();
+ if (this.confirmService.askForConfirmation(confirmationText)) {
+ const statementId = await this.statementId$.pipe(take(1)).toPromise();
+ this.store.dispatch(deleteCommentAction({statementId, commentId}));
+ }
}
}
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 fa2ac29..a19e25c 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
@@ -44,25 +44,34 @@
class="statement-details">
</app-statement-details-attachments>
-<app-collapsible
- [appTitle]="('statementEditorForm.container.contributionStatus' | translate)
+<ng-container *ngIf="(showContributions$ | async) === true">
+
+ <app-collapsible
+ *ngIf="(showContributionsControl$ | async) === true"
+ [appTitle]="('statementEditorForm.container.contributionStatus' | translate)
+ ' (' + (selectedContributionsCount$ | async) + '/' + (requiredContributionOptions$ | async)?.length + ')'"
- [formGroup]="appFormGroup">
+ [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>
+ <app-select-group
+ *ngIf="(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>
+ <div *ngIf="!((requiredContributionOptions$ | async)?.length > 0)"
+ class="placeholder">
+ {{"details.contributions.placeholder" | translate}}
+ </div>
+ </app-collapsible>
-</app-collapsible>
+ <app-statement-details-contributions
+ *ngIf="(showContributionsControl$ | async) === false"
+ class="statement-details">
+ </app-statement-details-contributions>
+
+</ng-container>
<app-collapsible
[appHeaderTemplateRef]="arrangementHeaderRef"
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 29e28d3..dc6bcf8 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
@@ -17,6 +17,7 @@
display: flex;
flex-flow: column;
max-width: 70em;
+ width: 100%;
margin: 0 auto;
& > * {
@@ -64,3 +65,8 @@
height: initial;
font-size: 1em;
}
+
+.statement-details {
+ width: 100%;
+ max-width: 70em;
+}
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 683b3b7..8701177 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
@@ -52,6 +52,14 @@
import {arrayJoin, filterDistinctValues} from "../../../../../util";
import {AbstractReactiveFormComponent} from "../../../abstract";
+/**
+ * This component displays information about the statement.
+ * Also gives the possibility to upload attachments to send as response. (outbox attachments)
+ * The component is shown for different task states.
+ * For the official in charge, when creating or checking the response, the current state of contributions is also shown and can be edited.
+ * The text content for the response can be edited by placing and filling the text blocks.
+ */
+
@Component({
selector: "app-statement-editor-form",
templateUrl: "./statement-editor-form.component.html",
@@ -78,6 +86,13 @@
public showContributions$ = defer(() => this.task$).pipe(
map((task) => task?.taskDefinitionKey),
map((taskDefinitionKey) => {
+ return taskDefinitionKey !== EAPIProcessTaskDefinitionKey.CREATE_NEGATIVE_RESPONSE;
+ })
+ );
+
+ public showContributionsControl$ = defer(() => this.task$).pipe(
+ map((task) => task?.taskDefinitionKey),
+ map((taskDefinitionKey) => {
return taskDefinitionKey === EAPIProcessTaskDefinitionKey.CREATE_DRAFT
|| taskDefinitionKey === EAPIProcessTaskDefinitionKey.CHECK_AND_FORMULATE_RESPONSE;
})
@@ -181,7 +196,7 @@
taskId: task.taskId,
value: {
...value,
- contributions: await this.showContributions$.pipe(take(1)).toPromise() ? value.contributions : null
+ contributions: await this.showContributionsControl$.pipe(take(1)).toPromise() ? value.contributions : null
},
options
}));
diff --git a/src/app/features/forms/statement-editor/components/text-block-control/text-block-control.component.html b/src/app/features/forms/statement-editor/components/text-block-control/text-block-control.component.html
index c10e5c2..72d726e 100644
--- a/src/app/features/forms/statement-editor/components/text-block-control/text-block-control.component.html
+++ b/src/app/features/forms/statement-editor/components/text-block-control/text-block-control.component.html
@@ -10,10 +10,11 @@
*
* SPDX-License-Identifier: EPL-2.0
-------------------------------------------------------------------------------->
+
<app-text-block
- (appButtonPress)="appRemove.emit(appValue.textblockId)"
+ (appDelete)="appRemove.emit(appValue.textblockId)"
(appNewLine)="addNewLine()"
- (appChangeText)="setReplacement($event)"
+ (appTextChange)="setReplacement($event)"
(appTextInput)="convertToFreeText()"
(appValueChange)="addPlaceholder($event)"
[appTextBlockData]="appValue | getBlockDataFromArrangement: appTextBlockModel: appValue?.placeholderValues: appReplacements: appSelects"
@@ -21,6 +22,7 @@
[appShowClose]="true"
[appTitle]="appValue | getTitle"
[appBlockText]="appValue?.replacement"
+ [appWithoutTitlePrefix]="appValue?.type !== 'block'"
[appDisabled]="appDisabled"
[appType]="appValue?.type"
[class.disable]="appDisabled">
diff --git a/src/app/features/forms/statement-editor/pipes/arrangement-to-preview.pipe.ts b/src/app/features/forms/statement-editor/pipes/arrangement-to-preview.pipe.ts
index ad230af..a93ef9c 100644
--- a/src/app/features/forms/statement-editor/pipes/arrangement-to-preview.pipe.ts
+++ b/src/app/features/forms/statement-editor/pipes/arrangement-to-preview.pipe.ts
@@ -17,6 +17,11 @@
import {IStatementEditorControlConfiguration} from "../../../../store/statements/model";
import {arrayJoin} from "../../../../util/store";
+/**
+ * Converts arrangement configuration to text. Integrates the current values set for the select/input/date replacements into the textblocks
+ * texts to then be able to display them as text.
+ */
+
@Pipe({
name: "arrangementToPreview"
})
diff --git a/src/app/features/forms/statement-editor/pipes/error-to-messages.pipe.ts b/src/app/features/forms/statement-editor/pipes/error-to-messages.pipe.ts
index 42bddf8..77db6cc 100644
--- a/src/app/features/forms/statement-editor/pipes/error-to-messages.pipe.ts
+++ b/src/app/features/forms/statement-editor/pipes/error-to-messages.pipe.ts
@@ -15,6 +15,10 @@
import {IAPITextArrangementErrorModel} from "../../../../core/api/text";
import {arrayJoin} from "../../../../util/store";
+/**
+ * For the given error model returns the error message for the most important error.
+ */
+
@Pipe({
name: "errorToMessages"
})
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 cb7e40d..1dba1cc 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
@@ -47,6 +47,14 @@
import {ExtractMailAddressPipe} from "../../../../mail/pipes/extract-mail-address.pipe";
import {AbstractReactiveFormComponent} from "../../../abstract";
+/**
+ * This component shows the basic statement information. The data can be edited. A contact can be selected
+ * and previous statements can be linked to the current one.
+ * This page is also used for the creation of a new statement. If this page is opened with a valid mailid in the url parameters,
+ * values for title, dates and also contact will be prefilled.
+ * All mandatory values have to be set else an error will be displayed on submitting.
+ */
+
@Component({
selector: "app-statement-information-form",
templateUrl: "./statement-information-form.component.html",
@@ -132,6 +140,10 @@
this.clearErrors(true);
await this.setInitialValue();
this.store.dispatch(fetchSettingsAction());
+
+ /**
+ * If a new statement is to be created and a mailid is set, prefill values to values from the email. (subject, receipt date)
+ */
if (this.mailId) {
await this.setEmailValues(mailId);
this.appFormGroup.markAllAsTouched();
@@ -229,6 +241,9 @@
const task = await this.task$.pipe(take(1)).toPromise();
this.store.dispatch(fetchEmailAction({mailId, statementId: this.appForNewStatement ? "new" : task?.statementId}));
+ /**
+ * As soon as the mail data has been fetched, set the form values.
+ */
combineLatest([this.selectedMail$, this.statementMail$]).pipe(
filter(([mail, statementMail]) => mail != null || statementMail != null),
take(1),
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 4ba7481..9a10f52 100644
--- a/src/app/features/forms/statement-information/pipes/sector.pipe.ts
+++ b/src/app/features/forms/statement-information/pipes/sector.pipe.ts
@@ -14,6 +14,10 @@
import {Pipe, PipeTransform} from "@angular/core";
import {IAPISectorsModel} from "../../../../core/api/statements/IAPISectorsModel";
+/**
+ * For given city and district, returns the available sectors.
+ */
+
@Pipe({
name: "sector"
})
diff --git a/src/app/features/forms/workflow-data/components/side-menu/workflow-data-side-menu.component.html b/src/app/features/forms/workflow-data/components/side-menu/workflow-data-side-menu.component.html
index 4d70c87..c79314f 100644
--- a/src/app/features/forms/workflow-data/components/side-menu/workflow-data-side-menu.component.html
+++ b/src/app/features/forms/workflow-data/components/side-menu/workflow-data-side-menu.component.html
@@ -34,8 +34,8 @@
<app-side-menu-status
*appSideMenu="'center'"
- [appLoadingMessage]="'core.submitting' | translate"
[appErrorMessage]="appErrorMessage"
+ [appLoadingMessage]="'core.submitting' | translate"
[appLoading]="appLoading">
</app-side-menu-status>
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 d7c7f3b..cef9f29 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
@@ -14,8 +14,8 @@
<app-workflow-data-side-menu
(appSubmit)="submit($event)"
[appDisabled]="appFormGroup.disabled"
- [appLoading]="isStatementLoading$ | async"
[appErrorMessage]="(appErrorMessage$ | async)?.errorMessage"
+ [appLoading]="isStatementLoading$ | async"
[appStatementId]="(task$ | async)?.statementId">
</app-workflow-data-side-menu>
@@ -39,7 +39,7 @@
<app-map-select
(appOpenGis)="openGis($event)"
- [appCenter]="'leaflet.defaultView' | translate"
+ [appCenter]="geographicPosition$ | async"
[formControlName]="'geographicPosition'"
class="geographic-position">
</app-map-select>
@@ -51,9 +51,9 @@
<app-select-group
[appGroups]="departmentGroups$ | async"
+ [appIndeterminate]="true"
[appOptions]="departmentOptions$ | async"
[formControlName]="'departments'"
- [appIndeterminate]="true"
class="departments">
</app-select-group>
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 cedcdd3..0c70992 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,7 +16,6 @@
import {RouterTestingModule} from "@angular/router/testing";
import {MockStore, provideMockStore} from "@ngrx/store/testing";
import {I18nModule, IAPISearchOptions} from "../../../../core";
-import {ILeafletBounds} from "../../../../shared/leaflet";
import {
IWorkflowFormValue,
openGisAction,
@@ -25,6 +24,7 @@
taskSelector,
userNameSelector
} from "../../../../store";
+import {ILeafletBounds} from "../../../map";
import {WorkflowDataFormModule} from "../workflow-data-form.module";
import {WorkflowDataFormComponent} from "./workflow-data-form.component";
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 e0b4122..9523676 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
@@ -16,7 +16,6 @@
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,
@@ -31,6 +30,7 @@
queryParamsIdSelector,
setErrorAction,
startStatementSearchAction,
+ statementGeographicPositionSelector,
statementLoadingSelector,
statementSelector,
statementTypesSelector,
@@ -39,8 +39,14 @@
userNameSelector,
workflowFormValueSelector
} from "../../../../store";
+import {ILeafletBounds, latLngZoomToString} from "../../../map";
import {AbstractReactiveFormComponent} from "../../abstract";
+/**
+ * This component displays all the workflow information for the statement. (departments/geo-coordinates/linked statements)
+ * The coordinates can be set by selecting a point on a integrated map. Previous statements can be selected from a list to link them
+ * together. Departments from whom input is needed, can be made mandatory or optional.
+ */
@Component({
selector: "app-workflow-data-form",
templateUrl: "./workflow-data-form.component.html",
@@ -66,15 +72,17 @@
public appErrorMessage$: Observable<IStatementErrorEntity> = this.store.pipe(select(getStatementErrorSelector));
- private form$ = this.store.pipe(select(workflowFormValueSelector));
-
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 defaultGeographicPosition = latLngZoomToString(this.configuration.leaflet, this.configuration.leaflet.zoom);
+
+ public geographicPosition$ = this.store.pipe(select(statementGeographicPositionSelector));
+
+ private form$ = this.store.pipe(select(workflowFormValueSelector));
public constructor(public store: Store, @Inject(APP_CONFIGURATION) public configuration: IAppConfiguration) {
super();
@@ -134,12 +142,9 @@
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,
- geographicPosition: value.geographicPosition == null ? this.defaultGeographicPosition : value.geographicPosition,
- });
+ const geographicPosition = value.geographicPosition == null ? this.defaultGeographicPosition : value.geographicPosition;
+ this.patchValue({...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 b03fc0e..bb805db 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
@@ -16,13 +16,13 @@
import {ReactiveFormsModule} from "@angular/forms";
import {MatIconModule} from "@angular/material/icon";
import {TranslateModule} from "@ngx-translate/core";
-import {MapSelectModule} from "../../../shared/controls/map-select/map-select.module";
import {SelectModule} from "../../../shared/controls/select";
import {StatementSelectModule} from "../../../shared/controls/statement-select";
import {ActionButtonModule} from "../../../shared/layout/action-button";
import {CollapsibleModule} from "../../../shared/layout/collapsible";
import {SideMenuModule} from "../../../shared/layout/side-menu";
import {StatementDetailsModule} from "../../details";
+import {MapModule} from "../../map";
import {WorkflowDataFormComponent, WorkflowDataSideMenuComponent} from "./components";
@NgModule({
@@ -37,7 +37,7 @@
StatementSelectModule,
SideMenuModule,
ActionButtonModule,
- MapSelectModule,
+ MapModule,
StatementDetailsModule
],
declarations: [
diff --git a/src/app/features/mail/components/mail-inbox/mail-inbox.component.html b/src/app/features/mail/components/mail-inbox/mail-inbox.component.html
index 54fad24..a413595 100644
--- a/src/app/features/mail/components/mail-inbox/mail-inbox.component.html
+++ b/src/app/features/mail/components/mail-inbox/mail-inbox.component.html
@@ -46,7 +46,7 @@
{{"mails.from" | translate}}
</span>
<div class="email-inbox--list--element--sender--column">
- <span *ngFor="let contact of (item.from | appSenderSplitNameMail)"
+ <span *ngFor="let contact of (item.from | appDivideSenderAndMail)"
class="email-inbox--list--element--sender--text">
{{contact}}
</span>
diff --git a/src/app/features/mail/components/mail/mail.component.spec.ts b/src/app/features/mail/components/mail/mail.component.spec.ts
index 377a3cb..61f9d6c 100644
--- a/src/app/features/mail/components/mail/mail.component.spec.ts
+++ b/src/app/features/mail/components/mail/mail.component.spec.ts
@@ -14,6 +14,7 @@
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";
import {MailModule} from "../../mail.module";
import {MailComponent} from "./mail.component";
@@ -26,7 +27,8 @@
TestBed.configureTestingModule({
imports: [
MailModule,
- RouterTestingModule
+ RouterTestingModule,
+ I18nModule
],
providers: [
provideMockStore()
diff --git a/src/app/features/mail/components/mail/mail.component.ts b/src/app/features/mail/components/mail/mail.component.ts
index 714793f..075da21 100644
--- a/src/app/features/mail/components/mail/mail.component.ts
+++ b/src/app/features/mail/components/mail/mail.component.ts
@@ -14,16 +14,27 @@
import {Component, OnDestroy, OnInit} from "@angular/core";
import {Router} from "@angular/router";
import {select, Store} from "@ngrx/store";
+import {TranslateService} from "@ngx-translate/core";
import {combineLatest, Subject} from "rxjs";
import {distinctUntilChanged, filter, map, takeUntil, withLatestFrom} from "rxjs/operators";
+import {ConfirmService} from "../../../../core";
import {
deleteEmailFromInboxAction,
downloadEmailAttachmentAction,
fetchEmailAction,
- fetchEmailInboxAction
-} from "../../../../store/mail/actions";
-import {getEmailInboxSelector, getEmailLoadingSelector, getSelectedEmailSelector} from "../../../../store/mail/selectors";
-import {queryParamsMailIdSelector} from "../../../../store/root/selectors";
+ fetchEmailInboxAction,
+ getEmailInboxSelector,
+ getEmailLoadingSelector,
+ getSelectedEmailSelector,
+ queryParamsMailIdSelector
+} from "../../../../store";
+
+/**
+ * This component displays the list of mails in the inbox.
+ * Upon loading the inbox the first time, the first mail is automatically selected.
+ * For the selected mail the mail details are displayed. That includes mail text, mail attachments, subject and sender information.
+ * The mails can be deleted. Attachments can be downloaded.
+ */
@Component({
selector: "app-mail",
@@ -44,7 +55,9 @@
public constructor(
public store: Store,
- public readonly router: Router
+ public readonly router: Router,
+ private confirmService: ConfirmService,
+ private translationService: TranslateService
) {
}
@@ -86,8 +99,11 @@
this.store.dispatch(downloadEmailAttachmentAction({mailId, name}));
}
- public remove(mailId: string) {
- this.store.dispatch(deleteEmailFromInboxAction({mailId}));
+ public async remove(mailId: string) {
+ const confirmationText = await this.translationService.get("mails.confirmDelete").toPromise();
+ if (this.confirmService.askForConfirmation(confirmationText)) {
+ this.store.dispatch(deleteEmailFromInboxAction({mailId}));
+ }
}
public selectMail(mailId: string) {
diff --git a/src/app/features/mail/mail.module.ts b/src/app/features/mail/mail.module.ts
index d03b4bc..56a65da 100644
--- a/src/app/features/mail/mail.module.ts
+++ b/src/app/features/mail/mail.module.ts
@@ -23,9 +23,9 @@
import {SharedPipesModule} from "../../shared/pipes";
import {ProgressSpinnerModule} from "../../shared/progress-spinner";
import {MailComponent, MailDetailsComponent, MailInboxComponent} from "./components";
+import {DivideSenderAndMailPipe} from "./pipes/divide-sender-and-mail.pipe";
import {EmailTextToArrayPipe} from "./pipes/email-text.pipe";
import {ExtractMailAddressPipe} from "./pipes/extract-mail-address.pipe";
-import {SenderSplitNameMailPipe} from "./pipes/sender-split-name-mail.pipe";
@NgModule({
imports: [
@@ -45,7 +45,7 @@
MailInboxComponent,
MailDetailsComponent,
EmailTextToArrayPipe,
- SenderSplitNameMailPipe,
+ DivideSenderAndMailPipe,
ExtractMailAddressPipe
],
exports: [
diff --git a/src/app/features/mail/pipes/sender-split-name-mail.pipe.spec.ts b/src/app/features/mail/pipes/divide-sender-and-mail.pipe.spec.ts
similarity index 91%
rename from src/app/features/mail/pipes/sender-split-name-mail.pipe.spec.ts
rename to src/app/features/mail/pipes/divide-sender-and-mail.pipe.spec.ts
index e40668b..cd9eb95 100644
--- a/src/app/features/mail/pipes/sender-split-name-mail.pipe.spec.ts
+++ b/src/app/features/mail/pipes/divide-sender-and-mail.pipe.spec.ts
@@ -11,12 +11,11 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+import {DivideSenderAndMailPipe} from "./divide-sender-and-mail.pipe";
-import {SenderSplitNameMailPipe} from "./sender-split-name-mail.pipe";
+describe("DivideSenderAndMailPipe", () => {
-describe("SenderSplitNameMailPipe", () => {
-
- const pipe = new SenderSplitNameMailPipe();
+ const pipe = new DivideSenderAndMailPipe();
describe("transform", () => {
diff --git a/src/app/features/mail/pipes/sender-split-name-mail.pipe.ts b/src/app/features/mail/pipes/divide-sender-and-mail.pipe.ts
similarity index 76%
rename from src/app/features/mail/pipes/sender-split-name-mail.pipe.ts
rename to src/app/features/mail/pipes/divide-sender-and-mail.pipe.ts
index 3cfb33d..97b67b7 100644
--- a/src/app/features/mail/pipes/sender-split-name-mail.pipe.ts
+++ b/src/app/features/mail/pipes/divide-sender-and-mail.pipe.ts
@@ -14,14 +14,19 @@
import {Pipe, PipeTransform} from "@angular/core";
import {arrayJoin} from "../../../util/store";
+/**
+ * From given string with format: <name e@mail.com>
+ * removes the brackets and returns name and email as separate strings in an array:
+ * ["name", "e@mail.com"]
+ */
+
@Pipe({
- name: "appSenderSplitNameMail"
+ name: "appDivideSenderAndMail"
})
-export class SenderSplitNameMailPipe implements PipeTransform {
+export class DivideSenderAndMailPipe implements PipeTransform {
public transform(text: string): Array<string> {
const textAsArray = arrayJoin(text?.split(/(?=<)/g));
return [textAsArray[0], textAsArray.slice(1).join("")].filter(x => x);
- // return text.split(/(?=<)(.+)/);
}
}
diff --git a/src/app/features/mail/pipes/email-text.pipe.ts b/src/app/features/mail/pipes/email-text.pipe.ts
index f96f557..718dc80 100644
--- a/src/app/features/mail/pipes/email-text.pipe.ts
+++ b/src/app/features/mail/pipes/email-text.pipe.ts
@@ -13,6 +13,10 @@
import {Pipe, PipeTransform} from "@angular/core";
+/**
+ * Removes all new lines (\n( from the input string and returns an array split by new lines.
+ */
+
@Pipe({
name: "appEmailTextToArray"
})
diff --git a/src/app/features/mail/pipes/extract-mail-address.pipe.ts b/src/app/features/mail/pipes/extract-mail-address.pipe.ts
index 07031e0..af5ebe4 100644
--- a/src/app/features/mail/pipes/extract-mail-address.pipe.ts
+++ b/src/app/features/mail/pipes/extract-mail-address.pipe.ts
@@ -14,6 +14,11 @@
import {Pipe, PipeTransform} from "@angular/core";
import {arrayJoin} from "../../../util/store";
+/**
+ * From given string with format: <name e@mail.com>
+ * returns only the email: "e@mail.com"
+ */
+
@Pipe({
name: "appExtractMailAddress"
})
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/map/components/index.ts
similarity index 90%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/map/components/index.ts
index 990bb42..7407c30 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/map/components/index.ts
@@ -11,4 +11,5 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./map-select";
+export * from "./leaflet";
diff --git a/src/app/shared/leaflet/components/index.ts b/src/app/features/map/components/leaflet/index.ts
similarity index 100%
rename from src/app/shared/leaflet/components/index.ts
rename to src/app/features/map/components/leaflet/index.ts
diff --git a/src/app/shared/leaflet/components/leaflet-map.component.html b/src/app/features/map/components/leaflet/leaflet-map.component.html
similarity index 65%
rename from src/app/shared/leaflet/components/leaflet-map.component.html
rename to src/app/features/map/components/leaflet/leaflet-map.component.html
index d71960e..c06e19e 100644
--- a/src/app/shared/leaflet/components/leaflet-map.component.html
+++ b/src/app/features/map/components/leaflet/leaflet-map.component.html
@@ -13,7 +13,7 @@
<div class="map">
<div #appLeaflet="appLeaflet"
- (appClick)="appClick.emit($event)"
+ (appClick)="appClick.emit($event )"
(appPopupClose)="appPopupClose.emit($event)"
(appLatLngZoomChange)="appCenterChange.emit($event)"
[appCenter]="appCenter | stringToLatLngZoom"
@@ -25,6 +25,22 @@
</div>
+ <div class="map--search">
+ <input #inputElement
+ (keydown.enter)="search(inputElement.value);"
+ [placeholder]="'search.placeHolder' | translate"
+ class="openk-input map--search--input"/>
+
+ <button (click)="search(inputElement.value);"
+ *ngIf="(appIsLoading$ | async) !== true"
+ class="openk-button openk-info map--search--btn">
+ <mat-icon class="map--search--icon">search</mat-icon>
+ </button>
+
+ <app-progress-spinner *ngIf="(appIsLoading$ | async) === true" class="progress-spinner">
+ </app-progress-spinner>
+ </div>
+
<app-action-button
(appClick)="appOpenGis.emit(appLeaflet.getBounds())"
[appIcon]="'my_location'"
diff --git a/src/app/shared/leaflet/components/leaflet-map.component.scss b/src/app/features/map/components/leaflet/leaflet-map.component.scss
similarity index 72%
rename from src/app/shared/leaflet/components/leaflet-map.component.scss
rename to src/app/features/map/components/leaflet/leaflet-map.component.scss
index e8e1a36..4a86137 100644
--- a/src/app/shared/leaflet/components/leaflet-map.component.scss
+++ b/src/app/features/map/components/leaflet/leaflet-map.component.scss
@@ -11,7 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "openk.styles";
+@import "src/styles/openk.styles";
:host {
width: 100%;
@@ -45,9 +45,37 @@
z-index: 1000;
}
+.map--search {
+ display: flex;
+ width: fit-content;
+ height: fit-content;
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ z-index: 1000;
+ align-items: center;
+}
+
.sub-caption {
color: $openk-form-border;
margin-left: auto;
font-size: smaller;
font-style: italic;
}
+
+.progress-spinner {
+ padding: 0 0.0275em;
+ transition: opacity 75ms ease-out;
+}
+
+.map--search--input {
+ margin-right: 0.25em;
+}
+
+.map--search--btn {
+ padding: 0.175em;
+}
+
+.map--search--icon {
+ padding: 0;
+}
diff --git a/src/app/features/map/components/leaflet/leaflet-map.component.spec.ts b/src/app/features/map/components/leaflet/leaflet-map.component.spec.ts
new file mode 100644
index 0000000..7a87ff9
--- /dev/null
+++ b/src/app/features/map/components/leaflet/leaflet-map.component.spec.ts
@@ -0,0 +1,70 @@
+/********************************************************************************
+ * 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, IAPINominatimSearchResult} from "../../../../core";
+import {getNominatimResponseContentSelector, searchMapAction} from "../../../../store";
+import {MapModule} from "../../map.module";
+import {latLngZoomToString} from "../../util";
+import {LeafletMapComponent} from "./leaflet-map.component";
+
+describe("LeafletMapComponent", () => {
+ let component: LeafletMapComponent;
+ let fixture: ComponentFixture<LeafletMapComponent>;
+ let store: MockStore;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [MapModule, I18nModule],
+ providers: [provideMockStore({initialState: {geo: {loading: false, responseContent: null}}})]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LeafletMapComponent);
+ component = fixture.componentInstance;
+ store = TestBed.inject(MockStore);
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should dispatch searchMapAction on search", async () => {
+ const dispatchSpy = spyOn(store, "dispatch");
+ await component.search("");
+ expect(dispatchSpy).not.toHaveBeenCalled();
+ await component.search(null);
+ expect(dispatchSpy).not.toHaveBeenCalled();
+ await component.search("test");
+ expect(dispatchSpy).toHaveBeenCalledWith(searchMapAction({q: "test"}));
+ });
+
+ it("should emit appViewChanged when search response is changed", () => {
+ const lat = 52.520008;
+ const lng = 13.404954;
+ const searchResultEntry: IAPINominatimSearchResult = {
+ ...{} as IAPINominatimSearchResult,
+ lat: "" + lat,
+ lon: "" + lng
+ };
+ store.overrideSelector(getNominatimResponseContentSelector, [searchResultEntry]);
+ const searchSpy = spyOn(component.appSearch, "emit");
+ store.refreshState();
+ const zoom = component.getZoom();
+ expect(searchSpy).toHaveBeenCalledWith(latLngZoomToString({lat, lng}, zoom));
+ });
+
+});
diff --git a/src/app/features/map/components/leaflet/leaflet-map.component.ts b/src/app/features/map/components/leaflet/leaflet-map.component.ts
new file mode 100644
index 0000000..58c5c5f
--- /dev/null
+++ b/src/app/features/map/components/leaflet/leaflet-map.component.ts
@@ -0,0 +1,107 @@
+/********************************************************************************
+ * 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, OnDestroy, OnInit, Output, ViewChild} from "@angular/core";
+import {select, Store} from "@ngrx/store";
+import {LeafletMouseEvent, PopupEvent} from "leaflet";
+import {Subject} from "rxjs";
+import {filter, map, skip, takeUntil} from "rxjs/operators";
+import {getNominatimLoadingSelector, getNominatimSearchResultSelector, searchMapAction} from "../../../../store";
+import {ILeafletBounds, LeafletDirective, LeafletHandler} from "../../directives";
+import {latLngZoomToString, stringToLatLngZoom} from "../../util";
+
+@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 implements OnInit, OnDestroy {
+
+ @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>();
+
+ @Output()
+ public appSearch = new EventEmitter<string>();
+
+ public appIsLoading$ = this.store.pipe(select(getNominatimLoadingSelector));
+
+ public searchResult$ = this.store.pipe(select(getNominatimSearchResultSelector));
+
+ private destroy$ = new Subject();
+
+ @ViewChild(LeafletDirective, {static: true})
+ public leafletDirective: LeafletDirective;
+
+ public constructor(
+ public readonly ngZone: NgZone,
+ private readonly store: Store
+ ) {
+ super();
+ }
+
+ public get instance() {
+ return this.leafletDirective.instance;
+ }
+
+ public getZoom() {
+ return this.leafletDirective.getZoom();
+ }
+
+ public ngOnInit() {
+ this.searchResult$.pipe(
+ skip(1),
+ filter((center) => center != null),
+ map((center) => latLngZoomToString(center, this.leafletDirective.getZoom())),
+ takeUntil(this.destroy$)
+ ).subscribe((result) => {
+ this.appCenter = result;
+ this.appSearch.emit(result);
+ this.leafletDirective.appCenter = stringToLatLngZoom(result);
+ });
+ }
+
+ public async ngOnDestroy() {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ public search(search: string) {
+ if (search) {
+ this.store.dispatch(searchMapAction({q: search}));
+ }
+ }
+}
diff --git a/src/app/shared/controls/map-select/components/index.ts b/src/app/features/map/components/map-select/index.ts
similarity index 100%
rename from src/app/shared/controls/map-select/components/index.ts
rename to src/app/features/map/components/map-select/index.ts
diff --git a/src/app/shared/controls/map-select/components/map-select.component.html b/src/app/features/map/components/map-select/map-select.component.html
similarity index 81%
rename from src/app/shared/controls/map-select/components/map-select.component.html
rename to src/app/features/map/components/map-select/map-select.component.html
index 21b71a7..64a610c 100644
--- a/src/app/shared/controls/map-select/components/map-select.component.html
+++ b/src/app/features/map/components/map-select/map-select.component.html
@@ -12,14 +12,16 @@
-------------------------------------------------------------------------------->
<app-leaflet-map
- (appCenterChange)="select($event)"
+ #map
+ (appClick)="select($event, map.getZoom())"
(appOpenGis)="appOpenGis.emit($event)"
- [appCenter]="appValue"
+ (appSearch)="writeValue($event, true)"
+ [appCenter]="appCenter"
[appDisabled]="appDisabled"
[appSubCaption]="appSubCaption"
class="map">
- <ng-container appLeafletCenterMarker>
+ <ng-container [appLeafletMarker]="appValue">
</ng-container>
</app-leaflet-map>
diff --git a/src/app/shared/controls/map-select/components/map-select.component.scss b/src/app/features/map/components/map-select/map-select.component.scss
similarity index 94%
rename from src/app/shared/controls/map-select/components/map-select.component.scss
rename to src/app/features/map/components/map-select/map-select.component.scss
index 7e59f1f..edc4cc5 100644
--- a/src/app/shared/controls/map-select/components/map-select.component.scss
+++ b/src/app/features/map/components/map-select/map-select.component.scss
@@ -11,7 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "openk.styles";
+@import "src/styles/openk.styles";
:host {
display: block;
diff --git a/src/app/shared/controls/map-select/components/map-select.component.spec.ts b/src/app/features/map/components/map-select/map-select.component.spec.ts
similarity index 62%
rename from src/app/shared/controls/map-select/components/map-select.component.spec.ts
rename to src/app/features/map/components/map-select/map-select.component.spec.ts
index dd5e766..91bcbf2 100644
--- a/src/app/shared/controls/map-select/components/map-select.component.spec.ts
+++ b/src/app/features/map/components/map-select/map-select.component.spec.ts
@@ -12,8 +12,10 @@
********************************************************************************/
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {provideMockStore} from "@ngrx/store/testing";
+import {LatLng, LeafletMouseEvent} from "leaflet";
import {I18nModule} from "../../../../core";
-import {MapSelectModule} from "../map-select.module";
+import {MapModule} from "../../map.module";
import {MapSelectComponent} from "./map-select.component";
describe("MapSelectComponent", () => {
@@ -22,7 +24,8 @@
beforeEach(async(() => {
TestBed.configureTestingModule({
- imports: [MapSelectModule, I18nModule]
+ imports: [MapModule, I18nModule],
+ providers: [provideMockStore()]
}).compileComponents();
}));
@@ -33,16 +36,19 @@
});
it("should select a new value", () => {
- const value = "52.520008,13.404954,11";
- const valueEmitSpy = spyOn(component.appValueChange, "emit");
- const onChangeSpy = spyOn(component, "onChange");
- const onTouchSpy = spyOn(component, "onTouch");
+ const changeSpy = spyOn(component.appValueChange, "emit");
+ const mouseEvent: LeafletMouseEvent = {
+ latlng: {
+ lat: 52.520008,
+ lng: 13.404954
+ } as LatLng
+ } as unknown as LeafletMouseEvent;
- component.select(value);
+ component.select(mouseEvent, 11);
- expect(valueEmitSpy).toHaveBeenCalledWith(value);
- expect(onChangeSpy).toHaveBeenCalledWith(value);
- expect(onTouchSpy).toHaveBeenCalledWith();
+ const expectedValue = "52.520008,13.404954,11";
+ expect(component.appValue).toEqual(expectedValue);
+ expect(changeSpy).toHaveBeenCalledWith(expectedValue);
});
});
diff --git a/src/app/shared/controls/map-select/components/map-select.component.ts b/src/app/features/map/components/map-select/map-select.component.ts
similarity index 73%
rename from src/app/shared/controls/map-select/components/map-select.component.ts
rename to src/app/features/map/components/map-select/map-select.component.ts
index 9c8759d..1f81674 100644
--- a/src/app/shared/controls/map-select/components/map-select.component.ts
+++ b/src/app/features/map/components/map-select/map-select.component.ts
@@ -13,8 +13,10 @@
import {Component, EventEmitter, forwardRef, Input, Output} from "@angular/core";
import {NG_VALUE_ACCESSOR} from "@angular/forms";
-import {ILeafletBounds} from "../../../leaflet";
-import {AbstractControlValueAccessorComponent} from "../../common";
+import {LeafletMouseEvent} from "leaflet";
+import {AbstractControlValueAccessorComponent} from "../../../../shared/controls/common";
+import {ILeafletBounds} from "../../directives/leaflet";
+import {latLngZoomToString} from "../../util";
@Component({
selector: "app-map-select",
@@ -39,12 +41,11 @@
@Output()
public appOpenGis = new EventEmitter<ILeafletBounds>();
- public select(value: string) {
- // Note that this.appValue should not be changed here:
- // Changing it here can create an infinite loop in which the map position changes without any user input.
- this.onChange(value);
- this.onTouch();
- this.appValueChange.emit(value);
+ public select(clickEvent: LeafletMouseEvent, zoom: number) {
+ const position = latLngZoomToString(clickEvent.latlng, zoom);
+ if (position) {
+ this.writeValue(position, true);
+ }
}
}
diff --git a/src/app/shared/leaflet/directives/index.ts b/src/app/features/map/directives/index.ts
similarity index 94%
rename from src/app/shared/leaflet/directives/index.ts
rename to src/app/features/map/directives/index.ts
index d2be228..770e43d 100644
--- a/src/app/shared/leaflet/directives/index.ts
+++ b/src/app/features/map/directives/index.ts
@@ -11,7 +11,6 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-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/features/map/directives/leaflet/LeafletHandler.ts
similarity index 100%
rename from src/app/shared/leaflet/directives/leaflet/LeafletHandler.ts
rename to src/app/features/map/directives/leaflet/LeafletHandler.ts
diff --git a/src/app/shared/leaflet/directives/leaflet/index.ts b/src/app/features/map/directives/leaflet/index.ts
similarity index 100%
rename from src/app/shared/leaflet/directives/leaflet/index.ts
rename to src/app/features/map/directives/leaflet/index.ts
diff --git a/src/app/shared/leaflet/directives/leaflet/leaflet.directive.spec.ts b/src/app/features/map/directives/leaflet/leaflet.directive.spec.ts
similarity index 97%
rename from src/app/shared/leaflet/directives/leaflet/leaflet.directive.spec.ts
rename to src/app/features/map/directives/leaflet/leaflet.directive.spec.ts
index ddf71fc..ba27e40 100644
--- a/src/app/shared/leaflet/directives/leaflet/leaflet.directive.spec.ts
+++ b/src/app/features/map/directives/leaflet/leaflet.directive.spec.ts
@@ -16,7 +16,7 @@
import {LatLngLiteral} from "leaflet";
import {Subject, Subscription} from "rxjs";
import {LEAFLET_RESIZE_TOKEN} from "../../leaflet-configuration.token";
-import {LeafletModule} from "../../leaflet.module";
+import {MapModule} from "../../map.module";
import {LeafletDirective} from "./leaflet.directive";
describe("LeafletDirective", () => {
@@ -27,7 +27,7 @@
beforeEach(async(() => {
TestBed.configureTestingModule({
- imports: [LeafletModule],
+ imports: [MapModule],
declarations: [LeafletSpecComponent],
providers: [
{
diff --git a/src/app/shared/leaflet/directives/leaflet/leaflet.directive.ts b/src/app/features/map/directives/leaflet/leaflet.directive.ts
similarity index 96%
rename from src/app/shared/leaflet/directives/leaflet/leaflet.directive.ts
rename to src/app/features/map/directives/leaflet/leaflet.directive.ts
index bbcc73d..8e30601 100644
--- a/src/app/shared/leaflet/directives/leaflet/leaflet.directive.ts
+++ b/src/app/features/map/directives/leaflet/leaflet.directive.ts
@@ -112,10 +112,14 @@
this.ngZone.runOutsideAngular(() => this.instance.remove());
}
+ public getZoom(): number {
+ return this.ngZone.runOutsideAngular(() => this.instance.getZoom());
+ }
+
public getBounds(): ILeafletBounds {
return this.ngZone.runOutsideAngular(() => {
const bounds = this.instance.getBounds();
- const zoom = this.instance.getZoom();
+ const zoom = this.getZoom();
const result: ILeafletBounds = {
northWest: bounds.getNorthWest(),
northEast: bounds.getNorthEast(),
diff --git a/src/app/shared/leaflet/directives/marker/abstract-leaflet-marker.directive.ts b/src/app/features/map/directives/marker/abstract-leaflet-marker.directive.ts
similarity index 100%
rename from src/app/shared/leaflet/directives/marker/abstract-leaflet-marker.directive.ts
rename to src/app/features/map/directives/marker/abstract-leaflet-marker.directive.ts
diff --git a/src/app/shared/leaflet/directives/marker/index.ts b/src/app/features/map/directives/marker/index.ts
similarity index 100%
rename from src/app/shared/leaflet/directives/marker/index.ts
rename to src/app/features/map/directives/marker/index.ts
diff --git a/src/app/shared/leaflet/directives/marker/leaflet-marker.directive.spec.ts b/src/app/features/map/directives/marker/leaflet-marker.directive.spec.ts
similarity index 97%
rename from src/app/shared/leaflet/directives/marker/leaflet-marker.directive.spec.ts
rename to src/app/features/map/directives/marker/leaflet-marker.directive.spec.ts
index 24f654f..9faa2ea 100644
--- a/src/app/shared/leaflet/directives/marker/leaflet-marker.directive.spec.ts
+++ b/src/app/features/map/directives/marker/leaflet-marker.directive.spec.ts
@@ -15,7 +15,7 @@
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
import {LatLngLiteral} from "leaflet";
import {Subject, Subscription} from "rxjs";
-import {LeafletModule} from "../../leaflet.module";
+import {MapModule} from "../../map.module";
import {LeafletMarkerDirective} from "./leaflet-marker.directive";
describe("LeafletMarkerDirective", () => {
@@ -31,7 +31,7 @@
beforeEach(async(() => {
TestBed.configureTestingModule({
- imports: [LeafletModule],
+ imports: [MapModule],
declarations: [LeafletMarkerSpecComponent]
}).compileComponents();
}));
diff --git a/src/app/shared/leaflet/directives/marker/leaflet-marker.directive.ts b/src/app/features/map/directives/marker/leaflet-marker.directive.ts
similarity index 100%
rename from src/app/shared/leaflet/directives/marker/leaflet-marker.directive.ts
rename to src/app/features/map/directives/marker/leaflet-marker.directive.ts
diff --git a/src/app/shared/leaflet/directives/popup/index.ts b/src/app/features/map/directives/popup/index.ts
similarity index 100%
rename from src/app/shared/leaflet/directives/popup/index.ts
rename to src/app/features/map/directives/popup/index.ts
diff --git a/src/app/shared/leaflet/directives/popup/leaflet-popup.directive.spec.ts b/src/app/features/map/directives/popup/leaflet-popup.directive.spec.ts
similarity index 82%
rename from src/app/shared/leaflet/directives/popup/leaflet-popup.directive.spec.ts
rename to src/app/features/map/directives/popup/leaflet-popup.directive.spec.ts
index fc8d984..e5343a8 100644
--- a/src/app/shared/leaflet/directives/popup/leaflet-popup.directive.spec.ts
+++ b/src/app/features/map/directives/popup/leaflet-popup.directive.spec.ts
@@ -11,22 +11,9 @@
* 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 {MapModule} from "../../map.module";
import {LeafletPopupDirective} from "./leaflet-popup.directive";
describe("LeafletPopupDirective", () => {
@@ -39,7 +26,7 @@
beforeEach(async(() => {
TestBed.configureTestingModule({
- imports: [LeafletModule],
+ imports: [MapModule],
declarations: [LeafletPopupSpecComponent]
}).compileComponents();
}));
diff --git a/src/app/shared/leaflet/directives/popup/leaflet-popup.directive.ts b/src/app/features/map/directives/popup/leaflet-popup.directive.ts
similarity index 100%
rename from src/app/shared/leaflet/directives/popup/leaflet-popup.directive.ts
rename to src/app/features/map/directives/popup/leaflet-popup.directive.ts
diff --git a/src/app/shared/leaflet/index.ts b/src/app/features/map/index.ts
similarity index 91%
rename from src/app/shared/leaflet/index.ts
rename to src/app/features/map/index.ts
index 849c149..e122506 100644
--- a/src/app/shared/leaflet/index.ts
+++ b/src/app/features/map/index.ts
@@ -11,9 +11,10 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+export * from "./components";
export * from "./directives";
export * from "./pipes";
export * from "./util";
-export * from "./leaflet.module";
+export * from "./map.module";
export * from "./leaflet-configuration.token";
diff --git a/src/app/shared/leaflet/leaflet-configuration.token.ts b/src/app/features/map/leaflet-configuration.token.ts
similarity index 100%
rename from src/app/shared/leaflet/leaflet-configuration.token.ts
rename to src/app/features/map/leaflet-configuration.token.ts
diff --git a/src/app/shared/leaflet/leaflet.module.ts b/src/app/features/map/map.module.ts
similarity index 65%
rename from src/app/shared/leaflet/leaflet.module.ts
rename to src/app/features/map/map.module.ts
index 5625400..4f3e261 100644
--- a/src/app/shared/leaflet/leaflet.module.ts
+++ b/src/app/features/map/map.module.ts
@@ -13,12 +13,17 @@
import {CommonModule} from "@angular/common";
import {NgModule} from "@angular/core";
+import {MatButtonModule} from "@angular/material/button";
+import {MatIconModule} from "@angular/material/icon";
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 {ActionButtonModule} from "../../shared/layout/action-button";
+import {SearchbarModule} from "../../shared/layout/searchbar";
+import {SideMenuRegistrationService} from "../../shared/layout/side-menu/services";
+import {ProgressSpinnerModule} from "../../shared/progress-spinner";
+import {LeafletMapComponent} from "./components/leaflet";
+import {MapSelectComponent} from "./components/map-select";
+import {LeafletDirective, LeafletMarkerDirective, LeafletPopupDirective} from "./directives";
import {LEAFLET_CONFIGURATION_TOKEN, LEAFLET_RESIZE_TOKEN} from "./leaflet-configuration.token";
import {StringToLatLngZoomPipe} from "./pipes";
@@ -26,23 +31,27 @@
imports: [
CommonModule,
ActionButtonModule,
- TranslateModule
+ TranslateModule,
+ SearchbarModule,
+ ProgressSpinnerModule,
+ MatButtonModule,
+ MatIconModule
],
declarations: [
- LeafletCenterMarkerDirective,
LeafletDirective,
LeafletMarkerDirective,
LeafletPopupDirective,
LeafletMapComponent,
- StringToLatLngZoomPipe
+ StringToLatLngZoomPipe,
+ MapSelectComponent
],
exports: [
- LeafletCenterMarkerDirective,
LeafletDirective,
LeafletMarkerDirective,
LeafletPopupDirective,
LeafletMapComponent,
- StringToLatLngZoomPipe
+ StringToLatLngZoomPipe,
+ MapSelectComponent
],
providers: [
{
@@ -57,6 +66,6 @@
}
]
})
-export class LeafletModule {
+export class MapModule {
}
diff --git a/src/app/shared/leaflet/pipes/index.ts b/src/app/features/map/pipes/index.ts
similarity index 100%
rename from src/app/shared/leaflet/pipes/index.ts
rename to src/app/features/map/pipes/index.ts
diff --git a/src/app/shared/leaflet/pipes/string-to-lat-lng-zoom.pipe.spec.ts b/src/app/features/map/pipes/string-to-lat-lng-zoom.pipe.spec.ts
similarity index 100%
rename from src/app/shared/leaflet/pipes/string-to-lat-lng-zoom.pipe.spec.ts
rename to src/app/features/map/pipes/string-to-lat-lng-zoom.pipe.spec.ts
diff --git a/src/app/shared/leaflet/pipes/string-to-lat-lng-zoom.pipe.ts b/src/app/features/map/pipes/string-to-lat-lng-zoom.pipe.ts
similarity index 100%
rename from src/app/shared/leaflet/pipes/string-to-lat-lng-zoom.pipe.ts
rename to src/app/features/map/pipes/string-to-lat-lng-zoom.pipe.ts
diff --git a/src/app/shared/leaflet/util/index.ts b/src/app/features/map/util/index.ts
similarity index 100%
rename from src/app/shared/leaflet/util/index.ts
rename to src/app/features/map/util/index.ts
diff --git a/src/app/shared/leaflet/util/leaflet.util.spec.ts b/src/app/features/map/util/leaflet.util.spec.ts
similarity index 100%
rename from src/app/shared/leaflet/util/leaflet.util.spec.ts
rename to src/app/features/map/util/leaflet.util.spec.ts
diff --git a/src/app/shared/leaflet/util/leaflet.util.ts b/src/app/features/map/util/leaflet.util.ts
similarity index 100%
rename from src/app/shared/leaflet/util/leaflet.util.ts
rename to src/app/features/map/util/leaflet.util.ts
diff --git a/src/app/features/navigation/components/nav-header/nav-header.component.html b/src/app/features/navigation/components/nav-header/nav-header.component.html
index 00c9237..238dca5 100644
--- a/src/app/features/navigation/components/nav-header/nav-header.component.html
+++ b/src/app/features/navigation/components/nav-header/nav-header.component.html
@@ -45,6 +45,13 @@
</a>
</ng-container>
+ <a [href]="helpRoute"
+ [title]="'core.header.help' | translate"
+ class="openk-button openk-info openk-button-rounded nav-header-menu-anchor nav-header-menu-anchor---large-icon"
+ target="_blank">
+ <mat-icon>help_outline</mat-icon>
+ </a>
+
<app-nav-drop-down
(appLogOut)="logOut()">
{{appUserName}}
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 9a927cd..7bcadc0 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
@@ -11,8 +11,8 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from "@angular/core";
-import {EAPIUserRoles} from "../../../../core";
+import {ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, Output} from "@angular/core";
+import {EAPIUserRoles, HELP_ROUTE} from "../../../../core";
import {arrayJoin} from "../../../../util/store";
export interface INavHeaderRoute {
@@ -82,16 +82,13 @@
link: "/settings",
tooltip: "core.header.settings",
roles: [EAPIUserRoles.SPA_OFFICIAL_IN_CHARGE, EAPIUserRoles.SPA_ADMIN]
- },
- {
- icon: "help_outline",
- link: "/help",
- tooltip: "core.header.help",
- target: "_blank",
- largeIcon: true
}
];
+ public constructor(@Inject(HELP_ROUTE) public helpRoute: string) {
+
+ }
+
public logOut() {
this.appLogout.emit();
}
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
index 76fb14e..bc74556 100644
--- a/src/app/features/search/components/date-filter/date-filter.component.html
+++ b/src/app/features/search/components/date-filter/date-filter.component.html
@@ -21,7 +21,7 @@
<div>
<app-date-control
#dueDateFromSelect
- (appValueChange)="emitNewValue(dueDateFromSelect.value)"
+ (appValueChange)="appActive = true; emitNewValue(dueDateFromSelect.value)"
class="openk-info">
</app-date-control>
</div>
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
index eeec9c8..e3eabcb 100644
--- 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
@@ -17,8 +17,8 @@
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 {ILeafletBounds} from "../../../map";
import {SearchModule} from "../../search.module";
import {PositionSearchComponent} from "./position-search.component";
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
index 9a4d1f1..e5d909c 100644
--- a/src/app/features/search/components/position-search/position-search.component.ts
+++ b/src/app/features/search/components/position-search/position-search.component.ts
@@ -16,7 +16,6 @@
import {select, Store} from "@ngrx/store";
import {take} from "rxjs/operators";
import {IAPIPositionSearchOptions, IAPIPositionSearchStatementModel} from "../../../../core";
-import {ILeafletBounds} from "../../../../shared/leaflet";
import {
getStatementPositionSearchSelector,
openGisAction,
@@ -26,8 +25,14 @@
statementTypesSelector,
userNameSelector
} from "../../../../store";
-import {IFilterToDisplay} from "../search-filter";
+import {ILeafletBounds} from "../../../map";
+import {ISearchFilterToDisplay, MAP_SEARCH_FILTER} from "../search-filter";
+/**
+ * This component display a list of statements on a map. A keyword search and filters can be applied to the list of statements.
+ * The statements are displayed as markers on the map and by clicking on a marker the statement details page of that statement can be
+ * opened.
+ */
@Component({
selector: "app-position-search",
templateUrl: "./position-search.component.html",
@@ -45,12 +50,7 @@
public userName$ = this.store.pipe(select(userNameSelector));
-
- public filtersToShow: IFilterToDisplay = {
- filterForType: false,
- dueDateFrom: false,
- dueDateTo: false
- };
+ public filtersToShow: ISearchFilterToDisplay = MAP_SEARCH_FILTER;
public selected: IAPIPositionSearchStatementModel;
diff --git a/src/app/features/search/components/search-filter/IFilterToDisplay.ts b/src/app/features/search/components/search-filter/ISearchFilterToDisplay.ts
similarity index 64%
rename from src/app/features/search/components/search-filter/IFilterToDisplay.ts
rename to src/app/features/search/components/search-filter/ISearchFilterToDisplay.ts
index 203b033..cdde65f 100644
--- a/src/app/features/search/components/search-filter/IFilterToDisplay.ts
+++ b/src/app/features/search/components/search-filter/ISearchFilterToDisplay.ts
@@ -11,14 +11,32 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export interface IFilterToDisplay {
- filterForType?: boolean;
- status?: boolean;
+export interface ISearchFilterToDisplay {
+ typeId?: boolean;
+ finished?: boolean;
editedByMe?: boolean;
- dueDateFrom?: boolean;
- dueDateTo?: boolean;
creationDateFrom?: boolean;
creationDateTo?: boolean;
+ dueDateFrom?: boolean;
+ dueDateTo?: boolean;
receiptDateFrom?: boolean;
receiptDateTo?: boolean;
}
+
+export const ALL_SEARCH_FILTER = {
+ typeId: true,
+ finished: true,
+ editedByMe: true,
+ dueDateFrom: true,
+ dueDateTo: true,
+ creationDateFrom: true,
+ creationDateTo: true,
+ receiptDateFrom: true,
+ receiptDateTo: true
+};
+
+export const MAP_SEARCH_FILTER = {
+ typeId: true,
+ dueDateFrom: true,
+ dueDateTo: true
+};
diff --git a/src/app/features/search/components/search-filter/index.ts b/src/app/features/search/components/search-filter/index.ts
index da57faa..1aa65bb 100644
--- a/src/app/features/search/components/search-filter/index.ts
+++ b/src/app/features/search/components/search-filter/index.ts
@@ -12,4 +12,4 @@
********************************************************************************/
export * from "./search-filter.component";
-export * from "./IFilterToDisplay";
+export * from "./ISearchFilterToDisplay";
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
index 37c2735..b52b23a 100644
--- a/src/app/features/search/components/search-filter/search-filter.component.html
+++ b/src/app/features/search/components/search-filter/search-filter.component.html
@@ -14,7 +14,7 @@
<div *ngIf="appShowSearch" class="filters--searchbar">
<span class="filters--searchbar--text">{{"search.title" | translate}}</span>
<app-searchbar
- (appSearch)="searchByString($event)"
+ (appSearch)="setSearchParameter('q', $event)"
[appIsLoading]="appLoading"
[appPlaceholder]="'search.executeSearch' | translate"
class="filters--searchbar--input">
@@ -32,19 +32,18 @@
</button>
<div class="all-filters">
- <div *ngIf="appFilters.filterForType != null || appFilters.editedByMe != null || appFilters.status != null"
+ <div *ngIf="appFilters.typeId != null || appFilters.editedByMe != null || appFilters.finished != 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"
+ (click)="toggleSearchParameter('typeId', typeSelect.appValue)"
+ [class.openk-info]="appValue.typeId != null"
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"
+ (appValueChange)="setSearchParameter('typeId', $event)"
[appDisabled]="appStatementTypeOptions?.length == null || appStatementTypeOptions.length === 0"
[appOptions]="appStatementTypeOptions"
[appPlaceholder]="typeSelect.appDisabled ? ('search.noData' | translate) : ''"
@@ -53,19 +52,17 @@
</app-select>
</div>
</div>
- <div *ngIf="appFilters.status != null"
+ <div *ngIf="appFilters.finished != 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"
+ (click)="toggleSearchParameter('finished', statusSelect.appValue)"
+ [class.openk-info]="appValue.finished != null"
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"
+ (appValueChange)="setSearchParameter('finished', statusSelect.appValue)"
[appOptions]="finishedOptions$ | async"
[appValue]="(finishedOptions$ | async)?.length > 0 ? (finishedOptions$ | async)[0].value : statusSelect.appValue"
class="openk-info filters--select--input-width">
@@ -75,9 +72,8 @@
<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"
+ (click)="toggleSearchParameter('editedByMe', true)"
+ [class.openk-info]="appValue.editedByMe != null"
class="openk-button openk-chip filters--btn">
{{"search.editedByMe" | translate}}
</button>
@@ -89,16 +85,16 @@
<div class="filters--date-selects--block">
<app-date-filter
#creationDateFrom
- (appValueChange)="setSearchParamsDate('creationDateFrom', $event, creationDateFrom.appActive)"
+ (appValueChange)="setSearchParameter('creationDateFrom', creationDateFrom.appActive ? $event : null)"
*ngIf="appFilters.creationDateFrom != null"
- [appActive]="appFilters.creationDateFrom"
+ [appActive]="appValue.creationDateFrom != null"
[appTitle]="'search.creationDateFrom' | translate">
</app-date-filter>
<app-date-filter
#creationDateTo
- (appValueChange)="setSearchParamsDate('creationDateTo', $event, creationDateTo.appActive)"
+ (appValueChange)="setSearchParameter('creationDateTo', creationDateTo.appActive ? $event : null)"
*ngIf="appFilters.creationDateTo != null"
- [appActive]="appFilters.creationDateTo"
+ [appActive]="appValue.creationDateTo != null"
[appTitle]="'search.creationDateTo' | translate">
</app-date-filter>
</div>
@@ -108,16 +104,16 @@
<div class="filters--date-selects--block">
<app-date-filter
#dueDateFrom
- (appValueChange)="setSearchParamsDate('dueDateFrom', $event, dueDateFrom.appActive)"
+ (appValueChange)="setSearchParameter('dueDateFrom', dueDateFrom.appActive ? $event : null)"
*ngIf="appFilters.dueDateFrom != null"
- [appActive]="appFilters.dueDateFrom"
+ [appActive]="appValue.dueDateFrom != null"
[appTitle]="'search.dueDateFrom' | translate">
</app-date-filter>
<app-date-filter
#dueDateTo
- (appValueChange)="setSearchParamsDate('dueDateTo', $event, dueDateTo.appActive)"
+ (appValueChange)="setSearchParameter('dueDateTo', dueDateTo.appActive ? $event : null)"
*ngIf="appFilters.dueDateTo != null"
- [appActive]="appFilters.dueDateTo"
+ [appActive]="appValue.dueDateTo != null"
[appTitle]="'search.dueDateTo' | translate">
</app-date-filter>
</div>
@@ -127,16 +123,16 @@
<div class="filters--date-selects--block">
<app-date-filter
#receiptDateFrom
- (appValueChange)="setSearchParamsDate('receiptDateFrom', $event, receiptDateFrom.appActive)"
+ (appValueChange)="setSearchParameter('receiptDateFrom', receiptDateFrom.appActive ? $event : null)"
*ngIf="appFilters.receiptDateFrom != null"
- [appActive]="appFilters.receiptDateFrom"
+ [appActive]="appValue.receiptDateFrom != null"
[appTitle]="'search.receiptDateFrom' | translate">
</app-date-filter>
<app-date-filter
#receiptDateTo
- (appValueChange)="setSearchParamsDate('receiptDateTo', $event, receiptDateTo.appActive)"
+ (appValueChange)="setSearchParameter('receiptDateTo', receiptDateTo.appActive ? $event : null)"
*ngIf="appFilters.receiptDateTo != null"
- [appActive]="appFilters.receiptDateTo"
+ [appActive]="appValue.receiptDateTo != null"
[appTitle]="'search.receiptDateTo' | translate">
</app-date-filter>
</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
index 6dae64d..1be7426 100644
--- a/src/app/features/search/components/search-filter/search-filter.component.scss
+++ b/src/app/features/search/components/search-filter/search-filter.component.scss
@@ -61,7 +61,7 @@
}
.filters--type-select-width {
- width: 12em;
+ width: 12.5em;
}
.filters--finished-select-width {
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
index ac06435..ee48854 100644
--- 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
@@ -14,8 +14,10 @@
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
import {Store} from "@ngrx/store";
import {provideMockStore} from "@ngrx/store/testing";
+import {IAPISearchOptions} from "../../../../core/api/shared";
import {I18nModule} from "../../../../core/i18n";
import {queryParamsIdSelector} from "../../../../store/root/selectors";
+import {momentFormatInternal, parseMomentToString} from "../../../../util/moment";
import {SearchModule} from "../../search.module";
import {SearchFilterComponent} from "./search-filter.component";
@@ -55,40 +57,49 @@
expect(component).toBeTruthy();
});
- it("should set all filter active status to false on disableAllFilters", () => {
- component.appFilters = {
- status: false,
- editedByMe: true,
- dueDateFrom: false,
- dueDateTo: false
- };
+ it("should toggle a search parameter", () => {
+ const emitSpy = spyOn(component.appValueChange, "emit");
+ const today = new Date();
+ const searchParams: IAPISearchOptions = {};
+
+ searchParams.q = "test";
+ component.setSearchParameter("q", "test");
+ expect(emitSpy).toHaveBeenCalledWith(searchParams);
+
+ searchParams.dueDateTo = parseMomentToString(today, momentFormatInternal, momentFormatInternal);
+ component.toggleSearchParameter("dueDateTo", today);
+ expect(emitSpy).toHaveBeenCalledWith(searchParams);
+
+ searchParams.dueDateTo = parseMomentToString(today, momentFormatInternal, momentFormatInternal);
+ component.toggleSearchParameter("dueDateTo", today);
+ expect(emitSpy).toHaveBeenCalledWith(searchParams);
+
+ delete searchParams.dueDateTo;
+ component.toggleSearchParameter("dueDateTo", today);
+ expect(emitSpy).toHaveBeenCalledWith(searchParams);
+ });
+
+ it("should disable all search filters", () => {
+ const emitSpy = spyOn(component.appValueChange, "emit");
+ const today = new Date();
+ const searchParams: IAPISearchOptions = {};
+
+ searchParams.q = "test";
+ component.setSearchParameter("q", "test");
+ expect(emitSpy).toHaveBeenCalledWith(searchParams);
+
+ searchParams.dueDateTo = parseMomentToString(today, momentFormatInternal, momentFormatInternal);
+ component.setSearchParameter("dueDateTo", today);
+ expect(emitSpy).toHaveBeenCalledWith(searchParams);
+
+ delete searchParams.dueDateTo;
component.disableAllFilters();
- expect(component.appFilters).toEqual({
- status: false,
- editedByMe: false,
- dueDateFrom: false,
- dueDateTo: false
- });
+ expect(emitSpy).toHaveBeenCalledWith(searchParams);
});
- 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 only write values which are set", () => {
+ component.writeValue(({q: "test", dueDateTo: ""}));
+ expect(component.appValue).toEqual({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.ts b/src/app/features/search/components/search-filter/search-filter.component.ts
index d407dcc..e71023c 100644
--- a/src/app/features/search/components/search-filter/search-filter.component.ts
+++ b/src/app/features/search/components/search-filter/search-filter.component.ts
@@ -13,28 +13,24 @@
import {Component, EventEmitter, Input, OnInit, Output} from "@angular/core";
import {TranslateService} from "@ngx-translate/core";
-import {Observable} from "rxjs";
+import {defer, 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";
+import {ALL_SEARCH_FILTER, ISearchFilterToDisplay} from "./ISearchFilterToDisplay";
/**
* 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.
+ * The displayed filters 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",
@@ -54,76 +50,52 @@
@Output()
public appFilterParams = new EventEmitter<IAPISearchOptions>();
+ /**
+ * For each set property key, the specific filter is displayed in the component.
+ */
@Input()
- public appFilters: IFilterToDisplay = {
- filterForType: false,
- status: false,
- editedByMe: false,
- dueDateFrom: false,
- dueDateTo: false,
- creationDateFrom: false,
- creationDateTo: false,
- receiptDateFrom: false,
- receiptDateTo: false
- };
+ public appFilters: ISearchFilterToDisplay = ALL_SEARCH_FILTER;
- 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 finishedOptions$: Observable<ISelectOption[]> = defer(() => this.getFinishedOptions());
public constructor(public readonly translateService: TranslateService) {
super();
}
public ngOnInit() {
- this.writeValue({
- q: this.appShowSearch ? "" : undefined
- }, true);
+ this.writeValue({}, true);
}
- public searchByString(q: string) {
- this.writeValue({...this.appValue, q}, true);
+ public toggleSearchParameter(label: keyof IAPISearchOptions, value?: boolean | string | number | Date) {
+ this.setSearchParameter(label, this.appValue[label] != null ? undefined : value);
}
- 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;
+ public setSearchParameter(label: keyof IAPISearchOptions, value?: boolean | string | number | Date) {
+ const newValue: IAPISearchOptions = {
+ ...this.appValue,
+ [label]: typeof value === "object" ? parseMomentToString(value, momentFormatInternal, momentFormatInternal) : value
+ };
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);
+ this.writeValue({q: this.appValue.q}, true);
+ }
+
+ public writeValue(obj: IAPISearchOptions, emit?: boolean) {
+ obj = {...obj};
+ Object.keys(obj)
+ .filter((key) => obj[key] == null || obj[key] === "")
+ .forEach((key) => delete obj[key]);
+ super.writeValue(obj, emit);
+ }
+
+ private getFinishedOptions() {
+ const options: ISelectOption[] = ["search.open", "search.finished"]
+ .map((label, index) => ({label, value: index > 0}));
+ return this.translateService.get(options.map((option) => option.label)).pipe(
+ map((translation) => options.map((option) => ({...option, label: translation[option.label]})))
+ );
}
}
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
index 518a4a0..fa53b57 100644
--- a/src/app/features/search/components/search-statements/search-statements.component.html
+++ b/src/app/features/search/components/search-statements/search-statements.component.html
@@ -25,7 +25,7 @@
[appEntries]="searchContent$ | async"
[appIsSortable]="true"
[appStatementTypeOptions]="statementTypeOptions$ | async"
- class="openk-table---last-row-without-border search-list">
+ class="search-list">
</app-statement-table>
<app-pagination-counter
diff --git a/src/app/features/search/components/search-statements/search-statements.component.scss b/src/app/features/search/components/search-statements/search-statements.component.scss
index 2d673ee..9cdebde 100644
--- a/src/app/features/search/components/search-statements/search-statements.component.scss
+++ b/src/app/features/search/components/search-statements/search-statements.component.scss
@@ -13,10 +13,11 @@
@import "openk.styles";
-
:host {
position: relative;
- display: block;
+ display: flex;
+ flex-flow: column;
+ min-height: 100%;
width: 100%;
box-sizing: border-box;
padding: 1em;
@@ -24,6 +25,7 @@
.search-list {
min-height: 5.3125em;
+ flex: 1 1 24em;
background: get-color($openk-default-palette);
}
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
index 90579db..c936234 100644
--- a/src/app/features/search/components/search-statements/search-statements.component.ts
+++ b/src/app/features/search/components/search-statements/search-statements.component.ts
@@ -21,7 +21,7 @@
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.
+ * This component displays 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.
*/
diff --git a/src/app/features/search/search.module.ts b/src/app/features/search/search.module.ts
index 937ca93..9478926 100644
--- a/src/app/features/search/search.module.ts
+++ b/src/app/features/search/search.module.ts
@@ -21,7 +21,7 @@
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 {MapModule} from "../map";
import {DateFilterComponent} from "./components/date-filter";
import {PositionSearchComponent} from "./components/position-search";
import {SearchFilterComponent} from "./components/search-filter";
@@ -38,7 +38,7 @@
TranslateModule,
MatIconModule,
RouterModule,
- LeafletModule
+ MapModule
],
declarations: [
SearchFilterComponent,
diff --git a/src/app/features/settings/components/search/index.ts b/src/app/features/settings/components/search/index.ts
deleted file mode 100644
index 154cd69..0000000
--- a/src/app/features/settings/components/search/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 "./settings.component";
diff --git a/src/app/features/settings/components/search/settings.component.scss b/src/app/features/settings/components/search/settings.component.scss
deleted file mode 100644
index 06db89a..0000000
--- a/src/app/features/settings/components/search/settings.component.scss
+++ /dev/null
@@ -1,13 +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
- ********************************************************************************/
-
diff --git a/src/app/features/settings/departments/components/department-table/departments-settings-table.component.html b/src/app/features/settings/departments/components/department-table/departments-settings-table.component.html
new file mode 100644
index 0000000..83c3723
--- /dev/null
+++ b/src/app/features/settings/departments/components/department-table/departments-settings-table.component.html
@@ -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
+ -------------------------------------------------------------------------------->
+
+<table [dataSource]="appDepartments" cdk-table
+ class="openk-table openk-table---without-last-border">
+
+ <caption hidden>{{ "settings.departments.title" | translate}}</caption>
+
+ <tr *cdkHeaderRowDef="appColumns; sticky: true" cdk-header-row></tr>
+
+ <tr *cdkRowDef="let myRowData; columns: appColumns" cdk-row></tr>
+
+ <ng-container cdkColumnDef="city">
+ <th *cdkHeaderCellDef="let header"
+ cdk-header-cell
+ class="table-column"
+ scope="col">
+ {{"settings.departments.table.city" | translate}}
+ </th>
+ <td *cdkCellDef="let department"
+ cdk-cell
+ class="table-column">
+ <span>{{department?.key?.split("#").length > 1 ? department.key.split("#")[0] : ""}}</span>
+ </td>
+ </ng-container>
+
+ <ng-container cdkColumnDef="district">
+ <th *cdkHeaderCellDef="let header"
+ cdk-header-cell
+ class="table-column"
+ scope="col">
+ {{"settings.departments.table.district" | translate}}
+ </th>
+ <td *cdkCellDef="let department"
+ cdk-cell
+ class="table-column">
+ <span>{{department?.key?.split("#").length > 1 ? department.key.split("#")[1] : ""}}</span>
+ </td>
+ </ng-container>
+
+ <ng-container cdkColumnDef="sectors">
+ <th *cdkHeaderCellDef="let header"
+ cdk-header-cell
+ class="table-column"
+ scope="col">
+ {{"settings.departments.table.sectors" | translate}}
+ </th>
+ <td *cdkCellDef="let department"
+ cdk-cell
+ class="table-column">
+ <span>{{department?.value?.provides.join(', ')}}</span>
+ </td>
+ </ng-container>
+
+ <ng-container cdkColumnDef="departments">
+ <th *cdkHeaderCellDef="let header"
+ cdk-header-cell
+ class="table-column"
+ scope="col">
+ {{"settings.departments.table.departments" | translate}}
+ </th>
+ <td *cdkCellDef="let department"
+ cdk-cell
+ class="table-column">
+ <ng-container *ngFor="let group of (department?.value?.departments | objToArray)">
+ <div *ngFor="let dep of group.value">
+ <mat-icon class="list--element--icon">fiber_manual_record</mat-icon>
+ <span class="list--element---bold">{{group.key}}</span>
+ <span>{{dep}}</span>
+ </div>
+ </ng-container>
+ </td>
+ </ng-container>
+
+</table>
diff --git a/src/app/shared/leaflet/components/leaflet-map.component.scss b/src/app/features/settings/departments/components/department-table/departments-settings-table.component.scss
similarity index 61%
copy from src/app/shared/leaflet/components/leaflet-map.component.scss
copy to src/app/features/settings/departments/components/department-table/departments-settings-table.component.scss
index e8e1a36..c11a6b5 100644
--- a/src/app/shared/leaflet/components/leaflet-map.component.scss
+++ b/src/app/features/settings/departments/components/department-table/departments-settings-table.component.scss
@@ -11,43 +11,38 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "openk.styles";
+@import "../../../../../../styles/openk.styles";
:host {
- width: 100%;
- height: 100%;
-
display: flex;
flex-flow: column;
-}
-
-.map {
- height: 100%;
width: 100%;
position: relative;
- overflow: hidden;
+ overflow: auto;
box-sizing: border-box;
border: 1px solid $openk-form-border;
+ border-radius: 4px;
+ background: $openk-background-card;
+ padding-bottom: 2px;
}
-.map--leaflet {
- width: 100%;
- height: 100%;
+.openk-table---last-row-without-border:host {
+ padding-bottom: 0;
}
-.map--button {
- display: block;
- width: fit-content;
- height: fit-content;
- position: absolute;
- bottom: 10px;
- left: 10px;
- z-index: 1000;
+.table-column {
+ vertical-align: baseline;
+ text-align: start;
}
-.sub-caption {
- color: $openk-form-border;
- margin-left: auto;
- font-size: smaller;
- font-style: italic;
+.list--element--icon {
+ width: initial;
+ height: initial;
+ font-size: 0.5em;
+ margin-right: 1em;
+}
+
+.list--element---bold {
+ font-weight: 600;
+ margin-right: 0.25em;
}
diff --git a/src/app/features/settings/departments/components/department-table/departments-settings-table.component.spec.ts b/src/app/features/settings/departments/components/department-table/departments-settings-table.component.spec.ts
new file mode 100644
index 0000000..eda42fd
--- /dev/null
+++ b/src/app/features/settings/departments/components/department-table/departments-settings-table.component.spec.ts
@@ -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
+ ********************************************************************************/
+
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {RouterTestingModule} from "@angular/router/testing";
+import {provideMockStore} from "@ngrx/store/testing";
+import {I18nModule} from "../../../../../core/i18n";
+import {DepartmentsSettingsModule} from "../../departments-settings.module";
+import {DepartmentsSettingsTableComponent} from "./departments-settings-table.component";
+
+describe("DepartmentsSettingsTableComponent", () => {
+ let component: DepartmentsSettingsTableComponent;
+ let fixture: ComponentFixture<DepartmentsSettingsTableComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ DepartmentsSettingsModule,
+ RouterTestingModule,
+ I18nModule
+ ],
+ providers: [
+ provideMockStore()
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DepartmentsSettingsTableComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/features/settings/departments/components/department-table/departments-settings-table.component.ts b/src/app/features/settings/departments/components/department-table/departments-settings-table.component.ts
new file mode 100644
index 0000000..0429153
--- /dev/null
+++ b/src/app/features/settings/departments/components/department-table/departments-settings-table.component.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 {Component, Input} from "@angular/core";
+import {IAPIDepartmentGroups} from "../../../../../core";
+
+@Component({
+ selector: "app-departments-settings-table",
+ templateUrl: "./departments-settings-table.component.html",
+ styleUrls: ["./departments-settings-table.component.scss"]
+})
+export class DepartmentsSettingsTableComponent {
+
+ @Input()
+ public appDepartments: { key: string, value: { provides: string[], departments: IAPIDepartmentGroups }, searchString?: string }[] = [];
+
+ @Input()
+ public appColumns = ["city", "district", "sectors", "departments"];
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/departments/components/department-table/index.ts
similarity index 90%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/departments/components/department-table/index.ts
index 990bb42..085e0fa 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/departments/components/department-table/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./departments-settings-table.component";
diff --git a/src/app/features/settings/departments/components/departments-search/departments-settings-search.component.html b/src/app/features/settings/departments/components/departments-search/departments-settings-search.component.html
new file mode 100644
index 0000000..539b17e
--- /dev/null
+++ b/src/app/features/settings/departments/components/departments-search/departments-settings-search.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
+ -------------------------------------------------------------------------------->
+
+<div class="search">
+ <span class="search--bar">{{"settings.departments.search" | translate}}</span>
+ <app-searchbar
+ (appSearch)="filterDepartmentList($event)"
+ [appPlaceholder]="'settings.departments.placeholderSearch' | translate"
+ [appSearchText]="searchText"
+ class="search--input">
+ </app-searchbar>
+</div>
+
+<app-departments-settings-table
+ [appDepartments]="filteredDepartmentList?.slice(appPage * appPageSize, (appPage + 1) * appPageSize)">
+</app-departments-settings-table>
+
+<app-pagination-counter
+ (appPageChange)="changePage($event.page, $event.size)"
+ [appPageSizeOptions]="appPageSizeOptions"
+ [appPageSize]="appPageSize"
+ [appPage]="appPage"
+ [appTotalPages]="totalPages">
+</app-pagination-counter>
diff --git a/src/app/shared/controls/map-select/components/map-select.component.scss b/src/app/features/settings/departments/components/departments-search/departments-settings-search.component.scss
similarity index 73%
copy from src/app/shared/controls/map-select/components/map-select.component.scss
copy to src/app/features/settings/departments/components/departments-search/departments-settings-search.component.scss
index 7e59f1f..5701f34 100644
--- a/src/app/shared/controls/map-select/components/map-select.component.scss
+++ b/src/app/features/settings/departments/components/departments-search/departments-settings-search.component.scss
@@ -11,10 +11,23 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "openk.styles";
+@import "../../../../../../styles/openk.styles";
:host {
- display: block;
width: 100%;
- height: 100%;
+}
+
+.search {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ margin-bottom: 1em;
+}
+
+.search--bar {
+ margin-right: 0.5em;
+}
+
+.search--input {
+ flex: 1;
}
diff --git a/src/app/features/settings/departments/components/departments-search/departments-settings-search.component.spec.ts b/src/app/features/settings/departments/components/departments-search/departments-settings-search.component.spec.ts
new file mode 100644
index 0000000..fa805d1
--- /dev/null
+++ b/src/app/features/settings/departments/components/departments-search/departments-settings-search.component.spec.ts
@@ -0,0 +1,106 @@
+/********************************************************************************
+ * 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 {SimpleChange} from "@angular/core";
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {RouterTestingModule} from "@angular/router/testing";
+import {provideMockStore} from "@ngrx/store/testing";
+import {I18nModule, IAPIDepartmentGroups} from "../../../../../core";
+import {DepartmentsSettingsModule} from "../../departments-settings.module";
+import {DepartmentsSettingsSearchComponent} from "./departments-settings-search.component";
+
+describe("DepartmentsSettingsSearchComponent", () => {
+
+ const departments: { key: string, value: { provides: string[], departments: IAPIDepartmentGroups }, searchString?: string }[] = [
+ {
+ key: "Ort#Ortsteil",
+ value: {
+ provides: [
+ "Gas",
+ "Strom"
+ ],
+ departments: {
+ Allgemein: [
+ "Medianet",
+ "Planung"
+ ]
+ }
+ }
+ },
+ {
+ key: "Ort#Süd",
+ value: {
+ provides: [
+ "Wasser",
+ "Strom"
+ ],
+ departments: {
+ Allgemein: [
+ "Planung"
+ ]
+ }
+ }
+ }
+ ];
+
+ let component: DepartmentsSettingsSearchComponent;
+ let fixture: ComponentFixture<DepartmentsSettingsSearchComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ DepartmentsSettingsModule,
+ RouterTestingModule,
+ I18nModule
+ ],
+ providers: [
+ provideMockStore()
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DepartmentsSettingsSearchComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should filter the given department table", () => {
+ component.appDepartmentList = departments;
+ component.ngOnChanges({appDepartmentList: new SimpleChange([], departments, false)});
+ expect(component.filteredDepartmentList).toEqual(departments);
+
+ component.filterDepartmentList("Allgemein, Media");
+ expect(component.filteredDepartmentList).toEqual([departments[0]]);
+ });
+
+ it("should set page info", () => {
+ component.appPage = 1;
+ component.appPageSize = 1;
+ component.appDepartmentList = departments;
+ component.filterDepartmentList("");
+ expect(component.appPage).toBe(0);
+
+ component.appPage = 1;
+ component.appPageSize = 1;
+ component.ngOnChanges({appPage: new SimpleChange(0, 1, false)});
+ expect(component.appPageSize).toBe(1);
+ expect(component.appPage).toBe(1);
+ expect(component.totalPages).toBe(2);
+ });
+
+});
diff --git a/src/app/features/settings/departments/components/departments-search/departments-settings-search.component.ts b/src/app/features/settings/departments/components/departments-search/departments-settings-search.component.ts
new file mode 100644
index 0000000..80ed8fb
--- /dev/null
+++ b/src/app/features/settings/departments/components/departments-search/departments-settings-search.component.ts
@@ -0,0 +1,91 @@
+/********************************************************************************
+ * 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, OnChanges, Output, SimpleChanges} from "@angular/core";
+import {IAPIDepartmentGroups} from "../../../../../core";
+import {ISelectOption} from "../../../../../shared/controls/select";
+import {arrayJoin} from "../../../../../util";
+
+@Component({
+ selector: "app-departments-settings-search",
+ templateUrl: "./departments-settings-search.component.html",
+ styleUrls: ["./departments-settings-search.component.scss"]
+})
+export class DepartmentsSettingsSearchComponent implements OnChanges {
+
+ @Input()
+ public appDepartmentList: { key: string, value: { provides: string[], departments: IAPIDepartmentGroups } }[];
+
+ @Input()
+ public appPage = 0;
+
+ @Input()
+ public appPageSize = 10;
+
+ @Input()
+ public appPageSizeOptions: ISelectOption[] = [
+ {label: "5", value: 5},
+ {label: "10", value: 10},
+ {label: "25", value: 25},
+ {label: "50", value: 50}
+ ];
+
+ @Output()
+ public appIsSearching = new EventEmitter<boolean>();
+
+ public filteredDepartmentList: { key: string, value: { provides: string[], departments: IAPIDepartmentGroups } }[] = [];
+
+ public totalPages: number;
+
+ public searchText = "";
+
+ public ngOnChanges(changes: SimpleChanges) {
+ const onChange = (keys: Array<keyof DepartmentsSettingsSearchComponent>, fn: () => any) => {
+ if (keys.some((key) => changes[key] != null)) {
+ fn();
+ }
+ };
+ onChange(["appDepartmentList"], () => {
+ this.filterDepartmentList("");
+ });
+ onChange(["appPage", "appPageSize"], () => {
+ this.changePage(this.appPage, this.appPageSize);
+ });
+ }
+
+ public filterDepartmentList(searchText: string) {
+ this.searchText = searchText;
+ this.filteredDepartmentList = arrayJoin(this.appDepartmentList)
+ .filter((_) => {
+ const searchValues = arrayJoin(
+ _.key.split("#"), // City and district
+ _.value.provides, // Sectors
+ Object.keys(_.value.departments), // Department group names
+ ...Object.values(_.value.departments) // Department names
+ ).join("").toLowerCase();
+ const searchTokens = this.searchText
+ .toLowerCase()
+ .replace(/[;,.]/g, " ")
+ .split(" ");
+ return !searchTokens.some((token) => !searchValues.includes(token));
+ });
+ this.changePage(0);
+ }
+
+ public changePage(page: number = this.appPage, pageSize: number = this.appPageSize) {
+ this.appPageSize = Math.max(1, pageSize);
+ this.appPage = Math.max(0, page);
+ this.totalPages = Math.ceil(this.filteredDepartmentList.length / this.appPageSize);
+ }
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/departments/components/departments-search/index.ts
similarity index 90%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/departments/components/departments-search/index.ts
index 990bb42..a94a8cf 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/departments/components/departments-search/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./departments-settings-search.component";
diff --git a/src/app/features/settings/departments/components/departments-settings.component.html b/src/app/features/settings/departments/components/departments-settings.component.html
new file mode 100644
index 0000000..d90909e
--- /dev/null
+++ b/src/app/features/settings/departments/components/departments-settings.component.html
@@ -0,0 +1,105 @@
+<!-------------------------------------------------------------------------------
+ * 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-settings-side-menu
+ [appForAdmin]="true">
+</app-settings-side-menu>
+
+<app-side-menu-status *appSideMenu="'center'"
+ [appLoadingMessage]="'core.loading' | translate"
+ [appLoading]="(loading$ | async) || searching">
+ <ng-container *ngIf="selectedFile != null">
+ {{'settings.departments.selectedFile' | translate}}: <br> {{selectedFile.name}}
+ </ng-container>
+
+</app-side-menu-status>
+
+<ng-container *appSideMenu="'bottom'">
+ <app-action-button (appClick)="anchorElement.click()"
+ [appDisabled]="currentFileUrl == null || (loading$ | async)"
+ [appIcon]="'get_app'"
+ class="openk-info side-menu-button">
+ {{'settings.departments.downloadCurrent' | translate}}
+ </app-action-button>
+ <app-action-button (appClick)="inputElement.click()"
+ [appDisabled]="loading$ | async"
+ [appIcon]="'attach_file'"
+ class="openk-info side-menu-button">
+ {{'settings.departments.selectNew' | translate}}
+ </app-action-button>
+ <app-action-button (appClick)="submit(selectedFileData)"
+ [appDisabled]="selectedFileData == null || (loading$ | async)"
+ [appIcon]="'publish'"
+ class="openk-success side-menu-button">
+ {{'settings.departments.submit' | translate}}
+ </app-action-button>
+
+ <input #inputElement
+ (change)="selectFile(inputElement.files?.item(0)); inputElement.value = null;"
+ style="display: none;"
+ type="file">
+ <a #anchorElement [download]="currentConfigFileName" [href]="currentFileUrl" style="display: none;"></a>
+</ng-container>
+
+<div class="title">
+ <span class="title--label">
+ {{"settings.title" | translate}} - {{"settings.departments.title" | translate}}
+ </span>
+</div>
+
+<app-collapsible
+ [appTitle]="'settings.departments.departments'| translate"
+ class="departments-info">
+ <div class="departments-info--container">
+ <app-list
+ *ngFor="let val of (selectedFileData ? selectedFileData : (currentData$ | async)) | getDepartmentGroupsFromTablePipe | objToArray"
+ [appListItems]="val.value | appStringArrayToLabelList"
+ [appTitle]="val.key">
+ </app-list>
+ <span
+ *ngIf="((selectedFileData ? selectedFileData : (currentData$ | async)) | getDepartmentGroupsFromTablePipe | objToArray).length <= 0">
+ {{'settings.departments.placeholderNoDepartments'| translate}}
+ </span>
+ </div>
+</app-collapsible>
+
+<app-collapsible
+ [appTitle]="'settings.departments.sectors'| translate"
+ class="departments-info">
+ <div class="departments-info--container">
+ <span>
+ <ng-container
+ *ngFor="let sector of (selectedFileData ? selectedFileData : (currentData$ | async)) | appSectorsFromDepartment; let last = last;">
+ {{sector + (last ? "" : ",")}}
+ </ng-container>
+
+ <ng-container
+ *ngIf="((selectedFileData ? selectedFileData : (currentData$ | async)) | appSectorsFromDepartment).length === 0">
+ {{'settings.departments.placeholderNoSectors'| translate}}
+ </ng-container>
+ </span>
+ </div>
+</app-collapsible>
+
+<app-collapsible
+ [appTitle]="'settings.departments.organisation'| translate">
+ <div class="departments-info--container">
+ <app-departments-settings-search
+ (appIsSearching)="searching = $event;"
+ [appDepartmentList]="(selectedFileData ? selectedFileData : (currentData$ | async)) | objToArray">
+ </app-departments-settings-search>
+ </div>
+</app-collapsible>
+
+
+
diff --git a/src/app/shared/leaflet/components/leaflet-map.component.scss b/src/app/features/settings/departments/components/departments-settings.component.scss
similarity index 60%
copy from src/app/shared/leaflet/components/leaflet-map.component.scss
copy to src/app/features/settings/departments/components/departments-settings.component.scss
index e8e1a36..a3793d7 100644
--- a/src/app/shared/leaflet/components/leaflet-map.component.scss
+++ b/src/app/features/settings/departments/components/departments-settings.component.scss
@@ -11,43 +11,37 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "openk.styles";
-
:host {
width: 100%;
- height: 100%;
-
+ padding: 1em;
+ box-sizing: border-box;
display: flex;
flex-flow: column;
+ max-width: 70em;
+ margin: 0 auto 0 auto;
}
-.map {
- height: 100%;
+.title {
+ margin-bottom: 1em;
+}
+
+.title--label {
+ font-size: x-large;
+ font-weight: 600;
+}
+
+.side-menu-button {
width: 100%;
- position: relative;
- overflow: hidden;
- box-sizing: border-box;
- border: 1px solid $openk-form-border;
+
+ & + & {
+ margin-top: 1em;
+ }
}
-.map--leaflet {
- width: 100%;
- height: 100%;
+.departments-info {
+ margin-bottom: 1em;
}
-.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;
+.departments-info--container {
+ padding: 1em;
}
diff --git a/src/app/features/settings/departments/components/departments-settings.component.spec.ts b/src/app/features/settings/departments/components/departments-settings.component.spec.ts
new file mode 100644
index 0000000..ac4cbd1
--- /dev/null
+++ b/src/app/features/settings/departments/components/departments-settings.component.spec.ts
@@ -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
+ ********************************************************************************/
+
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {RouterTestingModule} from "@angular/router/testing";
+import {provideMockStore} from "@ngrx/store/testing";
+import {I18nModule} from "../../../../core";
+import {DepartmentsSettingsModule} from "../departments-settings.module";
+import {DepartmentsSettingsComponent} from "./departments-settings.component";
+
+describe("DepartmentsSettingsComponent", () => {
+ let component: DepartmentsSettingsComponent;
+ let fixture: ComponentFixture<DepartmentsSettingsComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ DepartmentsSettingsModule,
+ RouterTestingModule,
+ I18nModule
+ ],
+ providers: [
+ provideMockStore()
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DepartmentsSettingsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/features/settings/departments/components/departments-settings.component.ts b/src/app/features/settings/departments/components/departments-settings.component.ts
new file mode 100644
index 0000000..aeed52b
--- /dev/null
+++ b/src/app/features/settings/departments/components/departments-settings.component.ts
@@ -0,0 +1,116 @@
+/********************************************************************************
+ * 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, Inject, OnDestroy, OnInit} from "@angular/core";
+import {DomSanitizer, SafeUrl} from "@angular/platform-browser";
+import {select, Store} from "@ngrx/store";
+import {Observable, of, Subject} from "rxjs";
+import {map, switchMap, takeUntil} from "rxjs/operators";
+import {IAPIDepartmentTable, URL_TOKEN} from "../../../../core";
+import {
+ EErrorCode,
+ fetchDepartmentsSettingsAction,
+ getDepartmentsSettingsSelector,
+ getSettingsLoadingSelector,
+ setErrorAction,
+ submitDepartmentsSettingsAction
+} from "../../../../store";
+import {catchErrorTo} from "../../../../util";
+import {parseDepartmentTableFromCsv, reduceDepartmentTableToCsv} from "../util";
+
+@Component({
+ selector: "app-departments-settings",
+ templateUrl: "./departments-settings.component.html",
+ styleUrls: ["./departments-settings.component.scss"]
+})
+export class DepartmentsSettingsComponent implements OnInit, OnDestroy {
+
+ public currentConfigFileName = "departments.config.csv";
+
+ public loading$ = this.store.pipe(select(getSettingsLoadingSelector));
+
+ public searching = false;
+
+ public currentData$ = this.store.pipe(select(getDepartmentsSettingsSelector));
+
+ public currentConfigFile$: Observable<File> = this.currentData$.pipe(
+ switchMap((data) => {
+ return of(data).pipe(
+ map(() => new File([reduceDepartmentTableToCsv(data)], this.currentConfigFileName)),
+ catchErrorTo(null)
+ );
+ })
+ );
+
+ public currentFileUrl: SafeUrl;
+
+ public selectedFile: File;
+
+ public selectedFileData: IAPIDepartmentTable;
+
+ private currentFileObjectUrl: string;
+
+ private destroy$ = new Subject();
+
+ public constructor(public store: Store, @Inject(URL_TOKEN) public url: typeof URL, public sanitizer: DomSanitizer) {
+
+ }
+
+ public ngOnInit() {
+ this.currentConfigFile$.pipe(takeUntil(this.destroy$)).subscribe((file) => this.createObjectURLForCurrentFile(file));
+ this.currentData$.pipe(takeUntil(this.destroy$)).subscribe(() => this.clearFileSelection());
+ this.store.dispatch(fetchDepartmentsSettingsAction());
+ }
+
+ public ngOnDestroy() {
+ this.destroy$.next();
+ this.destroy$.complete();
+ this.revokeObjectUrl();
+ }
+
+ public submit(data: IAPIDepartmentTable) {
+ this.store.dispatch(submitDepartmentsSettingsAction({data}));
+ }
+
+ public async selectFile(file: File) {
+ try {
+ const text = await file.text();
+ this.selectedFileData = parseDepartmentTableFromCsv(text);
+ this.selectedFile = file;
+ } catch (e) {
+ this.store.dispatch(setErrorAction({error: EErrorCode.INVALID_FILE_FORMAT}));
+ }
+ }
+
+ public clearFileSelection() {
+ this.selectedFile = null;
+ this.selectedFileData = null;
+ }
+
+ private createObjectURLForCurrentFile(file: File) {
+ this.revokeObjectUrl();
+ if (file != null) {
+ this.currentFileObjectUrl = this.url.createObjectURL(file);
+ this.currentFileUrl = this.sanitizer.bypassSecurityTrustUrl(this.currentFileObjectUrl);
+ }
+ }
+
+ private revokeObjectUrl() {
+ if (this.currentFileObjectUrl != null) {
+ this.url.revokeObjectURL(this.currentFileObjectUrl);
+ this.currentFileObjectUrl = null;
+ this.currentFileUrl = null;
+ }
+ }
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/departments/components/index.ts
similarity index 81%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/departments/components/index.ts
index 990bb42..1e625fb 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/departments/components/index.ts
@@ -11,4 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./department-table";
+export * from "./departments-search";
+
+export * from "./departments-settings.component";
diff --git a/src/app/features/settings/departments/departments-settings.module.ts b/src/app/features/settings/departments/departments-settings.module.ts
new file mode 100644
index 0000000..8567c49
--- /dev/null
+++ b/src/app/features/settings/departments/departments-settings.module.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 {CdkTableModule} from "@angular/cdk/table";
+import {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {MatIconModule} from "@angular/material/icon";
+import {TranslateModule} from "@ngx-translate/core";
+import {ActionButtonModule} from "../../../shared/layout/action-button";
+import {CollapsibleModule} from "../../../shared/layout/collapsible";
+import {ListModule} from "../../../shared/layout/list";
+import {PaginationCounterModule} from "../../../shared/layout/pagination-counter";
+import {SearchbarModule} from "../../../shared/layout/searchbar";
+import {SideMenuModule} from "../../../shared/layout/side-menu";
+import {SharedPipesModule} from "../../../shared/pipes";
+import {SharedSettingsModule} from "../shared";
+import {DepartmentsSettingsComponent, DepartmentsSettingsSearchComponent, DepartmentsSettingsTableComponent} from "./components";
+import {SectorsFromDepartmentPipe} from "./pipes";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ TranslateModule,
+ CdkTableModule,
+ MatIconModule,
+
+ ActionButtonModule,
+ CollapsibleModule,
+ ListModule,
+ PaginationCounterModule,
+ SearchbarModule,
+ SharedPipesModule,
+ SharedSettingsModule,
+ SideMenuModule
+ ],
+ declarations: [
+ DepartmentsSettingsTableComponent,
+ DepartmentsSettingsSearchComponent,
+ DepartmentsSettingsComponent,
+
+ SectorsFromDepartmentPipe
+ ],
+ exports: [
+ DepartmentsSettingsTableComponent,
+ DepartmentsSettingsSearchComponent,
+ DepartmentsSettingsComponent,
+
+ SectorsFromDepartmentPipe
+ ]
+})
+export class DepartmentsSettingsModule {
+
+}
diff --git a/src/app/shared/leaflet/index.ts b/src/app/features/settings/departments/index.ts
similarity index 84%
copy from src/app/shared/leaflet/index.ts
copy to src/app/features/settings/departments/index.ts
index 849c149..cc40d2e 100644
--- a/src/app/shared/leaflet/index.ts
+++ b/src/app/features/settings/departments/index.ts
@@ -11,9 +11,8 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./directives";
+export * from "./components";
export * from "./pipes";
export * from "./util";
-export * from "./leaflet.module";
-export * from "./leaflet-configuration.token";
+export * from "./departments-settings.module";
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/departments/pipes/index.ts
similarity index 90%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/departments/pipes/index.ts
index 990bb42..c88fbb8 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/departments/pipes/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./sectors-from-department-object.pipe";
diff --git a/src/app/features/settings/departments/pipes/sectors-from-department-object.pipe.spec.ts b/src/app/features/settings/departments/pipes/sectors-from-department-object.pipe.spec.ts
new file mode 100644
index 0000000..c3479d2
--- /dev/null
+++ b/src/app/features/settings/departments/pipes/sectors-from-department-object.pipe.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 {IAPIDepartmentTable} from "../../../../core/api/settings";
+import {SectorsFromDepartmentPipe} from "./sectors-from-department-object.pipe";
+
+describe("SectorsFromDepartmentPipe", () => {
+
+ const pipe = new SectorsFromDepartmentPipe();
+
+ const departmentTable: IAPIDepartmentTable = {
+ "Ort#Ortsteil": {
+ provides: [
+ "Gas",
+ "Strom",
+ "Wasser"
+ ],
+ departments: {
+ Allgemein: [
+ "Medianet",
+ "Planung"
+ ],
+ Weiteres: [
+ "Planung",
+ "Strom Beratung"
+ ]
+ }
+ },
+ "Zweiter#Ort": {
+ provides: [
+ "Beleuchtung"
+ ],
+ departments: {}
+ }
+ };
+
+ describe("transform", () => {
+
+ it("should return sectors from IAPIDepartment table object as a single string sorted alphabetically", () => {
+ let result = pipe.transform(departmentTable);
+ expect(result).toEqual(["Beleuchtung", "Gas", "Strom", "Wasser"]);
+
+ result = pipe.transform(null);
+ expect(result).toEqual([]);
+
+ result = pipe.transform(undefined);
+ expect(result).toEqual([]);
+
+ result = pipe.transform({});
+ expect(result).toEqual([]);
+ });
+ });
+});
+
diff --git a/src/app/features/settings/departments/pipes/sectors-from-department-object.pipe.ts b/src/app/features/settings/departments/pipes/sectors-from-department-object.pipe.ts
new file mode 100644
index 0000000..7d7d60d
--- /dev/null
+++ b/src/app/features/settings/departments/pipes/sectors-from-department-object.pipe.ts
@@ -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
+ ********************************************************************************/
+
+import {Pipe, PipeTransform} from "@angular/core";
+import {IAPIDepartmentTable} from "../../../../core/api/settings";
+import {arrayJoin, filterDistinctValues, objectToArray} from "../../../../util/store";
+
+/**
+ * From a IAPIDepartmentTable object returns a string with all distinct sectors in all department groups.
+ */
+
+@Pipe({
+ name: "appSectorsFromDepartment"
+})
+export class SectorsFromDepartmentPipe implements PipeTransform {
+
+ public transform(table: IAPIDepartmentTable): string[] {
+ const sectors: string[] = arrayJoin(
+ ...objectToArray(table).map((entry) => entry.value.provides)
+ );
+ return filterDistinctValues(sectors).sort();
+ }
+
+}
diff --git a/src/app/features/settings/departments/util/departments.util.spec.ts b/src/app/features/settings/departments/util/departments.util.spec.ts
new file mode 100644
index 0000000..c80691b
--- /dev/null
+++ b/src/app/features/settings/departments/util/departments.util.spec.ts
@@ -0,0 +1,55 @@
+/********************************************************************************
+ * 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 {IAPIDepartmentTable} from "../../../../core/api/settings";
+import {parseDepartmentTableFromCsv, reduceDepartmentTableToCsv} from "./departments.util";
+
+describe("DepartmentsUtil", () => {
+ const text = "City;District;Sectors;Departments;;;;;;;;;;" +
+ "\r\n" +
+ "Darmstadt;Darmstadt;Strom,Gas,Wasser,Straßenbeleuchtung;" +
+ "Allgemein;Gas;Allgemein;Wasser;Regionalstelle Darmstadt;Gas (Betrieb);Regionalstelle Darmstadt;Gas (Planung/Bau);" +
+ "Regionalstelle Darmstadt;Strom (Planung/Bau);Regionalstelle Darmstadt;Strom (Betrieb)" +
+ "\r\n" +
+ "Heppenheim;Heppenheim;Strom,Gas,Straßenbeleuchtung;" +
+ "Allgemein;Gas;Allgemein;Wasser;Regionalstelle Heppenheim;Gas (Betrieb);Regionalstelle Heppenheim;Gas (Planung/Bau);" +
+ "Regionalstelle Heppenheim;Strom (Planung/Bau);;";
+
+ const data: IAPIDepartmentTable = {
+ "Darmstadt#Darmstadt": {
+ provides: ["Strom", "Gas", "Wasser", "Straßenbeleuchtung"],
+ departments: {
+ Allgemein: ["Gas", "Wasser"],
+ "Regionalstelle Darmstadt": ["Gas (Betrieb)", "Gas (Planung/Bau)", "Strom (Planung/Bau)", "Strom (Betrieb)"]
+ }
+ },
+ "Heppenheim#Heppenheim": {
+ provides: ["Strom", "Gas", "Straßenbeleuchtung"],
+ departments: {
+ Allgemein: ["Gas", "Wasser"],
+ "Regionalstelle Heppenheim": ["Gas (Betrieb)", "Gas (Planung/Bau)", "Strom (Planung/Bau)"]
+ }
+ }
+ };
+
+ it("parseDepartmentTableFromCsv", () => {
+ expect(parseDepartmentTableFromCsv(text + "\r\n;;;;;;" + "\r\n")).toEqual(data);
+ expect(() => parseDepartmentTableFromCsv("\r\na;b;c")).not.toThrow();
+ expect(() => parseDepartmentTableFromCsv("\r\na;b")).toThrow();
+ });
+
+ it("reduceDepartmentTableToCsv", () => {
+ expect(reduceDepartmentTableToCsv(data)).toBe(text);
+ });
+
+});
diff --git a/src/app/features/settings/departments/util/departments.util.ts b/src/app/features/settings/departments/util/departments.util.ts
new file mode 100644
index 0000000..a76d7ec
--- /dev/null
+++ b/src/app/features/settings/departments/util/departments.util.ts
@@ -0,0 +1,59 @@
+/********************************************************************************
+ * 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 {IAPIDepartmentTable} from "../../../../core";
+import {arrayJoin, parseCsv, reduceRowToCsv, reduceToCsv} from "../../../../util";
+
+export function parseDepartmentTableFromCsv(text: string): IAPIDepartmentTable {
+ return parseCsv(text, ";").slice(1)
+ .map((row) => row.map((entry) => entry.trim()))
+ .filter((row) => row.length > 1)
+ .reduce<IAPIDepartmentTable>((result, value) => {
+ if (value.length < 3) {
+ throw new Error("Invalid format");
+ }
+ if (value.slice(0, 2).some((_) => _.length === 0)) {
+ return result;
+ }
+ const key: string = value[0] + "#" + value[1];
+ const provides: IAPIDepartmentTable[""]["provides"] = value[2].split(",").filter((sector) => sector.length > 0);
+ const departments: IAPIDepartmentTable[""]["departments"] = value.slice(3)
+ .reduce((dept, groupName, index, array) => {
+ const name: string = array[index + 1];
+ return index % 2 !== 0 || [groupName, name].some((_) => _.length === 0) ? dept : {
+ ...dept,
+ [groupName]: arrayJoin(dept[groupName], [name])
+ };
+ }, {});
+ return {
+ ...result,
+ [key]: {provides, departments}
+ };
+ }, {});
+}
+
+
+export function reduceDepartmentTableToCsv(table: IAPIDepartmentTable) {
+ const headers = ["City", "District", "Sectors", "Departments"];
+ const data: string[][] = Object.entries(table).map(([cityDistrict, entry]) => {
+ return arrayJoin(
+ [cityDistrict.replace("#", ";")],
+ [reduceRowToCsv(entry.provides, ",")],
+ ...Object.entries(entry.departments)
+ .map<string[]>(([groupName, names]) => {
+ return arrayJoin(...names.map((name) => [groupName, name]));
+ })
+ );
+ });
+ return reduceToCsv([headers, ...data], ";");
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/departments/util/index.ts
similarity index 93%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/departments/util/index.ts
index 990bb42..b04e96f 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/departments/util/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./departments.util";
diff --git a/src/app/features/settings/documents/components/documents-settings.component.html b/src/app/features/settings/documents/components/documents-settings.component.html
new file mode 100644
index 0000000..d2c1def
--- /dev/null
+++ b/src/app/features/settings/documents/components/documents-settings.component.html
@@ -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
+ -------------------------------------------------------------------------------->
+
+<app-settings-side-menu
+ [appForAdmin]="true">
+</app-settings-side-menu>
+
+<app-side-menu-status *appSideMenu="'center'"
+ [appLoadingMessage]="'core.loading' | translate"
+ [appLoading]="loading$ | async">
+</app-side-menu-status>
+
+<ng-container *appSideMenu="'bottom'">
+ <app-action-button (appClick)="save()" [appIcon]="'publish'" class="openk-info openk-success side-menu-button">
+ {{"settings.documents.save" | translate}}
+ </app-action-button>
+</ng-container>
+
+<div class="title">
+ <span class="title--label">
+ {{"settings.title" | translate}} - {{"settings.documents.title" | translate}}
+ </span>
+</div>
+
+<app-collapsible
+ [appTitle]="'settings.documents.tags' | translate">
+
+ <div style="padding: 1em;">
+
+ <div class="add-tag">
+ <span class="add-tag--title">{{"settings.documents.tag" | translate}}</span>
+ <input #inputElement
+ (keydown.enter)="addTag(inputElement.value); inputElement.value = '';"
+ [placeholder]="'settings.documents.placeholder' | translate"
+ [disabled]="loading$| async"
+ class="openk-input add-tag--input"/>
+
+ <button (click)="addTag(inputElement.value); inputElement.value = '';"
+ [disabled]="loading$| async"
+ class="openk-button openk-button-icon list-select--button add-tag--btn">
+ <mat-icon>add</mat-icon>
+ </button>
+
+ </div>
+
+ <app-list (appDelete)="deleteTag($event)"
+ [appListItems]="tagList"
+ [appTitle]="'settings.documents.listTitle' | translate">
+ </app-list>
+ </div>
+</app-collapsible>
+
diff --git a/src/app/shared/leaflet/components/leaflet-map.component.scss b/src/app/features/settings/documents/components/documents-settings.component.scss
similarity index 61%
copy from src/app/shared/leaflet/components/leaflet-map.component.scss
copy to src/app/features/settings/documents/components/documents-settings.component.scss
index e8e1a36..6587388 100644
--- a/src/app/shared/leaflet/components/leaflet-map.component.scss
+++ b/src/app/features/settings/documents/components/documents-settings.component.scss
@@ -11,43 +11,49 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "openk.styles";
-
:host {
width: 100%;
- height: 100%;
-
+ padding: 1em;
+ box-sizing: border-box;
display: flex;
flex-flow: column;
+ max-width: 70em;
+ margin: 0 auto 0 auto;
}
-.map {
- height: 100%;
+.title {
+ margin-bottom: 1em;
+}
+
+.title--label {
+ font-size: x-large;
+ font-weight: 600;
+}
+
+.side-menu-button {
width: 100%;
- position: relative;
- overflow: hidden;
- box-sizing: border-box;
- border: 1px solid $openk-form-border;
+
+ & + & {
+ margin-top: 1em;
+ }
}
-.map--leaflet {
+.add-tag {
+ display: inline-flex;
+ align-items: center;
+ margin-bottom: 1em;
width: 100%;
- height: 100%;
}
-.map--button {
- display: block;
- width: fit-content;
- height: fit-content;
- position: absolute;
- bottom: 10px;
- left: 10px;
- z-index: 1000;
+.add-tag--title {
+ margin-right: 0.5em;
}
-.sub-caption {
- color: $openk-form-border;
- margin-left: auto;
- font-size: smaller;
- font-style: italic;
+.add-tag--input {
+ margin-right: 0.3em;
+ flex: 1;
+}
+
+.add-tag--btn {
+ font-size: 0.8em;
}
diff --git a/src/app/features/settings/documents/components/documents-settings.component.spec.ts b/src/app/features/settings/documents/components/documents-settings.component.spec.ts
new file mode 100644
index 0000000..aca9aa0
--- /dev/null
+++ b/src/app/features/settings/documents/components/documents-settings.component.spec.ts
@@ -0,0 +1,88 @@
+/********************************************************************************
+ * 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 {Store} from "@ngrx/store";
+import {provideMockStore} from "@ngrx/store/testing";
+import {I18nModule} from "../../../../core";
+import {submitTagsAction} from "../../../../store/statements/actions";
+import {DocumentsSettingsModule} from "../documents.settings.module";
+import {DocumentsSettingsComponent} from "./documents-settings.component";
+
+describe("TagSettingsComponent", () => {
+ let component: DocumentsSettingsComponent;
+ let fixture: ComponentFixture<DocumentsSettingsComponent>;
+ let store: Store;
+
+ const tags = [
+ {label: "label", add: true},
+ {label: "label1"},
+ {label: "label2"},
+ {label: "label3", add: true}
+ ];
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ DocumentsSettingsModule,
+ RouterTestingModule,
+ I18nModule
+ ],
+ providers: [
+ provideMockStore()
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DocumentsSettingsComponent);
+ component = fixture.componentInstance;
+ store = fixture.componentRef.injector.get(Store);
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should add string to taglist if set", () => {
+ expect(component.tagList).toEqual([]);
+ component.addTag(null);
+ expect(component.tagList).toEqual([]);
+ component.addTag("");
+ expect(component.tagList).toEqual([]);
+ component.addTag("test");
+ expect(component.tagList.length).toEqual(1);
+ expect(component.tagList).toEqual([{label: "test", add: true}]);
+ });
+
+ it("should delete the specified entry from taglist", () => {
+ component.tagList = tags;
+ component.deleteTag(14);
+ expect(component.tagList).toEqual(tags);
+ component.deleteTag(2);
+ expect(component.tagList).toEqual([
+ {label: "label", add: true},
+ {label: "label1"},
+ {label: "label3", add: true}
+ ]);
+ });
+
+ it("should dispatch submitTagsAction with labels of tags to be added", async () => {
+ const dispatchSpy = spyOn(store, "dispatch");
+ component.tagList = tags;
+ await component.save();
+ expect(dispatchSpy).toHaveBeenCalledWith(submitTagsAction({labels: ["label", "label3"]}));
+ });
+});
diff --git a/src/app/features/settings/documents/components/documents-settings.component.ts b/src/app/features/settings/documents/components/documents-settings.component.ts
new file mode 100644
index 0000000..410dd78
--- /dev/null
+++ b/src/app/features/settings/documents/components/documents-settings.component.ts
@@ -0,0 +1,68 @@
+/********************************************************************************
+ * 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, OnDestroy, OnInit} from "@angular/core";
+import {select, Store} from "@ngrx/store";
+import {Subject} from "rxjs";
+import {map, takeUntil} from "rxjs/operators";
+import {getAttachmentTagsSelector, getSettingsLoadingSelector, submitTagsAction} from "../../../../store";
+
+@Component({
+ selector: "app-tag-settings",
+ templateUrl: "./documents-settings.component.html",
+ styleUrls: ["./documents-settings.component.scss"]
+})
+export class DocumentsSettingsComponent implements OnInit, OnDestroy {
+
+ public tags$ = this.store.pipe(select(getAttachmentTagsSelector)).pipe(
+ map((tags) => tags.map((_) => _.label))
+ );
+
+ public loading$ = this.store.pipe(select(getSettingsLoadingSelector));
+
+ public tagList: { label: string, add?: boolean }[] = [];
+
+ private destroy$ = new Subject();
+
+ public constructor(private readonly store: Store) {
+ }
+
+ public ngOnInit() {
+ this.tags$.pipe(
+ takeUntil(this.destroy$)
+ ).subscribe((tags) => {
+ this.tagList = tags.map((_) => ({label: _}));
+ });
+ }
+
+ public async ngOnDestroy() {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ public addTag(tagLabel: string) {
+ if (tagLabel) {
+ this.tagList.push({label: tagLabel, add: true});
+ }
+ }
+
+ public deleteTag(index: number) {
+ this.tagList.splice(index, 1);
+ }
+
+ public save() {
+ const labels = this.tagList.filter((tag) => tag.add != null).map((_) => _.label);
+ this.store.dispatch(submitTagsAction({labels}));
+ }
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/documents/components/index.ts
similarity index 91%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/documents/components/index.ts
index 990bb42..980f050 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/documents/components/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./documents-settings.component";
diff --git a/src/app/features/settings/documents/documents.settings.module.ts b/src/app/features/settings/documents/documents.settings.module.ts
new file mode 100644
index 0000000..fc71a7c
--- /dev/null
+++ b/src/app/features/settings/documents/documents.settings.module.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 {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {MatIconModule} from "@angular/material/icon";
+import {TranslateModule} from "@ngx-translate/core";
+import {ActionButtonModule} from "../../../shared/layout/action-button";
+import {CollapsibleModule} from "../../../shared/layout/collapsible";
+import {ListModule} from "../../../shared/layout/list";
+import {SideMenuModule} from "../../../shared/layout/side-menu";
+import {SharedSettingsModule} from "../shared";
+import {DocumentsSettingsComponent} from "./components";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ TranslateModule,
+ MatIconModule,
+
+ ActionButtonModule,
+ CollapsibleModule,
+ SideMenuModule,
+ SharedSettingsModule,
+ ListModule
+ ],
+ declarations: [
+ DocumentsSettingsComponent
+ ],
+ exports: [
+ DocumentsSettingsComponent
+ ]
+})
+export class DocumentsSettingsModule {
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/documents/index.ts
similarity index 88%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/documents/index.ts
index 990bb42..58635e5 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/documents/index.ts
@@ -11,4 +11,5 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./components";
+export * from "./documents.settings.module";
diff --git a/src/app/features/settings/index.ts b/src/app/features/settings/index.ts
index d659e68..dd97a22 100644
--- a/src/app/features/settings/index.ts
+++ b/src/app/features/settings/index.ts
@@ -11,4 +11,6 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./settings.module";
+
+export * from "./departments/util";
+
diff --git a/src/app/features/settings/settings-routing.module.ts b/src/app/features/settings/settings-routing.module.ts
index 62871ef..aa21f87 100644
--- a/src/app/features/settings/settings-routing.module.ts
+++ b/src/app/features/settings/settings-routing.module.ts
@@ -13,22 +13,50 @@
import {NgModule} from "@angular/core";
import {RouterModule, Routes} from "@angular/router";
-import {OfficialInChargeOrAdminRouteGuardService} from "../../store/root/services";
-import {SettingsComponent} from "./components";
-import {SettingsModule} from "./settings.module";
+import {AdminRouteGuardService, OfficialInChargeOrAdminRouteGuardService} from "../../store";
+import {DepartmentsSettingsComponent, DepartmentsSettingsModule} from "./departments";
+import {DocumentsSettingsComponent, DocumentsSettingsModule} from "./documents";
+import {TextBlockSettingsModule, TextBlocksSettingsComponent} from "./text-blocks";
+import {UsersSettingsComponent, UsersSettingsModule} from "./users";
const routes: Routes = [
{
path: "",
pathMatch: "full",
- component: SettingsComponent,
+ redirectTo: "text-blocks"
+ },
+ {
+ path: "departments",
+ pathMatch: "full",
+ component: DepartmentsSettingsComponent,
+ canActivate: [AdminRouteGuardService]
+ },
+ {
+ path: "documents",
+ pathMatch: "full",
+ component: DocumentsSettingsComponent,
+ canActivate: [AdminRouteGuardService]
+ },
+ {
+ path: "text-blocks",
+ pathMatch: "full",
+ component: TextBlocksSettingsComponent,
canActivate: [OfficialInChargeOrAdminRouteGuardService]
+ },
+ {
+ path: "users",
+ pathMatch: "full",
+ component: UsersSettingsComponent,
+ canActivate: [AdminRouteGuardService]
}
];
@NgModule({
imports: [
- SettingsModule,
+ DepartmentsSettingsModule,
+ DocumentsSettingsModule,
+ TextBlockSettingsModule,
+ UsersSettingsModule,
RouterModule.forChild(routes)
],
exports: [
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/shared/components/index.ts
similarity index 91%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/shared/components/index.ts
index 990bb42..d98e4bc 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/shared/components/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./settings-side-menu.component";
diff --git a/src/app/features/settings/components/search/settings.component.html b/src/app/features/settings/shared/components/settings-side-menu.component.html
similarity index 60%
rename from src/app/features/settings/components/search/settings.component.html
rename to src/app/features/settings/shared/components/settings-side-menu.component.html
index 0bc2dce..68fabde 100644
--- a/src/app/features/settings/components/search/settings.component.html
+++ b/src/app/features/settings/shared/components/settings-side-menu.component.html
@@ -11,6 +11,13 @@
* SPDX-License-Identifier: EPL-2.0
-------------------------------------------------------------------------------->
-<div style="padding: 1em;">
- Not yet implemented.
-</div>
+<ng-container *appSideMenu="'top'">
+ <app-action-button
+ *ngFor="let item of appForAdmin ? adminEntries : defaultEntries"
+ [appIcon]="item.icon"
+ [appRouterLink]="item.routerLink"
+ [class.openk-info]="!router.url?.startsWith(item.routerLink)"
+ class="side-menu-button openk-primary">
+ {{item.label | translate}}
+ </app-action-button>
+</ng-container>
diff --git a/src/app/shared/controls/map-select/components/map-select.component.scss b/src/app/features/settings/shared/components/settings-side-menu.component.scss
similarity index 83%
copy from src/app/shared/controls/map-select/components/map-select.component.scss
copy to src/app/features/settings/shared/components/settings-side-menu.component.scss
index 7e59f1f..d765276 100644
--- a/src/app/shared/controls/map-select/components/map-select.component.scss
+++ b/src/app/features/settings/shared/components/settings-side-menu.component.scss
@@ -11,10 +11,18 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "openk.styles";
-
:host {
- display: block;
+ display: none;
+}
+
+.side-menu-button {
width: 100%;
- height: 100%;
+
+ & + & {
+ margin-top: 1em;
+ }
+}
+
+.info-message {
+ font-style: italic;
}
diff --git a/src/app/features/settings/components/search/settings.component.spec.ts b/src/app/features/settings/shared/components/settings-side-menu.component.spec.ts
similarity index 60%
copy from src/app/features/settings/components/search/settings.component.spec.ts
copy to src/app/features/settings/shared/components/settings-side-menu.component.spec.ts
index 2bc2d9f..e868868 100644
--- a/src/app/features/settings/components/search/settings.component.spec.ts
+++ b/src/app/features/settings/shared/components/settings-side-menu.component.spec.ts
@@ -12,20 +12,27 @@
********************************************************************************/
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
-import {SettingsComponent} from "./settings.component";
+import {RouterTestingModule} from "@angular/router/testing";
+import {I18nModule} from "../../../../core";
+import {SharedSettingsModule} from "../shared-settings.module";
+import {SettingsSideMenuComponent} from "./settings-side-menu.component";
-describe("SettingsComponent", () => {
- let component: SettingsComponent;
- let fixture: ComponentFixture<SettingsComponent>;
+describe("SettingsSideMenuComponent", () => {
+ let component: SettingsSideMenuComponent;
+ let fixture: ComponentFixture<SettingsSideMenuComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
- declarations: [SettingsComponent]
+ imports: [
+ SharedSettingsModule,
+ RouterTestingModule,
+ I18nModule
+ ]
}).compileComponents();
}));
beforeEach(() => {
- fixture = TestBed.createComponent(SettingsComponent);
+ fixture = TestBed.createComponent(SettingsSideMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
diff --git a/src/app/features/settings/shared/components/settings-side-menu.component.ts b/src/app/features/settings/shared/components/settings-side-menu.component.ts
new file mode 100644
index 0000000..f5beedb
--- /dev/null
+++ b/src/app/features/settings/shared/components/settings-side-menu.component.ts
@@ -0,0 +1,68 @@
+/********************************************************************************
+ * 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 {Router} from "@angular/router";
+
+interface ISettingsSideMenuEntry {
+ label: string;
+ icon: string;
+ routerLink: string;
+}
+
+@Component({
+ selector: "app-settings-side-menu",
+ templateUrl: "./settings-side-menu.component.html",
+ styleUrls: ["./settings-side-menu.component.scss"]
+})
+export class SettingsSideMenuComponent {
+
+ @Input()
+ public appForAdmin: boolean;
+
+ public defaultEntries: ISettingsSideMenuEntry[] = [
+ {
+ label: "settings.textBlocks.title",
+ icon: "subject",
+ routerLink: "/settings/text-blocks"
+ }
+ ];
+
+ public adminEntries: ISettingsSideMenuEntry[] = [
+ {
+ label: "settings.textBlocks.title",
+ icon: "subject",
+ routerLink: "/settings/text-blocks"
+ },
+ {
+ label: "settings.documents.title",
+ icon: "description",
+ routerLink: "/settings/documents"
+ },
+ {
+ label: "settings.departments.title",
+ icon: "work",
+ routerLink: "/settings/departments"
+ },
+ {
+ label: "settings.users.title",
+ icon: "supervisor_account",
+ routerLink: "/settings/users"
+ }
+ ];
+
+ constructor(public router: Router) {
+
+ }
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/shared/index.ts
similarity index 89%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/shared/index.ts
index 990bb42..b5cf4ef 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/shared/index.ts
@@ -11,4 +11,5 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./shared-settings.module";
+export * from "./pipes";
diff --git a/src/app/features/settings/shared/pipes/get-department-groups-from-table.pipe.ts b/src/app/features/settings/shared/pipes/get-department-groups-from-table.pipe.ts
new file mode 100644
index 0000000..b41784f
--- /dev/null
+++ b/src/app/features/settings/shared/pipes/get-department-groups-from-table.pipe.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 {Pipe, PipeTransform} from "@angular/core";
+import {IAPIDepartmentGroups, IAPIDepartmentTable} from "../../../../core/api/settings";
+import {arrayJoin, filterDistinctValues} from "../../../../util/store";
+
+@Pipe({name: "getDepartmentGroupsFromTablePipe"})
+export class GetDepartmentGroupsFromTablePipe implements PipeTransform {
+
+ public transform(table: IAPIDepartmentTable): IAPIDepartmentGroups {
+ return Object.values({...table})
+ .map((entry) => entry.departments)
+ .reduce((current, departments) => {
+ current = {...departments, ...current};
+ Object.entries(current).forEach(([groupName, groups]) => {
+ current[groupName] = filterDistinctValues(arrayJoin(groups, departments[groupName])).sort();
+ });
+ return current;
+ }, {});
+ }
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/shared/pipes/index.ts
similarity index 90%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/shared/pipes/index.ts
index 990bb42..22a3b41 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/shared/pipes/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./get-department-groups-from-table.pipe";
diff --git a/src/app/features/settings/shared/shared-settings.module.ts b/src/app/features/settings/shared/shared-settings.module.ts
new file mode 100644
index 0000000..0458146
--- /dev/null
+++ b/src/app/features/settings/shared/shared-settings.module.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 {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {TranslateModule} from "@ngx-translate/core";
+import {ActionButtonModule} from "../../../shared/layout/action-button";
+import {SideMenuModule} from "../../../shared/layout/side-menu";
+import {SettingsSideMenuComponent} from "./components";
+import {GetDepartmentGroupsFromTablePipe} from "./pipes";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ TranslateModule,
+
+ ActionButtonModule,
+ SideMenuModule
+ ],
+ declarations: [
+ SettingsSideMenuComponent,
+
+ GetDepartmentGroupsFromTablePipe
+ ],
+ exports: [
+ SettingsSideMenuComponent,
+
+ GetDepartmentGroupsFromTablePipe
+ ]
+})
+export class SharedSettingsModule {
+
+}
+
diff --git a/src/app/features/settings/text-blocks/TextBlockSettingsForm.ts b/src/app/features/settings/text-blocks/TextBlockSettingsForm.ts
new file mode 100644
index 0000000..b606fdd
--- /dev/null
+++ b/src/app/features/settings/text-blocks/TextBlockSettingsForm.ts
@@ -0,0 +1,294 @@
+/********************************************************************************
+ * 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 {AbstractControl, FormArray, FormControl, FormGroup} from "@angular/forms";
+import {IAPITextBlockConfigurationModel, IAPITextBlockGroupModel, IAPITextBlockModel} from "../../../core/api/text";
+import {createFormGroup} from "../../../util/forms";
+import {arrayJoin, filterDistinctValues} from "../../../util/store";
+
+export class TextBlockSettingsForm {
+
+ public selects = new FormGroup({});
+
+ public groups = new FormArray([]);
+
+ public negativeGroups = new FormArray([]);
+
+ public formGroup = createFormGroup<IAPITextBlockConfigurationModel>({
+ selects: this.selects,
+ groups: this.groups,
+ negativeGroups: this.negativeGroups
+ });
+
+ private defaultSelectEntry = "";
+
+ private defaultTextBlockModel: IAPITextBlockModel = {id: "", text: "", excludes: [], requires: []};
+
+ private defaultTextBlockGroupModel: IAPITextBlockGroupModel = {groupName: "", textBlocks: []};
+
+ public disable(disabled: boolean) {
+ if (disabled) {
+ this.formGroup.disable();
+ } else {
+ this.formGroup.enable();
+ }
+ }
+
+ public getValue(): IAPITextBlockConfigurationModel {
+ return this.formGroup.value;
+ }
+
+ public setForm(value: IAPITextBlockConfigurationModel) {
+ const disabled = this.formGroup.disabled;
+
+ Object.keys(this.selects.controls).forEach((key) => this.removeSelect(key));
+ Object.entries(value.selects).forEach(([key, options]) => this.addSelect(key, options));
+
+ this.groups.clear();
+ value.groups.forEach((group) => {
+ if (group.textBlocks?.length > 0) {
+ this.groups.push(this.createTextBlockGroupControl(group));
+ } else {
+ this.groups.push(this.createTextBlockGroupControl({...group, textBlocks: [this.defaultTextBlockModel]}));
+ }
+ });
+
+ this.negativeGroups.clear();
+ value.negativeGroups.forEach((group) => {
+ this.negativeGroups.push(this.createTextBlockGroupControl(group));
+ });
+
+ this.changeTextBlockReferences();
+ this.insertObjectsForEmptyGroups(false);
+ this.insertObjectsForEmptyGroups(true);
+ this.disable(disabled);
+ }
+
+
+ public getSelectKeys(): string[] {
+ return Object.keys(this.selects.value);
+ }
+
+ public getSelectEntries(key: string): string[] {
+ return this.getValue().selects[key];
+ }
+
+ public getSelectControl(key: string): FormArray {
+ const control = this.selects.get([key]);
+ return control instanceof FormArray ? control : undefined;
+ }
+
+ public addSelect(key: string, entries?: string[]) {
+ const disabled = this.formGroup.disabled;
+ this.selects.addControl(key, new FormArray([]));
+ arrayJoin(entries).forEach((entry) => this.addSelectEntry(key, entry));
+ this.disable(disabled);
+ }
+
+ public changeSelectKey(key: string, newKey: string) {
+ const disabled = this.formGroup.disabled;
+ const selectControl = this.getSelectControl(key);
+ if (key === newKey || selectControl == null) {
+ return;
+ }
+
+ this.patchTextBlockValues((value) => ({
+ text: value.text.replace(new RegExp(`<s:${key}>`, "g"), `<s:${newKey}>`)
+ }));
+
+ this.selects.removeControl(key);
+ this.selects.addControl(newKey, selectControl);
+ this.disable(disabled);
+ }
+
+ public removeSelect(key: string) {
+ this.selects.removeControl(key);
+ this.patchTextBlockValues((value) => ({
+ text: value.text.replace(new RegExp(`<s:${key}>`, "g"), "")
+ }));
+ }
+
+ public addSelectEntry(key: string, entry: string) {
+ const disabled = this.formGroup.disabled;
+ this.getSelectControl(key)?.push(this.createSelectEntryControl(entry));
+ this.disable(disabled);
+ }
+
+ public removeSelectEntry(key: string, index: number) {
+ this.getSelectControl(key)?.removeAt(index);
+ }
+
+
+ public getTextBlockGroups(negative?: boolean): FormArray {
+ return negative ? this.negativeGroups : this.groups;
+ }
+
+ public insertGroup(groupIndex: number, negative?: boolean) {
+ const disabled = this.formGroup.disabled;
+ const groups = this.getTextBlockGroups(negative);
+ const groupControl = this.createTextBlockGroupControl();
+ groups.insert(groupIndex, groupControl);
+ this.disable(disabled);
+ }
+
+ public removeGroup(groupIndex: number, negative?: boolean) {
+ const groups = this.getTextBlockGroups(negative);
+ groups.removeAt(groupIndex);
+ this.changeTextBlockReferences();
+ return groups.length;
+ }
+
+
+ public getTextBlockValue(groupIndex: number, textBlockIndex: number, negative?: boolean): IAPITextBlockModel {
+ return this.getTextBlockFormArrayForGroup(groupIndex, negative)?.at(textBlockIndex)?.value;
+ }
+
+ public getTextBlockFormArrayForGroup(groupIndex: number, negative?: boolean): FormArray {
+ const result = this.getTextBlockGroups(negative).at(groupIndex)?.get("textBlocks");
+ return result instanceof FormArray ? result : undefined;
+ }
+
+ public insertTextBlock(groupIndex: number, index: number, negative?: boolean) {
+ const textBlockGroup = this.getTextBlockFormArrayForGroup(groupIndex, negative);
+ if (textBlockGroup != null) {
+ const disabled = this.formGroup.disabled;
+ const control = this.createTextBlockModelControl();
+ textBlockGroup.insert(index, control);
+ this.changeTextBlockReferences();
+ this.disable(disabled);
+ return control;
+ }
+ }
+
+ public moveTextBlockInGroup(groupIndex: number, from: number, to: number, negative?: boolean) {
+ const textBlockGroup = this.getTextBlockFormArrayForGroup(groupIndex, negative);
+ const control = textBlockGroup.get([from]);
+ if (to != null && from !== to && control != null) {
+ textBlockGroup.removeAt(from);
+ textBlockGroup.insert(to, control);
+ this.changeTextBlockReferences();
+ return control;
+ }
+ }
+
+ public removeTextBlock(groupIndex: number, textBlockIndex: number, negative?: boolean) {
+ const group = this.getTextBlockFormArrayForGroup(groupIndex, negative);
+ if (group != null) {
+ group.removeAt(textBlockIndex);
+ this.changeTextBlockReferences(); // This method removes all references to the now deleted text block.
+ return group.length;
+ }
+ }
+
+
+ public compareIds(a: string, b: string): number {
+ const allIds = this.getTextBlockControls().map((control) => control.value?.id);
+ return allIds.indexOf(a) - allIds.indexOf(b);
+ }
+
+ public getTextBlockControls(): AbstractControl[] {
+ return arrayJoin(
+ ...this.groups.controls.map((_, index) => this.getTextBlockFormArrayForGroup(index, false)?.controls),
+ ...this.negativeGroups.controls.map((_, index) => this.getTextBlockFormArrayForGroup(index, true)?.controls)
+ );
+ }
+
+ private changeTextBlockReferences() {
+ const lookup: { [id: string]: string } = {};
+ let offset = 0;
+
+ this.groups.controls.forEach((_, groupIndex) => {
+ offset++;
+ this.getTextBlockFormArrayForGroup(groupIndex).controls.forEach((control, textBlockIndex) => {
+ const id = this.getTextBlockValue(groupIndex, textBlockIndex).id;
+ lookup[id] = `${groupIndex + 1}.${textBlockIndex + 1}`;
+ });
+ });
+
+ this.negativeGroups.controls.forEach((_, groupIndex) => {
+ this.getTextBlockFormArrayForGroup(groupIndex, true).controls.forEach((control, textBlockIndex) => {
+ const id = this.getTextBlockValue(groupIndex, textBlockIndex, true).id;
+ lookup[id] = `${offset + groupIndex + 1}.${textBlockIndex + 1}`;
+ });
+ });
+
+ this.mapAllTextBlockIds((id) => lookup[id]);
+ }
+
+ private createSelectEntryControl(value: string = this.defaultSelectEntry) {
+ return new FormControl(value);
+ }
+
+ private createTextBlockGroupControl(group?: IAPITextBlockGroupModel): FormGroup {
+ group = {
+ ...this.defaultTextBlockGroupModel,
+ ...group
+ };
+
+ return createFormGroup<IAPITextBlockGroupModel>({
+ groupName: new FormControl(group.groupName),
+ textBlocks: group.textBlocks.reduce((formArray, textBlock) => {
+ formArray.push(this.createTextBlockModelControl(textBlock));
+ return formArray;
+ }, new FormArray([]))
+ });
+ }
+
+ private createTextBlockModelControl(textBlock?: IAPITextBlockModel): FormGroup {
+ textBlock = {
+ ...this.defaultTextBlockModel,
+ ...textBlock
+ };
+ return createFormGroup<IAPITextBlockModel>({
+ id: new FormControl(textBlock.id),
+ requires: new FormControl(textBlock.requires),
+ excludes: new FormControl(textBlock.excludes),
+ text: new FormControl(textBlock.text)
+ });
+ }
+
+ private patchTextBlockValues(fn: (value: IAPITextBlockModel, index: number) => Partial<IAPITextBlockModel>) {
+ this.getTextBlockControls().forEach((control, index) => {
+ control.patchValue(fn(control.value, index));
+ });
+ }
+
+ private mapAllTextBlockIds(fn: (id: string) => string | null) {
+ this.patchTextBlockValues((value) => ({
+ id: fn(value.id),
+ excludes: filterDistinctValues(arrayJoin(value.excludes).map(fn)),
+ requires: arrayJoin(value.requires)
+ .map((rule) => ({
+ ...rule,
+ ids: filterDistinctValues(arrayJoin(rule.ids).map(fn))
+ }))
+ .filter((rule) => rule.ids.length > 0)
+ }));
+ }
+
+ private insertObjectsForEmptyGroups(negative: boolean) {
+ const disabled = this.formGroup.disabled;
+ const value = this.getValue()[negative ? "negativeGroups" : "groups"];
+ if (value.length === 0) {
+ this.insertGroup(0, negative);
+ this.insertObjectsForEmptyGroups(negative);
+ }
+ value.forEach((group, groupIndex) => {
+ if (arrayJoin(group.textBlocks).length === 0) {
+ this.insertTextBlock(groupIndex, 0, negative);
+ }
+ });
+ this.disable(disabled);
+ }
+
+}
diff --git a/src/app/shared/leaflet/index.ts b/src/app/features/settings/text-blocks/components/index.ts
similarity index 72%
copy from src/app/shared/leaflet/index.ts
copy to src/app/features/settings/text-blocks/components/index.ts
index 849c149..09cfb15 100644
--- a/src/app/shared/leaflet/index.ts
+++ b/src/app/features/settings/text-blocks/components/index.ts
@@ -11,9 +11,9 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./directives";
-export * from "./pipes";
-export * from "./util";
+export * from "./text-block-form";
+export * from "./text-block-list-form";
+export * from "./text-block-select-form";
-export * from "./leaflet.module";
-export * from "./leaflet-configuration.token";
+export * from "./text-blocks-settings.component";
+export * from "../TextBlockSettingsForm";
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/text-blocks/components/text-block-form/index.ts
similarity index 92%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/text-blocks/components/text-block-form/index.ts
index 990bb42..a3bedcd 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/text-blocks/components/text-block-form/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./text-block-form.component";
diff --git a/src/app/features/settings/text-blocks/components/text-block-form/text-block-form.component.html b/src/app/features/settings/text-blocks/components/text-block-form/text-block-form.component.html
new file mode 100644
index 0000000..eb562d1
--- /dev/null
+++ b/src/app/features/settings/text-blocks/components/text-block-form/text-block-form.component.html
@@ -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
+ -------------------------------------------------------------------------------->
+
+<ng-template #templateRef>
+ {{ "settings.textBlocks.pickTextBlock" | translate }}
+</ng-template>
+
+<ng-container *ngIf="appFormGroup; else templateRef;">
+
+ <div class="text-block--title">
+ <label [for]="id">{{'textBlocks.textBlock' | translate}} {{appFormGroup?.value?.id}}</label>
+ </div>
+
+ <textarea #autoInsertTextField="appAutoInsertTextFieldToken"
+ #textAreaElement
+ (input)="appFormGroup?.get('text')?.patchValue(textAreaElement.value)"
+ [disabled]="appFormGroup?.get('text')?.disabled"
+ [id]="id"
+ [value]="appFormGroup?.get('text')?.value"
+ appAutoInsertTextFieldToken
+ class="openk-textarea text-block--textarea">
+ </textarea>
+
+ <div
+ *ngFor="let buttons of [appTextControlDefaultButtons, appTextControlReplacementButtons, appTextControlSelectButtons]"
+ class="text-block--format">
+ <button (click)="autoInsertTextField.insert(button.token, button.startToken, button?.requireLineBreak)"
+ *ngFor="let button of buttons"
+ [disabled]="appFormGroup.disabled"
+ class="openk-button openk-chip text-block--format--chip">
+ <span *ngIf="button.label" class="text-block--format--chip--label">
+ {{button.label}}
+ </span>
+ <mat-icon *ngIf="button.icon"
+ [class.text-block--format--chip--icon---rotate]="button.rotateIcon"
+ class="text-block--format--chip--icon">
+ {{button.icon}}
+ </mat-icon>
+ </button>
+ </div>
+
+ <div (cdkDropListDropped)="onDrop($event?.item?.data?.id, area)"
+ (cdkDropListEntered)="onEnter($event?.item?.data?.id, area)"
+ (cdkDropListExited)="onExit()"
+ *ngFor="let area of dropAreaConfig; let i = index;"
+ [cdkDropListEnterPredicate]="enterPredicate"
+ [id]="area.elementId"
+ cdkDropList class="drop-area openk-drag-list-with-hidden-placeholder">
+
+ <div class="drop-area--title">
+ {{area?.label | translate}}
+ </div>
+
+ <div class="drop--area--text-blocks">
+
+ <button (click)="removeIdFromArea(id, area)"
+ *ngFor="let id of appFormGroup.value | getRuleIdsOfTextBlock : area.key : area.type"
+ [disabled]="appFormGroup?.disabled"
+ class="openk-button openk-chip drop-area--button">
+ <mat-icon class="drop-area--button--icon">clear</mat-icon>
+ {{"textBlocks.textBlock" | translate }} {{id}}
+ </button>
+
+ <button *ngIf="area === selectedRule?.area" class="openk-button openk-chip drop-area--button"
+ disabled>
+ <mat-icon class="drop-area--button--icon">clear</mat-icon>
+ {{"textBlocks.textBlock" | translate }} {{selectedRule?.id}}
+ </button>
+ </div>
+ </div>
+
+</ng-container>
diff --git a/src/app/features/settings/text-blocks/components/text-block-form/text-block-form.component.scss b/src/app/features/settings/text-blocks/components/text-block-form/text-block-form.component.scss
new file mode 100644
index 0000000..9f5f845
--- /dev/null
+++ b/src/app/features/settings/text-blocks/components/text-block-form/text-block-form.component.scss
@@ -0,0 +1,112 @@
+/********************************************************************************
+ * 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 "src/styles/openk.styles";
+
+:host {
+ display: flex;
+ flex-flow: column;
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+ padding: 1em;
+}
+
+.text-block--title {
+ font-weight: 600;
+ margin-bottom: 0.25em;
+}
+
+.text-block--textarea {
+ resize: none;
+ width: 100%;
+ box-sizing: border-box;
+ min-height: 8.4em;
+ flex: 1 1 100%;
+ height: 10em;
+ overflow-y: auto;
+}
+
+.text-block--format {
+ display: flex;
+ flex-flow: row wrap;
+ margin-top: 2px;
+
+ &:empty {
+ display: none;
+ }
+}
+
+.text-block--format--chip {
+ margin-top: 0.25em;
+ margin-right: 0.25em;
+}
+
+.text-block--format--chip--label {
+ padding-right: 0.2em;
+
+ &:last-child {
+ padding-right: 0;
+ }
+}
+
+.text-block--format--chip--icon {
+ font-size: 1em;
+ height: initial;
+ width: initial;
+ padding: 0;
+ margin: 0;
+}
+
+.text-block--format--chip--icon---rotate {
+ transform: rotate(90deg);
+}
+
+
+.drop-area {
+ @include rounded-border-mixin();
+
+ width: 100%;
+ flex: 1 1 3em;
+ border: 1px dashed $openk-form-border;
+ margin-top: 0.5em;
+ padding: 0.25em;
+ box-sizing: border-box;
+
+ display: flex;
+ flex-flow: column;
+}
+
+.drop-area--title {
+ line-height: 1.25;
+ font-size: x-small;
+ width: 100%;
+}
+
+.drop--area--text-blocks {
+ display: flex;
+ flex-flow: row wrap;
+ width: 100%;
+}
+
+.drop-area--button {
+ margin: 0.25em 0.5em 0.25em 0;
+}
+
+.drop-area--button--icon {
+ font-size: inherit;
+ height: initial;
+ width: initial;
+ line-height: 1;
+ padding-right: 0.25em;
+}
diff --git a/src/app/features/settings/text-blocks/components/text-block-form/text-block-form.component.spec.ts b/src/app/features/settings/text-blocks/components/text-block-form/text-block-form.component.spec.ts
new file mode 100644
index 0000000..40cdd43
--- /dev/null
+++ b/src/app/features/settings/text-blocks/components/text-block-form/text-block-form.component.spec.ts
@@ -0,0 +1,98 @@
+/********************************************************************************
+ * 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 {CdkDrag} from "@angular/cdk/drag-drop";
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {FormGroup} from "@angular/forms";
+import {I18nModule, IAPITextBlockModel} from "../../../../../core";
+import {createTextBlockModelMock} from "../../../../../test";
+import {TextBlockSettingsModule} from "../../text-block-settings.module";
+import {TextBlockSettingsForm} from "../../TextBlockSettingsForm";
+import {TextBlockFormComponent} from "./text-block-form.component";
+
+describe("TextBlockFormComponent", () => {
+ let component: TextBlockFormComponent;
+ let fixture: ComponentFixture<TextBlockFormComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [TextBlockSettingsModule, I18nModule],
+ providers: [{
+ provide: TextBlockSettingsForm,
+ useValue: new TextBlockSettingsForm()
+ }]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TextBlockFormComponent);
+ component = fixture.componentInstance;
+ component.form.setForm({
+ selects: {},
+ groups: [{groupName: "Group", textBlocks: [createTextBlockModelMock("", "")]}],
+ negativeGroups: []
+ });
+ component.appFormGroup = component.form.getTextBlockControls()[0] as FormGroup;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should check the text block ID as enter predicate", () => {
+ const drag = {data: {id: "1.1"}} as CdkDrag<IAPITextBlockModel>;
+ expect(component.enterPredicate(drag)).toBe(false);
+ drag.data.id = "1.19";
+ expect(component.enterPredicate(drag)).toBe(true);
+ drag.data = null;
+ expect(component.enterPredicate(drag)).toBe(true);
+ expect(component.enterPredicate(null)).toBe(true);
+ component.appFormGroup = null;
+ });
+
+ it("should focus text area element", () => {
+ component.focus();
+ expect(document.activeElement).toBe(component.queryList.first.nativeElement);
+ });
+
+ it("should add id to rule on drag and drop", () => {
+ const id = "1.19";
+ component.dropAreaConfig.forEach((area) => {
+ component.onEnter(id, area);
+ expect(component.selectedRule).toEqual({id, area});
+ component.onExit();
+ expect(component.selectedRule).not.toBeDefined();
+ component.onEnter(id, area);
+ expect(component.selectedRule).toEqual({id, area});
+ component.onDrop(id, area);
+ expect(component.selectedRule).not.toBeDefined();
+ expect(component.getRuleIds(area)).toEqual([id]);
+ });
+ });
+
+ it("should remove id from rule", () => {
+ const id = "1.19";
+ component.dropAreaConfig.forEach((area) => {
+ component.onDrop(id, area);
+ expect(component.getRuleIds(area)).toEqual([id]);
+ });
+ component.dropAreaConfig.forEach((area) => {
+ component.removeIdFromArea(id, area);
+ component.removeIdFromArea(id, area);
+ expect(component.getRuleIds(area)).toEqual([]);
+ });
+ expect(component.getValue().requires.length).toBe(0);
+ });
+
+});
diff --git a/src/app/features/settings/text-blocks/components/text-block-form/text-block-form.component.ts b/src/app/features/settings/text-blocks/components/text-block-form/text-block-form.component.ts
new file mode 100644
index 0000000..a8f617b
--- /dev/null
+++ b/src/app/features/settings/text-blocks/components/text-block-form/text-block-form.component.ts
@@ -0,0 +1,182 @@
+/********************************************************************************
+ * 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 {CdkDrag} from "@angular/cdk/drag-drop";
+import {Component, ElementRef, Input, OnDestroy, OnInit, QueryList, ViewChildren} from "@angular/core";
+import {FormGroup} from "@angular/forms";
+import {merge, of, Subject} from "rxjs";
+import {filter, map, switchMap, take, takeUntil, tap} from "rxjs/operators";
+import {IAPITextBlockModel, TAPIRequireRuleType, TAPITextBlockRuleKey} from "../../../../../core";
+import {arrayJoin, filterDistinctValues} from "../../../../../util/store";
+import {GetRuleIdsOfTextBlockPipe} from "../../pipes";
+import {TextBlockSettingsForm} from "../../TextBlockSettingsForm";
+
+export interface ITextBlockFormTextControlButton {
+ label?: string;
+ icon?: string;
+ token: string;
+ startToken?: string;
+ rotateIcon?: boolean;
+ requireLineBreak?: boolean;
+}
+
+export interface ITextBlockFormDropAreaConfig {
+ label: string;
+ key: TAPITextBlockRuleKey;
+ type?: TAPIRequireRuleType;
+ elementId: string;
+}
+
+@Component({
+ selector: "app-text-block-form",
+ templateUrl: "./text-block-form.component.html",
+ styleUrls: ["./text-block-form.component.scss"]
+})
+export class TextBlockFormComponent implements OnInit, OnDestroy {
+
+ private static id = 0;
+
+ public readonly id = `TextBlockFormComponent-${TextBlockFormComponent.id++}`;
+
+ @Input()
+ public appTextControlDefaultButtons: ITextBlockFormTextControlButton[] = [];
+
+ @Input()
+ public appTextControlReplacementButtons: ITextBlockFormTextControlButton[] = [];
+
+ @Input()
+ public appTextControlSelectButtons: ITextBlockFormTextControlButton[] = [];
+
+ public readonly dropAreaConfig: ITextBlockFormDropAreaConfig[] = [
+ {
+ label: "settings.textBlocks.rules.excludes",
+ key: "excludes",
+ elementId: this.id + "-DropArea-Excludes",
+ },
+ {
+ label: "settings.textBlocks.rules.requiresAnd",
+ key: "requires",
+ type: "and",
+ elementId: this.id + "-DropArea-Required-And"
+ },
+ {
+ label: "settings.textBlocks.rules.requiresOr",
+ key: "requires",
+ type: "or",
+ elementId: this.id + "-DropArea-Required-Or"
+ },
+ {
+ label: "settings.textBlocks.rules.requiresXOR",
+ key: "requires",
+ type: "xor",
+ elementId: this.id + "-DropArea-Required-Xor"
+ }
+ ];
+
+ public readonly dropListElementIds = this.dropAreaConfig.map((area) => area.elementId);
+
+ public selectedRule: { id: string, area: ITextBlockFormDropAreaConfig };
+
+ @Input()
+ public appFormGroup: FormGroup = new FormGroup({});
+
+ @ViewChildren("textAreaElement")
+ public queryList: QueryList<ElementRef<HTMLElement>>;
+
+ private focus$ = new Subject<FocusOptions>();
+
+ private destroy$ = new Subject();
+
+ public constructor(public form: TextBlockSettingsForm) {
+
+ }
+
+ public enterPredicate = (drag: CdkDrag<IAPITextBlockModel>) => drag?.data?.id !== this.getValue()?.id;
+
+ public ngOnInit() {
+ this.focus$.pipe(
+ switchMap((options) => {
+ return merge(of(0), this.queryList.changes).pipe(
+ map(() => this.queryList.first),
+ filter((element) => element != null),
+ take(1),
+ tap((element) => element.nativeElement.focus(options))
+ );
+ }),
+ takeUntil(this.destroy$),
+ ).subscribe();
+ }
+
+ public ngOnDestroy() {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ public focus(options?: FocusOptions) {
+ this.focus$.next(options);
+ }
+
+ public getValue(): IAPITextBlockModel {
+ return {...this.appFormGroup?.value};
+ }
+
+ public getRuleIds(area: ITextBlockFormDropAreaConfig): string[] {
+ return new GetRuleIdsOfTextBlockPipe().transform(this.getValue(), area.key, area.type);
+ }
+
+ public isDropAllowed(id: string, area: ITextBlockFormDropAreaConfig): boolean {
+ return id != null && id !== this.getValue().id && !this.getRuleIds(area).includes(id);
+ }
+
+ public onEnter(id: string, area: ITextBlockFormDropAreaConfig) {
+ if (this.isDropAllowed(id, area)) {
+ this.selectedRule = {id, area};
+ }
+ }
+
+ public onExit() {
+ this.selectedRule = undefined;
+ }
+
+ public onDrop(id: string, area: ITextBlockFormDropAreaConfig) {
+ if (this.isDropAllowed(id, area)) {
+ const ids = arrayJoin(this.getRuleIds(area), [id]);
+ this.patchRule(ids, area);
+ }
+ this.selectedRule = undefined;
+ }
+
+ public removeIdFromArea(id: string, area: ITextBlockFormDropAreaConfig) {
+ const ids = this.getRuleIds(area).filter((_) => _ !== id);
+ this.patchRule(ids, area);
+ }
+
+ private patchRule(ids: string[], area: ITextBlockFormDropAreaConfig) {
+ ids = filterDistinctValues(ids).sort((a, b) => this.form.compareIds(a, b));
+ area = {...area};
+ const value = this.getValue();
+ switch (area.key) {
+ case "excludes":
+ value.excludes = ids;
+ break;
+ case "requires":
+ value.requires = arrayJoin(value.requires)
+ .filter((rule) => rule.type !== area.type);
+ value.requires.push({type: area.type, ids});
+ value.requires = value.requires.filter((rule) => arrayJoin(rule.ids).length > 0);
+ break;
+ }
+ this.appFormGroup.patchValue(value);
+ }
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/text-blocks/components/text-block-list-form/index.ts
similarity index 91%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/text-blocks/components/text-block-list-form/index.ts
index 990bb42..dfbb380 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/text-blocks/components/text-block-list-form/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./text-block-list-form.component";
diff --git a/src/app/features/settings/text-blocks/components/text-block-list-form/text-block-list-form.component.html b/src/app/features/settings/text-blocks/components/text-block-list-form/text-block-list-form.component.html
new file mode 100644
index 0000000..bf7fcb3
--- /dev/null
+++ b/src/app/features/settings/text-blocks/components/text-block-list-form/text-block-list-form.component.html
@@ -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
+ -------------------------------------------------------------------------------->
+
+<ng-container *ngFor="let group of appTextBlockGroups; trackBy: trackBy; let groupIndex = index;">
+ <ng-template #headerTemplateRef>
+ <div class="group-header">
+
+ <input #groupNameInputElement
+ (focusout)="appSelectedGroupIndex = undefined"
+ (input)="appGroupNameChange.emit({groupIndex: groupIndex, groupName: groupNameInputElement.value})"
+ (keydown.enter)="groupNameInputElement.blur()"
+ (keydown.escape)="groupNameInputElement.blur()"
+ *ngIf="groupIndex === appSelectedGroupIndex"
+ [disabled]="appDisabled"
+ [value]="group.groupName"
+ appAutoFocusAfterInit
+ autocomplete="off"
+ class="openk-input group-header--input">
+
+ <button (click)="appDeleteTextBlockGroup.emit({ groupIndex: groupIndex })"
+ *ngIf="appSelectedGroupIndex !== groupIndex"
+ [disabled]="appDisabled" class="openk-button openk-button-icon group-header--button"
+ style="margin-right: auto;"
+ type="button">
+ <mat-icon>delete_forever</mat-icon>
+ </button>
+
+ <button (click)="appSelectedGroupIndex = groupIndex"
+ *ngIf="appSelectedGroupIndex !== groupIndex"
+ [disabled]="appDisabled"
+ class="openk-button openk-button-icon group-header--button"
+ type="button">
+ <mat-icon>edit</mat-icon>
+ </button>
+
+ <button *ngIf="appSelectedGroupIndex === groupIndex"
+ [disabled]="appDisabled"
+ class="openk-button openk-button-icon group-header--button"
+ type="button">
+ <mat-icon>edit</mat-icon>
+ </button>
+
+ <button (click)="appAddTextBlockGroup.emit({ groupIndex: groupIndex }); appSelectedGroupIndex = groupIndex + 1;"
+ [disabled]="appDisabled"
+ class="openk-button openk-button-icon group-header--button"
+ type="button">
+ <mat-icon>add</mat-icon>
+ </button>
+ </div>
+ </ng-template>
+
+ <app-collapsible
+ [appHeaderTemplateRef]="headerTemplateRef"
+ [appSimpleCollapsible]="true"
+ [appTitle]="appSelectedGroupIndex === groupIndex ? '' : group.groupName"
+ class="text-block-group">
+ <app-text-block-list
+ (appAdd)="appCreateTextBlock.emit({ textBlock: $event.textBlock, index: $event.index, groupIndex: groupIndex})"
+ (appDelete)="appDeleteTextBlock.emit({ textBlock: $event.textBlock, index: $event.index, groupIndex: groupIndex})"
+ (appDown)="appMoveTextBlock.emit({ groupIndex: groupIndex, from: $event.index, to: $event.index + 1})"
+ (appEdit)="appEditTextBlock.emit({ textBlock: $event.textBlock, index: $event.index, groupIndex: groupIndex})"
+ (appUp)="appMoveTextBlock.emit({ groupIndex: groupIndex, from: $event.index, to: $event.index - 1})"
+ [appConnectedTo]="appConnectedTo"
+ [appDisabled]="appDisabled"
+ [appDuplicateOnDrag]="true"
+ [appForAdmin]="true"
+ [appListData]="group.textBlocks"
+ [appReplacements]="appReplacements"
+ [appSelectedIds]="[appSelectedId]"
+ [appShowPreview]="true"
+ class="openk-drag-list-with-hidden-placeholder">
+ </app-text-block-list>
+ </app-collapsible>
+</ng-container>
+
diff --git a/src/app/features/settings/text-blocks/components/text-block-list-form/text-block-list-form.component.scss b/src/app/features/settings/text-blocks/components/text-block-list-form/text-block-list-form.component.scss
new file mode 100644
index 0000000..44dd8b7
--- /dev/null
+++ b/src/app/features/settings/text-blocks/components/text-block-list-form/text-block-list-form.component.scss
@@ -0,0 +1,55 @@
+/********************************************************************************
+ * 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 "src/styles/openk.styles";
+
+:host {
+ display: block;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 0 0.5em 0.5em 0.5em;
+}
+
+.text-block-group {
+ border: 0;
+}
+
+.group-header {
+ display: flex;
+ flex: 1 1 auto;
+ width: auto;
+ align-items: center;
+ justify-content: flex-end;
+
+ color: get-color($openk-default-palette, 500, contrast);
+}
+
+.group-header--input {
+ padding: 0.1em;
+ width: auto;
+ flex: 1 1 auto;
+ box-sizing: border-box;
+ overflow-x: hidden;
+
+ &:focus {
+ display: block;
+ max-width: initial;
+ opacity: 1;
+ }
+}
+
+.group-header--button {
+ margin-left: 0.25em;
+}
+
+
diff --git a/src/app/features/settings/components/search/settings.component.spec.ts b/src/app/features/settings/text-blocks/components/text-block-list-form/text-block-list-form.component.spec.ts
similarity index 65%
copy from src/app/features/settings/components/search/settings.component.spec.ts
copy to src/app/features/settings/text-blocks/components/text-block-list-form/text-block-list-form.component.spec.ts
index 2bc2d9f..a8c0625 100644
--- a/src/app/features/settings/components/search/settings.component.spec.ts
+++ b/src/app/features/settings/text-blocks/components/text-block-list-form/text-block-list-form.component.spec.ts
@@ -12,20 +12,22 @@
********************************************************************************/
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
-import {SettingsComponent} from "./settings.component";
+import {I18nModule} from "../../../../../core/i18n";
+import {TextBlockSettingsModule} from "../../text-block-settings.module";
+import {TextBlockListFormComponent} from "./text-block-list-form.component";
-describe("SettingsComponent", () => {
- let component: SettingsComponent;
- let fixture: ComponentFixture<SettingsComponent>;
+describe("TextBlockListFormComponent", () => {
+ let component: TextBlockListFormComponent;
+ let fixture: ComponentFixture<TextBlockListFormComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
- declarations: [SettingsComponent]
+ imports: [TextBlockSettingsModule, I18nModule]
}).compileComponents();
}));
beforeEach(() => {
- fixture = TestBed.createComponent(SettingsComponent);
+ fixture = TestBed.createComponent(TextBlockListFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
diff --git a/src/app/features/settings/text-blocks/components/text-block-list-form/text-block-list-form.component.ts b/src/app/features/settings/text-blocks/components/text-block-list-form/text-block-list-form.component.ts
new file mode 100644
index 0000000..ea50f06
--- /dev/null
+++ b/src/app/features/settings/text-blocks/components/text-block-list-form/text-block-list-form.component.ts
@@ -0,0 +1,67 @@
+/********************************************************************************
+ * 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 {CdkDropList} from "@angular/cdk/drag-drop";
+import {Component, EventEmitter, Input, Output} from "@angular/core";
+import {IAPITextBlockGroupModel, IAPITextBlockModel} from "../../../../../core/api/text";
+
+@Component({
+ selector: "app-text-block-list-form",
+ templateUrl: "./text-block-list-form.component.html",
+ styleUrls: ["./text-block-list-form.component.scss"]
+})
+export class TextBlockListFormComponent {
+
+ @Input()
+ public appDisabled: boolean;
+
+ @Input()
+ public appConnectedTo: CdkDropList | string | Array<CdkDropList | string>;
+
+ @Input()
+ public appTextBlockGroups: IAPITextBlockGroupModel[];
+
+ @Input()
+ public appSelectedId: string;
+
+ @Output()
+ public appEditTextBlock = new EventEmitter<{ textBlock: IAPITextBlockModel, groupIndex: number, index: number }>();
+
+ @Output()
+ public appDeleteTextBlock = new EventEmitter<{ textBlock: IAPITextBlockModel, groupIndex: number, index: number }>();
+
+ @Output()
+ public appCreateTextBlock = new EventEmitter<{ textBlock: IAPITextBlockModel, groupIndex: number, index: number }>();
+
+ @Output()
+ public appAddTextBlockGroup = new EventEmitter<{ groupIndex: number }>();
+
+ @Output()
+ public appDeleteTextBlockGroup = new EventEmitter<{ groupIndex: number }>();
+
+ @Output()
+ public appGroupNameChange = new EventEmitter<{ groupIndex: number, groupName: string }>();
+
+ @Output()
+ public appMoveTextBlock = new EventEmitter<{ groupIndex: number, from: number, to: number }>();
+
+ public appSelectedGroupIndex: number;
+
+ @Input()
+ public appReplacements: { [key: string]: string } = {};
+
+ public trackBy(index: number) {
+ return index;
+ }
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/text-blocks/components/text-block-select-form/index.ts
similarity index 91%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/text-blocks/components/text-block-select-form/index.ts
index 990bb42..c38a89a 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/text-blocks/components/text-block-select-form/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./text-block-select-form.component";
diff --git a/src/app/features/settings/text-blocks/components/text-block-select-form/text-block-select-form.component.html b/src/app/features/settings/text-blocks/components/text-block-select-form/text-block-select-form.component.html
new file mode 100644
index 0000000..a4031bd
--- /dev/null
+++ b/src/app/features/settings/text-blocks/components/text-block-select-form/text-block-select-form.component.html
@@ -0,0 +1,110 @@
+<!-------------------------------------------------------------------------------
+ * 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="list-select">
+
+ <label [for]="id + '-select'" class="list-select--label">
+ {{"settings.textBlocks.select" | translate}}
+ </label>
+
+ <div class="list-select--control">
+ <app-select (appValueChange)="setSelectedKey($event)"
+ [appDisabled]="form.formGroup.disabled || (selectKeys$ | async)?.length == 0"
+ [appId]="id + '-select'"
+ [appOptions]="selectKeys$ | async | arrayToSelectOptions"
+ [appValue]="selectedKey">
+ </app-select>
+ </div>
+
+
+ <button (click)="createNewSelectKey(); keyInputElement.focus()"
+ [disabled]="form.formGroup.disabled"
+ class="openk-button openk-button-icon list-select--button">
+ <mat-icon>add</mat-icon>
+ </button>
+
+
+ <label [for]="id + '-name'" class="list-select--label">
+ {{"settings.textBlocks.name" | translate}}
+ </label>
+
+ <div class="list-select--control">
+ <input #keyInputElement
+ (input)="changeSelectKey(selectedKey, keyInputElement.value)"
+ [disabled]="form.formGroup.disabled || selectedKey == null"
+ [id]="id + '-name'"
+ [value]="editedKey == null ? '' : editedKey"
+ autocomplete="off"
+ class="openk-input" type="text">
+ </div>
+
+
+ <button (click)="removeSelectKey(selectedKey); keyInputElement.focus();"
+ [disabled]="form.formGroup.disabled"
+ class="openk-button openk-button-icon list-select--button">
+ <mat-icon>delete_forever</mat-icon>
+ </button>
+
+ <ng-container *ngIf="!isEditedKeyValid">
+ <div></div>
+ <div class="list-select--error">
+ <span class="list-select--error--label">
+ {{"settings.textBlocks.invalidSelectKey" | translate}}
+ </span>
+ </div>
+ <div></div>
+ </ng-container>
+
+
+ <div class="list-select--options--title">
+ {{"settings.textBlocks.entries" | translate}}
+ </div>
+
+ <ng-container *ngIf="(form.selects | getFormArray : [selectedKey]) != null">
+
+ <div class="list-select--options">
+
+ <div *ngFor="let control of (form.selects | getFormArray : [selectedKey]).controls; let index = index;"
+ class="list--select--options--control">
+
+ <input #selectOptionInput
+ (input)="control.patchValue(selectOptionInput.value)"
+ [disabled]="form.formGroup.disabled"
+ [value]="control.value"
+ appAutoFocusAfterInit
+ autocomplete="off"
+ class="openk-input list--select--options--control--input"
+ type="text">
+
+ <div class="list--select--options--control--button">
+ <button (click)="removeSelectEntry(selectedKey, index)"
+ [disabled]="form.formGroup.disabled" class="openk-button openk-button-icon">
+ <mat-icon>delete_forever</mat-icon>
+ </button>
+ </div>
+
+ </div>
+
+ </div>
+
+ <div class="list--select--options--control list-select--options--control--add">
+ <button (click)="addSelectEntry(selectedKey)"
+ [disabled]="form.formGroup.disabled"
+ class="openk-button openk-button-icon list-select--button">
+ <mat-icon>add</mat-icon>
+ </button>
+ </div>
+
+ </ng-container>
+
+</div>
diff --git a/src/app/features/settings/text-blocks/components/text-block-select-form/text-block-select-form.component.scss b/src/app/features/settings/text-blocks/components/text-block-select-form/text-block-select-form.component.scss
new file mode 100644
index 0000000..06e0a27
--- /dev/null
+++ b/src/app/features/settings/text-blocks/components/text-block-select-form/text-block-select-form.component.scss
@@ -0,0 +1,98 @@
+/********************************************************************************
+ * 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 "src/styles/openk.styles";
+
+:host {
+ display: flex;
+ flex-flow: column;
+ padding: 1em;
+ box-sizing: border-box;
+ width: 100%;
+}
+
+
+.list-select {
+ display: grid;
+ width: 100%;
+ grid-template-columns: max-content auto max-content;
+ grid-gap: 0.5em;
+ margin-bottom: auto;
+ align-items: center;
+}
+
+.list-select--control {
+ display: flex;
+ width: 100%;
+ margin-left: 0.2em;
+
+ & > * {
+ flex: 1 1 100%;
+ width: 100%;
+ }
+}
+
+.list-select--button {
+ font-size: 0.8em;
+}
+
+.list-select--error {
+ display: flex;
+ margin-top: -0.4em;
+ margin-left: 0.85em;
+}
+
+.list-select--error--label {
+ color: $openk-error-color;
+ font-size: x-small;
+ font-style: italic;
+}
+
+
+.list-select--options--title {
+ align-self: start;
+ margin-top: 0.25em;
+}
+
+.list-select--options {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));
+ grid-gap: 0.5em;
+}
+
+.list--select--options--control {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ position: relative;
+}
+
+.list--select--options--control--input {
+ margin-left: 0.5em;
+ padding-right: 2em;
+ width: 100%;
+ flex: 1 1 100%;
+}
+
+.list--select--options--control--button {
+ position: absolute;
+ right: 0.4em;
+}
+
+.list-select--options--control--add {
+ align-self: end;
+ min-height: 2.137em;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
diff --git a/src/app/features/settings/text-blocks/components/text-block-select-form/text-block-select-form.component.spec.ts b/src/app/features/settings/text-blocks/components/text-block-select-form/text-block-select-form.component.spec.ts
new file mode 100644
index 0000000..1b2e4ed
--- /dev/null
+++ b/src/app/features/settings/text-blocks/components/text-block-select-form/text-block-select-form.component.spec.ts
@@ -0,0 +1,115 @@
+/********************************************************************************
+ * 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 {IAPITextBlockModel} from "../../../../../core/api/text";
+import {I18nModule} from "../../../../../core/i18n";
+import {createTextBlockModelMock} from "../../../../../test";
+import {TextBlockSettingsModule} from "../../text-block-settings.module";
+import {TextBlockSettingsForm} from "../../TextBlockSettingsForm";
+import {TextBlockSelectFormComponent} from "./text-block-select-form.component";
+
+describe("TextBlockSelectFormComponent", () => {
+ const keyName = "test";
+ let component: TextBlockSelectFormComponent;
+ let fixture: ComponentFixture<TextBlockSelectFormComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [TextBlockSettingsModule, I18nModule],
+ providers: [{provide: TextBlockSettingsForm, useValue: new TextBlockSettingsForm()}]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TextBlockSelectFormComponent);
+ component = fixture.componentInstance;
+ component.form.setForm({
+ selects: {
+ test: []
+ },
+ groups: [{
+ groupName: "Group",
+ textBlocks: [{...createTextBlockModelMock("19", createToken(keyName))}]
+ }],
+ negativeGroups: []
+ });
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should automatically select first key", () => {
+ expect(component.selectedKey).toBe(keyName);
+ });
+
+ it("should add/remove select entries", () => {
+ component.appDefaultSelectEntry = "Test";
+ component.setSelectedKey(keyName);
+ component.addSelectEntry(keyName);
+ expect(component.form.getSelectEntries(keyName)).toEqual(["Test"]);
+ component.removeSelectEntry(keyName, 0);
+ expect(component.form.getSelectEntries(keyName)).toEqual([]);
+ });
+
+ it("should generate/remove select keys", () => {
+ component.appDefaultSelectKey = keyName;
+ component.createNewSelectKey();
+ expect(component.form.getSelectKeys()).toEqual([keyName, keyName + "1"]);
+ component.removeSelectKey(keyName + "1");
+ component.removeSelectKey(keyName);
+ expect(component.form.getSelectKeys()).toEqual([]);
+ forEachTextBlock((textBlock) => {
+ expect(textBlock.text).toEqual("");
+ });
+ });
+
+ it("should change select keys", () => {
+ const newKeyName = "newKey";
+ component.changeSelectKey(keyName, newKeyName);
+ expect(component.form.getSelectKeys()).toEqual([newKeyName]);
+ forEachTextBlock((textBlock) => expect(textBlock.text).toEqual(createToken(newKeyName)));
+ });
+
+ it("should verify key names", () => {
+ component.appDefaultSelectKey = keyName;
+ component.createNewSelectKey();
+ const newKeyName = component.form.getSelectKeys().reverse()[0];
+
+ expect(component.editedKey).toBe(newKeyName);
+ expect(component.selectedKey).toBe(newKeyName);
+ expect(component.isEditedKeyValid).toBe(true);
+
+ component.changeSelectKey(newKeyName, newKeyName);
+ expect(component.editedKey).toBe(newKeyName);
+ expect(component.selectedKey).toBe(newKeyName);
+ expect(component.isEditedKeyValid).toBe(true);
+
+ [keyName, "Another Invalid Keyname"].forEach((invalidKeyName) => {
+ component.changeSelectKey(newKeyName, invalidKeyName);
+ expect(component.selectedKey).toBe(newKeyName);
+ expect(component.editedKey).toBe(invalidKeyName);
+ expect(component.isEditedKeyValid).toBe(false);
+ });
+ });
+
+ function forEachTextBlock(fn: (textBlock: IAPITextBlockModel) => any) {
+ component.form.getValue().groups.forEach((group) => group.textBlocks.forEach(fn));
+ }
+
+ function createToken(key: string) {
+ return `<s:${key}>`;
+ }
+});
diff --git a/src/app/features/settings/text-blocks/components/text-block-select-form/text-block-select-form.component.ts b/src/app/features/settings/text-blocks/components/text-block-select-form/text-block-select-form.component.ts
new file mode 100644
index 0000000..7c53968
--- /dev/null
+++ b/src/app/features/settings/text-blocks/components/text-block-select-form/text-block-select-form.component.ts
@@ -0,0 +1,114 @@
+/********************************************************************************
+ * 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 {defer, merge, of} from "rxjs";
+import {map, tap} from "rxjs/operators";
+import {arrayJoin} from "../../../../../util/store";
+import {TextBlockSettingsForm} from "../../TextBlockSettingsForm";
+
+@Component({
+ selector: "app-text-block-select-form",
+ templateUrl: "./text-block-select-form.component.html",
+ styleUrls: ["./text-block-select-form.component.scss"]
+})
+export class TextBlockSelectFormComponent {
+
+ private static id = 0;
+
+ @Input()
+ public appDefaultSelectEntry = "";
+
+ @Input()
+ public appDefaultSelectKey = "";
+
+ public readonly id = `TextBlockSelectFormComponent-${TextBlockSelectFormComponent.id++}`;
+
+ public selectKeys$ = defer(() => merge(of(0), this.form.selects.valueChanges)).pipe(
+ map(() => this.form.getSelectKeys()),
+ tap(() => this.reselectKey())
+ );
+
+ public selectedKey: string;
+
+ public editedKey: string;
+
+ private keyRegExp = /^[0-9a-zA-Z_\-]+$/;
+
+ public constructor(public form: TextBlockSettingsForm) {
+
+ }
+
+ public get isEditedKeyValid() {
+ return this.keyRegExp.test(this.editedKey)
+ && (this.editedKey === this.selectedKey || !this.isKeyInOptions(this.editedKey));
+ }
+
+ public setSelectedKey(key: string) {
+ this.editedKey = this.selectedKey = key;
+ }
+
+ public createNewSelectKey() {
+ const key = this.generateNewSelectKey();
+ this.form.addSelect(key);
+ this.editedKey = this.selectedKey = key;
+ }
+
+ public changeSelectKey(key: string, newKey: string) {
+ this.selectedKey = key;
+ this.editedKey = newKey;
+ if (key !== newKey && this.isEditedKeyValid) {
+ this.form.changeSelectKey(this.selectedKey, this.editedKey);
+ this.selectedKey = this.editedKey = newKey;
+ }
+ }
+
+ public removeSelectKey(key: string) {
+ const options = arrayJoin(this.getSelectKeys());
+ const indexOfKey = options.indexOf(key);
+ this.form.removeSelect(key);
+ this.editedKey = this.selectedKey = options.filter((_) => _ !== key)[Math.max(indexOfKey - 1, 0)];
+ }
+
+ public addSelectEntry(key: string) {
+ this.form.addSelectEntry(key, this.appDefaultSelectEntry);
+ }
+
+ public removeSelectEntry(key: string, index: number) {
+ this.form.removeSelectEntry(key, index);
+ }
+
+
+ private getSelectKeys() {
+ return this.form.getSelectKeys();
+ }
+
+ private reselectKey() {
+ if (arrayJoin(this.getSelectKeys()).length > 0 && !this.isKeyInOptions(this.selectedKey)) {
+ this.editedKey = this.selectedKey = this.getSelectKeys()[0];
+ }
+ }
+
+ private isKeyInOptions(key: string): boolean {
+ return arrayJoin(this.getSelectKeys()).includes(key);
+ }
+
+ private generateNewSelectKey() {
+ let key = this.appDefaultSelectKey;
+ for (let i = 1; this.isKeyInOptions(key); i++) {
+ key = this.appDefaultSelectKey + i;
+ }
+ return key;
+ }
+
+}
diff --git a/src/app/features/settings/text-blocks/components/text-blocks-settings.component.html b/src/app/features/settings/text-blocks/components/text-blocks-settings.component.html
new file mode 100644
index 0000000..e921fed
--- /dev/null
+++ b/src/app/features/settings/text-blocks/components/text-blocks-settings.component.html
@@ -0,0 +1,81 @@
+<!-------------------------------------------------------------------------------
+ * 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-settings-side-menu
+ [appForAdmin]="isAdmin$ | async">
+</app-settings-side-menu>
+
+<app-side-menu-status
+ *appSideMenu="'center'"
+ [appLoadingMessage]="'core.loading' | translate"
+ [appLoading]="(loading$ | async)">
+
+</app-side-menu-status>
+
+<ng-container *appSideMenu="'bottom'">
+ <app-action-button (appClick)="submit()"
+ [appDisabled]="loading$ | async"
+ [appIcon]="'publish'"
+ class="openk-success side-menu-button">
+ {{ "settings.save" | translate }}
+ </app-action-button>
+</ng-container>
+
+
+<div class="title">
+ <span class="title--label">
+ {{"settings.title" | translate}} - {{"settings.textBlocks.title" | translate}}
+ </span>
+</div>
+
+<app-collapsible [appTitle]="'settings.textBlocks.selects' | translate">
+
+ <app-text-block-select-form
+ [appDefaultSelectEntry]="'settings.textBlocks.default.selectEntry' | translate"
+ [appDefaultSelectKey]="'settings.textBlocks.default.selectKey' | translate">
+ </app-text-block-select-form>
+
+</app-collapsible>
+
+<app-collapsible
+ *ngFor="let item of config"
+ [appTitle]="item.negative ? 'Textbausteine (Negativantwort)' : 'Textbausteine'">
+
+ <div class="text-block-container">
+ <app-text-block-list-form
+ (appAddTextBlockGroup)="insertTextBlockGroup($event.groupIndex + 1, item.negative)"
+ (appCreateTextBlock)="insertTextBlock($event.groupIndex, $event.index + 1, item.negative); textBlockForm.focus();"
+ (appDeleteTextBlock)="deleteTextBlock($event?.textBlock?.id, $event?.groupIndex, $event?.index, item.negative)"
+ (appDeleteTextBlockGroup)="deleteTextBlockGroup($event.groupIndex, item.negative)"
+ (appEditTextBlock)="selectTextBlock($event?.groupIndex, $event?.index, item.negative); textBlockForm.focus();"
+ (appGroupNameChange)="changeGroupName($event.groupIndex, $event.groupName, item.negative)"
+ (appMoveTextBlock)="moveTextBlock($event.groupIndex, $event.from, $event.to, item.negative)"
+ [appConnectedTo]="textBlockForm.dropListElementIds"
+ [appDisabled]="form.formGroup.disabled"
+ [appReplacements]="replacements$ | async"
+ [appSelectedId]="item?.selection?.control?.value?.id"
+ [appTextBlockGroups]="item.negative ? form.negativeGroups.value : form.groups.value"
+ class="pane">
+ </app-text-block-list-form>
+
+ <div class="pane pane---with-border">
+ <app-text-block-form #textBlockForm
+ [appFormGroup]="item.selection?.control"
+ [appTextControlDefaultButtons]="defaultButtons$ | async "
+ [appTextControlReplacementButtons]="replacementButtons$ | async"
+ [appTextControlSelectButtons]="selectButtons$ | async">
+ </app-text-block-form>
+ </div>
+ </div>
+
+</app-collapsible>
diff --git a/src/app/features/settings/text-blocks/components/text-blocks-settings.component.scss b/src/app/features/settings/text-blocks/components/text-blocks-settings.component.scss
new file mode 100644
index 0000000..ee9f6cb
--- /dev/null
+++ b/src/app/features/settings/text-blocks/components/text-blocks-settings.component.scss
@@ -0,0 +1,65 @@
+/********************************************************************************
+ * 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 "src/styles/openk.styles";
+
+:host {
+ width: 100%;
+ padding: 1em;
+ box-sizing: border-box;
+ display: flex;
+ flex-flow: column;
+ max-width: 70em;
+ margin: 0 auto 0 auto;
+
+ & > * {
+ margin-bottom: 1em;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+}
+
+.side-menu-button {
+ width: 100%;
+
+ & + & {
+ margin-top: 1em;
+ }
+}
+
+.title {
+ margin-bottom: 1em;
+}
+
+.title--label {
+ font-size: x-large;
+ font-weight: 600;
+}
+
+
+.text-block-container {
+ display: flex;
+ width: 100%;
+}
+
+.pane {
+ width: 50%;
+ height: max(30em, calc(100vh - 20em));
+ overflow: auto;
+}
+
+.pane---with-border {
+ border-left: 1px solid $openk-form-border;
+}
diff --git a/src/app/features/settings/text-blocks/components/text-blocks-settings.component.spec.ts b/src/app/features/settings/text-blocks/components/text-blocks-settings.component.spec.ts
new file mode 100644
index 0000000..c86751f
--- /dev/null
+++ b/src/app/features/settings/text-blocks/components/text-blocks-settings.component.spec.ts
@@ -0,0 +1,61 @@
+/********************************************************************************
+ * 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";
+import {submitTextblockSettingsAction} from "../../../../store";
+import {TextBlockSettingsModule} from "../text-block-settings.module";
+import {TextBlocksSettingsComponent} from "./text-blocks-settings.component";
+
+describe("TextblockSettingsComponent", () => {
+ let component: TextBlocksSettingsComponent;
+ let fixture: ComponentFixture<TextBlocksSettingsComponent>;
+ let store: MockStore;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ TextBlockSettingsModule,
+ RouterTestingModule,
+ I18nModule
+ ],
+ providers: [
+ provideMockStore()
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TextBlocksSettingsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ store = TestBed.inject(MockStore);
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should track by index", () => {
+ expect(component.trackByIndex(19)).toBe(19);
+ });
+
+ it("should submit data", () => {
+ spyOn(store, "dispatch");
+ component.submit();
+ expect(store.dispatch).toHaveBeenCalledWith(submitTextblockSettingsAction({data: component.form.getValue()}));
+ });
+
+});
diff --git a/src/app/features/settings/text-blocks/components/text-blocks-settings.component.ts b/src/app/features/settings/text-blocks/components/text-blocks-settings.component.ts
new file mode 100644
index 0000000..e5e1682
--- /dev/null
+++ b/src/app/features/settings/text-blocks/components/text-blocks-settings.component.ts
@@ -0,0 +1,212 @@
+/********************************************************************************
+ * 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, OnDestroy, OnInit} from "@angular/core";
+import {FormGroup} from "@angular/forms";
+import {select, Store} from "@ngrx/store";
+import {TranslateService} from "@ngx-translate/core";
+import {defer, merge, Observable, of, Subject} from "rxjs";
+import {map, switchMap, takeUntil} from "rxjs/operators";
+import {
+ fetchTextblockSettingsAction,
+ getSettingsLoadingSelector,
+ getTextblockSettingsSelector,
+ isAdminSelector,
+ submitTextblockSettingsAction
+} from "../../../../store";
+import {TextBlockSettingsForm} from "../TextBlockSettingsForm";
+import {ITextBlockFormTextControlButton} from "./text-block-form";
+
+
+export interface ITextBlockSettingConfig {
+ selection?: {
+ groupIndex: number;
+ index: number;
+ control: FormGroup;
+ };
+ negative?: boolean;
+}
+
+@Component({
+ selector: "app-text-block-settings",
+ templateUrl: "./text-blocks-settings.component.html",
+ styleUrls: ["./text-blocks-settings.component.scss"],
+ providers: [
+ {
+ provide: TextBlockSettingsForm,
+ useValue: new TextBlockSettingsForm()
+ }
+ ]
+})
+export class TextBlocksSettingsComponent implements OnInit, OnDestroy {
+
+ public loading$ = this.store.pipe(select(getSettingsLoadingSelector));
+
+ public textBlockConfig$ = this.store.pipe(select(getTextblockSettingsSelector));
+
+ public isAdmin$ = this.store.pipe(select(isAdminSelector));
+
+ public selectKeys$ = defer(() => merge(of(0), this.form.selects.valueChanges)).pipe(
+ map(() => this.form.getSelectKeys())
+ );
+
+ public replacementKeys = ["id", "title", "city", "district", "sectors", "creationDate", "receiptDate", "dueDate", "customerReference"];
+
+ public additionalReplacementKeys = ["c-community", "c-communitySuffix", "c-company", "c-email", "c-firstName", "c-lastName",
+ "c-houseNumber", "c-postCode", "c-salutation", "c-street", "c-title"];
+
+ public replacements$: Observable<{ [key: string]: string }> = of([...this.replacementKeys, ...this.additionalReplacementKeys]).pipe(
+ switchMap((keys) => this.translateKeys(keys, "settings.textBlocks.replacements."))
+ );
+
+ public defaultButtons$: Observable<ITextBlockFormTextControlButton[]> = of(["freeText", "date"]).pipe(
+ switchMap((keys) => this.translateKeys(keys, "settings.textBlocks.replacements.")),
+ map((obj: { freeText: string, date: string }) => {
+ return [
+ {
+ icon: "format_bold",
+ token: "**",
+ startToken: "**"
+ },
+ {
+ icon: "format_italic",
+ token: "__",
+ startToken: "__"
+ },
+ {
+ icon: "format_list_bulleted",
+ startToken: "* ",
+ token: "",
+ requireLineBreak: true
+ },
+ {
+ label: obj.freeText,
+ icon: "edit",
+ token: `<f:${obj.freeText}>`
+ },
+ {
+ label: obj.date,
+ icon: "today",
+ token: `<d:${obj.date}>`
+ }
+ ];
+ })
+ );
+
+ public replacementButtons$: Observable<ITextBlockFormTextControlButton[]> = of(this.replacementKeys).pipe(
+ switchMap((keys) => this.translateKeys(keys, "settings.textBlocks.replacements.")),
+ map((obj) => {
+ return this.replacementKeys.map((key) => ({label: obj[key], token: `<t:${key}>`}));
+ })
+ );
+
+ public selectButtons$: Observable<ITextBlockFormTextControlButton[]> = defer(() => this.selectKeys$).pipe(
+ map((keys) => keys.map((key) => ({label: key, token: `<s:${key}>`, icon: "play_arrow", rotateIcon: true})))
+ );
+
+ public config: ITextBlockSettingConfig[] = [{}, {negative: true}];
+
+ private destroy$ = new Subject();
+
+ constructor(public store: Store, public translateService: TranslateService, public form: TextBlockSettingsForm) {
+
+ }
+
+ public ngOnInit() {
+ this.store.dispatch(fetchTextblockSettingsAction());
+ this.textBlockConfig$.pipe(takeUntil(this.destroy$)).subscribe((value) => {
+ this.form.setForm(value);
+ this.config
+ .filter((_) => _.selection != null)
+ .forEach((_) => this.selectTextBlock(_.selection.groupIndex, _.selection.index, _.negative));
+ });
+ this.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => this.form.disable(loading));
+ }
+
+ public ngOnDestroy() {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ public submit() {
+ this.store.dispatch(submitTextblockSettingsAction({data: this.form.getValue()}));
+ }
+
+ public trackByIndex(index: number) {
+ return index;
+ }
+
+ public selectTextBlock(groupIndex: number, index: number, negative?: boolean) {
+ const control = this.form.getTextBlockFormArrayForGroup(groupIndex, negative)?.at(index);
+ this.config.find((_) => _.negative === negative).selection
+ = control instanceof FormGroup ? {groupIndex, index, control} : undefined;
+ }
+
+
+ public changeGroupName(groupIndex: number, groupName: string, negative?: boolean) {
+ this.form.getTextBlockGroups(negative).at(groupIndex)?.patchValue({groupName});
+ }
+
+ public insertTextBlockGroup(groupIndex: number, negative?: boolean) {
+ this.form.insertGroup(groupIndex, negative);
+ this.insertTextBlock(groupIndex, 0, negative);
+ }
+
+ public deleteTextBlockGroup(groupIndex: number, negative?: boolean) {
+ const length = this.form.removeGroup(groupIndex, negative);
+ if (length === 0) {
+ this.insertTextBlockGroup(0, negative);
+ }
+ this.deselect();
+ }
+
+
+ public insertTextBlock(groupIndex: number, index: number, negative?: boolean) {
+ this.form.insertTextBlock(groupIndex, index, negative);
+ this.selectTextBlock(groupIndex, index, negative);
+ }
+
+ public moveTextBlock(groupIndex: number, from: number, to: number, negative: boolean) {
+ this.form.moveTextBlockInGroup(groupIndex, from, to, negative);
+ }
+
+ public deleteTextBlock(id: string, groupIndex: number, index: number, negative?: boolean) {
+ const length = this.form.removeTextBlock(groupIndex, index, negative);
+ if (length === 0) {
+ this.insertTextBlock(groupIndex, 0, negative);
+ }
+ this.selectTextBlock(groupIndex, index, negative);
+ }
+
+ /**
+ * Returns an object, where each key from a given list has its translation as value.
+ * @param keys List of keys which are translated.
+ * @param translationKeyPrefix Prefix which is added to all keys prior translation (but is not part of any key in the result).
+ */
+ private translateKeys(keys: string[], translationKeyPrefix: string): Observable<{ [key: string]: string }> {
+ const translationKeys = keys.map((key) => translationKeyPrefix + key);
+ return this.translateService.get(translationKeys).pipe(
+ map((translation) => {
+ return keys.reduce((replacement, key, index) => ({
+ ...replacement,
+ [key]: translation[translationKeys[index]]
+ }), {});
+ })
+ );
+ }
+
+ private deselect(negative?: boolean) {
+ this.config.find((_) => _.negative === negative).selection = undefined;
+ }
+
+}
diff --git a/src/app/shared/leaflet/index.ts b/src/app/features/settings/text-blocks/index.ts
similarity index 81%
copy from src/app/shared/leaflet/index.ts
copy to src/app/features/settings/text-blocks/index.ts
index 849c149..5493fee 100644
--- a/src/app/shared/leaflet/index.ts
+++ b/src/app/features/settings/text-blocks/index.ts
@@ -11,9 +11,8 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./directives";
+export * from "./components";
export * from "./pipes";
-export * from "./util";
-export * from "./leaflet.module";
-export * from "./leaflet-configuration.token";
+export * from "./text-block-settings.module";
+export * from "./TextBlockSettingsForm";
diff --git a/src/app/features/settings/text-blocks/pipes/get-rule-ids-of-text-block.pipe.spec.ts b/src/app/features/settings/text-blocks/pipes/get-rule-ids-of-text-block.pipe.spec.ts
new file mode 100644
index 0000000..7eca5dc
--- /dev/null
+++ b/src/app/features/settings/text-blocks/pipes/get-rule-ids-of-text-block.pipe.spec.ts
@@ -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
+ ********************************************************************************/
+
+import {IAPITextBlockModel} from "../../../../core/api/text";
+import {GetRuleIdsOfTextBlockPipe} from "./get-rule-ids-of-text-block.pipe";
+
+describe("GetRuleIdsOfTextBlockPipe", () => {
+
+ let pipe: GetRuleIdsOfTextBlockPipe;
+
+ beforeEach(() => {
+ pipe = new GetRuleIdsOfTextBlockPipe();
+ });
+
+ it("should extract list of IDs for a given rule", () => {
+ const textBlock: IAPITextBlockModel = {
+ id: "1.9",
+ text: "",
+ requires: [
+ {
+ type: "and",
+ ids: ["1.119"]
+ }
+ ],
+ excludes: ["1.7", "1.19"]
+ };
+
+ expect(pipe.transform(null, "excludes")).toEqual([]);
+ expect(pipe.transform(textBlock, null)).toEqual([]);
+ expect(pipe.transform(textBlock, "requires")).toEqual([]);
+ expect(pipe.transform(textBlock, "requires", "xor")).toEqual([]);
+ expect(pipe.transform(textBlock, "excludes")).toEqual(textBlock.excludes);
+ expect(pipe.transform(textBlock, "requires", "and")).toEqual(textBlock.requires[0].ids);
+ });
+
+
+});
diff --git a/src/app/features/settings/text-blocks/pipes/get-rule-ids-of-text-block.pipe.ts b/src/app/features/settings/text-blocks/pipes/get-rule-ids-of-text-block.pipe.ts
new file mode 100644
index 0000000..5690593
--- /dev/null
+++ b/src/app/features/settings/text-blocks/pipes/get-rule-ids-of-text-block.pipe.ts
@@ -0,0 +1,40 @@
+/********************************************************************************
+ * 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 {IAPITextBlockModel, TAPIRequireRuleType, TAPITextBlockRuleKey} from "../../../../core";
+import {arrayJoin} from "../../../../util";
+
+@Pipe({name: "getRuleIdsOfTextBlock"})
+export class GetRuleIdsOfTextBlockPipe implements PipeTransform {
+
+ public transform(
+ value: IAPITextBlockModel,
+ key: TAPITextBlockRuleKey,
+ type?: TAPIRequireRuleType
+ ): string[] {
+ if (value == null) {
+ return [];
+ }
+
+ switch (key) {
+ case "excludes":
+ return arrayJoin(value.excludes);
+ case "requires":
+ return arrayJoin(arrayJoin(value.requires).find((rule) => rule.type === type)?.ids);
+ }
+
+ return [];
+ }
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/text-blocks/pipes/index.ts
similarity index 91%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/text-blocks/pipes/index.ts
index 990bb42..379ba06 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/text-blocks/pipes/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./get-rule-ids-of-text-block.pipe";
diff --git a/src/app/features/settings/text-blocks/text-block-settings.module.ts b/src/app/features/settings/text-blocks/text-block-settings.module.ts
new file mode 100644
index 0000000..40fa396
--- /dev/null
+++ b/src/app/features/settings/text-blocks/text-block-settings.module.ts
@@ -0,0 +1,70 @@
+/********************************************************************************
+ * 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 {DragDropModule} from "@angular/cdk/drag-drop";
+import {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {MatIconModule} from "@angular/material/icon";
+import {TranslateModule} from "@ngx-translate/core";
+import {CommonControlsModule} from "../../../shared/controls/common";
+import {SelectModule} from "../../../shared/controls/select";
+import {ActionButtonModule} from "../../../shared/layout/action-button";
+import {AutoFocusAfterInitModule} from "../../../shared/layout/auto-focus-after-init";
+import {CollapsibleModule} from "../../../shared/layout/collapsible";
+import {SideMenuModule} from "../../../shared/layout/side-menu";
+import {SharedPipesModule} from "../../../shared/pipes";
+import {TextBlockModule} from "../../../shared/text-block";
+import {SharedSettingsModule} from "../shared";
+import {TextBlocksSettingsComponent} from "./components";
+import {TextBlockFormComponent} from "./components/text-block-form";
+import {TextBlockListFormComponent} from "./components/text-block-list-form";
+import {TextBlockSelectFormComponent} from "./components/text-block-select-form";
+import {GetRuleIdsOfTextBlockPipe} from "./pipes";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ TranslateModule,
+ MatIconModule,
+ DragDropModule,
+
+ ActionButtonModule,
+ AutoFocusAfterInitModule,
+ CollapsibleModule,
+ CommonControlsModule,
+ SelectModule,
+ SharedPipesModule,
+ SharedSettingsModule,
+ SideMenuModule,
+ TextBlockModule
+ ],
+ declarations: [
+ TextBlockFormComponent,
+ TextBlockListFormComponent,
+ TextBlockSelectFormComponent,
+ TextBlocksSettingsComponent,
+
+ GetRuleIdsOfTextBlockPipe
+ ],
+ exports: [
+ TextBlockFormComponent,
+ TextBlockListFormComponent,
+ TextBlockSelectFormComponent,
+ TextBlocksSettingsComponent,
+
+ GetRuleIdsOfTextBlockPipe
+ ]
+})
+export class TextBlockSettingsModule {
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/users/components/index.ts
similarity index 79%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/users/components/index.ts
index 990bb42..2d31e8c 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/users/components/index.ts
@@ -11,4 +11,8 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./users-search";
+export * from "./users-settings-edit";
+export * from "./users-table";
+
+export * from "./users-settings.component";
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/users/components/users-search/index.ts
similarity index 91%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/users/components/users-search/index.ts
index 990bb42..ff482e5 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/users/components/users-search/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./users-settings-search.component";
diff --git a/src/app/features/settings/users/components/users-search/users-settings-search.component.html b/src/app/features/settings/users/components/users-search/users-settings-search.component.html
new file mode 100644
index 0000000..bf16ac1
--- /dev/null
+++ b/src/app/features/settings/users/components/users-search/users-settings-search.component.html
@@ -0,0 +1,96 @@
+<!-------------------------------------------------------------------------------
+ * 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">
+ <div class="search--bar">
+ <span class="search--bar--text">{{"settings.departments.search" | translate}}</span>
+ <app-searchbar
+ (appSearch)="setFilterParameter('q', $event)"
+ [appPlaceholder]="'settings.departments.placeholderSearch' | translate"
+ [appSearchText]="appValue?.q"
+ class="search--input">
+ </app-searchbar>
+ </div>
+
+ <div class="filters">
+
+ <button (click)="null"
+ class="openk-button openk-button-rounded"
+ type="button">
+ <mat-icon>filter_list</mat-icon>
+ </button>
+
+ <div class="filters--row">
+ <div *ngIf="(userRoleOptions$ | async) != null" class="filter-group filter-group---stacked">
+ <button
+ (click)="toggleFilterParameter('role', roleSelect.appValue)"
+ [class.openk-info]="appValue?.role != null"
+ class="openk-button openk-chip filters--btn filters--btn---margin">
+ {{"settings.users.table.role" | translate}}
+ </button>
+ <div class="filters--row-select-width">
+ <app-select #roleSelect
+ (appValueChange)="setFilterParameter('role', $event)"
+ [appOptions]="userRoleOptions$ | async"
+ [appValue]="(userRoleOptions$ | async)[0].value"
+ class="openk-info filters--select--input-width">
+ </app-select>
+ </div>
+
+ </div>
+
+ <div *ngIf="(appDepartmentGroups | objKeysToArray).length > 0" class="filter-group filter-group---stacked">
+ <div class="filters--block">
+ <div class="filter--block--entry">
+
+ <button
+ (click)="toggleFilterParameter('departmentGroupName', departmentGroupSelect.appValue)"
+ [class.openk-info]="appValue?.departmentGroupName != null"
+ class="openk-button openk-chip filters--btn filters--btn---margin">
+ {{"settings.users.departmentGroup" | translate}}
+ </button>
+ <div class="filters--row-select-width">
+ <app-select
+ #departmentGroupSelect
+ (appValueChange)="setFilterParameter('departmentGroupName', $event, true)"
+ [appOptions]="appDepartmentGroups | objKeysToArray | arrayToSelectOptions"
+ [appValue]="(appDepartmentGroups | objKeysToArray)[0]"
+ class="openk-info filters--select--input-width">
+ </app-select>
+ </div>
+
+ </div>
+ <div class="filter--block--entry">
+ <button
+ (click)="toggleFilterParameter('departmentName', departmentNameSelect.appValue)"
+ [class.openk-info]="appValue?.departmentName != null"
+ class="openk-button openk-chip filters--btn filters--btn---margin">
+ {{"settings.users.department" | translate}}
+ </button>
+ <div class="filters--row-select-width">
+ <app-select #departmentNameSelect
+ (appValueChange)="setFilterParameter('departmentName', $event)"
+ [appDisabled]="!(appDepartmentGroups[departmentGroupSelect.appValue]?.length > 0)"
+ [appOptions]="appDepartmentGroups[departmentGroupSelect.appValue] | arrayToSelectOptions"
+ [appValue]="appDepartmentGroups[departmentGroupSelect.appValue] == null ? null : appDepartmentGroups[departmentGroupSelect.appValue][0]"
+ class="openk-info filters--select--input-width">
+ </app-select>
+ </div>
+
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </div>
+</div>
diff --git a/src/app/features/settings/users/components/users-search/users-settings-search.component.scss b/src/app/features/settings/users/components/users-search/users-settings-search.component.scss
new file mode 100644
index 0000000..94f7248
--- /dev/null
+++ b/src/app/features/settings/users/components/users-search/users-settings-search.component.scss
@@ -0,0 +1,125 @@
+/********************************************************************************
+ * 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 "../../../../../../styles/openk.styles";
+
+:host {
+ width: 100%;
+ display: flex;
+ flex-flow: column;
+ overflow: auto;
+}
+
+.search {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 0.5em;
+}
+
+.search--bar {
+ display: flex;
+ align-items: center;
+ margin-bottom: 1em;
+}
+
+.search--bar--text {
+ margin-right: 0.5em;
+ min-width: 2.9em;
+}
+
+.search--input {
+ flex: 1;
+}
+
+.filters {
+ display: flex;
+ flex-direction: row;
+}
+
+.filters--row {
+ display: inline-flex;
+ align-items: baseline;
+ 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--row-select-width {
+ width: 15em;
+}
+
+.filters--select--input-width {
+ width: 100%;
+}
+
+.filters--block {
+ display: flex;
+ flex-direction: column;
+}
+
+.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;
+}
+
+.filter--block--entry {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+}
diff --git a/src/app/features/settings/users/components/users-search/users-settings-search.component.spec.ts b/src/app/features/settings/users/components/users-search/users-settings-search.component.spec.ts
new file mode 100644
index 0000000..769ccea
--- /dev/null
+++ b/src/app/features/settings/users/components/users-search/users-settings-search.component.spec.ts
@@ -0,0 +1,68 @@
+/********************************************************************************
+ * 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 {UsersSettingsModule} from "../../users-settings.module";
+import {UsersSettingsSearchComponent} from "./users-settings-search.component";
+
+describe("UsersSettingsSearchComponent", () => {
+ let component: UsersSettingsSearchComponent;
+ let fixture: ComponentFixture<UsersSettingsSearchComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ UsersSettingsModule,
+ I18nModule
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UsersSettingsSearchComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should set filter parameter", () => {
+ const departmentName = "test";
+ const departmentGroupName = "testGroup";
+ component.setFilterParameter("departmentName", departmentName);
+ expect(component.appValue).toEqual({departmentName});
+
+ component.setFilterParameter("departmentGroupName", departmentGroupName);
+ expect(component.appValue).toEqual({departmentName, departmentGroupName});
+
+ component.setFilterParameter("departmentGroupName", departmentGroupName, true);
+ expect(component.appValue).toEqual({departmentGroupName});
+ });
+
+ it("should toggle filter parameter", () => {
+ const departmentName = "test";
+ const departmentGroupName = "testGroup";
+ component.toggleFilterParameter("departmentName", departmentName);
+ expect(component.appValue).toEqual({departmentName});
+
+ component.toggleFilterParameter("departmentGroupName", departmentGroupName);
+ expect(component.appValue).toEqual({departmentName, departmentGroupName});
+
+ component.toggleFilterParameter("departmentGroupName", departmentGroupName);
+ expect(component.appValue).toEqual({departmentName, departmentGroupName: undefined});
+ });
+
+});
diff --git a/src/app/features/settings/users/components/users-search/users-settings-search.component.ts b/src/app/features/settings/users/components/users-search/users-settings-search.component.ts
new file mode 100644
index 0000000..ea02ebe
--- /dev/null
+++ b/src/app/features/settings/users/components/users-search/users-settings-search.component.ts
@@ -0,0 +1,70 @@
+/********************************************************************************
+ * 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 {TranslateService} from "@ngx-translate/core";
+import {Observable, of} from "rxjs";
+import {map, switchMap} from "rxjs/operators";
+import {ALL_NON_TRIVIAL_USER_ROLES, EAPIUserRoles, IAPIDepartmentGroups, IAPIUserInfoExtended} from "../../../../../core";
+import {AbstractControlValueAccessorComponent} from "../../../../../shared/controls/common";
+import {ISelectOption} from "../../../../../shared/controls/select";
+import {IUserListFilter, roleToText} from "../../pipes";
+
+export interface IUserPlusSearch extends IAPIUserInfoExtended {
+ searchString?: string;
+}
+
+@Component({
+ selector: "app-users-settings-search",
+ templateUrl: "./users-settings-search.component.html",
+ styleUrls: ["./users-settings-search.component.scss"]
+})
+export class UsersSettingsSearchComponent extends AbstractControlValueAccessorComponent<IUserListFilter> {
+
+ @Input()
+ public appLoading: boolean;
+
+ @Input()
+ public appDepartmentGroups: IAPIDepartmentGroups;
+
+ public userRoleOptions$: Observable<ISelectOption<EAPIUserRoles>[]> = of(ALL_NON_TRIVIAL_USER_ROLES).pipe(
+ switchMap((roles) => this.translationService.get(roles.map(roleToText)).pipe(
+ map((translatedRoles) => roles.map((role) => {
+ return {label: translatedRoles[roleToText(role)], value: role};
+ }))
+ ))
+ );
+
+ public constructor(
+ private translationService: TranslateService
+ ) {
+ super();
+ }
+
+ public toggleFilterParameter(key: keyof IUserListFilter, value: string) {
+ const oldValue = {...this.appValue}[key];
+ this.setFilterParameter(key, oldValue != null ? undefined : value);
+ }
+
+ public setFilterParameter(key: keyof IUserListFilter, value: string, removeDepartmentNameFilter?: boolean) {
+ const newValue: IUserListFilter = {
+ ...this.appValue,
+ [key]: value
+ };
+ if (removeDepartmentNameFilter) {
+ delete newValue.departmentName;
+ }
+ this.writeValue(newValue, true);
+ }
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/users/components/users-settings-edit/index.ts
similarity index 91%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/users/components/users-settings-edit/index.ts
index 990bb42..a880da1 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/users/components/users-settings-edit/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./users-settings-edit.component";
diff --git a/src/app/features/settings/users/components/users-settings-edit/users-settings-edit.component.html b/src/app/features/settings/users/components/users-settings-edit/users-settings-edit.component.html
new file mode 100644
index 0000000..14ad5c1
--- /dev/null
+++ b/src/app/features/settings/users/components/users-settings-edit/users-settings-edit.component.html
@@ -0,0 +1,58 @@
+<!-------------------------------------------------------------------------------
+ * 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]="false"
+ [appTitle]="('settings.users.editTitle' | translate) + appSelectedUser?.firstName + ' ' + appSelectedUser?.lastName + ' (' + appSelectedUser?.userName + ')'">
+
+ <div [formGroup]="formGroup" style="padding: 1em;">
+ <div class="email">
+ <span class="email--identifier">{{"settings.users.email" | translate}}</span>
+ <input [formControlName]="'email'"
+ [placeholder]="'settings.users.emailPlaceholder' | translate"
+ [type]="'email'"
+ appFormControlStatus
+ class="openk-input openk-info email--input"/>
+ </div>
+
+ <div *ngIf="appSelectedUser | appIsUserDivisionMember" class="department">
+ <span>{{"settings.users.departmentGroup" | translate}}</span>
+ <app-select #groupSelect
+ (appValueChange)="patchValue({departmentName: null})"
+ [appDisabled]="appDisabled"
+ [appOptions]="appDepartments | appOptionsFromDepartmentStructure | async"
+ [appPlaceholder]="'settings.users.departmentGroupPlaceholder' | translate"
+ [formControlName]="'departmentGroupName'"
+ class="openk-info department--select">
+ </app-select>
+ <span>{{"settings.users.department" | translate}}</span>
+ <app-select
+ [appDisabled]="appDisabled || ((appDepartments | appOptionsForDepartmentGroup: groupSelect.appValue).length === 0)"
+ [appOptions]="appDepartments | appOptionsForDepartmentGroup: groupSelect.appValue"
+ [appPlaceholder]="'settings.users.departmentPlaceholder' | translate"
+ [formControlName]="'departmentName'"
+ class="openk-info department--select"
+ ></app-select>
+
+ </div>
+
+ <div class="buttons">
+ <button (click)="submit(appSelectedUser.id)" [disabled]="appDisabled"
+ class="openk-button openk-success">
+ {{"settings.users.save" | translate}}
+ </button>
+ </div>
+ </div>
+
+</app-collapsible>
+
diff --git a/src/app/features/settings/users/components/users-settings-edit/users-settings-edit.component.scss b/src/app/features/settings/users/components/users-settings-edit/users-settings-edit.component.scss
new file mode 100644
index 0000000..f10fc7c
--- /dev/null
+++ b/src/app/features/settings/users/components/users-settings-edit/users-settings-edit.component.scss
@@ -0,0 +1,54 @@
+/********************************************************************************
+ * 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 "../../../../../../styles/openk.styles";
+
+:host {
+ width: 100%;
+}
+
+.email {
+ display: flex;
+ width: 100%;
+ align-items: center;
+ margin-bottom: 1em;
+}
+
+.email--identifier {
+ margin-right: 0.5em;
+}
+
+.email--input {
+ flex: 1;
+}
+
+.department {
+ display: flex;
+ width: 100%;
+ align-items: center;
+ margin-bottom: 1em;
+
+ > * {
+ margin-right: 0.5em;
+ }
+}
+
+.department--select {
+ flex: 1 1 15em;
+ width: 15em;
+}
+
+.buttons {
+ display: flex;
+ justify-content: flex-end;
+}
diff --git a/src/app/features/settings/users/components/users-settings-edit/users-settings-edit.component.spec.ts b/src/app/features/settings/users/components/users-settings-edit/users-settings-edit.component.spec.ts
new file mode 100644
index 0000000..824e3b9
--- /dev/null
+++ b/src/app/features/settings/users/components/users-settings-edit/users-settings-edit.component.spec.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 {SimpleChange} from "@angular/core";
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {I18nModule, IAPIUserInfoExtended, IAPIUserSettings} from "../../../../../core";
+import {EErrorCode} from "../../../../../store";
+import {UsersSettingsModule} from "../../users-settings.module";
+import {IUserSettingsEditFormValue, UsersSettingsEditComponent} from "./users-settings-edit.component";
+
+describe("UsersSettingsEditComponent", () => {
+ let component: UsersSettingsEditComponent;
+ let fixture: ComponentFixture<UsersSettingsEditComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ UsersSettingsModule,
+ I18nModule
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UsersSettingsEditComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should disable form", () => {
+ expect(component.formGroup.disabled).toBe(false);
+ component.appDisabled = true;
+ component.ngOnChanges({
+ appDisabled: new SimpleChange(false, component.appDisabled, false)
+ });
+ expect(component.formGroup.disabled).toBe(true);
+ });
+
+ it("should patch form", () => {
+ const email = "A";
+ const departmentName = "B";
+ const departmentGroupName = "C";
+ const value: IUserSettingsEditFormValue = {email, departmentName, departmentGroupName};
+ component.appSelectedUser = {
+ ...{} as IAPIUserInfoExtended,
+ settings: {
+ email,
+ department: {
+ group: departmentGroupName,
+ name: departmentName
+ }
+ }
+ };
+ component.ngOnChanges({
+ appSelectedUser: new SimpleChange(null, component.appSelectedUser, false)
+ });
+ expect(component.formGroup.value).toEqual(value);
+ });
+
+ it("should submit form", () => {
+ const id = 19;
+ const email = "abc@ef.gh";
+ const departmentName = "B";
+ const departmentGroupName = "C";
+ const formValue: IUserSettingsEditFormValue = {email, departmentName, departmentGroupName};
+ const userSettings: IAPIUserSettings = {email, department: {name: departmentName, group: departmentGroupName}};
+ spyOn(component.appError, "emit");
+ spyOn(component.appSubmit, "emit");
+
+ component.patchValue({...formValue, email: "abc"});
+ component.submit(id);
+ expect(component.appError.emit).toHaveBeenCalledWith(EErrorCode.BAD_USER_DATA);
+
+ component.patchValue({...formValue, departmentName: null});
+ component.submit(id);
+ expect(component.appError.emit).toHaveBeenCalledWith(EErrorCode.MISSING_FORM_DATA);
+
+ component.patchValue(formValue);
+ component.submit(id);
+ expect(component.appSubmit.emit).toHaveBeenCalledWith({id, value: userSettings});
+ });
+
+});
diff --git a/src/app/features/settings/users/components/users-settings-edit/users-settings-edit.component.ts b/src/app/features/settings/users/components/users-settings-edit/users-settings-edit.component.ts
new file mode 100644
index 0000000..99c453f
--- /dev/null
+++ b/src/app/features/settings/users/components/users-settings-edit/users-settings-edit.component.ts
@@ -0,0 +1,95 @@
+/********************************************************************************
+ * 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, OnChanges, Output, SimpleChanges} from "@angular/core";
+import {FormControl, Validators} from "@angular/forms";
+import {IAPIUserInfoExtended, IAPIUserSettings} from "../../../../../core";
+import {EErrorCode} from "../../../../../store";
+import {createFormGroup} from "../../../../../util/forms";
+
+export interface IUserSettingsEditFormValue {
+ email: string;
+ departmentGroupName?: string;
+ departmentName?: string;
+}
+
+@Component({
+ selector: "app-users-settings-edit",
+ templateUrl: "./users-settings-edit.component.html",
+ styleUrls: ["./users-settings-edit.component.scss"]
+})
+export class UsersSettingsEditComponent implements OnChanges {
+
+ @Input()
+ public appSelectedUser: IAPIUserInfoExtended;
+
+ @Input()
+ public appDisabled: boolean;
+
+ @Input()
+ public appDepartments: { key: string, value: string[] }[];
+
+ @Output()
+ public appSubmit = new EventEmitter<{ id: number, value: IAPIUserSettings }>();
+
+ @Output()
+ public appError = new EventEmitter<EErrorCode>();
+
+ public formGroup = createFormGroup<IUserSettingsEditFormValue>({
+ email: new FormControl("", [Validators.email, Validators.required]),
+ departmentGroupName: new FormControl(null),
+ departmentName: new FormControl(null)
+ });
+
+ public patchValue(value: Partial<IUserSettingsEditFormValue>) {
+ this.formGroup.patchValue({...value});
+ }
+
+ public getValue(): IUserSettingsEditFormValue {
+ return this.formGroup.value;
+ }
+
+ public ngOnChanges(changes: SimpleChanges) {
+ const onChange = (keys: Array<keyof UsersSettingsEditComponent>, fn: () => any) => {
+ if (keys.some((key) => changes[key])) {
+ fn();
+ }
+ };
+ onChange(["appSelectedUser"], () => this.patchValue({
+ email: this.appSelectedUser.settings?.email,
+ departmentGroupName: this.appSelectedUser.settings?.department?.group,
+ departmentName: this.appSelectedUser.settings?.department?.name
+ }));
+ onChange(["appDisabled"], () => this.appDisabled ? this.formGroup.disable() : this.formGroup.enable());
+ }
+
+ public submit(id: number) {
+ const {email, departmentName, departmentGroupName} = {...this.getValue()};
+ this.formGroup.markAsTouched();
+
+ if (this.formGroup.invalid) {
+ this.appError.emit(EErrorCode.BAD_USER_DATA);
+ return;
+ }
+
+ if (departmentGroupName != null && departmentName == null) {
+ this.appError.emit(EErrorCode.MISSING_FORM_DATA);
+ return;
+ }
+
+ this.appSubmit.emit({
+ id,
+ value: {email, department: {name: departmentName, group: departmentGroupName}}
+ });
+ }
+}
diff --git a/src/app/features/settings/users/components/users-settings.component.html b/src/app/features/settings/users/components/users-settings.component.html
new file mode 100644
index 0000000..92338c5
--- /dev/null
+++ b/src/app/features/settings/users/components/users-settings.component.html
@@ -0,0 +1,57 @@
+<!-------------------------------------------------------------------------------
+ * 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-settings-side-menu
+ [appForAdmin]="true">
+</app-settings-side-menu>
+
+<app-side-menu-status *appSideMenu="'center'"
+ [appLoadingMessage]="'core.loading' | translate"
+ [appLoading]="loading$ | async">
+</app-side-menu-status>
+
+<ng-container *appSideMenu="'bottom'">
+ <app-action-button (appClick)="sync()"
+ [appDisabled]="false"
+ [appIcon]="'refresh'"
+ class="openk-success side-menu-button">
+ {{"settings.users.sync" | translate}}
+ </app-action-button>
+</ng-container>
+
+<div class="title">
+ <span class="title--label">
+ {{"settings.title" | translate}} - {{"settings.users.title" | translate}}
+ </span>
+</div>
+
+<app-users-settings-search
+ [appLoading]="loading$ | async"
+ [(appValue)]="filter"
+ [appDepartmentGroups]="departmentSettings$ | async | getDepartmentGroupsFromTablePipe">
+</app-users-settings-search>
+
+<app-users-settings-table
+ (appSelectedUserChange)="selectedUserId = $event;"
+ [appUsers]="users$ | async | filterUserList : filter"
+ class="user-table">
+</app-users-settings-table>
+
+<app-users-settings-edit (appError)="showErrorMessage($event)"
+ (appSubmit)="submitUserSettings($event?.id, $event.value)"
+ *ngIf="selectedUserId != null"
+ [appDepartments]="departmentSettings$ | async | getDepartmentGroupsFromTablePipe | objToArray"
+ [appDisabled]="(loading$ | async)"
+ [appSelectedUser]="users$ | async | appGetUserForId : selectedUserId"
+ class="user-edit">
+</app-users-settings-edit>
diff --git a/src/app/shared/leaflet/components/leaflet-map.component.scss b/src/app/features/settings/users/components/users-settings.component.scss
similarity index 60%
copy from src/app/shared/leaflet/components/leaflet-map.component.scss
copy to src/app/features/settings/users/components/users-settings.component.scss
index e8e1a36..065a5e4 100644
--- a/src/app/shared/leaflet/components/leaflet-map.component.scss
+++ b/src/app/features/settings/users/components/users-settings.component.scss
@@ -11,43 +11,41 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "openk.styles";
-
:host {
width: 100%;
- height: 100%;
-
+ padding: 1em;
+ box-sizing: border-box;
display: flex;
flex-flow: column;
+ min-height: 100%;
+
+ max-width: 70em;
+ margin: 0 auto 0 auto;
}
-.map {
- height: 100%;
+.title {
+ margin-bottom: 1em;
+}
+
+.title--label {
+ font-size: x-large;
+ font-weight: 600;
+}
+
+.side-menu-button {
width: 100%;
- position: relative;
- overflow: hidden;
- box-sizing: border-box;
- border: 1px solid $openk-form-border;
+
+ & + & {
+ margin-top: 1em;
+ }
}
-.map--leaflet {
- width: 100%;
- height: 100%;
+.user-table {
+ flex: 1 1 12em;
+ min-height: 12em;
+ margin-bottom: auto;
}
-.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;
+.user-edit {
+ margin-top: 1em;
}
diff --git a/src/app/features/settings/users/components/users-settings.component.spec.ts b/src/app/features/settings/users/components/users-settings.component.spec.ts
new file mode 100644
index 0000000..349a716
--- /dev/null
+++ b/src/app/features/settings/users/components/users-settings.component.spec.ts
@@ -0,0 +1,88 @@
+/********************************************************************************
+ * 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 {Store} from "@ngrx/store";
+import {provideMockStore} from "@ngrx/store/testing";
+import {I18nModule, IAPIUserSettings} from "../../../../core";
+import {
+ EErrorCode,
+ fetchDepartmentsSettingsAction,
+ fetchUsersAction,
+ setErrorAction,
+ submitUserSettingsAction,
+ syncUserDataAction
+} from "../../../../store";
+import {UsersSettingsModule} from "../users-settings.module";
+import {UsersSettingsComponent} from "./users-settings.component";
+
+describe("UsersSettingsComponent", () => {
+ let component: UsersSettingsComponent;
+ let fixture: ComponentFixture<UsersSettingsComponent>;
+ let store: Store;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ UsersSettingsModule,
+ RouterTestingModule,
+ I18nModule
+ ],
+ providers: [
+ provideMockStore()
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UsersSettingsComponent);
+ component = fixture.componentInstance;
+ store = fixture.componentRef.injector.get(Store);
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should fetch users data and department settings", () => {
+ const dispatchSpy = spyOn(store, "dispatch");
+ component.ngOnInit();
+ expect(dispatchSpy).toHaveBeenCalledWith(fetchUsersAction());
+ expect(dispatchSpy).toHaveBeenCalledWith(fetchDepartmentsSettingsAction());
+ });
+
+ it("should dispatch syncUserDataAction", () => {
+ const dispatchSpy = spyOn(store, "dispatch");
+ component.sync();
+ expect(dispatchSpy).toHaveBeenCalledWith(syncUserDataAction());
+ });
+
+ it("should submit user settings", () => {
+ const dispatchSpy = spyOn(store, "dispatch");
+ const userId = 19;
+ const data: IAPIUserSettings = {
+ email: "abc@ef.gh"
+ };
+ component.submitUserSettings(userId, data);
+ expect(dispatchSpy).toHaveBeenCalledWith(submitUserSettingsAction({userId, data}));
+ });
+
+ it("show error message", () => {
+ const dispatchSpy = spyOn(store, "dispatch");
+ component.showErrorMessage(EErrorCode.MISSING_FORM_DATA);
+ expect(dispatchSpy).toHaveBeenCalledWith(setErrorAction({error: EErrorCode.MISSING_FORM_DATA}));
+ });
+
+});
diff --git a/src/app/features/settings/users/components/users-settings.component.ts b/src/app/features/settings/users/components/users-settings.component.ts
new file mode 100644
index 0000000..c4918fb
--- /dev/null
+++ b/src/app/features/settings/users/components/users-settings.component.ts
@@ -0,0 +1,69 @@
+/********************************************************************************
+ * 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 {IAPIUserSettings} from "../../../../core/api/settings";
+import {
+ EErrorCode,
+ fetchDepartmentsSettingsAction,
+ fetchUsersAction,
+ getDepartmentsSettingsSelector,
+ getSettingsLoadingSelector,
+ getUsersSettingsSelector,
+ setErrorAction,
+ submitUserSettingsAction,
+ syncUserDataAction
+} from "../../../../store";
+import {IUserListFilter} from "../pipes";
+
+@Component({
+ selector: "app-users-settings",
+ templateUrl: "./users-settings.component.html",
+ styleUrls: ["./users-settings.component.scss"]
+})
+export class UsersSettingsComponent implements OnInit {
+
+ public users$ = this.store.pipe(select(getUsersSettingsSelector));
+
+ public loading$ = this.store.pipe(select(getSettingsLoadingSelector));
+
+ public departmentSettings$ = this.store.pipe(select(getDepartmentsSettingsSelector));
+
+ public selectedUserId: number;
+
+ public filter: IUserListFilter = {};
+
+ public constructor(
+ private readonly store: Store
+ ) {
+ }
+
+ public ngOnInit() {
+ this.store.dispatch(fetchUsersAction());
+ this.store.dispatch(fetchDepartmentsSettingsAction());
+ }
+
+ public sync() {
+ this.store.dispatch(syncUserDataAction());
+ }
+
+ public submitUserSettings(userId: number, data: IAPIUserSettings) {
+ this.store.dispatch(submitUserSettingsAction({userId, data}));
+ }
+
+ public showErrorMessage(error: EErrorCode) {
+ this.store.dispatch(setErrorAction({error}));
+ }
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/users/components/users-table/index.ts
similarity index 91%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/users/components/users-table/index.ts
index 990bb42..b6bc5b4 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/users/components/users-table/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./users-settings-table.component";
diff --git a/src/app/features/settings/users/components/users-table/users-settings-table.component.html b/src/app/features/settings/users/components/users-table/users-settings-table.component.html
new file mode 100644
index 0000000..4aa72e5
--- /dev/null
+++ b/src/app/features/settings/users/components/users-table/users-settings-table.component.html
@@ -0,0 +1,107 @@
+<!-------------------------------------------------------------------------------
+ * 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
+ -------------------------------------------------------------------------------->
+
+<table [dataSource]="appUsers" cdk-table
+ class="openk-table openk-table---without-last-border">
+
+ <caption hidden>{{ "settings.users.title" | translate}}</caption>
+
+ <tr *cdkHeaderRowDef="appColumns; sticky: true" cdk-header-row></tr>
+
+ <tr *cdkRowDef="let myRowData; columns: appColumns" cdk-row class="table-row"></tr>
+
+ <ng-container cdkColumnDef="user-name">
+ <th *cdkHeaderCellDef="let header"
+ cdk-header-cell
+ class="table-column"
+ scope="col">
+ {{"settings.users.table.userName" | translate}}
+ </th>
+ <td (click)="onSelect(user)"
+ *cdkCellDef="let user"
+ [class.contact-list--table--cell---highlight]="user?.id === appSelectedUser"
+ cdk-cell
+ class="table-column">
+ {{user?.userName}}
+ </td>
+ </ng-container>
+
+ <ng-container cdkColumnDef="first-name">
+ <th *cdkHeaderCellDef="let header"
+ cdk-header-cell
+ class="table-column"
+ scope="col">
+ {{"settings.users.table.firstName" | translate}}
+ </th>
+ <td (click)="onSelect(user)"
+ *cdkCellDef="let user"
+ [class.contact-list--table--cell---highlight]="user?.id === appSelectedUser"
+ cdk-cell
+ class="table-column">
+ {{user?.firstName}}
+ </td>
+ </ng-container>
+
+ <ng-container cdkColumnDef="last-name">
+ <th *cdkHeaderCellDef="let header"
+ cdk-header-cell
+ class="table-column"
+ scope="col">
+ {{"settings.users.table.lastName" | translate}}
+ </th>
+ <td (click)="onSelect(user)"
+ *cdkCellDef="let user"
+ [class.contact-list--table--cell---highlight]="user?.id === appSelectedUser"
+ cdk-cell
+ class="table-column">
+ {{user?.lastName}}
+ </td>
+ </ng-container>
+
+ <ng-container cdkColumnDef="email">
+ <th *cdkHeaderCellDef="let header"
+ cdk-header-cell
+ class="table-column"
+ scope="col">
+ {{"settings.users.table.email" | translate}}
+ </th>
+ <td (click)="onSelect(user)"
+ *cdkCellDef="let user"
+ [class.contact-list--table--cell---highlight]="user?.id === appSelectedUser"
+ cdk-cell
+ class="table-column">
+ {{user?.settings?.email}}
+ </td>
+ </ng-container>
+
+ <ng-container cdkColumnDef="roles">
+ <th *cdkHeaderCellDef="let header"
+ cdk-header-cell
+ class="table-column"
+ scope="col">
+ {{"settings.users.table.role" | translate}}
+ </th>
+ <td (click)="onSelect(user)"
+ *cdkCellDef="let user"
+ [class.contact-list--table--cell---highlight]="user?.id === appSelectedUser"
+ cdk-cell
+ class="table-column">
+ <div class="roles">
+ <span *ngFor="let role of (user?.roles | appRolesToDisplayText); let last = last;" class="role">
+ {{(role | translate) + (last ? '' : ', ')}}
+ </span>
+ </div>
+ </td>
+ </ng-container>
+
+</table>
diff --git a/src/app/features/settings/users/components/users-table/users-settings-table.component.scss b/src/app/features/settings/users/components/users-table/users-settings-table.component.scss
new file mode 100644
index 0000000..423b369
--- /dev/null
+++ b/src/app/features/settings/users/components/users-table/users-settings-table.component.scss
@@ -0,0 +1,66 @@
+/********************************************************************************
+ * 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 "../../../../../../styles/openk.styles";
+
+:host {
+ display: flex;
+ flex-flow: column;
+ width: 100%;
+ position: relative;
+ overflow: auto;
+ box-sizing: border-box;
+ border: 1px solid $openk-form-border;
+ border-radius: 4px;
+ background: $openk-background-card;
+ padding-bottom: 2px;
+}
+
+.openk-table---last-row-without-border:host {
+ padding-bottom: 0;
+}
+
+.table-column {
+ vertical-align: baseline;
+ text-align: start;
+}
+
+.table-row {
+ cursor: pointer;
+}
+
+.list--element--icon {
+ width: initial;
+ height: initial;
+ font-size: 0.5em;
+ margin-right: 1em;
+}
+
+.list--element---bold {
+ font-weight: 600;
+ margin-right: 0.25em;
+}
+
+.contact-list--table--cell---highlight {
+ background-color: get-color($openk-info-palette, 500);
+ color: get-color($openk-info-palette, 500, contrast);
+}
+
+.roles {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.role {
+ margin-right: 0.25em;
+}
diff --git a/src/app/features/settings/users/components/users-table/users-settings-table.component.spec.ts b/src/app/features/settings/users/components/users-table/users-settings-table.component.spec.ts
new file mode 100644
index 0000000..f622c39
--- /dev/null
+++ b/src/app/features/settings/users/components/users-table/users-settings-table.component.spec.ts
@@ -0,0 +1,58 @@
+/********************************************************************************
+ * 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 {Store} from "@ngrx/store";
+import {provideMockStore} from "@ngrx/store/testing";
+import {I18nModule, IAPIUserInfoExtended} from "../../../../../core";
+import {UsersSettingsModule} from "../../users-settings.module";
+import {UsersSettingsTableComponent} from "./users-settings-table.component";
+
+describe("UsersSettingsTableComponent", () => {
+ let component: UsersSettingsTableComponent;
+ let fixture: ComponentFixture<UsersSettingsTableComponent>;
+ let store: Store;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ UsersSettingsModule,
+ RouterTestingModule,
+ I18nModule
+ ],
+ providers: [
+ provideMockStore()
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UsersSettingsTableComponent);
+ component = fixture.componentInstance;
+ store = fixture.componentRef.injector.get(Store);
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should emit user id when user is selected", () => {
+ const userChangeSpy = spyOn(component.appSelectedUserChange, "emit");
+ component.onSelect({id: 1} as IAPIUserInfoExtended);
+ expect(userChangeSpy).toHaveBeenCalledWith(1);
+ });
+
+});
diff --git a/src/app/features/settings/users/components/users-table/users-settings-table.component.ts b/src/app/features/settings/users/components/users-table/users-settings-table.component.ts
new file mode 100644
index 0000000..c20bc36
--- /dev/null
+++ b/src/app/features/settings/users/components/users-table/users-settings-table.component.ts
@@ -0,0 +1,40 @@
+/********************************************************************************
+ * 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 {IAPIUserInfoExtended} from "../../../../../core";
+
+@Component({
+ selector: "app-users-settings-table",
+ templateUrl: "./users-settings-table.component.html",
+ styleUrls: ["./users-settings-table.component.scss"]
+})
+export class UsersSettingsTableComponent {
+
+ @Input()
+ public appUsers: IAPIUserInfoExtended[];
+
+ @Input()
+ public appColumns = ["user-name", "first-name", "last-name", "email", "roles"];
+
+ @Output()
+ public appSelectedUserChange = new EventEmitter<number>();
+
+ public appSelectedUser: number = null;
+
+ public onSelect(user: IAPIUserInfoExtended) {
+ this.appSelectedUser = user.id;
+ this.appSelectedUserChange.emit(this.appSelectedUser);
+ }
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/features/settings/users/index.ts
similarity index 85%
copy from src/app/features/settings/components/index.ts
copy to src/app/features/settings/users/index.ts
index 990bb42..1e9e094 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/features/settings/users/index.ts
@@ -11,4 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./components";
+export * from "./pipes";
+
+export * from "./users-settings.module";
diff --git a/src/app/features/settings/users/pipes/filter-user-list.pipe.spec.ts b/src/app/features/settings/users/pipes/filter-user-list.pipe.spec.ts
new file mode 100644
index 0000000..22d50b9
--- /dev/null
+++ b/src/app/features/settings/users/pipes/filter-user-list.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 {EAPIUserRoles, IAPIUserInfoExtended} from "../../../../core";
+import {FilterUserListPipe} from "./filter-user-list.pipe";
+
+describe("FilterUserListPipe", () => {
+ const pipe = new FilterUserListPipe();
+ const departmentName = "departmentName";
+ const departmentGroupName = "departmentGroupName";
+ const userList: IAPIUserInfoExtended[] = [
+ createUserObject(17, EAPIUserRoles.SPA_ADMIN),
+ createUserObject(18, EAPIUserRoles.SPA_OFFICIAL_IN_CHARGE),
+ createUserObject(19, EAPIUserRoles.DIVISION_MEMBER, departmentGroupName, departmentName)
+ ];
+
+ it("should filter the user list", () => {
+ expect(pipe.transform(userList)).toEqual(userList);
+ expect(pipe.transform(userList, {q: "19"})).toEqual([userList[2]]);
+ expect(pipe.transform(userList, {role: EAPIUserRoles.DIVISION_MEMBER})).toEqual([userList[2]]);
+ expect(pipe.transform(userList, {departmentName})).toEqual([userList[2]]);
+ expect(pipe.transform(userList, {departmentGroupName})).toEqual([userList[2]]);
+ });
+
+});
+
+function createUserObject(id: number, role: EAPIUserRoles, departmentGroupName?: string, departmenName?: string): IAPIUserInfoExtended {
+ return {
+ ...{} as IAPIUserInfoExtended,
+ id,
+ roles: [role],
+ settings: {
+ email: "test@email" + id + ".org",
+ department: departmenName == null ? undefined : {
+ group: departmentGroupName,
+ name: departmenName
+ }
+ }
+ };
+}
diff --git a/src/app/features/settings/users/pipes/filter-user-list.pipe.ts b/src/app/features/settings/users/pipes/filter-user-list.pipe.ts
new file mode 100644
index 0000000..58749c1
--- /dev/null
+++ b/src/app/features/settings/users/pipes/filter-user-list.pipe.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 {Pipe, PipeTransform} from "@angular/core";
+import {EAPIUserRoles, IAPIUserInfoExtended} from "../../../../core";
+import {arrayJoin, filterDistinctValues} from "../../../../util";
+
+export interface IUserListFilter {
+ q?: string;
+ role?: EAPIUserRoles;
+ departmentGroupName?: string;
+ departmentName?: string;
+}
+
+@Pipe({name: "filterUserList"})
+export class FilterUserListPipe implements PipeTransform {
+
+ public transform(value: IAPIUserInfoExtended[], filter?: IUserListFilter): IAPIUserInfoExtended[] {
+ filter = {...filter};
+ return arrayJoin(value)
+ .filter((user) => {
+ const department = {...user?.settings?.department};
+ return user != null
+ && (filter.departmentGroupName == null || department.group === filter.departmentGroupName)
+ && (filter.departmentName == null || department.name === filter.departmentName)
+ && (filter.role == null || user.roles.includes(filter.role));
+ })
+ .filter((user) => {
+ const searchString = filterDistinctValues(arrayJoin([
+ user.firstName,
+ user.lastName,
+ user.userName,
+ user.settings?.email
+ ])).join(" ").toLowerCase().trim();
+ const searchTokens = filter?.q?.toLowerCase()
+ .replace(/[;,.]/g, " ")
+ .split(" ");
+ return !arrayJoin(searchTokens).some((_) => !searchString.includes(_));
+ });
+ }
+
+}
diff --git a/src/app/features/settings/users/pipes/get-user-for-id.pipe.spec.ts b/src/app/features/settings/users/pipes/get-user-for-id.pipe.spec.ts
new file mode 100644
index 0000000..ed3c0f7
--- /dev/null
+++ b/src/app/features/settings/users/pipes/get-user-for-id.pipe.spec.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 {IAPIUserInfoExtended} from "../../../../core";
+import {GetUserForIdPipe} from "./get-user-for-id.pipe";
+
+describe("GetUserForIdPipe", () => {
+
+ const pipe = new GetUserForIdPipe();
+ const user = {id: 2} as IAPIUserInfoExtended;
+ const users: IAPIUserInfoExtended[] = [
+ {id: 1},
+ user,
+ {id: 3},
+ {id: 4}
+ ] as IAPIUserInfoExtended[];
+
+ describe("transform", () => {
+
+ it("should return the user with the given id", () => {
+ const result = pipe.transform(users, 2);
+ expect(result).toBeTruthy();
+ expect(result).toEqual(user);
+ });
+ });
+});
+
diff --git a/src/app/features/settings/components/search/settings.component.ts b/src/app/features/settings/users/pipes/get-user-for-id.pipe.ts
similarity index 62%
rename from src/app/features/settings/components/search/settings.component.ts
rename to src/app/features/settings/users/pipes/get-user-for-id.pipe.ts
index edced66..4412627 100644
--- a/src/app/features/settings/components/search/settings.component.ts
+++ b/src/app/features/settings/users/pipes/get-user-for-id.pipe.ts
@@ -11,13 +11,17 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {Component} from "@angular/core";
+import {Pipe, PipeTransform} from "@angular/core";
+import {IAPIUserInfoExtended} from "../../../../core";
-@Component({
- selector: "app-settings",
- templateUrl: "./settings.component.html",
- styleUrls: ["./settings.component.scss"]
+@Pipe({
+ name: "appGetUserForId"
})
-export class SettingsComponent {
+export class GetUserForIdPipe implements PipeTransform {
+
+ public transform(users: IAPIUserInfoExtended[], id: number): IAPIUserInfoExtended {
+ return users.find((_) => _.id === id);
+ }
}
+
diff --git a/src/app/shared/leaflet/index.ts b/src/app/features/settings/users/pipes/index.ts
similarity index 66%
copy from src/app/shared/leaflet/index.ts
copy to src/app/features/settings/users/pipes/index.ts
index 849c149..a8205fe 100644
--- a/src/app/shared/leaflet/index.ts
+++ b/src/app/features/settings/users/pipes/index.ts
@@ -11,9 +11,9 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./directives";
-export * from "./pipes";
-export * from "./util";
-
-export * from "./leaflet.module";
-export * from "./leaflet-configuration.token";
+export * from "./filter-user-list.pipe";
+export * from "./get-user-for-id.pipe";
+export * from "./is-user-division-member.pipe";
+export * from "./options-for-department-group.pipe";
+export * from "./options-from-department-structure.pipe";
+export * from "./roles-to-display-text.pipe";
diff --git a/src/app/features/settings/users/pipes/is-user-division-member.pipe.spec.ts b/src/app/features/settings/users/pipes/is-user-division-member.pipe.spec.ts
new file mode 100644
index 0000000..40da5f4
--- /dev/null
+++ b/src/app/features/settings/users/pipes/is-user-division-member.pipe.spec.ts
@@ -0,0 +1,42 @@
+/********************************************************************************
+ * 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 {EAPIUserRoles, IAPIUserInfoExtended} from "../../../../core";
+import {IsUserDivisionMemberPipe} from "./is-user-division-member.pipe";
+
+describe("IsUserDivisionMemberPipe", () => {
+
+ const pipe = new IsUserDivisionMemberPipe();
+ const divisionMember: IAPIUserInfoExtended = {
+ roles: [
+ EAPIUserRoles.DIVISION_MEMBER,
+ EAPIUserRoles.SPA_CUSTOMER
+ ]
+ } as IAPIUserInfoExtended;
+ const officialInCharge: IAPIUserInfoExtended = {
+ roles: [
+ EAPIUserRoles.SPA_OFFICIAL_IN_CHARGE
+ ]
+ } as IAPIUserInfoExtended;
+
+ describe("transform", () => {
+
+ it("should return if the given user has role division member", () => {
+ let result = pipe.transform(divisionMember);
+ expect(result).toBeTrue();
+ result = pipe.transform(officialInCharge);
+ expect(result).toBeFalse();
+ });
+ });
+});
+
diff --git a/src/app/features/mail/pipes/sender-split-name-mail.pipe.ts b/src/app/features/settings/users/pipes/is-user-division-member.pipe.ts
similarity index 62%
copy from src/app/features/mail/pipes/sender-split-name-mail.pipe.ts
copy to src/app/features/settings/users/pipes/is-user-division-member.pipe.ts
index 3cfb33d..cab16d6 100644
--- a/src/app/features/mail/pipes/sender-split-name-mail.pipe.ts
+++ b/src/app/features/settings/users/pipes/is-user-division-member.pipe.ts
@@ -12,16 +12,16 @@
********************************************************************************/
import {Pipe, PipeTransform} from "@angular/core";
-import {arrayJoin} from "../../../util/store";
+import {EAPIUserRoles, IAPIUserInfoExtended} from "../../../../core";
@Pipe({
- name: "appSenderSplitNameMail"
+ name: "appIsUserDivisionMember"
})
-export class SenderSplitNameMailPipe implements PipeTransform {
+export class IsUserDivisionMemberPipe implements PipeTransform {
- public transform(text: string): Array<string> {
- const textAsArray = arrayJoin(text?.split(/(?=<)/g));
- return [textAsArray[0], textAsArray.slice(1).join("")].filter(x => x);
- // return text.split(/(?=<)(.+)/);
+ public transform(user: IAPIUserInfoExtended): boolean {
+ return user?.roles?.find((_) => _ === EAPIUserRoles.DIVISION_MEMBER) != null;
}
+
}
+
diff --git a/src/app/features/settings/users/pipes/options-for-department-group.pipe.spec.ts b/src/app/features/settings/users/pipes/options-for-department-group.pipe.spec.ts
new file mode 100644
index 0000000..7083144
--- /dev/null
+++ b/src/app/features/settings/users/pipes/options-for-department-group.pipe.spec.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 {OptionsForDepartmentGroupPipe} from "./options-for-department-group.pipe";
+
+describe("OptionsForDepartmentGroupPipe", () => {
+
+ const pipe = new OptionsForDepartmentGroupPipe();
+
+ const departments: { key: string, value: string[] }[] = [
+ {key: "group1", value: ["group1-value1", "group1-value2"]},
+ {key: "group2", value: ["group2-value1", "group2-value2"]},
+ {key: "group3", value: ["group3-value1", "group3-value2"]}
+ ];
+
+ describe("transform", () => {
+
+ it("should return an empty array for missing inputs", () => {
+ let result = pipe.transform(null, null);
+ expect(result).toEqual([]);
+ result = pipe.transform(undefined, undefined);
+ expect(result).toEqual([]);
+ });
+
+ it("should return the options for the given group from departments", () => {
+ let result = pipe.transform(departments, "anotherValue");
+ expect(result).toEqual([]);
+ result = pipe.transform(departments, "group2");
+ expect(result).toEqual([
+ {label: "group2-value1", value: "group2-value1"},
+ {label: "group2-value2", value: "group2-value2"}
+ ]);
+ });
+ });
+});
+
diff --git a/src/app/features/settings/users/pipes/options-for-department-group.pipe.ts b/src/app/features/settings/users/pipes/options-for-department-group.pipe.ts
new file mode 100644
index 0000000..f873fc9
--- /dev/null
+++ b/src/app/features/settings/users/pipes/options-for-department-group.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 {arrayJoin} from "../../../../util";
+
+@Pipe({
+ name: "appOptionsForDepartmentGroup"
+})
+export class OptionsForDepartmentGroupPipe implements PipeTransform {
+
+ public transform(departments: { key: string, value: string[] }[], group: string): { label: string, value: string }[] {
+ return arrayJoin(arrayJoin(departments)
+ .find((_) => _.key === group)?.value)
+ .map((_) => ({label: _, value: _}));
+ }
+
+}
diff --git a/src/app/features/settings/users/pipes/options-from-department-structure.pipe.spec.ts b/src/app/features/settings/users/pipes/options-from-department-structure.pipe.spec.ts
new file mode 100644
index 0000000..5294a1d
--- /dev/null
+++ b/src/app/features/settings/users/pipes/options-from-department-structure.pipe.spec.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 {async, TestBed} from "@angular/core/testing";
+import {TranslateService} from "@ngx-translate/core";
+import {of} from "rxjs";
+import {I18nModule} from "../../../../core";
+import {OptionsFromDepartmentStructurePipe} from "./options-from-department-structure.pipe";
+
+describe("OptionsFromDepartmentStructurePipe", () => {
+
+ const translatedLabel = "translatedLabel";
+ let pipe: OptionsFromDepartmentStructurePipe;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [I18nModule]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ const translateService = TestBed.inject(TranslateService);
+ pipe = new OptionsFromDepartmentStructurePipe(TestBed.inject(TranslateService));
+ spyOn(translateService, "get").and.returnValue(of(translatedLabel));
+ });
+
+ it("should get option array for departments", async () => {
+ const groupName = "groupName";
+ await expectAsync(pipe.transform([{key: groupName, value: []}])).toBeResolvedTo([
+ {
+ label: translatedLabel,
+ value: null
+ },
+ {
+ label: groupName,
+ value: groupName
+ }
+ ]);
+ });
+
+});
+
diff --git a/src/app/features/settings/users/pipes/options-from-department-structure.pipe.ts b/src/app/features/settings/users/pipes/options-from-department-structure.pipe.ts
new file mode 100644
index 0000000..aec9c2b
--- /dev/null
+++ b/src/app/features/settings/users/pipes/options-from-department-structure.pipe.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 {Pipe, PipeTransform} from "@angular/core";
+import {TranslateService} from "@ngx-translate/core";
+import {arrayJoin} from "../../../../util";
+
+@Pipe({
+ name: "appOptionsFromDepartmentStructure"
+})
+export class OptionsFromDepartmentStructurePipe implements PipeTransform {
+
+ public constructor(private translationService: TranslateService) {
+ }
+
+ public async transform(departments: { key: string, value: string[] }[]): Promise<{ label: string, value: string }[]> {
+ const translation = await this.translationService.get("settings.users.noDepartment").toPromise();
+ return arrayJoin(
+ [{label: translation, value: null}],
+ departmentsToGroupOptions(departments)
+ );
+ }
+
+}
+
+export function departmentsToGroupOptions(departments: { key: string, value: string[] }[]) {
+ return arrayJoin(departments).map((_) => ({label: _.key, value: _.key}));
+}
diff --git a/src/app/features/settings/users/pipes/roles-to-display-text.pipe.ts b/src/app/features/settings/users/pipes/roles-to-display-text.pipe.ts
new file mode 100644
index 0000000..1889180
--- /dev/null
+++ b/src/app/features/settings/users/pipes/roles-to-display-text.pipe.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 {Pipe, PipeTransform} from "@angular/core";
+import {EAPIUserRoles} from "../../../../core";
+import {arrayJoin} from "../../../../util";
+
+@Pipe({
+ name: "appRolesToDisplayText"
+})
+export class RolesToDisplayTextPipe implements PipeTransform {
+
+ public transform(roles: EAPIUserRoles[]): string[] {
+ return arrayJoin(roles)
+ .filter((_) => _ !== EAPIUserRoles.ROLE_SPA_ACCESS)
+ .sort()
+ .map((_) => roleToText(_))
+ .filter((_) => _ !== "");
+ }
+
+}
+
+export function roleToText(role: EAPIUserRoles): string {
+ switch (role) {
+ case EAPIUserRoles.DIVISION_MEMBER:
+ case EAPIUserRoles.SPA_ADMIN:
+ case EAPIUserRoles.SPA_APPROVER:
+ case EAPIUserRoles.SPA_OFFICIAL_IN_CHARGE:
+ case EAPIUserRoles.SPA_CUSTOMER:
+ return "settings.users.role." + role;
+ default:
+ return role;
+ }
+}
diff --git a/src/app/features/settings/users/users-settings.module.ts b/src/app/features/settings/users/users-settings.module.ts
new file mode 100644
index 0000000..915dd84
--- /dev/null
+++ b/src/app/features/settings/users/users-settings.module.ts
@@ -0,0 +1,84 @@
+/********************************************************************************
+ * 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 {CdkTableModule} from "@angular/cdk/table";
+import {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {ReactiveFormsModule} from "@angular/forms";
+import {MatIconModule} from "@angular/material/icon";
+import {TranslateModule} from "@ngx-translate/core";
+import {CommonControlsModule} from "../../../shared/controls/common";
+import {SelectModule} from "../../../shared/controls/select";
+import {ActionButtonModule} from "../../../shared/layout/action-button";
+import {CollapsibleModule} from "../../../shared/layout/collapsible";
+import {SearchbarModule} from "../../../shared/layout/searchbar";
+import {SideMenuModule} from "../../../shared/layout/side-menu";
+import {SharedPipesModule} from "../../../shared/pipes";
+import {SharedSettingsModule} from "../shared";
+import {UsersSettingsComponent, UsersSettingsEditComponent, UsersSettingsSearchComponent, UsersSettingsTableComponent} from "./components";
+import {
+ FilterUserListPipe,
+ GetUserForIdPipe,
+ IsUserDivisionMemberPipe,
+ OptionsForDepartmentGroupPipe,
+ OptionsFromDepartmentStructurePipe,
+ RolesToDisplayTextPipe
+} from "./pipes";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ TranslateModule,
+ MatIconModule,
+ ReactiveFormsModule,
+ CdkTableModule,
+
+ SharedSettingsModule,
+ ActionButtonModule,
+ CollapsibleModule,
+ SearchbarModule,
+ SelectModule,
+ SideMenuModule,
+ CommonControlsModule,
+ SharedPipesModule
+ ],
+ declarations: [
+ UsersSettingsSearchComponent,
+ UsersSettingsEditComponent,
+ UsersSettingsTableComponent,
+ UsersSettingsComponent,
+
+ FilterUserListPipe,
+ GetUserForIdPipe,
+ IsUserDivisionMemberPipe,
+ OptionsForDepartmentGroupPipe,
+ OptionsFromDepartmentStructurePipe,
+ RolesToDisplayTextPipe
+ ],
+ exports: [
+ UsersSettingsSearchComponent,
+ UsersSettingsEditComponent,
+ UsersSettingsTableComponent,
+ UsersSettingsComponent,
+
+ FilterUserListPipe,
+ GetUserForIdPipe,
+ IsUserDivisionMemberPipe,
+ OptionsForDepartmentGroupPipe,
+ OptionsFromDepartmentStructurePipe,
+ RolesToDisplayTextPipe
+ ]
+})
+export class UsersSettingsModule {
+
+}
diff --git a/src/app/shared/controls/common/common-controls.module.ts b/src/app/shared/controls/common/common-controls.module.ts
index d924cbd..dc525f5 100644
--- a/src/app/shared/controls/common/common-controls.module.ts
+++ b/src/app/shared/controls/common/common-controls.module.ts
@@ -11,17 +11,20 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
import {NgModule} from "@angular/core";
+import {AutoInsertTextFieldTokenDirective} from "./directives/auto-insert-text-field-token";
import {AutoTextFieldResizeDirective} from "./directives/auto-text-field-resize";
import {FormControlStatusDirective} from "./directives/form-control-status";
@NgModule({
declarations: [
FormControlStatusDirective,
- AutoTextFieldResizeDirective
+ AutoTextFieldResizeDirective,
+ AutoInsertTextFieldTokenDirective
],
exports: [
FormControlStatusDirective,
- AutoTextFieldResizeDirective
+ AutoTextFieldResizeDirective,
+ AutoInsertTextFieldTokenDirective
]
})
export class CommonControlsModule {
diff --git a/src/app/shared/controls/common/directives/auto-insert-text-field-token/auto-insert-text-field-token.directive.ts b/src/app/shared/controls/common/directives/auto-insert-text-field-token/auto-insert-text-field-token.directive.ts
new file mode 100644
index 0000000..2a82d55
--- /dev/null
+++ b/src/app/shared/controls/common/directives/auto-insert-text-field-token/auto-insert-text-field-token.directive.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 {Directive, ElementRef} from "@angular/core";
+
+@Directive({
+ selector: "textarea [appAutoInsertTextFieldToken]",
+ exportAs: "appAutoInsertTextFieldToken"
+})
+export class AutoInsertTextFieldTokenDirective {
+
+ public constructor(public elementRef: ElementRef<HTMLTextAreaElement>) {
+
+ }
+
+ public insert(token: string, startToken?: string, requireLineBreak?: boolean) {
+ const withStartToken = startToken != null;
+ startToken = withStartToken ? startToken : "";
+ const selectionStart = this.elementRef.nativeElement.selectionStart;
+ const selectionEnd = this.elementRef.nativeElement.selectionEnd;
+ const value = this.elementRef.nativeElement.value;
+ const valueFirstPart = value.slice(0, selectionStart);
+ if (requireLineBreak && withStartToken && valueFirstPart.length > 0 && !valueFirstPart.endsWith("\n")) {
+ startToken = "\n" + startToken;
+ }
+
+ this.setValue(""
+ + valueFirstPart
+ + startToken
+ + value.slice(selectionStart, selectionEnd)
+ + token
+ + value.slice(selectionEnd)
+ );
+
+ this.setCursor(selectionEnd + startToken.length + (withStartToken ? 0 : token.length));
+ }
+
+ public setValue(value: string) {
+ this.elementRef.nativeElement.value = value;
+ this.elementRef.nativeElement.dispatchEvent(new InputEvent("input", {
+ inputType: "insertText",
+ data: value
+ }));
+ }
+
+ public setCursor(position: number) {
+ this.elementRef.nativeElement.selectionStart = position;
+ this.elementRef.nativeElement.selectionEnd = position;
+ this.elementRef.nativeElement.focus();
+ }
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/shared/controls/common/directives/auto-insert-text-field-token/index.ts
similarity index 90%
copy from src/app/features/settings/components/index.ts
copy to src/app/shared/controls/common/directives/auto-insert-text-field-token/index.ts
index 990bb42..5bc37e9 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/shared/controls/common/directives/auto-insert-text-field-token/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./auto-insert-text-field-token.directive";
diff --git a/src/app/shared/controls/common/directives/auto-text-field-resize/auto-text-field-resize.directive.ts b/src/app/shared/controls/common/directives/auto-text-field-resize/auto-text-field-resize.directive.ts
index 3e5a51c..2c0affc 100644
--- a/src/app/shared/controls/common/directives/auto-text-field-resize/auto-text-field-resize.directive.ts
+++ b/src/app/shared/controls/common/directives/auto-text-field-resize/auto-text-field-resize.directive.ts
@@ -10,12 +10,15 @@
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {Directive, ElementRef, HostListener, Input} from "@angular/core";
+import {Directive, ElementRef, HostListener, Input, OnChanges, SimpleChanges} from "@angular/core";
@Directive({
selector: "[appAutoTextFieldResize]"
})
-export class AutoTextFieldResizeDirective {
+export class AutoTextFieldResizeDirective implements OnChanges {
+
+ @Input()
+ public appAutoResizeData: any;
public constructor(public inputElement: ElementRef<HTMLInputElement>) {
}
@@ -27,10 +30,17 @@
}
@HostListener("input")
- onInput() {
+ public onInput() {
this.resize();
}
+ public ngOnChanges(changes: SimpleChanges) {
+ const keys: Array<keyof AutoTextFieldResizeDirective> = ["appAutoResizeData"];
+ if (keys.some((key) => changes[key] != null)) {
+ setTimeout(() => this.resize());
+ }
+ }
+
public resize() {
this.inputElement.nativeElement.style.height = "1px";
this.inputElement.nativeElement.style.height = this.inputElement.nativeElement.scrollHeight + "px";
diff --git a/src/app/shared/controls/common/directives/index.ts b/src/app/shared/controls/common/directives/index.ts
index d646c4a..9d37dc3 100644
--- a/src/app/shared/controls/common/directives/index.ts
+++ b/src/app/shared/controls/common/directives/index.ts
@@ -11,5 +11,6 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+export * from "./auto-insert-text-field-token";
export * from "./form-control-status";
export * from "./auto-text-field-resize";
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 3dcc299..6e04b09 100644
--- a/src/app/shared/controls/contact-select/contact-select.component.ts
+++ b/src/app/shared/controls/contact-select/contact-select.component.ts
@@ -17,6 +17,13 @@
import {IAPIContactPersonDetails} from "../../../core/api/contacts/IAPIContactPersonDetails";
import {AbstractControlValueAccessorComponent} from "../common";
+/**
+ * This component displays a paginated list of contacts.
+ * Contacts from the list can be selected by clicking on the row. On selection the detailed information for the selected contact, if it
+ * exists, is displayed right next to the table of contacts. The value of this component is the id of the selected contact.
+ * There is also a searchbar on top of the table to be able to perform a filtering search on the list of statements. The search bar input
+ * text is emitted to the parent component and the filtering of the actual array of statements needs to be done in that parent component.
+ */
@Component({
selector: "app-contact-select",
templateUrl: "./contact-select.component.html",
diff --git a/src/app/shared/controls/date-control/component/date-control.component.ts b/src/app/shared/controls/date-control/component/date-control.component.ts
index fcc8631..f594899 100644
--- a/src/app/shared/controls/date-control/component/date-control.component.ts
+++ b/src/app/shared/controls/date-control/component/date-control.component.ts
@@ -20,6 +20,10 @@
import {momentFormatDisplayNumeric, momentFormatInternal, parseMomentToDate, parseMomentToString} from "../../../../util";
import {DropDownDirective} from "../../../layout/drop-down";
+/**
+ * This component displays a input field to input date values. On click a pop up of a calendar opens and dates can be selected from there.
+ * Those values are then put as appValue of this component. The popup always opens to display the month of the currently input value.
+ */
@Component({
selector: "app-date-control",
templateUrl: "./date-control.component.html",
diff --git a/src/app/features/mail/pipes/sender-split-name-mail.pipe.ts b/src/app/shared/controls/select/pipes/array-to-select-options.pipe.ts
similarity index 61%
copy from src/app/features/mail/pipes/sender-split-name-mail.pipe.ts
copy to src/app/shared/controls/select/pipes/array-to-select-options.pipe.ts
index 3cfb33d..29eba99 100644
--- a/src/app/features/mail/pipes/sender-split-name-mail.pipe.ts
+++ b/src/app/shared/controls/select/pipes/array-to-select-options.pipe.ts
@@ -12,16 +12,14 @@
********************************************************************************/
import {Pipe, PipeTransform} from "@angular/core";
-import {arrayJoin} from "../../../util/store";
+import {arrayJoin} from "../../../../util/store";
+import {ISelectOption} from "../model";
-@Pipe({
- name: "appSenderSplitNameMail"
-})
-export class SenderSplitNameMailPipe implements PipeTransform {
+@Pipe({name: "arrayToSelectOptions"})
+export class ArrayToSelectOptionsPipe implements PipeTransform {
- public transform(text: string): Array<string> {
- const textAsArray = arrayJoin(text?.split(/(?=<)/g));
- return [textAsArray[0], textAsArray.slice(1).join("")].filter(x => x);
- // return text.split(/(?=<)(.+)/);
+ public transform(value: string[], indexAsValue?: boolean): ISelectOption[] {
+ return arrayJoin(value).map((label, index) => ({value: indexAsValue ? index : label, label}));
}
+
}
diff --git a/src/app/shared/controls/select/pipes/index.ts b/src/app/shared/controls/select/pipes/index.ts
index 631e0c0..be35ac4 100644
--- a/src/app/shared/controls/select/pipes/index.ts
+++ b/src/app/shared/controls/select/pipes/index.ts
@@ -11,5 +11,6 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+export * from "./array-to-select-options.pipe";
export * from "./filter-options-by-group.pipe";
export * from "./selected.pipe";
diff --git a/src/app/shared/controls/select/select.module.ts b/src/app/shared/controls/select/select.module.ts
index b7c8a2f..1664f3d 100644
--- a/src/app/shared/controls/select/select.module.ts
+++ b/src/app/shared/controls/select/select.module.ts
@@ -18,7 +18,7 @@
import {DropDownModule} from "../../layout/drop-down";
import {SharedPipesModule} from "../../pipes";
import {SelectComponent, SelectGroupComponent} from "./components";
-import {FilterOptionsByGroupPipe, SelectedPipe} from "./pipes";
+import {ArrayToSelectOptionsPipe, FilterOptionsByGroupPipe, SelectedPipe} from "./pipes";
@NgModule({
imports: [
@@ -32,12 +32,14 @@
SelectComponent,
SelectGroupComponent,
SelectedPipe,
+ ArrayToSelectOptionsPipe,
FilterOptionsByGroupPipe
],
exports: [
SelectComponent,
SelectGroupComponent,
SelectedPipe,
+ ArrayToSelectOptionsPipe,
FilterOptionsByGroupPipe
]
})
diff --git a/src/app/shared/controls/statement-select/components/statement-select.component.ts b/src/app/shared/controls/statement-select/components/statement-select.component.ts
index 2f9c82b..cc3ee3d 100644
--- a/src/app/shared/controls/statement-select/components/statement-select.component.ts
+++ b/src/app/shared/controls/statement-select/components/statement-select.component.ts
@@ -19,6 +19,13 @@
import {AbstractControlValueAccessorComponent} from "../../common";
import {ISelectOption} from "../../select";
+/**
+ * This component displays a list of statements. The columns to displayed are specified in the columns constant.
+ * Statements from the list can be selected with a checkbox. The value of this component is the array of the selected statements ids.
+ * There is also a searchbar on top of the table to be able to perform a filtering search on the list of statements. The search bar input
+ * text is emitted to the parent component and the filtering of the actual array of statements needs to be done in that parent component.
+ * An initial value can be set to the search bar with the input property appSearchText.
+ */
@Component({
selector: "app-statement-select",
templateUrl: "statement-select.component.html",
diff --git a/src/app/shared/layout/auto-focus-after-init/auto-focus-after-init.directive.spec.ts b/src/app/shared/layout/auto-focus-after-init/auto-focus-after-init.directive.spec.ts
new file mode 100644
index 0000000..eab36c3
--- /dev/null
+++ b/src/app/shared/layout/auto-focus-after-init/auto-focus-after-init.directive.spec.ts
@@ -0,0 +1,36 @@
+/********************************************************************************
+ * 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 {ElementRef} from "@angular/core";
+import {AutoFocusAfterInitDirective} from "./auto-focus-after-init.directive";
+
+describe("AutoFocusAfterInitDirective", () => {
+
+ let directive: AutoFocusAfterInitDirective;
+
+ it("should focus element ref after ngAfterViewInit", () => {
+ const elementRef = {
+ nativeElement: {
+ focus: (options: FocusOptions) => null
+ }
+ } as ElementRef<HTMLElement>;
+ const focusOptions: FocusOptions = {};
+ const focusSpy = spyOn(elementRef.nativeElement, "focus");
+ directive = new AutoFocusAfterInitDirective(elementRef);
+ directive.appFocusOptions = focusOptions;
+ directive.ngAfterViewInit();
+ expect(focusSpy).toHaveBeenCalledWith(focusOptions);
+ });
+
+});
diff --git a/src/app/shared/layout/auto-focus-after-init/auto-focus-after-init.directive.ts b/src/app/shared/layout/auto-focus-after-init/auto-focus-after-init.directive.ts
new file mode 100644
index 0000000..83ef556
--- /dev/null
+++ b/src/app/shared/layout/auto-focus-after-init/auto-focus-after-init.directive.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 {AfterViewInit, Directive, ElementRef, Input} from "@angular/core";
+
+@Directive({
+ selector: "[appAutoFocusAfterInit]"
+})
+export class AutoFocusAfterInitDirective implements AfterViewInit {
+
+ @Input()
+ public appFocusOptions: FocusOptions;
+
+ public constructor(public elementRef: ElementRef<HTMLElement>) {
+
+ }
+
+ public ngAfterViewInit() {
+ this.elementRef.nativeElement.focus(this.appFocusOptions);
+ }
+
+}
diff --git a/src/app/features/settings/settings.module.ts b/src/app/shared/layout/auto-focus-after-init/auto-focus-after-init.module.ts
similarity index 74%
rename from src/app/features/settings/settings.module.ts
rename to src/app/shared/layout/auto-focus-after-init/auto-focus-after-init.module.ts
index 2f125dd..8588de6 100644
--- a/src/app/features/settings/settings.module.ts
+++ b/src/app/shared/layout/auto-focus-after-init/auto-focus-after-init.module.ts
@@ -10,22 +10,17 @@
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-
-import {CommonModule} from "@angular/common";
import {NgModule} from "@angular/core";
-import {SettingsComponent} from "./components";
+import {AutoFocusAfterInitDirective} from "./auto-focus-after-init.directive";
@NgModule({
- imports: [
- CommonModule
- ],
declarations: [
- SettingsComponent
+ AutoFocusAfterInitDirective
],
exports: [
- SettingsComponent
+ AutoFocusAfterInitDirective
]
})
-export class SettingsModule {
+export class AutoFocusAfterInitModule {
}
diff --git a/src/app/features/settings/components/index.ts b/src/app/shared/layout/auto-focus-after-init/index.ts
similarity index 84%
copy from src/app/features/settings/components/index.ts
copy to src/app/shared/layout/auto-focus-after-init/index.ts
index 990bb42..2deca2b 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/shared/layout/auto-focus-after-init/index.ts
@@ -11,4 +11,5 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./auto-focus-after-init.directive";
+export * from "./auto-focus-after-init.module";
diff --git a/src/app/features/settings/components/index.ts b/src/app/shared/layout/list/components/index.ts
similarity index 94%
copy from src/app/features/settings/components/index.ts
copy to src/app/shared/layout/list/components/index.ts
index 990bb42..79d1963 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/shared/layout/list/components/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./list.component";
diff --git a/src/app/shared/layout/list/components/list.component.html b/src/app/shared/layout/list/components/list.component.html
new file mode 100644
index 0000000..036a50e
--- /dev/null
+++ b/src/app/shared/layout/list/components/list.component.html
@@ -0,0 +1,27 @@
+<!-------------------------------------------------------------------------------
+ * 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="list">
+ <div *ngFor="let item of appListItems; let i = index;" class="list--element">
+ <mat-icon class="list--element--icon">fiber_manual_record</mat-icon>
+ <span class="list--element--text">{{item.label}}</span>
+ <button (click)="appDelete.emit(i);"
+ *ngIf="item.add || appIsDeletable"
+ class="openk-mat-icon-button"
+ mat-icon-button>
+ <mat-icon>clear</mat-icon>
+ </button>
+ </div>
+</div>
diff --git a/src/app/shared/layout/list/components/list.component.scss b/src/app/shared/layout/list/components/list.component.scss
new file mode 100644
index 0000000..7755067
--- /dev/null
+++ b/src/app/shared/layout/list/components/list.component.scss
@@ -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
+ ********************************************************************************/
+
+@import "src/styles/openk.styles";
+
+:host {
+ display: block;
+}
+
+.title {
+ font-weight: 600;
+}
+
+.list {
+ display: flex;
+ flex-flow: row wrap;
+ padding: 0.25em;
+}
+
+.list--element {
+ flex: 0 1 calc(100% / 3 - 1.5em);
+ display: flex;
+ min-width: 15em;
+ align-items: center;
+ margin-right: 0.5em;
+}
+
+.list--element--text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.list--element--icon {
+ width: initial;
+ height: initial;
+ font-size: 0.5em;
+ margin-right: 1em;
+}
diff --git a/src/app/features/settings/components/search/settings.component.spec.ts b/src/app/shared/layout/list/components/list.component.spec.ts
similarity index 67%
rename from src/app/features/settings/components/search/settings.component.spec.ts
rename to src/app/shared/layout/list/components/list.component.spec.ts
index 2bc2d9f..a3c4a2f 100644
--- a/src/app/features/settings/components/search/settings.component.spec.ts
+++ b/src/app/shared/layout/list/components/list.component.spec.ts
@@ -12,25 +12,30 @@
********************************************************************************/
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
-import {SettingsComponent} from "./settings.component";
+import {I18nModule} from "../../../../core/i18n";
+import {ListModule} from "../list.module";
+import {ListComponent} from "./list.component";
-describe("SettingsComponent", () => {
- let component: SettingsComponent;
- let fixture: ComponentFixture<SettingsComponent>;
+describe("ListComponent", () => {
+ let component: ListComponent;
+ let fixture: ComponentFixture<ListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
- declarations: [SettingsComponent]
+ imports: [
+ ListModule,
+ I18nModule
+ ]
}).compileComponents();
}));
beforeEach(() => {
- fixture = TestBed.createComponent(SettingsComponent);
+ fixture = TestBed.createComponent(ListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
- it("should create", () => {
+ it("should be created", () => {
expect(component).toBeTruthy();
});
});
diff --git a/src/app/shared/layout/list/components/list.component.stories.ts b/src/app/shared/layout/list/components/list.component.stories.ts
new file mode 100644
index 0000000..d070c0a
--- /dev/null
+++ b/src/app/shared/layout/list/components/list.component.stories.ts
@@ -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
+ ********************************************************************************/
+import {withKnobs} from "@storybook/addon-knobs";
+import {moduleMetadata, storiesOf} from "@storybook/angular";
+import {ListModule} from "../list.module";
+
+const list = [
+ {label: "Medianet (Planung/Bau)"},
+ {label: "Medianet (Betrieb"},
+ {label: "Wasser (Planung/Bau)"},
+ {label: "Wasser (Betrieb)"},
+ {label: "Allg. Baulandentwicklung (Planung, Bau)"},
+ {label: "Allg. Baulandentwicklung (Betrieb)"},
+ {label: "Gas Hochdruck (Planung, Bau)"},
+ {label: "Gas Hochdruck (Betrieb)"},
+ {label: "GDRM/KKS/ELEX (Planung/Bau)"},
+ {label: "GDRM/KKS/ELEX (Betrieb)"}
+];
+
+storiesOf("Shared/Layout", module)
+ .addDecorator(withKnobs)
+ .addDecorator(moduleMetadata({imports: [ListModule]}))
+ .add("ListComponent", () => ({
+ template: `
+ <div style="padding: 1em; height: 100%; width: 100%; box-sizing: border-box;">
+ <app-list [appTitle]="'Allgemein'" [appListItems]="appListItems" [appIsDeletable]="true"></app-list>
+ <app-list [appTitle]="'Regionalstelle Entenhausen'" [appListItems]="appListItems"></app-list>
+ </div>
+ `,
+ props: {
+ appListItems: list
+ }
+ }));
+
+
diff --git a/src/app/shared/layout/list/components/list.component.ts b/src/app/shared/layout/list/components/list.component.ts
new file mode 100644
index 0000000..676b8f0
--- /dev/null
+++ b/src/app/shared/layout/list/components/list.component.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 {Component, EventEmitter, Input, Output} from "@angular/core";
+
+@Component({
+ selector: "app-list",
+ templateUrl: "./list.component.html",
+ styleUrls: ["./list.component.scss"]
+})
+export class ListComponent {
+
+ @Input()
+ public appTitle: string;
+
+ @Input()
+ public appListItems: { label: string; add?: boolean }[];
+
+ @Input()
+ public appIsDeletable: boolean;
+
+ @Output()
+ public appDelete = new EventEmitter<number>();
+
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/shared/layout/list/index.ts
similarity index 86%
copy from src/app/features/settings/components/index.ts
copy to src/app/shared/layout/list/index.ts
index 990bb42..29b1286 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/shared/layout/list/index.ts
@@ -11,4 +11,6 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./components";
+export * from "./pipes";
+export * from "./list.module";
diff --git a/src/app/shared/controls/map-select/map-select.module.ts b/src/app/shared/layout/list/list.module.ts
similarity index 65%
rename from src/app/shared/controls/map-select/map-select.module.ts
rename to src/app/shared/layout/list/list.module.ts
index 278693d..4e550ef 100644
--- a/src/app/shared/controls/map-select/map-select.module.ts
+++ b/src/app/shared/layout/list/list.module.ts
@@ -10,26 +10,29 @@
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-
import {CommonModule} from "@angular/common";
import {NgModule} from "@angular/core";
-import {ActionButtonModule} from "../../layout/action-button";
-import {LeafletModule} from "../../leaflet";
-import {MapSelectComponent} from "./components";
+import {MatButtonModule} from "@angular/material/button";
+import {MatIconModule} from "@angular/material/icon";
+import {ListComponent} from "./components";
+import {StringArrayToLabelListPipe} from "./pipes";
+
@NgModule({
imports: [
CommonModule,
- LeafletModule,
- ActionButtonModule
+ MatIconModule,
+ MatButtonModule
],
declarations: [
- MapSelectComponent
+ ListComponent,
+ StringArrayToLabelListPipe
],
exports: [
- MapSelectComponent
+ ListComponent,
+ StringArrayToLabelListPipe
]
})
-export class MapSelectModule {
+export class ListModule {
}
diff --git a/src/app/features/settings/components/index.ts b/src/app/shared/layout/list/pipes/index.ts
similarity index 91%
copy from src/app/features/settings/components/index.ts
copy to src/app/shared/layout/list/pipes/index.ts
index 990bb42..f99e601 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/shared/layout/list/pipes/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./string-array-to-label-list.pipe";
diff --git a/src/app/shared/layout/list/pipes/string-array-to-label-list.pipe.spec.ts b/src/app/shared/layout/list/pipes/string-array-to-label-list.pipe.spec.ts
new file mode 100644
index 0000000..68d9cb8
--- /dev/null
+++ b/src/app/shared/layout/list/pipes/string-array-to-label-list.pipe.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 {StringArrayToLabelListPipe} from "./string-array-to-label-list.pipe";
+
+describe("StringArrayToLabelListPipe", () => {
+
+ const pipe = new StringArrayToLabelListPipe();
+
+ describe("transform", () => {
+
+ it("should add all array entries to objects as label property and return that new array", () => {
+
+ const strings = [
+ "first",
+ "second",
+ "third",
+ "fourth"
+ ];
+
+ let result = pipe.transform(strings);
+
+ expect(result).toEqual([
+ {label: "first"},
+ {label: "second"},
+ {label: "third"},
+ {label: "fourth"}
+ ]);
+
+ result = pipe.transform(null);
+ expect(result).toEqual([]);
+
+ result = pipe.transform(undefined);
+ expect(result).toEqual([]);
+
+ result = pipe.transform([]);
+ expect(result).toEqual([]);
+ });
+ });
+});
+
diff --git a/src/app/shared/controls/map-select/components/map-select.component.scss b/src/app/shared/layout/list/pipes/string-array-to-label-list.pipe.ts
similarity index 63%
copy from src/app/shared/controls/map-select/components/map-select.component.scss
copy to src/app/shared/layout/list/pipes/string-array-to-label-list.pipe.ts
index 7e59f1f..cfc33ed 100644
--- a/src/app/shared/controls/map-select/components/map-select.component.scss
+++ b/src/app/shared/layout/list/pipes/string-array-to-label-list.pipe.ts
@@ -11,10 +11,16 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-@import "openk.styles";
+import {Pipe, PipeTransform} from "@angular/core";
+import {arrayJoin} from "../../../../util/store";
-:host {
- display: block;
- width: 100%;
- height: 100%;
+@Pipe({
+ name: "appStringArrayToLabelList"
+})
+export class StringArrayToLabelListPipe implements PipeTransform {
+
+ public transform(list: string[]): any {
+ return arrayJoin(list).map((_) => ({label: _}));
+ }
+
}
diff --git a/src/app/shared/leaflet/components/leaflet-map.component.spec.ts b/src/app/shared/leaflet/components/leaflet-map.component.spec.ts
deleted file mode 100644
index f839910..0000000
--- a/src/app/shared/leaflet/components/leaflet-map.component.spec.ts
+++ /dev/null
@@ -1,38 +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 {async, ComponentFixture, TestBed} from "@angular/core/testing";
-import {I18nModule} from "../../../core/i18n";
-import {LeafletModule} from "../leaflet.module";
-import {LeafletMapComponent} from "./leaflet-map.component";
-
-describe("LeafletMapComponent", () => {
- let component: LeafletMapComponent;
- let fixture: ComponentFixture<LeafletMapComponent>;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- imports: [LeafletModule, I18nModule]
- }).compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(LeafletMapComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it("should create", () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/src/app/shared/leaflet/components/leaflet-map.component.ts b/src/app/shared/leaflet/components/leaflet-map.component.ts
deleted file mode 100644
index 53e8e48..0000000
--- a/src/app/shared/leaflet/components/leaflet-map.component.ts
+++ /dev/null
@@ -1,63 +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 {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/leaflet/directives/center-marker/index.ts b/src/app/shared/leaflet/directives/center-marker/index.ts
deleted file mode 100644
index a8d18a6..0000000
--- a/src/app/shared/leaflet/directives/center-marker/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 "./leaflet-center-marker.directive";
diff --git a/src/app/shared/leaflet/directives/center-marker/leaflet-center-marker.directive.spec.ts b/src/app/shared/leaflet/directives/center-marker/leaflet-center-marker.directive.spec.ts
deleted file mode 100644
index 6922af1..0000000
--- a/src/app/shared/leaflet/directives/center-marker/leaflet-center-marker.directive.spec.ts
+++ /dev/null
@@ -1,73 +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 {Component, ViewChild} from "@angular/core";
-import {async, ComponentFixture, TestBed} from "@angular/core/testing";
-import {LatLng, LatLngLiteral} from "leaflet";
-import {Subject} from "rxjs";
-import {LeafletModule} from "../../leaflet.module";
-import {LeafletCenterMarkerDirective} from "./leaflet-center-marker.directive";
-
-describe("LeafletCenterMarkerDirective", () => {
-
- const latLng: LatLngLiteral = {
- lat: 52.520008,
- lng: 13.404954
- };
-
- let component: LeafletCenterMarkerSpecComponent;
- let fixture: ComponentFixture<LeafletCenterMarkerSpecComponent>;
- let directive: LeafletCenterMarkerDirective;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- imports: [LeafletModule],
- declarations: [LeafletCenterMarkerSpecComponent]
- }).compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(LeafletCenterMarkerSpecComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- directive = component.directive;
- });
-
- it("should update marker coordinates on move events", () => {
- const center = new LatLng(latLng.lat, latLng.lng);
- const move$ = new Subject<any>();
- const setLatLngSpy = spyOn(directive.marker, "setLatLng").and.callThrough();
- spyOn(directive.leafletHandler.instance, "getCenter").and.returnValue(center);
- spyOn(directive, "on").and.returnValue(move$.asObservable());
- directive.ngOnInit();
- move$.next();
- expect(setLatLngSpy).toHaveBeenCalledWith(center);
- });
-
-});
-
-@Component({
- selector: "app-leaflet-center-marker-spec",
- template: `
- <div appLeaflet>
- <ng-container appLeafletCenterMarker>
- </ng-container>
- </div>
- `
-})
-class LeafletCenterMarkerSpecComponent {
-
- @ViewChild(LeafletCenterMarkerDirective, {static: true})
- public directive: LeafletCenterMarkerDirective;
-
-}
diff --git a/src/app/shared/leaflet/directives/center-marker/leaflet-center-marker.directive.ts b/src/app/shared/leaflet/directives/center-marker/leaflet-center-marker.directive.ts
deleted file mode 100644
index 9db8965..0000000
--- a/src/app/shared/leaflet/directives/center-marker/leaflet-center-marker.directive.ts
+++ /dev/null
@@ -1,50 +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 {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 {ILeafletConfiguration, LEAFLET_CONFIGURATION_TOKEN} from "../../leaflet-configuration.token";
-import {LeafletHandler} from "../leaflet";
-import {AbstractLeafletMarkerDirective} from "../marker/abstract-leaflet-marker.directive";
-
-@Directive({
- selector: "[appLeafletCenterMarker]",
- exportAs: "appLeafletCenterMarker"
-})
-export class LeafletCenterMarkerDirective extends AbstractLeafletMarkerDirective implements OnInit, OnDestroy {
-
- protected destroy$ = new Subject();
-
- public constructor(
- ngZone: NgZone,
- leafletHandler: LeafletHandler,
- @Inject(LEAFLET_CONFIGURATION_TOKEN) configuration: ILeafletConfiguration,
- ) {
- super(ngZone, leafletHandler, configuration);
- }
-
- public async ngOnInit() {
- merge(of(null), this.leafletHandler.on("move", true))
- .pipe(takeUntil(this.destroy$), runOutsideZone(this.ngZone))
- .subscribe(() => this.setLatLng(this.leafletHandler.instance.getCenter()));
- }
-
- public ngOnDestroy() {
- super.ngOnDestroy();
- this.destroy$.next();
- this.destroy$.complete();
- }
-
-}
diff --git a/src/app/shared/pipes/get-form-array/get-form-array.pipe.ts b/src/app/shared/pipes/get-form-array/get-form-array.pipe.ts
index 4f2817e..5f343b2 100644
--- a/src/app/shared/pipes/get-form-array/get-form-array.pipe.ts
+++ b/src/app/shared/pipes/get-form-array/get-form-array.pipe.ts
@@ -12,14 +12,14 @@
********************************************************************************/
import {Pipe, PipeTransform} from "@angular/core";
-import {FormArray, FormGroup} from "@angular/forms";
+import {AbstractControl, FormArray, FormGroup} from "@angular/forms";
@Pipe({
name: "getFormArray"
})
export class GetFormArrayPipe implements PipeTransform {
- public transform(group: FormGroup, key: string): FormArray {
+ public transform(group: FormGroup | AbstractControl, key: Array<string | number> | string): FormArray {
const control = group?.get(key);
return control instanceof FormArray ? control : undefined;
}
diff --git a/src/app/shared/pipes/get-form-group/get-form-group.pipe.ts b/src/app/shared/pipes/get-form-group/get-form-group.pipe.ts
index 647e9ed..3b71227 100644
--- a/src/app/shared/pipes/get-form-group/get-form-group.pipe.ts
+++ b/src/app/shared/pipes/get-form-group/get-form-group.pipe.ts
@@ -12,14 +12,14 @@
********************************************************************************/
import {Pipe, PipeTransform} from "@angular/core";
-import {FormGroup} from "@angular/forms";
+import {AbstractControl, FormGroup} from "@angular/forms";
@Pipe({
name: "getFormGroup"
})
export class GetFormGroupPipe implements PipeTransform {
- public transform(group: FormGroup, key: string, getControls?: boolean): FormGroup {
+ public transform(group: FormGroup | AbstractControl, key: Array<string | number> | string, getControls?: boolean): FormGroup {
const control = group?.get(key);
return control instanceof FormGroup ? control : undefined;
}
diff --git a/src/app/shared/pipes/index.ts b/src/app/shared/pipes/index.ts
index 0da4dbd..8233d97 100644
--- a/src/app/shared/pipes/index.ts
+++ b/src/app/shared/pipes/index.ts
@@ -15,6 +15,7 @@
export * from "./get-form-array";
export * from "./get-form-error";
export * from "./get-form-group";
+export * from "./obj-keys-to-array";
export * from "./obj-to-array";
export * from "./pair";
export * from "./strings-to-options";
diff --git a/src/app/features/settings/components/index.ts b/src/app/shared/pipes/obj-keys-to-array/index.ts
similarity index 92%
copy from src/app/features/settings/components/index.ts
copy to src/app/shared/pipes/obj-keys-to-array/index.ts
index 990bb42..98db981 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/shared/pipes/obj-keys-to-array/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./obj-keys-to-array.pipe";
diff --git a/src/app/shared/pipes/obj-keys-to-array/obj-keys-to-array.pipe.spec.ts b/src/app/shared/pipes/obj-keys-to-array/obj-keys-to-array.pipe.spec.ts
new file mode 100644
index 0000000..160f31b
--- /dev/null
+++ b/src/app/shared/pipes/obj-keys-to-array/obj-keys-to-array.pipe.spec.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 {ObjKeysToArrayPipe} from "./obj-keys-to-array.pipe";
+
+describe("ObjToArrayPipe", () => {
+
+ const pipe = new ObjKeysToArrayPipe();
+
+ it("should convert an object to an array of its keys", () => {
+ const testObject = {
+ prop1: "propValue1",
+ prop2: undefined,
+ prop3: null
+ };
+ expect(pipe.transform(testObject)).toEqual(["prop1"]);
+ expect(pipe.transform(testObject, true)).toEqual(["prop1", "prop2", "prop3"]);
+ expect(pipe.transform(undefined)).toEqual([]);
+ expect(pipe.transform(null)).toEqual([]);
+ });
+
+});
diff --git a/src/app/shared/pipes/obj-keys-to-array/obj-keys-to-array.pipe.ts b/src/app/shared/pipes/obj-keys-to-array/obj-keys-to-array.pipe.ts
new file mode 100644
index 0000000..6a042fc
--- /dev/null
+++ b/src/app/shared/pipes/obj-keys-to-array/obj-keys-to-array.pipe.ts
@@ -0,0 +1,29 @@
+/********************************************************************************
+ * 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 {objectToArray} from "../../../util/store";
+
+@Pipe({name: "objKeysToArray"})
+export class ObjKeysToArrayPipe implements PipeTransform {
+
+ /**
+ * Converts an object to an array containing its keys.
+ */
+ public transform<T extends object>(value: T, keepNullOrUndefined?: boolean): string[] {
+ return objectToArray<T>({...value}, keepNullOrUndefined)
+ .map((entry) => entry.key)
+ .sort();
+ }
+
+}
diff --git a/src/app/shared/pipes/obj-to-array/obj-to-array.pipe.spec.ts b/src/app/shared/pipes/obj-to-array/obj-to-array.pipe.spec.ts
index 84b1cb6..b0f5b98 100644
--- a/src/app/shared/pipes/obj-to-array/obj-to-array.pipe.spec.ts
+++ b/src/app/shared/pipes/obj-to-array/obj-to-array.pipe.spec.ts
@@ -17,7 +17,7 @@
const pipe = new ObjToArrayPipe();
- it("should convert a object to an array", () => {
+ it("should convert an object to an array of its key value pairs", () => {
const testObject = {
prop1: "propValue1",
prop2: undefined,
diff --git a/src/app/shared/pipes/obj-to-array/obj-to-array.pipe.ts b/src/app/shared/pipes/obj-to-array/obj-to-array.pipe.ts
index 0dab401..fda7324 100644
--- a/src/app/shared/pipes/obj-to-array/obj-to-array.pipe.ts
+++ b/src/app/shared/pipes/obj-to-array/obj-to-array.pipe.ts
@@ -10,22 +10,22 @@
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+
import {Pipe, PipeTransform} from "@angular/core";
import {objectToArray} from "../../../util/store";
-/**
- * Converts an object to an array containing objects with key and value tags
- * example: Object{prop1: "propValue1", prop2: "propValue2"}
- * ->
- * [Object{key: 'prop1', value: 'propValue1'},
- * Object{key: 'prop2', value: 'propValue2'}]
- */
@Pipe({name: "objToArray"})
export class ObjToArrayPipe implements PipeTransform {
+ /**
+ * Converts an object to an array containing its key value pairs.
+ */
public transform<T extends object>(value: T, keepNullOrUndefined: boolean = false) {
- return objectToArray<T>(value, keepNullOrUndefined);
+ return objectToArray<T>({...value}, keepNullOrUndefined)
+ .sort((a, b) => {
+ return a.key.localeCompare(b.key);
+ });
}
}
diff --git a/src/app/shared/pipes/shared-pipes.module.ts b/src/app/shared/pipes/shared-pipes.module.ts
index 153655f..7bdc1f2 100644
--- a/src/app/shared/pipes/shared-pipes.module.ts
+++ b/src/app/shared/pipes/shared-pipes.module.ts
@@ -16,6 +16,7 @@
import {GetFormArrayPipe} from "./get-form-array";
import {GetFormErrorPipe} from "./get-form-error";
import {GetFormGroupPipe} from "./get-form-group";
+import {ObjKeysToArrayPipe} from "./obj-keys-to-array";
import {ObjToArrayPipe} from "./obj-to-array";
import {PairPipe} from "./pair";
import {StringsToOptionsPipe} from "./strings-to-options";
@@ -23,6 +24,7 @@
@NgModule({
declarations: [
ObjToArrayPipe,
+ ObjKeysToArrayPipe,
PairPipe,
GetFormArrayPipe,
GetFormGroupPipe,
@@ -32,6 +34,7 @@
],
exports: [
ObjToArrayPipe,
+ ObjKeysToArrayPipe,
PairPipe,
GetFormArrayPipe,
GetFormGroupPipe,
diff --git a/src/app/shared/text-block/components/editor-textblock-select/text-block-select.component.html b/src/app/shared/text-block/components/editor-textblock-select/text-block-select.component.html
index e1d8824..7831398 100644
--- a/src/app/shared/text-block/components/editor-textblock-select/text-block-select.component.html
+++ b/src/app/shared/text-block/components/editor-textblock-select/text-block-select.component.html
@@ -16,11 +16,12 @@
class="select openk-drag-list-with-hidden-placeholder">
<app-text-block-list
- (appAdd)="appAdd.emit($event)"
+ (appAdd)="appAdd.emit($event?.textBlock)"
[appConnectedTo]="appConnectedTo"
[appDuplicateOnDrag]="true"
[appDisabled]="appDisabled"
- [appListData]="standardBlocks">
+ [appListData]="standardBlocks"
+ [appWithoutTitlePrefix]="true">
</app-text-block-list>
<app-collapsible
@@ -32,11 +33,11 @@
class="text-block-group">
<app-text-block-list
- (appAdd)="appAdd.emit($event)"
+ (appAdd)="appAdd.emit($event?.textBlock)"
[appConnectedTo]="appConnectedTo"
[appDuplicateOnDrag]="true"
[appListData]="group?.textBlocks.slice()"
- [appSelectedIds]="appSelectedIds"
+ [appHiddenIds]="appSelectedIds"
[appDisabled]="appDisabled"
[appReplacements]="appReplacements"
[appShortMode]="appShortMode">
diff --git a/src/app/shared/text-block/components/editor-textblock-select/text-block-select.component.ts b/src/app/shared/text-block/components/editor-textblock-select/text-block-select.component.ts
index 9fe7502..1bbef1d 100644
--- a/src/app/shared/text-block/components/editor-textblock-select/text-block-select.component.ts
+++ b/src/app/shared/text-block/components/editor-textblock-select/text-block-select.component.ts
@@ -16,6 +16,12 @@
import {IAPITextBlockGroupModel} from "../../../../core";
import {IExtendedTextBlockModel} from "../../model";
+/**
+ * This component shows two lists of text blocks that can be selected, by drag and drop or pressing a button.
+ * First part are the standard blocks that are always available (newline, freetext, pagebreak). They can be placed multiple times so they
+ * stay in the list after selecting one. The other part are the text blocks, provided in groups. A textblock can only be placed once, so it
+ * is removed from the list after being selected once.
+ */
@Component({
selector: "app-text-block-select",
templateUrl: "./text-block-select.component.html",
@@ -50,6 +56,9 @@
@ViewChild(CdkDropList, {static: true})
public dropList: CdkDropList;
+ /**
+ * Block models for the always available blocks. They are not part of the backend textblockconfig so they need to be defined here.
+ */
public standardBlocks: IExtendedTextBlockModel[] = [
{id: "Freitext", text: "", excludes: [], requires: [], type: "text"},
{id: "Zeilenumbruch", text: "", excludes: [], requires: [], type: "newline"},
diff --git a/src/app/shared/text-block/components/text-block-list/text-block-list.component.html b/src/app/shared/text-block/components/text-block-list/text-block-list.component.html
index e8a9426..2cc0758 100644
--- a/src/app/shared/text-block/components/text-block-list/text-block-list.component.html
+++ b/src/app/shared/text-block/components/text-block-list/text-block-list.component.html
@@ -21,19 +21,35 @@
(cdkDragEnded)="isDragging = false; stopDrag(block)"
(cdkDragReleased)="isDragging = false"
(cdkDragStarted)="isDragging = true; startDrag(block)"
- (appButtonPress)="appAdd.emit(block)"
- *ngFor="let block of appListData"
+ (appAdd)="appAdd.emit({ textBlock: block, index: index })"
+ (appDelete)="appDelete.emit({ textBlock: block, index: index })"
+ (appEdit)="appEdit.emit({ textBlock: block, index: index })"
+ (appDown)="appDown.emit({ textBlock: block, index: index })"
+ (appUp)="appUp.emit({ textBlock: block, index: index })"
+ *ngFor="let block of appListData; let index = index; trackBy: trackBy; let first = first; let last = last;"
+ [appForAdmin]="appForAdmin"
+ [appDownDisabled]="last"
+ [appUpDisabled]="first"
+ [appSelected]="appSelectedIds?.includes(block?.id)"
[appShortMode]="appShortMode"
[appShowNewLine]="false"
- [appTextBlockData]="block | getBlockDataFromBlockModel: appReplacements"
+ [appTextBlockData]="block | getBlockDataFromBlockModel : appReplacements"
+ [appWithoutTitlePrefix]="appWithoutTitlePrefix"
[appTitle]="block.id"
[cdkDragData]="block"
- [cdkDragDisabled]="(block?.id | findElementInArray : appSelectedIds) != null"
- [class.text-block-list-entry---selected]="(block?.id | findElementInArray : appSelectedIds) != null"
+ [cdkDragDisabled]="appHiddenIds?.includes(block?.id)"
+ [class.text-block-list-entry---hidden]="appHiddenIds?.includes(block?.id)"
[appDisabled]="appDisabled"
cdkDrag
[class.disable]="appDisabled"
class="text-block-list-entry grab">
+
+ <ng-container *ngIf="appShowPreview">
+ <button *cdkDragPreview class="openk-button openk-chip">
+ {{'textBlocks.textBlock' | translate}} {{ block.id }}
+ </button>
+ </ng-container>
+
</app-text-block>
</div>
diff --git a/src/app/shared/text-block/components/text-block-list/text-block-list.component.scss b/src/app/shared/text-block/components/text-block-list/text-block-list.component.scss
index 004a177..1db2483 100644
--- a/src/app/shared/text-block/components/text-block-list/text-block-list.component.scss
+++ b/src/app/shared/text-block/components/text-block-list/text-block-list.component.scss
@@ -11,11 +11,17 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+@import "openk.styles";
+
.text-block-list-entry {
position: relative;
margin-bottom: 0.5em;
}
-.text-block-list-entry---selected {
+.text-block-list-entry---hidden {
display: none;
}
+
+.test {
+ background-color: get-color($openk-default-palette);
+}
diff --git a/src/app/shared/text-block/components/text-block-list/text-block-list.component.spec.ts b/src/app/shared/text-block/components/text-block-list/text-block-list.component.spec.ts
index 49ac9c7..1c91567 100644
--- a/src/app/shared/text-block/components/text-block-list/text-block-list.component.spec.ts
+++ b/src/app/shared/text-block/components/text-block-list/text-block-list.component.spec.ts
@@ -12,7 +12,7 @@
********************************************************************************/
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
-import {IAPITextBlockModel} from "../../../../core/api/text";
+import {I18nModule, IAPITextBlockModel} from "../../../../core";
import {TextBlockModule} from "../../text-block.module";
import {TextBlocksListComponent} from "./text-block-list.component";
@@ -23,7 +23,8 @@
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
- TextBlockModule
+ TextBlockModule,
+ I18nModule
]
}).compileComponents();
}));
diff --git a/src/app/shared/text-block/components/text-block-list/text-block-list.component.ts b/src/app/shared/text-block/components/text-block-list/text-block-list.component.ts
index 257454d..5c0fb5a 100644
--- a/src/app/shared/text-block/components/text-block-list/text-block-list.component.ts
+++ b/src/app/shared/text-block/components/text-block-list/text-block-list.component.ts
@@ -24,28 +24,55 @@
export class TextBlocksListComponent {
@Input()
- public appListData: IAPITextBlockModel[];
-
- @Input()
- public appDuplicateOnDrag: boolean;
-
- @Input()
public appConnectedTo: CdkDropList | string | Array<CdkDropList | string>;
@Input()
- public appShortMode: boolean;
-
- @Input()
- public appSelectedIds: string[];
-
- @Input()
public appDisabled: boolean;
@Input()
+ public appDuplicateOnDrag: boolean;
+
+ @Input()
+ public appEnterPredicate: (drag: CdkDrag, drop: CdkDropList) => boolean = returnFalse;
+
+ @Input()
+ public appForAdmin: boolean;
+
+ @Input()
+ public appHiddenIds: string[];
+
+ @Input()
+ public appListData: IAPITextBlockModel[];
+
+ @Input()
public appReplacements: { [key: string]: string };
+ @Input()
+ public appSelectedIds: string[];
+
+ @Input()
+ public appShortMode: boolean;
+
+ @Input()
+ public appShowPreview: boolean;
+
+ @Input()
+ public appWithoutTitlePrefix: boolean;
+
@Output()
- public appAdd = new EventEmitter<IAPITextBlockModel>();
+ public appAdd = new EventEmitter<{ textBlock: IAPITextBlockModel; index: number; }>();
+
+ @Output()
+ public appDelete = new EventEmitter<{ textBlock: IAPITextBlockModel; index: number; }>();
+
+ @Output()
+ public appEdit = new EventEmitter<{ textBlock: IAPITextBlockModel; index: number; }>();
+
+ @Output()
+ public appUp = new EventEmitter<{ textBlock: IAPITextBlockModel; index: number; }>();
+
+ @Output()
+ public appDown = new EventEmitter<{ textBlock: IAPITextBlockModel; index: number; }>();
public isDragging: boolean;
@@ -53,9 +80,8 @@
}
- @Input()
- public appEnterPredicate(drag: CdkDrag, drop: CdkDropList): boolean {
- return false;
+ public trackBy(index) {
+ return index;
}
public startDrag(block: IAPITextBlockModel) {
@@ -73,3 +99,7 @@
}
}
+
+function returnFalse() {
+ return false;
+}
diff --git a/src/app/shared/text-block/components/text-block/text-block.component.html b/src/app/shared/text-block/components/text-block/text-block.component.html
index 7b0eb34..c9d6f78 100644
--- a/src/app/shared/text-block/components/text-block/text-block.component.html
+++ b/src/app/shared/text-block/components/text-block/text-block.component.html
@@ -13,37 +13,94 @@
<div #textBlock (mousedown)="(editText || appDisabled || editReplacement) ? $event.stopPropagation() : null"
[class.disabled]="appDisabled"
+ [class.text-block---selected]="appSelected"
[class.text-block---error]="appErrors?.length > 0" class="text-block openk-drag-element">
<div class="title-bar">
- <span class="title-bar--title">{{appTitle | translate}}</span>
- <div class="title-bar--buttons">
- <button (click)="convertToTextInput()" *ngIf="!editText && (appType === 'block' || appType === 'text')"
- [disabled]="appDisabled" class="openk-mat-icon-button title-bar--buttons-btn" mat-icon-button>
- <mat-icon>edit</mat-icon>
- </button>
- <button (click)="revert()"
- *ngIf="(appType === 'block' && appBlockText != null) || (appType === 'text' && appBlockText)"
- [disabled]="appDisabled" class="openk-mat-icon-button title-bar--buttons-btn"
- mat-icon-button name="revertButton">
- <mat-icon class="">backspace</mat-icon>
- </button>
- <button (click)="addNewLineAfter()" *ngIf="appShowNewLine && appTextBlockData?.length === 0"
- [disabled]="appDisabled" class="openk-mat-icon-button" mat-icon-button>
- <mat-icon class="">keyboard_return</mat-icon>
- </button>
- </div>
+ <span class="title-bar--title">
+ <ng-container *ngIf="!appWithoutTitlePrefix">
+ {{'textBlocks.textBlock' | translate}}
+ </ng-container>
+ {{appTitle | translate}}
+ </span>
- <div class="title-bar--error-msg">
- <span *ngIf="appErrors?.length > 0"
- class="title-bar--block-error">{{(appErrors[0]?.message | translate) + appErrors[0]?.ids.toString()}}</span>
- </div>
+ <ng-template #buttonsForAdminTemplateRef>
- <div>
- <button (click)="appButtonPress.emit()" [disabled]="appDisabled" class="openk-mat-icon-button" mat-icon-button>
- <mat-icon class="">{{appShowClose ? "clear" : "add"}}</mat-icon>
- </button>
- </div>
+ <div class="title-bar--buttons title-bar--buttons---admin">
+
+ <button (click)="appDelete.emit()"
+ [disabled]="appDisabled"
+ class="openk-button openk-button-icon title-bar--buttons-btn">
+ <mat-icon>delete_forever</mat-icon>
+ </button>
+
+ <div style="flex: 1;"></div>
+
+ <button (click)="appUp.emit()"
+ [disabled]="appDisabled || appUpDisabled"
+ class="openk-button openk-button-icon title-bar--buttons-btn title-bar--buttons-btn---rotate-up">
+ <mat-icon>play_arrow</mat-icon>
+ </button>
+
+ <button (click)="appDown.emit()"
+ [disabled]="appDisabled || appDownDisabled"
+ class="openk-button openk-button-icon title-bar--buttons-btn title-bar--buttons-btn---rotate-down">
+ <mat-icon>play_arrow</mat-icon>
+ </button>
+
+ <button (click)="appEdit.emit()"
+ [disabled]="appDisabled"
+ class="openk-button openk-button-icon title-bar--buttons-btn">
+ <mat-icon>edit</mat-icon>
+ </button>
+
+ <button (click)="appAdd.emit()" [disabled]="appDisabled"
+ class="openk-button openk-button-icon title-bar--buttons-btn">
+ <mat-icon>add</mat-icon>
+ </button>
+
+ </div>
+
+ </ng-template>
+
+ <ng-container *ngIf="!appForAdmin; else buttonsForAdminTemplateRef">
+ <div class="title-bar--buttons">
+ <button (click)="convertToTextInput()" *ngIf="(appType === 'block' || appType === 'text')"
+ [disabled]="appDisabled" class="openk-button openk-button-icon title-bar--buttons-btn">
+ <mat-icon>edit</mat-icon>
+ </button>
+ <button (click)="revert()"
+ *ngIf="(appType === 'block' && appBlockText != null) || (appType === 'text' && appBlockText)"
+ [disabled]="appDisabled"
+ class="openk-button openk-button-icon title-bar--buttons-btn title-bar--buttons-btn---revert"
+ name="revertButton">
+ <mat-icon>backspace</mat-icon>
+ </button>
+ <button (click)="appNewLine.emit()" *ngIf="appShowNewLine && appTextBlockData?.length === 0"
+ [disabled]="appDisabled" class="openk-button openk-button-icon">
+ <mat-icon>keyboard_return</mat-icon>
+ </button>
+ </div>
+
+ <div class="title-bar--error-msg">
+ <span *ngIf="appErrors?.length > 0"
+ class="title-bar--block-error">
+ {{(appErrors[0]?.message | translate) + appErrors[0]?.ids.toString()}}
+ </span>
+ </div>
+
+ <div>
+ <button (click)="appDelete.emit()" *ngIf="appShowClose"
+ [disabled]="appDisabled" class="openk-button openk-button-icon">
+ <mat-icon>clear</mat-icon>
+ </button>
+
+ <button (click)="appAdd.emit()" *ngIf="!appShowClose"
+ [disabled]="appDisabled" class="openk-button openk-button-icon">
+ <mat-icon>add</mat-icon>
+ </button>
+ </div>
+ </ng-container>
</div>
@@ -53,25 +110,36 @@
<span *ngIf="block?.type === 'text'">{{block?.value}}</span>
- <span *ngIf="block?.type === 'highlight-text'" class="highlight-text">{{block?.value}}</span>
+ <span *ngIf="block?.type === 'highlight-text'" class="highlight-text">
+ {{block?.value}}
+ <mat-icon *ngIf="block?.iconType === 'input'" class="editable-text--icon">edit</mat-icon>
+ <mat-icon *ngIf="block?.iconType === 'select'" class="editable-text--icon editable-text--icon---rotate">play_arrow</mat-icon>
+ <mat-icon *ngIf="block?.iconType === 'date'" class="editable-text--icon">today</mat-icon>
+ </span>
<ng-container *ngIf="block?.type === 'newline'"><br></ng-container>
<app-text-replacement (appValueChange)="valueChange(block?.value, $event, block?.type)"
*ngIf="block?.type === 'input' || block?.type === 'date' || block?.type === 'select'"
[appPlaceholder]="block?.value"
- [appOptions]="block?.options" [appType]="block?.type"
+ [appOptions]="block?.options"
+ [appType]="block?.type"
[appDisabled]="appDisabled"
(appEdit)="editReplacement = $event"
- [appMaxWidth]="textBlockWidth"
[appValue]="block?.placeholder">
</app-text-replacement>
- <span *ngIf="block?.type === 'text-fill'" [class.error]="!block?.placeholder"
- class="highlight-text"><!--
- -->{{block?.placeholder ? block.placeholder : block?.value}}
- <mat-icon *ngIf="!block?.placeholder" class="error--icon">warning</mat-icon><!--
- --></span>
+ <ng-container *ngIf="block?.type === 'text-fill'">
+
+ <span *ngIf="block?.placeholder" class="highlight-text"><!--
+ -->{{block.placeholder}}<!--
+ --></span>
+
+ <span *ngIf="!block?.placeholder" class="highlight-text error"><!--
+ -->{{ block.value }}
+ <mat-icon *ngIf="!block?.placeholder" class="error--icon">warning</mat-icon><!--
+ --></span>
+ </ng-container>
</ng-container>
@@ -83,8 +151,8 @@
</textarea>
</div>
- <button (click)="addNewLineAfter()" *ngIf="appShowNewLine && appTextBlockData?.length > 0"
- [disabled]="appDisabled" class="openk-mat-icon-button" mat-icon-button>
+ <button (click)="appNewLine.emit()" *ngIf="appShowNewLine && appTextBlockData?.length > 0"
+ [disabled]="appDisabled" class="openk-button openk-button-icon">
<mat-icon>keyboard_return</mat-icon>
</button>
diff --git a/src/app/shared/text-block/components/text-block/text-block.component.scss b/src/app/shared/text-block/components/text-block/text-block.component.scss
index 42249fc..9be8586 100644
--- a/src/app/shared/text-block/components/text-block/text-block.component.scss
+++ b/src/app/shared/text-block/components/text-block/text-block.component.scss
@@ -14,18 +14,24 @@
@import "src/styles/openk.styles";
:host {
- display: flex;
- flex-direction: column;
- overflow: hidden;
+ display: block;
}
.text-block {
- padding: 0.3em;
+ display: flex;
+ flex-direction: column;
+ overflow-x: hidden;
+ padding: 0.125em 0.375em;
border: 1px dashed get-color($openk-default-palette, 900);
border-radius: 6px;
background-color: get-color($openk-default-palette);
}
+.text-block---selected {
+ border-color: get-color($openk-info-palette, A300);
+ background-color: rgba(get-color($openk-info-palette, A300), 0.04);
+}
+
.text-block---error {
border-color: $openk-error-color;
}
@@ -33,32 +39,46 @@
.title-bar {
display: flex;
flex-direction: row;
- font-size: 0.8em;
align-items: center;
}
.title-bar--title {
+ font-style: italic;
margin-right: 0.2em;
+ font-size: 0.8em;
}
.title-bar--error-msg {
- flex: 1;
- margin-right: 1em;
+ flex: 1 1 auto;
+ margin-right: 0.25em;
+ font-size: 0.8em;
}
.title-bar--buttons {
+ flex: 1 1 auto;
display: flex;
flex-direction: row;
+ align-items: center;
+}
+
+.title-bar--buttons---admin {
+ justify-content: flex-end;
}
.title-bar--buttons-btn {
- margin-right: 0.2em;
+ margin-left: 0.125em;
}
-.title-bar--buttons-btn-icon {
- padding: 0;
- height: initial;
- width: initial;
+.title-bar--buttons-btn---rotate-up {
+ transform: rotate(-90deg);
+}
+
+.title-bar--buttons-btn---rotate-down {
+ transform: rotate(90deg);
+}
+
+.title-bar--buttons-btn---revert {
+ --icon-scale-factor: 0.55;
}
.title-bar--block-info {
@@ -72,6 +92,7 @@
.highlight-text {
color: get-color($openk-info-palette, A300);
+ display: inline-flex;
}
.text-block--text {
@@ -120,3 +141,14 @@
min-height: 2.5em;
white-space: break-spaces;
}
+
+.editable-text--icon {
+ height: initial;
+ width: initial;
+ font-size: 1em;
+ margin: auto;
+}
+
+.editable-text--icon---rotate {
+ transform: rotate(90deg);
+}
diff --git a/src/app/shared/text-block/components/text-block/text-block.component.spec.ts b/src/app/shared/text-block/components/text-block/text-block.component.spec.ts
index ff8ce19..c65c0b2 100644
--- a/src/app/shared/text-block/components/text-block/text-block.component.spec.ts
+++ b/src/app/shared/text-block/components/text-block/text-block.component.spec.ts
@@ -39,11 +39,8 @@
expect(component).toBeTruthy();
});
- it("should emit appNewLine with appTitle as parameter", () => {
- spyOn(component.appNewLine, "emit").and.callThrough();
- component.appTitle = "TestTitle";
- component.addNewLineAfter();
- expect(component.appNewLine.emit).toHaveBeenCalledWith("TestTitle");
+ it("should track by index", () => {
+ expect(component.trackByIndex(19)).toBe(19);
});
it("should emit appDeleteComment with the comment id", () => {
@@ -60,25 +57,25 @@
expect(component.editText).toBeTrue();
});
- it("should emit appChangeText with the given value", () => {
- spyOn(component.appChangeText, "emit").and.callThrough();
+ it("should emit appTextChange with the given value", () => {
+ spyOn(component.appTextChange, "emit").and.callThrough();
const value = "test value";
component.onInput(value);
- expect(component.appChangeText.emit).toHaveBeenCalledWith(value);
+ expect(component.appTextChange.emit).toHaveBeenCalledWith(value);
});
- it("should emit appChangeText with empty string for freetext and otherwise undefined and set editText to false", () => {
- spyOn(component.appChangeText, "emit").and.callThrough();
+ it("should emit appTextChange with empty string for freetext and otherwise undefined and set editText to false", () => {
+ spyOn(component.appTextChange, "emit").and.callThrough();
component.editText = true;
component.appType = "text";
component.revert();
- expect(component.appChangeText.emit).toHaveBeenCalledWith("");
+ expect(component.appTextChange.emit).toHaveBeenCalledWith("");
expect(component.editText).toBeFalse();
component.editText = true;
component.appType = "block";
component.revert();
- expect(component.appChangeText.emit).toHaveBeenCalledWith(undefined);
+ expect(component.appTextChange.emit).toHaveBeenCalledWith(undefined);
expect(component.editText).toBeFalse();
});
diff --git a/src/app/shared/text-block/components/text-block/text-block.component.ts b/src/app/shared/text-block/components/text-block/text-block.component.ts
index 6d5fe6a..273c4e0 100644
--- a/src/app/shared/text-block/components/text-block/text-block.component.ts
+++ b/src/app/shared/text-block/components/text-block/text-block.component.ts
@@ -11,7 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {AfterViewChecked, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, Output, ViewChild} from "@angular/core";
+import {Component, ElementRef, EventEmitter, Input, Output, ViewChild} from "@angular/core";
import {ITextblockError} from "../../../../features/forms/statement-editor/pipes";
import {ITextBlockRenderItem} from "../../model/ITextBlockRenderItem";
import {addTypeToName} from "../../pipes/get-blockdata-array";
@@ -21,7 +21,16 @@
templateUrl: "./text-block.component.html",
styleUrls: ["./text-block.component.scss"]
})
-export class TextBlockComponent implements AfterViewChecked {
+export class TextBlockComponent {
+
+ @Input()
+ public appDownDisabled: boolean;
+
+ @Input()
+ public appUpDisabled: boolean;
+
+ @Input()
+ public appForAdmin: boolean;
@Input()
public appErrors: ITextblockError[] = [];
@@ -50,43 +59,49 @@
@Input()
public appDisabled: boolean;
+ @Input()
+ public appSelected: boolean;
+
+ @Input()
+ public appWithoutTitlePrefix: boolean;
+
+ @Output()
+ public appAdd = new EventEmitter<void>();
+
+ @Output()
+ public appDelete = new EventEmitter<void>();
+
+ @Output()
+ public appDown = new EventEmitter<void>();
+
+ @Output()
+ public appEdit = new EventEmitter<void>();
+
+ @Output()
+ public appNewLine = new EventEmitter<void>();
+
+ @Output()
+ public appTextChange = new EventEmitter<string>();
+
@Output()
public appTextInput = new EventEmitter<void>();
@Output()
- public appNewLine = new EventEmitter<string>();
-
- @Output()
- public appButtonPress = new EventEmitter<void>();
+ public appUp = new EventEmitter<void>();
@Output()
public appValueChange = new EventEmitter<{ name: string, newValue: string }>();
- @Output()
- public appChangeText = new EventEmitter<string>();
-
@ViewChild("inputElement") inputElement: ElementRef;
@ViewChild("textBlock") textBlock: ElementRef;
- public textBlockWidth = "0px";
-
public editText = false;
public editReplacement = false;
- public constructor(public changeDetectorRef: ChangeDetectorRef) {
- }
-
- public ngAfterViewChecked() {
- this.textBlockWidth = this.textBlock.nativeElement.offsetWidth;
- this.changeDetectorRef.detectChanges();
- }
-
- public trackByIndex = (index: number) => index;
-
- public addNewLineAfter() {
- this.appNewLine.emit(this.appTitle);
+ public trackByIndex(index: number) {
+ return index;
}
public valueChange(name: string, newValue: string, type: string) {
@@ -96,16 +111,15 @@
public convertToTextInput() {
this.appTextInput.emit();
this.editText = true;
- this.changeDetectorRef.detectChanges();
this.inputElement.nativeElement.focus();
}
public onInput(value: string) {
- this.appChangeText.emit(value);
+ this.appTextChange.emit(value);
}
public revert() {
- this.appChangeText.emit(this.appType === "text" ? "" : undefined);
+ this.appTextChange.emit(this.appType === "text" ? "" : undefined);
this.editText = false;
}
@@ -116,4 +130,5 @@
this.editText = false;
}
}
+
}
diff --git a/src/app/shared/text-block/components/text-replacement/text-replacement.component.html b/src/app/shared/text-block/components/text-replacement/text-replacement.component.html
index 61ad4c6..ee07ba2 100644
--- a/src/app/shared/text-block/components/text-replacement/text-replacement.component.html
+++ b/src/app/shared/text-block/components/text-replacement/text-replacement.component.html
@@ -61,7 +61,7 @@
[appPlaceholder]="appPlaceholder"
[appSmall]="true"
[appValue]="appValue"
- [appMaxWidth]="appMaxWidth"
+ [appMaxWidth]="'33em'"
class="select">
</app-select>
</div>
diff --git a/src/app/shared/text-block/components/text-replacement/text-replacement.component.spec.ts b/src/app/shared/text-block/components/text-replacement/text-replacement.component.spec.ts
index 174cbe9..f8a98b4 100644
--- a/src/app/shared/text-block/components/text-replacement/text-replacement.component.spec.ts
+++ b/src/app/shared/text-block/components/text-replacement/text-replacement.component.spec.ts
@@ -14,6 +14,7 @@
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {timer} from "rxjs";
+import {I18nModule} from "../../../../core";
import {TextBlockModule} from "../../text-block.module";
import {TextReplacementComponent} from "./text-replacement.component";
@@ -25,7 +26,8 @@
TestBed.configureTestingModule({
imports: [
TextBlockModule,
- BrowserAnimationsModule
+ BrowserAnimationsModule,
+ I18nModule
]
}).compileComponents();
}));
diff --git a/src/app/shared/text-block/components/text-replacement/text-replacement.component.ts b/src/app/shared/text-block/components/text-replacement/text-replacement.component.ts
index 248d383..e885993 100644
--- a/src/app/shared/text-block/components/text-replacement/text-replacement.component.ts
+++ b/src/app/shared/text-block/components/text-replacement/text-replacement.component.ts
@@ -11,7 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, Output, ViewChild} from "@angular/core";
+import {Component, ElementRef, EventEmitter, Input, Output, ViewChild} from "@angular/core";
import {timer} from "rxjs";
import {DateControlComponent} from "../../../controls/date-control";
import {SelectComponent} from "../../../controls/select/components/select";
@@ -41,9 +41,6 @@
@Input()
public appDisabled: boolean;
- @Input()
- public appMaxWidth: string;
-
@Output()
public appEdit = new EventEmitter<boolean>();
@@ -58,13 +55,9 @@
@Output()
public appValueChange = new EventEmitter<string>();
- public constructor(private changeDetectorRef: ChangeDetectorRef) {
- }
-
public async onClick() {
this.appEditable = true;
this.appEdit.emit(this.appEditable);
- this.changeDetectorRef.detectChanges();
this.resizeInput();
switch (this.appType) {
case "input":
diff --git a/src/app/shared/text-block/model/ITextBlockRenderItem.ts b/src/app/shared/text-block/model/ITextBlockRenderItem.ts
index 3cac859..da77e1f 100644
--- a/src/app/shared/text-block/model/ITextBlockRenderItem.ts
+++ b/src/app/shared/text-block/model/ITextBlockRenderItem.ts
@@ -16,4 +16,5 @@
value: string;
options?: string[];
placeholder?: string;
+ iconType?: string;
}
diff --git a/src/app/shared/text-block/pipes/combine-blockdata-text/combine-blockdata-text.pipe.ts b/src/app/shared/text-block/pipes/combine-blockdata-text/combine-blockdata-text.pipe.ts
index ed3b4c5..152c7d1 100644
--- a/src/app/shared/text-block/pipes/combine-blockdata-text/combine-blockdata-text.pipe.ts
+++ b/src/app/shared/text-block/pipes/combine-blockdata-text/combine-blockdata-text.pipe.ts
@@ -16,6 +16,9 @@
import {arrayJoin} from "../../../../util/store";
import {ITextBlockRenderItem} from "../../model/ITextBlockRenderItem";
+/**
+ * Takes in an array of TextBlockRender items and combines adjacent ones of type text into one.
+ */
@Pipe({
name: "combineBlockdataText"
})
diff --git a/src/app/shared/text-block/pipes/get-blockdata-array/IReplacement.ts b/src/app/shared/text-block/pipes/get-blockdata-array/IReplacement.ts
index 9106d8e..e63ba06 100644
--- a/src/app/shared/text-block/pipes/get-blockdata-array/IReplacement.ts
+++ b/src/app/shared/text-block/pipes/get-blockdata-array/IReplacement.ts
@@ -12,6 +12,7 @@
********************************************************************************/
export interface IReplacement {
- separator: RegExp;
+ separator: string | RegExp;
type: string;
+ iconType?: string;
}
diff --git a/src/app/shared/text-block/pipes/get-blockdata-array/block-data-helper.ts b/src/app/shared/text-block/pipes/get-blockdata-array/block-data-helper.ts
index 9a7311f..3533454 100644
--- a/src/app/shared/text-block/pipes/get-blockdata-array/block-data-helper.ts
+++ b/src/app/shared/text-block/pipes/get-blockdata-array/block-data-helper.ts
@@ -15,17 +15,30 @@
import {ITextBlockRenderItem} from "../../model/ITextBlockRenderItem";
import {IReplacement} from "./IReplacement";
+/**
+ * Regex for replacement tags in text blocks. These tags mark the position values have to be integrated into the textblock for display
+ * purposes.
+ * E.g. <f:name> is to be replaced free text input by the user.
+ */
export const replacements: IReplacement[] = [
{separator: /(<f:[A-Za-z0-9_\\-]+>)/g, type: "input"},
{separator: /(<d:[A-Za-z0-9_\\-]+>)/g, type: "date"},
{separator: /(<s:[A-Za-z0-9_\\-]+>)/g, type: "select"}
];
-
export const alwaysReplace: IReplacement[] = [
{separator: /(\n)/, type: "newline"},
{separator: /(<t:[A-Za-z0-9_\\-]+>)/g, type: "text-fill"}
];
+
+/**
+ * Function that takes in a TextBlockModel and splits it up by the replacement tags into the different elements to display.
+ * The result array is of type TextBlockRenderItem.
+ * The normal text part is simply put as the value of the element with type "text".
+ * For the other types the placeholder is set as the name of the replacement tag and value is only set if there is a value already set
+ * (placeholderValues)
+ * Options are only set for select type.
+ */
export function textToBlockDataArray(
blockModel: IAPITextBlockModel,
placeholderValues: { [key: string]: string },
@@ -34,7 +47,7 @@
replace: boolean = true): ITextBlockRenderItem[] {
const whatToReplace: IReplacement[] = replace ? [...alwaysReplace, ...replacements]
- : [...alwaysReplace, ...replacements.map((_) => ({..._, type: "highlight-text"}))];
+ : [...alwaysReplace, ...replacements.map((_) => ({..._, type: "highlight-text", iconType: _.type}))];
const blockData: ITextBlockRenderItem[] = [{value: blockModel ? blockModel.text : "", type: "text"}];
@@ -55,8 +68,7 @@
arrays.push(
...replaceBySeparator(
textElement.value,
- replacement.separator,
- replacement.type,
+ replacement,
placeholderValues,
replacementTexts,
selectOptions
@@ -72,32 +84,32 @@
export function replaceBySeparator(
inputValue: string,
- separator: string | RegExp,
- replaceType: string,
+ replacement: IReplacement,
placeholderValues?: { [key: string]: string },
replacementTexts?: { [key: string]: string },
selectOptions?: { [key: string]: string[] }): ITextBlockRenderItem[] {
- const splitBySeparator: string[] = inputValue.split(separator);
+ const splitBySeparator: string[] = inputValue.split(replacement?.separator);
const blockData: ITextBlockRenderItem[] = [];
for (const renderItem of splitBySeparator) {
- const isText: boolean = !(new RegExp(separator)).test(renderItem);
+ const isText: boolean = !(new RegExp(replacement?.separator)).test(renderItem);
const value: string = isText ? renderItem
: renderItem.replace(/(<[fdts]:)/, "")
.replace(">", "")
.replace("\n", "");
let placeholder: string = placeholderValues ? placeholderValues[renderItem] : undefined;
- if (replaceType === "text-fill") {
+ if (replacement?.type === "text-fill") {
placeholder = replacementTexts ? replacementTexts[value] : undefined;
}
if (value !== "" || !isText) {
blockData.push({
value,
- type: isText ? "text" : replaceType,
+ type: isText ? "text" : replacement?.type,
placeholder,
- options: (!isText && replaceType === "select" && !!selectOptions) ? selectOptions[value] : undefined
+ options: (!isText && replacement?.type === "select" && !!selectOptions) ? selectOptions[value] : undefined,
+ iconType: isText ? undefined : replacement?.iconType
});
}
diff --git a/src/app/shared/text-block/pipes/get-blockdata-array/get-blockdata-array-from-arrangement.pipe.ts b/src/app/shared/text-block/pipes/get-blockdata-array/get-blockdata-array-from-arrangement.pipe.ts
index c6bc1a7..f7ec64f 100644
--- a/src/app/shared/text-block/pipes/get-blockdata-array/get-blockdata-array-from-arrangement.pipe.ts
+++ b/src/app/shared/text-block/pipes/get-blockdata-array/get-blockdata-array-from-arrangement.pipe.ts
@@ -18,6 +18,11 @@
import {alwaysReplace, replaceTags, textToBlockDataArray} from "./block-data-helper";
import {IReplacement} from "./IReplacement";
+/**
+ * Takes in a IAPITextArrangementItemModel and splits it into an array of ITextBlockRenderItem.
+ * Text, new lined and the replacements are separated. The replacements like freetext, date and select are grouped together as one type,
+ * text-fill. That is done to be able to mark them in the ui.
+ */
@Pipe({
name: "getBlockDataFromArrangement"
})
diff --git a/src/app/shared/text-block/pipes/get-blockdata-array/get-blockdata-array-from-block.pipe.spec.ts b/src/app/shared/text-block/pipes/get-blockdata-array/get-blockdata-array-from-block.pipe.spec.ts
index 5eb6dee..025cad7 100644
--- a/src/app/shared/text-block/pipes/get-blockdata-array/get-blockdata-array-from-block.pipe.spec.ts
+++ b/src/app/shared/text-block/pipes/get-blockdata-array/get-blockdata-array-from-block.pipe.spec.ts
@@ -44,7 +44,8 @@
},
{
type: "highlight-text",
- value: "freetext"
+ value: "freetext",
+ iconType: "input"
},
{
type: "text-fill",
@@ -52,11 +53,13 @@
},
{
type: "highlight-text",
- value: "select"
+ value: "select",
+ iconType: "select"
},
{
type: "highlight-text",
- value: "date"
+ value: "date",
+ iconType: "date"
},
{
type: "text",
diff --git a/src/app/shared/text-block/pipes/get-blockdata-array/get-blockdata-array-from-block.pipe.ts b/src/app/shared/text-block/pipes/get-blockdata-array/get-blockdata-array-from-block.pipe.ts
index c2c1494..548e7ec 100644
--- a/src/app/shared/text-block/pipes/get-blockdata-array/get-blockdata-array-from-block.pipe.ts
+++ b/src/app/shared/text-block/pipes/get-blockdata-array/get-blockdata-array-from-block.pipe.ts
@@ -22,6 +22,16 @@
public transform(blockModel: IAPITextBlockModel, replacements?: { [key: string]: string }):
Array<{ value: string, type: string, placeholder?: string, options?: string[] }> {
- return blockModel ? textToBlockDataArray(blockModel, undefined, replacements, undefined, false) : [];
+
+ if (blockModel == null) {
+ return [];
+ }
+
+ blockModel = {
+ ...blockModel,
+ text: blockModel.text?.endsWith("\n") ? blockModel.text + " " : blockModel.text
+ };
+
+ return textToBlockDataArray(blockModel, undefined, replacements, undefined, false);
}
}
diff --git a/src/app/store/attachments/attachments-store.module.ts b/src/app/store/attachments/attachments-store.module.ts
index 8b95c07..2db4d24 100644
--- a/src/app/store/attachments/attachments-store.module.ts
+++ b/src/app/store/attachments/attachments-store.module.ts
@@ -18,6 +18,7 @@
import {FetchAttachmentsEffect, SubmitAttachmentsEffect} from "./effects";
import {AttachmentDownloadEffect} from "./effects/download";
import {SubmitConsiderationsEffect} from "./effects/submit/submit-considerations.effect";
+import {SubmitTagsEffect} from "./effects/submit/submit-tags.effect";
@NgModule({
imports: [
@@ -26,7 +27,8 @@
AttachmentDownloadEffect,
FetchAttachmentsEffect,
SubmitAttachmentsEffect,
- SubmitConsiderationsEffect
+ SubmitConsiderationsEffect,
+ SubmitTagsEffect
])
]
})
diff --git a/src/app/store/attachments/effects/submit/submit-tags.effect.ts b/src/app/store/attachments/effects/submit/submit-tags.effect.ts
new file mode 100644
index 0000000..fa56e47
--- /dev/null
+++ b/src/app/store/attachments/effects/submit/submit-tags.effect.ts
@@ -0,0 +1,76 @@
+/********************************************************************************
+ * 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} from "rxjs";
+import {catchError, endWith, ignoreElements, mergeMap, startWith, switchMap} from "rxjs/operators";
+import {AttachmentsApiService} from "../../../../core/api/attachments";
+import {endWithObservable, ignoreError} from "../../../../util";
+import {setErrorAction} from "../../../root/actions";
+import {EErrorCode} from "../../../root/model";
+import {setSettingsLoadingStateAction} from "../../../settings/actions";
+import {submitTagsAction} from "../../../statements/actions";
+import {fetchAttachmentTagsAction} from "../../actions";
+
+@Injectable({providedIn: "root"})
+export class SubmitTagsEffect {
+
+ public submit$ = createEffect(() => this.actions.pipe(
+ ofType(submitTagsAction),
+ switchMap((action) => {
+ return this.submit(action.labels).pipe(
+ ignoreError()
+ );
+ })
+ ));
+
+ public constructor(
+ public readonly actions: Actions,
+ public readonly attachmentsApiService: AttachmentsApiService
+ ) {
+
+ }
+
+ public submit(
+ labels: string[]
+ ): Observable<Action> {
+ const errors: string[] = [];
+ return this.addTags(labels, errors).pipe(
+ endWithObservable(() => {
+ return errors.length > 0 ? concat(
+ of(setErrorAction({error: EErrorCode.COULD_NOT_ADD_TAG, errorValue: {value: errors.toString().replace(/,/g, ", ")}})),
+ of(fetchAttachmentTagsAction())
+ ) : of(fetchAttachmentTagsAction());
+ }),
+ startWith(setSettingsLoadingStateAction({state: {addingTags: true}})),
+ endWith(setSettingsLoadingStateAction({state: {addingTags: false}}))
+ );
+ }
+
+ public addTags(labels: string[], errors: any[]): Observable<Action> {
+ return of(...labels).pipe(
+ mergeMap((item) => {
+ return this.attachmentsApiService.addNewTag(item).pipe(
+ catchError(() => {
+ errors.push(item);
+ return EMPTY;
+ }),
+ ignoreElements()
+ );
+ })
+ );
+ }
+
+}
diff --git a/src/app/store/geo/actions/geo.actions.ts b/src/app/store/geo/actions/geo.actions.ts
index 9c1a7f8..305a6bc 100644
--- a/src/app/store/geo/actions/geo.actions.ts
+++ b/src/app/store/geo/actions/geo.actions.ts
@@ -12,9 +12,25 @@
********************************************************************************/
import {createAction, props} from "@ngrx/store";
-import {ILeafletBounds} from "../../../shared/leaflet";
+import {IAPINominatimSearchResult} from "../../../core";
+import {ILeafletBounds} from "../../../features/map";
export const openGisAction = createAction(
"[Map] Open GIS",
props<{ bounds: ILeafletBounds, user: string }>()
);
+
+export const searchMapAction = createAction(
+ "[Map] Search for a place to display on map",
+ props<{ q: string }>()
+);
+
+export const setSearchResponseAction = createAction(
+ "[Store] Sets the search result",
+ props<{ response: IAPINominatimSearchResult[] }>()
+);
+
+export const setMapSearchLoadingAction = createAction(
+ "[Store] Sets the search loading state",
+ props<{ loading: boolean }>()
+);
diff --git a/src/app/store/geo/effects/index.ts b/src/app/store/geo/effects/index.ts
index 1cfc850..a77c8e6 100644
--- a/src/app/store/geo/effects/index.ts
+++ b/src/app/store/geo/effects/index.ts
@@ -11,4 +11,5 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+export * from "./search";
export * from "./open-gis";
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
index c33cff7..6f23692 100644
--- 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
@@ -17,8 +17,8 @@
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 {IAPIGeographicPositions, SPA_BACKEND_ROUTE} from "../../../../core";
+import {ILeafletBounds} from "../../../../features/map";
import {openGisAction} from "../../actions";
import {OpenGisEffect} from "./open-gis.effect";
@@ -64,13 +64,6 @@
{
provide: SPA_BACKEND_ROUTE,
useValue: "/"
- },
- {
- provide: WINDOW,
- useValue: ({
- open(url?: string, target?: string, features?: string, replace?: boolean) {
- }
- })
}
]
});
@@ -87,7 +80,6 @@
it("should open GIS in new window", () => {
const results: Action[] = [];
- const spy = spyOn(effect.window, "open");
const actionSubject = new Subject<Action>();
actions$ = actionSubject;
@@ -95,7 +87,7 @@
actionSubject.next(openGisAction({bounds, user}));
expectTransformRequest(geographicPositions);
- expect(spy).toHaveBeenCalledWith(gisUrl, "_blank");
+ expectGisRequest(gisUrl);
expect(results).toEqual([]);
httpTestingController.verify();
});
@@ -117,6 +109,12 @@
request.flush(body);
}
+ function expectGisRequest(url: string) {
+ const request = httpTestingController.expectOne(url);
+ expect(request.request.method).toBe("GET");
+ request.flush("");
+ }
+
});
function createLatLngMock(): LatLngLiteral {
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
index 19da54f..3298232 100644
--- a/src/app/store/geo/effects/open-gis/open-gis.effect.ts
+++ b/src/app/store/geo/effects/open-gis/open-gis.effect.ts
@@ -11,14 +11,15 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+import {HttpClient} from "@angular/common/http";
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 {Observable, of} from "rxjs";
+import {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 {ILeafletBounds} from "../../../../features/map";
+import {catchErrorTo, ignoreError} from "../../../../util/rxjs";
import {EErrorCode, setErrorAction} from "../../../root";
import {openGisAction} from "../../actions";
@@ -27,7 +28,7 @@
public open$ = createEffect(() => this.actions.pipe(
ofType(openGisAction),
- exhaustMap((action) => this.openGis(action.bounds, action.user))
+ switchMap((action) => this.openGis(action.bounds, action.user))
));
/**
@@ -53,6 +54,7 @@
public constructor(
public actions: Actions,
public geoApiService: GeoApiService,
+ public http: HttpClient,
@Inject(WINDOW) public window: Window,
@Inject(APP_CONFIGURATION) public configuration: IAppConfiguration
) {
@@ -64,8 +66,7 @@
switchMap(() => this.transform(this.extractGeographicPositionFromBounds(bounds))),
switchMap((geographicPositions) => {
const gisUrl = this.generateUrl(geographicPositions, user);
- this.window.open(gisUrl, "_blank");
- return EMPTY;
+ return this.http.get(gisUrl, {responseType: "text"}).pipe(ignoreError());
}),
ignoreElements(),
catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED}))
diff --git a/src/app/features/settings/components/index.ts b/src/app/store/geo/effects/search/index.ts
similarity index 91%
copy from src/app/features/settings/components/index.ts
copy to src/app/store/geo/effects/search/index.ts
index 990bb42..7a682ab 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/store/geo/effects/search/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./nominatim-search-effect.service";
diff --git a/src/app/store/geo/effects/search/nominatim-search-effect.service.ts b/src/app/store/geo/effects/search/nominatim-search-effect.service.ts
new file mode 100644
index 0000000..d145230
--- /dev/null
+++ b/src/app/store/geo/effects/search/nominatim-search-effect.service.ts
@@ -0,0 +1,54 @@
+/********************************************************************************
+ * 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 {endWith, startWith, switchMap, throttleTime} from "rxjs/operators";
+import {GeoApiService} from "../../../../core";
+import {catchErrorTo} from "../../../../util/rxjs";
+import {setErrorAction} from "../../../root/actions";
+import {EErrorCode} from "../../../root/model";
+import {searchMapAction, setMapSearchLoadingAction, setSearchResponseAction} from "../../actions";
+
+@Injectable({providedIn: "root"})
+export class NominatimSearchEffect {
+
+ public search$ = createEffect(() => this.actions.pipe(
+ ofType(searchMapAction),
+ throttleTime(200),
+ switchMap((action) => this.search(action.q))
+ ));
+
+ public constructor(
+ public actions: Actions,
+ public geoApiService: GeoApiService
+ ) {
+
+ }
+
+ public search(q: string): Observable<Action> {
+ return this.geoApiService.search(q).pipe(
+ switchMap((response) => {
+ return response.length > 0 ?
+ of(setSearchResponseAction({response})) :
+ of(setSearchResponseAction({response}), setErrorAction({error: EErrorCode.SEARCH_NO_RESULT}));
+ }),
+ catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
+ startWith(setMapSearchLoadingAction({loading: true})),
+ endWith(setMapSearchLoadingAction({loading: false}))
+ );
+ }
+
+}
diff --git a/src/app/store/geo/geo-reducers.token.ts b/src/app/store/geo/geo-reducers.token.ts
index a1453d3..ac044c2 100644
--- a/src/app/store/geo/geo-reducers.token.ts
+++ b/src/app/store/geo/geo-reducers.token.ts
@@ -14,10 +14,15 @@
import {InjectionToken} from "@angular/core";
import {ActionReducerMap} from "@ngrx/store";
import {IGeoStoreState} from "./model";
+import {mapLoadingReducer} from "./reducers/search/map-loading.reducer";
+import {mapSearchReducer} from "./reducers/search/map-search.reducer";
export const GEO_NAME = "geo";
export const GEO_REDUCERS = new InjectionToken<ActionReducerMap<IGeoStoreState>>("Geo store reducer", {
providedIn: "root",
- factory: () => ({})
+ factory: () => ({
+ loading: mapLoadingReducer,
+ responseContent: mapSearchReducer
+ })
});
diff --git a/src/app/store/geo/geo-store.module.ts b/src/app/store/geo/geo-store.module.ts
index 1253f7d..9493e61 100644
--- a/src/app/store/geo/geo-store.module.ts
+++ b/src/app/store/geo/geo-store.module.ts
@@ -14,14 +14,15 @@
import {NgModule} from "@angular/core";
import {EffectsModule} from "@ngrx/effects";
import {StoreModule} from "@ngrx/store";
-import {OpenGisEffect} from "./effects";
+import {NominatimSearchEffect, OpenGisEffect} from "./effects";
import {GEO_NAME, GEO_REDUCERS} from "./geo-reducers.token";
@NgModule({
imports: [
StoreModule.forFeature(GEO_NAME, GEO_REDUCERS),
EffectsModule.forFeature([
- OpenGisEffect
+ OpenGisEffect,
+ NominatimSearchEffect
])
]
})
diff --git a/src/app/store/geo/index.ts b/src/app/store/geo/index.ts
index 5b5ab49..3d060a9 100644
--- a/src/app/store/geo/index.ts
+++ b/src/app/store/geo/index.ts
@@ -12,6 +12,8 @@
********************************************************************************/
export * from "./actions";
+export * from "./effects";
export * from "./model";
+export * from "./selectors";
export * from "./geo-store.module";
diff --git a/src/app/store/geo/model/IGeoStoreState.ts b/src/app/store/geo/model/IGeoStoreState.ts
index c4a0812..8192227 100644
--- a/src/app/store/geo/model/IGeoStoreState.ts
+++ b/src/app/store/geo/model/IGeoStoreState.ts
@@ -10,9 +10,11 @@
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+import {IAPINominatimSearchResult} from "../../../core";
export interface IGeoStoreState {
- loading?: any;
+ loading?: boolean;
+ responseContent: IAPINominatimSearchResult[];
}
diff --git a/src/app/shared/leaflet/index.ts b/src/app/store/geo/reducers/search/map-loading.reducer.ts
similarity index 67%
copy from src/app/shared/leaflet/index.ts
copy to src/app/store/geo/reducers/search/map-loading.reducer.ts
index 849c149..f963ee0 100644
--- a/src/app/shared/leaflet/index.ts
+++ b/src/app/store/geo/reducers/search/map-loading.reducer.ts
@@ -10,10 +10,13 @@
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+import {createReducer, on} from "@ngrx/store";
+import {setMapSearchLoadingAction} from "../../actions";
-export * from "./directives";
-export * from "./pipes";
-export * from "./util";
-export * from "./leaflet.module";
-export * from "./leaflet-configuration.token";
+export const mapLoadingReducer = createReducer<boolean>(
+ false,
+ on(setMapSearchLoadingAction, (state, payload) => {
+ return payload?.loading;
+ })
+);
diff --git a/src/app/store/settings/reducers/sectors.reducer.ts b/src/app/store/geo/reducers/search/map-search.reducer.ts
similarity index 66%
copy from src/app/store/settings/reducers/sectors.reducer.ts
copy to src/app/store/geo/reducers/search/map-search.reducer.ts
index 53cc6cf..9873d83 100644
--- a/src/app/store/settings/reducers/sectors.reducer.ts
+++ b/src/app/store/geo/reducers/search/map-search.reducer.ts
@@ -10,14 +10,14 @@
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-
import {createReducer, on} from "@ngrx/store";
-import {IAPISectorsModel} from "../../../core/api/statements/IAPISectorsModel";
-import {setSectorsAction} from "../actions";
+import {IAPINominatimSearchResult} from "../../../../core";
+import {setSearchResponseAction} from "../../actions";
-export const sectorsReducer = createReducer<IAPISectorsModel>(
- {},
- on(setSectorsAction, (state, payload) => {
- return payload.sectors ? {...payload.sectors} : state;
+
+export const mapSearchReducer = createReducer<IAPINominatimSearchResult[]>(
+ null,
+ on(setSearchResponseAction, (state, payload) => {
+ return payload?.response;
})
);
diff --git a/src/app/store/geo/selectors/geo.selectors.ts b/src/app/store/geo/selectors/geo.selectors.ts
new file mode 100644
index 0000000..4a15cd7
--- /dev/null
+++ b/src/app/store/geo/selectors/geo.selectors.ts
@@ -0,0 +1,36 @@
+/********************************************************************************
+ * 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 {createFeatureSelector, createSelector} from "@ngrx/store";
+import {arrayJoin, selectArrayProjector, selectPropertyProjector} from "../../../util/store";
+import {GEO_NAME} from "../geo-reducers.token";
+import {IGeoStoreState} from "../model";
+
+export const geoStateSelector = createFeatureSelector<IGeoStoreState>(GEO_NAME);
+
+export const getNominatimResponseContentSelector = createSelector(
+ geoStateSelector,
+ selectArrayProjector("responseContent", [])
+);
+
+export const getNominatimSearchResultSelector = createSelector(
+ getNominatimResponseContentSelector,
+ (result) => {
+ return arrayJoin(result).length === 0 ? undefined : {lat: parseFloat(result[0].lat), lng: parseFloat(result[0].lon)};
+ }
+);
+
+export const getNominatimLoadingSelector = createSelector(
+ geoStateSelector,
+ selectPropertyProjector("loading")
+);
diff --git a/src/app/features/settings/components/index.ts b/src/app/store/geo/selectors/index.ts
similarity index 94%
rename from src/app/features/settings/components/index.ts
rename to src/app/store/geo/selectors/index.ts
index 990bb42..f2e1df6 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/store/geo/selectors/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./geo.selectors";
diff --git a/src/app/store/process/actions/process.actions.ts b/src/app/store/process/actions/process.actions.ts
index 538ff93..3db71d7 100644
--- a/src/app/store/process/actions/process.actions.ts
+++ b/src/app/store/process/actions/process.actions.ts
@@ -22,7 +22,13 @@
export const claimAndCompleteTask = createAction(
"[Details] Claim and complete Task",
- props<{ statementId: number, taskId: string, variables: TCompleteTaskVariable, claimNext?: boolean | EAPIProcessTaskDefinitionKey }>()
+ props<{
+ statementId: number,
+ taskId: string,
+ assignee: string,
+ variables: TCompleteTaskVariable,
+ claimNext?: boolean | EAPIProcessTaskDefinitionKey
+ }>()
);
export const unclaimAllTasksAction = createAction(
diff --git a/src/app/store/process/effects/process-task.effect.spec.ts b/src/app/store/process/effects/process-task.effect.spec.ts
index 262f791..e7b8358 100644
--- a/src/app/store/process/effects/process-task.effect.spec.ts
+++ b/src/app/store/process/effects/process-task.effect.spec.ts
@@ -94,7 +94,8 @@
const statementId = 19;
const taskId = "191919";
const variables = {};
- actions$ = of(claimAndCompleteTask({statementId, taskId, variables, claimNext: true}));
+ const assignee = "hugo";
+ actions$ = of(claimAndCompleteTask({statementId, taskId, assignee, variables, claimNext: true}));
const claimTaskSpy = spyOn(effect, "claimTask").and.returnValue(EMPTY);
const completeTaskSpy = spyOn(effect, "completeTask").and.returnValue(EMPTY);
@@ -107,7 +108,7 @@
subscription = effect.claimAndComplete$.subscribe((action) => results.push(action));
expect(results).toEqual(expectedResult);
- expect(claimTaskSpy).toHaveBeenCalledWith(statementId, taskId);
+ expect(claimTaskSpy).toHaveBeenCalledWith(statementId, taskId, assignee);
expect(completeTaskSpy).toHaveBeenCalledWith(statementId, taskId, variables, true);
httpTestingController.verify();
diff --git a/src/app/store/process/effects/process-task.effect.ts b/src/app/store/process/effects/process-task.effect.ts
index 39cdc55..f99da56 100644
--- a/src/app/store/process/effects/process-task.effect.ts
+++ b/src/app/store/process/effects/process-task.effect.ts
@@ -40,7 +40,7 @@
public claim$ = createEffect(() => this.actions.pipe(
ofType(claimTaskAction),
- switchMap((action) => this.claimTask(action.statementId, action.taskId, true).pipe(
+ switchMap((action) => this.claimTask(action.statementId, action.taskId, null, true).pipe(
catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
startWith(setProcessLoadingAction({statementId: action.statementId, loading: true})),
endWith(setProcessLoadingAction({statementId: action.statementId, loading: false}))
@@ -49,7 +49,9 @@
public claimAndComplete$ = createEffect(() => this.actions.pipe(
ofType(claimAndCompleteTask),
- switchMap((action) => this.claimAndCompleteTask(action.statementId, action.taskId, action.variables, action.claimNext).pipe(
+ switchMap((action) => this.claimAndCompleteTask(
+ action.statementId, action.taskId, action.variables, action.assignee, action.claimNext
+ ).pipe(
catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
startWith(setProcessLoadingAction({statementId: action.statementId, loading: true})),
endWith(setProcessLoadingAction({statementId: action.statementId, loading: false}))
@@ -58,7 +60,7 @@
public claimAndSend$ = createEffect(() => this.actions.pipe(
ofType(sendStatementViaMailAction),
- switchMap((action) => this.claimAndSend(action.statementId, action.taskId).pipe(
+ switchMap((action) => this.claimAndSend(action.statementId, action.taskId, action.assignee).pipe(
catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
startWith(setProcessLoadingAction({statementId: action.statementId, loading: true})),
endWith(setProcessLoadingAction({statementId: action.statementId, loading: false}))
@@ -87,13 +89,14 @@
}
- public claimTask(statementId: number, taskId: string, navigate?: boolean): Observable<Action> {
+ public claimTask(statementId: number, taskId: string, assignee?: string, navigate?: boolean): Observable<Action> {
return this.processApiService.claimStatementTask(statementId, taskId).pipe(
map((task) => setTaskEntityAction({task})),
endWithObservable(() => navigate ? this.navigateTo(statementId, taskId) : EMPTY),
catchHttpErrorTo(setErrorAction({
statementId,
- error: EErrorCode.CLAIMED_BY_OTHER_USER
+ error: assignee ? EErrorCode.CLAIMED_BY_OTHER_USER : EErrorCode.ALREADY_CLAIMED,
+ errorValue: {user: assignee}
}), EHttpStatusCodes.BAD_REQUEST),
catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
this.fetchTasksAfterError(statementId)
@@ -104,9 +107,10 @@
statementId: number,
taskId: string,
variable: TCompleteTaskVariable,
+ assignee: string,
claimNext?: boolean | EAPIProcessTaskDefinitionKey
): Observable<Action> {
- return this.claimTask(statementId, taskId).pipe(
+ return this.claimTask(statementId, taskId, assignee).pipe(
throwAfterActionType(setErrorAction),
endWithObservable(() => this.completeTask(statementId, taskId, variable, claimNext)),
ignoreError()
@@ -115,9 +119,10 @@
public claimAndSend(
statementId: number,
- taskId: string
+ taskId: string,
+ assignee: string
): Observable<Action> {
- return this.claimTask(statementId, taskId).pipe(
+ return this.claimTask(statementId, taskId, assignee).pipe(
throwAfterActionType(setErrorAction),
endWithObservable(() => concat(
this.sendMailAndComplete(statementId, taskId),
@@ -160,13 +165,17 @@
),
this.claimNext(statementId, claimNext, false).pipe(
map((task) => {
- nextTaskId = task.taskId;
- return setTaskEntityAction({task});
+ nextTaskId = task?.task?.taskId;
+ const user = task?.task?.assignee;
+ return task?.errorOccured ? setErrorAction({
+ statementId,
+ error: user == null ? EErrorCode.ALREADY_CLAIMED : EErrorCode.CLAIMED_BY_OTHER_USER,
+ errorValue: {user}
+ }) : setTaskEntityAction({task: task?.task});
}),
catchHttpErrorTo(setErrorAction({
- statementId,
- error: EErrorCode.CLAIMED_BY_OTHER_USER
- }), EHttpStatusCodes.BAD_REQUEST)
+ error: EErrorCode.UNEXPECTED,
+ }))
),
this.fetchTasks(statementId)
).pipe(
@@ -202,20 +211,30 @@
statementId: number,
claimNext: boolean | EAPIProcessTaskDefinitionKey,
navigate?: boolean
- ): Observable<IAPIProcessTask> {
+ ): Observable<{ task: IAPIProcessTask, errorOccured?: boolean }> {
let taskId: string;
+ let nextTask: IAPIProcessTask;
+ let error = false;
return claimNext == null || claimNext === false ? EMPTY : this.getNextTask(statementId, claimNext).pipe(
- switchMap((_taskId) => _taskId ? this.processApiService.claimStatementTask(statementId, taskId = _taskId) : EMPTY),
+ switchMap((task) => {
+ nextTask = task;
+ return task?.taskId ? this.processApiService.claimStatementTask(statementId, taskId = task?.taskId) : EMPTY;
+ }),
+ catchHttpError(() => {
+ error = true;
+ return EMPTY;
+ }, EHttpStatusCodes.BAD_REQUEST),
+ map(() => ({task: nextTask, errorOccured: error})),
endWithObservable(() => navigate ? this.navigateTo(statementId, taskId) : EMPTY)
);
}
- public getNextTask(statementId: number, next?: boolean | EAPIProcessTaskDefinitionKey): Observable<string> {
+ public getNextTask(statementId: number, next?: boolean | EAPIProcessTaskDefinitionKey): Observable<IAPIProcessTask> {
return this.processApiService.getStatementTasks(statementId).pipe(
map((tasks) => {
return arrayJoin(tasks).find((_) => {
return next === true || _.taskDefinitionKey === next;
- })?.taskId;
+ });
})
);
}
diff --git a/src/app/store/root/actions/error.actions.ts b/src/app/store/root/actions/error.actions.ts
index af04c23..bccc8a7 100644
--- a/src/app/store/root/actions/error.actions.ts
+++ b/src/app/store/root/actions/error.actions.ts
@@ -15,5 +15,5 @@
export const setErrorAction = createAction(
"[API] Set error",
- props<{ error: string, statementId?: number | "new" }>()
+ props<{ error: string, errorValue?: { [key: string]: string }, statementId?: number | "new" }>()
);
diff --git a/src/app/store/root/effects/toast.effect.ts b/src/app/store/root/effects/toast.effect.ts
index 56da623..2e8da0e 100644
--- a/src/app/store/root/effects/toast.effect.ts
+++ b/src/app/store/root/effects/toast.effect.ts
@@ -24,7 +24,7 @@
public toast$ = createEffect(() => this.actions.pipe(
ofType(setErrorAction),
filter((action) => action.statementId == null && action.error != null),
- mergeMap(async (action) => this.toast(action.error))
+ mergeMap(async (action) => this.toast(action.error, action.errorValue))
), {dispatch: false});
public constructor(
@@ -34,17 +34,17 @@
) {
}
- public async toast(error: string) {
+ public async toast(error: string, errorValue?: { [key: string]: string }) {
this.messageService.add({
severity: "error",
life: 7000,
summary: await this.getTranslation("shared.errorMessages.title"),
- detail: await this.getTranslation(error)
+ detail: await this.getTranslation(error, errorValue)
});
}
- private async getTranslation(msg: string) {
- return this.translateService.get(msg).pipe(take(1)).toPromise();
+ private async getTranslation(msg: string, value?: { [key: string]: string }) {
+ return this.translateService.get(msg, value).pipe(take(1)).toPromise();
}
}
diff --git a/src/app/store/root/model/EErrorCode.ts b/src/app/store/root/model/EErrorCode.ts
index 02a601c..5750bbb 100644
--- a/src/app/store/root/model/EErrorCode.ts
+++ b/src/app/store/root/model/EErrorCode.ts
@@ -14,6 +14,7 @@
export enum EErrorCode {
UNEXPECTED = "shared.errorMessages.unexpected",
TASK_TO_COMPLETE_NOT_FOUND = "shared.errorMessages.taskToCompleteNotFound",
+ ALREADY_CLAIMED = "shared.errorMessages.alreadyClaimed",
CLAIMED_BY_OTHER_USER = "shared.errorMessages.claimedByAnotherUser",
MISSING_FORM_DATA = "shared.errorMessages.missingFormData",
FAILED_LOADING_CONTACT = "shared.errorMessages.failedLoadingContact",
@@ -22,5 +23,9 @@
FAILED_MAIL_TRANSFER = "shared.errorMessages.failedMailTransfer",
INVALID_TEXT_ARRANGEMENT = "shared.errorMessages.invalidTextArrangement",
COULD_NOT_LOAD_MAIL_DATA = "shared.errorMessages.couldNotLoadMailData",
- COULD_NOT_SEND_MAIL = "shared.errorMessages.couldNotSendMail"
+ COULD_NOT_SEND_MAIL = "shared.errorMessages.couldNotSendMail",
+ COULD_NOT_ADD_TAG = "shared.errorMessages.couldNotAddTag",
+ INVALID_FILE_FORMAT = "shared.errorMessages.invalidFileFormat",
+ SEARCH_NO_RESULT = "shared.errorMessages.searchNoResult",
+ BAD_USER_DATA = "shared.errorMessages.badUserData"
}
diff --git a/src/app/store/root/model/IRootStoreState.ts b/src/app/store/root/model/IRootStoreState.ts
index 2316bdf..6c209b1 100644
--- a/src/app/store/root/model/IRootStoreState.ts
+++ b/src/app/store/root/model/IRootStoreState.ts
@@ -17,7 +17,7 @@
export interface IRootStoreState {
/**
- * Is true iff initialization has finished
+ * Is true if initialization has finished
*/
isLoading?: boolean;
diff --git a/src/app/store/root/services/user-role-route-guard.service.ts b/src/app/store/root/services/user-role-route-guard.service.ts
index e7dc0fa..b271fee 100644
--- a/src/app/store/root/services/user-role-route-guard.service.ts
+++ b/src/app/store/root/services/user-role-route-guard.service.ts
@@ -72,6 +72,15 @@
}
@Injectable({providedIn: "root"})
+export class AdminRouteGuardService extends UserRoleRouteGuardService {
+
+ public constructor(store: Store, router: Router) {
+ super(store, router, {allowed: [EAPIUserRoles.SPA_ADMIN]});
+ }
+
+}
+
+@Injectable({providedIn: "root"})
export class OfficialInChargeOrAdminRouteGuardService extends UserRoleRouteGuardService {
public constructor(store: Store, router: Router) {
diff --git a/src/app/store/settings/actions/departments-settings.actions.ts b/src/app/store/settings/actions/departments-settings.actions.ts
new file mode 100644
index 0000000..2359e26
--- /dev/null
+++ b/src/app/store/settings/actions/departments-settings.actions.ts
@@ -0,0 +1,29 @@
+/********************************************************************************
+ * 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 {createAction, props} from "@ngrx/store";
+import {IAPIDepartmentTable} from "../../../core/api/settings";
+
+export const fetchDepartmentsSettingsAction = createAction(
+ "[Settings] Fetch departments settings"
+);
+
+export const setDepartmentsSettingsAction = createAction(
+ "[API] Set department settings",
+ props<{ data: IAPIDepartmentTable }>()
+);
+
+export const submitDepartmentsSettingsAction = createAction(
+ "[Settings] Submit departments settings",
+ props<{ data: IAPIDepartmentTable }>()
+);
diff --git a/src/app/store/settings/actions/index.ts b/src/app/store/settings/actions/index.ts
index 77e876e..552479d 100644
--- a/src/app/store/settings/actions/index.ts
+++ b/src/app/store/settings/actions/index.ts
@@ -11,4 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+export * from "./departments-settings.actions";
export * from "./settings.actions";
+export * from "./textblock-settings.actions";
+export * from "./users-settings.actions";
diff --git a/src/app/store/settings/actions/settings.actions.ts b/src/app/store/settings/actions/settings.actions.ts
index 75b4845..c3d20b9 100644
--- a/src/app/store/settings/actions/settings.actions.ts
+++ b/src/app/store/settings/actions/settings.actions.ts
@@ -12,8 +12,8 @@
********************************************************************************/
import {createAction, props} from "@ngrx/store";
-import {IAPIStatementType} from "../../../core";
-import {IAPISectorsModel} from "../../../core/api/statements/IAPISectorsModel";
+import {IAPISectorsModel, IAPIStatementType} from "../../../core";
+import {ISettingsLoadingState} from "../model";
export const fetchSettingsAction = createAction(
"[New] Fetch settings"
@@ -28,3 +28,8 @@
"[API] Get sectors",
props<{ sectors: IAPISectorsModel }>()
);
+
+export const setSettingsLoadingStateAction = createAction(
+ "[App/API] Set settings loading state",
+ props<{ state: ISettingsLoadingState }>()
+);
diff --git a/src/app/store/settings/actions/textblock-settings.actions.ts b/src/app/store/settings/actions/textblock-settings.actions.ts
new file mode 100644
index 0000000..da5427d
--- /dev/null
+++ b/src/app/store/settings/actions/textblock-settings.actions.ts
@@ -0,0 +1,29 @@
+/********************************************************************************
+ * 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 {createAction, props} from "@ngrx/store";
+import {IAPITextBlockConfigurationModel} from "../../../core/api/text";
+
+export const fetchTextblockSettingsAction = createAction(
+ "[Settings] Fetch text block settings"
+);
+
+export const setTextblockSettingsAction = createAction(
+ "[API] Set text block settings",
+ props<{ data: IAPITextBlockConfigurationModel }>()
+);
+
+export const submitTextblockSettingsAction = createAction(
+ "[Settings] Submit text block settings",
+ props<{ data: IAPITextBlockConfigurationModel }>()
+);
diff --git a/src/app/store/settings/actions/users-settings.actions.ts b/src/app/store/settings/actions/users-settings.actions.ts
new file mode 100644
index 0000000..9cc8ced
--- /dev/null
+++ b/src/app/store/settings/actions/users-settings.actions.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 {createAction, props} from "@ngrx/store";
+import {IAPIUserInfoExtended, IAPIUserSettings} from "../../../core";
+
+export const syncUserDataAction = createAction(
+ "[Settings] Syncs user data with auth-n-auth"
+);
+
+export const fetchUsersAction = createAction(
+ "[Settings] Fetches users from back end"
+);
+
+export const setUsersDataAction = createAction(
+ "[Store] Set user data",
+ props<{ data: IAPIUserInfoExtended[] }>()
+);
+
+export const submitUserSettingsAction = createAction(
+ "[Settings] Submits settings property for specific user",
+ props<{ userId: number, data: IAPIUserSettings }>()
+);
diff --git a/src/app/store/settings/effects/departments/departments-settings.effect.spec.ts b/src/app/store/settings/effects/departments/departments-settings.effect.spec.ts
new file mode 100644
index 0000000..2f06509
--- /dev/null
+++ b/src/app/store/settings/effects/departments/departments-settings.effect.spec.ts
@@ -0,0 +1,113 @@
+/********************************************************************************
+ * 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 {Observable, Subject, Subscription} from "rxjs";
+import {IAPIDepartmentTable, SPA_BACKEND_ROUTE} from "../../../../core";
+import {
+ fetchDepartmentsSettingsAction,
+ setDepartmentsSettingsAction,
+ setSettingsLoadingStateAction,
+ submitDepartmentsSettingsAction
+} from "../../actions";
+import {DepartmentsSettingsEffect} from "./departments-settings.effect";
+
+describe("DepartmentsSettingsEffect", () => {
+
+ let actions$: Observable<Action>;
+ let httpTestingController: HttpTestingController;
+ let effect: DepartmentsSettingsEffect;
+ let subscription: Subscription;
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ imports: [
+ HttpClientTestingModule
+ ],
+ providers: [
+ provideMockActions(() => actions$),
+ {
+ provide: SPA_BACKEND_ROUTE,
+ useValue: "/"
+ }
+ ]
+ });
+ effect = TestBed.inject(DepartmentsSettingsEffect);
+ httpTestingController = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ if (subscription != null) {
+ subscription.unsubscribe();
+ }
+ });
+
+ it("should fetch department settings", () => {
+ const results: Action[] = [];
+ const actionSubject = new Subject<Action>();
+ const data: IAPIDepartmentTable = {};
+ actions$ = actionSubject;
+ subscription = effect.fetch$.subscribe((_) => results.push(_));
+
+ actionSubject.next(fetchDepartmentsSettingsAction());
+ expectGetDepartmentsSettingsRequest(data);
+
+ expect(results).toEqual([
+ setSettingsLoadingStateAction({state: {fetchingDepartments: true}}),
+ setDepartmentsSettingsAction({data}),
+ setSettingsLoadingStateAction({state: {fetchingDepartments: false}})
+ ]);
+ httpTestingController.verify();
+ });
+
+ it("should submit departments settings", () => {
+ const results: Action[] = [];
+ const actionSubject = new Subject<Action>();
+ const data: IAPIDepartmentTable = {};
+ actions$ = actionSubject;
+ subscription = effect.submit$.subscribe((_) => results.push(_));
+
+ actionSubject.next(submitDepartmentsSettingsAction({data}));
+ expectPutDepartmentsSettingsRequest(data);
+ expectGetDepartmentsSettingsRequest(data);
+
+ expect(results).toEqual([
+ setSettingsLoadingStateAction({state: {submittingDepartments: true}}),
+ setSettingsLoadingStateAction({state: {fetchingDepartments: true}}),
+ setDepartmentsSettingsAction({data}),
+ setSettingsLoadingStateAction({state: {fetchingDepartments: false}}),
+ setSettingsLoadingStateAction({state: {submittingDepartments: false}})
+ ]);
+ httpTestingController.verify();
+ });
+
+ function expectGetDepartmentsSettingsRequest(data: IAPIDepartmentTable) {
+ const url = `/admin/departments`;
+ const request = httpTestingController.expectOne(url);
+ expect(request.request.method).toBe("GET");
+ request.flush(data);
+ }
+
+ function expectPutDepartmentsSettingsRequest(data: IAPIDepartmentTable) {
+ const url = `/admin/departments`;
+ const request = httpTestingController.expectOne(url);
+ expect(request.request.method).toBe("PUT");
+ expect(request.request.body).toEqual(data);
+ request.flush(data);
+ }
+
+});
+
diff --git a/src/app/store/settings/effects/departments/departments-settings.effect.ts b/src/app/store/settings/effects/departments/departments-settings.effect.ts
new file mode 100644
index 0000000..435f0cf
--- /dev/null
+++ b/src/app/store/settings/effects/departments/departments-settings.effect.ts
@@ -0,0 +1,65 @@
+/********************************************************************************
+ * 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} from "rxjs";
+import {endWith, map, startWith, switchMap} from "rxjs/operators";
+import {IAPIDepartmentTable, SettingsApiService} from "../../../../core";
+import {catchErrorTo, catchHttpErrorTo, EHttpStatusCodes} from "../../../../util";
+import {EErrorCode, setErrorAction} from "../../../root";
+import {
+ fetchDepartmentsSettingsAction,
+ setDepartmentsSettingsAction,
+ setSettingsLoadingStateAction,
+ submitDepartmentsSettingsAction
+} from "../../actions";
+
+@Injectable({providedIn: "root"})
+export class DepartmentsSettingsEffect {
+
+ public fetch$ = createEffect(() => this.actions.pipe(
+ ofType(fetchDepartmentsSettingsAction),
+ switchMap(() => this.fetch())
+ ));
+
+ public submit$ = createEffect(() => this.actions.pipe(
+ ofType(submitDepartmentsSettingsAction),
+ switchMap((action) => this.submit(action.data))
+ ));
+
+ public constructor(public actions: Actions, public settingsApiService: SettingsApiService) {
+
+ }
+
+ public fetch(): Observable<Action> {
+ return this.settingsApiService.getDepartmentTable().pipe(
+ map((data) => setDepartmentsSettingsAction({data})),
+ catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
+ startWith(setSettingsLoadingStateAction({state: {fetchingDepartments: true}})),
+ endWith(setSettingsLoadingStateAction({state: {fetchingDepartments: false}}))
+ );
+ }
+
+ public submit(data: IAPIDepartmentTable): Observable<Action> {
+ return this.settingsApiService.putDepartmentTable(data).pipe(
+ switchMap(() => this.fetch()),
+ catchHttpErrorTo(setErrorAction({error: EErrorCode.INVALID_FILE_FORMAT}), EHttpStatusCodes.BAD_REQUEST),
+ catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
+ startWith(setSettingsLoadingStateAction({state: {submittingDepartments: true}})),
+ endWith(setSettingsLoadingStateAction({state: {submittingDepartments: false}}))
+ );
+ }
+
+}
diff --git a/src/app/store/settings/effects/index.ts b/src/app/store/settings/effects/index.ts
index 8ea1eaa..b16c561 100644
--- a/src/app/store/settings/effects/index.ts
+++ b/src/app/store/settings/effects/index.ts
@@ -11,4 +11,6 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+export * from "./departments/departments-settings.effect";
+export * from "./textblock/textblock-settings.effect";
export * from "./fetch-settings.effect";
diff --git a/src/app/store/settings/effects/textblock/textblock-settings.effect.spec.ts b/src/app/store/settings/effects/textblock/textblock-settings.effect.spec.ts
new file mode 100644
index 0000000..766b668
--- /dev/null
+++ b/src/app/store/settings/effects/textblock/textblock-settings.effect.spec.ts
@@ -0,0 +1,113 @@
+/********************************************************************************
+ * 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 {Observable, Subject, Subscription} from "rxjs";
+import {IAPITextBlockConfigurationModel, SPA_BACKEND_ROUTE} from "../../../../core";
+import {
+ fetchTextblockSettingsAction,
+ setSettingsLoadingStateAction,
+ setTextblockSettingsAction,
+ submitTextblockSettingsAction
+} from "../../actions";
+import {TextblockSettingsEffect} from "./textblock-settings.effect";
+
+describe("TextblockSettingsEffect", () => {
+
+ let actions$: Observable<Action>;
+ let httpTestingController: HttpTestingController;
+ let effect: TextblockSettingsEffect;
+ let subscription: Subscription;
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ imports: [
+ HttpClientTestingModule
+ ],
+ providers: [
+ provideMockActions(() => actions$),
+ {
+ provide: SPA_BACKEND_ROUTE,
+ useValue: "/"
+ }
+ ]
+ });
+ effect = TestBed.inject(TextblockSettingsEffect);
+ httpTestingController = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ if (subscription != null) {
+ subscription.unsubscribe();
+ }
+ });
+
+ it("should fetch department settings", () => {
+ const results: Action[] = [];
+ const actionSubject = new Subject<Action>();
+ const data: IAPITextBlockConfigurationModel = {...{} as IAPITextBlockConfigurationModel, selects: {selectField: ["option1"]}};
+ actions$ = actionSubject;
+ subscription = effect.fetch$.subscribe((_) => results.push(_));
+
+ actionSubject.next(fetchTextblockSettingsAction());
+ expectGetTextblockConfigRequest(data);
+
+ expect(results).toEqual([
+ setSettingsLoadingStateAction({state: {fetchingTextblocks: true}}),
+ setTextblockSettingsAction({data}),
+ setSettingsLoadingStateAction({state: {fetchingTextblocks: false}})
+ ]);
+ httpTestingController.verify();
+ });
+
+ it("should submit departments settings", () => {
+ const results: Action[] = [];
+ const actionSubject = new Subject<Action>();
+ const data: IAPITextBlockConfigurationModel = {...{} as IAPITextBlockConfigurationModel, selects: {selectField: ["option1"]}};
+ actions$ = actionSubject;
+ subscription = effect.submit$.subscribe((_) => results.push(_));
+
+ actionSubject.next(submitTextblockSettingsAction({data}));
+ expectPutTextblockConfigRequest(data);
+ expectGetTextblockConfigRequest(data);
+
+ expect(results).toEqual([
+ setSettingsLoadingStateAction({state: {submittingTextblocks: true}}),
+ setSettingsLoadingStateAction({state: {fetchingTextblocks: true}}),
+ setTextblockSettingsAction({data}),
+ setSettingsLoadingStateAction({state: {fetchingTextblocks: false}}),
+ setSettingsLoadingStateAction({state: {submittingTextblocks: false}})
+ ]);
+ httpTestingController.verify();
+ });
+
+ function expectGetTextblockConfigRequest(data: IAPITextBlockConfigurationModel) {
+ const url = `/admin/textblockconfig`;
+ const request = httpTestingController.expectOne(url);
+ expect(request.request.method).toBe("GET");
+ request.flush(data);
+ }
+
+ function expectPutTextblockConfigRequest(data: IAPITextBlockConfigurationModel) {
+ const url = `/admin/textblockconfig`;
+ const request = httpTestingController.expectOne(url);
+ expect(request.request.method).toBe("PUT");
+ expect(request.request.body).toEqual(data);
+ request.flush(data);
+ }
+
+});
+
diff --git a/src/app/store/settings/effects/textblock/textblock-settings.effect.ts b/src/app/store/settings/effects/textblock/textblock-settings.effect.ts
new file mode 100644
index 0000000..cc8db32
--- /dev/null
+++ b/src/app/store/settings/effects/textblock/textblock-settings.effect.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 {Injectable} from "@angular/core";
+import {Actions, createEffect, ofType} from "@ngrx/effects";
+import {Action} from "@ngrx/store";
+import {Observable} from "rxjs";
+import {endWith, map, startWith, switchMap} from "rxjs/operators";
+import {IAPITextBlockConfigurationModel, SettingsApiService} from "../../../../core";
+import {catchErrorTo} from "../../../../util";
+import {EErrorCode, setErrorAction} from "../../../root";
+import {
+ fetchTextblockSettingsAction,
+ setSettingsLoadingStateAction,
+ setTextblockSettingsAction,
+ submitTextblockSettingsAction
+} from "../../actions";
+
+@Injectable({providedIn: "root"})
+export class TextblockSettingsEffect {
+
+ public fetch$ = createEffect(() => this.actions.pipe(
+ ofType(fetchTextblockSettingsAction),
+ switchMap(() => this.fetch())
+ ));
+
+ public submit$ = createEffect(() => this.actions.pipe(
+ ofType(submitTextblockSettingsAction),
+ switchMap((action) => this.submit(action.data))
+ ));
+
+ public constructor(public actions: Actions, public settingsApiService: SettingsApiService) {
+
+ }
+
+ public fetch(): Observable<Action> {
+ return this.settingsApiService.getTextblockConfig().pipe(
+ map((data) => setTextblockSettingsAction({data})),
+ catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
+ startWith(setSettingsLoadingStateAction({state: {fetchingTextblocks: true}})),
+ endWith(setSettingsLoadingStateAction({state: {fetchingTextblocks: false}}))
+ );
+ }
+
+ public submit(data: IAPITextBlockConfigurationModel): Observable<Action> {
+ return this.settingsApiService.putTextblockConfig(data).pipe(
+ switchMap(() => this.fetch()),
+ catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
+ startWith(setSettingsLoadingStateAction({state: {submittingTextblocks: true}})),
+ endWith(setSettingsLoadingStateAction({state: {submittingTextblocks: false}}))
+ );
+ }
+
+}
diff --git a/src/app/store/settings/effects/users/users-settings-effect.service.ts b/src/app/store/settings/effects/users/users-settings-effect.service.ts
new file mode 100644
index 0000000..eb22c34
--- /dev/null
+++ b/src/app/store/settings/effects/users/users-settings-effect.service.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 {Injectable} from "@angular/core";
+import {Actions, createEffect, ofType} from "@ngrx/effects";
+import {Action} from "@ngrx/store";
+import {Observable} from "rxjs";
+import {endWith, map, startWith, switchMap} from "rxjs/operators";
+import {IAPIUserSettings, SettingsApiService} from "../../../../core";
+import {catchHttpErrorTo, EHttpStatusCodes} from "../../../../util";
+import {catchErrorTo} from "../../../../util/rxjs";
+import {EErrorCode, setErrorAction} from "../../../root";
+import {
+ fetchUsersAction,
+ setSettingsLoadingStateAction,
+ setUsersDataAction,
+ submitUserSettingsAction,
+ syncUserDataAction
+} from "../../actions";
+
+@Injectable({providedIn: "root"})
+export class UsersSettingsEffect {
+
+ public submit$ = createEffect(() => this.actions.pipe(
+ ofType(submitUserSettingsAction),
+ switchMap((action) => this.submit(action.userId, {...action.data}))
+ ));
+
+ public fetch$ = createEffect(() => this.actions.pipe(
+ ofType(fetchUsersAction),
+ switchMap(() => this.fetch())
+ ));
+
+ public sync$ = createEffect(() => this.actions.pipe(
+ ofType(syncUserDataAction),
+ switchMap(() => this.sync())
+ ));
+
+ public constructor(public actions: Actions, public settingsApiService: SettingsApiService) {
+ }
+
+ public sync(): Observable<Action> {
+ return this.settingsApiService.syncUserData().pipe(
+ switchMap(() => this.fetch()),
+ catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
+ startWith(setSettingsLoadingStateAction({state: {fetchingUsers: true}})),
+ endWith(setSettingsLoadingStateAction({state: {fetchingUsers: false}}))
+ );
+ }
+
+ public fetch(): Observable<Action> {
+ return this.settingsApiService.fetchUsers().pipe(
+ map((data) => setUsersDataAction({data})),
+ catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
+ startWith(setSettingsLoadingStateAction({state: {fetchingUsers: true}})),
+ endWith(setSettingsLoadingStateAction({state: {fetchingUsers: false}}))
+ );
+ }
+
+ public submit(userId: number, data: IAPIUserSettings): Observable<Action> {
+ const isDepartmentSet = data?.department?.group != null && data?.department?.name != null;
+ data = {
+ ...data,
+ department: isDepartmentSet ? data.department : undefined
+ };
+ return this.settingsApiService.setUserData(userId, data).pipe(
+ switchMap(() => this.fetch()),
+ catchHttpErrorTo(setErrorAction({error: EErrorCode.BAD_USER_DATA}), EHttpStatusCodes.BAD_REQUEST),
+ catchErrorTo(setErrorAction({error: EErrorCode.UNEXPECTED})),
+ startWith(setSettingsLoadingStateAction({state: {submittingUserData: true}})),
+ endWith(setSettingsLoadingStateAction({state: {submittingUserData: false}}))
+ );
+ }
+
+}
diff --git a/src/app/features/search/components/search-filter/IFilterToDisplay.ts b/src/app/store/settings/model/ISettingsLoadingState.ts
similarity index 60%
copy from src/app/features/search/components/search-filter/IFilterToDisplay.ts
copy to src/app/store/settings/model/ISettingsLoadingState.ts
index 203b033..c387831 100644
--- a/src/app/features/search/components/search-filter/IFilterToDisplay.ts
+++ b/src/app/store/settings/model/ISettingsLoadingState.ts
@@ -11,14 +11,15 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export interface IFilterToDisplay {
- filterForType?: boolean;
- status?: boolean;
- editedByMe?: boolean;
- dueDateFrom?: boolean;
- dueDateTo?: boolean;
- creationDateFrom?: boolean;
- creationDateTo?: boolean;
- receiptDateFrom?: boolean;
- receiptDateTo?: boolean;
+/**
+ * Interface which models the loading state of the whole settings store module.
+ */
+export interface ISettingsLoadingState {
+ fetchingDepartments?: boolean;
+ submittingDepartments?: boolean;
+ fetchingTextblocks?: boolean;
+ submittingTextblocks?: boolean;
+ addingTags?: boolean;
+ fetchingUsers?: boolean;
+ submittingUserData?: boolean;
}
diff --git a/src/app/store/settings/model/ISettingsStoreState.ts b/src/app/store/settings/model/ISettingsStoreState.ts
index c9012de..d4cb7c2 100644
--- a/src/app/store/settings/model/ISettingsStoreState.ts
+++ b/src/app/store/settings/model/ISettingsStoreState.ts
@@ -11,9 +11,15 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {IAPIStatementType} from "../../../core";
-import {IAPISectorsModel} from "../../../core/api/statements/IAPISectorsModel";
+import {
+ IAPIDepartmentTable,
+ IAPISectorsModel,
+ IAPIStatementType,
+ IAPITextBlockConfigurationModel,
+ IAPIUserInfoExtended
+} from "../../../core";
import {TStoreEntities} from "../../../util";
+import {ISettingsLoadingState} from "./ISettingsLoadingState";
export interface ISettingsStoreState {
@@ -23,8 +29,28 @@
statementTypes: TStoreEntities<IAPIStatementType>;
/**
- * Object all the available sectors for all newly created statements.
+ * Object with all the available sectors for newly created statements.
*/
sectors: IAPISectorsModel;
+ /**
+ * Object with all departments and sectors; the key is a pair of city and district concatenated with a #.
+ */
+ departments: IAPIDepartmentTable;
+
+ /**
+ * Loading object which indicates which part of the application is loading.
+ */
+ loading?: ISettingsLoadingState;
+
+ /**
+ * Textblock configuration object for newly created statements.
+ */
+ textblock: IAPITextBlockConfigurationModel;
+
+ /**
+ * List of all users with access to the app
+ */
+ users: IAPIUserInfoExtended[];
+
}
diff --git a/src/app/store/settings/model/index.ts b/src/app/store/settings/model/index.ts
index fe19fde..b24c876 100644
--- a/src/app/store/settings/model/index.ts
+++ b/src/app/store/settings/model/index.ts
@@ -11,4 +11,5 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+export * from "./ISettingsLoadingState";
export * from "./ISettingsStoreState";
diff --git a/src/app/store/settings/reducers/departments/departments.reducer.spec.ts b/src/app/store/settings/reducers/departments/departments.reducer.spec.ts
new file mode 100644
index 0000000..0b5c032
--- /dev/null
+++ b/src/app/store/settings/reducers/departments/departments.reducer.spec.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 {IAPIDepartmentTable} from "../../../../core";
+import {setDepartmentsSettingsAction} from "../../actions";
+import {departmentsSettingsReducer} from "./departments.reducer";
+
+describe("departmentsSettingsReducer", () => {
+
+ it("should set state on setDepartmentsSettingsAction", () => {
+ const data: IAPIDepartmentTable = {};
+ expect(departmentsSettingsReducer(null, setDepartmentsSettingsAction({data}))).toBe(data);
+ });
+
+});
diff --git a/src/app/store/settings/reducers/sectors.reducer.ts b/src/app/store/settings/reducers/departments/departments.reducer.ts
similarity index 67%
copy from src/app/store/settings/reducers/sectors.reducer.ts
copy to src/app/store/settings/reducers/departments/departments.reducer.ts
index 53cc6cf..4cb3569 100644
--- a/src/app/store/settings/reducers/sectors.reducer.ts
+++ b/src/app/store/settings/reducers/departments/departments.reducer.ts
@@ -12,12 +12,12 @@
********************************************************************************/
import {createReducer, on} from "@ngrx/store";
-import {IAPISectorsModel} from "../../../core/api/statements/IAPISectorsModel";
-import {setSectorsAction} from "../actions";
+import {IAPIDepartmentTable} from "../../../../core/api/settings";
+import {setDepartmentsSettingsAction} from "../../actions";
-export const sectorsReducer = createReducer<IAPISectorsModel>(
+export const departmentsSettingsReducer = createReducer<IAPIDepartmentTable>(
{},
- on(setSectorsAction, (state, payload) => {
- return payload.sectors ? {...payload.sectors} : state;
+ on(setDepartmentsSettingsAction, (state, action) => {
+ return action.data;
})
);
diff --git a/src/app/store/settings/reducers/index.ts b/src/app/store/settings/reducers/index.ts
index fd47105..098580f 100644
--- a/src/app/store/settings/reducers/index.ts
+++ b/src/app/store/settings/reducers/index.ts
@@ -11,4 +11,8 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./statement-types.reducer";
+export * from "./departments/departments.reducer";
+export * from "./loading/loading.reducer";
+export * from "./sectors/sectors.reducer";
+export * from "./statement-types/statement-types.reducer";
+export * from "./textblock/textblock.reducer";
diff --git a/src/app/store/settings/reducers/loading/loading.reducer.spec.ts b/src/app/store/settings/reducers/loading/loading.reducer.spec.ts
new file mode 100644
index 0000000..8cbd9ac
--- /dev/null
+++ b/src/app/store/settings/reducers/loading/loading.reducer.spec.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 {setSettingsLoadingStateAction} from "../../actions";
+import {settingsLoadingReducer} from "./loading.reducer";
+
+describe("settingsLoadingReducer", () => {
+
+ it("should update state on setSettingsLoadingStateAction", () => {
+ const action = setSettingsLoadingStateAction({state: {fetchingDepartments: false}});
+ expect(settingsLoadingReducer({fetchingDepartments: true}, action)).toEqual({fetchingDepartments: false});
+ });
+
+});
diff --git a/src/app/store/settings/reducers/sectors.reducer.ts b/src/app/store/settings/reducers/loading/loading.reducer.ts
similarity index 66%
copy from src/app/store/settings/reducers/sectors.reducer.ts
copy to src/app/store/settings/reducers/loading/loading.reducer.ts
index 53cc6cf..5149686 100644
--- a/src/app/store/settings/reducers/sectors.reducer.ts
+++ b/src/app/store/settings/reducers/loading/loading.reducer.ts
@@ -12,12 +12,12 @@
********************************************************************************/
import {createReducer, on} from "@ngrx/store";
-import {IAPISectorsModel} from "../../../core/api/statements/IAPISectorsModel";
-import {setSectorsAction} from "../actions";
+import {setSettingsLoadingStateAction} from "../../actions";
+import {ISettingsLoadingState} from "../../model";
-export const sectorsReducer = createReducer<IAPISectorsModel>(
- {},
- on(setSectorsAction, (state, payload) => {
- return payload.sectors ? {...payload.sectors} : state;
+export const settingsLoadingReducer = createReducer<ISettingsLoadingState>(
+ undefined,
+ on(setSettingsLoadingStateAction, (state, action) => {
+ return {...state, ...action.state};
})
);
diff --git a/src/app/store/settings/reducers/sectors.reducer.spec.ts b/src/app/store/settings/reducers/sectors/sectors.reducer.spec.ts
similarity index 90%
rename from src/app/store/settings/reducers/sectors.reducer.spec.ts
rename to src/app/store/settings/reducers/sectors/sectors.reducer.spec.ts
index a228615..07d779f 100644
--- a/src/app/store/settings/reducers/sectors.reducer.spec.ts
+++ b/src/app/store/settings/reducers/sectors/sectors.reducer.spec.ts
@@ -11,8 +11,8 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {IAPISectorsModel} from "../../../core/api/statements/IAPISectorsModel";
-import {setSectorsAction} from "../actions";
+import {IAPISectorsModel} from "../../../../core";
+import {setSectorsAction} from "../../actions";
import {sectorsReducer} from "./sectors.reducer";
describe("sectorsReducer", () => {
diff --git a/src/app/store/settings/reducers/sectors.reducer.ts b/src/app/store/settings/reducers/sectors/sectors.reducer.ts
similarity index 86%
rename from src/app/store/settings/reducers/sectors.reducer.ts
rename to src/app/store/settings/reducers/sectors/sectors.reducer.ts
index 53cc6cf..2a30ab7 100644
--- a/src/app/store/settings/reducers/sectors.reducer.ts
+++ b/src/app/store/settings/reducers/sectors/sectors.reducer.ts
@@ -12,8 +12,8 @@
********************************************************************************/
import {createReducer, on} from "@ngrx/store";
-import {IAPISectorsModel} from "../../../core/api/statements/IAPISectorsModel";
-import {setSectorsAction} from "../actions";
+import {IAPISectorsModel} from "../../../../core";
+import {setSectorsAction} from "../../actions";
export const sectorsReducer = createReducer<IAPISectorsModel>(
{},
diff --git a/src/app/store/settings/reducers/statement-types.reducer.spec.ts b/src/app/store/settings/reducers/statement-types/statement-types.reducer.spec.ts
similarity index 91%
rename from src/app/store/settings/reducers/statement-types.reducer.spec.ts
rename to src/app/store/settings/reducers/statement-types/statement-types.reducer.spec.ts
index 9143e32..bf66ee7 100644
--- a/src/app/store/settings/reducers/statement-types.reducer.spec.ts
+++ b/src/app/store/settings/reducers/statement-types/statement-types.reducer.spec.ts
@@ -11,9 +11,9 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {IAPIStatementType} from "../../../core/api/settings";
-import {TStoreEntities} from "../../../util/store";
-import {setStatementTypesAction} from "../actions";
+import {IAPIStatementType} from "../../../../core/api/settings";
+import {TStoreEntities} from "../../../../util/store";
+import {setStatementTypesAction} from "../../actions";
import {statementTypesReducer} from "./statement-types.reducer";
describe("statementTypesReducer", () => {
diff --git a/src/app/store/settings/reducers/statement-types.reducer.ts b/src/app/store/settings/reducers/statement-types/statement-types.reducer.ts
similarity index 83%
rename from src/app/store/settings/reducers/statement-types.reducer.ts
rename to src/app/store/settings/reducers/statement-types/statement-types.reducer.ts
index bbadf4b..44bb241 100644
--- a/src/app/store/settings/reducers/statement-types.reducer.ts
+++ b/src/app/store/settings/reducers/statement-types/statement-types.reducer.ts
@@ -12,9 +12,9 @@
********************************************************************************/
import {createReducer, on} from "@ngrx/store";
-import {IAPIStatementType} from "../../../core";
-import {arrayToEntities, TStoreEntities} from "../../../util";
-import {setStatementTypesAction} from "../actions";
+import {IAPIStatementType} from "../../../../core";
+import {arrayToEntities, TStoreEntities} from "../../../../util";
+import {setStatementTypesAction} from "../../actions";
export const statementTypesReducer = createReducer<TStoreEntities<IAPIStatementType>>(
{},
diff --git a/src/app/store/settings/reducers/textblock/textblock.reducer.spec.ts b/src/app/store/settings/reducers/textblock/textblock.reducer.spec.ts
new file mode 100644
index 0000000..0efbf62
--- /dev/null
+++ b/src/app/store/settings/reducers/textblock/textblock.reducer.spec.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 {IAPITextBlockConfigurationModel} from "../../../../core";
+import {setTextblockSettingsAction} from "../../actions";
+import {textblockSettingsReducer} from "./textblock.reducer";
+
+describe("textblockSettingsReducer", () => {
+
+ it("should set state on setTextblockSettingsAction", () => {
+ const data: IAPITextBlockConfigurationModel = {} as IAPITextBlockConfigurationModel;
+ expect(textblockSettingsReducer(null, setTextblockSettingsAction({data}))).toBe(data);
+ });
+
+});
diff --git a/src/app/store/settings/reducers/sectors.reducer.ts b/src/app/store/settings/reducers/textblock/textblock.reducer.ts
similarity index 65%
copy from src/app/store/settings/reducers/sectors.reducer.ts
copy to src/app/store/settings/reducers/textblock/textblock.reducer.ts
index 53cc6cf..c1a8957 100644
--- a/src/app/store/settings/reducers/sectors.reducer.ts
+++ b/src/app/store/settings/reducers/textblock/textblock.reducer.ts
@@ -12,12 +12,12 @@
********************************************************************************/
import {createReducer, on} from "@ngrx/store";
-import {IAPISectorsModel} from "../../../core/api/statements/IAPISectorsModel";
-import {setSectorsAction} from "../actions";
+import {IAPITextBlockConfigurationModel} from "../../../../core/api/text";
+import {setTextblockSettingsAction} from "../../actions";
-export const sectorsReducer = createReducer<IAPISectorsModel>(
- {},
- on(setSectorsAction, (state, payload) => {
- return payload.sectors ? {...payload.sectors} : state;
+export const textblockSettingsReducer = createReducer<IAPITextBlockConfigurationModel>(
+ undefined,
+ on(setTextblockSettingsAction, (state, action) => {
+ return action.data;
})
);
diff --git a/src/app/store/settings/reducers/sectors.reducer.ts b/src/app/store/settings/reducers/users/users.reducer.ts
similarity index 67%
copy from src/app/store/settings/reducers/sectors.reducer.ts
copy to src/app/store/settings/reducers/users/users.reducer.ts
index 53cc6cf..42c20ff 100644
--- a/src/app/store/settings/reducers/sectors.reducer.ts
+++ b/src/app/store/settings/reducers/users/users.reducer.ts
@@ -12,12 +12,12 @@
********************************************************************************/
import {createReducer, on} from "@ngrx/store";
-import {IAPISectorsModel} from "../../../core/api/statements/IAPISectorsModel";
-import {setSectorsAction} from "../actions";
+import {IAPIUserInfoExtended} from "../../../../core";
+import {setUsersDataAction} from "../../actions";
-export const sectorsReducer = createReducer<IAPISectorsModel>(
- {},
- on(setSectorsAction, (state, payload) => {
- return payload.sectors ? {...payload.sectors} : state;
+export const usersSettingsReducer = createReducer<IAPIUserInfoExtended[]>(
+ [],
+ on(setUsersDataAction, (state, action) => {
+ return action.data;
})
);
diff --git a/src/app/store/settings/selectors/settings.selectors.ts b/src/app/store/settings/selectors/settings.selectors.ts
index 367f788..bf352f3 100644
--- a/src/app/store/settings/selectors/settings.selectors.ts
+++ b/src/app/store/settings/selectors/settings.selectors.ts
@@ -13,7 +13,7 @@
import {createFeatureSelector, createSelector} from "@ngrx/store";
import {ISelectOption} from "../../../shared/controls/select";
-import {entitiesToArray} from "../../../util";
+import {entitiesToArray, selectPropertyProjector} from "../../../util";
import {ISettingsStoreState} from "../model";
import {SETTINGS_FEATURE_NAME} from "../settings-reducers.token";
@@ -26,3 +26,28 @@
.map<ISelectOption<number>>((t) => ({label: t?.name, value: t?.id}));
}
);
+
+const settingsLoadingStateSelector = createSelector(
+ settingsStoreSelector,
+ selectPropertyProjector("loading", {})
+);
+
+export const getSettingsLoadingSelector = createSelector(
+ settingsLoadingStateSelector,
+ (loading): boolean => Object.values({...loading}).some((_) => _)
+);
+
+export const getDepartmentsSettingsSelector = createSelector(
+ settingsStoreSelector,
+ selectPropertyProjector("departments", {})
+);
+
+export const getUsersSettingsSelector = createSelector(
+ settingsStoreSelector,
+ selectPropertyProjector("users", [])
+);
+
+export const getTextblockSettingsSelector = createSelector(
+ settingsStoreSelector,
+ selectPropertyProjector("textblock", {selects: {}, groups: [], negativeGroups: []})
+);
diff --git a/src/app/store/settings/settings-reducers.token.ts b/src/app/store/settings/settings-reducers.token.ts
index a45a536..539d7cb 100644
--- a/src/app/store/settings/settings-reducers.token.ts
+++ b/src/app/store/settings/settings-reducers.token.ts
@@ -14,15 +14,25 @@
import {InjectionToken} from "@angular/core";
import {ActionReducerMap} from "@ngrx/store";
import {ISettingsStoreState} from "./model";
-import {statementTypesReducer} from "./reducers";
-import {sectorsReducer} from "./reducers/sectors.reducer";
+import {
+ departmentsSettingsReducer,
+ sectorsReducer,
+ settingsLoadingReducer,
+ statementTypesReducer,
+ textblockSettingsReducer
+} from "./reducers";
+import {usersSettingsReducer} from "./reducers/users/users.reducer";
export const SETTINGS_FEATURE_NAME = "settings";
export const SETTINGS_REDUCER = new InjectionToken<ActionReducerMap<ISettingsStoreState>>("Settings store reducer", {
providedIn: "root",
factory: () => ({
+ departments: departmentsSettingsReducer,
+ loading: settingsLoadingReducer,
+ sectors: sectorsReducer,
statementTypes: statementTypesReducer,
- sectors: sectorsReducer
+ users: usersSettingsReducer,
+ textblock: textblockSettingsReducer
})
});
diff --git a/src/app/store/settings/settings-store.module.ts b/src/app/store/settings/settings-store.module.ts
index 1a3f1c4..65b2e89 100644
--- a/src/app/store/settings/settings-store.module.ts
+++ b/src/app/store/settings/settings-store.module.ts
@@ -14,14 +14,18 @@
import {NgModule} from "@angular/core";
import {EffectsModule} from "@ngrx/effects";
import {StoreModule} from "@ngrx/store";
-import {FetchSettingsEffect} from "./effects";
+import {UsersSettingsEffect} from "./effects/users/users-settings-effect.service";
+import {DepartmentsSettingsEffect, FetchSettingsEffect, TextblockSettingsEffect} from "./effects";
import {SETTINGS_FEATURE_NAME, SETTINGS_REDUCER} from "./settings-reducers.token";
@NgModule({
imports: [
StoreModule.forFeature(SETTINGS_FEATURE_NAME, SETTINGS_REDUCER),
EffectsModule.forFeature([
- FetchSettingsEffect
+ DepartmentsSettingsEffect,
+ FetchSettingsEffect,
+ UsersSettingsEffect,
+ TextblockSettingsEffect
])
]
})
diff --git a/src/app/store/statements/actions/submit.actions.ts b/src/app/store/statements/actions/submit.actions.ts
index 003026a..cccae5c 100644
--- a/src/app/store/statements/actions/submit.actions.ts
+++ b/src/app/store/statements/actions/submit.actions.ts
@@ -83,7 +83,14 @@
}>()
);
+export const submitTagsAction = createAction(
+ "[Edit] Submit tags",
+ props<{
+ labels: string[]
+ }>()
+);
+
export const sendStatementViaMailAction = createAction(
"[Details] Resend statement via email",
- props<{ statementId: number, taskId: string }>()
+ props<{ statementId: number, taskId: string, assignee: string }>()
);
diff --git a/src/app/store/statements/effects/submit-information-form/submit-statement-information-form.effect.ts b/src/app/store/statements/effects/submit-information-form/submit-statement-information-form.effect.ts
index 06a42db..3525bbe 100644
--- a/src/app/store/statements/effects/submit-information-form/submit-statement-information-form.effect.ts
+++ b/src/app/store/statements/effects/submit-information-form/submit-statement-information-form.effect.ts
@@ -124,8 +124,8 @@
return this.taskEffect.claimNext(statementId, EAPIProcessTaskDefinitionKey.ADD_BASIC_INFO_DATA).pipe(
filter((task) => task != null),
map((task) => {
- afterClaim(task.taskId);
- return setTaskEntityAction({task});
+ afterClaim(task?.task?.taskId);
+ return setTaskEntityAction({task: task?.task});
})
);
}
diff --git a/src/app/util/file/file.util.spec.ts b/src/app/util/file/file.util.spec.ts
new file mode 100644
index 0000000..c616a30
--- /dev/null
+++ b/src/app/util/file/file.util.spec.ts
@@ -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
+ ********************************************************************************/
+
+import {parseCsv, reduceToCsv} from "./file.util";
+
+describe("FileUtil", () => {
+ const text = "1;2;3;4\r\n5;6;7;\r\n8;9;;";
+
+ const data = [
+ ["1", "2", "3", "4"],
+ ["5", "6", "7"],
+ ["8", "9"]
+ ];
+
+ it("parseCsv", () => {
+ const expectedData = [data[0], [...data[1], ""], [...data[2], "", ""]];
+ expect(parseCsv(text, ";")).toEqual(expectedData);
+ });
+
+ it("reduceToCsv", () => {
+ expect(reduceToCsv(data, ";")).toEqual(text);
+ });
+
+});
diff --git a/src/app/util/file/file.util.ts b/src/app/util/file/file.util.ts
new file mode 100644
index 0000000..5bd985f
--- /dev/null
+++ b/src/app/util/file/file.util.ts
@@ -0,0 +1,58 @@
+/********************************************************************************
+ * 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 {arrayJoin} from "../store";
+
+/**
+ * Parses a string as CSV file and returns it entries in a list of arrays.
+ * @param text String in CSV format.
+ * @param separator Separator which is used in the CSV format.
+ */
+export function parseCsv(text: string, separator: string): string[][] {
+ return text
+ .replace(new RegExp("\r", "g"), "")
+ .split("\n")
+ .map((row) => row.split(separator));
+}
+
+/**
+ * Reduces a single row of entries to a CSV string.
+ * @param dataRow List of values to reduce.
+ * @param separator Separator which is used in the CSV format.
+ */
+export function reduceRowToCsv(dataRow: string[], separator: string): string {
+ return arrayJoin(dataRow)
+ .reduce<string>((result, rowString) => result + separator + rowString, "")
+ .slice(separator.length);
+}
+
+/**
+ * Reduces a table of entries to a CSV string.
+ * @param data Entries of the table to reduce.
+ * @param separator Separator which is used in the CSV format.
+ */
+export function reduceToCsv(data: string[][], separator: string): string {
+ const rowLength = Math.max(...data.map((row) => row.length));
+ return reduceRowToCsv(
+ data
+ .map<string[]>((row) => {
+ row = [...row];
+ while (row.length < rowLength) {
+ row.push("");
+ }
+ return row;
+ })
+ .map<string>((row) => reduceRowToCsv(row, separator)),
+ "\r\n"
+ );
+}
diff --git a/src/app/features/settings/components/index.ts b/src/app/util/file/index.ts
similarity index 94%
copy from src/app/features/settings/components/index.ts
copy to src/app/util/file/index.ts
index 990bb42..d7dd6c0 100644
--- a/src/app/features/settings/components/index.ts
+++ b/src/app/util/file/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./search";
+export * from "./file.util";
diff --git a/src/app/util/index.ts b/src/app/util/index.ts
index 1efd8d2..cd3b388 100644
--- a/src/app/util/index.ts
+++ b/src/app/util/index.ts
@@ -12,9 +12,9 @@
********************************************************************************/
export * from "./events";
+export * from "./file";
export * from "./forms";
export * from "./http";
+export * from "./moment";
export * from "./rxjs";
export * from "./store";
-
-export * from "./moment";
diff --git a/src/app/util/moment/moment.util.ts b/src/app/util/moment/moment.util.ts
index d475589..9f48a25 100644
--- a/src/app/util/moment/moment.util.ts
+++ b/src/app/util/moment/moment.util.ts
@@ -14,10 +14,19 @@
import * as moment from "moment";
import {MomentFormatSpecification, MomentInput} from "moment";
+/**
+ * Date format for internal use. Field values, component inputs. Is not used for display.
+ */
export const momentFormatInternal = "YYYY-MM-DD";
+/**
+ * Date format to be used for displaying a date in UI components where no time is necessary.
+ */
export const momentFormatDisplayNumeric = "DD.MM.YYYY";
+/**
+ * Date format including time to be used for displaying a date in UI components.
+ */
export const momentFormatDisplayFullDateAndTime = "DD.MM.YYYY HH:mm:ss";
/**
diff --git a/src/app/util/store/store-projectors.util.ts b/src/app/util/store/store-projectors.util.ts
index ea5f0a0..7a1d920 100644
--- a/src/app/util/store/store-projectors.util.ts
+++ b/src/app/util/store/store-projectors.util.ts
@@ -11,6 +11,10 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+/**
+ * Function that returns the store entitiy for the specified key and id. If no value for that key and id exist, returns the default value if
+ * specified.
+ */
export function selectEntityWithIdProjector<TState, TKey extends keyof TState, TId extends keyof TState[TKey]>(
defaultValue: TState[TKey][TId], key: TKey
): (state: TState, id: TId) => TState[TKey][TId];
@@ -23,6 +27,9 @@
(state, id) => state == null || state[key] == null || state[key][id] == null ? defaultValue : state[key][id];
}
+/**
+ * Function that returns the object property for the specified key. If no value for that key exist, returns the default value if specified.
+ */
export function selectPropertyProjector<TState, TKey extends keyof TState>(
key: TKey,
defaultValue?: TState[TKey]
@@ -30,6 +37,10 @@
return (state) => state == null || state[key] == null ? defaultValue : state[key];
}
+/**
+ * Function that returns the object property for the specified key only if it is an array.
+ * If no value for that key exist, returns the default value if specified.
+ */
export function selectArrayProjector<TState, TKey extends keyof TState>(
key: TKey,
defaultValue?: TState[TKey] & Array<any>
diff --git a/src/app/util/store/store.util.ts b/src/app/util/store/store.util.ts
index 76e1439..c7958c9 100644
--- a/src/app/util/store/store.util.ts
+++ b/src/app/util/store/store.util.ts
@@ -50,6 +50,9 @@
}), {});
}
+/**
+ * For an object adds a the items of a list as properties with an id.
+ */
export function setEntitiesObject<T>(
object: TStoreEntities<T>,
list: T[],
diff --git a/src/assets/docu/userDocumentation.pdf b/src/assets/docu/userDocumentation.pdf
new file mode 100644
index 0000000..c60a9df
--- /dev/null
+++ b/src/assets/docu/userDocumentation.pdf
Binary files differ
diff --git a/src/assets/i18n/de.i18.json b/src/assets/i18n/de.i18.json
index 3b1bd96..3198d03 100644
--- a/src/assets/i18n/de.i18.json
+++ b/src/assets/i18n/de.i18.json
@@ -88,6 +88,7 @@
"title": "Eingangsdokumente",
"filter": "Filter",
"noResult": "Keine Anhänge vorhanden.",
+ "added": "Manuell hinzugefügte Anhänge",
"emailDocuments": "Email-Dokumente",
"email": "Email",
"placeholder": "Es sind keine Anhänge zu der Stellungnahme vorhanden."
@@ -108,11 +109,18 @@
"placeholder": "Es wurden keine Fachbereiche zur Bearbeitung ausgewählt."
},
"geoPositions": {
+ "title": "Geographische Position",
"placeholder": "Es wurde keine Position zu der Stellungnahme hinterlegt."
},
"linkedStatements": {
"title": "Verknüpfte Vorgänge",
"placeholder": "Es gibt keine verlinkten Stellungnahmen."
+ },
+ "information": {
+ "title": "Allgemeine Informationen"
+ },
+ "processInformation": {
+ "title": "Prozessinformationen"
}
},
"edit": {
@@ -145,7 +153,8 @@
"title": "Kommentare",
"showPrevious": "Vorherige anzeigen...",
"showAll": "Alle anzeigen...",
- "placeholder": "Einen Kommentar anlegen..."
+ "placeholder": "Einen Kommentar anlegen...",
+ "confirmDelete": "Möchten Sie den Kommentar wirklich löschen?"
},
"contacts": {
"title": "Kontakte",
@@ -231,7 +240,7 @@
"size": "Einträge pro Seite"
},
"map": {
- "openGIS": "Im GIS öffnen"
+ "openGIS": "Im GIS anzeigen"
},
"linkedStatements": {
"precedingStatements": "Vorhergehende Vorgänge",
@@ -265,7 +274,8 @@
"title": "Fehlerbenachrichtung",
"unexpected": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es nocheinmal oder kontaktieren Sie den Support.",
"taskToCompleteNotFound": "Der aktuelle Bearbeitungsschritt wurde bereits abgeschlossen. Bitte prüfen Sie die Daten der Stellungnahmen.",
- "claimedByAnotherUser": "Die Stellungnahme ist bereits von einem anderen Nutzer in Bearbeitung. Bitte kehren Sie zu einem späteren Zeitpunkt zurück.",
+ "alreadyClaimed": "Die Stellungnahme wird bereits von einem anderen Nutzer bearbeitet. Bitte kehren Sie zu einem späteren Zeitpunkt zurück.",
+ "claimedByAnotherUser": "Die Stellungnahme wird bereits von einem anderen Nutzer ({{user}}) bearbeitet. Bitte kehren Sie zu einem späteren Zeitpunkt zurück.",
"missingFormData": "Ein Feld des Formulars benötigt noch einen Wert. Bitte prüfen Sie Ihre Eingaben auf Vollständigkeit.",
"failedLoadingContact": "Die Details für den ausgewählten Kontakt konnten nicht geladen werden. Bitte prüfen Sie Ihre Auswahl auf Vollständigkeit.",
"failedFileUpload": "Beim Hochladen einer Datei ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut oder kontaktieren Sie den Support.",
@@ -273,10 +283,15 @@
"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."
+ "noAccessToContactModule": "Der Zugriff auf das Kontaktstammdatenmodul ist nicht möglich. Bitte kontaktieren Sie den Support.",
+ "couldNotAddTag": "Folgende Tags konnten nicht angelegt werden: {{value}}",
+ "invalidFileFormat": "Das Format der ausgewählten Datei ist ungültig. Bitte prüfen Sie den Inhalt und versuchen Sie es dann erneut.",
+ "searchNoResult": "Zu der Suchanfrage gab es keine passenden Ergebnisse. Passen Sie die Suchbegriffe an und versuchen es erneut.",
+ "badUserData": "Die eingestellten Nutzerdaten konnten nicht gespeichert werden. Überprüfen Sie das Format der Email-Adresse."
}
},
"textBlocks": {
+ "textBlock": "Textbaustein",
"errors": {
"after": "Muss platziert werden nach: ",
"excludes": "Schließt aus: ",
@@ -298,7 +313,8 @@
"from": "von:",
"at": "vom:",
"inbox": "Email Eingang",
- "attachments": "Anhänge"
+ "attachments": "Anhänge",
+ "confirmDelete": "Möchten Sie die selektierte E-Mail wirklich löschen?"
},
"search": {
"title": "Suche",
@@ -313,6 +329,110 @@
"dueDateTo": "Frist bis:",
"finished": "Abgeschlossen",
"open": "Offen",
- "editedByMe": "Eigene Vorgänge"
+ "editedByMe": "Eigene Vorgänge",
+ "placeHolder": "Nach Ort suchen..."
+ },
+ "settings": {
+ "title": "Einstellungen",
+ "save": "Änderungen übernehmen",
+ "departments": {
+ "title": "Fachbereiche und Sparten",
+ "downloadCurrent": "Aktuelle Konfigurationsdatei herunterladen",
+ "selectNew": "Neue Konfigurationsdatei auswählen",
+ "submit": "Konfiguration speichern",
+ "selectedFile": "Ausgewählte Datei:",
+ "departments": "Fachbereiche",
+ "sectors": "Sparten",
+ "organisation": "Organisation",
+ "placeholderNoDepartments": "Es sind keine Fachbereiche vorhanden.",
+ "placeholderNoSectors": "Es sind keine Sparten vorhanden.",
+ "placeholderSearch": "Suchbegriffe eingeben...",
+ "search": "Suche",
+ "table": {
+ "city": "Ort",
+ "district": "Ortsteil",
+ "sectors": "Sparten",
+ "departments": "Fachbereiche"
+ }
+ },
+ "documents": {
+ "title": "Dokumente",
+ "tags": "Marken",
+ "tag": "Marke",
+ "placeholder": "Einen Namen für die Marke eingeben...",
+ "listTitle": "Liste von Marken",
+ "save": "Änderungen übernehmen"
+ },
+ "textBlocks": {
+ "title": "Textbausteine",
+ "titleNegative": "Textbausteine (Negativantwort)",
+ "select": "Auswahlliste:",
+ "entries": "Einträge:",
+ "name": "Name:",
+ "selects": "Auswahllisten",
+ "invalidSelectKey": "Der Name der Auswahlliste ist ungültig.",
+ "pickTextBlock": "Bitte wählen Sie einen Textbaustein zur Bearbeitung aus.",
+ "default": {
+ "selectKey": "Auswahlliste",
+ "selectEntry": "Neuer Eintrag"
+ },
+ "rules": {
+ "excludes": "Schließt aus:",
+ "requiresAnd": "Erfordert alle von:",
+ "requiresOr": "Erfordert mindestens einen von:",
+ "requiresXOR": "Erfordert genau einen von:"
+ },
+ "replacements": {
+ "freeText": "Freitext",
+ "date": "Datum",
+ "city": "Stadt",
+ "district": "Ortsteil",
+ "sectors": "Sparten",
+ "id": "Unser Zeichen",
+ "receiptDate": "Eingangsdatum",
+ "dueDate": "Fristende",
+ "creationDate": "Erstellungsdatum",
+ "customerReference": "Ihr Zeichen",
+ "title": "Titel",
+ "c-community": "Ort (Kontakt)",
+ "c-communitySuffix": "Addressenzusatz (Kontakt)",
+ "c-company": "Firma (Kontakt)",
+ "c-email": "Email (Kontakt)",
+ "c-firstName": "Vorname (Kontakt)",
+ "c-lastName": "Nachname (Kontakt)",
+ "c-houseNumber": "Hausnummer (Kontakt)",
+ "c-postCode": "Postleitzahl (Kontakt)",
+ "c-salutation": "Anrede (Kontakt)",
+ "c-street": "Straße (Kontakt)",
+ "c-title": "Titel (Kontakt)"
+ }
+ },
+ "users": {
+ "title": "Nutzerverwaltung",
+ "email": "Email",
+ "emailPlaceholder": "Email-Adresse für Nutzer festlegen...",
+ "departmentGroup": "Fachbereichsgruppe",
+ "departmentGroupPlaceholder": "Fachbereichgruppe auswählen...",
+ "department": "Fachbereich",
+ "departmentPlaceholder": "Fachbereich auswählen...",
+ "save": "Nutzerdaten speichern",
+ "editTitle": "Nutzer: ",
+ "table": {
+ "userName": "Nutzerkennung",
+ "firstName": "Vorname",
+ "lastName": "Nachname",
+ "email": "Email",
+ "role": "Nutzerrolle"
+ },
+ "role": {
+ "ROLE_SPA_DIVISION_MEMBER": "Fachbearbeiter",
+ "ROLE_SPA_APPROVER": "Genehmiger",
+ "ROLE_SPA_OFFICIAL_IN_CHARGE": "Sachbearbeiter",
+ "ROLE_SPA_ADMIN": "Admin",
+ "ROLE_SPA_CUSTOMER": "Kunde"
+ },
+ "sync": "Nutzer aus Auth-N-Auth synchronisieren",
+ "noDepartment": "Kein Fachbereich"
+ }
}
}
diff --git a/src/theme/user-controls/_button.theme.scss b/src/theme/user-controls/_button.theme.scss
index 835deb3..e3aba61 100644
--- a/src/theme/user-controls/_button.theme.scss
+++ b/src/theme/user-controls/_button.theme.scss
@@ -102,6 +102,7 @@
}
}
+.openk-button.openk-button-icon,
.openk-button.openk-button-rounded {
--icon-scale-factor: 0.7;
@@ -121,6 +122,22 @@
}
}
+.openk-button.openk-button-icon {
+ font-size: 0.66em;
+ border: 0;
+ color: get-color($openk-default-palette, 500, contrast);
+
+ &:not(.openk-info) {
+ background-color: transparent;
+ }
+
+ &:not(.openk-info):active,
+ &:not(.openk-info):focus,
+ &:not(.openk-info):hover {
+ background-color: $openk-background-highlight;
+ }
+}
+
.openk-primary {
.openk-button,