[TOB-219] feat: Add selection of parent statements to workflow form
* Change API statement search interfaces for pagination
* Add pagination to statement search in store
* Include parents when fetching statement details
* Add a component for list of statements
* Add data mocks for testing
* Open collapsibles if content is focused
* Reorganize translations
* Integrate statement select in workflow data form
[TOB-211] feat: Add selection of contact information to info form
* Display zero instead of null in comments component
* Add property contactId to IAPIStatementModel
* Add property contactId to statement info form value
* Add directive to assign css class on invalid form controls
* Add components for pagination and contact data
* Reorganize forms into feature modules
* Extend task complete action to claim next camunda task
* Add back end calls for contact data
* Add utility functions for store reducers
* Add store effects for searching and fetching contact data
* Add store effect for opening contact base data module in new tab
* Integrate contact selection in info form
[TOB-246] feat: Add attachment controls to info form
* Add new back end calls for attachments and remove old code
* Add store module for attachments
* Extend store for fetching and uploading attachments
* Reorganize statement store and adjust submit effect
* Add further utility functions for store reducers
* Add action and effect to open attachments
* Include attachments when fetching statement details
* Add UI components for attachment control
* Change main overflow property to scroll
[TOB-277] feat: Display affected business section to info form
* Add back end calls for avaialable business sections
* Add action and effects for fetching business sections from back end
* Add Integrate into info form
* Fetch settings when visiting new statement form
[TOB-260] test: Increase test coverage
* Add tests for setting reducers
* Add tests for root reducers
* Add tests for process reducers
* Change style to avoid jumping text blocks
Signed-off-by: Christopher Keim <keim@develop-group.de>
diff --git a/package.json b/package.json
index 7f86af6..67386b7 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,13 @@
{
"name": "openkonsequenz-statement-public-affairs",
- "version": "0.4.0",
+ "version": "0.5.0",
"description": "Statement Public Affairs",
"license": "Eclipse Public License - v 2.0",
"routes": {
"spaFrontend": "/statementpaFE",
"spaBackend": "/statementpaBE",
- "portal": "/portalFE"
+ "portal": "/portalFE",
+ "contactDataBase": "/contactdatabase"
},
"scripts": {
"-- Build ----------------": "",
diff --git a/proxy.conf.json b/proxy.conf.json
index d0bec9a..d2dde6c 100644
--- a/proxy.conf.json
+++ b/proxy.conf.json
@@ -10,5 +10,9 @@
"/portalFE": {
"target": "http://localhost:8280",
"secure": false
+ },
+ "/contactdatabase": {
+ "target": "http://localhost:8280",
+ "secure": false
}
}
diff --git a/src/app/core/api/attachments/IAPIAttachmentModel.ts b/src/app/core/api/attachments/IAPIAttachmentModel.ts
new file mode 100644
index 0000000..932efc1
--- /dev/null
+++ b/src/app/core/api/attachments/IAPIAttachmentModel.ts
@@ -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
+ ********************************************************************************/
+
+/**
+ * Interface which represents the model of an uploaded attachment in the back end data base.
+ */
+export interface IAPIAttachmentModel {
+
+ /**
+ * Unique ID of a specifcic attachment.
+ */
+ id: number;
+
+ /**
+ * Name which is used for display in the app.
+ */
+ name: string;
+
+ /**
+ * Type of the attachment, e.g. a PDF or text file.
+ */
+ type: string;
+
+ /**
+ * Size of a specific attachment in bytes.
+ */
+ size: number;
+
+ /**
+ * Timestamp file.
+ */
+ timestamp: string;
+
+ /**
+ * List of IDs for tagging.
+ */
+ tagIds: number[];
+}
diff --git a/src/app/core/api/attachments/attachments-api.service.ts b/src/app/core/api/attachments/attachments-api.service.ts
new file mode 100644
index 0000000..171cfb0
--- /dev/null
+++ b/src/app/core/api/attachments/attachments-api.service.ts
@@ -0,0 +1,56 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {HttpClient} from "@angular/common/http";
+import {Inject, Injectable} from "@angular/core";
+import {urlJoin} from "../../../util/http";
+import {SPA_BACKEND_ROUTE} from "../../external-routes";
+import {IAPIAttachmentModel} from "./IAPIAttachmentModel";
+
+@Injectable({providedIn: "root"})
+export class AttachmentsApiService {
+
+ public constructor(
+ protected readonly httpClient: HttpClient,
+ @Inject(SPA_BACKEND_ROUTE) protected readonly baseUrl: string
+ ) {
+
+ }
+
+ /**
+ * Fetches a list of all attachments belonging to a statement.
+ */
+ public getAttachments(statementId: number) {
+ const endPoint = `/statements/${statementId}/attachments`;
+ return this.httpClient.get<IAPIAttachmentModel[]>(urlJoin(this.baseUrl, endPoint));
+ }
+
+ /**
+ * Uploads a new file to the back end linked to a specific statement.
+ */
+ public postAttachment(statementId: number, taskId: string, file: File) {
+ const endPoint = `/process/statements/${statementId}/task/${taskId}/attachments`;
+ const formData = new FormData();
+ formData.append("attachment", file, file.name);
+ return this.httpClient.post<IAPIAttachmentModel>(urlJoin(this.baseUrl, endPoint), formData);
+ }
+
+ /**
+ * Uploads a new file to the back end linked to a specific statement.
+ */
+ public deleteAttachment(statementId: number, taskId: string, attachmentId: number) {
+ const endPoint = `/process/statements/${statementId}/task/${taskId}/attachments/${attachmentId}`;
+ return this.httpClient.delete(urlJoin(this.baseUrl, endPoint));
+ }
+
+}
diff --git a/src/app/core/api/attachments/index.ts b/src/app/core/api/attachments/index.ts
new file mode 100644
index 0000000..fe19c7e
--- /dev/null
+++ b/src/app/core/api/attachments/index.ts
@@ -0,0 +1,15 @@
+/********************************************************************************
+ * 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 "./attachments-api.service";
+export * from "./IAPIAttachmentModel";
diff --git a/src/app/core/api/contacts/IAPIContactPerson.ts b/src/app/core/api/contacts/IAPIContactPerson.ts
new file mode 100644
index 0000000..d92f4c1
--- /dev/null
+++ b/src/app/core/api/contacts/IAPIContactPerson.ts
@@ -0,0 +1,21 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+export interface IAPIContactPerson {
+ companyId: string;
+ companyName: string;
+ email: string;
+ id: string;
+ firstName: string;
+ lastName: string;
+}
diff --git a/src/app/core/api/contacts/IAPIContactPersonDetails.ts b/src/app/core/api/contacts/IAPIContactPersonDetails.ts
new file mode 100644
index 0000000..4fcda36
--- /dev/null
+++ b/src/app/core/api/contacts/IAPIContactPersonDetails.ts
@@ -0,0 +1,26 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+export interface IAPIContactPersonDetails {
+ community: string;
+ communitySuffix: string;
+ company: string;
+ email: string;
+ firstName: string;
+ houseNumber: string;
+ lastName: string;
+ postCode: string;
+ salutation: string;
+ street: string;
+ title: string;
+}
diff --git a/src/app/core/api/contacts/contacts-api.service.ts b/src/app/core/api/contacts/contacts-api.service.ts
new file mode 100644
index 0000000..9007693
--- /dev/null
+++ b/src/app/core/api/contacts/contacts-api.service.ts
@@ -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
+ ********************************************************************************/
+
+import {HttpClient} from "@angular/common/http";
+import {Inject, Injectable} from "@angular/core";
+import {objectToHttpParams, urlJoin} from "../../../util/http";
+import {SPA_BACKEND_ROUTE} from "../../external-routes";
+import {IAPIPaginationResponse, IAPISearchOptions} from "../shared";
+import {IAPIContactPerson} from "./IAPIContactPerson";
+import {IAPIContactPersonDetails} from "./IAPIContactPersonDetails";
+
+@Injectable({providedIn: "root"})
+export class ContactsApiService {
+
+ public constructor(
+ protected readonly httpClient: HttpClient,
+ @Inject(SPA_BACKEND_ROUTE) protected readonly baseUrl: string
+ ) {
+
+ }
+
+ /**
+ * Fetches a paginated list of contacts from the back end data base.
+ */
+ public getContacts(searchOptions: IAPISearchOptions) {
+ const endPoint = `contacts`;
+ const params = objectToHttpParams({...searchOptions});
+ return this.httpClient.get<IAPIPaginationResponse<IAPIContactPerson>>(urlJoin(this.baseUrl, endPoint), {params});
+ }
+
+ /**
+ * Fetches details of a specific contact from the back end data base.
+ */
+ public getContactDetails(contactId: string) {
+ const endPoint = `contacts/${contactId}`;
+ return this.httpClient.get<IAPIContactPersonDetails>(urlJoin(this.baseUrl, endPoint));
+ }
+
+ /**
+ * Fetches contact details of a specific statement from the back end data base.
+ */
+ public getStatementsContact(statementId: number) {
+ const endPoint = `/statements/${statementId}/contact`;
+ return this.httpClient.get<IAPIContactPersonDetails>(urlJoin(this.baseUrl, endPoint));
+ }
+
+}
diff --git a/src/app/core/api/contacts/index.ts b/src/app/core/api/contacts/index.ts
new file mode 100644
index 0000000..f311667
--- /dev/null
+++ b/src/app/core/api/contacts/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./contacts-api.service";
diff --git a/src/app/core/api/core/core-api.service.ts b/src/app/core/api/core/core-api.service.ts
index e3d769d..07a64b8 100644
--- a/src/app/core/api/core/core-api.service.ts
+++ b/src/app/core/api/core/core-api.service.ts
@@ -44,7 +44,6 @@
public getUserInfo() {
const endPoint = `userinfo`;
return this.httpClient.get<IAPIUserInfo>(urlJoin(this.baseUrl, endPoint));
-
}
/**
diff --git a/src/app/core/api/index.ts b/src/app/core/api/index.ts
index 56c77de..f558792 100644
--- a/src/app/core/api/index.ts
+++ b/src/app/core/api/index.ts
@@ -11,7 +11,10 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+export * from "./attachments";
+export * from "./contacts";
export * from "./core";
export * from "./process";
export * from "./settings";
export * from "./statements";
+export * from "./shared";
diff --git a/src/app/core/api/process/IAPIStatementHistory.ts b/src/app/core/api/process/IAPIStatementHistory.ts
index 17a0cb3..8177eb2 100644
--- a/src/app/core/api/process/IAPIStatementHistory.ts
+++ b/src/app/core/api/process/IAPIStatementHistory.ts
@@ -11,19 +11,6 @@
* 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
- ********************************************************************************/
-
export interface IAPIStatementHistory {
processName: string;
processVersion: number;
diff --git a/src/app/core/api/process/index.ts b/src/app/core/api/process/index.ts
index 79ba488..ba3072a 100644
--- a/src/app/core/api/process/index.ts
+++ b/src/app/core/api/process/index.ts
@@ -11,6 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+export * from "./EAPIProcessTaskDefinitionKey";
export * from "./IAPIStatementHistory";
export * from "./IAPIProcessTask";
export * from "./IAPIProcessObject";
diff --git a/src/app/core/api/settings/settings-api.service.ts b/src/app/core/api/settings/settings-api.service.ts
index dbd53e5..e84168b 100644
--- a/src/app/core/api/settings/settings-api.service.ts
+++ b/src/app/core/api/settings/settings-api.service.ts
@@ -15,6 +15,7 @@
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 {IAPIStatementType} from "./IAPIStatementType";
@@ -46,4 +47,12 @@
return this.httpClient.get<IAPIDepartmentsConfiguration>(urlJoin(this.baseUrl, endPoint));
}
+ /**
+ * Fetches the list of all sectors.
+ */
+ public getSectors() {
+ const endPoint = `/sectors`;
+ return this.httpClient.get<IAPISectorsModel>(urlJoin(this.baseUrl, endPoint));
+ }
+
}
diff --git a/src/app/core/api/shared/IAPIPaginationResponse.ts b/src/app/core/api/shared/IAPIPaginationResponse.ts
new file mode 100644
index 0000000..e030fa9
--- /dev/null
+++ b/src/app/core/api/shared/IAPIPaginationResponse.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
+ ********************************************************************************/
+
+/**
+ * Interface which represents the response of a paginated search in the back end data base.
+ */
+export interface IAPIPaginationResponse<T> {
+ content: T[];
+ pageable: string;
+ last: boolean;
+ totalPages: number;
+ totalElements: number;
+ size: number;
+ number: number;
+ numberOfElements: number;
+ first: boolean;
+ sort: {
+ sorted: boolean;
+ unsorted: boolean;
+ empty: boolean;
+ };
+ empty: boolean;
+}
diff --git a/src/app/core/api/shared/IAPISearchOptions.ts b/src/app/core/api/shared/IAPISearchOptions.ts
new file mode 100644
index 0000000..3489b18
--- /dev/null
+++ b/src/app/core/api/shared/IAPISearchOptions.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
+ ********************************************************************************/
+
+/**
+ * Interface which represents the options for a paginated search in the back end data base.
+ */
+export interface IAPISearchOptions {
+
+ /**
+ * Search strings to find statements.
+ */
+ q: string;
+
+ /**
+ * Size of the loaded page.
+ */
+ size?: number;
+
+ /**
+ * Number of page.
+ */
+ page?: number;
+
+ /**
+ * Key for sorting the results.
+ */
+ sort?: string;
+
+}
+
diff --git a/src/app/core/api/shared/index.ts b/src/app/core/api/shared/index.ts
new file mode 100644
index 0000000..12dd624
--- /dev/null
+++ b/src/app/core/api/shared/index.ts
@@ -0,0 +1,15 @@
+/********************************************************************************
+ * 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 "./IAPIPaginationResponse";
+export * from "./IAPISearchOptions";
diff --git a/src/app/core/api/statements/IAPISectorsModel.ts b/src/app/core/api/statements/IAPISectorsModel.ts
new file mode 100644
index 0000000..c6cffa9
--- /dev/null
+++ b/src/app/core/api/statements/IAPISectorsModel.ts
@@ -0,0 +1,18 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+export interface IAPISectorsModel {
+
+ [sectorIdentifier: string]: string[];
+
+}
diff --git a/src/app/core/api/statements/IAPIStatementModel.ts b/src/app/core/api/statements/IAPIStatementModel.ts
index c4d0612..5a6a571 100644
--- a/src/app/core/api/statements/IAPIStatementModel.ts
+++ b/src/app/core/api/statements/IAPIStatementModel.ts
@@ -14,24 +14,28 @@
/**
* Interface which represents the model of all basic information of a specific statement in the back end data base.
*/
-export interface IAPIStatementModel {
+export interface IAPIStatementModel extends IAPIPartialStatementModel {
id: number;
+ finished: boolean;
+
+}
+
+export interface IAPIPartialStatementModel {
+
title: string;
dueDate: string;
receiptDate: string;
- taskId: number;
-
- finished: boolean;
-
typeId: number;
city: string;
district: string;
+ contactId: string;
+
}
diff --git a/src/app/core/api/statements/IAPIWorkflowData.ts b/src/app/core/api/statements/IAPIWorkflowData.ts
index 9be5978..41f1ffc 100644
--- a/src/app/core/api/statements/IAPIWorkflowData.ts
+++ b/src/app/core/api/statements/IAPIWorkflowData.ts
@@ -14,7 +14,7 @@
import {IAPIDepartmentGroups} from "../settings";
/**
- * Interface which represents the model of all workflow information of a specific statement in the back end data base.
+ * Interface which represents the workflow information for a specific statement in the back end data base.
*/
export interface IAPIWorkflowData {
diff --git a/src/app/core/api/statements/index.ts b/src/app/core/api/statements/index.ts
index 8270e20..491e3ec 100644
--- a/src/app/core/api/statements/index.ts
+++ b/src/app/core/api/statements/index.ts
@@ -11,7 +11,6 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./IAPIAttachmentModel";
export * from "./IAPICommentModel";
export * from "./IAPIStatementModel";
export * from "./IAPIWorkflowData";
diff --git a/src/app/core/api/statements/statements-api.service.ts b/src/app/core/api/statements/statements-api.service.ts
index c48e2fb..769991a 100644
--- a/src/app/core/api/statements/statements-api.service.ts
+++ b/src/app/core/api/statements/statements-api.service.ts
@@ -13,11 +13,12 @@
import {HttpClient} from "@angular/common/http";
import {Inject, Injectable} from "@angular/core";
-import {urlJoin} from "../../../util";
+import {objectToHttpParams, urlJoin} from "../../../util";
import {SPA_BACKEND_ROUTE} from "../../external-routes";
-import {IAPIAttachmentModel} from "./IAPIAttachmentModel";
+import {IAPIPaginationResponse, IAPISearchOptions} from "../shared";
import {IAPICommentModel} from "./IAPICommentModel";
-import {IAPIStatementModel} from "./IAPIStatementModel";
+import {IAPISectorsModel} from "./IAPISectorsModel";
+import {IAPIPartialStatementModel, IAPIStatementModel} from "./IAPIStatementModel";
import {IAPIWorkflowData} from "./IAPIWorkflowData";
@Injectable({
@@ -33,11 +34,22 @@
}
/**
- * Fetches a list of all existing statements from the back end.
+ * Fetches a list of existing statements from the back end data base.
+ * @param id IDs of statements to fetch.
*/
- public getStatements() {
+ public getStatements(...id: number[]) {
const endPoint = `statements`;
- return this.httpClient.get<IAPIStatementModel[]>(urlJoin(this.baseUrl, endPoint));
+ const params = {id: id.map((_) => "" + _)};
+ return this.httpClient.get<IAPIStatementModel[]>(urlJoin(this.baseUrl, endPoint), {params});
+ }
+
+ /**
+ * Search for a paginated list of statements in the back end data base.
+ */
+ public getStatementSearch(searchOptions: IAPISearchOptions) {
+ const endPoint = `statementsearch`;
+ const params = objectToHttpParams({...searchOptions});
+ return this.httpClient.get<IAPIPaginationResponse<IAPIStatementModel>>(urlJoin(this.baseUrl, endPoint), {params});
}
/**
@@ -52,27 +64,17 @@
/**
* Creates a new statement in the back end data base.
*/
- public postStatement(statement: Partial<IAPIStatementModel>) {
+ public putStatement(statement: IAPIPartialStatementModel) {
const endPoint = `statements`;
return this.httpClient.post<IAPIStatementModel>(urlJoin(this.baseUrl, endPoint), statement);
}
/**
- * Fetches a list of all attachments belonging to a statement.
+ * Creates a new statement in the back end data base.
*/
- public getAllAttachments(statementId: number) {
- const endPoint = `/statements/${statementId}/attachments`;
- return this.httpClient.get<IAPIAttachmentModel[]>(urlJoin(this.baseUrl, endPoint));
- }
-
- /**
- * Uploads a new file to the back end linked to a specific statement.
- */
- public postAttachment(statementId: number, file: File) {
- const endPoint = `/statements/${statementId}/attachments`;
- const formData = new FormData();
- formData.append("attachment", file, file.name);
- return this.httpClient.post<IAPIAttachmentModel>(urlJoin(this.baseUrl, endPoint), formData);
+ public postStatement(statementId: number, taskId: string, statement: IAPIPartialStatementModel) {
+ const endPoint = `/process/statements/${statementId}/task/${taskId}/statement`;
+ return this.httpClient.post<IAPIStatementModel>(urlJoin(this.baseUrl, endPoint), statement);
}
/**
@@ -92,6 +94,22 @@
}
/**
+ * Fetches the IDs of all parents to a specific statement.
+ */
+ public getParentIds(statementId: number) {
+ const endPoint = `/process/statements/${statementId}/workflow/parents`;
+ return this.httpClient.get<number[]>(urlJoin(this.baseUrl, endPoint));
+ }
+
+ /**
+ * Updates the IDs of all parents to specific statement in the back end data base.
+ */
+ public postParentIds(statementId: number, taskId: string, parentIds: number[]) {
+ const endPoint = `/process/statements/${statementId}/task/${taskId}/workflow/parents`;
+ return this.httpClient.post(urlJoin(this.baseUrl, endPoint), parentIds);
+ }
+
+ /**
* Returns a list of all comments for a specific statement.
*/
public getComments(statementId: number) {
@@ -115,4 +133,12 @@
return this.httpClient.delete<void>(urlJoin(this.baseUrl, endPoint));
}
+ /**
+ * Fetches the list of all sectors.
+ */
+ public getSectors(statementId: number) {
+ const endPoint = `/statements/${statementId}/sectors`;
+ return this.httpClient.get<IAPISectorsModel>(urlJoin(this.baseUrl, endPoint));
+ }
+
}
diff --git a/src/app/core/external-routes/contact-data-base-route.token.ts b/src/app/core/external-routes/contact-data-base-route.token.ts
new file mode 100644
index 0000000..d0d9d47
--- /dev/null
+++ b/src/app/core/external-routes/contact-data-base-route.token.ts
@@ -0,0 +1,23 @@
+/********************************************************************************
+ * 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 {environment} from "../../../environments/environment";
+
+/**
+ * Injection token for the external route to the contact base module.
+ */
+export const CONTACT_DATA_BASE_ROUTE = new InjectionToken<string>("External route to the contact data base module", {
+ providedIn: "root",
+ factory: () => environment.routes.contactDataBase
+});
diff --git a/src/app/core/external-routes/index.ts b/src/app/core/external-routes/index.ts
index f40ae01..a4576e0 100644
--- a/src/app/core/external-routes/index.ts
+++ b/src/app/core/external-routes/index.ts
@@ -11,5 +11,6 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+export * from "./contact-data-base-route.token";
export * from "./portal-route.token";
export * from "./spa-backend-route.token";
diff --git a/src/app/features/dashboard/components/dashboard/dashboard.component.ts b/src/app/features/dashboard/components/dashboard/dashboard.component.ts
index a63b2e9..0f397ff 100644
--- a/src/app/features/dashboard/components/dashboard/dashboard.component.ts
+++ b/src/app/features/dashboard/components/dashboard/dashboard.component.ts
@@ -14,9 +14,9 @@
import {Component, OnInit} from "@angular/core";
import {select, Store} from "@ngrx/store";
import {
- fetchAllStatementsAction,
finishedStatementListSelector,
isOfficialInChargeSelector,
+ startStatementSearchAction,
unfinishedStatementListSelector
} from "../../../../store";
@@ -33,17 +33,12 @@
public readonly unfinishedStatements$ = this.store.pipe(select(unfinishedStatementListSelector));
- // public readonly finishedStatements$ = this.store.pipe(select(finishedStatementsListSelector));
-
- // public readonly unfinishedStatements$ = this.store.pipe(select(unfinishedStatementsListSelector));
-
public constructor(private readonly store: Store) {
}
public ngOnInit(): void {
- this.store.dispatch(fetchAllStatementsAction());
- // this.store.dispatch(fetchStatementsAction());
+ this.store.dispatch(startStatementSearchAction({options: {q: ""}}));
}
}
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 a4bb4ce..6eb8c9c 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,14 +64,11 @@
<div class="placeholder"></div>
</app-collapsible>
- <app-comments
+ <app-comments-form
class="statement-details"
- [appCollapsed]="true"
- [appComments]="comments$ | async"
- (appDelete)="deleteComment($event)"
- (appAdd)="addComment($event)">
+ [appCollapsed]="true">
- </app-comments>
+ </app-comments-form>
<app-collapsible
[appCollapsed]="false"
diff --git a/src/app/features/details/statement-details.module.ts b/src/app/features/details/statement-details.module.ts
index e8a1141..d731669 100644
--- a/src/app/features/details/statement-details.module.ts
+++ b/src/app/features/details/statement-details.module.ts
@@ -17,11 +17,11 @@
import {MatIconModule} from "@angular/material/icon";
import {MatTableModule} from "@angular/material/table";
import {TranslateModule} from "@ngx-translate/core";
-import {CommentsModule} from "../../shared/comments";
import {DateControlModule} from "../../shared/controls/date-control";
import {CardModule} from "../../shared/layout/card";
import {CollapsibleModule} from "../../shared/layout/collapsible";
import {SharedPipesModule} from "../../shared/pipes";
+import {CommentsFormModule} from "../forms/comments";
import {ProcessDiagramComponent, ProcessHistoryComponent, ProcessInformationComponent, StatementDetailsComponent} from "./components";
import {BpmnDirective} from "./directives";
import {StatementDetailsRoutingModule} from "./statement-details-routing.module";
@@ -39,7 +39,7 @@
SharedPipesModule,
DateControlModule,
CollapsibleModule,
- CommentsModule
+ CommentsFormModule
],
declarations: [
StatementDetailsComponent,
diff --git a/src/app/features/edit/components/edit-portal/statement-edit-portal.component.html b/src/app/features/edit/components/edit-portal/statement-edit-portal.component.html
index f021ddd..4e24859 100644
--- a/src/app/features/edit/components/edit-portal/statement-edit-portal.component.html
+++ b/src/app/features/edit/components/edit-portal/statement-edit-portal.component.html
@@ -23,21 +23,24 @@
<label class="loading-page-label">{{"edit.loading" | translate}}</label>
</div>
- <app-edit-negative-answer
- (appSubmit)="completeTask($event)"
- *ngSwitchCase="EStatementEditorSites.DRAFT_FOR_NEGATIVE_ANSWER_FORM">
- </app-edit-negative-answer>
+ <app-statement-information-form
+ *ngSwitchCase="EStatementEditorSites.STATEMENT_INFORMATION_FORM">
+
+ <app-comments-form
+ [appCollapsed]="false"
+ class="statement-details">
+ </app-comments-form>
+
+ </app-statement-information-form>
<app-workflow-data-form
- (appSubmit)="submitWorkflowForm($event)"
- (appSubmitAndComplete)="submitWorkflowForm($event, true)"
- *ngSwitchCase="EStatementEditorSites.WORKFLOW_DATA_FORM"
- [appDepartmentGroups]="departmentGroups$ | async"
- [appDepartmentOptions]="departmentOptions$ | async"
- [appValue]="workflowFormData$ | async"
- [appComments]="comments$ | async"
- (appAddComment)="addComment($event)"
- (appDeleteComment)="deleteComment($event)">
+ *ngSwitchCase="EStatementEditorSites.WORKFLOW_DATA_FORM">
+
+ <app-comments-form
+ [appCollapsed]="true"
+ class="statement-details">
+ </app-comments-form>
+
</app-workflow-data-form>
<app-edit-debug
diff --git a/src/app/features/edit/components/edit-portal/statement-edit-portal.component.spec.ts b/src/app/features/edit/components/edit-portal/statement-edit-portal.component.spec.ts
index 533e307..d7cfc6e 100644
--- a/src/app/features/edit/components/edit-portal/statement-edit-portal.component.spec.ts
+++ b/src/app/features/edit/components/edit-portal/statement-edit-portal.component.spec.ts
@@ -13,24 +13,17 @@
import {CommonModule} from "@angular/common";
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
-import {MatIconModule} from "@angular/material/icon";
import {RouterTestingModule} from "@angular/router/testing";
import {MockStore, provideMockStore} from "@ngrx/store/testing";
import {I18nModule} from "../../../../core/i18n";
-import {CardModule} from "../../../../shared/layout/card";
import {PageHeaderModule} from "../../../../shared/layout/page-header";
import {SharedPipesModule} from "../../../../shared/pipes";
import {ProgressSpinnerModule} from "../../../../shared/progress-spinner";
-import {
- addCommentAction,
- completeTaskAction,
- deleteCommentAction,
- fetchStatementDetailsAction,
- queryParamsIdSelector,
- submitWorkflowFormAction,
- taskSelector
-} from "../../../../store";
-import {EditNegativeAnswerComponent} from "../edit-negative-answer";
+import {completeTaskAction, fetchStatementDetailsAction, queryParamsIdSelector, taskSelector} from "../../../../store";
+import {CommentsFormModule} from "../../../forms/comments";
+import {StatementInformationFormModule} from "../../../forms/statement-information";
+import {WorkflowDataFormModule} from "../../../forms/workflow-data/workflow-data-form.module";
+import {StatementEditRoutingModule} from "../../statement-edit-routing.module";
import {StatementEditPortalComponent} from "./statement-edit-portal.component";
describe("StatementEditPortalComponent", () => {
@@ -43,18 +36,20 @@
imports: [
CommonModule,
I18nModule,
- CardModule,
- MatIconModule,
+ StatementEditRoutingModule,
PageHeaderModule,
+ ProgressSpinnerModule,
+ CommentsFormModule,
+ StatementInformationFormModule,
+ WorkflowDataFormModule,
SharedPipesModule,
- RouterTestingModule,
- ProgressSpinnerModule
+
+ RouterTestingModule
],
providers: [
provideMockStore({initialState: {process: {}}})
],
declarations: [
- EditNegativeAnswerComponent,
StatementEditPortalComponent
]
}).compileComponents();
@@ -85,60 +80,12 @@
const action = completeTaskAction({
statementId: 1,
taskId: "19",
- variables: {responsible: {type: "Boolean", value: true}}
+ variables: {responsible: {type: "Boolean", value: true}},
+ claimNext: true
});
taskSelectorMock.setResult({statementId: 1, taskId: "19"} as any);
await component.completeTask(action.variables);
expect(dispatchSpy).toHaveBeenCalledWith(action);
});
- it("should dispatch submit workflow form action", async () => {
- const taskSelectorMock = mockStore.overrideSelector(taskSelector, undefined);
- const dispatchSpy = spyOn(mockStore, "dispatch");
- const data: any = {};
- const action = submitWorkflowFormAction({
- statementId: 1,
- taskId: "19",
- data,
- completeTask: true
- });
-
- taskSelectorMock.setResult({statementId: 1, taskId: "19"} as any);
-
- await component.submitWorkflowForm(data, true);
- expect(dispatchSpy).toHaveBeenCalledWith(action);
-
- action.completeTask = false;
- await component.submitWorkflowForm(data, false);
- expect(dispatchSpy).toHaveBeenCalledWith(action);
- });
-
- it("should dispatch add comment action", async () => {
- const dispatchSpy = spyOn(mockStore, "dispatch");
-
- const queryParamsIdSelectorMock = mockStore.overrideSelector(queryParamsIdSelector, undefined);
- queryParamsIdSelectorMock.setResult(19);
- mockStore.refreshState();
-
- await component.addComment("test comment");
- expect(dispatchSpy).toHaveBeenCalledWith(addCommentAction({
- statementId: 19,
- text: "test comment"
- }));
- });
-
- it("should dispatch delete comment action", async () => {
- const dispatchSpy = spyOn(mockStore, "dispatch");
-
- const queryParamsIdSelectorMock = mockStore.overrideSelector(queryParamsIdSelector, undefined);
- queryParamsIdSelectorMock.setResult(19);
- mockStore.refreshState();
-
- await component.deleteComment(2);
- expect(dispatchSpy).toHaveBeenCalledWith(deleteCommentAction({
- statementId: 19,
- commentId: 2
- }));
- });
-
});
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 bfb046c..e999eea 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
@@ -13,23 +13,10 @@
import {Component, OnDestroy, OnInit} from "@angular/core";
import {select, Store} from "@ngrx/store";
-import {Subscription} from "rxjs";
-import {take} from "rxjs/operators";
+import {Subject} from "rxjs";
+import {switchMap, take, takeUntil} from "rxjs/operators";
import {IAPIProcessObject} from "../../../../core";
-import {
- addCommentAction,
- completeTaskAction,
- deleteCommentAction,
- departmentGroupsSelector,
- departmentOptionsSelector,
- fetchStatementDetailsAction,
- IWorkflowFormValue,
- queryParamsIdSelector,
- statementCommentsSelector,
- submitWorkflowFormAction,
- taskSelector,
- workflowFormValueSelector
-} from "../../../../store";
+import {completeTaskAction, fetchStatementDetailsAction, queryParamsIdSelector, queryParamsSelector, taskSelector} from "../../../../store";
import {EStatementEditSites} from "../../model";
import {statementEditHeaderButtonSelector, statementEditSiteSelector} from "../../selectors";
@@ -45,67 +32,40 @@
public site$ = this.store.pipe(select(statementEditSiteSelector));
- public task$ = this.store.pipe(select(taskSelector));
+ public queryParams$ = this.store.pipe(select(queryParamsSelector));
public statementId$ = this.store.pipe(select(queryParamsIdSelector));
- public departmentOptions$ = this.store.pipe(select(departmentOptionsSelector));
-
- public departmentGroups$ = this.store.pipe(select(departmentGroupsSelector));
-
- public workflowFormData$ = this.store.pipe(select(workflowFormValueSelector));
-
- public comments$ = this.store.pipe(select(statementCommentsSelector));
-
- public isLoading = true;
+ public task$ = this.store.pipe(select(taskSelector));
public headerActions$ = this.store.pipe(select(statementEditHeaderButtonSelector));
- private subscription: Subscription;
+ private destroy$ = new Subject();
constructor(private readonly store: Store) {
}
- public ngOnInit(): void {
- this.subscription = this.statementId$
- .subscribe((statementId) => this.store.dispatch(fetchStatementDetailsAction({statementId})));
+ public ngOnInit() {
+ this.queryParams$.pipe(takeUntil(this.destroy$), switchMap(() => this.statementId$))
+ .subscribe((statementId) => {
+ this.store.dispatch(fetchStatementDetailsAction({statementId}));
+ });
}
public ngOnDestroy() {
- if (this.subscription != null) {
- this.subscription.unsubscribe();
- this.subscription = null;
- }
+ this.destroy$.next();
+ this.destroy$.complete();
}
public async completeTask(variables: IAPIProcessObject) {
const task = await this.task$.pipe(take(1)).toPromise();
this.store.dispatch(completeTaskAction({
- statementId: task?.statementId,
- taskId: task?.taskId,
- variables
- }));
- }
-
- public async submitWorkflowForm(data: IWorkflowFormValue, completeTask?: boolean) {
- const task = await this.task$.pipe(take(1)).toPromise();
- this.store.dispatch(submitWorkflowFormAction({
statementId: task.statementId,
taskId: task.taskId,
- data,
- completeTask
+ variables,
+ claimNext: true
}));
}
- public async addComment(text: string) {
- const statementId = await this.statementId$.pipe(take(1)).toPromise();
- this.store.dispatch(addCommentAction({statementId, text}));
- }
-
- public async deleteComment(commentId: number) {
- const statementId = await this.statementId$.pipe(take(1)).toPromise();
- this.store.dispatch(deleteCommentAction({statementId, commentId}));
- }
-
}
diff --git a/src/app/features/edit/components/index.ts b/src/app/features/edit/components/index.ts
index e7b4a69..52fc836 100644
--- a/src/app/features/edit/components/index.ts
+++ b/src/app/features/edit/components/index.ts
@@ -12,6 +12,4 @@
********************************************************************************/
export * from "./edit-debug";
-export * from "./edit-negative-answer";
export * from "./edit-portal";
-export * from "./workflow-data-form";
diff --git a/src/app/features/edit/model/EStatementEditSites.ts b/src/app/features/edit/model/EStatementEditSites.ts
index 3966994..9d3c335 100644
--- a/src/app/features/edit/model/EStatementEditSites.ts
+++ b/src/app/features/edit/model/EStatementEditSites.ts
@@ -15,7 +15,7 @@
LOADING = "loading",
- BASIC_INFO_DATA_FORM = "basicInfoDataForm",
+ STATEMENT_INFORMATION_FORM = "statementInformationForm",
WORKFLOW_DATA_FORM = "workflowDataForm",
diff --git a/src/app/features/edit/selectors/statement-edit-site.selector.ts b/src/app/features/edit/selectors/statement-edit-site.selector.ts
index 4804f42..b0044ce 100644
--- a/src/app/features/edit/selectors/statement-edit-site.selector.ts
+++ b/src/app/features/edit/selectors/statement-edit-site.selector.ts
@@ -28,7 +28,7 @@
case EAPIProcessTaskDefinitionKey.ADD_BASIC_INFO_DATA:
return queryParams?.negative ?
EStatementEditSites.DRAFT_FOR_NEGATIVE_ANSWER_FORM :
- EStatementEditSites.BASIC_INFO_DATA_FORM;
+ EStatementEditSites.STATEMENT_INFORMATION_FORM;
case EAPIProcessTaskDefinitionKey.ADD_WORK_FLOW_DATA:
return EStatementEditSites.WORKFLOW_DATA_FORM;
}
diff --git a/src/app/features/edit/statement-edit.module.ts b/src/app/features/edit/statement-edit.module.ts
index 298078d..182dca3 100644
--- a/src/app/features/edit/statement-edit.module.ts
+++ b/src/app/features/edit/statement-edit.module.ts
@@ -13,43 +13,33 @@
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 {CommentsModule} from "../../shared/comments";
-import {SelectModule} from "../../shared/controls/select";
-import {CardModule} from "../../shared/layout/card";
-import {CollapsibleModule} from "../../shared/layout/collapsible";
import {PageHeaderModule} from "../../shared/layout/page-header";
import {SharedPipesModule} from "../../shared/pipes";
import {ProgressSpinnerModule} from "../../shared/progress-spinner";
-import {EditDebugComponent, EditNegativeAnswerComponent, StatementEditPortalComponent, WorkflowDataFormComponent} from "./components";
+import {CommentsFormModule} from "../forms/comments";
+import {StatementInformationFormModule} from "../forms/statement-information";
+import {WorkflowDataFormModule} from "../forms/workflow-data/workflow-data-form.module";
+import {EditDebugComponent, StatementEditPortalComponent} from "./components";
import {StatementEditRoutingModule} from "./statement-edit-routing.module";
@NgModule({
imports: [
CommonModule,
- ReactiveFormsModule,
- // FormsModule,
- MatIconModule,
+ TranslateModule,
StatementEditRoutingModule,
- TranslateModule,
-
- CollapsibleModule,
- CardModule,
- SelectModule,
- SharedPipesModule,
PageHeaderModule,
ProgressSpinnerModule,
- CommentsModule,
+ CommentsFormModule,
+ StatementInformationFormModule,
+ WorkflowDataFormModule,
+ SharedPipesModule
],
declarations: [
StatementEditPortalComponent,
- EditNegativeAnswerComponent,
- EditDebugComponent,
- WorkflowDataFormComponent
+ EditDebugComponent
]
})
export class StatementEditModule {
diff --git a/src/app/features/forms/abstract/abstract-reactive-form.component.spec.ts b/src/app/features/forms/abstract/abstract-reactive-form.component.spec.ts
new file mode 100644
index 0000000..4e66572
--- /dev/null
+++ b/src/app/features/forms/abstract/abstract-reactive-form.component.spec.ts
@@ -0,0 +1,63 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {Component} from "@angular/core";
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {FormControl} from "@angular/forms";
+import {provideMockStore} from "@ngrx/store/testing";
+import {createFormGroup} from "../../../util/forms";
+import {AbstractReactiveFormComponent} from "./abstract-reactive-form.component";
+
+@Component({templateUrl: ""})
+class AbstractReactiveFormSpecComponent extends AbstractReactiveFormComponent<{ test: string }> {
+
+ public appFormGroup = createFormGroup<any>({test: new FormControl("")});
+
+}
+
+describe("AbstractReactiveFormComponent", () => {
+
+ let component: AbstractReactiveFormSpecComponent;
+ let fixture: ComponentFixture<AbstractReactiveFormSpecComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ AbstractReactiveFormSpecComponent
+ ],
+ providers: [
+ provideMockStore()
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AbstractReactiveFormSpecComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ expect(() => component.ngOnDestroy()).not.toThrow();
+ });
+
+ it("should patch value to form", () => {
+ let value: any = null;
+ component.appValueChange.subscribe((_) => value = _);
+ component.patchValue({test: "19123"});
+ expect(component.getValue()).toEqual({test: "19123"});
+ expect(value).toEqual({test: "19123"});
+ });
+
+});
diff --git a/src/app/features/forms/abstract/abstract-reactive-form.component.ts b/src/app/features/forms/abstract/abstract-reactive-form.component.ts
new file mode 100644
index 0000000..ea5c102
--- /dev/null
+++ b/src/app/features/forms/abstract/abstract-reactive-form.component.ts
@@ -0,0 +1,46 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {Input, OnDestroy, Output} from "@angular/core";
+import {FormGroup} from "@angular/forms";
+import {defer, Subject} from "rxjs";
+import {map} from "rxjs/operators";
+
+export abstract class AbstractReactiveFormComponent<T extends object> implements OnDestroy {
+
+ @Input()
+ public abstract appFormGroup: FormGroup;
+
+ @Output()
+ public appValueChange = defer(() => this.appFormGroup.valueChanges).pipe(
+ map(() => this.getValue())
+ );
+
+ protected destroy$ = new Subject();
+
+ public ngOnDestroy() {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ @Input("appValue")
+ public patchValue(value: Partial<T>) {
+ this.appFormGroup.patchValue(value);
+ }
+
+ public getValue(): T {
+ return this.appFormGroup.value;
+ }
+
+
+}
diff --git a/src/app/features/forms/abstract/index.ts b/src/app/features/forms/abstract/index.ts
new file mode 100644
index 0000000..e4c0865
--- /dev/null
+++ b/src/app/features/forms/abstract/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./abstract-reactive-form.component";
diff --git a/src/app/features/forms/attachments/attachments-form.module.ts b/src/app/features/forms/attachments/attachments-form.module.ts
new file mode 100644
index 0000000..5aa1c8f
--- /dev/null
+++ b/src/app/features/forms/attachments/attachments-form.module.ts
@@ -0,0 +1,43 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {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 {FileDropModule} from "../../../shared/controls/file-drop";
+import {FileSelectModule} from "../../../shared/controls/file-select";
+import {CollapsibleModule} from "../../../shared/layout/collapsible";
+import {AttachmentsFormGroupComponent} from "./components";
+
+@NgModule({
+ declarations: [
+ AttachmentsFormGroupComponent
+ ],
+ imports: [
+ CommonModule,
+ ReactiveFormsModule,
+ MatIconModule,
+ TranslateModule,
+ CollapsibleModule,
+ FileSelectModule,
+ FileDropModule
+ ],
+ exports: [
+ AttachmentsFormGroupComponent
+ ]
+})
+export class AttachmentsFormModule {
+
+}
diff --git a/src/app/features/forms/attachments/components/attachments-form-group.component.html b/src/app/features/forms/attachments/components/attachments-form-group.component.html
new file mode 100644
index 0000000..5245bbf
--- /dev/null
+++ b/src/app/features/forms/attachments/components/attachments-form-group.component.html
@@ -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
+ -------------------------------------------------------------------------------->
+
+<div [formGroup]="appFormGroup" class="attachments">
+
+ <div *ngIf="(attachments$ | async)?.length > 0"
+ class="attachments--container">
+
+ <label>
+ {{"attachments.edit" | translate }}
+ </label>
+
+ <app-file-select
+ (appOpenAttachment)="openAttachment($event)"
+ [appAttachments]="attachments$ | async"
+ [formControlName]="'removeAttachments'">
+
+ </app-file-select>
+ </div>
+
+ <div class="attachments--container">
+
+ <label>
+ {{"attachments.add" | translate }}
+ </label>
+
+ <app-file-drop
+ #fileDropComponent
+ [formControlName]="'addAttachments'"
+ class="attachments--container--control">
+ </app-file-drop>
+
+ <button (click)="fileDropComponent.openDialog()"
+ [disabled]="fileDropComponent.appDisabled"
+ class="openk-button attachments--select-file-button">
+ <mat-icon>attach_file</mat-icon>
+ {{"attachments.selectFile" | translate }}
+ </button>
+
+ </div>
+
+</div>
diff --git a/src/app/features/forms/attachments/components/attachments-form-group.component.scss b/src/app/features/forms/attachments/components/attachments-form-group.component.scss
new file mode 100644
index 0000000..55bd916
--- /dev/null
+++ b/src/app/features/forms/attachments/components/attachments-form-group.component.scss
@@ -0,0 +1,43 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+:host {
+ display: block;
+ width: 100%;
+}
+
+.attachments {
+ display: flex;
+ flex-flow: row wrap;
+ min-height: 15em;
+ padding: 0.5em;
+ box-sizing: border-box;
+ max-height: 25em;
+ overflow: auto;
+}
+
+.attachments--container {
+ flex: 1 1 calc(50% - 2em);
+ display: flex;
+ flex-flow: column;
+ margin: 0.5em;
+}
+
+.attachments--container--control {
+ flex: 1 1 100%;
+ margin: 0.25em 0 0.5em 0;
+}
+
+.attachments--select-file-button {
+ margin-left: auto;
+}
diff --git a/src/app/features/forms/attachments/components/attachments-form-group.component.spec.ts b/src/app/features/forms/attachments/components/attachments-form-group.component.spec.ts
new file mode 100644
index 0000000..591c0a6
--- /dev/null
+++ b/src/app/features/forms/attachments/components/attachments-form-group.component.spec.ts
@@ -0,0 +1,92 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {MockStore, provideMockStore} from "@ngrx/store/testing";
+import {I18nModule} from "../../../../core/i18n";
+import {clearFileCacheAction} from "../../../../store/attachments/actions";
+import {getStatementAttachmentsSelector, getStatementFileCacheSelector} from "../../../../store/attachments/selectors";
+import {openAttachmentAction} from "../../../../store/root/actions";
+import {queryParamsIdSelector} from "../../../../store/root/selectors";
+import {createAttachmentModelMock} from "../../../../test";
+import {AttachmentsFormModule} from "../attachments-form.module";
+import {AttachmentsFormGroupComponent} from "./attachments-form-group.component";
+
+describe("AttachmentsFormGroupComponent", () => {
+
+ const statementId = 19;
+
+ let storeMock: MockStore;
+ let component: AttachmentsFormGroupComponent;
+ let fixture: ComponentFixture<AttachmentsFormGroupComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ I18nModule,
+ AttachmentsFormModule
+ ],
+ providers: [
+ provideMockStore({
+ initialState: {statements: {}, settings: {}, contacts: {}, attachments: {}},
+ selectors: [
+ {
+ selector: queryParamsIdSelector,
+ value: statementId
+ }
+ ]
+ })
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AttachmentsFormGroupComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ storeMock = TestBed.inject(MockStore);
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should clear file cache on destruction", async () => {
+ const dispatchSpy = spyOn(storeMock, "dispatch");
+ component.ngOnDestroy();
+ await fixture.whenStable();
+ expect(dispatchSpy).toHaveBeenCalledWith(clearFileCacheAction({statementId}));
+ });
+
+ it("should open attachments", async () => {
+ const dispatchSpy = spyOn(storeMock, "dispatch");
+ const attachmentId = 1919;
+ await component.openAttachment(attachmentId);
+ expect(dispatchSpy).toHaveBeenCalledWith(openAttachmentAction({statementId, attachmentId}));
+ });
+
+ it("should add files via the file cache selector", () => {
+ const files = [new File([], "test.pdf")];
+ storeMock.overrideSelector(getStatementFileCacheSelector, files);
+ storeMock.refreshState();
+ expect(component.getValue().addAttachments).toBe(files);
+ });
+
+ it("should remove selected attachments from form value if not available anymore", () => {
+ storeMock.overrideSelector(getStatementAttachmentsSelector, [createAttachmentModelMock(19)]);
+ component.patchValue({removeAttachments: [19, 20]});
+ storeMock.refreshState();
+ expect(component.getValue().removeAttachments).toEqual([19]);
+ });
+
+});
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
new file mode 100644
index 0000000..073a161
--- /dev/null
+++ b/src/app/features/forms/attachments/components/attachments-form-group.component.ts
@@ -0,0 +1,100 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {Component, Input, OnDestroy, OnInit} from "@angular/core";
+import {FormControl} from "@angular/forms";
+import {select, Store} from "@ngrx/store";
+import {take, takeUntil} from "rxjs/operators";
+import {clearFileCacheAction} from "../../../../store/attachments/actions";
+import {getStatementAttachmentsSelector, getStatementFileCacheSelector} from "../../../../store/attachments/selectors";
+import {openAttachmentAction} from "../../../../store/root/actions";
+import {queryParamsIdSelector} from "../../../../store/root/selectors";
+import {createFormGroup} from "../../../../util/forms";
+import {arrayJoin} from "../../../../util/store";
+import {AbstractReactiveFormComponent} from "../../abstract";
+
+export interface IAttachmentFormValue {
+
+ addAttachments: File[];
+
+ removeAttachments: number[];
+
+}
+
+@Component({
+ selector: "app-attachments-form-group",
+ templateUrl: "./attachments-form-group.component.html",
+ styleUrls: ["./attachments-form-group.component.scss"]
+})
+export class AttachmentsFormGroupComponent extends AbstractReactiveFormComponent<IAttachmentFormValue> implements OnInit, OnDestroy {
+
+ @Input()
+ public appCollapsed: boolean;
+
+ @Input()
+ public appFormGroup = createFormGroup<IAttachmentFormValue>({
+ addAttachments: new FormControl([]),
+ removeAttachments: new FormControl([])
+ });
+
+ @Input()
+ public appTitle: string;
+
+ public statementId$ = this.store.pipe(select(queryParamsIdSelector));
+
+ public attachments$ = this.store.pipe(select(getStatementAttachmentsSelector));
+
+ public fileCache$ = this.store.pipe(select(getStatementFileCacheSelector));
+
+ public constructor(public store: Store) {
+ super();
+ }
+
+ public ngOnInit() {
+ this.filterAttachmentsInCurrentValue();
+ this.updateFiles();
+ }
+
+ public ngOnDestroy() {
+ this.clearFileCache();
+ super.ngOnDestroy();
+ }
+
+ public async openAttachment(attachmentId: number) {
+ const statementId = await this.statementId$.pipe(take(1)).toPromise();
+ this.store.dispatch(openAttachmentAction({statementId, attachmentId}));
+ }
+
+ private clearFileCache() {
+ this.statementId$.pipe(take(1))
+ .subscribe((statementId) => this.store.dispatch(clearFileCacheAction({statementId})));
+ }
+
+ private filterAttachmentsInCurrentValue() {
+ this.attachments$.pipe(takeUntil(this.destroy$)).subscribe((attachments) => {
+ const value = this.getValue();
+ this.patchValue({
+ ...value,
+ removeAttachments: arrayJoin(value.removeAttachments)
+ .filter((id) => attachments.some((_) => id === _.id))
+ });
+ });
+ }
+
+ private updateFiles() {
+ this.fileCache$.pipe(takeUntil(this.destroy$)).subscribe((files) => {
+ this.patchValue({addAttachments: files});
+ });
+ }
+
+}
diff --git a/src/app/features/forms/attachments/components/index.ts b/src/app/features/forms/attachments/components/index.ts
new file mode 100644
index 0000000..32dd0e6
--- /dev/null
+++ b/src/app/features/forms/attachments/components/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./attachments-form-group.component";
diff --git a/src/app/features/forms/attachments/index.ts b/src/app/features/forms/attachments/index.ts
new file mode 100644
index 0000000..acb30b4
--- /dev/null
+++ b/src/app/features/forms/attachments/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./attachments-form.module";
diff --git a/src/app/features/forms/comments/comments-form.module.ts b/src/app/features/forms/comments/comments-form.module.ts
new file mode 100644
index 0000000..a88e385
--- /dev/null
+++ b/src/app/features/forms/comments/comments-form.module.ts
@@ -0,0 +1,43 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {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 {DateControlModule} from "../../../shared/controls/date-control";
+import {CollapsibleModule} from "../../../shared/layout/collapsible";
+import {CommentsControlComponent, CommentsFormComponent} from "./components";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ MatIconModule,
+ TranslateModule,
+ MatButtonModule,
+ DateControlModule,
+ CollapsibleModule
+ ],
+ declarations: [
+ CommentsControlComponent,
+ CommentsFormComponent
+ ],
+ exports: [
+ CommentsControlComponent,
+ CommentsFormComponent
+ ]
+})
+export class CommentsFormModule {
+
+}
diff --git a/src/app/features/forms/comments/components/comments-control/comments-control.component.html b/src/app/features/forms/comments/components/comments-control/comments-control.component.html
new file mode 100644
index 0000000..5dffe56
--- /dev/null
+++ b/src/app/features/forms/comments/components/comments-control/comments-control.component.html
@@ -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
+ -------------------------------------------------------------------------------->
+
+<div *ngIf="appComments?.length > 0" class="comments--list">
+
+ <div *ngIf="appCommentsToShow < appComments?.length"
+ class="comments--list--buttons comments--list--buttons---size">
+
+ <button (click)="showMore()"
+ *ngIf="appCommentsToShow + 5 < appComments?.length"
+ class="comments--list--buttons--button openk-button">
+ {{"comments.showPrevious" | translate}}
+ </button>
+ <button (click)="showMore(true)"
+ class="openk-button">
+ {{"comments.showAll" | translate}}
+ </button>
+
+ </div>
+
+ <div *ngFor="let comment of appComments?.slice(-appCommentsToShow)"
+ class="comments--list--comment">
+
+ <div class="comments--list--comment--header">
+ <span class="comments--list--comment--header--author">{{comment.firstName + " " + comment.lastName}}</span>
+ <span class="comments--list--comment--header--time">
+ {{(comment.timestamp | appMomentPipe).format(timeDisplayFormat)}}
+ </span>
+ <button (click)="onDelete(comment.id)" *ngIf="comment.editable"
+ class="comments--list--comment--header--button"
+ mat-icon-button>
+ <mat-icon class="comments--list--comment--header--button--icon">delete_forever</mat-icon>
+ </button>
+ </div>
+ <div class="comments--list--comment--text">
+ <ng-container *ngFor="let block of comment?.text?.split('\n')">
+ {{block}} <br>
+ </ng-container>
+ </div>
+
+ </div>
+</div>
+<div class="comments--newcomment">
+
+ <textarea #textAreaElement (input)="onInput()"
+ [placeholder]="'comments.placeholder' | translate"
+ [value]="''"
+ class="openk-textarea comments--newcomment--textfield"
+ rows="1">
+ </textarea>
+
+ <div *ngIf="hasInputSomething" class="comments--newcomment--textfield--buttons">
+ <button #test (click)="clear()"
+ class="comments--newcomment--textfield--buttons--first-button openk-button openk-danger">
+ {{"shared.actions.delete" | translate}}
+ </button>
+ <button (click)="onSave()"
+ class="openk-button openk-success">{{"shared.actions.save" | translate}}</button>
+ </div>
+</div>
diff --git a/src/app/features/forms/comments/components/comments-control/comments-control.component.scss b/src/app/features/forms/comments/components/comments-control/comments-control.component.scss
new file mode 100644
index 0000000..125d8b4
--- /dev/null
+++ b/src/app/features/forms/comments/components/comments-control/comments-control.component.scss
@@ -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
+ ********************************************************************************/
+
+@import "openk.styles";
+
+:host {
+ width: 100%;
+ height: 100%;
+ padding: 1em;
+ box-sizing: border-box;
+ position: relative;
+ display: grid;
+ grid-template-rows: 1fr auto;
+ grid-template-columns: 100%;
+}
+
+.comments--list--buttons {
+ margin-bottom: 1em;
+}
+
+.comments--list--buttons---size {
+ font-size: 11px;
+}
+
+.comments--list--buttons--button {
+ margin-right: 0.5em;
+}
+
+.comments--list--comment {
+ margin-bottom: 0.75em;
+}
+
+.comments--list--comment--header {
+ margin-bottom: 0.2em;
+ display: flex;
+ align-items: center;
+}
+
+.comments--list--comment--text {
+ width: 100%;
+ word-wrap: break-word;
+}
+
+.comments--list--comment--header--author {
+ font-weight: bold;
+ margin-right: 0.25em;
+}
+
+.comments--list--comment--header--time {
+ margin-right: 0.25em;
+}
+
+.comments--list--comment--header--button {
+ width: 16px;
+ height: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ line-height: 0;
+}
+
+.comments--list--comment--header--button--icon {
+ font-size: 14px;
+ width: 16px;
+ height: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.comments--newcomment--textfield {
+ resize: none;
+ width: 100%;
+ box-sizing: border-box;
+ min-height: 2.5em;
+}
+
+.comments--newcomment--textfield--buttons {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 0.5em;
+}
+
+.comments--newcomment--textfield--buttons--first-button {
+ margin-right: 0.5em;
+}
diff --git a/src/app/features/forms/comments/components/comments-control/comments-control.component.spec.ts b/src/app/features/forms/comments/components/comments-control/comments-control.component.spec.ts
new file mode 100644
index 0000000..110b143
--- /dev/null
+++ b/src/app/features/forms/comments/components/comments-control/comments-control.component.spec.ts
@@ -0,0 +1,172 @@
+/********************************************************************************
+ * 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, ElementRef, ViewChild} from "@angular/core";
+import {ComponentFixture, TestBed} from "@angular/core/testing";
+import {By} from "@angular/platform-browser";
+import {IAPICommentModel} from "../../../../../core/api/statements";
+import {I18nModule} from "../../../../../core/i18n";
+import {CommentsFormModule} from "../../comments-form.module";
+import {CommentsControlComponent} from "./comments-control.component";
+
+@Component({
+ selector: `app-host-component`,
+ template: `
+ <app-comments-control
+ #comments
+ [appComments]="appComments"
+ [appCommentsToShow]="appCommentsToShow"
+ (appDelete)="deleteComment($event)"
+ (appAdd)="addComment($event)">
+ </app-comments-control>
+ `
+})
+class TestHostComponent {
+
+ public appComments: Array<IAPICommentModel>;
+
+ @ViewChild("comments", {read: ElementRef}) comments: ElementRef;
+
+ public appCommentsToShow = 3;
+
+ public constructor() {
+ this.appComments = [];
+ }
+
+ public addComment(text: string) {
+ this.appComments.push(
+ {
+ id: this.appComments.length,
+ text,
+ userName: "test01",
+ firstName: "Vorname",
+ lastName: "Nachname",
+ timestamp: new Date().toString(),
+ editable: true
+ }
+ );
+ }
+
+ public deleteComment(id: number) {
+ this.appComments.splice(id - 1, 1);
+ }
+}
+
+describe("CommentsControlComponent", () => {
+ let component: TestHostComponent;
+ let fixture: ComponentFixture<TestHostComponent>;
+ let childComponent: CommentsControlComponent;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [
+ TestHostComponent
+ ],
+ imports: [
+ CommentsFormModule,
+ I18nModule
+ ]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestHostComponent);
+ component = fixture.componentInstance;
+ childComponent = fixture.debugElement.query(By.directive(CommentsControlComponent)).componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should emit appNewComment with the comment text", () => {
+ spyOn(childComponent.appAdd, "emit").and.callThrough();
+ const textField = fixture.debugElement.query(By.css(".comments--newcomment--textfield"));
+ textField.nativeElement.value = "test comment text";
+ childComponent.onSave();
+ expect(childComponent.appAdd.emit).toHaveBeenCalledWith("test comment text");
+ });
+
+ it("should emit appDeleteComment with the comment id", () => {
+ spyOn(childComponent.appDelete, "emit").and.callThrough();
+ childComponent.onDelete(1);
+ expect(childComponent.appDelete.emit).toHaveBeenCalledWith(1);
+ });
+
+ it("should always show the full textarea", () => {
+ spyOn(childComponent, "resize").and.callThrough();
+ const textField = fixture.debugElement.query(By.css(".comments--newcomment--textfield")).nativeElement;
+ expect(textField.scrollTop).toBe(0);
+ textField.value = `A long comment that wraps to multiple rows.
+ There should be no scroll bar, the textarea field has to grow with the given input. So scrolltop has to stay 0.`;
+ textField.dispatchEvent(new Event("input"));
+ fixture.detectChanges();
+ expect(textField.scrollTop).toBe(0);
+ expect(childComponent.resize).toHaveBeenCalled();
+ });
+
+ it("should reset value on save and delete", () => {
+ const textField = fixture.debugElement.query(By.css(".comments--newcomment--textfield")).nativeElement;
+ textField.value = `Example value`;
+ childComponent.onSave();
+ expect(textField.value).toEqual("");
+ textField.value = `Example value`;
+ childComponent.clear();
+ expect(textField.value).toEqual("");
+ });
+
+ it("should set hasInputSomething on input", () => {
+ const textField = fixture.debugElement.query(By.css(".comments--newcomment--textfield")).nativeElement;
+ expect(childComponent.hasInputSomething).toBe(false);
+ textField.value = "Example value";
+ textField.dispatchEvent(new Event("input"));
+ expect(childComponent.hasInputSomething).toBe(true);
+ textField.value = "";
+ textField.dispatchEvent(new Event("input"));
+ expect(childComponent.hasInputSomething).toBe(false);
+ });
+
+ it("should only show save button when there was a text input", () => {
+ let button = fixture.debugElement.query(By.css(".openk-button.openk-success"));
+ expect(button).toBeFalsy();
+ childComponent.hasInputSomething = true;
+ fixture.detectChanges();
+ button = fixture.debugElement.query(By.css(".openk-button.openk-success"));
+ expect(button).toBeTruthy();
+ });
+
+ it("should show commentsToShow amount of comments", () => {
+ childComponent.appComments = new Array(22).fill(1).map((el, index) => ({
+ id: index,
+ text: "test text",
+ userName: "User1",
+ firstName: "Franz",
+ lastName: "Meier",
+ timestamp: "2007-08-31T16:47+00:00",
+ editable: true
+ }));
+ childComponent.appCommentsToShow = 5;
+ fixture.detectChanges();
+ let comments = fixture.debugElement.queryAll(By.css(".comments--list--comment"));
+ expect(comments.length).toEqual(5);
+ childComponent.showMore();
+ fixture.detectChanges();
+ comments = fixture.debugElement.queryAll(By.css(".comments--list--comment"));
+ expect(comments.length).toEqual(10);
+ childComponent.showMore(true);
+ fixture.detectChanges();
+ comments = fixture.debugElement.queryAll(By.css(".comments--list--comment"));
+ expect(comments.length).toEqual(22);
+ });
+});
diff --git a/src/app/features/forms/comments/components/comments-control/comments-control.component.stories.ts b/src/app/features/forms/comments/components/comments-control/comments-control.component.stories.ts
new file mode 100644
index 0000000..03b5c21
--- /dev/null
+++ b/src/app/features/forms/comments/components/comments-control/comments-control.component.stories.ts
@@ -0,0 +1,171 @@
+/********************************************************************************
+ * 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 {RouterTestingModule} from "@angular/router/testing";
+import {moduleMetadata, storiesOf} from "@storybook/angular";
+import {IAPICommentModel} from "../../../../../core/api/statements";
+import {I18nModule} from "../../../../../core/i18n";
+import {CommentsFormModule} from "../../comments-form.module";
+import {CommentsControlComponent} from "./comments-control.component";
+
+const comments: IAPICommentModel[] = [
+ {
+ id: 0,
+ text: "Ein kurzer Kommentar.",
+ userName: "User1",
+ firstName: "Franz",
+ lastName: "Meier",
+ timestamp: "2015-08-31T16:47+00:00",
+ editable: true
+ },
+ {
+ id: 1,
+ text: "Ein längerer Kommentar. Ein weiterer Satz. Ein weiterer Satz. Ein weiterer Satz. Ein weiterer Satz. Ein weiterer Satz.",
+ userName: "User1",
+ firstName: "Franz",
+ lastName: "Meier",
+ timestamp: "2015-08-31T18:13+00:00",
+ editable: true
+ },
+ {
+ id: 2,
+ text: "Ein Kommentar von einem anderen Nutzer mit einem Zeilenumbruch. \nWeitere Zeile.",
+ userName: "User2",
+ firstName: "Peter",
+ lastName: "Fox",
+ timestamp: "2015-09-01T06:17+00:00",
+ editable: false
+ },
+ {
+ id: 3,
+ text: "Ein weiterer Kommentar von einem anderen Nutzer.",
+ userName: "User2",
+ firstName: "Peter",
+ lastName: "Fox",
+ timestamp: "2015-09-01T11:17+00:00",
+ editable: false
+ },
+ {
+ id: 4,
+ text: "a",
+ userName: "User1",
+ firstName: "Franz",
+ lastName: "Meier",
+ timestamp: "2015-09-01T11:17+00:00",
+ editable: true
+ },
+ {
+ id: 5,
+ text: `Extrem langer Kommentar. Das ist ein Satz. Das ist ein Satz. Das ist ein Satz.
+ Das ist ein Satz. Das ist ein Satz. Das ist ein Satz. Das ist ein Satz. Das ist ein Satz.
+ Das ist ein Satz. Das ist ein Satz. Das ist ein Satz. Das ist ein Satz. Das ist ein Satz.
+ Das ist ein Satz. Das ist ein Satz. Das ist ein Satz. Das ist ein Satz. Das ist ein Satz.
+ Das ist ein Satz. Das ist ein Satz.`,
+ userName: "User1",
+ firstName: "Franz",
+ lastName: "Meier",
+ timestamp: "2015-09-01T11:17+00:00",
+ editable: true
+ },
+ {
+ id: 6,
+ text: "test text",
+ userName: "User1",
+ firstName: "Franz",
+ lastName: "Meier",
+ timestamp: "2007-08-31T16:47+00:00",
+ editable: true
+ },
+ {
+ id: 7,
+ text: "test text",
+ userName: "User1",
+ firstName: "Franz",
+ lastName: "Meier",
+ timestamp: "2007-08-31T16:47+00:00",
+ editable: true
+ },
+ {
+ id: 8,
+ text: "test text",
+ userName: "User1",
+ firstName: "Franz",
+ lastName: "Meier",
+ timestamp: "2007-08-31T16:47+00:00",
+ editable: true
+ },
+ {
+ id: 9,
+ text: "test text",
+ userName: "User1",
+ firstName: "Franz",
+ lastName: "Meier",
+ timestamp: "2007-08-31T16:47+00:00",
+ editable: true
+ },
+ {
+ id: 10,
+ text: "test text",
+ userName: "User1",
+ firstName: "Franz",
+ lastName: "Meier",
+ timestamp: "2007-08-31T16:47+00:00",
+ editable: true
+ }
+].map((comment, id) => ({...comment, text: id + " " + comment.text}));
+
+const addComment = (text: string) => {
+ comments.push(
+ {
+ id: comments.length,
+ text: comments.length + " " + text,
+ userName: "test01",
+ firstName: "Vorname",
+ lastName: "Nachname",
+ timestamp: new Date().toString(),
+ editable: true
+ }
+ );
+};
+
+const deleteComment = (id: number) => {
+ comments.splice(id, 1);
+ for (let i = id; i < comments.length; i++) {
+ comments[i].id--;
+ }
+};
+
+storiesOf("Features / Forms", module)
+ .addDecorator(moduleMetadata({
+ imports: [
+ I18nModule,
+ RouterTestingModule,
+ CommentsFormModule
+ ]
+ }))
+ .add("CommentsControlComponent", () => ({
+ template: `
+ <app-comments-control style="padding: 1em; box-sizing: border-box"
+ [appComments]="comments"
+ (appAdd)="addComment($event)"
+ (appDelete)="deleteComment($event)">
+ </app-comments-control>
+ `,
+ props: {
+ comments,
+ addComment,
+ deleteComment
+ }
+ }));
+
+
diff --git a/src/app/features/forms/comments/components/comments-control/comments-control.component.ts b/src/app/features/forms/comments/components/comments-control/comments-control.component.ts
new file mode 100644
index 0000000..cc7ac4d
--- /dev/null
+++ b/src/app/features/forms/comments/components/comments-control/comments-control.component.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 {Component, ElementRef, EventEmitter, Input, Output, ViewChild} from "@angular/core";
+import {IAPICommentModel} from "../../../../../core/api/statements";
+import {momentFormatDisplayFullDateAndTime} from "../../../../../util/moment";
+
+@Component({
+ selector: "app-comments-control",
+ templateUrl: "./comments-control.component.html",
+ styleUrls: ["./comments-control.component.scss"]
+})
+export class CommentsControlComponent {
+
+ @Input()
+ public appCollapsed: boolean;
+
+ @Input()
+ public appCommentsToShow = 5;
+
+ @Input()
+ public appComments: Array<IAPICommentModel>;
+
+ public hasInputSomething = false;
+
+ @Output()
+ public appDelete: EventEmitter<number> = new EventEmitter();
+
+ @Output()
+ public appAdd: EventEmitter<string> = new EventEmitter();
+
+ @Input()
+ public timeDisplayFormat: string = momentFormatDisplayFullDateAndTime;
+
+ @Output()
+ public appCommentsToShowChange = new EventEmitter<number>();
+
+ @ViewChild("textAreaElement")
+ private textAreaRef: ElementRef<HTMLTextAreaElement>;
+
+ public onInput() {
+ this.hasInputSomething = this.textAreaRef.nativeElement.value !== "";
+ this.resize();
+ }
+
+ public resize() {
+ this.textAreaRef.nativeElement.style.height = "1px";
+ this.textAreaRef.nativeElement.style.height = this.textAreaRef.nativeElement.scrollHeight + "px";
+ }
+
+ public onSave() {
+ this.appAdd.emit(this.textAreaRef.nativeElement.value);
+ this.clear();
+ }
+
+ public onDelete(id: number) {
+ this.appDelete.emit(id);
+ }
+
+ public clear() {
+ this.textAreaRef.nativeElement.value = "";
+ this.hasInputSomething = false;
+ this.resize();
+ }
+
+ public showMore(all = false) {
+ if (this.appCommentsToShow == null) {
+ this.appCommentsToShow = 0;
+ }
+ this.appCommentsToShow = all ? undefined : Math.min(this.appComments?.length, this.appCommentsToShow + 5);
+ this.appCommentsToShowChange.emit(this.appCommentsToShow);
+ }
+
+}
diff --git a/src/app/features/forms/comments/components/comments-control/index.ts b/src/app/features/forms/comments/components/comments-control/index.ts
new file mode 100644
index 0000000..f5c60ff
--- /dev/null
+++ b/src/app/features/forms/comments/components/comments-control/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./comments-control.component";
diff --git a/src/app/features/forms/comments/components/comments-form/comments-form.component.html b/src/app/features/forms/comments/components/comments-form/comments-form.component.html
new file mode 100644
index 0000000..32127e3
--- /dev/null
+++ b/src/app/features/forms/comments/components/comments-form/comments-form.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
+ -------------------------------------------------------------------------------->
+
+<app-collapsible (appCollapsedChange)="$event ? appCommentsToShow = 5 : null"
+ *ngIf="(statementId$ | async) != null"
+ [appCollapsed]="appCollapsed"
+ [appTitle]="('comments.title' | translate) + ' (' + (numberOfComments$ | async) +')'">
+
+ <app-comments-control
+ (appAdd)="addComment($event)"
+ (appDelete)="deleteComment($event)"
+ [(appCommentsToShow)]="appCommentsToShow"
+ [appComments]="comments$ | async">
+
+ </app-comments-control>
+
+</app-collapsible>
diff --git a/src/app/features/forms/comments/components/comments-form/comments-form.component.scss b/src/app/features/forms/comments/components/comments-form/comments-form.component.scss
new file mode 100644
index 0000000..de05421
--- /dev/null
+++ b/src/app/features/forms/comments/components/comments-form/comments-form.component.scss
@@ -0,0 +1,17 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+:host {
+ display: block;
+ width: 100%;
+}
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
new file mode 100644
index 0000000..07b491b
--- /dev/null
+++ b/src/app/features/forms/comments/components/comments-form/comments-form.component.spec.ts
@@ -0,0 +1,82 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {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 {CommentsFormModule} from "../../comments-form.module";
+import {CommentsFormComponent} from "./comments-form.component";
+
+describe("CommentsFormComponent", () => {
+ let store: Store;
+ let component: CommentsFormComponent;
+ let fixture: ComponentFixture<CommentsFormComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CommentsFormModule,
+ I18nModule
+ ],
+ providers: [
+ provideMockStore({
+ initialState: {},
+ selectors: [
+ {
+ selector: queryParamsIdSelector,
+ value: 19
+ },
+ {
+ selector: statementCommentsSelector,
+ value: [{id: 1919} as any]
+ }
+ ]
+ })
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CommentsFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ store = fixture.componentRef.injector.get(Store);
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should dispatch add comment actions", async () => {
+ const dispatchSpy = spyOn(store, "dispatch");
+ await component.addComment("Comment");
+ expect(dispatchSpy).toHaveBeenCalledWith(addCommentAction({statementId: 19, text: "Comment"}));
+ });
+
+ it("should dispatch delete comment actions", async () => {
+ const dispatchSpy = spyOn(store, "dispatch");
+ await component.deleteComment(1919);
+ expect(dispatchSpy).toHaveBeenCalledWith(deleteCommentAction({statementId: 19, commentId: 1919}));
+ });
+
+ it("should observe the correct number of comments", async () => {
+ const numberOfComments = await component.numberOfComments$.pipe(take(1)).toPromise();
+ expect(numberOfComments).toBe(1);
+ });
+
+});
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
new file mode 100644
index 0000000..a5a380c
--- /dev/null
+++ b/src/app/features/forms/comments/components/comments-form/comments-form.component.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 {Component, Input} from "@angular/core";
+import {select, Store} from "@ngrx/store";
+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";
+
+@Component({
+ selector: "app-comments-form",
+ templateUrl: "./comments-form.component.html",
+ styleUrls: ["./comments-form.component.scss"]
+})
+export class CommentsFormComponent {
+
+ @Input()
+ public appCollapsed: boolean;
+
+ @Input()
+ public appCommentsToShow = 5;
+
+ public statementId$ = this.store.pipe(select(queryParamsIdSelector));
+
+ public comments$ = this.store.pipe(select(statementCommentsSelector));
+
+ public numberOfComments$ = defer(() => this.comments$).pipe(
+ map((comments) => arrayJoin(comments).length)
+ );
+
+ public constructor(public store: Store) {
+
+ }
+
+ public async addComment(text: string) {
+ const statementId = await this.statementId$.pipe(take(1)).toPromise();
+ this.store.dispatch(addCommentAction({statementId, text}));
+ }
+
+ public async deleteComment(commentId: number) {
+ const statementId = await this.statementId$.pipe(take(1)).toPromise();
+ this.store.dispatch(deleteCommentAction({statementId, commentId}));
+ }
+
+}
diff --git a/src/app/features/forms/comments/components/comments-form/index.ts b/src/app/features/forms/comments/components/comments-form/index.ts
new file mode 100644
index 0000000..aa1982d
--- /dev/null
+++ b/src/app/features/forms/comments/components/comments-form/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./comments-form.component";
diff --git a/src/app/features/forms/comments/components/index.ts b/src/app/features/forms/comments/components/index.ts
new file mode 100644
index 0000000..dc439a4
--- /dev/null
+++ b/src/app/features/forms/comments/components/index.ts
@@ -0,0 +1,15 @@
+/********************************************************************************
+ * 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 "./comments-control";
+export * from "./comments-form";
diff --git a/src/app/features/forms/comments/index.ts b/src/app/features/forms/comments/index.ts
new file mode 100644
index 0000000..5759b1e
--- /dev/null
+++ b/src/app/features/forms/comments/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./comments-form.module";
diff --git a/src/app/features/forms/statement-information/components/general-information-form-group/general-information-form-group.component.html b/src/app/features/forms/statement-information/components/general-information-form-group/general-information-form-group.component.html
new file mode 100644
index 0000000..c7d6b4a
--- /dev/null
+++ b/src/app/features/forms/statement-information/components/general-information-form-group/general-information-form-group.component.html
@@ -0,0 +1,127 @@
+<!-------------------------------------------------------------------------------
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ -------------------------------------------------------------------------------->
+
+<div [formGroup]="appFormGroup" class="form-group-container form-group-container---fill">
+
+ <div class="form-group-container--label">
+ <label [for]="appId + '-title'">
+ {{"statementInformationForm.controls.title" | translate}}
+ </label>
+ </div>
+
+ <input [formControlName]="'title'"
+ [id]="appId + '-title'"
+ appFormControlStatus
+ autocomplete="off"
+ class="openk-input openk-info form-group-container--input"
+ required>
+
+ <div class="form-group-container--label">
+ <label [for]="appId + '-city'">
+ {{"statementInformationForm.controls.city" | translate}}
+ </label>
+ </div>
+
+ <input [formControlName]="'city'"
+ [id]="appId + '-city'"
+ appFormControlStatus
+ autocomplete="off"
+ class="openk-input openk-info form-group-container--input"
+ required>
+
+ <div class="form-group-container--label">
+ <label [for]="appId + '-district'">
+ {{"statementInformationForm.controls.district" | translate}}
+ </label>
+ </div>
+
+ <input [formControlName]="'district'"
+ [id]="appId + '-district'"
+ appFormControlStatus
+ autocomplete="off"
+ class="openk-input openk-info form-group-container--input"
+ required>
+
+ <div class="form-group-container"></div>
+
+ <div class="form-group-container form-group-container--sectors">
+ <ng-container
+ *ngIf="(appSectors | sector: appFormGroup.value) != null">
+ <span class="form-group-container--sectors--label">
+ {{("shared.sectors.available" | translate)}}
+ </span>
+ <span class="form-group-container--sectors--label---italic">
+ {{(appSectors | sector: appFormGroup.value)}}
+ </span>
+ </ng-container>
+ <span *ngIf="(appSectors | sector: appFormGroup.value ) == null"
+ class="form-group-container--sectors--label">
+ {{"shared.sectors.none" | translate}}
+ </span>
+ </div>
+
+
+</div>
+
+<div [formGroup]="appFormGroup" class="form-group-container">
+
+ <div class="form-group-container--label">
+ <label [for]="appId + '-type'">
+ {{"statementInformationForm.controls.typeId" | translate}}
+ </label>
+ </div>
+
+ <div>
+ <app-select #selectComponent
+ [appDisabled]="appStatementTypeOptions?.length == null || appStatementTypeOptions.length === 0"
+ [appId]="appId + '-type'"
+ [appOptions]="appStatementTypeOptions"
+ [appPlaceholder]="selectComponent.appDisabled ? 'Keine Daten verfügbar...' : ''"
+ [formControlName]="'typeId'"
+ appFormControlStatus
+ class="openk-info form-group-container--select"
+ required>
+ </app-select>
+ </div>
+
+ <div class="form-group-container--label">
+ <label [for]="appId + '-receipt-date'">
+ {{"statementInformationForm.controls.receiptDate" | translate}}
+ </label>
+ </div>
+
+ <div>
+ <app-date-control [appId]="appId + '-receipt-date'"
+ [formControlName]="'receiptDate'"
+ appFormControlStatus
+ class="openk-info"
+ required>
+ </app-date-control>
+ </div>
+
+ <div class="form-group-container--label">
+ <label [for]="appId + '-due-date'">
+ {{"statementInformationForm.controls.dueDate" | translate}}
+ </label>
+ </div>
+
+ <div>
+ <app-date-control [appId]="appId + '-due-date'"
+ [formControlName]="'dueDate'"
+ appFormControlStatus
+ class="openk-info"
+ required>
+ </app-date-control>
+ </div>
+
+</div>
diff --git a/src/app/features/forms/statement-information/components/general-information-form-group/general-information-form-group.component.scss b/src/app/features/forms/statement-information/components/general-information-form-group/general-information-form-group.component.scss
new file mode 100644
index 0000000..e869db7
--- /dev/null
+++ b/src/app/features/forms/statement-information/components/general-information-form-group/general-information-form-group.component.scss
@@ -0,0 +1,63 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+@import "openk.styles";
+
+:host {
+ width: 100%;
+ padding: 1em 0.5em 0.5em 0.5em;
+ box-sizing: border-box;
+ display: flex;
+ flex-flow: row wrap;
+}
+
+.form-group-container {
+ flex: 0 1 29em;
+ display: grid;
+ box-sizing: border-box;
+ padding: 0 0.5em 0.5em 0.5em;
+ grid-template-columns: max-content auto;
+ grid-gap: 0.5em;
+ margin-bottom: auto;
+}
+
+.form-group-container--sectors {
+ display: flex;
+ flex-flow: row nowrap;
+ overflow-x: hidden;
+ font-size: small;
+ column-gap: 0.25em;
+ margin-top: -0.25em;
+}
+
+.form-group-container--sectors--label {
+ flex: 1 1 100%;
+ max-width: fit-content;
+}
+
+.form-group-container--sectors--label---italic {
+ font-style: italic;
+}
+
+.form-group-container---fill {
+ flex: 10 1 25em;
+}
+
+.form-group-container--label {
+ display: flex;
+ align-items: center;
+}
+
+.form-group-container--select {
+ width: 100%;
+}
diff --git a/src/app/features/forms/statement-information/components/general-information-form-group/general-information-form-group.component.spec.ts b/src/app/features/forms/statement-information/components/general-information-form-group/general-information-form-group.component.spec.ts
new file mode 100644
index 0000000..6a9a85a
--- /dev/null
+++ b/src/app/features/forms/statement-information/components/general-information-form-group/general-information-form-group.component.spec.ts
@@ -0,0 +1,41 @@
+/********************************************************************************
+ * 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 {StatementInformationFormModule} from "../../statement-information-form.module";
+import {GeneralInformationFormGroupComponent} from "./general-information-form-group.component";
+
+describe("GeneralInformationFormGroupComponent", () => {
+ let component: GeneralInformationFormGroupComponent;
+ let fixture: ComponentFixture<GeneralInformationFormGroupComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ StatementInformationFormModule,
+ I18nModule
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(GeneralInformationFormGroupComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/features/forms/statement-information/components/general-information-form-group/general-information-form-group.component.ts b/src/app/features/forms/statement-information/components/general-information-form-group/general-information-form-group.component.ts
new file mode 100644
index 0000000..2f9e4f9
--- /dev/null
+++ b/src/app/features/forms/statement-information/components/general-information-form-group/general-information-form-group.component.ts
@@ -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 {Component, Input} from "@angular/core";
+import {FormControl, FormGroup} from "@angular/forms";
+import {IAPISectorsModel} from "../../../../../core/api/statements/IAPISectorsModel";
+import {ISelectOption} from "../../../../../shared/controls/select/model";
+import {IStatementInformationFormValue} from "../../../../../store/statements/model";
+import {createFormGroup} from "../../../../../util/forms";
+
+@Component({
+ selector: "app-general-information-form-group",
+ templateUrl: "./general-information-form-group.component.html",
+ styleUrls: ["./general-information-form-group.component.scss"]
+})
+export class GeneralInformationFormGroupComponent {
+
+ private static id = 0;
+
+ public appId = `GeneralInfoFormGroupComponent${GeneralInformationFormGroupComponent.id++}`;
+
+ @Input()
+ public appStatementTypeOptions: ISelectOption[] = [];
+
+ @Input()
+ public appFormGroup: FormGroup = createFormGroup<Partial<IStatementInformationFormValue>>({
+ title: new FormControl(),
+ dueDate: new FormControl(),
+ receiptDate: new FormControl(),
+ typeId: new FormControl(),
+ city: new FormControl(),
+ district: new FormControl()
+ });
+
+ @Input()
+ public appSectors: IAPISectorsModel = {};
+
+}
diff --git a/src/app/features/forms/statement-information/components/general-information-form-group/index.ts b/src/app/features/forms/statement-information/components/general-information-form-group/index.ts
new file mode 100644
index 0000000..4c1b28c
--- /dev/null
+++ b/src/app/features/forms/statement-information/components/general-information-form-group/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./general-information-form-group.component";
diff --git a/src/app/features/forms/statement-information/components/index.ts b/src/app/features/forms/statement-information/components/index.ts
new file mode 100644
index 0000000..ab97d26
--- /dev/null
+++ b/src/app/features/forms/statement-information/components/index.ts
@@ -0,0 +1,15 @@
+/********************************************************************************
+ * 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 "./general-information-form-group";
+export * from "./statement-information-form";
diff --git a/src/app/features/forms/statement-information/components/statement-information-form/index.ts b/src/app/features/forms/statement-information/components/statement-information-form/index.ts
new file mode 100644
index 0000000..c2f449b
--- /dev/null
+++ b/src/app/features/forms/statement-information/components/statement-information-form/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+export * from "./statement-information-form.component";
diff --git a/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.html b/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.html
new file mode 100644
index 0000000..830c525
--- /dev/null
+++ b/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.html
@@ -0,0 +1,86 @@
+<!-------------------------------------------------------------------------------
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ -------------------------------------------------------------------------------->
+
+<div [formGroup]="appFormGroup" class="info-form">
+
+ <app-collapsible
+ [appCollapsed]="false"
+ [appTitle]="'statementInformationForm.container.general' | translate">
+
+ <app-general-information-form-group
+ [appSectors]="sectors$ | async"
+ [appFormGroup]="appFormGroup"
+ [appStatementTypeOptions]="statementTypeOptions$ | async">
+ </app-general-information-form-group>
+
+ </app-collapsible>
+
+ <app-collapsible
+ [appCollapsed]="false"
+ [appTitle]="'statementInformationForm.container.contact' | translate">
+
+ <app-contact-select
+ (appOpenContactModule)="openContactDataBaseModule()"
+ (appPageChange)="changePage($event)"
+ (appSearchChange)="search($event)"
+ [appDetails]="selectedContact$ | async"
+ [appEntries]="contactSearchContent$ | async"
+ [appIsLoading]="(contactLoading$ | async)?.searching"
+ [appMessage]="'contacts.selectContact' | translate"
+ [appPageSize]="(contactSearch$ | async)?.totalPages"
+ [appPage]="(contactSearch$ | async)?.number"
+ [formControlName]="'contactId'"
+ class="form-control">
+ </app-contact-select>
+
+ </app-collapsible>
+
+ <app-collapsible
+ [appCollapsed]="false"
+ [appTitle]="'statementInformationForm.container.inboxAttachments' | translate">
+
+ <app-attachments-form-group
+ [appFormGroup]="appFormGroup">
+ </app-attachments-form-group>
+
+ </app-collapsible>
+
+ <ng-content></ng-content>
+
+</div>
+
+<div class="form-actions">
+
+ <button (click)="submit(false)"
+ [disabled]="appFormGroup.disabled"
+ class="openk-button openk-danger form-actions--button">
+ <mat-icon>redo</mat-icon>
+ {{ "statementInformationForm.submitAndReject" | translate}}
+ </button>
+
+ <button (click)="submit()"
+ *ngIf="!appForNewStatement"
+ [disabled]="appFormGroup.disabled"
+ class="openk-button openk-info form-actions--button">
+ <mat-icon>redo</mat-icon>
+ {{ "statementInformationForm.submit" | translate}}
+ </button>
+
+ <button (click)="submit(true)"
+ [disabled]="appFormGroup.disabled"
+ class="openk-button openk-success form-actions--button">
+ <mat-icon>redo</mat-icon>
+ {{ "statementInformationForm.submitAndComplete" | translate}}
+ </button>
+
+</div>
diff --git a/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.scss b/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.scss
new file mode 100644
index 0000000..25dec60
--- /dev/null
+++ b/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.scss
@@ -0,0 +1,74 @@
+/********************************************************************************
+ * 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%;
+ max-width: 70em;
+ margin: 0 auto;
+}
+
+.info-form {
+ width: 100%;
+
+ & > * {
+ margin-bottom: 1em;
+ }
+}
+
+.form-control {
+ padding: 1em;
+ box-sizing: border-box;
+}
+
+.form-actions {
+ margin-top: 1em;
+ display: flex;
+ width: 100%;
+ justify-content: flex-end;
+ align-items: flex-start;
+}
+
+.form-actions--button {
+ margin-left: 1em;
+ min-width: 14.5em;
+ display: flex;
+
+ &:first-child {
+ margin: 0;
+ }
+}
+
+.attachments {
+ display: flex;
+ flex-flow: row wrap;
+ min-height: 15em;
+ padding: 0.5em;
+ box-sizing: border-box;
+ max-height: 25em;
+ overflow: auto;
+}
+
+.attachments--container {
+ flex: 1 1 calc(50% - 2em);
+ display: flex;
+ flex-flow: column;
+ margin: 0.5em;
+}
+
+.attachments--container--control {
+ flex: 1 1 100%;
+ margin: 0.25em 0 0.5em 0;
+}
diff --git a/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.spec.ts b/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.spec.ts
new file mode 100644
index 0000000..2b6d7b0
--- /dev/null
+++ b/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.spec.ts
@@ -0,0 +1,311 @@
+/********************************************************************************
+ * 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 {EventEmitter} from "@angular/core";
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {MockStore, provideMockStore} from "@ngrx/store/testing";
+import {I18nModule, IAPIProcessTask, IAPISearchOptions} from "../../../../../core";
+import {
+ fetchSettingsAction,
+ getStatementLoadingSelector,
+ IStatementInformationFormValue,
+ openContactDataBaseAction,
+ statementInformationFormValueSelector,
+ statementTypesSelector,
+ submitStatementInformationFormAction
+} from "../../../../../store";
+import {fetchContactDetailsAction, startContactSearchAction} from "../../../../../store/contacts/actions";
+import {taskSelector} from "../../../../../store/process/selectors";
+import {createSelectOptionsMock} from "../../../../../test";
+import {StatementInformationFormModule} from "../../statement-information-form.module";
+import {StatementInformationFormComponent} from "./statement-information-form.component";
+
+describe("StatementInformationFormComponent", () => {
+ const today = new Date().toISOString().slice(0, 10);
+ let component: StatementInformationFormComponent;
+ let fixture: ComponentFixture<StatementInformationFormComponent>;
+ let mockStore: MockStore;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ I18nModule,
+ StatementInformationFormModule
+ ],
+ providers: [
+ provideMockStore({
+ initialState: {statements: {}, settings: {}, contacts: {}, attachments: {}},
+ selectors: [
+ {
+ selector: statementTypesSelector,
+ value: createSelectOptionsMock(5, "Statement Type")
+ }
+ ]
+ })
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StatementInformationFormComponent);
+ mockStore = TestBed.inject(MockStore);
+ component = fixture.componentInstance;
+ });
+
+ it("should initialize form for existing statements", async () => {
+ const statementInformationFormValueSelectorMock = mockStore.overrideSelector(statementInformationFormValueSelector, {});
+ const value: IStatementInformationFormValue = {
+ title: "Title",
+ dueDate: null,
+ receiptDate: null,
+ typeId: 19,
+ city: null,
+ district: null,
+ contactId: null,
+ addAttachments: [],
+ removeAttachments: []
+ };
+ expect(component).toBeDefined();
+ fixture.detectChanges();
+ await fixture.whenStable();
+ statementInformationFormValueSelectorMock.setResult({...value});
+ mockStore.refreshState();
+ await fixture.whenStable();
+ expect(component.appFormGroup.touched).toBeTrue();
+ expect(component.getValue()).toEqual(value);
+ });
+
+ it("should initialize for new statements", async () => {
+ const statementInformationFormValueSelectorMock = mockStore.overrideSelector(statementInformationFormValueSelector, {});
+ const value: IStatementInformationFormValue = {
+ title: null,
+ dueDate: today,
+ receiptDate: today,
+ typeId: 0,
+ city: null,
+ district: null,
+ contactId: null,
+ addAttachments: [],
+ removeAttachments: []
+ };
+ component.appForNewStatement = true;
+ expect(component).toBeDefined();
+ fixture.detectChanges();
+ await fixture.whenStable();
+ statementInformationFormValueSelectorMock.setResult({title: "Title"});
+ mockStore.refreshState();
+ await fixture.whenStable();
+ expect(component.appFormGroup.untouched).toBeTrue();
+ expect(component.getValue()).toEqual(value);
+ });
+
+ it("should initialize for new statements without statement types", async () => {
+ const statementInformationFormValueSelectorMock = mockStore.overrideSelector(statementInformationFormValueSelector, {});
+ mockStore.overrideSelector(statementTypesSelector, null);
+ const value: IStatementInformationFormValue = {
+ title: null,
+ dueDate: today,
+ receiptDate: today,
+ typeId: undefined,
+ city: null,
+ district: null,
+ contactId: null,
+ addAttachments: [],
+ removeAttachments: []
+ };
+ component.appForNewStatement = true;
+ expect(component).toBeDefined();
+ fixture.detectChanges();
+ await fixture.whenStable();
+ statementInformationFormValueSelectorMock.setResult({title: "Title"});
+ mockStore.refreshState();
+ await fixture.whenStable();
+ expect(component.appFormGroup.untouched).toBeTrue();
+ expect(component.getValue()).toEqual(value);
+ });
+
+ it("should disable form when loading", async () => {
+ const getStatementLoadingSelectorMock = mockStore.overrideSelector(getStatementLoadingSelector, {});
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ getStatementLoadingSelectorMock.setResult({});
+ mockStore.refreshState();
+
+ expect(component.appFormGroup.enabled).toBeTrue();
+
+ getStatementLoadingSelectorMock.setResult({submittingStatementInformation: true});
+ mockStore.refreshState();
+
+ expect(component.appFormGroup.enabled).toBeFalse();
+
+ getStatementLoadingSelectorMock.setResult({submittingStatementInformation: false});
+ mockStore.refreshState();
+
+ expect(component.appFormGroup.enabled).toBeTrue();
+ });
+
+ it("should fetch settings for new statements", async () => {
+ const dispatchSpy = spyOn(component.store, "dispatch");
+ component.appForNewStatement = true;
+ fixture.detectChanges();
+ expect(dispatchSpy).toHaveBeenCalledWith(fetchSettingsAction());
+ });
+
+ it("should fetch contact details on value changes", async () => {
+ const dispatchSpy = spyOn(component.store, "dispatch");
+ const formValueChangesMock = new EventEmitter<any>();
+ (component.appFormGroup as any).valueChanges = formValueChangesMock;
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const value: Partial<IStatementInformationFormValue> = {
+ contactId: "19191919"
+ };
+
+ component.appFormGroup.patchValue(value);
+ formValueChangesMock.next(value);
+
+ expect(dispatchSpy).toHaveBeenCalledWith(fetchContactDetailsAction({contactId: value.contactId}));
+ });
+
+ it("should open contact data base module", async () => {
+ const dispatchSpy = spyOn(component.store, "dispatch");
+ component.openContactDataBaseModule();
+ expect(dispatchSpy).toHaveBeenCalledWith(openContactDataBaseAction());
+ });
+
+ it("should search for new contacts", async () => {
+ const dispatchSpy = spyOn(component.store, "dispatch");
+ const options: IAPISearchOptions = {
+ q: "",
+ page: 0,
+ size: 10
+ };
+
+ component.search();
+ expect(dispatchSpy).toHaveBeenCalledWith(startContactSearchAction({options}));
+
+ options.q = "191919";
+ component.searchText = options.q;
+ component.changePage(null);
+ expect(dispatchSpy).toHaveBeenCalledWith(startContactSearchAction({options}));
+
+ options.page = 19;
+ component.changePage(options.page);
+ expect(dispatchSpy).toHaveBeenCalledWith(startContactSearchAction({options}));
+ });
+
+ it("should mark all as touched when invalid form is submitted", async () => {
+ component.appForNewStatement = true;
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const dispatchSpy = spyOn(component.store, "dispatch");
+
+ expect(component.appFormGroup.touched).toBeFalse();
+ expect(component.appFormGroup.invalid).toBeTrue();
+ component.submit();
+ expect(component.appFormGroup.touched).toBeTrue();
+ expect(dispatchSpy).not.toHaveBeenCalled();
+ });
+
+ it("should submit information for a new statement", async () => {
+ const value: IStatementInformationFormValue = {
+ title: "Title",
+ dueDate: today,
+ receiptDate: today,
+ typeId: 3,
+ city: "city",
+ district: "district",
+ contactId: "contactId",
+ addAttachments: [],
+ removeAttachments: []
+ };
+
+ component.appForNewStatement = true;
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ component.appFormGroup.patchValue(value);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const dispatchSpy = spyOn(component.store, "dispatch");
+
+ expect(component.getValue()).toEqual(value);
+
+ await component.submit(true);
+ expect(dispatchSpy).toHaveBeenCalledWith(submitStatementInformationFormAction({
+ new: true,
+ value,
+ responsible: true
+ }));
+
+ await component.submit(false);
+ expect(dispatchSpy).toHaveBeenCalledWith(submitStatementInformationFormAction({
+ new: true,
+ value,
+ responsible: false
+ }));
+ });
+
+ it("should submit information for an existing statement", async () => {
+ const task: Partial<IAPIProcessTask> = {
+ taskId: "19191919",
+ statementId: 19
+ };
+ const value: IStatementInformationFormValue = {
+ title: "Title",
+ dueDate: today,
+ receiptDate: today,
+ typeId: 3,
+ city: "city",
+ district: "district",
+ contactId: "contactId",
+ addAttachments: [],
+ removeAttachments: []
+ };
+
+ mockStore.overrideSelector(taskSelector, task as IAPIProcessTask);
+
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ component.appFormGroup.patchValue(value);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const dispatchSpy = spyOn(component.store, "dispatch");
+
+ expect(component.getValue()).toEqual(value);
+
+ await component.submit(true);
+ expect(dispatchSpy).toHaveBeenCalledWith(submitStatementInformationFormAction({
+ statementId: task.statementId,
+ taskId: task.taskId,
+ value,
+ responsible: true
+ }));
+
+ await component.submit(false);
+ expect(dispatchSpy).toHaveBeenCalledWith(submitStatementInformationFormAction({
+ statementId: task.statementId,
+ taskId: task.taskId,
+ value,
+ responsible: false
+ }));
+ });
+
+
+});
diff --git a/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.stories.ts b/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.stories.ts
new file mode 100644
index 0000000..6e218f6
--- /dev/null
+++ b/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.stories.ts
@@ -0,0 +1,60 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
+import {provideMockStore} from "@ngrx/store/testing";
+import {action} from "@storybook/addon-actions";
+import {boolean, withKnobs} from "@storybook/addon-knobs";
+import {moduleMetadata, storiesOf} from "@storybook/angular";
+import {I18nModule} from "../../../../../core/i18n";
+import {statementInformationFormValueSelector, statementTypesSelector} from "../../../../../store";
+import {createSelectOptionsMock} from "../../../../../test/create-select-options.spec";
+import {StatementInformationFormModule} from "../../statement-information-form.module";
+
+storiesOf("Features / Forms", module)
+ .addDecorator(withKnobs)
+ .addDecorator(moduleMetadata({
+ imports: [
+ I18nModule,
+ BrowserAnimationsModule,
+ StatementInformationFormModule
+ ],
+ providers: [
+ provideMockStore({
+ selectors: [
+ {
+ selector: statementTypesSelector,
+ value: createSelectOptionsMock(5, "Statement Type")
+ },
+ {
+ selector: statementInformationFormValueSelector,
+ value: {}
+ }
+ ]
+ }),
+
+ ]
+ }))
+ .add("StatementInformationFormComponent", () => ({
+ template: `
+ <app-statement-information-form
+ (appValueChange)="appValueChange($event)"
+ [appForNewStatement]="appForNewStatement"
+ style="padding: 1em; box-sizing: border-box;">
+ </app-statement-information-form>
+ `,
+ props: {
+ appValueChange: action("appValueChange"),
+ appForNewStatement: boolean("appForNewStatement", false),
+ }
+ }));
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
new file mode 100644
index 0000000..104317f
--- /dev/null
+++ b/src/app/features/forms/statement-information/components/statement-information-form/statement-information-form.component.ts
@@ -0,0 +1,167 @@
+/********************************************************************************
+ * 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, OnInit} from "@angular/core";
+import {FormControl, Validators} from "@angular/forms";
+import {select, Store} from "@ngrx/store";
+import {concat, defer, of} from "rxjs";
+import {distinctUntilChanged, filter, map, switchMap, take, takeUntil} from "rxjs/operators";
+import {IAPISearchOptions} from "../../../../../core";
+import {
+ fetchContactDetailsAction,
+ fetchSettingsAction,
+ getContactDetailsSelector,
+ getContactLoadingSelector,
+ getContactSearchContentSelector,
+ getContactSearchSelector,
+ getStatementLoadingSelector,
+ getStatementSectorsSelector,
+ IStatementInformationFormValue,
+ openContactDataBaseAction,
+ startContactSearchAction,
+ statementInformationFormValueSelector,
+ statementTypesSelector,
+ submitStatementInformationFormAction,
+ taskSelector
+} from "../../../../../store";
+import {arrayJoin, createFormGroup} from "../../../../../util";
+import {AbstractReactiveFormComponent} from "../../../abstract";
+
+@Component({
+ selector: "app-statement-information-form",
+ templateUrl: "./statement-information-form.component.html",
+ styleUrls: ["./statement-information-form.component.scss"]
+})
+export class StatementInformationFormComponent extends AbstractReactiveFormComponent<IStatementInformationFormValue> implements OnInit {
+
+ @Input()
+ public appForNewStatement: boolean;
+
+ public statementLoading$ = this.store.pipe(select(getStatementLoadingSelector));
+
+ public task$ = this.store.pipe(select(taskSelector));
+
+ public statementTypeOptions$ = this.store.pipe(select(statementTypesSelector));
+
+ public contactSearch$ = this.store.pipe(select(getContactSearchSelector));
+
+ public contactSearchContent$ = this.store.pipe(select(getContactSearchContentSelector));
+
+ public contactLoading$ = this.store.pipe(select(getContactLoadingSelector));
+
+ public sectors$ = this.store.pipe(select(getStatementSectorsSelector));
+
+ public selectedContact$ = defer(() => this.selectedContactId$).pipe(
+ switchMap((id) => this.store.pipe(select(getContactDetailsSelector, {id})))
+ );
+
+ public searchText: string;
+
+ public appFormGroup = createFormGroup<IStatementInformationFormValue>({
+ title: new FormControl(undefined, [Validators.required]),
+ dueDate: new FormControl(undefined, [Validators.required]),
+ receiptDate: new FormControl(undefined, [Validators.required]),
+ typeId: new FormControl(undefined, [Validators.required]),
+ city: new FormControl(undefined, [Validators.required]),
+ district: new FormControl(undefined, [Validators.required]),
+ contactId: new FormControl(undefined, [Validators.required]),
+ addAttachments: new FormControl([]),
+ removeAttachments: new FormControl([])
+ });
+
+ public selectedContactId$ = defer(() => concat(of(null), this.appFormGroup.valueChanges)).pipe(
+ map(() => this.getValue().contactId)
+ );
+
+ private value$ = this.store.pipe(select(statementInformationFormValueSelector));
+
+ private searchSize = 10;
+
+ public constructor(public store: Store) {
+ super();
+ }
+
+ public ngOnInit() {
+ if (this.appForNewStatement) {
+ this.setInitialValue();
+ this.store.dispatch(fetchSettingsAction());
+ } else {
+ this.appFormGroup.markAllAsTouched();
+ }
+
+ this.updateForm();
+ this.fetchContactDetails();
+ this.search("");
+ }
+
+ public openContactDataBaseModule() {
+ this.store.dispatch(openContactDataBaseAction());
+ }
+
+ public search(searchText?: string) {
+ this.searchText = searchText;
+ this.changePage(0);
+ }
+
+ public changePage(page: number) {
+ const options: IAPISearchOptions = {
+ q: this.searchText == null ? "" : this.searchText,
+ page: page == null ? 0 : page,
+ size: this.searchSize
+ };
+ this.store.dispatch(startContactSearchAction({options}));
+ }
+
+ public async submit(responsible?: boolean) {
+ if (this.appFormGroup.invalid) {
+ this.appFormGroup.markAllAsTouched();
+ return;
+ }
+
+ if (this.appForNewStatement) {
+ this.store.dispatch(submitStatementInformationFormAction({
+ new: true,
+ value: this.getValue(),
+ responsible
+ }));
+ } else {
+ const task = await this.task$.pipe(take(1)).toPromise();
+ this.store.dispatch(submitStatementInformationFormAction({
+ statementId: task.statementId,
+ taskId: task.taskId,
+ value: this.getValue(),
+ responsible
+ }));
+ }
+ }
+
+ private async setInitialValue() {
+ const statementTypeOptions = await this.statementTypeOptions$.pipe(take(1)).toPromise();
+ const typeId = arrayJoin(statementTypeOptions)[0]?.value;
+ const today = new Date().toISOString().slice(0, 10);
+ this.patchValue({typeId, dueDate: today, receiptDate: today});
+ }
+
+ private fetchContactDetails() {
+ this.selectedContactId$.pipe(distinctUntilChanged(), takeUntil(this.destroy$))
+ .subscribe((contactId) => this.store.dispatch(fetchContactDetailsAction({contactId})));
+ }
+
+ private updateForm() {
+ this.value$.pipe(takeUntil(this.destroy$), filter(() => !this.appForNewStatement))
+ .subscribe((value) => this.patchValue(value));
+ this.statementLoading$.pipe(takeUntil(this.destroy$))
+ .subscribe((loading) => loading?.submittingStatementInformation ? this.appFormGroup.disable() : this.appFormGroup.enable());
+ }
+
+}
diff --git a/src/app/features/forms/statement-information/index.ts b/src/app/features/forms/statement-information/index.ts
new file mode 100644
index 0000000..2703a6e
--- /dev/null
+++ b/src/app/features/forms/statement-information/index.ts
@@ -0,0 +1,15 @@
+/********************************************************************************
+ * 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 "./components/statement-information-form/statement-information-form.component";
+export * from "./statement-information-form.module";
diff --git a/src/app/features/forms/statement-information/pipes/sector.pipe.spec.ts b/src/app/features/forms/statement-information/pipes/sector.pipe.spec.ts
new file mode 100644
index 0000000..e53135a
--- /dev/null
+++ b/src/app/features/forms/statement-information/pipes/sector.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 {IAPISectorsModel} from "../../../../core/api/statements/IAPISectorsModel";
+import {SectorPipe} from "./sector.pipe";
+
+describe("SectorPipe", () => {
+
+ const pipe = new SectorPipe();
+
+ describe("transform", () => {
+
+ it("should return the sector information for given city and district", () => {
+ const sectors: IAPISectorsModel = {
+ "Ort#Ortsteil": [
+ "Strom", "Gas", "Beleuchtung"
+ ]
+ };
+
+ let result = pipe.transform(sectors, {city: "", district: ""});
+ expect(result).toEqual(undefined);
+ expect(result).toBeFalsy();
+
+ result = pipe.transform(sectors, null);
+ expect(result).toEqual(undefined);
+ expect(result).toBeFalsy();
+
+ result = pipe.transform(sectors, {city: "Stadt", district: "Straße"});
+ expect(result).toEqual(undefined);
+ expect(result).toBeFalsy();
+
+ result = pipe.transform(sectors, {city: "Ort", district: "Ortsteil"});
+ expect(result).toEqual(" Strom, Gas, Beleuchtung");
+ });
+ });
+});
+
diff --git a/src/app/features/forms/statement-information/pipes/sector.pipe.ts b/src/app/features/forms/statement-information/pipes/sector.pipe.ts
new file mode 100644
index 0000000..b0d72f9
--- /dev/null
+++ b/src/app/features/forms/statement-information/pipes/sector.pipe.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 {Pipe, PipeTransform} from "@angular/core";
+import {IAPISectorsModel} from "../../../../core/api/statements/IAPISectorsModel";
+import {IStatementInformationFormValue} from "../../../../store/statements/model";
+
+@Pipe({
+ name: "sector"
+})
+export class SectorPipe implements PipeTransform {
+ transform(sectors: IAPISectorsModel, args?: Partial<IStatementInformationFormValue>): any {
+
+ if (sectors && args?.city && args.district) {
+ return sectors[args.city + "#" + args.district]?.map((_) => " " + _).toString();
+ } else {
+ return undefined;
+ }
+ }
+}
diff --git a/src/app/features/forms/statement-information/statement-information-form.module.ts b/src/app/features/forms/statement-information/statement-information-form.module.ts
new file mode 100644
index 0000000..7832e5f
--- /dev/null
+++ b/src/app/features/forms/statement-information/statement-information-form.module.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 {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 {ContactSelectModule} from "../../../shared/controls/contact-select/contact-select.module";
+import {DateControlModule} from "../../../shared/controls/date-control";
+import {FileDropModule} from "../../../shared/controls/file-drop";
+import {FileSelectModule} from "../../../shared/controls/file-select";
+import {SelectModule} from "../../../shared/controls/select";
+import {CollapsibleModule} from "../../../shared/layout/collapsible";
+import {AttachmentsFormModule} from "../attachments";
+import {CommentsFormModule} from "../comments";
+import {GeneralInformationFormGroupComponent, StatementInformationFormComponent} from "./components";
+import {SectorPipe} from "./pipes/sector.pipe";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ ReactiveFormsModule,
+ MatIconModule,
+ TranslateModule,
+
+ CommentsFormModule,
+ CollapsibleModule,
+ SelectModule,
+ DateControlModule,
+ FileDropModule,
+ CommonControlsModule,
+ ContactSelectModule,
+ FileSelectModule,
+ AttachmentsFormModule
+ ],
+ declarations: [
+ StatementInformationFormComponent,
+ GeneralInformationFormGroupComponent,
+ SectorPipe
+ ],
+ exports: [
+ StatementInformationFormComponent,
+ GeneralInformationFormGroupComponent,
+ SectorPipe
+ ]
+})
+export class StatementInformationFormModule {
+
+}
diff --git a/src/app/features/forms/workflow-data/components/index.ts b/src/app/features/forms/workflow-data/components/index.ts
new file mode 100644
index 0000000..9f0f79a
--- /dev/null
+++ b/src/app/features/forms/workflow-data/components/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./workflow-data-form.component";
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
new file mode 100644
index 0000000..12a5cdf
--- /dev/null
+++ b/src/app/features/forms/workflow-data/components/workflow-data-form.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
+ -------------------------------------------------------------------------------->
+
+<ng-container [formGroup]="appFormGroup">
+
+ <app-collapsible
+ [appCollapsed]="true"
+ [appTitle]="'workflowDataForm.container.general' | translate">
+ </app-collapsible>
+
+ <app-collapsible
+ [appCollapsed]="true"
+ [appTitle]="'workflowDataForm.container.inboxAttachments' | translate">
+ </app-collapsible>
+
+ <app-collapsible
+ [appCollapsed]="true"
+ [appTitle]="'workflowDataForm.container.geographicPosition' | translate">
+ </app-collapsible>
+
+ <app-collapsible
+ [appTitle]="'workflowDataForm.container.departments' | translate">
+
+ <app-select-group
+ [appGroups]="departmentGroups$ | async"
+ [appOptions]="departmentOptions$ | async"
+ [formControlName]="'departments'"
+ class="departments">
+
+ </app-select-group>
+
+ </app-collapsible>
+
+ <app-collapsible
+ [appCollapsed]="true"
+ [appTitle]="('workflowDataForm.container.linkedIssues' | translate) + ' (' + appFormGroup.value.parentIds?.length + ')'">
+
+ <app-statement-select
+ (appSearch)="search($event)"
+ [appIsLoading]="(isStatementLoading$ | async)?.search"
+ [appSearchContent]="searchContent$ | async"
+ [appStatementTypeOptions]="statementTypes$ | async"
+ [formControlName]="'parentIds'"
+ class="parents">
+
+ </app-statement-select>
+ </app-collapsible>
+
+ <ng-content>
+ </ng-content>
+
+ <div class="form-actions">
+
+ <button (click)="submit(false)"
+ [disabled]="appFormGroup.disabled"
+ class="openk-button openk-info form-actions--button"
+ type="button">
+ <mat-icon>redo</mat-icon>
+ {{'workflowDataForm.submit' | translate}}
+ </button>
+
+ <button (click)="submit(true)"
+ [disabled]="appFormGroup.disabled"
+ class="openk-button openk-success form-actions--button"
+ type="button">
+ <mat-icon>redo</mat-icon>
+ {{'workflowDataForm.submitAndComplete' | translate}}
+ </button>
+ </div>
+
+</ng-container>
diff --git a/src/app/features/forms/workflow-data/components/workflow-data-form.component.scss b/src/app/features/forms/workflow-data/components/workflow-data-form.component.scss
new file mode 100644
index 0000000..2d75d77
--- /dev/null
+++ b/src/app/features/forms/workflow-data/components/workflow-data-form.component.scss
@@ -0,0 +1,63 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+@import "../../../../../styles/openk.styles";
+
+:host {
+ display: block;
+ width: 100%;
+
+ & > * {
+ margin-bottom: 1em;
+ }
+}
+
+.workflow-form {
+ width: 100%;
+
+ & > * {
+ margin-bottom: 1em;
+ }
+}
+
+.geographic-position {
+ box-sizing: border-box;
+ height: 3em;
+}
+
+.departments {
+ padding: 1em;
+}
+
+.parents {
+ padding: 1em;
+}
+
+
+.form-actions {
+ margin-top: 1em;
+ display: flex;
+ width: 100%;
+ justify-content: flex-end;
+ align-items: flex-start;
+}
+
+.form-actions--button {
+ margin-left: 1em;
+ min-width: 14.5em;
+ display: flex;
+
+ &:first-child {
+ margin: 0;
+ }
+}
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
new file mode 100644
index 0000000..02d4158
--- /dev/null
+++ b/src/app/features/forms/workflow-data/components/workflow-data-form.component.spec.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 {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {FormGroup} from "@angular/forms";
+import {RouterTestingModule} from "@angular/router/testing";
+import {MockStore, provideMockStore} from "@ngrx/store/testing";
+import {IAPISearchOptions} from "../../../../core/api/shared";
+import {I18nModule} from "../../../../core/i18n";
+import {taskSelector} from "../../../../store/process/selectors";
+import {startStatementSearchAction, submitWorkflowDataFormAction} from "../../../../store/statements/actions";
+import {IWorkflowFormValue} from "../../../../store/statements/model";
+import {WorkflowDataFormModule} from "../workflow-data-form.module";
+import {WorkflowDataFormComponent} from "./workflow-data-form.component";
+
+describe("WorkflowDataFormComponent", () => {
+
+ const initialState = {
+ statements: {},
+ process: {},
+ settings: {}
+ };
+
+ let mockStore: MockStore;
+ let component: WorkflowDataFormComponent;
+ let fixture: ComponentFixture<WorkflowDataFormComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ WorkflowDataFormModule,
+ I18nModule,
+ RouterTestingModule
+ ],
+ providers: [
+ provideMockStore({initialState})
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(WorkflowDataFormComponent);
+ mockStore = fixture.componentRef.injector.get(MockStore);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeDefined();
+ });
+
+ it("should dispatch submit workflow form action", async () => {
+ mockStore.overrideSelector(taskSelector, {statementId: 1, taskId: "19"} as any);
+ const dispatchSpy = spyOn(mockStore, "dispatch");
+ const value: IWorkflowFormValue = {
+ geographicPosition: "1919",
+ departments: [],
+ parentIds: [19, 199]
+ };
+ const formMock = {value} as FormGroup;
+ const action = submitWorkflowDataFormAction({
+ statementId: 1,
+ taskId: "19",
+ data: value,
+ completeTask: true
+ });
+
+ component.appFormGroup = formMock;
+
+ await component.submit(true);
+ expect(dispatchSpy).toHaveBeenCalledWith(action);
+
+ action.completeTask = false;
+ await component.submit(false);
+ expect(dispatchSpy).toHaveBeenCalledWith(action);
+ });
+
+ it("should dispatch search statements action", () => {
+ const dispatchSpy = spyOn(mockStore, "dispatch");
+ const options: IAPISearchOptions = {q: ""};
+ component.search(options);
+ expect(dispatchSpy).toHaveBeenCalledWith(startStatementSearchAction({options}));
+ });
+
+});
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
new file mode 100644
index 0000000..b976857
--- /dev/null
+++ b/src/app/features/forms/workflow-data/components/workflow-data-form.component.ts
@@ -0,0 +1,89 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {Component, OnInit} from "@angular/core";
+import {FormControl} from "@angular/forms";
+import {select, Store} from "@ngrx/store";
+import {take, takeUntil} from "rxjs/operators";
+import {IAPISearchOptions} from "../../../../core/api";
+import {
+ departmentGroupsSelector,
+ departmentOptionsSelector,
+ getSearchContentStatementsSelector,
+ getStatementLoadingSelector,
+ IWorkflowFormValue,
+ startStatementSearchAction,
+ statementTypesSelector,
+ submitWorkflowDataFormAction,
+ taskSelector,
+ workflowFormValueSelector
+} from "../../../../store";
+import {createFormGroup} from "../../../../util";
+import {AbstractReactiveFormComponent} from "../../abstract";
+
+@Component({
+ selector: "app-workflow-data-form",
+ templateUrl: "./workflow-data-form.component.html",
+ styleUrls: ["./workflow-data-form.component.scss"]
+})
+export class WorkflowDataFormComponent extends AbstractReactiveFormComponent<IWorkflowFormValue> implements OnInit {
+
+ public task$ = this.store.pipe(select(taskSelector));
+
+ public statementTypes$ = this.store.pipe(select(statementTypesSelector));
+
+ public searchContent$ = this.store.pipe(select(getSearchContentStatementsSelector));
+
+ public departmentOptions$ = this.store.pipe(select(departmentOptionsSelector));
+
+ public departmentGroups$ = this.store.pipe(select(departmentGroupsSelector));
+
+ public isStatementLoading$ = this.store.pipe(select(getStatementLoadingSelector));
+
+ public appFormGroup = createFormGroup<IWorkflowFormValue>({
+ departments: new FormControl(),
+ geographicPosition: new FormControl(),
+ parentIds: new FormControl()
+ });
+
+ private form$ = this.store.pipe(select(workflowFormValueSelector));
+
+ public constructor(public store: Store) {
+ super();
+ }
+
+ public ngOnInit() {
+ this.patchValue({geographicPosition: "", departments: [], parentIds: []});
+ this.form$.pipe(takeUntil(this.destroy$))
+ .subscribe((value) => this.patchValue(value));
+ this.task$.pipe(takeUntil(this.destroy$))
+ .subscribe(() => {
+ this.search({q: ""});
+ });
+ }
+
+ public async submit(completeTask?: boolean) {
+ const task = await this.task$.pipe(take(1)).toPromise();
+ this.store.dispatch(submitWorkflowDataFormAction({
+ statementId: task.statementId,
+ taskId: task.taskId,
+ data: this.getValue(),
+ completeTask
+ }));
+ }
+
+ public search(options: IAPISearchOptions) {
+ this.store.dispatch(startStatementSearchAction({options}));
+ }
+
+}
diff --git a/src/app/features/forms/workflow-data/index.ts b/src/app/features/forms/workflow-data/index.ts
new file mode 100644
index 0000000..908b226
--- /dev/null
+++ b/src/app/features/forms/workflow-data/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./components/workflow-data-form.component";
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
new file mode 100644
index 0000000..b8d5c76
--- /dev/null
+++ b/src/app/features/forms/workflow-data/workflow-data-form.module.ts
@@ -0,0 +1,43 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+import {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 {SelectModule} from "../../../shared/controls/select";
+import {StatementSelectModule} from "../../../shared/controls/statement-select";
+import {CollapsibleModule} from "../../../shared/layout/collapsible";
+import {WorkflowDataFormComponent} from "./components";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ ReactiveFormsModule,
+ MatIconModule,
+ TranslateModule,
+
+ CollapsibleModule,
+ SelectModule,
+ StatementSelectModule
+ ],
+ declarations: [
+ WorkflowDataFormComponent
+ ],
+ exports: [
+ WorkflowDataFormComponent
+ ]
+})
+export class WorkflowDataFormModule {
+
+}
diff --git a/src/app/features/navigation/components/nav-frame/nav-frame.component.scss b/src/app/features/navigation/components/nav-frame/nav-frame.component.scss
index 04ad58d..b7ad9c5 100644
--- a/src/app/features/navigation/components/nav-frame/nav-frame.component.scss
+++ b/src/app/features/navigation/components/nav-frame/nav-frame.component.scss
@@ -24,7 +24,8 @@
width: 100%;
display: flex;
flex-flow: column;
- overflow: auto;
+ overflow-x: auto;
+ overflow-y: scroll;
}
.nav-frame-content-main {
diff --git a/src/app/features/new/components/new-statement/new-statement.component.html b/src/app/features/new/components/new-statement/new-statement.component.html
index 1771b32..7ce4251 100644
--- a/src/app/features/new/components/new-statement/new-statement.component.html
+++ b/src/app/features/new/components/new-statement/new-statement.component.html
@@ -13,14 +13,9 @@
<app-page-header
[appActions]="pageHeaderActions"
- [appTitle]="'core.title'">
+ [appTitle]="'statementInformationForm.titleNew'">
</app-page-header>
-<app-new-statement-form
- (appSubmit)="submit($event)"
- [appDisabled]="isLoading$ | async"
- [appError]="error$ | async"
- [appIsLoading]="isLoading$ | async"
- [appTypeOptions]="typeOptions$ | async"
- [appValue]="form">
-</app-new-statement-form>
+<app-statement-information-form
+ [appForNewStatement]="true">
+</app-statement-information-form>
diff --git a/src/app/features/new/components/new-statement/new-statement.component.scss b/src/app/features/new/components/new-statement/new-statement.component.scss
index af1b9db..db88091 100644
--- a/src/app/features/new/components/new-statement/new-statement.component.scss
+++ b/src/app/features/new/components/new-statement/new-statement.component.scss
@@ -18,4 +18,10 @@
flex-flow: column;
padding: 1em;
align-items: center;
+
+ & > *:not(:last-child) {
+ margin-bottom: 1em;
+ }
}
+
+
diff --git a/src/app/features/new/components/new-statement/new-statement.component.spec.ts b/src/app/features/new/components/new-statement/new-statement.component.spec.ts
index 7bb0d52..ff2dd26 100644
--- a/src/app/features/new/components/new-statement/new-statement.component.spec.ts
+++ b/src/app/features/new/components/new-statement/new-statement.component.spec.ts
@@ -13,98 +13,41 @@
import {ComponentFixture, TestBed} from "@angular/core/testing";
import {RouterTestingModule} from "@angular/router/testing";
-import {MemoizedSelector} from "@ngrx/store";
-import {MockStore, provideMockStore} from "@ngrx/store/testing";
+import {provideMockStore} from "@ngrx/store/testing";
import {I18nModule} from "../../../../core";
-import {
- IStatementInfoForm,
- IStatementInfoFormValue,
- newStatementFormErrorSelector,
- newStatementFormLoadingSelector,
- newStatementFormValueSelector,
- submitNewStatementAction
-} from "../../../../store";
-import {NewStatementModule} from "../../new-statement.module";
+import {PageHeaderModule} from "../../../../shared/layout/page-header";
+import {StatementInformationFormModule} from "../../../forms/statement-information";
import {NewStatementComponent} from "./new-statement.component";
describe("NewStatementComponent", () => {
+
let component: NewStatementComponent;
let fixture: ComponentFixture<NewStatementComponent>;
- let store: MockStore;
- let mockIsLoadingSelector: MemoizedSelector<IStatementInfoForm, boolean>;
- let mockErrorSelector: MemoizedSelector<IStatementInfoForm, string>;
- let mockFormSelector: MemoizedSelector<IStatementInfoForm, IStatementInfoFormValue>;
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [RouterTestingModule, NewStatementModule, I18nModule],
- providers: [provideMockStore({})]
+ declarations: [
+ NewStatementComponent
+ ],
+ imports: [
+ I18nModule,
+ RouterTestingModule,
+ PageHeaderModule,
+ StatementInformationFormModule
+ ],
+ providers: [
+ provideMockStore({})
+ ]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(NewStatementComponent);
component = fixture.componentInstance;
- fixture.detectChanges();
- store = TestBed.inject(MockStore);
- mockIsLoadingSelector = store.overrideSelector(
- newStatementFormLoadingSelector,
- false
- );
- mockErrorSelector = store.overrideSelector(
- newStatementFormErrorSelector,
- undefined
- );
- mockFormSelector = store.overrideSelector(
- newStatementFormValueSelector,
- newStatementFormValue
- );
});
it("should create", () => {
expect(component).toBeTruthy();
});
- it("should dispatch action on submit", () => {
- const expectedAction = submitNewStatementAction({value: {...newStatementFormValue}});
- spyOn(store, "dispatch");
- component.submit(newStatementFormValue);
- expect(store.dispatch).toHaveBeenCalledWith(expectedAction);
- });
-
- it("should show an empty form if there is no user data", async () => {
- mockIsLoadingSelector.setResult(false);
- mockErrorSelector.setResult(undefined);
- mockFormSelector.setResult(newStatementFormValue);
- fixture.detectChanges();
- await component.ngOnInit();
- expect(component.form).toEqual(undefined);
- });
-
- it("should show the saved form data if appIsLoading is true", async () => {
- mockIsLoadingSelector.setResult(true);
- mockErrorSelector.setResult(undefined);
- mockFormSelector.setResult(newStatementFormValue);
- fixture.detectChanges();
- await component.ngOnInit();
- expect(component.form).toEqual(newStatementFormValue);
- });
-
- it("should show the saved form data if there was an error", async () => {
- mockIsLoadingSelector.setResult(false);
- mockErrorSelector.setResult("some error");
- mockFormSelector.setResult(newStatementFormValue);
- fixture.detectChanges();
- await component.ngOnInit();
- expect(component.form).toEqual(newStatementFormValue);
- });
-
- const newStatementFormValue: IStatementInfoFormValue = {
- title: "title",
- receiptDate: "21-05-2020",
- dueDate: "21-05-2020",
- typeId: 2,
- city: "city",
- district: "district"
- };
});
diff --git a/src/app/features/new/components/new-statement/new-statement.component.ts b/src/app/features/new/components/new-statement/new-statement.component.ts
index 3f6c636..a1ae176 100644
--- a/src/app/features/new/components/new-statement/new-statement.component.ts
+++ b/src/app/features/new/components/new-statement/new-statement.component.ts
@@ -11,25 +11,15 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {Component, OnInit} from "@angular/core";
-import {select, Store} from "@ngrx/store";
-import {take} from "rxjs/operators";
+import {Component} from "@angular/core";
import {IPageHeaderAction} from "../../../../shared/layout/page-header";
-import {
- IStatementInfoFormValue,
- newStatementFormErrorSelector,
- newStatementFormLoadingSelector,
- newStatementFormValueSelector,
- statementTypesSelector,
- submitNewStatementAction
-} from "../../../../store";
@Component({
selector: "app-new-statement",
templateUrl: "./new-statement.component.html",
styleUrls: ["./new-statement.component.scss"]
})
-export class NewStatementComponent implements OnInit {
+export class NewStatementComponent {
public readonly pageHeaderActions: IPageHeaderAction[] = [
{
@@ -39,35 +29,4 @@
}
];
- public typeOptions$ = this.store.pipe(select(statementTypesSelector));
-
- public value$ = this.store.pipe(select(newStatementFormValueSelector));
-
- public isLoading$ = this.store.pipe(select(newStatementFormLoadingSelector));
-
- public error$ = this.store.pipe(select(newStatementFormErrorSelector));
-
- public form: IStatementInfoFormValue;
-
- public constructor(private readonly store: Store) {
-
- }
-
- /**
- * Shows the saved form-data if there was an error or the form was previously submitted (isLoading)
- * so the input data is still present and doesn't have to be repeated.
- */
- public async ngOnInit() {
- const isLoading = await this.isLoading$.pipe(take(1)).toPromise();
- const error = await this.error$.pipe(take(1)).toPromise();
-
- if (isLoading || error != null) {
- this.form = await this.value$.pipe(take(1)).toPromise();
- }
- }
-
- public submit(value: IStatementInfoFormValue) {
- this.store.dispatch(submitNewStatementAction({value}));
- }
-
}
diff --git a/src/app/features/new/new-statement.module.ts b/src/app/features/new/new-statement.module.ts
index 6703252..5013826 100644
--- a/src/app/features/new/new-statement.module.ts
+++ b/src/app/features/new/new-statement.module.ts
@@ -11,48 +11,23 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {A11yModule} from "@angular/cdk/a11y";
-import {CommonModule} from "@angular/common";
import {NgModule} from "@angular/core";
-import {FormsModule} from "@angular/forms";
-import {MatIconModule} from "@angular/material/icon";
-import {TranslateModule} from "@ngx-translate/core";
-import {CalendarModule} from "primeng/calendar";
-import {DropdownModule} from "primeng/dropdown";
-import {DateControlModule} from "../../shared/controls/date-control";
-import {FileDropModule} from "../../shared/controls/file-drop";
-import {SelectModule} from "../../shared/controls/select";
-import {CardModule} from "../../shared/layout/card";
import {PageHeaderModule} from "../../shared/layout/page-header";
-import {ProgressSpinnerModule} from "../../shared/progress-spinner";
+import {StatementInformationFormModule} from "../forms/statement-information";
import {NewStatementComponent} from "./components";
-import {NewStatementFormComponent} from "./components/new-statement-form/new-statement-form.component";
import {NewStatementRoutingModule} from "./new-statement-routing.module";
@NgModule({
imports: [
- CommonModule,
- FormsModule,
NewStatementRoutingModule,
- MatIconModule,
- CardModule,
- CalendarModule,
- DateControlModule,
- A11yModule,
PageHeaderModule,
- TranslateModule,
- DropdownModule,
- FileDropModule,
- ProgressSpinnerModule,
- SelectModule
+ StatementInformationFormModule
],
declarations: [
- NewStatementComponent,
- NewStatementFormComponent
+ NewStatementComponent
],
exports: [
- NewStatementComponent,
- NewStatementFormComponent
+ NewStatementComponent
]
})
export class NewStatementModule {
diff --git a/src/app/shared/controls/common/abstract/abstract-control-value-accessor.component.spec.ts b/src/app/shared/controls/common/abstract/abstract-control-value-accessor.component.spec.ts
new file mode 100644
index 0000000..0c0c3f6
--- /dev/null
+++ b/src/app/shared/controls/common/abstract/abstract-control-value-accessor.component.spec.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 {AbstractControlValueAccessorComponent} from "./abstract-control-value-accessor.component";
+
+class ControlValueAccessorSpec extends AbstractControlValueAccessorComponent<number> {
+
+}
+
+describe("ControlValueAccessor", () => {
+
+ let component: ControlValueAccessorSpec;
+
+ beforeEach((() => {
+ component = new ControlValueAccessorSpec();
+ }));
+
+ it("#.writeValue should change appValue", () => {
+ const onChangeSpy = spyOn(component, "onChange");
+ const onTouchSpy = spyOn(component, "onTouch");
+ component.appValue = 0;
+ component.writeValue(19);
+ expect(component.appValue).toBe(19);
+ expect(onChangeSpy).not.toHaveBeenCalled();
+ expect(onTouchSpy).not.toHaveBeenCalled();
+
+ component.writeValue(190, true);
+ expect(component.appValue).toBe(190);
+ expect(onChangeSpy).toHaveBeenCalledWith(190);
+ expect(onTouchSpy).toHaveBeenCalled();
+ });
+
+ it("#.setDisable should set appDisabled", () => {
+ expect(component.appDisabled).not.toBeDefined();
+ component.setDisabledState(true);
+ expect(component.appDisabled).toBe(true);
+ component.setDisabledState(true);
+ expect(component.appDisabled).toBe(true);
+ });
+
+ it("should register listeners", () => {
+ // These calls should be ignored (because null is not a function)
+ component.registerOnChange(null);
+ component.registerOnTouched(null);
+ expect(component.onChange).not.toThrow();
+ expect(component.onTouch).not.toThrow();
+
+ const fn = () => 19;
+ component.registerOnChange(fn);
+ component.registerOnTouched(fn);
+ expect(component.onChange).toBe(fn);
+ expect(component.onTouch).toBe(fn);
+ });
+
+});
diff --git a/src/app/shared/controls/common/abstract/abstract-control-value-accessor.component.ts b/src/app/shared/controls/common/abstract/abstract-control-value-accessor.component.ts
new file mode 100644
index 0000000..a679d25
--- /dev/null
+++ b/src/app/shared/controls/common/abstract/abstract-control-value-accessor.component.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 {EventEmitter, Input, Output} from "@angular/core";
+import {ControlValueAccessor} from "@angular/forms";
+
+export abstract class AbstractControlValueAccessorComponent<T> implements ControlValueAccessor {
+
+ @Input()
+ public appDisabled: boolean;
+
+ @Input()
+ public appValue: T;
+
+ @Output()
+ public appValueChange: EventEmitter<T> = new EventEmitter<T>();
+
+ public onChange = (_: T) => null;
+
+ public onTouch = () => null;
+
+ /**
+ * Sets a new value to the control.
+ * @param obj New value for the control
+ * @param emit If true, the new value will be emitted via the appValueChange event emitter.
+ */
+ public writeValue(obj: T, emit?: boolean) {
+ this.appValue = obj;
+ if (emit) {
+ this.appValueChange.emit(this.appValue);
+ this.onChange(this.appValue);
+ this.onTouch();
+ }
+ }
+
+ public registerOnChange(fn: any): void {
+ this.onChange = typeof fn === "function" ? fn : this.onChange;
+ }
+
+ public registerOnTouched(fn: any): void {
+ this.onTouch = typeof fn === "function" ? fn : this.onTouch;
+ }
+
+ public setDisabledState(isDisabled: boolean) {
+ this.appDisabled = isDisabled;
+ }
+
+}
diff --git a/src/app/shared/controls/common/common-controls.module.ts b/src/app/shared/controls/common/common-controls.module.ts
new file mode 100644
index 0000000..6fa5e1f
--- /dev/null
+++ b/src/app/shared/controls/common/common-controls.module.ts
@@ -0,0 +1,26 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+import {NgModule} from "@angular/core";
+import {FormControlStatusDirective} from "./directives";
+
+@NgModule({
+ declarations: [
+ FormControlStatusDirective
+ ],
+ exports: [
+ FormControlStatusDirective
+ ]
+})
+export class CommonControlsModule {
+
+}
diff --git a/src/app/shared/controls/common/directives/form-control-status.directive.ts b/src/app/shared/controls/common/directives/form-control-status.directive.ts
new file mode 100644
index 0000000..1dc59a9
--- /dev/null
+++ b/src/app/shared/controls/common/directives/form-control-status.directive.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
+ ********************************************************************************/
+
+import {Directive, ElementRef, HostBinding, Optional, Self} from "@angular/core";
+import {NgControl} from "@angular/forms";
+
+@Directive({
+ selector: "[appFormControlStatus]"
+})
+export class FormControlStatusDirective {
+
+ public constructor(
+ @Optional() @Self() public appFormControl: NgControl,
+ public elmentRef: ElementRef<HTMLElement>
+ ) {
+
+ }
+
+ @HostBinding("class.openk-danger")
+ public get classDanger() {
+ return this.appFormControl?.invalid && this.appFormControl?.touched;
+ }
+
+ @HostBinding("class.openk-success")
+ public get classSucecss() {
+ return this.appFormControl?.valid;
+ }
+
+}
diff --git a/src/app/shared/controls/common/directives/index.ts b/src/app/shared/controls/common/directives/index.ts
new file mode 100644
index 0000000..8607d95
--- /dev/null
+++ b/src/app/shared/controls/common/directives/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./form-control-status.directive";
diff --git a/src/app/shared/controls/common/index.ts b/src/app/shared/controls/common/index.ts
index 9926023..79021f5 100644
--- a/src/app/shared/controls/common/index.ts
+++ b/src/app/shared/controls/common/index.ts
@@ -11,4 +11,6 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./abstract-control-value-accessor.component";
+export * from "./abstract/abstract-control-value-accessor.component";
+export * from "./directives";
+export * from "./common-controls.module";
diff --git a/src/app/shared/controls/contact-select/contact-select.component.html b/src/app/shared/controls/contact-select/contact-select.component.html
new file mode 100644
index 0000000..f9bb7d1
--- /dev/null
+++ b/src/app/shared/controls/contact-select/contact-select.component.html
@@ -0,0 +1,72 @@
+<!-------------------------------------------------------------------------------
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ -------------------------------------------------------------------------------->
+
+<div class="contacts--selection">
+
+ <div class="contacts--selection--search">
+ <span class="contacts--selection--search--text">{{"contacts.search" | translate}}</span>
+ <app-searchbar
+ (appSearch)="appSearchChange.emit($event)"
+ [appIsLoading]="appIsLoading"
+ [appPlaceholder]="'contacts.searchContact' | translate"
+ [appSearchText]="appSearch"
+ class="contacts--selection--search--input">
+ </app-searchbar>
+ </div>
+
+ <div class="contacts--selection--list">
+ <div class="contacts--selection--list--box">
+
+ <app-contact-table
+ (appSelectedIdChange)="writeValue($event, true)"
+ [appDisabled]="appDisabled"
+ [appEntries]="appEntries"
+ [appSelectedId]="appValue">
+ </app-contact-table>
+
+ <div class="contacts--selection--list--box--info">
+ <button (click)="appOpenContactModule.emit()"
+ class="openk-button openk-info contacts--selection--list--box--info--button">
+ {{"contacts.addNew" | translate}}
+ </button>
+
+ <app-pagination-counter
+ (appPageChange)="appPageChange.emit($event)"
+ *ngIf="appPageSize >= 2"
+ [appDisabled]="appDisabled || appIsLoading"
+ [appPageSize]="appPageSize"
+ [appPage]="appPage">
+ </app-pagination-counter>
+ </div>
+
+ </div>
+ </div>
+
+</div>
+
+<div class="contacts--details">
+
+ <div *ngIf="appDetails" class="contacts--details--address">
+ <span>{{appDetails?.company}}</span>
+ <span>{{appDetails?.firstName}} {{appDetails?.lastName}}</span>
+ <span>{{appDetails?.street}} {{appDetails?.houseNumber}}</span>
+ <span>{{appDetails?.postCode}} {{appDetails?.community}} {{appDetails?.communitySuffix}}</span>
+ <span>{{appDetails?.email}}</span>
+ </div>
+
+ <div *ngIf="!appDetails" class="contacts--address">
+ <span class="contacts--address--message">{{appMessage}}</span>
+ </div>
+
+</div>
+
diff --git a/src/app/shared/controls/contact-select/contact-select.component.scss b/src/app/shared/controls/contact-select/contact-select.component.scss
new file mode 100644
index 0000000..c44d826
--- /dev/null
+++ b/src/app/shared/controls/contact-select/contact-select.component.scss
@@ -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 "openk.styles";
+
+:host {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+}
+
+.contacts--selection {
+ flex: 1 1 100%;
+}
+
+.contacts--selection--search {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ margin-bottom: 1em;
+}
+
+.contacts--selection--search--text {
+ margin-right: 0.5em;
+}
+
+.contacts--selection--search--input {
+ flex: 1;
+}
+
+.contacts--selection--text {
+ margin-bottom: 0.5em;
+}
+
+.contacts--selection--list {
+ display: flex;
+}
+
+.contacts--selection--list--box {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+}
+
+.contacts--selection--list--box--info {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+.contacts--selection--list--box--info--button {
+ margin-top: 1em;
+}
+
+.contacts--details {
+ display: flex;
+ flex-direction: column;
+ font-size: 16px;
+ padding: 1em;
+ margin-left: 1em;
+ flex: 1 1 25em;
+ justify-content: center;
+}
+
+.contacts--details--address {
+ display: flex;
+ flex-flow: column;
+}
+
+.contacts--address--message {
+ color: get-color($openk-danger-palette, A200);
+ display: block;
+ width: 100%;
+ text-align: center;
+}
diff --git a/src/app/shared/controls/contact-select/contact-select.component.spec.ts b/src/app/shared/controls/contact-select/contact-select.component.spec.ts
new file mode 100644
index 0000000..d8802e2
--- /dev/null
+++ b/src/app/shared/controls/contact-select/contact-select.component.spec.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
+ ********************************************************************************/
+
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {I18nModule} from "../../../core/i18n";
+import {ContactSelectComponent} from "./contact-select.component";
+
+describe("ContactSelectComponent", () => {
+ let component: ContactSelectComponent;
+ let fixture: ComponentFixture<ContactSelectComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ContactSelectComponent],
+ imports: [I18nModule]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ContactSelectComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+});
diff --git a/src/app/shared/controls/contact-select/contact-select.component.ts b/src/app/shared/controls/contact-select/contact-select.component.ts
new file mode 100644
index 0000000..e916aae
--- /dev/null
+++ b/src/app/shared/controls/contact-select/contact-select.component.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 {Component, EventEmitter, forwardRef, Input, Output} from "@angular/core";
+import {NG_VALUE_ACCESSOR} from "@angular/forms";
+import {IAPIContactPerson} from "../../../core/api/contacts/IAPIContactPerson";
+import {IAPIContactPersonDetails} from "../../../core/api/contacts/IAPIContactPersonDetails";
+import {AbstractControlValueAccessorComponent} from "../common";
+
+@Component({
+ selector: "app-contact-select",
+ templateUrl: "./contact-select.component.html",
+ styleUrls: ["./contact-select.component.scss"],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => ContactSelectComponent),
+ multi: true
+ }
+ ]
+})
+export class ContactSelectComponent extends AbstractControlValueAccessorComponent<string> {
+
+ @Input()
+ public appEntries: IAPIContactPerson[];
+
+ @Input()
+ public appDetails: IAPIContactPersonDetails;
+
+ @Input()
+ public appIsLoading: boolean;
+
+ @Input()
+ public appPage: number;
+
+ @Input()
+ public appMessage: string;
+
+ @Output()
+ public appPageChange = new EventEmitter<number>();
+
+ @Input()
+ public appPageSize: number;
+
+ @Input()
+ public appSearch: string;
+
+ @Output()
+ public appSearchChange = new EventEmitter<string>();
+
+ @Output()
+ public appOpenContactModule = new EventEmitter<void>();
+
+}
diff --git a/src/app/shared/controls/contact-select/contact-select.module.ts b/src/app/shared/controls/contact-select/contact-select.module.ts
new file mode 100644
index 0000000..6a3f320
--- /dev/null
+++ b/src/app/shared/controls/contact-select/contact-select.module.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 {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {TranslateModule} from "@ngx-translate/core";
+import {ContactTableModule} from "../../layout/contact-table";
+import {PaginationCounterModule} from "../../layout/pagination-counter";
+
+import {SearchbarModule} from "../../layout/searchbar";
+import {ContactSelectComponent} from "./contact-select.component";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ TranslateModule,
+ PaginationCounterModule,
+ SearchbarModule,
+ ContactTableModule
+ ],
+ declarations: [ContactSelectComponent],
+ exports: [ContactSelectComponent]
+})
+export class ContactSelectModule {
+
+}
diff --git a/src/app/shared/controls/contact-select/contact-select.stories.ts b/src/app/shared/controls/contact-select/contact-select.stories.ts
new file mode 100644
index 0000000..3f4a0fe
--- /dev/null
+++ b/src/app/shared/controls/contact-select/contact-select.stories.ts
@@ -0,0 +1,101 @@
+/********************************************************************************
+ * 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 {number, withKnobs} from "@storybook/addon-knobs";
+import {moduleMetadata, storiesOf} from "@storybook/angular";
+import {timer} from "rxjs";
+import {IAPIContactPerson} from "../../../core/api/contacts/IAPIContactPerson";
+import {IAPIContactPersonDetails} from "../../../core/api/contacts/IAPIContactPersonDetails";
+import {I18nModule} from "../../../core/i18n";
+import {ContactSelectModule} from "./contact-select.module";
+
+const appContacts: IAPIContactPerson[] = new Array(20).fill(0).map(() => (
+ {
+ firstName: "Vorname",
+ lastName: "Nachname",
+ email: "test@email.com",
+ companyName: "Straßenbau Quak GmbH",
+ companyId: "1",
+ id: "1"
+ })
+);
+
+const appDetails: IAPIContactPersonDetails = {
+ community: "Entenhausen",
+ communitySuffix: "",
+ company: "Straßenbau Quak GmbH",
+ email: "dagobert.duck@quak.de",
+ firstName: "Dagobert",
+ houseNumber: "19",
+ lastName: "Duck",
+ postCode: "98765",
+ salutation: "",
+ street: "An der Schnabelweide",
+ title: ""
+};
+
+let detailsToShow = false;
+
+const toggleDetails = (id: number) => {
+ detailsToShow = !!id;
+};
+
+const details: () => IAPIContactPersonDetails = () => {
+ return detailsToShow ? appDetails : null;
+};
+
+let isLoading = false;
+
+let searchText = "";
+
+const search = async (text: string) => {
+ searchText = text;
+ isLoading = true;
+
+ await timer(2000).toPromise();
+ if (searchText === text) {
+ isLoading = false;
+ }
+};
+
+const searchIsLoading: () => boolean = () => isLoading;
+
+storiesOf("Features / 05 Contacts", module)
+ .addDecorator(withKnobs)
+ .addDecorator(moduleMetadata({imports: [ContactSelectModule, I18nModule]}))
+ .add("ContactSelectComponent", () => ({
+ template: `
+ <div style="padding: 1em;">
+ <app-contact-select
+ (appSearchChange)="search($event)"
+ [appIsLoading]="searchIsLoading()"
+ [appPage]="currentPage"
+ [appPageSize]="maxPages"
+ [appEntries]="appContacts.slice(0, numberOfRows)"
+ [appMessage]="'contacts.selectContact' | translate"
+ [appDetails]="details()"
+ (appValueChange)="toggleDetails($event)">
+ </app-contact-select>
+ </div>
+ `,
+ props: {
+ currentPage: number("current page", 1, {min: 1, max: 10}),
+ maxPages: number("max pages", 1, {min: 1, max: 10}),
+ appContacts,
+ numberOfRows: number("number of rows", 11, {min: 0, max: 15}),
+ toggleDetails,
+ details,
+ search,
+ searchIsLoading
+ }
+ }));
diff --git a/src/app/shared/controls/contact-select/index.ts b/src/app/shared/controls/contact-select/index.ts
new file mode 100644
index 0000000..5f0f206
--- /dev/null
+++ b/src/app/shared/controls/contact-select/index.ts
@@ -0,0 +1,12 @@
+/********************************************************************************
+ * 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/shared/controls/date-control/component/date-control.component.ts b/src/app/shared/controls/date-control/component/date-control.component.ts
index 05bff12..461d984 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
@@ -170,14 +170,10 @@
}
const internalValue = parseMomentToString(this.value, this.appInternalFormat, this.appInternalFormat);
-
- if (obj !== internalValue || emit) {
- this.onChange(internalValue);
- }
-
if (emit) {
this.appValueChange.emit(internalValue);
this.onTouch();
+ this.onChange(internalValue);
}
}
diff --git a/src/app/shared/controls/file-drop/component/file-drop.component.scss b/src/app/shared/controls/file-drop/component/file-drop.component.scss
index 1c3ce95..02f7922 100644
--- a/src/app/shared/controls/file-drop/component/file-drop.component.scss
+++ b/src/app/shared/controls/file-drop/component/file-drop.component.scss
@@ -18,13 +18,11 @@
background: get-color($openk-default-palette);
display: inline-flex;
- width: 20em;
border: 1px dashed get-color($openk-default-palette, A700);
flex-flow: column;
align-items: flex-start;
box-sizing: border-box;
- min-height: 5em;
padding: 0.4em;
font-size: 0.875em;
font-weight: 400;
diff --git a/src/app/shared/controls/file-select/component/file-select.component.html b/src/app/shared/controls/file-select/component/file-select.component.html
new file mode 100644
index 0000000..cfb9458
--- /dev/null
+++ b/src/app/shared/controls/file-select/component/file-select.component.html
@@ -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
+ -------------------------------------------------------------------------------->
+
+<div *ngFor="let attachment of appAttachments" [class.attachment---disabled]="appDisabled" class="attachment">
+
+ <input #inputElement (keydown.enter)="inputElement.click()"
+ (ngModelChange)="$event ? select(attachment?.id) : deselect(attachment?.id)"
+ [class.cursor-pointer]="!appDisabled"
+ [disabled]="appDisabled"
+ [id]="appId + '-' + attachment?.id"
+ [ngModel]="isSelected(attachment?.id)"
+ class="attachments--input"
+ type="checkbox">
+
+ <label
+ [class.attachments--label---deselected]="!isSelected(attachment?.id)"
+ [class.cursor-pointer]="!appDisabled"
+ [for]="appId + '-' + attachment?.id"
+ class="attachments--label">
+ {{attachment?.name}}
+ </label>
+
+ <button
+ (click)="appOpenAttachment.emit(attachment?.id)"
+ (pointerdown)="$event.preventDefault();"
+ [disabled]="appDisabled"
+ class="openk-button openk-info openk-button-rounded attachments--button"
+ type="button">
+ <mat-icon>call_made</mat-icon>
+ </button>
+
+</div>
diff --git a/src/app/shared/controls/file-select/component/file-select.component.scss b/src/app/shared/controls/file-select/component/file-select.component.scss
new file mode 100644
index 0000000..ebda584
--- /dev/null
+++ b/src/app/shared/controls/file-select/component/file-select.component.scss
@@ -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 "openk.styles";
+
+:host {
+ display: flex;
+ flex-flow: column;
+ box-sizing: border-box;
+}
+
+.attachment {
+ display: flex;
+ flex-flow: row;
+ width: 100%;
+ align-items: center;
+ line-height: 1.25;
+ padding: 0 0.25em;
+ transition: background-color 100ms ease-in;
+
+ &:hover {
+ background-color: $openk-background-highlight;
+ }
+}
+
+.attachment---disabled {
+ opacity: 0.66;
+
+ &:hover {
+ background-color: initial;
+ }
+}
+
+.attachments--input {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1em;
+ width: 1em;
+ height: 1em;
+}
+
+
+.attachments--label {
+ display: inline-flex;
+ flex: 1 1 100%;
+ padding: 0.05em 0.25em;
+}
+
+.attachments--label---deselected {
+ text-decoration: line-through;
+}
+
+.attachments--button {
+ font-size: 0.56em;
+ border: 0;
+ margin-left: 0.4em;
+}
diff --git a/src/app/shared/controls/file-select/component/file-select.component.spec.ts b/src/app/shared/controls/file-select/component/file-select.component.spec.ts
new file mode 100644
index 0000000..8e1a692
--- /dev/null
+++ b/src/app/shared/controls/file-select/component/file-select.component.spec.ts
@@ -0,0 +1,129 @@
+/********************************************************************************
+ * 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} from "@angular/core";
+import {ComponentFixture, TestBed} from "@angular/core/testing";
+import {By} from "@angular/platform-browser";
+import {IAPIAttachmentModel} from "../../../../core/api";
+import {I18nModule} from "../../../../core/i18n";
+import {FileSelectModule} from "../file-select.module";
+import {FileSelectComponent} from "./file-select.component";
+
+@Component({
+ selector: `app-host-component`,
+ template: `
+ <app-file-select
+ [appAttachments]="attachments"
+ [appValue]="appValue">
+ </app-file-select>
+ `
+})
+class TestHostComponent {
+
+ public attachments: IAPIAttachmentModel[] = [
+ {
+ id: 1,
+ name: "Attachment 1",
+ type: "string",
+ size: 1,
+ timestamp: "string",
+ tagIds: []
+ },
+ {
+ id: 2,
+ name: "Attachment 1",
+ type: "string",
+ size: 1,
+ timestamp: "string",
+ tagIds: []
+ }
+ ];
+
+ public appValue: number[] = [2];
+}
+
+describe("FileSelectComponent", () => {
+ let component: TestHostComponent;
+ let fixture: ComponentFixture<TestHostComponent>;
+ let childComponent: FileSelectComponent;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [
+ TestHostComponent
+ ],
+ imports: [
+ FileSelectModule,
+ I18nModule
+ ]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestHostComponent);
+ component = fixture.componentInstance;
+ childComponent = fixture.debugElement.query(By.directive(FileSelectComponent)).componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should emit appOpenAttachment with the file id of the given file", () => {
+ spyOn(childComponent.appOpenAttachment, "emit").and.callThrough();
+ const button = fixture.debugElement.query(By.css(".openk-button"));
+ button.nativeElement.click();
+ expect(childComponent.appOpenAttachment.emit).toHaveBeenCalledWith(1);
+ });
+
+ it("should return true only if the file id is not in appValue", () => {
+ const isSelected = childComponent.isSelected(childComponent.appAttachments[0].id);
+ expect(isSelected).toBeTrue();
+ const isNotSelected = childComponent.isSelected(childComponent.appAttachments[1].id);
+ expect(isNotSelected).toBeFalse();
+ });
+
+ it("should remove the file id from app value on select", () => {
+ childComponent.appValue = [2, 3];
+ childComponent.select(2);
+ expect(childComponent.appValue).toEqual([3]);
+ childComponent.select(3);
+ expect(childComponent.appValue).toEqual([]);
+ });
+
+ it("should add the file id to app value on deselect", () => {
+ childComponent.appValue = [];
+ childComponent.deselect(2);
+ expect(childComponent.appValue).toEqual([2]);
+ childComponent.deselect(3);
+ expect(childComponent.appValue).toEqual([2, 3]);
+ });
+
+ it("should add the file id to app value on deselect", () => {
+ childComponent.appValue = [];
+ childComponent.deselect(2);
+ expect(childComponent.appValue).toEqual([2]);
+ childComponent.deselect(3);
+ expect(childComponent.appValue).toEqual([2, 3]);
+ });
+
+ it("should add and remove items from appValue when no input value was given", () => {
+ childComponent.appValue = undefined;
+ childComponent.select(2);
+ expect(childComponent.appValue).toEqual([]);
+ childComponent.appValue = undefined;
+ childComponent.deselect(3);
+ expect(childComponent.appValue).toEqual([3]);
+ });
+});
diff --git a/src/app/shared/controls/file-select/component/file-select.component.stories.ts b/src/app/shared/controls/file-select/component/file-select.component.stories.ts
new file mode 100644
index 0000000..bb65083
--- /dev/null
+++ b/src/app/shared/controls/file-select/component/file-select.component.stories.ts
@@ -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
+ ********************************************************************************/
+
+import {withKnobs} from "@storybook/addon-knobs";
+import {moduleMetadata, storiesOf} from "@storybook/angular";
+import {IAPIAttachmentModel} from "../../../../core/api";
+import {FileSelectModule} from "../file-select.module";
+
+
+const attachments: IAPIAttachmentModel[] = [
+ {
+ id: 1,
+ name: "Attachment 1",
+ type: "string",
+ size: 1,
+ timestamp: "string",
+ tagIds: []
+ },
+ {
+ id: 2,
+ name: "Attachment 1",
+ type: "string",
+ size: 1,
+ timestamp: "string",
+ tagIds: []
+ }
+];
+
+const appValue: number[] = [1];
+
+const deleteAttachment = (ids: number[]) => {
+ console.log(ids);
+};
+
+storiesOf("Shared/Controls", module)
+ .addDecorator(withKnobs)
+ .addDecorator(moduleMetadata({imports: [FileSelectModule]}))
+ .add("FileSelectComponent", () => ({
+ template: `
+ <app-file-select [appAttachments]="attachments" [appValue]="appValue" (appValueChange)="deleteAttachment($event)"></app-file-select>
+ `,
+ props: {
+ attachments,
+ appValue,
+ deleteAttachment
+ }
+ }));
diff --git a/src/app/shared/controls/file-select/component/file-select.component.ts b/src/app/shared/controls/file-select/component/file-select.component.ts
new file mode 100644
index 0000000..b194ec5
--- /dev/null
+++ b/src/app/shared/controls/file-select/component/file-select.component.ts
@@ -0,0 +1,72 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, forwardRef, Input, Output} from "@angular/core";
+import {NG_VALUE_ACCESSOR} from "@angular/forms";
+import {IAPIAttachmentModel} from "../../../../core/api";
+import {arrayJoin} from "../../../../util/store";
+import {AbstractControlValueAccessorComponent} from "../../common";
+
+@Component({
+ selector: "app-file-select",
+ templateUrl: "./file-select.component.html",
+ styleUrls: ["./file-select.component.scss"],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => FileSelectComponent),
+ multi: true
+ }
+ ]
+})
+export class FileSelectComponent extends AbstractControlValueAccessorComponent<number[]> {
+
+ private static id = 0;
+
+ @Input()
+ public appId = `FileSelectComponent-${FileSelectComponent.id}`;
+
+ @Input()
+ public appAttachments: IAPIAttachmentModel[];
+
+ @Output()
+ public appOpenAttachment = new EventEmitter<number>();
+
+ public constructor(public readonly changeDetectorRef: ChangeDetectorRef) {
+ super();
+ }
+
+ public isSelected(attachmentId: number): boolean {
+ return !arrayJoin(this.appValue).some((id) => id === attachmentId);
+ }
+
+ public select(attachmentId: number) {
+ const value = arrayJoin(this.appValue).filter((id) => id !== attachmentId);
+ this.writeValue(value, true);
+ }
+
+ public deselect(attachmentId: number) {
+ this.writeValue(arrayJoin(this.appValue, [attachmentId]), true);
+ }
+
+ public writeValue(obj: number[], emit?: boolean) {
+ super.writeValue(obj, emit);
+ this.changeDetectorRef.markForCheck();
+ }
+
+ public setDisabledState(isDisabled: boolean) {
+ super.setDisabledState(isDisabled);
+ this.changeDetectorRef.markForCheck();
+ }
+}
diff --git a/src/app/shared/controls/file-select/file-select.module.ts b/src/app/shared/controls/file-select/file-select.module.ts
new file mode 100644
index 0000000..3dced60
--- /dev/null
+++ b/src/app/shared/controls/file-select/file-select.module.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 {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {FormsModule} from "@angular/forms";
+import {MatIconModule} from "@angular/material/icon";
+import {FileSelectComponent} from "./component/file-select.component";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ FormsModule,
+ MatIconModule
+ ],
+ declarations: [
+ FileSelectComponent
+ ],
+ exports: [
+ FileSelectComponent
+ ]
+})
+export class FileSelectModule {
+
+}
diff --git a/src/app/shared/controls/file-select/index.ts b/src/app/shared/controls/file-select/index.ts
new file mode 100644
index 0000000..f751a69
--- /dev/null
+++ b/src/app/shared/controls/file-select/index.ts
@@ -0,0 +1,15 @@
+/********************************************************************************
+ * 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 "./file-select.module";
+export * from "./component/file-select.component";
diff --git a/src/app/shared/controls/statement-select/index.ts b/src/app/shared/controls/statement-select/index.ts
new file mode 100644
index 0000000..1d0a651
--- /dev/null
+++ b/src/app/shared/controls/statement-select/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+export * from "./statement-select.module";
diff --git a/src/app/shared/controls/statement-select/statement-select.component.html b/src/app/shared/controls/statement-select/statement-select.component.html
new file mode 100644
index 0000000..bfc979f
--- /dev/null
+++ b/src/app/shared/controls/statement-select/statement-select.component.html
@@ -0,0 +1,31 @@
+<!-------------------------------------------------------------------------------
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ -------------------------------------------------------------------------------->
+
+<app-searchbar
+ (appSearch)="appSearch.emit({q: $event})"
+ [appIsLoading]="appIsLoading"
+ [appSearchText]="appSearchText"
+ class="searchbar">
+</app-searchbar>
+
+<app-statement-table
+ (appToggleSelect)="toggle($event.id, $event.value)"
+ *ngIf="appSearchContent?.length > 0"
+ [appEntries]="getEntries()"
+ [appStatementTypeOptions]="appStatementTypeOptions"
+ class="statements-table">
+</app-statement-table>
+
+<button (click)="writeValue([], true)" class="openk-button" type="button">
+ {{"shared.statementSelect.clear" | translate}}
+</button>
diff --git a/src/app/shared/controls/statement-select/statement-select.component.scss b/src/app/shared/controls/statement-select/statement-select.component.scss
new file mode 100644
index 0000000..ed13cd6
--- /dev/null
+++ b/src/app/shared/controls/statement-select/statement-select.component.scss
@@ -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
+ ********************************************************************************/
+
+:host {
+ display: block;
+}
+
+.searchbar {
+ width: 100%;
+ margin-bottom: 0.75em;
+}
+
+.statements-table {
+ height: 100%;
+ max-height: 25em;
+ margin-bottom: 0.75em;
+}
diff --git a/src/app/shared/controls/statement-select/statement-select.component.stories.ts b/src/app/shared/controls/statement-select/statement-select.component.stories.ts
new file mode 100644
index 0000000..ac279d0
--- /dev/null
+++ b/src/app/shared/controls/statement-select/statement-select.component.stories.ts
@@ -0,0 +1,49 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {RouterTestingModule} from "@angular/router/testing";
+import {action} from "@storybook/addon-actions";
+import {withKnobs} from "@storybook/addon-knobs";
+import {moduleMetadata, storiesOf} from "@storybook/angular";
+import {I18nModule} from "../../../core/i18n";
+import {createStatementModelMock} from "../../../test";
+import {createSelectOptionsMock} from "../../../test/create-select-options.spec";
+import {StatementSelectModule} from "./statement-select.module";
+
+storiesOf("Shared / Controls", module)
+ .addDecorator(withKnobs)
+ .addDecorator(moduleMetadata({
+ imports: [
+ RouterTestingModule,
+ I18nModule,
+ StatementSelectModule
+ ]
+ }))
+ .add("StatementSelect", () => ({
+ template: `
+ <div style="padding: 1em; height: 100%; width: 100%; box-sizing: border-box;">
+ <app-statement-select
+ [appSearchContent]="appOptions"
+ [appStatementTypeOptions]="appStatementTypeOptions"
+ [appValue]="appValue"
+ (appValueChange)="appValueChange($event)">
+ </app-statement-select>
+ </div>
+ `,
+ props: {
+ appValue: [3, 5],
+ appStatementTypeOptions: createSelectOptionsMock(5, "StatementType"),
+ appOptions: Array(20).fill(0).map((_, id) => createStatementModelMock(id, id % 5)),
+ appValueChange: action("appValueChange")
+ }
+ }));
diff --git a/src/app/shared/controls/statement-select/statement-select.component.ts b/src/app/shared/controls/statement-select/statement-select.component.ts
new file mode 100644
index 0000000..a076d62
--- /dev/null
+++ b/src/app/shared/controls/statement-select/statement-select.component.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 {ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, forwardRef, Input, Output} from "@angular/core";
+import {NG_VALUE_ACCESSOR} from "@angular/forms";
+import {IAPISearchOptions, IAPIStatementModel} from "../../../core";
+import {arrayJoin, filterDistinctValues} from "../../../util/store";
+import {IStatementTableEntry} from "../../layout/statement-table";
+import {AbstractControlValueAccessorComponent} from "../common";
+import {ISelectOption} from "../select/model";
+
+@Component({
+ selector: "app-statement-select",
+ templateUrl: "statement-select.component.html",
+ styleUrls: ["statement-select.component.scss"],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => StatementSelectComponent),
+ multi: true
+ }
+ ]
+})
+export class StatementSelectComponent extends AbstractControlValueAccessorComponent<number[]> {
+
+ @Input()
+ public appIsLoading: boolean;
+
+ @Input()
+ public appSearchText: string;
+
+ @Input()
+ public appStatementTypeOptions: ISelectOption<number>[];
+
+ @Input()
+ public appSearchContent: IAPIStatementModel[];
+
+ @Output()
+ public appSearch: EventEmitter<IAPISearchOptions> = new EventEmitter();
+
+ public constructor(private readonly changeDetectorRef: ChangeDetectorRef) {
+ super();
+ }
+
+ public getEntries(onlySelected?: boolean): IStatementTableEntry[] {
+ const entries = arrayJoin(this.appSearchContent);
+ const value = arrayJoin(this.appValue);
+ return entries.map((statement) => {
+ return {
+ ...statement,
+ isSelected: value.indexOf(statement.id) !== -1
+ };
+ }).filter((entry) => !onlySelected || entry.isSelected);
+ }
+
+ public toggle(id: number, isSelected: boolean) {
+ if (isSelected) {
+ const value = filterDistinctValues(arrayJoin(this.appValue, [id]))
+ .sort((a, b) => a - b);
+ this.writeValue(value, true);
+ } else {
+ const value = arrayJoin(this.appValue)
+ .filter((statementId) => statementId !== id);
+ this.writeValue(value, true);
+ }
+ }
+
+ public writeValue(obj: number[], emit?: boolean) {
+ super.writeValue(obj, emit);
+ this.changeDetectorRef.markForCheck();
+ }
+
+}
diff --git a/src/app/shared/controls/statement-select/statement-select.module.ts b/src/app/shared/controls/statement-select/statement-select.module.ts
new file mode 100644
index 0000000..5f250e1
--- /dev/null
+++ b/src/app/shared/controls/statement-select/statement-select.module.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 {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {TranslateModule} from "@ngx-translate/core";
+import {SearchbarModule} from "../../layout/searchbar";
+import {StatementTableModule} from "../../layout/statement-table";
+import {StatementSelectComponent} from "./statement-select.component";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ TranslateModule,
+
+ StatementTableModule,
+ SearchbarModule
+ ],
+ declarations: [
+ StatementSelectComponent
+ ],
+ exports: [
+ StatementSelectComponent
+ ]
+})
+export class StatementSelectModule {
+
+}
diff --git a/src/app/shared/layout/collapsible/collapsible.component.html b/src/app/shared/layout/collapsible/collapsible.component.html
index 2ab52e6..7f5f9df 100644
--- a/src/app/shared/layout/collapsible/collapsible.component.html
+++ b/src/app/shared/layout/collapsible/collapsible.component.html
@@ -12,7 +12,7 @@
-------------------------------------------------------------------------------->
<div [class.collapsible-header---with-content]="appHeaderTemplateRef"
- class="collapsible-header">
+ #header class="collapsible-header">
<button (click)="toggle()"
type="button"
@@ -31,7 +31,7 @@
</div>
<div #bodyElement
- class="collapsible-body">
+ (focusin)="toggle(false)" class="collapsible-body">
<ng-content></ng-content>
diff --git a/src/app/shared/layout/contact-table/contact-table.component.html b/src/app/shared/layout/contact-table/contact-table.component.html
new file mode 100644
index 0000000..0a4bc50
--- /dev/null
+++ b/src/app/shared/layout/contact-table/contact-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
+ -------------------------------------------------------------------------------->
+
+<div #table class="contact-list">
+
+ <table [dataSource]="appEntries" class="contact-list--table" mat-table>
+
+ <caption hidden>{{"contacts.title" | translate}}</caption>
+
+ <ng-container matColumnDef="lastName">
+ <th *matHeaderCellDef
+ class="contact-list--table--cell contact-list--table--cell---border contact-list--table--cell---bold "
+ mat-header-cell
+ scope="col">{{"contacts.name" | translate}}
+ </th>
+ <td (click)="onSelect(contact?.id)" *matCellDef="let contact"
+ [class.contact-list--table--cell---highlight]="contact?.id === appSelectedId"
+ class="contact-list--table--cell contact-list--table--cell---border"
+ mat-cell>
+ {{contact.lastName}}
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="firstName">
+ <th *matHeaderCellDef
+ class="contact-list--table--cell contact-list--table--cell---border contact-list--table--cell---bold "
+ mat-header-cell
+ scope="col">{{"contacts.firstName" | translate}}
+ </th>
+ <td (click)="onSelect(contact?.id)" *matCellDef="let contact"
+ [class.contact-list--table--cell---highlight]="contact?.id === appSelectedId"
+ class="contact-list--table--cell contact-list--table--cell---border"
+ mat-cell>
+ {{contact.firstName}}
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="email">
+ <th *matHeaderCellDef
+ class="contact-list--table--cell contact-list--table--cell---border contact-list--table--cell---bold "
+ mat-header-cell
+ scope="col">{{"contacts.email" | translate}}
+ </th>
+ <td (click)="onSelect(contact?.id)" *matCellDef="let contact"
+ [class.contact-list--table--cell---highlight]="contact?.id === appSelectedId"
+ class="contact-list--table--cell contact-list--table--cell---border"
+ mat-cell>
+ {{contact.email}}
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="company">
+ <th *matHeaderCellDef
+ class="contact-list--table--cell contact-list--table--cell---border contact-list--table--cell---bold "
+ mat-header-cell
+ scope="col">{{"contacts.company" | translate}}
+ </th>
+ <td (click)="onSelect(contact?.id)" *matCellDef="let contact"
+ [class.contact-list--table--cell---highlight]="contact?.id === appSelectedId"
+ class="contact-list--table--cell contact-list--table--cell---border"
+ mat-cell>
+ {{contact.companyName}}
+ </td>
+ </ng-container>
+
+ <tr *matHeaderRowDef="columnsToDisplay; sticky: true"
+ class="contact-list--table--row" mat-header-row></tr>
+ <tr *matRowDef="let contact; columns: columnsToDisplay"
+ [class.disabled]="appDisabled"
+ class="contact-list--table--row contact-list--table--row---color contact-list--table--row---pointer"
+ mat-row></tr>
+
+ </table>
+
+</div>
diff --git a/src/app/shared/layout/contact-table/contact-table.component.scss b/src/app/shared/layout/contact-table/contact-table.component.scss
new file mode 100644
index 0000000..76c78e9
--- /dev/null
+++ b/src/app/shared/layout/contact-table/contact-table.component.scss
@@ -0,0 +1,104 @@
+/********************************************************************************
+ * 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";
+
+.contact-list {
+ width: 100%;
+ flex: 1;
+ overflow: auto;
+ display: flex;
+ box-sizing: border-box;
+ border: 1px solid $openk-form-border;
+ border-radius: 4px;
+}
+
+.contact-list--table {
+ flex: 1;
+ width: 100%;
+}
+
+.contact-list--table--row {
+ background: $openk-background-card;
+ height: auto;
+}
+
+.contact-list--table--row---pointer {
+ cursor: pointer;
+}
+
+.contact-list--table--row---color {
+
+ &:nth-child(odd) {
+ background: $openk-background-highlight;
+ }
+
+ &:hover {
+ background-color: get-color($openk-info-palette, 100);
+ }
+}
+
+.contact-list--table--cell {
+ padding: 0.3em;
+}
+
+.contact-list--table--cell---border {
+ border-bottom: 1px solid $openk-form-border;
+}
+
+.contact-list--table--cell---bold {
+ font-weight: bold;
+ font-size: 14px;
+}
+
+.contact-list--table--cell---icon-size {
+ min-width: 24px;
+ width: 24px;
+}
+
+.contact-list--table--cell--icon---red {
+ color: get-color($openk-danger-palette, 300);
+}
+
+.mat-header-cell:first-of-type {
+ padding-left: 0.6em;
+}
+
+.mat-cell:first-of-type {
+ padding-left: 0.6em;
+}
+
+.mat-header-cell:last-of-type {
+ padding-right: 0.6em;
+}
+
+.mat-cell:last-of-type {
+ padding-right: 0.6em;
+}
+
+.mat-row:last-of-type {
+
+ .mat-cell {
+ border-bottom: 0;
+ }
+}
+
+.contact-list--table--cell---highlight {
+ background-color: get-color($openk-info-palette, 500);
+ color: get-color($openk-info-palette, 500, contrast);
+}
+
+.disabled {
+ pointer-events: none;
+ opacity: 0.6;
+}
diff --git a/src/app/shared/layout/contact-table/contact-table.component.spec.ts b/src/app/shared/layout/contact-table/contact-table.component.spec.ts
new file mode 100644
index 0000000..f2c1ef0
--- /dev/null
+++ b/src/app/shared/layout/contact-table/contact-table.component.spec.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 {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {IAPIContactPerson} from "../../../core/api/contacts/IAPIContactPerson";
+import {I18nModule} from "../../../core/i18n";
+import {ContactTableComponent} from "./contact-table.component";
+import {ContactTableModule} from "./contact-table.module";
+
+describe("ContactTableComponent", () => {
+ let component: ContactTableComponent;
+ let fixture: ComponentFixture<ContactTableComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ ContactTableModule,
+ I18nModule
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ContactTableComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should be created", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should emit appSelectedContact", () => {
+ spyOn(component.appSelectedIdChange, "emit").and.callThrough();
+ const contact: IAPIContactPerson = {
+ companyId: "string",
+ companyName: "string",
+ email: "string",
+ id: "string",
+ firstName: "string",
+ lastName: "string"
+ };
+
+ component.onSelect(contact.id);
+ expect(component.appSelectedIdChange.emit).toHaveBeenCalledWith(contact.id);
+ component.onSelect(contact.id);
+ expect(component.appSelectedIdChange.emit).toHaveBeenCalledWith(undefined);
+ });
+});
diff --git a/src/app/shared/layout/contact-table/contact-table.component.ts b/src/app/shared/layout/contact-table/contact-table.component.ts
new file mode 100644
index 0000000..63fb387
--- /dev/null
+++ b/src/app/shared/layout/contact-table/contact-table.component.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 {Component, EventEmitter, Input, Output} from "@angular/core";
+import {IAPIContactPerson} from "../../../core/api/contacts/IAPIContactPerson";
+
+type AvailableColumns = "lastName" | "firstName" | "email" | "company";
+
+@Component({
+ selector: "app-contact-table",
+ templateUrl: "./contact-table.component.html",
+ styleUrls: ["./contact-table.component.scss"],
+})
+export class ContactTableComponent {
+
+ @Input()
+ public appDisabled: boolean;
+
+ @Input()
+ public appEntries: IAPIContactPerson[];
+
+ @Input()
+ public appSelectedId: string;
+
+ @Output()
+ public appSelectedIdChange = new EventEmitter<string>();
+
+ public columnsToDisplay: AvailableColumns[] = ["lastName", "firstName", "email", "company"];
+
+ public onSelect(id: string) {
+ this.appSelectedId = this.appSelectedId === id ? undefined : id;
+ this.appSelectedIdChange.emit(this.appSelectedId);
+ }
+
+}
diff --git a/src/app/shared/layout/contact-table/contact-table.module.ts b/src/app/shared/layout/contact-table/contact-table.module.ts
new file mode 100644
index 0000000..2146e14
--- /dev/null
+++ b/src/app/shared/layout/contact-table/contact-table.module.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 {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {MatTableModule} from "@angular/material/table";
+import {TranslateModule} from "@ngx-translate/core";
+import {ContactTableComponent} from "./contact-table.component";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ MatTableModule,
+ TranslateModule
+ ],
+ exports: [
+ ContactTableComponent
+ ],
+ declarations: [ContactTableComponent]
+})
+export class ContactTableModule {
+
+}
diff --git a/src/app/shared/layout/contact-table/index.ts b/src/app/shared/layout/contact-table/index.ts
new file mode 100644
index 0000000..bf3c466
--- /dev/null
+++ b/src/app/shared/layout/contact-table/index.ts
@@ -0,0 +1,15 @@
+/********************************************************************************
+ * 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 "./contact-table.module";
+export * from "./contact-table.component";
diff --git a/src/app/shared/layout/pagination-counter/index.ts b/src/app/shared/layout/pagination-counter/index.ts
new file mode 100644
index 0000000..9f53598
--- /dev/null
+++ b/src/app/shared/layout/pagination-counter/index.ts
@@ -0,0 +1,15 @@
+/********************************************************************************
+ * 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 "./pagination-counter.module";
+export * from "./pagination-counter.component";
diff --git a/src/app/shared/layout/pagination-counter/pagination-counter.component.html b/src/app/shared/layout/pagination-counter/pagination-counter.component.html
new file mode 100644
index 0000000..c599d92
--- /dev/null
+++ b/src/app/shared/layout/pagination-counter/pagination-counter.component.html
@@ -0,0 +1,31 @@
+<!-------------------------------------------------------------------------------
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ -------------------------------------------------------------------------------->
+
+<div *ngIf="appPageSize > 0" class="page-info">
+ <button (click)="getPreviousPage()"
+ [class.openk-button-disabled]="!(appPage > 0)"
+ [disabled]="appDisabled || !(appPage > 0)"
+ class="openk-button openk-info page-info--button">
+ <mat-icon class="page-info--button--icon">keyboard_arrow_left</mat-icon>
+ </button>
+ <span
+ [class.disabled]="appDisabled"
+ class="page-info--text">
+ {{appPage == null ? 1 : appPage + 1}} / {{appPageSize}}
+ </span>
+ <button (click)="getNextPage()"
+ [disabled]="appDisabled || appPage >= appPageSize - 1"
+ class="openk-button openk-info page-info--button">
+ <mat-icon class="page-info--button--icon">keyboard_arrow_right</mat-icon>
+ </button>
+</div>
diff --git a/src/app/shared/layout/pagination-counter/pagination-counter.component.scss b/src/app/shared/layout/pagination-counter/pagination-counter.component.scss
new file mode 100644
index 0000000..e6268bb
--- /dev/null
+++ b/src/app/shared/layout/pagination-counter/pagination-counter.component.scss
@@ -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
+ ********************************************************************************/
+
+.page-info {
+ display: flex;
+ justify-content: flex-end;
+}
+
+.page-info--button {
+ transform: scale(0.7);
+ padding: 0;
+}
+
+.page-info--button--icon {
+ padding: 0;
+}
+
+.page-info--text {
+ align-self: center;
+}
+
+.disabled {
+ pointer-events: none;
+ opacity: 0.6;
+}
diff --git a/src/app/shared/layout/pagination-counter/pagination-counter.component.spec.ts b/src/app/shared/layout/pagination-counter/pagination-counter.component.spec.ts
new file mode 100644
index 0000000..464321f
--- /dev/null
+++ b/src/app/shared/layout/pagination-counter/pagination-counter.component.spec.ts
@@ -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 {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {PaginationCounterComponent} from "./pagination-counter.component";
+import {PaginationCounterModule} from "./pagination-counter.module";
+
+describe("PaginationCounterComponent", () => {
+ let component: PaginationCounterComponent;
+ let fixture: ComponentFixture<PaginationCounterComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ PaginationCounterModule
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PaginationCounterComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should be created", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should emit appPageChange with the correct page number", () => {
+ component.appPage = 3;
+ spyOn(component.appPageChange, "emit").and.callThrough();
+ component.getNextPage();
+ expect(component.appPageChange.emit).toHaveBeenCalledWith(4);
+ component.getPreviousPage();
+ expect(component.appPageChange.emit).toHaveBeenCalledWith(2);
+ });
+});
diff --git a/src/app/shared/layout/pagination-counter/pagination-counter.component.ts b/src/app/shared/layout/pagination-counter/pagination-counter.component.ts
new file mode 100644
index 0000000..0e89738
--- /dev/null
+++ b/src/app/shared/layout/pagination-counter/pagination-counter.component.ts
@@ -0,0 +1,43 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {Component, EventEmitter, Input, Output} from "@angular/core";
+
+@Component({
+ selector: "app-pagination-counter",
+ templateUrl: "./pagination-counter.component.html",
+ styleUrls: ["./pagination-counter.component.scss"],
+})
+export class PaginationCounterComponent {
+
+ @Input()
+ public appDisabled: boolean;
+
+ @Input()
+ public appPage: number;
+
+ @Input()
+ public appPageSize: number;
+
+ @Output()
+ public appPageChange = new EventEmitter<number>();
+
+ public getPreviousPage() {
+ this.appPageChange.emit(this.appPage - 1);
+ }
+
+ public getNextPage() {
+ this.appPageChange.emit(this.appPage + 1);
+ }
+
+}
diff --git a/src/app/shared/layout/pagination-counter/pagination-counter.module.ts b/src/app/shared/layout/pagination-counter/pagination-counter.module.ts
new file mode 100644
index 0000000..c440039
--- /dev/null
+++ b/src/app/shared/layout/pagination-counter/pagination-counter.module.ts
@@ -0,0 +1,31 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {MatIconModule} from "@angular/material/icon";
+import {PaginationCounterComponent} from "./pagination-counter.component";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ MatIconModule
+ ],
+ exports: [
+ PaginationCounterComponent
+ ],
+ declarations: [PaginationCounterComponent]
+})
+export class PaginationCounterModule {
+
+}
diff --git a/src/app/shared/layout/searchbar/index.ts b/src/app/shared/layout/searchbar/index.ts
new file mode 100644
index 0000000..f59d9b8
--- /dev/null
+++ b/src/app/shared/layout/searchbar/index.ts
@@ -0,0 +1,15 @@
+/********************************************************************************
+ * 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 "./searchbar.component";
+export * from "./searchbar.module";
diff --git a/src/app/shared/layout/searchbar/searchbar.component.html b/src/app/shared/layout/searchbar/searchbar.component.html
new file mode 100644
index 0000000..ea38c09
--- /dev/null
+++ b/src/app/shared/layout/searchbar/searchbar.component.html
@@ -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
+ -------------------------------------------------------------------------------->
+
+<div class="search">
+
+ <span class="search--bar">
+ <input
+ #inputField
+ (input)="appSearch.emit(inputField.value)"
+ (keydown.enter)="$event.preventDefault(); appSearch.emit(inputField.value)"
+ [placeholder]="appPlaceholder == null ? '' : appPlaceholder"
+ [value]="appSearchText == null ? '' : appSearchText"
+ autocomplete="off"
+ class="openk-input search--bar--input"
+ type="text">
+
+ <mat-icon
+ (dblclick)="inputField.select()"
+ (pointerdown)="$event.preventDefault(); inputField.focus()"
+ class="search--bar--icon">
+ search
+ </mat-icon>
+ </span>
+</div>
+
+<div [style.opacity]="appIsLoading ? 1 : 0" class="progress-spinner">
+ <app-progress-spinner>
+ </app-progress-spinner>
+</div>
diff --git a/src/app/shared/layout/searchbar/searchbar.component.scss b/src/app/shared/layout/searchbar/searchbar.component.scss
new file mode 100644
index 0000000..a06dd6e
--- /dev/null
+++ b/src/app/shared/layout/searchbar/searchbar.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 "../../../../styles/openk.styles";
+
+:host {
+ display: inline-flex;
+ flex-flow: row nowrap;
+ align-items: center;
+}
+
+.search {
+ flex: 1 1 100%;
+ display: inline-flex;
+ position: relative;
+ font-size: 0.875em;
+}
+
+.search--bar {
+ display: inline-flex;
+ width: 100%;
+ height: 100%;
+}
+
+.search--bar--icon {
+ display: inline-flex;
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: initial;
+ height: 100%;
+ padding: 0 0.3em;
+ justify-content: center;
+ align-items: center;
+ font-size: 1em;
+ margin: auto 0;
+ color: get-color($openk-default-palette, 800);
+ transition: color ease-in-out 100ms;
+}
+
+.search--bar--input {
+ width: 100%;
+ height: 100%;
+ font-size: 1em;
+ padding-right: 1.5em;
+}
+
+.search--bar--input:focus + .search--bar--icon {
+ color: get-color($openk-info-palette, A300);
+}
+
+.progress-spinner {
+ margin-left: 0.5em;
+ transition: opacity 75ms ease-out;
+}
diff --git a/src/app/shared/layout/searchbar/searchbar.component.spec.ts b/src/app/shared/layout/searchbar/searchbar.component.spec.ts
new file mode 100644
index 0000000..c6dabe3
--- /dev/null
+++ b/src/app/shared/layout/searchbar/searchbar.component.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 {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {By} from "@angular/platform-browser";
+import {I18nModule} from "../../../core/i18n";
+import {SearchbarComponent} from "./searchbar.component";
+import {SearchbarModule} from "./searchbar.module";
+
+describe("SearchbarComponent", () => {
+ let component: SearchbarComponent;
+ let fixture: ComponentFixture<SearchbarComponent>;
+ let inputElement: HTMLInputElement;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [I18nModule, SearchbarModule]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SearchbarComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ inputElement = fixture.debugElement.query(By.css(".search--bar--input")).nativeElement;
+ });
+
+ it("should emit the current value of the input field", () => {
+ spyOn(component.appSearch, "emit").and.callThrough();
+
+ inputElement.value = "on input";
+ inputElement.dispatchEvent(new InputEvent("input"));
+ expect(component.appSearch.emit).toHaveBeenCalledWith("on input");
+
+ inputElement.value = "on enter";
+ inputElement.dispatchEvent(new KeyboardEvent("keydown", {key: "enter"}));
+ expect(component.appSearch.emit).toHaveBeenCalledWith("on enter");
+ });
+
+});
diff --git a/src/app/shared/layout/searchbar/searchbar.component.stories.ts b/src/app/shared/layout/searchbar/searchbar.component.stories.ts
new file mode 100644
index 0000000..beadbc3
--- /dev/null
+++ b/src/app/shared/layout/searchbar/searchbar.component.stories.ts
@@ -0,0 +1,43 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {action} from "@storybook/addon-actions";
+import {boolean, text, withKnobs} from "@storybook/addon-knobs";
+import {moduleMetadata, storiesOf} from "@storybook/angular";
+import {SearchbarModule} from "./searchbar.module";
+
+storiesOf("Shared / Layout", module)
+ .addDecorator(withKnobs)
+ .addDecorator(moduleMetadata({
+ imports: [
+ SearchbarModule
+ ]
+ }))
+ .add("SearchbarComponent", () => ({
+ template: `
+ <div style="padding: 1em;">
+ <app-searchbar style="width: 100%;"
+ [appIsLoading]="appIsLoading"
+ [appPlaceholder]="appPlaceholder"
+ [appSearchText]="appSearchText"
+ (appSearch)="appSearch($event)">
+ </app-searchbar>
+ </div>
+ `,
+ props: {
+ appIsLoading: boolean("appIsLoading", false),
+ appPlaceholder: text("appPlaceholder", ""),
+ appSearchText: text("appSearchText", ""),
+ appSearch: action("appSearch")
+ }
+ }));
diff --git a/src/app/shared/layout/searchbar/searchbar.component.ts b/src/app/shared/layout/searchbar/searchbar.component.ts
new file mode 100644
index 0000000..59586a3
--- /dev/null
+++ b/src/app/shared/layout/searchbar/searchbar.component.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 {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from "@angular/core";
+
+@Component({
+ selector: "app-searchbar",
+ templateUrl: "./searchbar.component.html",
+ styleUrls: ["./searchbar.component.scss"],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class SearchbarComponent {
+
+ @Input()
+ public appPlaceholder: string;
+
+ @Input()
+ public appSearchText: string;
+
+ @Input()
+ public appIsLoading: boolean;
+
+ @Output()
+ public appSearch: EventEmitter<string> = new EventEmitter();
+
+}
diff --git a/src/app/shared/layout/searchbar/searchbar.module.ts b/src/app/shared/layout/searchbar/searchbar.module.ts
new file mode 100644
index 0000000..34fe116
--- /dev/null
+++ b/src/app/shared/layout/searchbar/searchbar.module.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 {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {MatIconModule} from "@angular/material/icon";
+import {TranslateModule} from "@ngx-translate/core";
+import {ProgressSpinnerModule} from "../../progress-spinner";
+import {SearchbarComponent} from "./searchbar.component";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ TranslateModule,
+ MatIconModule,
+ ProgressSpinnerModule
+ ],
+ declarations: [
+ SearchbarComponent
+ ],
+ exports: [
+ SearchbarComponent
+ ]
+})
+export class SearchbarModule {
+
+}
diff --git a/src/app/shared/layout/statement-table/IStatementTableEntry.ts b/src/app/shared/layout/statement-table/IStatementTableEntry.ts
new file mode 100644
index 0000000..a18cce9
--- /dev/null
+++ b/src/app/shared/layout/statement-table/IStatementTableEntry.ts
@@ -0,0 +1,18 @@
+/********************************************************************************
+ * 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 {IAPIStatementModel} from "../../../core/api/statements";
+
+export interface IStatementTableEntry extends IAPIStatementModel {
+ isSelected?: boolean;
+}
diff --git a/src/app/shared/layout/statement-table/index.ts b/src/app/shared/layout/statement-table/index.ts
new file mode 100644
index 0000000..62e8b86
--- /dev/null
+++ b/src/app/shared/layout/statement-table/index.ts
@@ -0,0 +1,16 @@
+/********************************************************************************
+ * 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 "./IStatementTableEntry";
+export * from "./statement-table.component";
+export * from "./statement-table.module";
diff --git a/src/app/shared/layout/statement-table/statement-table.component.html b/src/app/shared/layout/statement-table/statement-table.component.html
new file mode 100644
index 0000000..894f5d9
--- /dev/null
+++ b/src/app/shared/layout/statement-table/statement-table.component.html
@@ -0,0 +1,102 @@
+<!-------------------------------------------------------------------------------
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ -------------------------------------------------------------------------------->
+
+<table [dataSource]="appEntries" class="predecessors--table" mat-table>
+
+ <caption hidden>{{ "shared.statementTable.caption" | translate}}</caption>
+
+ <ng-container matColumnDef="select">
+ <th *matHeaderCellDef
+ class="predecessors--table--cell predecessors--table--cell---border predecessors--table--cell---bold"
+ mat-header-cell scope="col"></th>
+ <td *matCellDef="let statement" class="predecessors--table--cell predecessors--table--cell---border"
+ mat-cell>
+ <div class="predecessors--table--cell--checkbox">
+ <input #inputElement (keydown.enter)="inputElement.click()"
+ (ngModelChange)="$event ? select(statement.id) : deselect(statement.id)"
+ [class.cursor-pointer]="!appDisabled"
+ [disabled]="appDisabled"
+ [ngModel]="statement?.isSelected"
+ class="predecessors--table--cell--checkbox---size"
+ type="checkbox">
+ </div>
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="id">
+ <th *matHeaderCellDef
+ class="predecessors--table--cell predecessors--table--cell---border predecessors--table--cell---bold predecessors--table--cell---center"
+ mat-header-cell scope="col">{{"shared.statementTable.id" | translate}}</th>
+ <td *matCellDef="let statement"
+ class="predecessors--table--cell predecessors--table--cell---border predecessors--table--cell---center"
+ mat-cell>{{statement?.id}}</td>
+ </ng-container>
+
+ <ng-container matColumnDef="title">
+ <th *matHeaderCellDef
+ class="predecessors--table--cell predecessors--table--cell---border predecessors--table--cell---bold"
+ mat-header-cell scope="col">{{"shared.statementTable.title" | translate}}</th>
+ <td *matCellDef="let statement" class="predecessors--table--cell predecessors--table--cell---border"
+ mat-cell>{{statement?.title}}</td>
+ </ng-container>
+
+ <ng-container matColumnDef="type">
+ <th *matHeaderCellDef
+ class="predecessors--table--cell predecessors--table--cell---border predecessors--table--cell---bold"
+ mat-header-cell scope="col">{{"shared.statementTable.statementType" | translate}}</th>
+ <td *matCellDef="let statement" class="predecessors--table--cell predecessors--table--cell---border"
+ mat-cell>{{ (appStatementTypeOptions | selected : statement?.typeId)?.label }}</td>
+ </ng-container>
+
+ <ng-container matColumnDef="date">
+ <th *matHeaderCellDef
+ class="predecessors--table--cell predecessors--table--cell---border predecessors--table--cell---bold"
+ mat-header-cell scope="col">{{"shared.statementTable.receiptDate" | translate}}</th>
+ <td *matCellDef="let statement" class="predecessors--table--cell predecessors--table--cell---border"
+ mat-cell>{{statement?.receiptDate ? (statement.receiptDate | appMomentPipe).format(appTimeDisplayFormat) : ""}} </td>
+ </ng-container>
+
+ <ng-container matColumnDef="city">
+ <th *matHeaderCellDef
+ class="predecessors--table--cell predecessors--table--cell---border predecessors--table--cell---bold"
+ mat-header-cell scope="col">{{"shared.statementTable.city" | translate}}</th>
+ <td *matCellDef="let statement" class="predecessors--table--cell predecessors--table--cell---border"
+ mat-cell>{{statement?.city}}</td>
+ </ng-container>
+
+ <ng-container matColumnDef="district">
+ <th *matHeaderCellDef
+ class="predecessors--table--cell predecessors--table--cell---border predecessors--table--cell---bold"
+ mat-header-cell scope="col">{{"shared.statementTable.district" | translate}}</th>
+ <td *matCellDef="let statement" class="predecessors--table--cell predecessors--table--cell---border"
+ mat-cell>{{statement?.district}}</td>
+ </ng-container>
+
+ <ng-container matColumnDef="link">
+ <th *matHeaderCellDef
+ class="predecessors--table--cell predecessors--table--cell---border predecessors--table--cell---bold"
+ mat-header-cell scope="col"></th>
+ <td *matCellDef="let statement" class="predecessors--table--cell predecessors--table--cell---border"
+ mat-cell>
+ <a [queryParams]="{id: statement.id}"
+ class="openk-button openk-button-rounded openk-info predecessors--table--cell--btn" routerLink="/details"
+ target="_blank">
+ <mat-icon>call_made</mat-icon>
+ </a>
+ </td>
+ </ng-container>
+
+ <tr *matHeaderRowDef="appColumnsToDisplay; sticky: true" class="predecessors--table--row" mat-header-row></tr>
+ <tr *matRowDef="let myRowData; columns: appColumnsToDisplay"
+ class="predecessors--table--row predecessors--table--row---alternating-color" mat-row></tr>
+</table>
diff --git a/src/app/shared/layout/statement-table/statement-table.component.scss b/src/app/shared/layout/statement-table/statement-table.component.scss
new file mode 100644
index 0000000..3f9fdec
--- /dev/null
+++ b/src/app/shared/layout/statement-table/statement-table.component.scss
@@ -0,0 +1,119 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+@import "openk.styles";
+
+:host {
+ display: flex;
+ flex-flow: column;
+ width: 100%;
+ overflow: auto;
+ box-sizing: border-box;
+ border: 1px solid $openk-form-border;
+ border-radius: 4px;
+}
+
+.predecessors {
+ width: 100%;
+ overflow: auto;
+ display: flex;
+ box-sizing: border-box;
+ height: 100%;
+}
+
+.predecessors---border {
+ border: 1px solid $openk-form-border;
+ border-radius: 4px;
+}
+
+.predecessors--table {
+ flex: 1;
+ width: 100%;
+}
+
+.predecessors--table--row {
+ background: $openk-background-card;
+ height: auto;
+}
+
+.predecessors--table--row---alternating-color {
+
+ &:nth-child(odd) {
+ background: $openk-background-highlight;
+ }
+}
+
+.predecessors--table--cell {
+ padding: 0.25em 0.75em;
+}
+
+.predecessors--table--cell---border {
+ border-bottom: 1px solid $openk-form-border;
+}
+
+.predecessors--table--cell---bold {
+ font-weight: bold;
+ font-size: 14px;
+}
+
+.predecessors--table--cell--checkbox {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.predecessors--table--cell---center {
+ text-align: center;
+}
+
+.predecessors--table--cell---fill {
+ width: 100%;
+ line-break: anywhere;
+}
+
+.predecessors--table--cell--checkbox---size {
+ font-size: 1em;
+ width: 1em;
+ height: 1em;
+}
+
+.predecessors--table--cell--btn {
+ transform: scale(0.7);
+}
+
+.predecessors--table--cell--icon---red {
+ color: get-color($openk-danger-palette, 300);
+}
+
+.mat-header-cell:first-of-type {
+ padding-left: 0.6em;
+}
+
+.mat-cell:first-of-type {
+ padding-left: 0.6em;
+}
+
+.mat-header-cell:last-of-type {
+ padding-right: 0.6em;
+}
+
+.mat-cell:last-of-type {
+ padding-right: 0.6em;
+}
+
+.mat-row:last-of-type {
+
+ .mat-cell {
+ border-bottom: 0;
+ }
+}
diff --git a/src/app/shared/layout/statement-table/statement-table.component.spec.ts b/src/app/shared/layout/statement-table/statement-table.component.spec.ts
new file mode 100644
index 0000000..0cfd30c
--- /dev/null
+++ b/src/app/shared/layout/statement-table/statement-table.component.spec.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 {Component, ElementRef, ViewChild} from "@angular/core";
+import {ComponentFixture, TestBed} from "@angular/core/testing";
+import {MatIconModule} from "@angular/material/icon";
+import {By} from "@angular/platform-browser";
+import {RouterTestingModule} from "@angular/router/testing";
+import {I18nModule} from "../../../core/i18n";
+import {IStatementTableEntry} from "./IStatementTableEntry";
+import {StatementTableComponent} from "./statement-table.component";
+import {StatementTableModule} from "./statement-table.module";
+
+@Component({
+ selector: `app-host-component`,
+ template: `
+ <div style="height: 200px;">
+ <app-statement-table #predecessors [appEntries]="appData"></app-statement-table>
+ </div>
+ `
+})
+class TestHostComponent {
+ public appData: Array<IStatementTableEntry>;
+ @ViewChild("history", {read: ElementRef}) history: ElementRef;
+
+ public constructor() {
+ this.appData = (new Array(40)).fill({
+ isPredecessor: true,
+ id: 1,
+ title: "ein Titel",
+ type: "Bauvorhaben",
+ receiptDate: "2007-08-31T16:47+00:00",
+ city: "Stadt",
+ district: "Ortsteil"
+ });
+ }
+}
+
+describe("StatementTableComponent", () => {
+ let component: TestHostComponent;
+ let fixture: ComponentFixture<TestHostComponent>;
+ let childComponent: StatementTableComponent;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [StatementTableComponent, TestHostComponent],
+ imports: [
+ StatementTableModule,
+ I18nModule,
+ MatIconModule,
+ RouterTestingModule
+ ],
+ }).compileComponents();
+ });
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestHostComponent);
+ component = fixture.componentInstance;
+ const childDebugElement = fixture.debugElement.query(By.directive(StatementTableComponent));
+ childComponent = childDebugElement.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should emit appToggleSelect with the id and corresponding checked value", () => {
+ spyOn(childComponent.appToggleSelect, "emit").and.callThrough();
+ childComponent.select(1);
+ expect(childComponent.appToggleSelect.emit).toHaveBeenCalledWith({id: 1, value: true});
+ childComponent.deselect(1);
+ expect(childComponent.appToggleSelect.emit).toHaveBeenCalledWith({id: 1, value: false});
+ });
+});
diff --git a/src/app/shared/layout/statement-table/statement-table.component.stories.ts b/src/app/shared/layout/statement-table/statement-table.component.stories.ts
new file mode 100644
index 0000000..168ad74
--- /dev/null
+++ b/src/app/shared/layout/statement-table/statement-table.component.stories.ts
@@ -0,0 +1,56 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {RouterTestingModule} from "@angular/router/testing";
+import {action} from "@storybook/addon-actions";
+import {boolean, text, withKnobs} from "@storybook/addon-knobs";
+import {moduleMetadata, storiesOf} from "@storybook/angular";
+import {I18nModule} from "../../../core/i18n";
+import {createStatementModelMock} from "../../../test";
+import {createSelectOptionsMock} from "../../../test/create-select-options.spec";
+import {StatementTableModule} from "./statement-table.module";
+
+storiesOf("Shared / Layout", module)
+ .addDecorator(withKnobs)
+ .addDecorator(moduleMetadata({
+ imports: [
+ RouterTestingModule,
+ I18nModule,
+ StatementTableModule
+ ]
+ }))
+ .add("StatementTableComponent", () => ({
+ template: `
+ <div style="padding: 1em; height: 100%; box-sizing: border-box;">
+ <app-statement-table style="width: 100%; height: 100%;"
+ [appColumnsToDisplay]="restricted ? columnsRestricted : columnsAll"
+ [appDisabled]="appDisabled"
+ [appEntries]="appEntries"
+ [appStatementTypeOptions]="appStatementTypeOptions"
+ [style.maxHeight]="maxHeight"
+ (appToggleSelect)="appToggleSelect($event)">
+ </app-statement-table>
+ </div>
+ `,
+ props: {
+ restricted: boolean("Restrict columns", false),
+ maxHeight: text("maxHeight", "initial"),
+ appDisabled: boolean("appDisabled", false),
+ appStatementTypeOptions: createSelectOptionsMock(5, "StatementType"),
+ appToggleSelect: action("appToggleSelect"),
+ appEntries: Array(20).fill(0)
+ .map((_, id) => createStatementModelMock(id, id % 5)),
+ columnsAll: ["select", "id", "title", "type", "date", "city", "district", "link"],
+ columnsRestricted: ["id", "title", "type", "date", "city", "district", "link"],
+ }
+ }));
diff --git a/src/app/shared/layout/statement-table/statement-table.component.ts b/src/app/shared/layout/statement-table/statement-table.component.ts
new file mode 100644
index 0000000..3e472b3
--- /dev/null
+++ b/src/app/shared/layout/statement-table/statement-table.component.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 {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from "@angular/core";
+import {momentFormatDisplayNumeric} from "../../../util/moment";
+import {ISelectOption} from "../../controls/select/model";
+import {IStatementTableEntry} from "./IStatementTableEntry";
+
+export type TStatementTableColumns = "select" | "id" | "title" | "type" | "date" | "city" | "district" | "link";
+
+@Component({
+ selector: "app-statement-table",
+ templateUrl: "./statement-table.component.html",
+ styleUrls: ["./statement-table.component.scss"],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class StatementTableComponent {
+
+ @Input()
+ public appColumnsToDisplay: TStatementTableColumns[] = ["select", "id", "title", "type", "date", "city", "district", "link"];
+
+ @Input()
+ public appDisabled: boolean;
+
+ @Input()
+ public appEntries: IStatementTableEntry[];
+
+ @Input()
+ public appStatementTypeOptions: ISelectOption<number>[] = [];
+
+ @Input()
+ public appTimeDisplayFormat: string = momentFormatDisplayNumeric;
+
+ @Output()
+ public appToggleSelect: EventEmitter<{ id: number, value: boolean }> = new EventEmitter();
+
+ public select(id: number) {
+ this.appToggleSelect.emit({id, value: true});
+ }
+
+ public deselect(id: number) {
+ this.appToggleSelect.emit({id, value: false});
+ }
+}
diff --git a/src/app/shared/layout/statement-table/statement-table.module.ts b/src/app/shared/layout/statement-table/statement-table.module.ts
new file mode 100644
index 0000000..2f1cf08
--- /dev/null
+++ b/src/app/shared/layout/statement-table/statement-table.module.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 {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {FormsModule} from "@angular/forms";
+import {MatIconModule} from "@angular/material/icon";
+import {MatTableModule} from "@angular/material/table";
+import {RouterModule} from "@angular/router";
+import {TranslateModule} from "@ngx-translate/core";
+import {DateControlModule} from "../../controls/date-control";
+import {SelectModule} from "../../controls/select";
+import {StatementTableComponent} from "./statement-table.component";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ FormsModule,
+ MatIconModule,
+ MatTableModule,
+ TranslateModule,
+ DateControlModule,
+ RouterModule,
+ SelectModule
+ ],
+ declarations: [
+ StatementTableComponent,
+ ],
+ exports: [
+ StatementTableComponent,
+ ]
+})
+export class StatementTableModule {
+
+}
diff --git a/src/app/shared/linked-statements/index.ts b/src/app/shared/linked-statements/index.ts
new file mode 100644
index 0000000..ef4c339
--- /dev/null
+++ b/src/app/shared/linked-statements/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+export * from "./linked-statements.module";
diff --git a/src/app/shared/linked-statements/linked-statements.module.ts b/src/app/shared/linked-statements/linked-statements.module.ts
new file mode 100644
index 0000000..3869a3f
--- /dev/null
+++ b/src/app/shared/linked-statements/linked-statements.module.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
+ ********************************************************************************/
+
+import {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {MatIconModule} from "@angular/material/icon";
+import {TranslateModule} from "@ngx-translate/core";
+import {SearchbarModule} from "../layout/searchbar";
+import {StatementTableModule} from "../layout/statement-table";
+import {LinkedStatementsComponent} from "./linked-statements";
+
+@NgModule({
+ imports: [
+ CommonModule,
+ MatIconModule,
+ TranslateModule,
+ StatementTableModule,
+ SearchbarModule
+ ],
+ declarations: [
+ LinkedStatementsComponent,
+ ],
+ exports: [
+ LinkedStatementsComponent
+ ]
+})
+export class LinkedStatementsModule {
+
+}
diff --git a/src/app/shared/linked-statements/linked-statements/index.ts b/src/app/shared/linked-statements/linked-statements/index.ts
new file mode 100644
index 0000000..18e8a60
--- /dev/null
+++ b/src/app/shared/linked-statements/linked-statements/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+export * from "./linked-statements.component";
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.html b/src/app/shared/linked-statements/linked-statements/linked-statements.component.html
new file mode 100644
index 0000000..076d351
--- /dev/null
+++ b/src/app/shared/linked-statements/linked-statements/linked-statements.component.html
@@ -0,0 +1,32 @@
+<!-------------------------------------------------------------------------------
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ -------------------------------------------------------------------------------->
+
+<div *ngIf="appPredecessors?.length > 0" class="statements">
+ <span class="statements--titlebar">{{"shared.linkedStatements.precedingStatements" | translate}}</span>
+ <app-statement-table
+ [appColumnsToDisplay]="columnsToDisplay"
+ [appEntries]="appSuccessors"
+ [appStatementTypeOptions]="appStatementTypeOptions">
+ </app-statement-table>
+</div>
+
+<div *ngIf="appSuccessors?.length > 0" class="statements">
+ <span class="statements--titlebar">{{"shared.linkedStatements.successiveStatements" | translate}}</span>
+ <app-statement-table
+ [appColumnsToDisplay]="columnsToDisplay"
+ [appEntries]="appSuccessors"
+ [appStatementTypeOptions]="appStatementTypeOptions">
+ </app-statement-table>
+</div>
+
+
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss b/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
new file mode 100644
index 0000000..4d2360d
--- /dev/null
+++ b/src/app/shared/linked-statements/linked-statements/linked-statements.component.scss
@@ -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
+ ********************************************************************************/
+
+@import "../../../../styles/openk.styles";
+
+:host {
+ width: 100%;
+}
+
+.statements {
+ margin-bottom: 1em;
+ display: grid;
+}
+
+.statements--titlebar {
+ margin-bottom: 0.5em;
+}
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.spec.ts b/src/app/shared/linked-statements/linked-statements/linked-statements.component.spec.ts
new file mode 100644
index 0000000..9c7ed3b
--- /dev/null
+++ b/src/app/shared/linked-statements/linked-statements/linked-statements.component.spec.ts
@@ -0,0 +1,43 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {RouterTestingModule} from "@angular/router/testing";
+import {I18nModule} from "../../../core/i18n";
+import {LinkedStatementsModule} from "../linked-statements.module";
+import {LinkedStatementsComponent} from "./linked-statements.component";
+
+describe("LinkedStatementsComponent", () => {
+ let component: LinkedStatementsComponent;
+ let fixture: ComponentFixture<LinkedStatementsComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule,
+ I18nModule,
+ LinkedStatementsModule
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LinkedStatementsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should be created", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.stories.ts b/src/app/shared/linked-statements/linked-statements/linked-statements.component.stories.ts
new file mode 100644
index 0000000..f86ce60
--- /dev/null
+++ b/src/app/shared/linked-statements/linked-statements/linked-statements.component.stories.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 {RouterTestingModule} from "@angular/router/testing";
+import {number, withKnobs} from "@storybook/addon-knobs";
+import {moduleMetadata, storiesOf} from "@storybook/angular";
+import {I18nModule} from "../../../core/i18n";
+import {createStatementModelMock} from "../../../test";
+import {createSelectOptionsMock} from "../../../test/create-select-options.spec";
+import {LinkedStatementsModule} from "../linked-statements.module";
+
+storiesOf("Shared", module)
+ .addDecorator(withKnobs)
+ .addDecorator(moduleMetadata({
+ imports: [
+ RouterTestingModule,
+ I18nModule,
+ LinkedStatementsModule
+ ]
+ }))
+ .add("LinkedStatementsComponent", () => ({
+ template: `
+ <div style="padding: 1em;">
+ <app-linked-statements
+ [appPredecessors]="entries.slice(0, numberOfRowsToDisplay)"
+ [appSuccessors]="entries.slice(0, numberOfRowsToDisplay)"
+ [appStatementTypeOptions]="appStatementTypeOptions">
+ </app-linked-statements>
+ </div>
+ `,
+ props: {
+ entries: Array(20).fill(0)
+ .map((_, id) => createStatementModelMock(id, id % 5)),
+ appStatementTypeOptions: createSelectOptionsMock(5, "StatementType"),
+ numberOfRowsToDisplay: number("number of rows to display", 4, {min: 0, max: 11}),
+ }
+ }));
diff --git a/src/app/shared/linked-statements/linked-statements/linked-statements.component.ts b/src/app/shared/linked-statements/linked-statements/linked-statements.component.ts
new file mode 100644
index 0000000..83fbc69
--- /dev/null
+++ b/src/app/shared/linked-statements/linked-statements/linked-statements.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, Input} from "@angular/core";
+import {ISelectOption} from "../../controls/select/model";
+import {IStatementTableEntry, TStatementTableColumns} from "../../layout/statement-table";
+
+@Component({
+ selector: "app-linked-statements",
+ templateUrl: "./linked-statements.component.html",
+ styleUrls: ["./linked-statements.component.scss"]
+})
+export class LinkedStatementsComponent {
+
+ public columnsToDisplay: TStatementTableColumns[] = ["id", "title", "type", "date", "city", "district", "link"];
+
+ @Input()
+ public appPredecessors: Array<IStatementTableEntry>;
+
+ @Input()
+ public appSuccessors: Array<IStatementTableEntry>;
+
+ @Input()
+ public appStatementTypeOptions: ISelectOption<number>[];
+}
diff --git a/src/app/store/app-store.module.ts b/src/app/store/app-store.module.ts
index b13e0ad..3f7599d 100644
--- a/src/app/store/app-store.module.ts
+++ b/src/app/store/app-store.module.ts
@@ -12,20 +12,21 @@
********************************************************************************/
import {NgModule, Optional, SkipSelf} from "@angular/core";
+import {AttachmentsStoreModule} from "./attachments";
+import {ContactsStoreModule} from "./contacts";
import {ProcessStoreModule} from "./process";
import {RootStoreModule} from "./root";
import {SettingsStoreModule} from "./settings";
import {StatementsStoreModule} from "./statements";
-// import {StatementsStoreModule} from "./statements";
-
@NgModule({
imports: [
+ AttachmentsStoreModule,
RootStoreModule,
SettingsStoreModule,
- // StatementsStoreModule,
ProcessStoreModule,
- StatementsStoreModule
+ StatementsStoreModule,
+ ContactsStoreModule
]
})
export class AppStoreModule {
diff --git a/src/app/store/attachments/actions/attachments.actions.ts b/src/app/store/attachments/actions/attachments.actions.ts
new file mode 100644
index 0000000..47c8358
--- /dev/null
+++ b/src/app/store/attachments/actions/attachments.actions.ts
@@ -0,0 +1,56 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {createAction, props} from "@ngrx/store";
+import {IAPIAttachmentModel} from "../../../core/api";
+import {IAttachmentError} from "../model";
+
+export const fetchAttachmentsAction = createAction(
+ "[Details/Edit] Fetch attachments",
+ props<{ statementId: number }>()
+);
+
+export const addOrRemoveAttachmentsAction = createAction(
+ "[Edit/Details] Add or remove attachments",
+ props<{ statementId: number, taskId: string, add?: File[], remove?: number[] }>()
+);
+
+export const addAttachmentErrorAction = createAction(
+ "[API] Add attachment error",
+ props<{
+ statementId: number,
+ taskId: string,
+ addError: IAttachmentError<File>[],
+ removeError: IAttachmentError<number>[]
+ }>()
+);
+
+export const clearFileCacheAction = createAction(
+ "[Edit/API] Clear file cache",
+ props<{ statementId: number; }>()
+);
+
+export const setAttachmentsAction = createAction(
+ "[API] Set attachments",
+ props<{ statementId: number, entities: IAPIAttachmentModel[] }>()
+);
+
+export const addAttachmentEntityAction = createAction(
+ "[API] Add attachment",
+ props<{ statementId: number, entity: IAPIAttachmentModel }>()
+);
+
+export const deleteAttachmentsAction = createAction(
+ "[API] Delete attachments",
+ props<{ statementId: number, entityIds: number[] }>()
+);
diff --git a/src/app/store/attachments/actions/index.ts b/src/app/store/attachments/actions/index.ts
new file mode 100644
index 0000000..af17144
--- /dev/null
+++ b/src/app/store/attachments/actions/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./attachments.actions";
diff --git a/src/app/store/attachments/attachments-reducers.token.ts b/src/app/store/attachments/attachments-reducers.token.ts
new file mode 100644
index 0000000..4c82830
--- /dev/null
+++ b/src/app/store/attachments/attachments-reducers.token.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 {InjectionToken} from "@angular/core";
+import {ActionReducerMap} from "@ngrx/store";
+import {IAttachmentsStoreState} from "./model";
+import {attachmentEntitiesReducer, statementAttachmentsReducer, statementFileCacheReducer} from "./reducers";
+
+export const ATTACHMENTS_NAME = "attachments";
+
+export const ATTACHMENTS_REDUCER = new InjectionToken<ActionReducerMap<IAttachmentsStoreState>>("Attachments store reducer", {
+ providedIn: "root",
+ factory: () => ({
+ entities: attachmentEntitiesReducer,
+ statementFileCache: statementFileCacheReducer,
+ statementAttachments: statementAttachmentsReducer
+ })
+});
diff --git a/src/app/store/attachments/attachments-store.module.ts b/src/app/store/attachments/attachments-store.module.ts
new file mode 100644
index 0000000..eaa5059
--- /dev/null
+++ b/src/app/store/attachments/attachments-store.module.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 {NgModule} from "@angular/core";
+import {EffectsModule} from "@ngrx/effects";
+import {StoreModule} from "@ngrx/store";
+import {ATTACHMENTS_NAME, ATTACHMENTS_REDUCER} from "./attachments-reducers.token";
+import {AddOrRemoveAttachmentsEffect} from "./effects/add-or-remove";
+import {FetchAttachmentsEffect} from "./effects/fetch";
+
+@NgModule({
+ imports: [
+ StoreModule.forFeature(ATTACHMENTS_NAME, ATTACHMENTS_REDUCER),
+ EffectsModule.forFeature([
+ AddOrRemoveAttachmentsEffect,
+ FetchAttachmentsEffect
+ ])
+ ]
+})
+export class AttachmentsStoreModule {
+
+}
diff --git a/src/app/store/attachments/effects/add-or-remove/add-or-remove-attachments.effect.ts b/src/app/store/attachments/effects/add-or-remove/add-or-remove-attachments.effect.ts
new file mode 100644
index 0000000..7fe16e3
--- /dev/null
+++ b/src/app/store/attachments/effects/add-or-remove/add-or-remove-attachments.effect.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 {Injectable} from "@angular/core";
+import {Actions, createEffect, ofType} from "@ngrx/effects";
+import {Action} from "@ngrx/store";
+import {concat, defer, EMPTY, Observable, of, throwError} from "rxjs";
+import {catchError, map, switchMap} from "rxjs/operators";
+import {AttachmentsApiService} from "../../../../core/api/attachments";
+import {ignoreError} from "../../../../util/rxjs";
+import {arrayJoin} from "../../../../util/store";
+import {
+ addAttachmentEntityAction,
+ addAttachmentErrorAction,
+ addOrRemoveAttachmentsAction,
+ clearFileCacheAction,
+ deleteAttachmentsAction,
+} from "../../actions";
+import {IAttachmentError} from "../../model";
+
+@Injectable({providedIn: "root"})
+export class AddOrRemoveAttachmentsEffect {
+
+ public addOrRemove$ = createEffect(() => this.actions.pipe(
+ ofType(addOrRemoveAttachmentsAction),
+ switchMap((action) => {
+ return this.addOrRemoveAttachments(action.statementId, action.taskId, action.add, action.remove).pipe(
+ ignoreError()
+ );
+ })
+ ));
+
+ public constructor(public readonly actions: Actions, public readonly attachmentsApiService: AttachmentsApiService) {
+
+ }
+
+ public addOrRemoveAttachments(
+ statementId: number,
+ taskId: string,
+ add?: File[],
+ remove?: number[]
+ ): Observable<Action> {
+ const removeError: IAttachmentError<number>[] = [];
+ const addError: IAttachmentError<File>[] = [];
+ let isSuccess = false;
+ return concat(
+ this.removeAttachments(statementId, taskId, remove, removeError),
+ this.addAttachments(statementId, taskId, add, addError),
+ defer(() => {
+ isSuccess = addError.length === 0 && removeError.length === 0;
+ return isSuccess ?
+ of(clearFileCacheAction({statementId})) :
+ of(addAttachmentErrorAction({statementId, taskId, addError, removeError}));
+ }),
+ defer(() => isSuccess ? EMPTY : throwError([...removeError, ...addError]))
+ );
+ }
+
+ public removeAttachments(
+ statementId: number,
+ taskId: string,
+ remove?: number[],
+ errors: IAttachmentError<number>[] = []
+ ): Observable<Action> {
+ const remove$s = arrayJoin(remove).map((attachmentId) => {
+ return this.attachmentsApiService.deleteAttachment(statementId, taskId, attachmentId).pipe(
+ map(() => deleteAttachmentsAction({statementId, entityIds: [attachmentId]})),
+ catchError((error) => {
+ errors.push({attachment: attachmentId, error});
+ return EMPTY;
+ })
+ );
+ });
+
+ return concat(...remove$s);
+ }
+
+ public addAttachments(
+ statementId: number,
+ taskId: string,
+ add?: File[],
+ errors: IAttachmentError<File>[] = []
+ ): Observable<Action> {
+ const add$s = arrayJoin(add).filter((file) => file instanceof File).map((file) => {
+ return this.attachmentsApiService.postAttachment(statementId, taskId, file).pipe(
+ map((entity) => addAttachmentEntityAction({statementId, entity})),
+ catchError((error) => {
+ errors.push({attachment: file, error});
+ return EMPTY;
+ })
+ );
+ });
+
+ return concat(...add$s);
+ }
+
+}
diff --git a/src/app/store/attachments/effects/add-or-remove/index.ts b/src/app/store/attachments/effects/add-or-remove/index.ts
new file mode 100644
index 0000000..8a74c8f
--- /dev/null
+++ b/src/app/store/attachments/effects/add-or-remove/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./add-or-remove-attachments.effect";
diff --git a/src/app/store/attachments/effects/fetch/fetch-attachments.effect.ts b/src/app/store/attachments/effects/fetch/fetch-attachments.effect.ts
new file mode 100644
index 0000000..6a4b345
--- /dev/null
+++ b/src/app/store/attachments/effects/fetch/fetch-attachments.effect.ts
@@ -0,0 +1,41 @@
+/********************************************************************************
+ * 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 {filter, map, switchMap} from "rxjs/operators";
+import {AttachmentsApiService} from "../../../../core/api/attachments";
+import {fetchAttachmentsAction, setAttachmentsAction} from "../../actions";
+
+@Injectable({providedIn: "root"})
+export class FetchAttachmentsEffect {
+
+ public fetch$ = createEffect(() => this.actions.pipe(
+ ofType(fetchAttachmentsAction),
+ filter((action) => action.statementId != null),
+ switchMap((action) => this.fetchAttachments(action.statementId))
+ ));
+
+ public constructor(public actions: Actions, private attachmentsApiService: AttachmentsApiService) {
+
+ }
+
+ public fetchAttachments(statementId: number): Observable<Action> {
+ return this.attachmentsApiService.getAttachments(statementId).pipe(
+ map((entities) => setAttachmentsAction({statementId, entities}))
+ );
+ }
+
+}
diff --git a/src/app/store/attachments/effects/fetch/index.ts b/src/app/store/attachments/effects/fetch/index.ts
new file mode 100644
index 0000000..fe8e881
--- /dev/null
+++ b/src/app/store/attachments/effects/fetch/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./fetch-attachments.effect";
diff --git a/src/app/store/attachments/effects/index.ts b/src/app/store/attachments/effects/index.ts
new file mode 100644
index 0000000..536301a
--- /dev/null
+++ b/src/app/store/attachments/effects/index.ts
@@ -0,0 +1,15 @@
+/********************************************************************************
+ * 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 "./add-or-remove";
+export * from "./fetch";
diff --git a/src/app/store/attachments/index.ts b/src/app/store/attachments/index.ts
new file mode 100644
index 0000000..8aeda86
--- /dev/null
+++ b/src/app/store/attachments/index.ts
@@ -0,0 +1,19 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+export * from "./actions";
+export * from "./effects";
+export * from "./model";
+export * from "./selectors";
+
+export * from "./attachments-store.module";
diff --git a/src/app/store/attachments/model/IAttachmentError.ts b/src/app/store/attachments/model/IAttachmentError.ts
new file mode 100644
index 0000000..c320020
--- /dev/null
+++ b/src/app/store/attachments/model/IAttachmentError.ts
@@ -0,0 +1,17 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+export interface IAttachmentError<T> {
+ attachment: T;
+ error: any;
+}
diff --git a/src/app/store/attachments/model/IAttachmentsStoreState.ts b/src/app/store/attachments/model/IAttachmentsStoreState.ts
new file mode 100644
index 0000000..78fd303
--- /dev/null
+++ b/src/app/store/attachments/model/IAttachmentsStoreState.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 {IAPIAttachmentModel} from "../../../core/api";
+import {TStoreEntities} from "../../../util/store";
+
+export interface IAttachmentsStoreState {
+
+ entities: TStoreEntities<IAPIAttachmentModel>;
+
+ statementFileCache?: TStoreEntities<File[]>;
+
+ statementAttachments: TStoreEntities<number[]>;
+
+}
diff --git a/src/app/store/attachments/model/index.ts b/src/app/store/attachments/model/index.ts
new file mode 100644
index 0000000..90e6922
--- /dev/null
+++ b/src/app/store/attachments/model/index.ts
@@ -0,0 +1,15 @@
+/********************************************************************************
+ * 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 "./IAttachmentError";
+export * from "./IAttachmentsStoreState";
diff --git a/src/app/store/attachments/reducers/entities/attachment-entities.reducer.spec.ts b/src/app/store/attachments/reducers/entities/attachment-entities.reducer.spec.ts
new file mode 100644
index 0000000..a90ae7c
--- /dev/null
+++ b/src/app/store/attachments/reducers/entities/attachment-entities.reducer.spec.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 {Action} from "@ngrx/store";
+import {createAttachmentModelMock} from "../../../../test";
+import {addAttachmentEntityAction, deleteAttachmentsAction, setAttachmentsAction} from "../../actions";
+import {IAttachmentsStoreState} from "../../model";
+import {attachmentEntitiesReducer} from "./attachment-entities.reducer";
+
+describe("attachmentEntitiesReducer", () => {
+
+ const initialState: IAttachmentsStoreState["entities"] = {
+ 17: createAttachmentModelMock(17),
+ 18: createAttachmentModelMock(18)
+ };
+
+ it("should set a list of attachment entities to store", () => {
+ const attachment = createAttachmentModelMock(19, [1, 9]);
+ const action: Action = setAttachmentsAction({
+ statementId: 1919,
+ entities: [attachment]
+ });
+ const state = attachmentEntitiesReducer(initialState, action);
+ expect(state).toEqual({
+ ...initialState,
+ 19: attachment
+ });
+ });
+
+ it("should add a single attachment entitiy to store", () => {
+ const attachment = createAttachmentModelMock(19, [1, 9]);
+ const action: Action = addAttachmentEntityAction({
+ statementId: 1919,
+ entity: attachment
+ });
+ const state = attachmentEntitiesReducer(initialState, action);
+ expect(state).toEqual({
+ ...initialState,
+ 19: attachment
+ });
+ });
+
+ it("should delete attachment entities from store", () => {
+ const action: Action = deleteAttachmentsAction({
+ statementId: 1919,
+ entityIds: [17]
+ });
+ const state = attachmentEntitiesReducer(initialState, action);
+ expect(state).toEqual({
+ 18: initialState[18]
+ });
+ });
+
+
+});
diff --git a/src/app/store/attachments/reducers/entities/attachment-entities.reducer.ts b/src/app/store/attachments/reducers/entities/attachment-entities.reducer.ts
new file mode 100644
index 0000000..058bb92
--- /dev/null
+++ b/src/app/store/attachments/reducers/entities/attachment-entities.reducer.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 {createReducer, on} from "@ngrx/store";
+import {IAPIAttachmentModel} from "../../../../core/api";
+import {deleteEntities, setEntitiesObject, TStoreEntities, updateEntitiesObject} from "../../../../util/store";
+import {addAttachmentEntityAction, deleteAttachmentsAction, setAttachmentsAction} from "../../actions";
+
+export const attachmentEntitiesReducer = createReducer<TStoreEntities<IAPIAttachmentModel>>(
+ {},
+ on(setAttachmentsAction, (state, payload) => {
+ return setEntitiesObject(state, payload.entities, (_) => _.id);
+ }),
+ on(addAttachmentEntityAction, (state, payload) => {
+ return updateEntitiesObject(state, [payload.entity], (_) => _.id);
+ }),
+ on(deleteAttachmentsAction, (state, payload) => {
+ return deleteEntities(state, payload.entityIds);
+ })
+);
diff --git a/src/app/store/attachments/reducers/index.ts b/src/app/store/attachments/reducers/index.ts
new file mode 100644
index 0000000..526049c
--- /dev/null
+++ b/src/app/store/attachments/reducers/index.ts
@@ -0,0 +1,16 @@
+/********************************************************************************
+ * 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 "./entities/attachment-entities.reducer";
+export * from "./statement-file-cache/statement-file-cache.reducer";
+export * from "./statement-attachments/statement-attachments.reducer";
diff --git a/src/app/store/attachments/reducers/statement-attachments/statement-attachments.reducer.spec.ts b/src/app/store/attachments/reducers/statement-attachments/statement-attachments.reducer.spec.ts
new file mode 100644
index 0000000..1665b2d
--- /dev/null
+++ b/src/app/store/attachments/reducers/statement-attachments/statement-attachments.reducer.spec.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 {createAttachmentModelMock} from "../../../../test";
+import {addAttachmentEntityAction, deleteAttachmentsAction, setAttachmentsAction} from "../../actions";
+import {IAttachmentsStoreState} from "../../model";
+import {statementAttachmentsReducer} from "./statement-attachments.reducer";
+
+describe("statementAttachmentsReducer", () => {
+
+ const initialState: IAttachmentsStoreState["statementAttachments"] = {
+ 1919: [1, 2, 3]
+ };
+
+ it("should set a list of attachment entity IDs to store", () => {
+ const attachment = createAttachmentModelMock(19, [1, 9]);
+ const action = setAttachmentsAction({
+ statementId: 1919,
+ entities: [attachment, null]
+ });
+ const state = statementAttachmentsReducer(initialState, action);
+ expect(state).toEqual({
+ ...initialState,
+ 1919: [attachment.id]
+ });
+ });
+
+ it("should add a single attachment ID to store", () => {
+ const attachment = createAttachmentModelMock(19, [1, 9]);
+ const action = addAttachmentEntityAction({
+ statementId: 1919,
+ entity: null
+ });
+ let state = statementAttachmentsReducer(initialState, action);
+ expect(state).toEqual(initialState);
+ action.entity = attachment;
+ state = statementAttachmentsReducer(initialState, action);
+ expect(state).toEqual({
+ ...initialState,
+ 1919: [1, 2, 3, attachment.id]
+ });
+ });
+
+ it("should delete attachment IDs from store", () => {
+ const action = deleteAttachmentsAction({
+ statementId: 1919,
+ entityIds: [2]
+ });
+ const state = statementAttachmentsReducer(initialState, action);
+ expect(state).toEqual({
+ 1919: [1, 3]
+ });
+ });
+
+});
diff --git a/src/app/store/attachments/reducers/statement-attachments/statement-attachments.reducer.ts b/src/app/store/attachments/reducers/statement-attachments/statement-attachments.reducer.ts
new file mode 100644
index 0000000..034ba7d
--- /dev/null
+++ b/src/app/store/attachments/reducers/statement-attachments/statement-attachments.reducer.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 {createReducer, on} from "@ngrx/store";
+import {arrayJoin, filterDistinctValues, setEntitiesObject, TStoreEntities} from "../../../../util/store";
+import {addAttachmentEntityAction, deleteAttachmentsAction, setAttachmentsAction} from "../../actions";
+
+export const statementAttachmentsReducer = createReducer<TStoreEntities<number[]>>(
+ {},
+ on(addAttachmentEntityAction, (state, action) => {
+ const statementId = action.statementId;
+ const entityIds = filterDistinctValues(arrayJoin(state[statementId], [action.entity?.id]));
+ return setEntitiesObject(state, [entityIds], () => statementId);
+ }),
+ on(setAttachmentsAction, (state, action) => {
+ const statementId = action.statementId;
+ const entityIds = filterDistinctValues(arrayJoin(action.entities).map((_) => _?.id));
+
+ return setEntitiesObject(state, [entityIds], () => statementId);
+ }),
+ on(deleteAttachmentsAction, (state, action) => {
+ const statementId = action.statementId;
+ const entityIdsToDelete = arrayJoin(action.entityIds);
+ const entityIds = arrayJoin(state[statementId]).filter((_) => entityIdsToDelete.indexOf(_) === -1);
+
+ return setEntitiesObject(state, [entityIds], () => statementId);
+ })
+);
diff --git a/src/app/store/attachments/reducers/statement-file-cache/statement-file-cache.reducer.spec.ts b/src/app/store/attachments/reducers/statement-file-cache/statement-file-cache.reducer.spec.ts
new file mode 100644
index 0000000..f44c8e4
--- /dev/null
+++ b/src/app/store/attachments/reducers/statement-file-cache/statement-file-cache.reducer.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 {Action} from "@ngrx/store";
+import {createFileMock} from "../../../../test";
+import {addAttachmentErrorAction, clearFileCacheAction} from "../../actions";
+import {IAttachmentsStoreState} from "../../model";
+import {statementFileCacheReducer} from "./statement-file-cache.reducer";
+
+describe("statementFileCacheReducer", () => {
+
+ const file = createFileMock("test.pdf");
+
+ const initialState: IAttachmentsStoreState["statementFileCache"] = {
+ 1919: [file]
+ };
+
+ it("should set file cache on add attachment errors", () => {
+ const action = addAttachmentErrorAction({
+ statementId: 1919,
+ taskId: "1919",
+ addError: [
+ {
+ attachment: createFileMock("File 0.pdf"),
+ error: new Error("")
+ },
+ {
+ attachment: createFileMock("File 0.pdf"),
+ error: new Error("")
+ }
+ ],
+ removeError: []
+ });
+ const state = statementFileCacheReducer(initialState, action);
+ expect(state).toEqual({
+ 1919: action.addError.map((_) => _.attachment)
+ });
+ });
+
+ it("should clear file cache", () => {
+ const action: Action = clearFileCacheAction({
+ statementId: 1919
+ });
+ const state = statementFileCacheReducer(initialState, action);
+ expect(state).toEqual({});
+ });
+
+});
diff --git a/src/app/store/attachments/reducers/statement-file-cache/statement-file-cache.reducer.ts b/src/app/store/attachments/reducers/statement-file-cache/statement-file-cache.reducer.ts
new file mode 100644
index 0000000..eadf2d5
--- /dev/null
+++ b/src/app/store/attachments/reducers/statement-file-cache/statement-file-cache.reducer.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 {createReducer, on} from "@ngrx/store";
+import {arrayJoin, deleteEntities, setEntitiesObject} from "../../../../util/store";
+import {addAttachmentErrorAction, clearFileCacheAction} from "../../actions";
+import {IAttachmentsStoreState} from "../../model";
+
+export const statementFileCacheReducer = createReducer<IAttachmentsStoreState["statementFileCache"]>(
+ undefined,
+ on(clearFileCacheAction, (state, payload) => {
+ return deleteEntities(state, [payload.statementId]);
+ }),
+ on(addAttachmentErrorAction, (state, payload) => {
+ const files = arrayJoin(payload.addError)
+ .map((error) => error.attachment)
+ .filter((file) => file instanceof File);
+ return setEntitiesObject(state, [files], () => payload.statementId);
+ })
+);
diff --git a/src/app/store/attachments/selectors/attachments.selectors.spec.ts b/src/app/store/attachments/selectors/attachments.selectors.spec.ts
new file mode 100644
index 0000000..87ffb3e
--- /dev/null
+++ b/src/app/store/attachments/selectors/attachments.selectors.spec.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 {createAttachmentModelMock, createFileMock} from "../../../test";
+import {IAttachmentsStoreState} from "../model";
+import {attachmentsEntitiesSelector, getStatementAttachmentsSelector, getStatementFileCacheSelector} from "./attachments.selectors";
+
+describe("attachmentsSelectors", () => {
+
+ const statementId = 1919;
+
+ const state: IAttachmentsStoreState = {
+ entities: {
+ 17: createAttachmentModelMock(17),
+ 18: createAttachmentModelMock(18),
+ 19: createAttachmentModelMock(19)
+ },
+ statementAttachments: {
+ 1919: [17, 19]
+ },
+ statementFileCache: {
+ 1919: [
+ createFileMock("Test1.pdf"),
+ createFileMock("Test2.pdf")
+ ]
+ }
+ };
+
+ it("attachmentsEntitiesSelector", () => {
+ const projector = attachmentsEntitiesSelector.projector;
+ expect(projector(null)).toEqual({});
+ expect(projector({...state, entities: null})).toEqual({});
+ expect(projector(state)).toBe(state.entities);
+ });
+
+ it("getStatementAttachmentsSelector", () => {
+ const projector = getStatementAttachmentsSelector.projector;
+ expect(projector(null, state.entities, statementId)).toEqual([]);
+ expect(projector({...state, statementAttachments: null}, state.entities, statementId)).toEqual([]);
+ expect(projector(state, state.entities, statementId)).toEqual([state.entities[17], state.entities[19]]);
+ });
+
+ it("getStatementFileCacheSelector", () => {
+ const projector = getStatementFileCacheSelector.projector;
+ expect(projector(null, statementId)).toEqual([]);
+ expect(projector({...state, statementFileCache: null}, statementId)).toEqual([]);
+ expect(projector(state, statementId)).toEqual(state.statementFileCache[statementId]);
+ });
+});
diff --git a/src/app/store/attachments/selectors/attachments.selectors.ts b/src/app/store/attachments/selectors/attachments.selectors.ts
new file mode 100644
index 0000000..6f943f6
--- /dev/null
+++ b/src/app/store/attachments/selectors/attachments.selectors.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 {createFeatureSelector, createSelector} from "@ngrx/store";
+import {IAPIAttachmentModel} from "../../../core/api/attachments";
+import {arrayJoin} from "../../../util/store";
+import {queryParamsIdSelector} from "../../root/selectors";
+import {ATTACHMENTS_NAME} from "../attachments-reducers.token";
+import {IAttachmentsStoreState} from "../model";
+
+export const attachmentsStoreStateSelector = createFeatureSelector<IAttachmentsStoreState>(ATTACHMENTS_NAME);
+
+export const attachmentsEntitiesSelector = createSelector(
+ attachmentsStoreStateSelector,
+ (state) => state?.entities == null ? {} : state.entities
+);
+
+export const getStatementAttachmentsSelector = createSelector(
+ attachmentsStoreStateSelector,
+ attachmentsEntitiesSelector,
+ queryParamsIdSelector,
+ (state, entities, statementId): IAPIAttachmentModel[] => {
+ return state?.statementAttachments == null ? [] : arrayJoin(state.statementAttachments[statementId])
+ .map((attachmentId) => entities[attachmentId]);
+ }
+);
+
+export const getStatementFileCacheSelector = createSelector(
+ attachmentsStoreStateSelector,
+ queryParamsIdSelector,
+ (state, id): File[] => {
+ return state?.statementFileCache == null ? [] : arrayJoin(state.statementFileCache[id]);
+ }
+);
diff --git a/src/app/store/attachments/selectors/index.ts b/src/app/store/attachments/selectors/index.ts
new file mode 100644
index 0000000..ea7fa16
--- /dev/null
+++ b/src/app/store/attachments/selectors/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./attachments.selectors";
diff --git a/src/app/store/contacts/actions/contact.actions.ts b/src/app/store/contacts/actions/contact.actions.ts
new file mode 100644
index 0000000..7939f8d
--- /dev/null
+++ b/src/app/store/contacts/actions/contact.actions.ts
@@ -0,0 +1,41 @@
+/********************************************************************************
+ * 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 {IAPIPaginationResponse, IAPISearchOptions} from "../../../core/api";
+import {IContactEntity, IContactsLoadingState} from "../model";
+
+export const startContactSearchAction = createAction(
+ "[Edit] Search for contacts",
+ props<{ options: IAPISearchOptions }>()
+);
+
+export const fetchContactDetailsAction = createAction(
+ "[Edit] Fetch contact details",
+ props<{ contactId: string }>()
+);
+
+export const setContactEntityAction = createAction(
+ "[API] Set contact entity",
+ props<{ entity: Partial<IContactEntity> }>()
+);
+
+export const setContactsLoadingState = createAction(
+ "[API] Set loading state for contacts",
+ props<{ state: IContactsLoadingState }>()
+);
+
+export const setContactsSearchAction = createAction(
+ "[API] Set search results for contacts",
+ props<{ results: IAPIPaginationResponse<Partial<IContactEntity>> }>()
+);
diff --git a/src/app/store/contacts/actions/index.ts b/src/app/store/contacts/actions/index.ts
new file mode 100644
index 0000000..d70cd97
--- /dev/null
+++ b/src/app/store/contacts/actions/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./contact.actions";
diff --git a/src/app/store/contacts/contacts-reducers.token.ts b/src/app/store/contacts/contacts-reducers.token.ts
new file mode 100644
index 0000000..e8112b2
--- /dev/null
+++ b/src/app/store/contacts/contacts-reducers.token.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 {InjectionToken} from "@angular/core";
+import {ActionReducerMap} from "@ngrx/store";
+import {IContactsStoreState} from "./model";
+import {contactEntitiesReducer, contactsLoadingReducer, contactsSearchReducer} from "./reducers";
+
+export const CONTACTS_NAME = "contacts";
+
+export const CONTACTS_REDUCER = new InjectionToken<ActionReducerMap<IContactsStoreState>>("Contacts store reducer", {
+ providedIn: "root",
+ factory: () => ({
+ entities: contactEntitiesReducer,
+ loading: contactsLoadingReducer,
+ search: contactsSearchReducer
+ })
+});
diff --git a/src/app/store/contacts/contacts-store.module.ts b/src/app/store/contacts/contacts-store.module.ts
new file mode 100644
index 0000000..727d8d0
--- /dev/null
+++ b/src/app/store/contacts/contacts-store.module.ts
@@ -0,0 +1,31 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {NgModule} from "@angular/core";
+import {EffectsModule} from "@ngrx/effects";
+import {StoreModule} from "@ngrx/store";
+import {CONTACTS_NAME, CONTACTS_REDUCER} from "./contacts-reducers.token";
+import {FetchContactDetailsEffect, SearchContactsEffectService} from "./effects";
+
+@NgModule({
+ imports: [
+ StoreModule.forFeature(CONTACTS_NAME, CONTACTS_REDUCER),
+ EffectsModule.forFeature([
+ FetchContactDetailsEffect,
+ SearchContactsEffectService
+ ])
+ ]
+})
+export class ContactsStoreModule {
+
+}
diff --git a/src/app/store/contacts/effects/fetch-details/fetch-contact-details.effect.ts b/src/app/store/contacts/effects/fetch-details/fetch-contact-details.effect.ts
new file mode 100644
index 0000000..55c5487
--- /dev/null
+++ b/src/app/store/contacts/effects/fetch-details/fetch-contact-details.effect.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} from "rxjs";
+import {endWith, filter, map, retry, startWith, switchMap} from "rxjs/operators";
+import {ContactsApiService} from "../../../../core/api/contacts";
+import {ignoreError} from "../../../../util/rxjs";
+import {fetchContactDetailsAction, setContactEntityAction, setContactsLoadingState} from "../../actions";
+
+@Injectable({providedIn: "root"})
+export class FetchContactDetailsEffect {
+
+ public fetch$ = createEffect(() => this.actions.pipe(
+ ofType(fetchContactDetailsAction),
+ filter((action) => action.contactId != null),
+ switchMap((action) => this.fetch(action.contactId))
+ ));
+
+ public constructor(public actions: Actions, public contactsApiService: ContactsApiService) {
+
+ }
+
+ public fetch(contactId: string): Observable<Action> {
+ return this.contactsApiService.getContactDetails(contactId).pipe(
+ map((result) => {
+ return setContactEntityAction({
+ entity: {
+ id: contactId,
+ ...result,
+ }
+ });
+ }),
+ retry(2),
+ ignoreError(),
+ startWith(setContactsLoadingState({state: {fetching: true}})),
+ endWith(setContactsLoadingState({state: {fetching: false}}))
+ );
+ }
+
+
+}
diff --git a/src/app/store/contacts/effects/fetch-details/index.ts b/src/app/store/contacts/effects/fetch-details/index.ts
new file mode 100644
index 0000000..0dcf03f
--- /dev/null
+++ b/src/app/store/contacts/effects/fetch-details/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./fetch-contact-details.effect";
diff --git a/src/app/store/contacts/effects/index.ts b/src/app/store/contacts/effects/index.ts
new file mode 100644
index 0000000..053c243
--- /dev/null
+++ b/src/app/store/contacts/effects/index.ts
@@ -0,0 +1,15 @@
+/********************************************************************************
+ * 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 "./fetch-details";
+export * from "./search";
diff --git a/src/app/store/contacts/effects/search/index.ts b/src/app/store/contacts/effects/search/index.ts
new file mode 100644
index 0000000..dad1562
--- /dev/null
+++ b/src/app/store/contacts/effects/search/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./search-contacts-effect.service";
diff --git a/src/app/store/contacts/effects/search/search-contacts-effect.service.ts b/src/app/store/contacts/effects/search/search-contacts-effect.service.ts
new file mode 100644
index 0000000..b7b7af4
--- /dev/null
+++ b/src/app/store/contacts/effects/search/search-contacts-effect.service.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 {Injectable} from "@angular/core";
+import {Actions, createEffect, ofType} from "@ngrx/effects";
+import {Action} from "@ngrx/store";
+import {asyncScheduler, Observable} from "rxjs";
+import {concatMap, endWith, filter, map, startWith, throttleTime} from "rxjs/operators";
+import {ContactsApiService} from "../../../../core/api/contacts";
+import {IAPISearchOptions} from "../../../../core/api/shared";
+import {ignoreError} from "../../../../util/rxjs";
+import {setContactsLoadingState, setContactsSearchAction, startContactSearchAction} from "../../actions";
+
+@Injectable({providedIn: "root"})
+export class SearchContactsEffectService {
+
+ public search$ = createEffect(() => this.actions.pipe(
+ ofType(startContactSearchAction),
+ filter((action) => action.options != null),
+ throttleTime(200, asyncScheduler, {leading: true, trailing: true}),
+ concatMap((action) => this.search(action.options))
+ ));
+
+ public constructor(public actions: Actions, public contactsApiService: ContactsApiService) {
+
+ }
+
+ public search(options: IAPISearchOptions): Observable<Action> {
+ return this.contactsApiService.getContacts(options).pipe(
+ map((results) => setContactsSearchAction({results})),
+ ignoreError(),
+ startWith(setContactsLoadingState({state: {searching: true}})),
+ endWith(setContactsLoadingState({state: {searching: false}}))
+ );
+ }
+
+}
diff --git a/src/app/store/contacts/index.ts b/src/app/store/contacts/index.ts
new file mode 100644
index 0000000..1c00eb8
--- /dev/null
+++ b/src/app/store/contacts/index.ts
@@ -0,0 +1,18 @@
+/********************************************************************************
+ * 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 "./actions";
+export * from "./model";
+export * from "./selectors";
+
+export * from "./contacts-store.module";
diff --git a/src/app/store/contacts/model/IContactEntity.ts b/src/app/store/contacts/model/IContactEntity.ts
new file mode 100644
index 0000000..257db19
--- /dev/null
+++ b/src/app/store/contacts/model/IContactEntity.ts
@@ -0,0 +1,19 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {IAPIContactPerson} from "../../../core/api/contacts/IAPIContactPerson";
+import {IAPIContactPersonDetails} from "../../../core/api/contacts/IAPIContactPersonDetails";
+
+export interface IContactEntity extends IAPIContactPerson, IAPIContactPersonDetails {
+
+}
diff --git a/src/app/store/contacts/model/IContactsLoadingState.ts b/src/app/store/contacts/model/IContactsLoadingState.ts
new file mode 100644
index 0000000..af1dec0
--- /dev/null
+++ b/src/app/store/contacts/model/IContactsLoadingState.ts
@@ -0,0 +1,20 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+export interface IContactsLoadingState {
+
+ searching?: boolean;
+
+ fetching?: boolean;
+
+}
diff --git a/src/app/store/contacts/model/IContactsStoreState.ts b/src/app/store/contacts/model/IContactsStoreState.ts
new file mode 100644
index 0000000..4f39c4c
--- /dev/null
+++ b/src/app/store/contacts/model/IContactsStoreState.ts
@@ -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
+ ********************************************************************************/
+
+import {IAPIPaginationResponse} from "../../../core/api/shared";
+import {TStoreEntities} from "../../../util/store";
+import {IContactEntity} from "./IContactEntity";
+import {IContactsLoadingState} from "./IContactsLoadingState";
+
+export interface IContactsStoreState {
+
+ entities: TStoreEntities<IContactEntity>;
+
+ loading?: IContactsLoadingState;
+
+ search?: IAPIPaginationResponse<string>;
+
+}
diff --git a/src/app/store/contacts/model/index.ts b/src/app/store/contacts/model/index.ts
new file mode 100644
index 0000000..de1c0e0
--- /dev/null
+++ b/src/app/store/contacts/model/index.ts
@@ -0,0 +1,16 @@
+/********************************************************************************
+ * 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 "./IContactEntity";
+export * from "./IContactsLoadingState";
+export * from "./IContactsStoreState";
diff --git a/src/app/store/contacts/reducers/entities/contact-entities.reducer.spec.ts b/src/app/store/contacts/reducers/entities/contact-entities.reducer.spec.ts
new file mode 100644
index 0000000..77cd562
--- /dev/null
+++ b/src/app/store/contacts/reducers/entities/contact-entities.reducer.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 {IAPIPaginationResponse} from "../../../../core/api/shared";
+import {TStoreEntities} from "../../../../util/store";
+import {setContactEntityAction, setContactsSearchAction} from "../../actions";
+import {IContactEntity} from "../../model";
+import {contactEntitiesReducer} from "./contact-entities.reducer";
+
+describe("contactEntitiesReducer", () => {
+
+ it("should not change the state on setContactEntityAction with no values given", () => {
+ const initialState: TStoreEntities<IContactEntity> = {};
+ let action = setContactEntityAction(undefined);
+ let state = contactEntitiesReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ action = setContactEntityAction({entity: undefined});
+ state = contactEntitiesReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ action = setContactEntityAction({entity: null});
+ state = contactEntitiesReducer(initialState, action);
+ expect(state).toEqual(initialState);
+ });
+
+ it("should add the entity only if it has an id property", () => {
+ const initialState: TStoreEntities<IContactEntity> = {};
+ let action = setContactEntityAction({entity: {firstName: "firstName"}});
+ let state = contactEntitiesReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ action = setContactEntityAction({entity: {id: "1", firstName: "firstName"}});
+ state = contactEntitiesReducer(initialState, action);
+ const newState = {};
+ newState["1"] = {id: "1", firstName: "firstName"};
+ expect(state).toEqual(newState);
+ });
+
+ it("should not change the state on setContactsSearchAction with no values given", () => {
+ const initialState: TStoreEntities<IContactEntity> = {};
+ let action = setContactsSearchAction(undefined);
+ let state = contactEntitiesReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ let response = {content: null} as IAPIPaginationResponse<Partial<IContactEntity>>;
+ action = setContactsSearchAction({results: response});
+ state = contactEntitiesReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ response = {content: []} as IAPIPaginationResponse<Partial<IContactEntity>>;
+ action = setContactsSearchAction({results: response});
+ state = contactEntitiesReducer(initialState, action);
+ expect(state).toEqual(initialState);
+ });
+
+ it("should add the entities only if it has an id property", () => {
+ const initialState: TStoreEntities<IContactEntity> = {};
+ let response = {
+ content: [
+ {
+ firstName: "firstName"
+ }
+ ]
+ } as IAPIPaginationResponse<Partial<IContactEntity>>;
+ let action = setContactsSearchAction({results: response});
+ let state = contactEntitiesReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ response = {
+ content: [
+ {
+ id: "1",
+ firstName: "firstName"
+ }
+ ]
+ } as IAPIPaginationResponse<Partial<IContactEntity>>;
+ action = setContactsSearchAction({results: response});
+ state = contactEntitiesReducer(initialState, action);
+ let newState = {};
+ newState["1"] = {id: "1", firstName: "firstName"};
+ expect(state).toEqual(newState);
+
+ response = {
+ content: [
+ {
+ id: "1",
+ firstName: "firstName"
+ },
+ {
+ lastName: "lastName"
+ }
+ ]
+ } as IAPIPaginationResponse<Partial<IContactEntity>>;
+ action = setContactsSearchAction({results: response});
+ state = contactEntitiesReducer(initialState, action);
+ newState = {};
+ newState["1"] = {id: "1", firstName: "firstName"};
+ expect(state).toEqual(newState);
+ });
+
+});
diff --git a/src/app/store/contacts/reducers/entities/contact-entities.reducer.ts b/src/app/store/contacts/reducers/entities/contact-entities.reducer.ts
new file mode 100644
index 0000000..6574386
--- /dev/null
+++ b/src/app/store/contacts/reducers/entities/contact-entities.reducer.ts
@@ -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
+ ********************************************************************************/
+
+import {createReducer, on} from "@ngrx/store";
+import {TStoreEntities, updateEntitiesObject} from "../../../../util";
+import {setContactEntityAction, setContactsSearchAction} from "../../actions";
+import {IContactEntity} from "../../model";
+
+export const contactEntitiesReducer = createReducer<TStoreEntities<IContactEntity>>(
+ {},
+ on(setContactEntityAction, (state, payload) => {
+ return updateEntitiesObject(state, [payload.entity], (item) => item.id);
+ }),
+ on(setContactsSearchAction, (state, payload) => {
+ return updateEntitiesObject(state, payload.results?.content, (item) => item.id);
+ })
+);
diff --git a/src/app/store/contacts/reducers/index.ts b/src/app/store/contacts/reducers/index.ts
new file mode 100644
index 0000000..8217651
--- /dev/null
+++ b/src/app/store/contacts/reducers/index.ts
@@ -0,0 +1,16 @@
+/********************************************************************************
+ * 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 "./loading/contacts-loading.reducer";
+export * from "./search/contacts-search.reducer";
+export * from "./entities/contact-entities.reducer";
diff --git a/src/app/store/contacts/reducers/loading/contacts-loading.reducer.spec.ts b/src/app/store/contacts/reducers/loading/contacts-loading.reducer.spec.ts
new file mode 100644
index 0000000..15e3582
--- /dev/null
+++ b/src/app/store/contacts/reducers/loading/contacts-loading.reducer.spec.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
+ ********************************************************************************/
+
+import {setContactsLoadingState} from "../../actions";
+import {IContactsLoadingState} from "../../model";
+import {contactsLoadingReducer} from "./contacts-loading.reducer";
+
+describe("contactsLoadingReducer", () => {
+
+ it("should override loading/fetching in the state", () => {
+ let initialState: IContactsLoadingState = {searching: true, fetching: false};
+ let action = setContactsLoadingState({state: {}});
+ let state = contactsLoadingReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ action = setContactsLoadingState({state: {searching: false}});
+ state = contactsLoadingReducer(initialState, action);
+ expect(state).toEqual({searching: false, fetching: false});
+
+ action = setContactsLoadingState({state: {fetching: null}});
+ state = contactsLoadingReducer(initialState, action);
+ expect(state).toEqual({searching: true, fetching: null});
+
+ initialState = {};
+ action = setContactsLoadingState({state: {searching: false, fetching: true}});
+ state = contactsLoadingReducer(initialState, action);
+ expect(state).toEqual({searching: false, fetching: true});
+ });
+});
diff --git a/src/app/store/contacts/reducers/loading/contacts-loading.reducer.ts b/src/app/store/contacts/reducers/loading/contacts-loading.reducer.ts
new file mode 100644
index 0000000..d27f8a2
--- /dev/null
+++ b/src/app/store/contacts/reducers/loading/contacts-loading.reducer.ts
@@ -0,0 +1,26 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {createReducer, on} from "@ngrx/store";
+import {setContactsLoadingState} from "../../actions";
+import {IContactsLoadingState} from "../../model";
+
+export const contactsLoadingReducer = createReducer<IContactsLoadingState>(
+ {},
+ on(setContactsLoadingState, (state, payload) => {
+ return {
+ ...state,
+ ...payload.state
+ };
+ })
+);
diff --git a/src/app/store/contacts/reducers/search/contacts-search.reducer.spec.ts b/src/app/store/contacts/reducers/search/contacts-search.reducer.spec.ts
new file mode 100644
index 0000000..c1da4f1
--- /dev/null
+++ b/src/app/store/contacts/reducers/search/contacts-search.reducer.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 {IAPIPaginationResponse} from "../../../../core/api/shared";
+import {setContactsSearchAction} from "../../actions";
+import {IContactEntity} from "../../model";
+import {contactsSearchReducer} from "./contacts-search.reducer";
+
+
+describe("contactsSearchReducer", () => {
+
+ it("should set the list of ids from the response to the state content", () => {
+ const initialState: IAPIPaginationResponse<string> = undefined;
+ let action = setContactsSearchAction(undefined);
+ let state = contactsSearchReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ action = setContactsSearchAction({results: null});
+ state = contactsSearchReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ let response = {
+ content: [
+ {
+ firstName: "firstName"
+ },
+ {
+ firstName: "firstName"
+ }
+ ]
+ } as IAPIPaginationResponse<Partial<IContactEntity>>;
+ action = setContactsSearchAction({results: response});
+ state = contactsSearchReducer(initialState, action);
+ let expectedState = {content: []} as IAPIPaginationResponse<string>;
+ expect(state).toEqual(expectedState);
+
+ response = {
+ content: [
+ {
+ id: "1",
+ firstName: "firstName"
+ },
+ {
+ id: "2",
+ lastName: "lastName"
+ }
+ ]
+ } as IAPIPaginationResponse<Partial<IContactEntity>>;
+ action = setContactsSearchAction({results: response});
+ state = contactsSearchReducer(initialState, action);
+ expectedState = {content: ["1", "2"]} as IAPIPaginationResponse<string>;
+ expect(state).toEqual(expectedState);
+ });
+});
+
+
+
+
diff --git a/src/app/store/contacts/reducers/search/contacts-search.reducer.ts b/src/app/store/contacts/reducers/search/contacts-search.reducer.ts
new file mode 100644
index 0000000..32f6c3d
--- /dev/null
+++ b/src/app/store/contacts/reducers/search/contacts-search.reducer.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 {createReducer, on} from "@ngrx/store";
+import {IAPIPaginationResponse} from "../../../../core/api/shared";
+import {arrayJoin} from "../../../../util/store";
+import {setContactsSearchAction} from "../../actions";
+
+export const contactsSearchReducer = createReducer<IAPIPaginationResponse<string>>(
+ undefined,
+ on(setContactsSearchAction, (state, payload) => {
+ return payload.results == null ? undefined : {
+ ...payload.results,
+ content: arrayJoin(payload.results.content)
+ .filter((_) => _.id != null)
+ .map((_) => _.id)
+ };
+ })
+);
diff --git a/src/app/store/contacts/selectors/contacts.selectors.ts b/src/app/store/contacts/selectors/contacts.selectors.ts
new file mode 100644
index 0000000..fb6237c
--- /dev/null
+++ b/src/app/store/contacts/selectors/contacts.selectors.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 {createFeatureSelector, createSelector} from "@ngrx/store";
+import {arrayJoin} from "../../../util/store";
+import {CONTACTS_NAME} from "../contacts-reducers.token";
+import {IContactEntity, IContactsStoreState} from "../model";
+
+export const contactsStateSelector = createFeatureSelector<IContactsStoreState>(CONTACTS_NAME);
+
+export const contactEntitiesSelector = createSelector(
+ contactsStateSelector,
+ (state) => ({...state?.entities})
+);
+
+export const getContactSearchSelector = createSelector(
+ contactsStateSelector,
+ (state) => state?.search
+);
+
+export const getContactSearchContentSelector = createSelector(
+ contactEntitiesSelector,
+ getContactSearchSelector,
+ (entities, search) => {
+ return arrayJoin(search?.content).map((id) => entities[id]);
+ }
+);
+
+export const getContactDetailsSelector = createSelector(
+ contactEntitiesSelector,
+ (entities, props: { id: string }): IContactEntity => entities[props.id]
+);
+
+export const getContactLoadingSelector = createSelector(
+ contactsStateSelector,
+ (state) => state?.loading
+);
diff --git a/src/app/store/contacts/selectors/index.ts b/src/app/store/contacts/selectors/index.ts
new file mode 100644
index 0000000..c9da8d7
--- /dev/null
+++ b/src/app/store/contacts/selectors/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./contacts.selectors";
diff --git a/src/app/store/index.ts b/src/app/store/index.ts
index 23be021..0044745 100644
--- a/src/app/store/index.ts
+++ b/src/app/store/index.ts
@@ -11,9 +11,11 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+export * from "./attachments";
+export * from "./contacts";
+export * from "./process";
export * from "./root";
export * from "./settings";
export * from "./statements";
-export * from "./process";
export * from "./app-store.module";
diff --git a/src/app/store/process/actions/process.actions.ts b/src/app/store/process/actions/process.actions.ts
index dafee0a..b7dd725 100644
--- a/src/app/store/process/actions/process.actions.ts
+++ b/src/app/store/process/actions/process.actions.ts
@@ -11,8 +11,8 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {createAction, props} from "@ngrx/store";
-import {IAPIProcessObject, IAPIProcessTask, IAPIStatementHistory} from "../../../core/api/process";
+import {Action, createAction, props} from "@ngrx/store";
+import {IAPIProcessObject, IAPIProcessTask, IAPIStatementHistory} from "../../../core";
export const claimTaskAction = createAction(
"[Details] Claim task",
@@ -21,7 +21,13 @@
export const completeTaskAction = createAction(
"[Edit] Complete task",
- props<{ statementId: number; taskId: string, variables: IAPIProcessObject }>()
+ props<{
+ statementId: number;
+ taskId: string,
+ variables: IAPIProcessObject,
+ claimNext?: boolean,
+ endWith?: Action[]
+ }>()
);
diff --git a/src/app/store/process/effects/claim-task.effect.ts b/src/app/store/process/effects/claim-task.effect.ts
new file mode 100644
index 0000000..86cb700
--- /dev/null
+++ b/src/app/store/process/effects/claim-task.effect.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 {Injectable} from "@angular/core";
+import {Router} from "@angular/router";
+import {Actions, createEffect, ofType} from "@ngrx/effects";
+import {Action} from "@ngrx/store";
+import {Observable} from "rxjs";
+import {catchError, filter, map, switchMap} from "rxjs/operators";
+import {ProcessApiService} from "../../../core/api/process";
+import {claimTaskAction, deleteTaskAction, updateTaskAction} from "../actions";
+
+@Injectable({providedIn: "root"})
+export class ClaimTaskEffect {
+
+ public readonly editTask$ = createEffect(() => this.actions$.pipe(
+ ofType(claimTaskAction),
+ filter((action) => typeof action.statementId === "number" && typeof action.taskId === "string"),
+ switchMap((action) => this.editTask(action.statementId, action.taskId, action.options))
+ ));
+
+ public constructor(
+ private readonly actions$: Actions,
+ private readonly processApiService: ProcessApiService,
+ private readonly router: Router
+ ) {
+
+ }
+
+ public editTask(statementId: number, taskId: string, queryParams?: any): Observable<Action> {
+ queryParams = {...queryParams, id: statementId, taskId};
+ return this.processApiService.claimStatementTask(statementId, taskId).pipe(
+ map((task) => updateTaskAction({task})),
+ catchError(async () => deleteTaskAction({statementId, taskId})),
+ switchMap(async (action) => {
+ await this.router.navigate(["/edit"], {queryParams});
+ return action;
+ }),
+ );
+ }
+
+}
diff --git a/src/app/store/process/effects/complete-task.effect.ts b/src/app/store/process/effects/complete-task.effect.ts
index dc72960..2784052 100644
--- a/src/app/store/process/effects/complete-task.effect.ts
+++ b/src/app/store/process/effects/complete-task.effect.ts
@@ -12,12 +12,15 @@
********************************************************************************/
import {Injectable} from "@angular/core";
-import {Router} from "@angular/router";
+import {Params, Router} from "@angular/router";
import {Actions, createEffect, ofType} from "@ngrx/effects";
import {Action} from "@ngrx/store";
-import {Observable} from "rxjs";
-import {exhaustMap, filter, switchMap} from "rxjs/operators";
-import {IAPIProcessObject, ProcessApiService} from "../../../core/api/process";
+import {concat, defer, EMPTY, Observable, of} from "rxjs";
+import {exhaustMap, filter, map, switchMap} from "rxjs/operators";
+import {EAPIProcessTaskDefinitionKey} from "../../../core/api";
+import {IAPIProcessObject, IAPIProcessTask, ProcessApiService} from "../../../core/api/process";
+import {ignoreError} from "../../../util/rxjs";
+import {arrayJoin} from "../../../util/store";
import {completeTaskAction, deleteTaskAction} from "../actions";
@Injectable({providedIn: "root"})
@@ -26,7 +29,7 @@
public readonly completeTask$ = createEffect(() => this.actions$.pipe(
ofType(completeTaskAction),
filter((action) => typeof action.statementId === "number" && typeof action.taskId === "string"),
- exhaustMap((action) => this.completeTask(action.statementId, action.taskId, action.variables))
+ exhaustMap((action) => this.completeTask(action.statementId, action.taskId, action.variables, action.claimNext, action.endWith))
));
public constructor(
@@ -37,14 +40,53 @@
}
- public completeTask(statementId: number, taskId: string, variables: IAPIProcessObject): Observable<Action> {
- const queryParams = {id: statementId};
- return this.processApiService.completeStatementTask(statementId, taskId, variables).pipe(
- switchMap(async () => {
- await this.router.navigate(["/details"], {queryParams});
- return deleteTaskAction({statementId, taskId});
+ public completeTask(
+ statementId: number,
+ taskId: string,
+ variables: IAPIProcessObject,
+ claimNext?: boolean | EAPIProcessTaskDefinitionKey,
+ endWithActions?: Action[]
+ ): Observable<Action> {
+ return concat(
+ this.processApiService.completeStatementTask(statementId, taskId, variables).pipe(
+ switchMap(() => {
+ endWithActions = arrayJoin([deleteTaskAction({statementId, taskId})], endWithActions);
+ return claimNext == null ? of<IAPIProcessTask>(null) : this.claimNextTask(statementId);
+ }),
+ switchMap((task) => {
+ return this.navigateToStatement(statementId, task?.taskId);
+ }),
+ switchMap(() => EMPTY),
+ ignoreError()
+ ),
+ defer(() => of(...endWithActions))
+ );
+ }
+
+ public claimNextTask(statementId: number, key?: EAPIProcessTaskDefinitionKey): Observable<IAPIProcessTask> {
+ return this.processApiService.getStatementTasks(statementId).pipe(
+ map((taskList) => {
+ return taskList
+ .filter((task) => task?.taskId != null)
+ .filter((task) => key == null || task.taskDefinitionKey === key)[0];
+ }),
+ switchMap((task) => {
+ return task == null ? of(task) : this.processApiService.claimStatementTask(statementId, task.taskId);
})
);
}
+ public navigateToStatement(statementId: number, taskId?: string, queryParams: Params = {}): Observable<boolean> {
+ return defer(() => {
+ const route = statementId == null ? "/" : taskId == null ? "/details" : "/edit";
+ if (statementId != null) {
+ queryParams.id = statementId;
+ if (taskId != null) {
+ queryParams.taskId = taskId;
+ }
+ }
+ return this.router.navigate([route], {queryParams});
+ });
+ }
+
}
diff --git a/src/app/store/process/effects/index.ts b/src/app/store/process/effects/index.ts
index fdd3b2d..7ea19bf 100644
--- a/src/app/store/process/effects/index.ts
+++ b/src/app/store/process/effects/index.ts
@@ -12,4 +12,4 @@
********************************************************************************/
export * from "./complete-task.effect";
-export * from "./edit-task.effect";
+export * from "./claim-task.effect";
diff --git a/src/app/store/process/index.ts b/src/app/store/process/index.ts
index 4ce5cd3..d75a910 100644
--- a/src/app/store/process/index.ts
+++ b/src/app/store/process/index.ts
@@ -12,6 +12,7 @@
********************************************************************************/
export * from "./actions";
+export * from "./effects";
export * from "./model";
export * from "./selectors";
export * from "./process-store.module";
diff --git a/src/app/store/process/process-store.module.ts b/src/app/store/process/process-store.module.ts
index c02b6b5..5ee81a7 100644
--- a/src/app/store/process/process-store.module.ts
+++ b/src/app/store/process/process-store.module.ts
@@ -14,14 +14,14 @@
import {NgModule} from "@angular/core";
import {EffectsModule} from "@ngrx/effects";
import {StoreModule} from "@ngrx/store";
-import {CompleteTaskEffect, EditTaskEffect} from "./effects";
+import {ClaimTaskEffect, CompleteTaskEffect} from "./effects";
import {PROCESS_FEATURE_NAME, PROCESS_REDUCER} from "./process-reducers.token";
@NgModule({
imports: [
StoreModule.forFeature(PROCESS_FEATURE_NAME, PROCESS_REDUCER),
EffectsModule.forFeature([
- EditTaskEffect,
+ ClaimTaskEffect,
CompleteTaskEffect
])
]
diff --git a/src/app/store/process/reducers/diagram.reducer.spec.ts b/src/app/store/process/reducers/diagram.reducer.spec.ts
new file mode 100644
index 0000000..0ddc191
--- /dev/null
+++ b/src/app/store/process/reducers/diagram.reducer.spec.ts
@@ -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 {TStoreEntities} from "../../../util/store";
+import {setDiagramAction} from "../actions";
+import {diagramReducer} from "./diagram.reducer";
+
+describe("diagramReducer", () => {
+
+ it("should return the previous state if not supplied with a statementid", () => {
+ const initialState: TStoreEntities<string> = {};
+ let action = setDiagramAction(undefined);
+ let state = diagramReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ action = setDiagramAction({statementId: null, diagram: "string"});
+ state = diagramReducer(initialState, action);
+ expect(state).toEqual(initialState);
+ });
+
+ it("should set the diagram for the given statementid", () => {
+ let initialState: TStoreEntities<string> = {};
+ let action = setDiagramAction({statementId: 1, diagram: "string"});
+ let state = diagramReducer(initialState, action);
+ expect(state).toEqual({1: "string"});
+
+ initialState = state;
+ action = setDiagramAction({statementId: 2, diagram: "string2"});
+ state = diagramReducer(initialState, action);
+ expect(state).toEqual({1: "string", 2: "string2"});
+
+ initialState = state;
+ action = setDiagramAction({statementId: 2, diagram: "another string"});
+ state = diagramReducer(initialState, action);
+ expect(state).toEqual({1: "string", 2: "another string"});
+ });
+
+});
diff --git a/src/app/store/process/reducers/history.reducer.spec.ts b/src/app/store/process/reducers/history.reducer.spec.ts
new file mode 100644
index 0000000..7731074
--- /dev/null
+++ b/src/app/store/process/reducers/history.reducer.spec.ts
@@ -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
+ ********************************************************************************/
+
+import {IAPIStatementHistory} from "../../../core/api/process";
+import {TStoreEntities} from "../../../util/store";
+import {setHistoryAction} from "../actions";
+import {historyReducer} from "./history.reducer";
+
+describe("historyReducer", () => {
+
+ const history: IAPIStatementHistory = {
+ processName: "processName",
+ processVersion: 1,
+ finishedProcessActivities: [],
+ currentProcessActivities: []
+ };
+
+ it("should return the previous state if not supplied with a statementid", () => {
+ const initialState: TStoreEntities<IAPIStatementHistory> = {};
+ let action = setHistoryAction(undefined);
+ let state = historyReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ action = setHistoryAction({statementId: null, history});
+ state = historyReducer(initialState, action);
+ expect(state).toEqual(initialState);
+ });
+
+ it("should set the history data for the given statementid", () => {
+ let initialState: TStoreEntities<IAPIStatementHistory> = {};
+ let action = setHistoryAction({statementId: 1, history});
+ let state = historyReducer(initialState, action);
+ expect(state).toEqual({1: history});
+
+ initialState = state;
+ const anotherHistory = {...history, processName: "anotherProcessName"};
+ action = setHistoryAction({statementId: 2, history: anotherHistory});
+ state = historyReducer(initialState, action);
+ expect(state).toEqual({1: history, 2: anotherHistory});
+
+ initialState = state;
+ action = setHistoryAction({statementId: 2, history});
+ state = historyReducer(initialState, action);
+ expect(state).toEqual({1: history, 2: history});
+ });
+
+});
diff --git a/src/app/store/process/reducers/statement-tasks.reducer.spec.ts b/src/app/store/process/reducers/statement-tasks.reducer.spec.ts
new file mode 100644
index 0000000..7cba67d
--- /dev/null
+++ b/src/app/store/process/reducers/statement-tasks.reducer.spec.ts
@@ -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
+ ********************************************************************************/
+
+import {IAPIProcessTask} from "../../../core/api/process";
+import {TStoreEntities} from "../../../util/store";
+import {deleteTaskAction, setTasksAction} from "../actions";
+import {statementTaskReducer} from "./statement-tasks.reducer";
+
+describe("statementTaskReducer", () => {
+
+ it("should set the taskids to the given statementid to the state", () => {
+
+ const actionPayload = {statementId: "1"} as unknown as { statementId: number, tasks: IAPIProcessTask[] };
+ let initialState: TStoreEntities<string[]> = {};
+ let action = setTasksAction(actionPayload);
+ let state = statementTaskReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ action = setTasksAction(undefined);
+ state = statementTaskReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ actionPayload.statementId = 1;
+ actionPayload.tasks = [
+ {
+ statementId: 1,
+ taskId: "taskId"
+ },
+ {
+ statementId: 2,
+ taskId: "taskId2"
+ }
+ ] as IAPIProcessTask[];
+ action = setTasksAction(actionPayload);
+ state = statementTaskReducer(initialState, action);
+ expect(state).toEqual({1: ["taskId"]});
+
+ initialState = state;
+ actionPayload.statementId = 2;
+ action = setTasksAction(actionPayload);
+ state = statementTaskReducer(initialState, action);
+ expect(state).toEqual({1: ["taskId"], 2: ["taskId2"]});
+
+ initialState = {};
+ actionPayload.statementId = 1;
+ actionPayload.tasks = {} as IAPIProcessTask[];
+ action = setTasksAction(actionPayload);
+ state = statementTaskReducer(initialState, action);
+ expect(state).toEqual({1: undefined});
+ });
+
+ it("should delete the taskid for the given statementid from the state", () => {
+
+ const actionPayload = {statementId: "1", taskId: "taskId"} as unknown as { statementId: number, taskId: string };
+ let initialState: TStoreEntities<string[]> = {};
+ let action = deleteTaskAction(actionPayload);
+ let state = statementTaskReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ actionPayload.statementId = 1;
+ actionPayload.taskId = {} as string;
+ action = deleteTaskAction(actionPayload);
+ state = statementTaskReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ initialState = {1: ["taskId"], 2: ["taskId2"]};
+ actionPayload.statementId = 1;
+ actionPayload.taskId = "taskId";
+ action = deleteTaskAction(actionPayload);
+ state = statementTaskReducer(initialState, action);
+ expect(state).toEqual({1: undefined, 2: ["taskId2"]});
+
+ initialState = state;
+ actionPayload.statementId = 2;
+ actionPayload.taskId = "taskId2";
+ action = deleteTaskAction(actionPayload);
+ state = statementTaskReducer(initialState, action);
+ expect(state).toEqual({1: undefined, 2: undefined});
+
+ initialState = {1: ["taskId"], 2: ["taskId2"]};
+ actionPayload.statementId = 1;
+ actionPayload.taskId = "wrongTaskId";
+ action = deleteTaskAction(actionPayload);
+ state = statementTaskReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ initialState = {1: ["taskId"], 2: ["taskId2"]};
+ actionPayload.statementId = 3;
+ actionPayload.taskId = "taskId";
+ action = deleteTaskAction(actionPayload);
+ state = statementTaskReducer(initialState, action);
+ expect(state).toEqual({...initialState, 3: undefined});
+ });
+
+});
diff --git a/src/app/store/process/reducers/tasks.reducer.spec.ts b/src/app/store/process/reducers/tasks.reducer.spec.ts
new file mode 100644
index 0000000..ffc41ab
--- /dev/null
+++ b/src/app/store/process/reducers/tasks.reducer.spec.ts
@@ -0,0 +1,144 @@
+/********************************************************************************
+ * 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 {EAPIProcessTaskDefinitionKey, IAPIProcessTask} from "../../../core/api/process";
+import {TStoreEntities} from "../../../util/store";
+import {deleteTaskAction, setTasksAction, updateTaskAction} from "../actions";
+import {tasksReducer} from "./tasks.reducer";
+
+describe("tasksReducer", () => {
+
+ it("should set the tasks to the given statementid", () => {
+
+ const actionPayload = {statementId: "1"} as unknown as { statementId: number, tasks: IAPIProcessTask[] };
+ let initialState: TStoreEntities<IAPIProcessTask> = {};
+ let action = setTasksAction(actionPayload);
+ let state = tasksReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ action = setTasksAction(undefined);
+ state = tasksReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ actionPayload.statementId = 1;
+ actionPayload.tasks = [
+ {
+ statementId: 1,
+ taskId: "taskId",
+ taskDefinitionKey: EAPIProcessTaskDefinitionKey.ADD_BASIC_INFO_DATA,
+ processDefinitionKey: "processDefinitionKey",
+ assignee: "assignee",
+ requiredVariables: {}
+ },
+ {
+ statementId: 2,
+ taskId: "taskId2",
+ taskDefinitionKey: EAPIProcessTaskDefinitionKey.ADD_BASIC_INFO_DATA,
+ processDefinitionKey: "processDefinitionKey",
+ assignee: "assignee",
+ requiredVariables: {}
+ }
+ ] as IAPIProcessTask[];
+ action = setTasksAction(actionPayload);
+ state = tasksReducer(initialState, action);
+ expect(state).toEqual({taskId: actionPayload.tasks[0]});
+
+ initialState = state;
+ actionPayload.statementId = 2;
+ action = setTasksAction(actionPayload);
+ state = tasksReducer(initialState, action);
+ expect(state).toEqual({taskId: actionPayload.tasks[0], taskId2: actionPayload.tasks[1]});
+
+ initialState = state;
+ actionPayload.statementId = 3;
+ action = setTasksAction(actionPayload);
+ state = tasksReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ initialState = state;
+ actionPayload.statementId = 1;
+ actionPayload.tasks[0].taskId = "changedTaskId";
+ action = setTasksAction(actionPayload);
+ state = tasksReducer(initialState, action);
+ expect(state).toEqual({changedTaskId: actionPayload.tasks[0], taskId2: actionPayload.tasks[1]});
+ });
+
+ it("should delete the task for the given taskId from the state", () => {
+
+ const actionPayload = {statementId: "1", taskId: "taskId"} as unknown as { statementId: number, taskId: string };
+ const initialState: TStoreEntities<IAPIProcessTask> = {
+ taskId: {
+ statementId: 1,
+ taskId: "taskId",
+ taskDefinitionKey: EAPIProcessTaskDefinitionKey.ADD_BASIC_INFO_DATA,
+ processDefinitionKey: "processDefinitionKey",
+ assignee: "assignee",
+ requiredVariables: {}
+ }
+ };
+
+ let action = deleteTaskAction(actionPayload);
+ let state = tasksReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ actionPayload.statementId = 1;
+ actionPayload.taskId = {} as string;
+ action = deleteTaskAction(actionPayload);
+ state = tasksReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ actionPayload.statementId = 1;
+ actionPayload.taskId = "taskId";
+ action = deleteTaskAction(actionPayload);
+ state = tasksReducer(initialState, action);
+ expect(state).toEqual({});
+ });
+
+ it("should update the given task in the state", () => {
+ const actionPayload = {task: null} as unknown as { task: IAPIProcessTask };
+ const initialState: TStoreEntities<IAPIProcessTask> = {
+ taskId: {
+ statementId: 1,
+ taskId: "taskId",
+ taskDefinitionKey: EAPIProcessTaskDefinitionKey.ADD_BASIC_INFO_DATA,
+ processDefinitionKey: "processDefinitionKey",
+ assignee: "assignee",
+ requiredVariables: {}
+ }
+ };
+
+ let action = updateTaskAction(actionPayload);
+ let state = tasksReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ actionPayload.task = {taskId: "unknownTaskId"} as IAPIProcessTask;
+ action = updateTaskAction(actionPayload);
+ state = tasksReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ actionPayload.task = {taskId: "taskId", assignee: "changedValue"} as IAPIProcessTask;
+ action = updateTaskAction(actionPayload);
+ state = tasksReducer(initialState, action);
+ expect(state).toEqual({
+ taskId: {
+ statementId: 1,
+ taskId: "taskId",
+ taskDefinitionKey: EAPIProcessTaskDefinitionKey.ADD_BASIC_INFO_DATA,
+ processDefinitionKey: "processDefinitionKey",
+ assignee: "changedValue",
+ requiredVariables: {}
+ }
+ });
+ });
+
+});
diff --git a/src/app/store/root/actions/root.actions.ts b/src/app/store/root/actions/root.actions.ts
index 5216cf2..966b185 100644
--- a/src/app/store/root/actions/root.actions.ts
+++ b/src/app/store/root/actions/root.actions.ts
@@ -64,3 +64,12 @@
"[Router] Set query params",
props<{ queryParams: TStoreEntities<any> }>()
);
+
+export const openContactDataBaseAction = createAction(
+ "[Details/Edit] Open contact data base"
+);
+
+export const openAttachmentAction = createAction(
+ "[Details/Edit] Open attachment in new tab",
+ props<{ statementId: number, attachmentId: number }>()
+);
diff --git a/src/app/store/root/effects/index.ts b/src/app/store/root/effects/index.ts
index 3a4235a..a792a0f 100644
--- a/src/app/store/root/effects/index.ts
+++ b/src/app/store/root/effects/index.ts
@@ -13,6 +13,7 @@
export * from "./initialization.effect";
export * from "./keep-alive.effect";
+export * from "./open-new-tab.effect";
export * from "./router.effects";
export * from "./user.effect";
export * from "./version.effect";
diff --git a/src/app/store/root/effects/open-new-tab.effect.ts b/src/app/store/root/effects/open-new-tab.effect.ts
new file mode 100644
index 0000000..14a7e8e
--- /dev/null
+++ b/src/app/store/root/effects/open-new-tab.effect.ts
@@ -0,0 +1,60 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {Inject, Injectable} from "@angular/core";
+import {Actions, createEffect, ofType} from "@ngrx/effects";
+import {filter, switchMap} from "rxjs/operators";
+import {AuthService} from "../../../core/auth";
+import {WINDOW} from "../../../core/dom";
+import {CONTACT_DATA_BASE_ROUTE, SPA_BACKEND_ROUTE} from "../../../core/external-routes";
+import {urlJoin} from "../../../util/http";
+import {openAttachmentAction, openContactDataBaseAction} from "../actions";
+
+@Injectable({providedIn: "root"})
+export class OpenNewTabEffect {
+
+ public openContactDataBase$ = createEffect(() => this.actions.pipe(
+ ofType(openContactDataBaseAction),
+ filter(() => this.authenticationService.token != null),
+ switchMap(() => this.open(this.contactDataBaseRoute))
+ ), {dispatch: false});
+
+ public openAttachment$ = createEffect(() => this.actions.pipe(
+ ofType(openAttachmentAction),
+ filter(() => this.authenticationService.token != null),
+ filter((action) => action.statementId != null && action.attachmentId != null),
+ switchMap((action) => this.openAttachment(action.statementId, action.attachmentId))
+ ), {dispatch: false});
+
+ public constructor(
+ public actions: Actions,
+ private readonly authenticationService: AuthService,
+ @Inject(SPA_BACKEND_ROUTE) private readonly spaBackendRoute: string,
+ @Inject(CONTACT_DATA_BASE_ROUTE) private readonly contactDataBaseRoute: string,
+ @Inject(WINDOW) private readonly window: Window
+ ) {
+
+ }
+
+ public async openAttachment(statementId: number, attachmentId: number) {
+ const endPoint = `/statements/${statementId}/attachments/${attachmentId}/file`;
+ return this.open(urlJoin(this.spaBackendRoute, endPoint));
+ }
+
+ public async open(url: string) {
+ url += "?accessToken=" + this.authenticationService.token;
+ this.window.open(url, "_blank");
+ }
+
+
+}
diff --git a/src/app/store/root/reducers/exit-code.reducer.spec.ts b/src/app/store/root/reducers/exit-code.reducer.spec.ts
new file mode 100644
index 0000000..2275023
--- /dev/null
+++ b/src/app/store/root/reducers/exit-code.reducer.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 {openExitPageAction} from "../actions";
+import {EExitCode} from "../model";
+import {exitCodeReducer} from "./exit-code.reducer";
+
+describe("exitCodeReducer", () => {
+
+ it("should set the payload code as state", () => {
+ let initialState: EExitCode;
+ let action = openExitPageAction({code: EExitCode.FORBIDDEN});
+ let state = exitCodeReducer(initialState, action);
+ expect(state).toEqual(EExitCode.FORBIDDEN);
+
+ initialState = EExitCode.LOGOUT;
+ action = openExitPageAction(undefined);
+ state = exitCodeReducer(initialState, action);
+ expect(state).toEqual(undefined);
+
+ initialState = EExitCode.NO_TOKEN;
+ action = openExitPageAction({code: null});
+ state = exitCodeReducer(initialState, action);
+ expect(state).toEqual(null);
+ });
+
+});
diff --git a/src/app/store/root/reducers/exit-code.reducer.ts b/src/app/store/root/reducers/exit-code.reducer.ts
index 6d70780..609b0b9 100644
--- a/src/app/store/root/reducers/exit-code.reducer.ts
+++ b/src/app/store/root/reducers/exit-code.reducer.ts
@@ -13,8 +13,9 @@
import {createReducer, on} from "@ngrx/store";
import {openExitPageAction} from "../actions";
+import {EExitCode} from "../model";
-export const exitCodeReducer = createReducer(
+export const exitCodeReducer = createReducer<EExitCode>(
undefined,
on(openExitPageAction, (state, payload) => payload.code)
);
diff --git a/src/app/store/root/reducers/is-loading.reducer.spec.ts b/src/app/store/root/reducers/is-loading.reducer.spec.ts
new file mode 100644
index 0000000..7c87d07
--- /dev/null
+++ b/src/app/store/root/reducers/is-loading.reducer.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 {toggleLoadingPageAction} from "../actions";
+import {isLoadingReducer} from "./is-loading.reducer";
+
+describe("isLoadingReducer", () => {
+
+ it("should set the isLoading state to the given value, false for no value", () => {
+ let initialState = true;
+ let action = toggleLoadingPageAction({isLoading: false});
+ let state = isLoadingReducer(initialState, action);
+ expect(state).toBeFalse();
+
+ initialState = true;
+ action = toggleLoadingPageAction({});
+ state = isLoadingReducer(initialState, action);
+ expect(state).toBeFalse();
+
+ initialState = true;
+ action = toggleLoadingPageAction(null);
+ state = isLoadingReducer(initialState, action);
+ expect(state).toBeFalse();
+
+ initialState = true;
+ action = toggleLoadingPageAction({isLoading: null});
+ state = isLoadingReducer(initialState, action);
+ expect(state).toBeFalse();
+
+ initialState = false;
+ action = toggleLoadingPageAction({isLoading: true});
+ state = isLoadingReducer(initialState, action);
+ expect(state).toBeTrue();
+ });
+
+});
diff --git a/src/app/store/root/reducers/is-loading.reducer.ts b/src/app/store/root/reducers/is-loading.reducer.ts
index 6d321ec..b505997 100644
--- a/src/app/store/root/reducers/is-loading.reducer.ts
+++ b/src/app/store/root/reducers/is-loading.reducer.ts
@@ -14,7 +14,7 @@
import {createReducer, on} from "@ngrx/store";
import {toggleLoadingPageAction} from "../actions";
-export const isLoadingReducer = createReducer(
+export const isLoadingReducer = createReducer<boolean>(
true,
- on(toggleLoadingPageAction, (state, payload) => payload != null && payload.isLoading)
+ on(toggleLoadingPageAction, (state, payload) => payload != null && !!payload.isLoading)
);
diff --git a/src/app/store/root/reducers/query-params.reducer.spec.ts b/src/app/store/root/reducers/query-params.reducer.spec.ts
new file mode 100644
index 0000000..f2fd629
--- /dev/null
+++ b/src/app/store/root/reducers/query-params.reducer.spec.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
+ ********************************************************************************/
+
+import {Params} from "@angular/router";
+import {setQueryParamsAction} from "../actions";
+import {queryParamsReducer} from "./query-params.reducer";
+
+describe("queryParamsReducer", () => {
+
+ it("should add all query parameters to the state object", () => {
+ const initialState: Params = {};
+ let action = setQueryParamsAction(undefined);
+ let state = queryParamsReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ action = setQueryParamsAction({
+ queryParams: {
+ param1: "param1value",
+ param2: "param2value"
+ }
+ });
+ state = queryParamsReducer(initialState, action);
+ expect(state).toEqual({
+ param1: "param1value",
+ param2: "param2value"
+ });
+ });
+
+});
diff --git a/src/app/store/root/reducers/user.reducer.spec.ts b/src/app/store/root/reducers/user.reducer.spec.ts
new file mode 100644
index 0000000..ae22fd1
--- /dev/null
+++ b/src/app/store/root/reducers/user.reducer.spec.ts
@@ -0,0 +1,62 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {Action} from "@ngrx/store";
+import {clearUserAction, setUserAction} from "../actions";
+import {ESPAUserRoles} from "../model";
+import {userReducer} from "./user.reducer";
+
+describe("userReducer", () => {
+
+ it("should set the user information to the state", () => {
+ let initialState;
+ let action: Action = setUserAction(null);
+ let state = userReducer(initialState, action);
+ expect(state).toEqual({firstName: undefined, lastName: undefined, roles: []});
+
+ action = setUserAction({
+ firstName: "firstName",
+ lastName: "lastName",
+ roles: [
+ ESPAUserRoles.DIVISION_MEMBER,
+ ESPAUserRoles.ROLE_SPA_ACCESS
+ ]
+ });
+ state = userReducer(initialState, action);
+ expect(state).toEqual({
+ firstName: "firstName",
+ lastName: "lastName",
+ roles: [
+ ESPAUserRoles.DIVISION_MEMBER,
+ ESPAUserRoles.ROLE_SPA_ACCESS
+ ]
+ });
+
+ action = clearUserAction();
+ state = userReducer(initialState, action);
+ expect(state).toEqual(undefined);
+
+ initialState = {
+ firstName: "firstName",
+ lastName: "lastName",
+ roles: [
+ ESPAUserRoles.DIVISION_MEMBER,
+ ESPAUserRoles.ROLE_SPA_ACCESS
+ ]
+ };
+ action = clearUserAction();
+ state = userReducer(initialState, action);
+ expect(state).toEqual(undefined);
+ });
+
+});
diff --git a/src/app/store/root/reducers/version-back-end.reducer.spec.ts b/src/app/store/root/reducers/version-back-end.reducer.spec.ts
new file mode 100644
index 0000000..75569b1
--- /dev/null
+++ b/src/app/store/root/reducers/version-back-end.reducer.spec.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 {Action} from "@ngrx/store";
+import {setBackEndVersionAction} from "../actions";
+import {versionBackEndReducer} from "./version-back-end.reducer";
+
+describe("versionBackEndReducer", () => {
+
+ it("should set the version to the state", () => {
+ const initialState: string = undefined;
+ let action: Action = setBackEndVersionAction(null);
+ let state = versionBackEndReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ action = setBackEndVersionAction({buildVersion: "1.04", applicationName: "tob"});
+ state = versionBackEndReducer(initialState, action);
+ expect(state).toEqual("1.04");
+
+ action = setBackEndVersionAction({buildVersion: undefined, applicationName: undefined});
+ state = versionBackEndReducer(initialState, action);
+ expect(state).toEqual(undefined);
+ });
+
+});
diff --git a/src/app/store/root/reducers/version-back-end.reducer.ts b/src/app/store/root/reducers/version-back-end.reducer.ts
index 248ab5e..b0cbaa4 100644
--- a/src/app/store/root/reducers/version-back-end.reducer.ts
+++ b/src/app/store/root/reducers/version-back-end.reducer.ts
@@ -14,7 +14,7 @@
import {createReducer, on} from "@ngrx/store";
import {setBackEndVersionAction} from "../actions";
-export const versionBackEndReducer = createReducer(
+export const versionBackEndReducer = createReducer<string>(
undefined,
on(setBackEndVersionAction, (state, payload) => payload == null ? undefined : payload.buildVersion)
);
diff --git a/src/app/store/root/root-store.module.ts b/src/app/store/root/root-store.module.ts
index 96e5aec..ab44541 100644
--- a/src/app/store/root/root-store.module.ts
+++ b/src/app/store/root/root-store.module.ts
@@ -15,7 +15,7 @@
import {EffectsModule} from "@ngrx/effects";
import {Store, StoreModule} from "@ngrx/store";
import {intializeAction} from "./actions";
-import {InitializationEffect, KeepAliveEffect, RouterEffects, UserEffect, VersionEffect} from "./effects";
+import {InitializationEffect, KeepAliveEffect, OpenNewTabEffect, RouterEffects, UserEffect, VersionEffect} from "./effects";
import {ROOT_REDUCER} from "./root-reducers.token";
export function dispatchInitialization(store: Store) {
@@ -28,6 +28,7 @@
EffectsModule.forRoot([
InitializationEffect,
KeepAliveEffect,
+ OpenNewTabEffect,
RouterEffects,
UserEffect,
VersionEffect
diff --git a/src/app/store/settings/actions/settings.actions.ts b/src/app/store/settings/actions/settings.actions.ts
index d47c6ad..75b4845 100644
--- a/src/app/store/settings/actions/settings.actions.ts
+++ b/src/app/store/settings/actions/settings.actions.ts
@@ -13,8 +13,18 @@
import {createAction, props} from "@ngrx/store";
import {IAPIStatementType} from "../../../core";
+import {IAPISectorsModel} from "../../../core/api/statements/IAPISectorsModel";
+
+export const fetchSettingsAction = createAction(
+ "[New] Fetch settings"
+);
export const setStatementTypesAction = createAction(
"[API] Set statement types",
props<{ statementTypes: IAPIStatementType[] }>()
);
+
+export const setSectorsAction = createAction(
+ "[API] Get sectors",
+ props<{ sectors: IAPISectorsModel }>()
+);
diff --git a/src/app/store/settings/effects/fetch-settings.effect.ts b/src/app/store/settings/effects/fetch-settings.effect.ts
new file mode 100644
index 0000000..7cd6c44
--- /dev/null
+++ b/src/app/store/settings/effects/fetch-settings.effect.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 {Injectable} from "@angular/core";
+import {Actions, createEffect, ofType} from "@ngrx/effects";
+import {Action} from "@ngrx/store";
+import {concat, merge, Observable} from "rxjs";
+import {map, retry, switchMap} from "rxjs/operators";
+import {SettingsApiService} from "../../../core";
+import {ignoreError, retryAfter} from "../../../util";
+import {intializeAction} from "../../root/actions";
+import {fetchSettingsAction, setSectorsAction, setStatementTypesAction} from "../actions";
+
+@Injectable({providedIn: "root"})
+export class FetchSettingsEffect {
+
+ public initialize$ = createEffect(() => this.actions.pipe(
+ ofType(intializeAction),
+ switchMap(() => {
+ return concat(
+ this.fetchSettings().pipe(retryAfter(30000)),
+ this.actions.pipe(
+ ofType(fetchSettingsAction),
+ switchMap(() => this.fetchSettings())
+ )
+ );
+ })
+ ));
+
+ public constructor(private readonly actions: Actions, private readonly settingsApiService: SettingsApiService) {
+
+ }
+
+ public fetchSettings(): Observable<Action> {
+ return merge<Action>(
+ this.fetchStatementTypes(),
+ this.fetchSectors()
+ );
+ }
+
+ public fetchStatementTypes(): Observable<Action> {
+ return this.settingsApiService.getStatementTypes().pipe(
+ map((statementTypes) => setStatementTypesAction({statementTypes})),
+ retry(2),
+ ignoreError()
+ );
+ }
+
+ public fetchSectors(): Observable<Action> {
+ return this.settingsApiService.getSectors().pipe(
+ map((sectors) => setSectorsAction({sectors})),
+ retry(2),
+ ignoreError()
+ );
+ }
+
+}
diff --git a/src/app/store/settings/effects/index.ts b/src/app/store/settings/effects/index.ts
index 592ec09..8ea1eaa 100644
--- a/src/app/store/settings/effects/index.ts
+++ b/src/app/store/settings/effects/index.ts
@@ -11,4 +11,4 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./statement-types.effects";
+export * from "./fetch-settings.effect";
diff --git a/src/app/store/settings/model/ISettingsStoreState.ts b/src/app/store/settings/model/ISettingsStoreState.ts
index c084876..c9012de 100644
--- a/src/app/store/settings/model/ISettingsStoreState.ts
+++ b/src/app/store/settings/model/ISettingsStoreState.ts
@@ -12,6 +12,7 @@
********************************************************************************/
import {IAPIStatementType} from "../../../core";
+import {IAPISectorsModel} from "../../../core/api/statements/IAPISectorsModel";
import {TStoreEntities} from "../../../util";
export interface ISettingsStoreState {
@@ -21,4 +22,9 @@
*/
statementTypes: TStoreEntities<IAPIStatementType>;
+ /**
+ * Object all the available sectors for all newly created statements.
+ */
+ sectors: IAPISectorsModel;
+
}
diff --git a/src/app/store/settings/reducers/sectors.reducer.spec.ts b/src/app/store/settings/reducers/sectors.reducer.spec.ts
new file mode 100644
index 0000000..a228615
--- /dev/null
+++ b/src/app/store/settings/reducers/sectors.reducer.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 {IAPISectorsModel} from "../../../core/api/statements/IAPISectorsModel";
+import {setSectorsAction} from "../actions";
+import {sectorsReducer} from "./sectors.reducer";
+
+describe("sectorsReducer", () => {
+
+ it("should set the sectors object as state if provided", () => {
+ const initialState: IAPISectorsModel = undefined;
+ let action = setSectorsAction(undefined);
+ let state = sectorsReducer(initialState, action);
+ expect(state).toEqual({});
+
+ const sectors: IAPISectorsModel = {
+ "Ort#Ortsteil": [
+ "Strom", "Gas", "Beleuchtung"
+ ]
+ };
+ action = setSectorsAction({sectors});
+ state = sectorsReducer(initialState, action);
+ expect(state).toEqual(sectors);
+ });
+
+});
diff --git a/src/app/store/settings/reducers/sectors.reducer.ts b/src/app/store/settings/reducers/sectors.reducer.ts
new file mode 100644
index 0000000..53cc6cf
--- /dev/null
+++ b/src/app/store/settings/reducers/sectors.reducer.ts
@@ -0,0 +1,23 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {createReducer, on} from "@ngrx/store";
+import {IAPISectorsModel} from "../../../core/api/statements/IAPISectorsModel";
+import {setSectorsAction} from "../actions";
+
+export const sectorsReducer = createReducer<IAPISectorsModel>(
+ {},
+ on(setSectorsAction, (state, payload) => {
+ return payload.sectors ? {...payload.sectors} : state;
+ })
+);
diff --git a/src/app/store/settings/reducers/statement-types.reducer.spec.ts b/src/app/store/settings/reducers/statement-types.reducer.spec.ts
new file mode 100644
index 0000000..9143e32
--- /dev/null
+++ b/src/app/store/settings/reducers/statement-types.reducer.spec.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 {IAPIStatementType} from "../../../core/api/settings";
+import {TStoreEntities} from "../../../util/store";
+import {setStatementTypesAction} from "../actions";
+import {statementTypesReducer} from "./statement-types.reducer";
+
+describe("statementTypesReducer", () => {
+
+ it("should not change the state on setStatementTypesAction with no values given", () => {
+ const initialState: TStoreEntities<IAPIStatementType> = {};
+ let action = setStatementTypesAction(undefined);
+ let state = statementTypesReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ action = setStatementTypesAction({statementTypes: null});
+ state = statementTypesReducer(initialState, action);
+ expect(state).toEqual(initialState);
+
+ action = setStatementTypesAction({statementTypes: []});
+ state = statementTypesReducer(initialState, action);
+ expect(state).toEqual(initialState);
+ });
+
+ it("should add the statementtypes to the state object", () => {
+ const initialState: TStoreEntities<IAPIStatementType> = {};
+ const action = setStatementTypesAction({
+ statementTypes: [
+ {
+ id: 1,
+ name: "name"
+ },
+ {
+ id: 2,
+ name: "name2"
+ }
+ ]
+ });
+ const state = statementTypesReducer(initialState, action);
+ expect(state).toEqual({
+ 1: {
+ id: 1,
+ name: "name"
+ },
+ 2: {
+ id: 2,
+ name: "name2"
+ }
+ });
+ });
+
+});
diff --git a/src/app/store/settings/selectors/settings.selectors.ts b/src/app/store/settings/selectors/settings.selectors.ts
index 28b8d49..367f788 100644
--- a/src/app/store/settings/selectors/settings.selectors.ts
+++ b/src/app/store/settings/selectors/settings.selectors.ts
@@ -23,6 +23,6 @@
settingsStoreSelector,
(state) => {
return entitiesToArray(state?.statementTypes)
- .map<ISelectOption>((t) => ({label: t?.name, value: t?.id}));
+ .map<ISelectOption<number>>((t) => ({label: t?.name, value: t?.id}));
}
);
diff --git a/src/app/store/settings/settings-reducers.token.ts b/src/app/store/settings/settings-reducers.token.ts
index c4c0dd2..a45a536 100644
--- a/src/app/store/settings/settings-reducers.token.ts
+++ b/src/app/store/settings/settings-reducers.token.ts
@@ -15,12 +15,14 @@
import {ActionReducerMap} from "@ngrx/store";
import {ISettingsStoreState} from "./model";
import {statementTypesReducer} from "./reducers";
+import {sectorsReducer} from "./reducers/sectors.reducer";
export const SETTINGS_FEATURE_NAME = "settings";
export const SETTINGS_REDUCER = new InjectionToken<ActionReducerMap<ISettingsStoreState>>("Settings store reducer", {
providedIn: "root",
factory: () => ({
- statementTypes: statementTypesReducer
+ statementTypes: statementTypesReducer,
+ sectors: sectorsReducer
})
});
diff --git a/src/app/store/settings/settings-store.module.ts b/src/app/store/settings/settings-store.module.ts
index 44e465a..1a3f1c4 100644
--- a/src/app/store/settings/settings-store.module.ts
+++ b/src/app/store/settings/settings-store.module.ts
@@ -14,14 +14,14 @@
import {NgModule} from "@angular/core";
import {EffectsModule} from "@ngrx/effects";
import {StoreModule} from "@ngrx/store";
-import {StatementTypesEffects} from "./effects";
+import {FetchSettingsEffect} from "./effects";
import {SETTINGS_FEATURE_NAME, SETTINGS_REDUCER} from "./settings-reducers.token";
@NgModule({
imports: [
StoreModule.forFeature(SETTINGS_FEATURE_NAME, SETTINGS_REDUCER),
EffectsModule.forFeature([
- StatementTypesEffects
+ FetchSettingsEffect
])
]
})
diff --git a/src/app/store/statements/actions/fetch.actions.ts b/src/app/store/statements/actions/fetch.actions.ts
index 3f38c4c..a13078c 100644
--- a/src/app/store/statements/actions/fetch.actions.ts
+++ b/src/app/store/statements/actions/fetch.actions.ts
@@ -12,24 +12,20 @@
********************************************************************************/
import {createAction, props} from "@ngrx/store";
-import {IAPIAttachmentModel} from "../../../core/api/statements";
+import {IAPIStatementModel} from "../../../core/api";
import {IStatementEntity} from "../model";
-export const fetchAllStatementsAction = createAction(
- "[Dashboard] Fetch all statements"
-);
-
export const fetchStatementDetailsAction = createAction(
"[Details/Edit] Fetch statement's details",
props<{ statementId: number; withoutConfiguration?: boolean }>()
);
-export const setStatementDetails = createAction(
- "[API] Set statement's details",
- props<{ statementId: number, details: IStatementEntity }>()
+export const updateStatementEntityAction = createAction(
+ "[API] Update statement entity",
+ props<{ statementId: number, entity: IStatementEntity }>()
);
-export const setAttachments = createAction(
- "[API] Set statement's attachments",
- props<{ statementId: number, attachments: IAPIAttachmentModel[] }>()
+export const updateStatementInfoAction = createAction(
+ "[API] Update statement info",
+ props<{ items: IAPIStatementModel[] }>()
);
diff --git a/src/app/store/statements/actions/index.ts b/src/app/store/statements/actions/index.ts
index 9f677e5..6f7b32b 100644
--- a/src/app/store/statements/actions/index.ts
+++ b/src/app/store/statements/actions/index.ts
@@ -16,3 +16,5 @@
export * from "./new-statement-form.actions";
export * from "./submit.actions";
+export * from "./loading.actions";
+export * from "./search.actions";
diff --git a/src/app/store/statements/actions/loading.actions.ts b/src/app/store/statements/actions/loading.actions.ts
new file mode 100644
index 0000000..3b556d7
--- /dev/null
+++ b/src/app/store/statements/actions/loading.actions.ts
@@ -0,0 +1,20 @@
+/********************************************************************************
+ * 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 {IStatementLoadingState} from "../model";
+
+export const setStatementLoadingAction = createAction(
+ "[Effects/API] Set statement loading state",
+ props<{ loading: Partial<IStatementLoadingState> }>()
+);
diff --git a/src/app/store/statements/actions/new-statement-form.actions.ts b/src/app/store/statements/actions/new-statement-form.actions.ts
index ad1e04b..2bceadc 100644
--- a/src/app/store/statements/actions/new-statement-form.actions.ts
+++ b/src/app/store/statements/actions/new-statement-form.actions.ts
@@ -12,7 +12,7 @@
********************************************************************************/
import {createAction, props} from "@ngrx/store";
-import {IStatementInfoFormValue} from "../model";
+import {IStatementInformationFormValue} from "../model";
export const clearNewStatementFormAction = createAction(
"[NewStatement] Clear form"
@@ -20,7 +20,7 @@
export const submitNewStatementAction = createAction(
"[NewStatement] Submit",
- props<{ value: IStatementInfoFormValue }>()
+ props<{ value: IStatementInformationFormValue }>()
);
export const setNewStatementProgressAction = createAction(
diff --git a/src/app/store/statements/actions/search.actions.ts b/src/app/store/statements/actions/search.actions.ts
new file mode 100644
index 0000000..2b51abe
--- /dev/null
+++ b/src/app/store/statements/actions/search.actions.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 {createAction, props} from "@ngrx/store";
+import {IAPIPaginationResponse, IAPISearchOptions, IAPIStatementModel} from "../../../core";
+
+export const startStatementSearchAction = createAction(
+ "[Dashboard/Edit/List] Start search for statements",
+ props<{ options: IAPISearchOptions }>()
+);
+
+export const setStatementSearchResultAction = createAction(
+ "[API] Set search results for statements",
+ props<{ results: IAPIPaginationResponse<IAPIStatementModel> }>()
+);
diff --git a/src/app/store/statements/actions/submit.actions.ts b/src/app/store/statements/actions/submit.actions.ts
index 8817ff7..d3c4d24 100644
--- a/src/app/store/statements/actions/submit.actions.ts
+++ b/src/app/store/statements/actions/submit.actions.ts
@@ -12,9 +12,34 @@
********************************************************************************/
import {createAction, props} from "@ngrx/store";
-import {IWorkflowFormValue} from "../model";
+import {IStatementInformationFormValue, IWorkflowFormValue} from "../model";
-export const submitWorkflowFormAction = createAction(
- "[Edit] Submit workflow form",
+/**
+ * This action submits the value of the statement information form to the back end.
+ *
+ * If new is set to true, a new statement will be created from the given values.
+ * If not, statementId and taskId are required to update the values of a statement.
+ *
+ * If responsible is set to true, the current statement task will be completed. Also,
+ * the new created task will be claimed und the page navigates further.
+ *
+ * If responsible is set to false or unset, the current statement task will not be
+ * touched. But, if set to false, the page automatically navigates to the site
+ * for creating the draft of a the negative answer.
+ */
+export const submitStatementInformationFormAction = createAction(
+ "[New/Edit] Submit statement information form",
+ props<{
+ new?: boolean,
+ statementId?: number,
+ taskId?: string,
+ value: IStatementInformationFormValue,
+ // If set to true, the addBasicInfoTask will be completed and the addWorkflowData task will be claimed.
+ responsible?: boolean
+ }>()
+);
+
+export const submitWorkflowDataFormAction = createAction(
+ "[Edit] Submit workflow data form",
props<{ statementId: number, taskId: string, data: IWorkflowFormValue, completeTask?: boolean }>()
);
diff --git a/src/app/store/statements/effects/comments/comments.effect.spec.ts b/src/app/store/statements/effects/comments/comments.effect.spec.ts
index 676650a..4022e7b 100644
--- a/src/app/store/statements/effects/comments/comments.effect.spec.ts
+++ b/src/app/store/statements/effects/comments/comments.effect.spec.ts
@@ -17,7 +17,7 @@
import {Action} from "@ngrx/store";
import {merge, Observable, of, Subscription} from "rxjs";
import {IAPICommentModel, SPA_BACKEND_ROUTE} from "../../../../core";
-import {addCommentAction, deleteCommentAction, fetchCommentsAction, setStatementDetails} from "../../actions";
+import {addCommentAction, deleteCommentAction, fetchCommentsAction, updateStatementEntityAction} from "../../actions";
import {CommentsEffect} from "./comments.effect";
describe("CommentsEffect", () => {
@@ -54,7 +54,7 @@
it("should fetch comments", () => {
const statementId = 19;
const comments: IAPICommentModel[] = [getCommentObject(190)];
- const expectedResult = setStatementDetails({statementId, details: {comments}});
+ const expectedResult = updateStatementEntityAction({statementId, entity: {comments}});
const results: Action[] = [];
actions$ = of(fetchCommentsAction({statementId}));
@@ -69,7 +69,7 @@
const statementId = 19;
const commentId = 1919;
const comments: IAPICommentModel[] = [getCommentObject(190)];
- const expectedResult = setStatementDetails({statementId, details: {comments}});
+ const expectedResult = updateStatementEntityAction({statementId, entity: {comments}});
const results: Action[] = [];
actions$ = of(deleteCommentAction({statementId, commentId}));
@@ -86,7 +86,7 @@
const statementId = 19;
const text = "1919";
const comments: IAPICommentModel[] = [getCommentObject(190)];
- const expectedResult = setStatementDetails({statementId, details: {comments}});
+ const expectedResult = updateStatementEntityAction({statementId, entity: {comments}});
const results: Action[] = [];
actions$ = of(addCommentAction({statementId, text}));
diff --git a/src/app/store/statements/effects/comments/comments.effect.ts b/src/app/store/statements/effects/comments/comments.effect.ts
index a0dd6cd..bc7fbf3 100644
--- a/src/app/store/statements/effects/comments/comments.effect.ts
+++ b/src/app/store/statements/effects/comments/comments.effect.ts
@@ -16,7 +16,7 @@
import {filter, map, retry, switchMap} from "rxjs/operators";
import {StatementsApiService} from "../../../../core/api/statements";
import {ignoreError} from "../../../../util/rxjs";
-import {addCommentAction, deleteCommentAction, fetchCommentsAction, setStatementDetails} from "../../actions";
+import {addCommentAction, deleteCommentAction, fetchCommentsAction, updateStatementEntityAction} from "../../actions";
@Injectable({providedIn: "root"})
export class CommentsEffect {
@@ -45,7 +45,7 @@
public fetchComments(statementId: number) {
return this.statementsApiService.getComments(statementId).pipe(
- map((comments) => setStatementDetails({statementId, details: {comments}})),
+ map((comments) => updateStatementEntityAction({statementId, entity: {comments}})),
retry(2),
ignoreError()
);
diff --git a/src/app/store/statements/effects/fetch-statement-details/fetch-statement-details.effect.ts b/src/app/store/statements/effects/fetch-statement-details/fetch-statement-details.effect.ts
index d5713d7..3f0adbd 100644
--- a/src/app/store/statements/effects/fetch-statement-details/fetch-statement-details.effect.ts
+++ b/src/app/store/statements/effects/fetch-statement-details/fetch-statement-details.effect.ts
@@ -18,23 +18,14 @@
import {filter, map, retry, startWith, switchMap} from "rxjs/operators";
import {ProcessApiService, SettingsApiService, StatementsApiService} from "../../../../core";
import {ignoreError} from "../../../../util/rxjs";
+import {arrayJoin} from "../../../../util/store";
+import {fetchAttachmentsAction} from "../../../attachments/actions";
import {setDiagramAction, setHistoryAction, setTasksAction} from "../../../process/actions";
-import {
- fetchAllStatementsAction,
- fetchCommentsAction,
- fetchStatementDetailsAction,
- setAttachments,
- setStatementDetails
-} from "../../actions";
+import {fetchCommentsAction, fetchStatementDetailsAction, updateStatementEntityAction, updateStatementInfoAction} from "../../actions";
@Injectable({providedIn: "root"})
export class FetchStatementDetailsEffect {
- public readonly fetchAllStatements$ = createEffect(() => this.actions.pipe(
- ofType(fetchAllStatementsAction),
- switchMap(() => this.fetchAllStatements())
- ));
-
public readonly fetchStatementDetails$ = createEffect(() => this.actions.pipe(
ofType(fetchStatementDetailsAction),
filter((action) => action.statementId != null),
@@ -50,56 +41,56 @@
}
- public fetchAllStatements() {
- return this.statementsApiService.getStatements().pipe(
- switchMap((statements) => {
- const actions = statements
- .filter((info) => info?.id != null)
- .map((info) => of(setStatementDetails({statementId: info?.id, details: {info}})));
- return merge(...actions);
- })
- );
- }
-
public fetchStatementDetails(statementId: number, withoutConfiguration?: boolean): Observable<Action> {
return this.statementsApiService.getStatement(statementId).pipe(
retry(2),
switchMap((info) => {
return merge<Action>(
this.fetchTasks(statementId),
- this.fetchAttachments(statementId),
this.fetchWorkflowData(statementId),
+ this.fetchParents(statementId),
this.fetchHistory(statementId),
this.fetchDiagram(statementId),
+ of(fetchAttachmentsAction({statementId})),
of(fetchCommentsAction({statementId})),
- withoutConfiguration ? EMPTY : this.fetchConfiguration(statementId)
+ withoutConfiguration ? EMPTY : this.fetchConfiguration(statementId),
+ this.fetchSectors(statementId)
).pipe(
- startWith(setStatementDetails({statementId, details: {info}}))
+ startWith(updateStatementEntityAction({statementId, entity: {info}}))
);
}),
ignoreError()
);
}
- public fetchAttachments(statementId: number) {
- return this.statementsApiService.getAllAttachments(statementId).pipe(
- map((attachments) => setAttachments({statementId, attachments})),
+ public fetchParents(statementId: number) {
+ return this.statementsApiService.getParentIds(statementId).pipe(
retry(2),
- ignoreError()
+ switchMap((parentIds) => {
+ parentIds = arrayJoin(parentIds);
+ const fetchParentsInfo$ = this.statementsApiService.getStatements(...parentIds).pipe(
+ map((items) => updateStatementInfoAction({items})),
+ retry(2)
+ );
+ return merge(
+ of(updateStatementEntityAction({statementId, entity: {parentIds}})),
+ parentIds.length === 0 ? EMPTY : fetchParentsInfo$
+ );
+ })
);
}
public fetchWorkflowData(statementId: number) {
return this.statementsApiService.getWorkflowData(statementId).pipe(
- map((workflow) => setStatementDetails({statementId, details: {workflow}})),
retry(2),
+ map((workflow) => updateStatementEntityAction({statementId, entity: {workflow}})),
ignoreError()
);
}
public fetchConfiguration(statementId: number) {
return this.settingsApiService.getDepartmentsConfiguration(statementId).pipe(
- map((configuration) => setStatementDetails({statementId, details: {configuration}})),
+ map((configuration) => updateStatementEntityAction({statementId, entity: {configuration}})),
retry(2),
ignoreError()
);
@@ -129,4 +120,12 @@
);
}
+ public fetchSectors(statementId: number) {
+ return this.statementsApiService.getSectors(statementId).pipe(
+ map((sectors) => updateStatementEntityAction({statementId, entity: {sectors}})),
+ retry(2),
+ ignoreError()
+ );
+ }
+
}
diff --git a/src/app/store/statements/effects/index.ts b/src/app/store/statements/effects/index.ts
index 818065b..0a0fc80 100644
--- a/src/app/store/statements/effects/index.ts
+++ b/src/app/store/statements/effects/index.ts
@@ -13,5 +13,6 @@
export * from "./comments";
export * from "./fetch-statement-details";
-export * from "./submit-info-form";
+export * from "./search";
+export * from "./submit-information-form";
export * from "./submit-workflow-form";
diff --git a/src/app/store/statements/effects/search/index.ts b/src/app/store/statements/effects/search/index.ts
new file mode 100644
index 0000000..589e408
--- /dev/null
+++ b/src/app/store/statements/effects/search/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./search-statements.effect";
diff --git a/src/app/store/statements/effects/search/search-statements.effect.spec.ts b/src/app/store/statements/effects/search/search-statements.effect.spec.ts
new file mode 100644
index 0000000..2fc76c7
--- /dev/null
+++ b/src/app/store/statements/effects/search/search-statements.effect.spec.ts
@@ -0,0 +1,127 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {HttpParams} from "@angular/common/http";
+import {HttpClientTestingModule, HttpTestingController} from "@angular/common/http/testing";
+import {fakeAsync, TestBed, tick} from "@angular/core/testing";
+import {provideMockActions} from "@ngrx/effects/testing";
+import {Action} from "@ngrx/store";
+import {concat, NEVER, Observable, of, Subscription, timer} from "rxjs";
+import {map} from "rxjs/operators";
+import {IAPIPaginationResponse, IAPISearchOptions, IAPIStatementModel, SPA_BACKEND_ROUTE} from "../../../../core";
+import {createPaginationResponseMock, createStatementModelMock} from "../../../../test";
+import {objectToHttpParams} from "../../../../util/http";
+import {
+ setStatementLoadingAction,
+ setStatementSearchResultAction,
+ startStatementSearchAction,
+ updateStatementInfoAction
+} from "../../actions";
+import {SearchStatementsEffect} from "./search-statements.effect";
+
+describe("SearchStatementsEffect", () => {
+
+ const DEBOUNCE_TIME = 200;
+
+ let actions$: Observable<Action>;
+ let httpTestingController: HttpTestingController;
+ let effect: SearchStatementsEffect;
+ let subscription: Subscription;
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ imports: [
+ HttpClientTestingModule
+ ],
+ providers: [
+ SearchStatementsEffect,
+ provideMockActions(() => actions$),
+ {
+ provide: SPA_BACKEND_ROUTE,
+ useValue: "/"
+ }
+ ]
+ });
+ effect = TestBed.inject(SearchStatementsEffect);
+ httpTestingController = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ if (subscription != null) {
+ subscription.unsubscribe();
+ }
+ });
+
+ it("should fetch statements after a debounce time", fakeAsync(() => {
+ const options: IAPISearchOptions = {q: "Darmstadt Heppenheim"};
+ const results: Action[] = [];
+ const content: IAPIStatementModel[] = Array(19).fill(0)
+ .map((_, id) => createStatementModelMock(id));
+
+ const expectedResults: Action[] = [
+ setStatementLoadingAction({loading: {search: true}}),
+ updateStatementInfoAction({items: content}),
+ setStatementSearchResultAction({results: createPaginationResponseMock(content)}),
+ setStatementLoadingAction({loading: {search: false}})
+ ];
+
+ actions$ = concat(
+ of(startStatementSearchAction({options})),
+ timer(DEBOUNCE_TIME - 2).pipe(map(() => startStatementSearchAction({options}))),
+ // For testing the debounce time, the observable shall never end; otherwise, it will emit its last value
+ NEVER
+ );
+ subscription = effect.search$.subscribe((action) => results.push(action));
+
+ tick(DEBOUNCE_TIME - 1);
+ expect(results).toEqual([]);
+ tick(DEBOUNCE_TIME);
+ expectFetchRequest(options, createPaginationResponseMock(content));
+ expect(results).toEqual(expectedResults);
+
+ httpTestingController.verify();
+ }));
+
+ it("should only update store when content is provided", fakeAsync(() => {
+ const options: IAPISearchOptions = {q: "Darmstadt Heppenheim"};
+ const results: Action[] = [];
+
+ const expectedResults: Action[] = [
+ setStatementLoadingAction({loading: {search: true}}),
+ setStatementLoadingAction({loading: {search: false}})
+ ];
+
+ actions$ = of(startStatementSearchAction({options}));
+ subscription = effect.search$.subscribe((action) => results.push(action));
+
+ expectFetchRequest(options, null);
+ expect(results).toEqual(expectedResults);
+
+ httpTestingController.verify();
+ }));
+
+ function expectFetchRequest(options: IAPISearchOptions, returnValue: IAPIPaginationResponse<IAPIStatementModel>) {
+ const params = new HttpParams({
+ fromObject: objectToHttpParams({
+ q: options.q,
+ size: options.size == null ? undefined : "" + options.size
+ })
+ });
+ const url = `/statementsearch` + (params.keys().length > 0 ? `?${params.toString()}` : "");
+ const request = httpTestingController.expectOne(url);
+ expect(request.request.method).toBe("GET");
+ request.flush(returnValue);
+ }
+
+});
+
diff --git a/src/app/store/statements/effects/search/search-statements.effect.ts b/src/app/store/statements/effects/search/search-statements.effect.ts
new file mode 100644
index 0000000..80aab75
--- /dev/null
+++ b/src/app/store/statements/effects/search/search-statements.effect.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 {Injectable} from "@angular/core";
+import {Actions, createEffect, ofType} from "@ngrx/effects";
+import {Action} from "@ngrx/store";
+import {Observable, of} from "rxjs";
+import {concatMap, debounceTime, endWith, filter, startWith, switchMap} from "rxjs/operators";
+import {IAPISearchOptions, StatementsApiService} from "../../../../core";
+import {ignoreError} from "../../../../util/rxjs";
+import {
+ setStatementLoadingAction,
+ setStatementSearchResultAction,
+ startStatementSearchAction,
+ updateStatementInfoAction
+} from "../../actions";
+
+@Injectable({providedIn: "root"})
+export class SearchStatementsEffect {
+
+ public search$ = createEffect(() => this.actions.pipe(
+ ofType(startStatementSearchAction),
+ debounceTime(200),
+ concatMap((action) => this.search(action.options))
+ ));
+
+ public constructor(
+ private readonly actions: Actions,
+ private readonly statementsApiService: StatementsApiService
+ ) {
+
+ }
+
+ public search(options: IAPISearchOptions): Observable<Action> {
+ return this.statementsApiService.getStatementSearch(options).pipe(
+ filter((results) => Array.isArray(results?.content)),
+ switchMap((results) => {
+ return of(
+ updateStatementInfoAction({items: results.content}),
+ setStatementSearchResultAction({results})
+ );
+ }),
+ ignoreError(),
+ startWith(setStatementLoadingAction({loading: {search: true}})),
+ endWith(setStatementLoadingAction({loading: {search: false}}))
+ );
+ }
+
+}
diff --git a/src/app/store/statements/effects/submit-information-form/index.ts b/src/app/store/statements/effects/submit-information-form/index.ts
new file mode 100644
index 0000000..82bb76b
--- /dev/null
+++ b/src/app/store/statements/effects/submit-information-form/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * 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 "./submit-statement-information-form.effect";
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
new file mode 100644
index 0000000..b12d55b
--- /dev/null
+++ b/src/app/store/statements/effects/submit-information-form/submit-statement-information-form.effect.ts
@@ -0,0 +1,159 @@
+/********************************************************************************
+ * 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 {Router} from "@angular/router";
+import {Actions, createEffect, ofType} from "@ngrx/effects";
+import {Action} from "@ngrx/store";
+import {concat, defer, EMPTY, Observable} from "rxjs";
+import {catchError, endWith, exhaustMap, filter, map, startWith, switchMap} from "rxjs/operators";
+import {EAPIProcessTaskDefinitionKey} from "../../../../core/api";
+import {ProcessApiService} from "../../../../core/api/process";
+import {StatementsApiService} from "../../../../core/api/statements";
+import {ignoreError} from "../../../../util/rxjs";
+import {AddOrRemoveAttachmentsEffect} from "../../../attachments/effects/add-or-remove";
+import {CompleteTaskEffect} from "../../../process/effects";
+import {setStatementLoadingAction, submitStatementInformationFormAction} from "../../actions";
+import {IStatementInformationFormValue} from "../../model";
+
+@Injectable({providedIn: "root"})
+export class SubmitStatementInformationFormEffect {
+
+ public readonly submit$ = createEffect(() => this.actions.pipe(
+ ofType(submitStatementInformationFormAction),
+ filter((action) => action.value != null),
+ filter((action) => action.new || action.statementId != null && action.taskId != null),
+ exhaustMap((action) => {
+ return action.new ?
+ this.submitNewStatement(action.value, action.responsible) :
+ this.submit(action.statementId, action.taskId, action.value, action.responsible);
+ })
+ ));
+
+ public constructor(
+ private readonly actions: Actions,
+ private readonly router: Router,
+ private readonly addOrRemoveAttachmentsEffect: AddOrRemoveAttachmentsEffect,
+ private readonly completeTaskEffect: CompleteTaskEffect,
+ private readonly processApiService: ProcessApiService,
+ private readonly statementsApiService: StatementsApiService,
+ ) {
+
+ }
+
+ public submit(
+ statementId: number,
+ taskId: string,
+ value: IStatementInformationFormValue,
+ responsible?: boolean
+ ): Observable<Action> {
+ return this.updateStatement(statementId, taskId, value).pipe(
+ switchMap(() => {
+ const addOrRemoveAttachments$ = this.addOrRemoveAttachmentsEffect.addOrRemoveAttachments(
+ statementId, taskId, value.addAttachments, value.removeAttachments
+ ).pipe(
+ catchError(() => {
+ responsible = undefined;
+ return EMPTY;
+ })
+ );
+
+ return concat(
+ addOrRemoveAttachments$,
+ defer(() => this.finalizeSubmit(statementId, taskId, responsible))
+ );
+ }),
+ ignoreError(),
+ startWith(setStatementLoadingAction({loading: {submittingStatementInformation: true}})),
+ endWith(setStatementLoadingAction({loading: {submittingStatementInformation: false}}))
+ );
+ }
+
+ public submitNewStatement(
+ value: IStatementInformationFormValue,
+ responsible?: boolean
+ ): Observable<Action> {
+ let statementId: number;
+ let taskId: string;
+ return this.createStatement(value).pipe(
+ map((info) => statementId = info.id),
+ switchMap(() => this.completeTaskEffect.claimNextTask(statementId, EAPIProcessTaskDefinitionKey.ADD_BASIC_INFO_DATA)),
+ switchMap((task) => {
+ taskId = task?.taskId;
+ const addOrRemoveAttachments$ = this.addOrRemoveAttachmentsEffect.addOrRemoveAttachments(
+ statementId, taskId, value.addAttachments, value.removeAttachments
+ ).pipe(
+ catchError(() => {
+ responsible = undefined;
+ return EMPTY;
+ })
+ );
+
+ return concat(
+ addOrRemoveAttachments$,
+ defer(() => this.finalizeSubmit(statementId, taskId, responsible))
+ );
+ }),
+ catchError(() => {
+ if (statementId == null) {
+ return EMPTY;
+ }
+ return this.completeTaskEffect.navigateToStatement(statementId).pipe(switchMap(() => EMPTY));
+ }),
+ ignoreError(),
+ startWith(setStatementLoadingAction({loading: {submittingStatementInformation: true}})),
+ endWith(setStatementLoadingAction({loading: {submittingStatementInformation: false}}))
+ );
+ }
+
+ public finalizeSubmit(statementId: number, taskId: string, responsible?: boolean): Observable<Action> {
+ return defer(() => {
+ if (taskId != null && responsible) {
+ return this.completeTaskEffect
+ .completeTask(statementId, taskId, {responsible: {type: "Boolean", value: true}}, true);
+ } else {
+ const queryParams = taskId != null && responsible === false ? {negative: true} : {};
+ return this.completeTaskEffect.navigateToStatement(statementId, taskId, queryParams).pipe(
+ switchMap(() => EMPTY)
+ );
+ }
+ }).pipe(
+ ignoreError()
+ );
+ }
+
+ public createStatement(value: IStatementInformationFormValue) {
+ return this.statementsApiService.putStatement({
+ title: value.title,
+ dueDate: value.dueDate,
+ receiptDate: value.receiptDate,
+ typeId: value.typeId,
+ city: value.city,
+ district: value.district,
+ contactId: value.contactId
+ });
+ }
+
+ public updateStatement(statementId: number, taskId: string, value: IStatementInformationFormValue) {
+ return this.statementsApiService.postStatement(statementId, taskId, {
+ title: value.title,
+ dueDate: value.dueDate,
+ receiptDate: value.receiptDate,
+ typeId: value.typeId,
+ city: value.city,
+ district: value.district,
+ contactId: value.contactId
+ });
+ }
+
+}
diff --git a/src/app/store/statements/effects/submit-workflow-form/submit-workflow-form.effect.spec.ts b/src/app/store/statements/effects/submit-workflow-form/submit-workflow-form.effect.spec.ts
index 91807cc..5ba56c9 100644
--- a/src/app/store/statements/effects/submit-workflow-form/submit-workflow-form.effect.spec.ts
+++ b/src/app/store/statements/effects/submit-workflow-form/submit-workflow-form.effect.spec.ts
@@ -13,12 +13,13 @@
import {HttpClientTestingModule, HttpTestingController} from "@angular/common/http/testing";
import {TestBed} from "@angular/core/testing";
+import {RouterTestingModule} from "@angular/router/testing";
import {provideMockActions} from "@ngrx/effects/testing";
import {Action} from "@ngrx/store";
-import {Observable, of, Subscription} from "rxjs";
+import {EMPTY, Observable, of, Subscription} from "rxjs";
import {IAPIWorkflowData, SPA_BACKEND_ROUTE} from "../../../../core";
-import {completeTaskAction} from "../../../process/actions";
-import {setStatementDetails, submitWorkflowFormAction} from "../../actions";
+import {CompleteTaskEffect} from "../../../process/effects";
+import {submitWorkflowDataFormAction, updateStatementEntityAction} from "../../actions";
import {IWorkflowFormValue} from "../../model";
import {SubmitWorkflowFormEffect} from "./submit-workflow-form.effect";
@@ -32,7 +33,8 @@
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [
- HttpClientTestingModule
+ HttpClientTestingModule,
+ RouterTestingModule
],
providers: [
SubmitWorkflowFormEffect,
@@ -57,40 +59,20 @@
const results: Action[] = [];
const statementId = 19;
const taskId = "1919";
+ const parentIds = [18, 19];
+ const data = createFormValue(parentIds);
+ const expectedData = createData();
- const data: IWorkflowFormValue = {
- departments: [
- {
- groupName: "Group A",
- name: "Department 1"
- },
- {
- groupName: "Group A",
- name: "Department 2"
- },
- {
- groupName: "Group B",
- name: "Department 1"
- }
- ],
- geographicPosition: ""
- };
- const expectedData: IAPIWorkflowData = {
- geoPosition: "",
- selectedDepartments: {
- "Group A": ["Department 1", "Department 2"],
- "Group B": ["Department 1"]
- }
- };
-
- actions$ = of(submitWorkflowFormAction({statementId, taskId, data, completeTask: false}));
+ actions$ = of(submitWorkflowDataFormAction({statementId, taskId, data, completeTask: false}));
subscription = effect.submit$.subscribe((action) => results.push(action));
expectPostWorkflowRequest(statementId, taskId, expectedData);
+ expectPostParentIdsRequest(statementId, taskId, parentIds);
subscription.unsubscribe();
expect(results).toEqual([
- setStatementDetails({statementId, details: {workflow: expectedData}})
+ updateStatementEntityAction({statementId, entity: {workflow: expectedData}}),
+ updateStatementEntityAction({statementId, entity: {parentIds}})
]);
httpTestingController.verify();
@@ -100,41 +82,22 @@
const results: Action[] = [];
const statementId = 19;
const taskId = "1919";
+ const parentIds = [18, 19];
+ const data = createFormValue(parentIds);
+ const expectedData = createData();
+ const completeTaskSpy = spyOn(TestBed.inject(CompleteTaskEffect), "completeTask").and.returnValue(EMPTY);
- const data: IWorkflowFormValue = {
- departments: [
- {
- groupName: "Group A",
- name: "Department 1"
- },
- {
- groupName: "Group A",
- name: "Department 2"
- },
- {
- groupName: "Group B",
- name: "Department 1"
- }
- ],
- geographicPosition: ""
- };
- const expectedData: IAPIWorkflowData = {
- geoPosition: "",
- selectedDepartments: {
- "Group A": ["Department 1", "Department 2"],
- "Group B": ["Department 1"]
- }
- };
-
- actions$ = of(submitWorkflowFormAction({statementId, taskId, data, completeTask: true}));
+ actions$ = of(submitWorkflowDataFormAction({statementId, taskId, data, completeTask: true}));
subscription = effect.submit$.subscribe((action) => results.push(action));
expectPostWorkflowRequest(statementId, taskId, expectedData);
+ expectPostParentIdsRequest(statementId, taskId, parentIds);
expect(results).toEqual([
- setStatementDetails({statementId, details: {workflow: expectedData}}),
- completeTaskAction({statementId, taskId, variables: {}})
+ updateStatementEntityAction({statementId, entity: {workflow: expectedData}}),
+ updateStatementEntityAction({statementId, entity: {parentIds}})
]);
+ expect(completeTaskSpy).toHaveBeenCalledWith(statementId, taskId, {}, true);
httpTestingController.verify();
});
@@ -142,9 +105,9 @@
it("should not make API calls if action data is missing", () => {
const data = {} as any;
actions$ = of(...[
- submitWorkflowFormAction({statementId: null, taskId: "19", data, completeTask: true}),
- submitWorkflowFormAction({statementId: 19, taskId: null, data, completeTask: false}),
- submitWorkflowFormAction({statementId: 19, taskId: "19", data: null}),
+ submitWorkflowDataFormAction({statementId: null, taskId: "19", data, completeTask: true}),
+ submitWorkflowDataFormAction({statementId: 19, taskId: null, data, completeTask: false}),
+ submitWorkflowDataFormAction({statementId: 19, taskId: "19", data: null}),
]);
subscription = effect.submit$.subscribe();
expect(() => httpTestingController.verify()).not.toThrow();
@@ -158,5 +121,44 @@
request.flush(body);
}
+ function expectPostParentIdsRequest(statementId: number, taskId: string, parentIds: number[]) {
+ const url = `/process/statements/${statementId}/task/${taskId}/workflow/parents`;
+ const request = httpTestingController.expectOne(url);
+ expect(request.request.method).toBe("POST");
+ expect(request.request.body).toEqual(parentIds);
+ request.flush(parentIds);
+ }
+
+ function createFormValue(parentIds: number[]): IWorkflowFormValue {
+ return {
+ departments: [
+ {
+ groupName: "Group A",
+ name: "Department 1"
+ },
+ {
+ groupName: "Group A",
+ name: "Department 2"
+ },
+ {
+ groupName: "Group B",
+ name: "Department 1"
+ }
+ ],
+ geographicPosition: "",
+ parentIds
+ };
+ }
+
+ function createData(): IAPIWorkflowData {
+ return {
+ geoPosition: "",
+ selectedDepartments: {
+ "Group A": ["Department 1", "Department 2"],
+ "Group B": ["Department 1"]
+ }
+ };
+ }
+
});
diff --git a/src/app/store/statements/effects/submit-workflow-form/submit-workflow-form.effect.ts b/src/app/store/statements/effects/submit-workflow-form/submit-workflow-form.effect.ts
index b2ac024..7a06174 100644
--- a/src/app/store/statements/effects/submit-workflow-form/submit-workflow-form.effect.ts
+++ b/src/app/store/statements/effects/submit-workflow-form/submit-workflow-form.effect.ts
@@ -14,28 +14,29 @@
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 {filter, map, retry, switchMap} from "rxjs/operators";
+import {concat, EMPTY, merge, Observable, of} from "rxjs";
+import {exhaustMap, filter, map, retry, switchMap, toArray} from "rxjs/operators";
import {IAPIDepartmentGroups} from "../../../../core/api/settings";
import {IAPIWorkflowData, StatementsApiService} from "../../../../core/api/statements";
import {arrayJoin} from "../../../../util/store";
-import {completeTaskAction} from "../../../process/actions";
-import {setStatementDetails, submitWorkflowFormAction} from "../../actions";
+import {CompleteTaskEffect} from "../../../process/effects";
+import {submitWorkflowDataFormAction, updateStatementEntityAction} from "../../actions";
import {IWorkflowFormValue} from "../../model";
@Injectable({providedIn: "root"})
export class SubmitWorkflowFormEffect {
public readonly submit$ = createEffect(() => this.actions.pipe(
- ofType(submitWorkflowFormAction),
+ ofType(submitWorkflowDataFormAction),
filter((action) => action.statementId != null && action.taskId != null && action.data != null),
- switchMap((action) => this.submitWorkflowForm(action.statementId, action.taskId, action.data, action.completeTask))
+ exhaustMap((action) => this.submitWorkflowForm(action.statementId, action.taskId, action.data, action.completeTask))
));
public constructor(
private readonly actions: Actions,
- private readonly statementsApiService: StatementsApiService
+ private readonly statementsApiService: StatementsApiService,
+ private readonly completeTaskEffect: CompleteTaskEffect
) {
}
@@ -47,8 +48,14 @@
completeTask?: boolean
): Observable<Action> {
return concat(
- this.postWorkflowData(statementId, taskId, data),
- completeTask ? of(completeTaskAction({statementId, taskId, variables: {}})) : EMPTY
+ merge(
+ this.postWorkflowData(statementId, taskId, data),
+ this.postParentIds(statementId, taskId, data),
+ ).pipe(
+ toArray(),
+ switchMap((results) => of(...results))
+ ),
+ completeTask ? this.completeTaskEffect.completeTask(statementId, taskId, {}, true) : EMPTY
);
}
@@ -68,7 +75,19 @@
geoPosition: ""
};
return this.statementsApiService.postWorkflowData(statementId, taskId, body).pipe(
- map(() => setStatementDetails({statementId, details: {workflow: body}})),
+ map(() => updateStatementEntityAction({statementId, entity: {workflow: body}})),
+ retry(3)
+ );
+ }
+
+ private postParentIds(
+ statementId: number,
+ taskId: string,
+ data: IWorkflowFormValue
+ ): Observable<Action> {
+ const parentIds = data.parentIds;
+ return this.statementsApiService.postParentIds(statementId, taskId, parentIds).pipe(
+ map(() => updateStatementEntityAction({statementId, entity: {parentIds}})),
retry(3)
);
}
diff --git a/src/app/store/statements/model/IStatementEntity.ts b/src/app/store/statements/model/IStatementEntity.ts
index 185858d..02f9465 100644
--- a/src/app/store/statements/model/IStatementEntity.ts
+++ b/src/app/store/statements/model/IStatementEntity.ts
@@ -13,6 +13,7 @@
import {IAPIDepartmentsConfiguration} from "../../../core/api/settings";
import {IAPICommentModel, IAPIStatementModel, IAPIWorkflowData} from "../../../core/api/statements";
+import {IAPISectorsModel} from "../../../core/api/statements/IAPISectorsModel";
export interface IStatementEntity {
@@ -20,10 +21,12 @@
workflow?: IAPIWorkflowData;
- attachments?: number[];
-
configuration?: IAPIDepartmentsConfiguration;
+ parentIds?: number[];
+
comments?: IAPICommentModel[];
+ sectors?: IAPISectorsModel;
+
}
diff --git a/src/app/store/statements/model/IStatementLoadingState.ts b/src/app/store/statements/model/IStatementLoadingState.ts
new file mode 100644
index 0000000..1241eb7
--- /dev/null
+++ b/src/app/store/statements/model/IStatementLoadingState.ts
@@ -0,0 +1,22 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+export interface IStatementLoadingState {
+
+ search?: boolean;
+
+ submittingStatementInformation?: boolean;
+
+ submittingWorkflowData?: boolean;
+
+}
diff --git a/src/app/store/statements/model/IStatementsStoreState.ts b/src/app/store/statements/model/IStatementsStoreState.ts
index b3f74dd..d1d3392 100644
--- a/src/app/store/statements/model/IStatementsStoreState.ts
+++ b/src/app/store/statements/model/IStatementsStoreState.ts
@@ -11,14 +11,20 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+import {IAPIPaginationResponse} from "../../../core";
import {TStoreEntities} from "../../../util/store";
import {IStatementEntity} from "./IStatementEntity";
+import {IStatementLoadingState} from "./IStatementLoadingState";
import {IStatementInfoForm} from "./statement-info-form/IStatementInfoForm";
export interface IStatementsStoreState {
entities: TStoreEntities<IStatementEntity>;
+ search?: IAPIPaginationResponse<number>;
+
newStatementForm?: IStatementInfoForm;
+ loading?: IStatementLoadingState;
+
}
diff --git a/src/app/store/statements/model/index.ts b/src/app/store/statements/model/index.ts
index 6e11ed5..82542a2 100644
--- a/src/app/store/statements/model/index.ts
+++ b/src/app/store/statements/model/index.ts
@@ -13,10 +13,11 @@
export * from "./statement-info-form/ENewStatementError";
export * from "./statement-info-form/IStatementInfoForm";
-export * from "./statement-info-form/IStatementInfoFormValue";
+export * from "./statement-info-form/IStatementInformationFormValue";
export * from "./workflow-form/IWorkflowFormValue";
export * from "./workflow-form/IDepartmentOptionValue";
export * from "./IStatementsStoreState";
+export * from "./IStatementLoadingState";
export * from "./IStatementEntity";
diff --git a/src/app/store/statements/model/statement-info-form/IStatementInfoForm.ts b/src/app/store/statements/model/statement-info-form/IStatementInfoForm.ts
index d64d986..bc2442b 100644
--- a/src/app/store/statements/model/statement-info-form/IStatementInfoForm.ts
+++ b/src/app/store/statements/model/statement-info-form/IStatementInfoForm.ts
@@ -11,11 +11,11 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {IStatementInfoFormValue} from "./IStatementInfoFormValue";
+import {IStatementInformationFormValue} from "./IStatementInformationFormValue";
export interface IStatementInfoForm {
- value: IStatementInfoFormValue;
+ value: IStatementInformationFormValue;
isLoading?: boolean;
diff --git a/src/app/store/statements/model/statement-info-form/IStatementInformationFormValue.ts b/src/app/store/statements/model/statement-info-form/IStatementInformationFormValue.ts
new file mode 100644
index 0000000..e21e896
--- /dev/null
+++ b/src/app/store/statements/model/statement-info-form/IStatementInformationFormValue.ts
@@ -0,0 +1,22 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {IAPIPartialStatementModel} from "../../../../core/api/statements";
+
+export interface IStatementInformationFormValue extends IAPIPartialStatementModel {
+
+ addAttachments?: File[];
+
+ removeAttachments?: number[];
+
+}
diff --git a/src/app/store/statements/model/workflow-form/IWorkflowFormValue.ts b/src/app/store/statements/model/workflow-form/IWorkflowFormValue.ts
index f1edda2..d18c6e6 100644
--- a/src/app/store/statements/model/workflow-form/IWorkflowFormValue.ts
+++ b/src/app/store/statements/model/workflow-form/IWorkflowFormValue.ts
@@ -19,4 +19,6 @@
geographicPosition: string;
+ parentIds: number[];
+
}
diff --git a/src/app/store/statements/reducers/entities/statement-entities.reducer.spec.ts b/src/app/store/statements/reducers/entities/statement-entities.reducer.spec.ts
new file mode 100644
index 0000000..8f3bb85
--- /dev/null
+++ b/src/app/store/statements/reducers/entities/statement-entities.reducer.spec.ts
@@ -0,0 +1,79 @@
+/********************************************************************************
+ * 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 {Action} from "@ngrx/store";
+import {createStatementModelMock, createWorkflowDataMock} from "../../../../test";
+import {TStoreEntities} from "../../../../util/store";
+import {fetchStatementDetailsAction, updateStatementEntityAction, updateStatementInfoAction} from "../../actions";
+import {IStatementEntity} from "../../model";
+import {statementEntitiesReducer} from "./statement-entities.reducer";
+
+describe("statementEntitiesReducer", () => {
+
+ it("should not affect state on fetch action", () => {
+ const initialState: TStoreEntities<IStatementEntity> = {};
+ const action = fetchStatementDetailsAction({statementId: 19});
+ const state = statementEntitiesReducer(initialState, action);
+ expect(state).toBe(initialState);
+ });
+
+ it("should create new entity of statement on set action", () => {
+ const initialState: TStoreEntities<IStatementEntity> = {};
+ const entity: IStatementEntity = {info: createStatementModelMock(19)};
+ const action = updateStatementEntityAction({statementId: 19, entity});
+ const state = statementEntitiesReducer(initialState, action);
+ expect(state).toEqual({
+ 19: {
+ info: createStatementModelMock(19)
+ }
+ });
+ });
+
+ it("should update entity of statement on set action", () => {
+ const info = createStatementModelMock(19);
+ const workflow = createWorkflowDataMock();
+ const initialState: TStoreEntities<IStatementEntity> = {18: {}, 19: {workflow}};
+ const entity: IStatementEntity = {info};
+ const action = updateStatementEntityAction({statementId: 19, entity});
+ const state = statementEntitiesReducer(initialState, action);
+ expect(state).toEqual({
+ 18: {},
+ 19: {info, workflow}
+ });
+ });
+
+ it("should update info for list of statements", () => {
+ const workflow = createWorkflowDataMock();
+ const initialState: TStoreEntities<IStatementEntity> = {18: {}, 19: {workflow}};
+ const action = updateStatementInfoAction({items: [createStatementModelMock(18), createStatementModelMock(19)]});
+ const state = statementEntitiesReducer(initialState, action);
+ expect(state).toEqual({
+ 18: {info: createStatementModelMock(18)},
+ 19: {info: createStatementModelMock(19), workflow}
+ });
+ });
+
+ it("should not change state without any id", () => {
+ const initialState: TStoreEntities<IStatementEntity> = {18: {}, 19: {workflow: createWorkflowDataMock()}};
+ let action: Action = updateStatementEntityAction({statementId: undefined, entity: {}});
+ let state = statementEntitiesReducer(initialState, action);
+ expect(state).toBe(initialState);
+ action = updateStatementInfoAction({items: null});
+ state = statementEntitiesReducer(initialState, action);
+ expect(state).toBe(initialState);
+ action = updateStatementInfoAction({items: [null, createStatementModelMock(null)]});
+ state = statementEntitiesReducer(initialState, action);
+ expect(state).toBe(initialState);
+ });
+
+});
diff --git a/src/app/store/statements/reducers/entities/statement-entities.reducer.ts b/src/app/store/statements/reducers/entities/statement-entities.reducer.ts
new file mode 100644
index 0000000..07915fc
--- /dev/null
+++ b/src/app/store/statements/reducers/entities/statement-entities.reducer.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 {createReducer, on} from "@ngrx/store";
+import {arrayToEntities, TStoreEntities} from "../../../../util/store";
+import {updateStatementEntityAction, updateStatementInfoAction} from "../../actions";
+import {IStatementEntity} from "../../model";
+
+export const statementEntitiesReducer = createReducer<TStoreEntities<IStatementEntity>>(
+ {},
+ on(updateStatementEntityAction, (state, payload) => {
+ return payload.statementId == null ? state : {
+ ...state,
+ [payload.statementId]: {
+ ...state[payload.statementId],
+ ...payload.entity
+ }
+ };
+ }),
+ on(updateStatementInfoAction, (state, payload) => {
+ const statementEntitiesArray = (Array.isArray(payload.items) ? payload.items : [])
+ .filter((item) => item?.id != null)
+ .map((item) => ({...state[item.id], info: item}));
+
+ if (statementEntitiesArray.length === 0) {
+ return state;
+ }
+
+ return {
+ ...state,
+ ...arrayToEntities(statementEntitiesArray, (entity) => entity.info.id)
+ };
+ })
+);
diff --git a/src/app/store/statements/reducers/index.ts b/src/app/store/statements/reducers/index.ts
index b62b1f8..317b0f6 100644
--- a/src/app/store/statements/reducers/index.ts
+++ b/src/app/store/statements/reducers/index.ts
@@ -11,4 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-export * from "./statement-entities.reducer";
+export * from "./entities/statement-entities.reducer";
+export * from "./loading/statement-loading.reducer";
+export * from "./search/statement-search.reducer";
+export * from "./new-statement/new-statement-form.reducer";
diff --git a/src/app/store/statements/reducers/loading/statement-loading.reducer.spec.ts b/src/app/store/statements/reducers/loading/statement-loading.reducer.spec.ts
new file mode 100644
index 0000000..487d734
--- /dev/null
+++ b/src/app/store/statements/reducers/loading/statement-loading.reducer.spec.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 {setStatementLoadingAction} from "../../actions";
+import {IStatementLoadingState} from "../../model";
+import {statementLoadingReducer} from "./statement-loading.reducer";
+
+describe("statementLoadingReducer", () => {
+
+ it("should update loading state ", () => {
+ const initialState: IStatementLoadingState = {search: true};
+ let action = setStatementLoadingAction({loading: {}});
+ let state = statementLoadingReducer(initialState, action);
+ expect(state).toEqual(initialState);
+ action = setStatementLoadingAction({loading: {search: false}});
+ state = statementLoadingReducer(initialState, action);
+ expect(state).toEqual({...initialState, search: false});
+ });
+
+});
diff --git a/src/app/store/statements/reducers/loading/statement-loading.reducer.ts b/src/app/store/statements/reducers/loading/statement-loading.reducer.ts
new file mode 100644
index 0000000..8ef62ea
--- /dev/null
+++ b/src/app/store/statements/reducers/loading/statement-loading.reducer.ts
@@ -0,0 +1,21 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {createReducer, on} from "@ngrx/store";
+import {setStatementLoadingAction} from "../../actions";
+import {IStatementLoadingState} from "../../model";
+
+export const statementLoadingReducer = createReducer<IStatementLoadingState>(
+ undefined,
+ on(setStatementLoadingAction, (state, action) => ({...state, ...action.loading}))
+);
diff --git a/src/app/store/statements/reducers/new-statement/new-statement-form.reducer.spec.ts b/src/app/store/statements/reducers/new-statement/new-statement-form.reducer.spec.ts
new file mode 100644
index 0000000..0518e7b
--- /dev/null
+++ b/src/app/store/statements/reducers/new-statement/new-statement-form.reducer.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 {clearNewStatementFormAction, setNewStatementProgressAction, submitNewStatementAction} from "../../actions";
+import {IStatementInfoForm, IStatementInformationFormValue} from "../../model";
+import {newStatementFormReducer} from "./new-statement-form.reducer";
+
+describe("newStatementFormReducer", () => {
+
+ const value = {} as IStatementInformationFormValue;
+
+ it("should update the form value on submit", () => {
+ const initialState = undefined;
+ const action = submitNewStatementAction({value});
+ const state = newStatementFormReducer(initialState, action);
+ expect(state).toEqual({value, isLoading: true, error: undefined});
+ });
+
+ it("should clear the form", () => {
+ const initialState = {} as IStatementInfoForm;
+ const action = clearNewStatementFormAction();
+ const state = newStatementFormReducer(initialState, action);
+ expect(state).toEqual(undefined);
+ });
+
+ it("should update the progress information", () => {
+ const action = setNewStatementProgressAction({isLoading: true, error: undefined});
+ const state = newStatementFormReducer({value}, action);
+ expect(state).toEqual({value, isLoading: true, error: undefined});
+ });
+
+});
diff --git a/src/app/store/statements/reducers/new-statement/new-statement-form.reducer.ts b/src/app/store/statements/reducers/new-statement/new-statement-form.reducer.ts
new file mode 100644
index 0000000..e88fdcf
--- /dev/null
+++ b/src/app/store/statements/reducers/new-statement/new-statement-form.reducer.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 {createReducer, on} from "@ngrx/store";
+import {clearNewStatementFormAction, setNewStatementProgressAction, submitNewStatementAction} from "../../actions";
+import {IStatementInfoForm} from "../../model";
+
+export const newStatementFormReducer = createReducer<IStatementInfoForm>(
+ undefined,
+ on(submitNewStatementAction, (state, payload) => {
+ return {
+ value: payload.value,
+ isLoading: true,
+ error: undefined
+ };
+ }),
+ on(clearNewStatementFormAction, () => undefined),
+ on(setNewStatementProgressAction, (state, payload) => {
+ return {
+ ...state,
+ isLoading: payload.isLoading,
+ error: payload.error
+ };
+ })
+);
diff --git a/src/app/store/statements/reducers/search/statement-search.reducer.spec.ts b/src/app/store/statements/reducers/search/statement-search.reducer.spec.ts
new file mode 100644
index 0000000..535d37f
--- /dev/null
+++ b/src/app/store/statements/reducers/search/statement-search.reducer.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 {IAPISearchOptions} from "../../../../core";
+import {createPaginationResponseMock, createStatementModelMock} from "../../../../test";
+import {setStatementSearchResultAction, startStatementSearchAction} from "../../actions";
+import {statementSearchReducer} from "./statement-search.reducer";
+
+describe("statementSearchReducer", () => {
+
+ it("should not affect state on start search action ", () => {
+ const initialState = createPaginationResponseMock([0]);
+ const options: IAPISearchOptions = {q: ""};
+ const action = startStatementSearchAction({options});
+ const state = statementSearchReducer(initialState, action);
+ expect(state).toBe(initialState);
+ });
+
+ it("should set search results on set search result action", () => {
+ const results = createPaginationResponseMock([createStatementModelMock(18), createStatementModelMock(19)]);
+ const search = createPaginationResponseMock([18, 19]);
+ const action = setStatementSearchResultAction({results});
+
+ let state = statementSearchReducer(undefined, action);
+ expect(state).toEqual(search);
+
+ results.content.push(null, createStatementModelMock(null));
+ state = statementSearchReducer(undefined, action);
+ expect(state).toEqual(search);
+
+ results.content = null;
+ search.content = [];
+ state = statementSearchReducer(undefined, action);
+ expect(state).toEqual(search);
+
+ state = statementSearchReducer(undefined, setStatementSearchResultAction({results: undefined}));
+ expect(state).toEqual(undefined);
+ });
+
+});
diff --git a/src/app/store/statements/reducers/search/statement-search.reducer.ts b/src/app/store/statements/reducers/search/statement-search.reducer.ts
new file mode 100644
index 0000000..5e34a60
--- /dev/null
+++ b/src/app/store/statements/reducers/search/statement-search.reducer.ts
@@ -0,0 +1,31 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {createReducer, on} from "@ngrx/store";
+import {IAPIPaginationResponse} from "../../../../core";
+import {setStatementSearchResultAction} from "../../actions";
+
+export const statementSearchReducer = createReducer<IAPIPaginationResponse<number>>(
+ undefined,
+ on(setStatementSearchResultAction, (state, action) => {
+ if (action.results == null) {
+ return undefined;
+ }
+ return {
+ ...action.results,
+ content: (Array.isArray(action.results.content) ? action.results.content : [])
+ .filter((statement) => statement?.id != null)
+ .map((statement) => statement.id)
+ };
+ })
+);
diff --git a/src/app/store/statements/selectors/index.ts b/src/app/store/statements/selectors/index.ts
index 80c8215..be2e36b 100644
--- a/src/app/store/statements/selectors/index.ts
+++ b/src/app/store/statements/selectors/index.ts
@@ -13,6 +13,8 @@
export * from "./list/statement-list.selectors";
export * from "./new-statement-form/new-statement-form.selectors";
+export * from "./search";
+export * from "./statement-information-form/statement-information-form.selectors";
export * from "./workflow-form/workflow-form.selectors";
export * from "./statements-store-state.selectors";
diff --git a/src/app/store/statements/selectors/search/index.ts b/src/app/store/statements/selectors/search/index.ts
new file mode 100644
index 0000000..267229e
--- /dev/null
+++ b/src/app/store/statements/selectors/search/index.ts
@@ -0,0 +1,14 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+export * from "./statement-search.selectors";
diff --git a/src/app/store/statements/selectors/search/statement-search.selectors.spec.ts b/src/app/store/statements/selectors/search/statement-search.selectors.spec.ts
new file mode 100644
index 0000000..0f28c02
--- /dev/null
+++ b/src/app/store/statements/selectors/search/statement-search.selectors.spec.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 {createPaginationResponseMock, createStatementModelMock} from "../../../../test";
+import {getSearchContentStatementsSelector} from "./statement-search.selectors";
+
+describe("statementSelectors", () => {
+
+ it("getSearchContentStatementsSelector", () => {
+ const search = createPaginationResponseMock([19, 18, 17]);
+ const entities = {
+ 18: {
+ info: createStatementModelMock(18)
+ },
+ 19: {
+ info: createStatementModelMock(19)
+ }
+ };
+
+ expect(getSearchContentStatementsSelector.projector(null, null)).toEqual([]);
+ expect(getSearchContentStatementsSelector.projector(entities, null)).toEqual([]);
+ expect(getSearchContentStatementsSelector.projector(null, search)).toEqual([]);
+ expect(getSearchContentStatementsSelector.projector(null, {...search, content: null})).toEqual([]);
+ expect(getSearchContentStatementsSelector.projector(entities, search))
+ .toEqual([createStatementModelMock(19), createStatementModelMock(18)]);
+ });
+
+});
diff --git a/src/app/store/statements/selectors/search/statement-search.selectors.ts b/src/app/store/statements/selectors/search/statement-search.selectors.ts
new file mode 100644
index 0000000..9ffa58f
--- /dev/null
+++ b/src/app/store/statements/selectors/search/statement-search.selectors.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 {createSelector} from "@ngrx/store";
+import {getStatementSearchSelector, statementEntitiesSelector} from "../statements-store-state.selectors";
+
+export const getSearchContentStatementsSelector = createSelector(
+ statementEntitiesSelector,
+ getStatementSearchSelector,
+ (entities, search) => {
+ if (Array.isArray(search?.content) && entities != null) {
+ return search.content
+ .map((id) => entities[id]?.info)
+ .filter((_) => _ != null);
+ }
+ return [];
+ }
+);
diff --git a/src/app/store/statements/selectors/statement-information-form/statement-information-form.selectors.ts b/src/app/store/statements/selectors/statement-information-form/statement-information-form.selectors.ts
new file mode 100644
index 0000000..bea0c8c
--- /dev/null
+++ b/src/app/store/statements/selectors/statement-information-form/statement-information-form.selectors.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 {createSelector} from "@ngrx/store";
+import {IStatementInformationFormValue} from "../../model";
+import {statementInfoSelector} from "../statements-store-state.selectors";
+
+export const statementInformationFormValueSelector = createSelector(
+ statementInfoSelector,
+ (info): Partial<IStatementInformationFormValue> => {
+ return {
+ ...info
+ };
+ }
+);
diff --git a/src/app/store/statements/selectors/statements-store-state.selectors.spec.ts b/src/app/store/statements/selectors/statements-store-state.selectors.spec.ts
index c619eb7..415d985 100644
--- a/src/app/store/statements/selectors/statements-store-state.selectors.spec.ts
+++ b/src/app/store/statements/selectors/statements-store-state.selectors.spec.ts
@@ -11,12 +11,17 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+import {IAPISectorsModel} from "../../../core/api/statements/IAPISectorsModel";
+import {IStatementEntity} from "../model";
import {
- statementAttachmentsSelector,
+ getStatementLoadingSelector,
+ getStatementSearchSelector,
+ getStatementSectorsSelector,
statementCommentsSelector,
statementConfigurationSelector,
statementEntitiesSelector,
statementInfoSelector,
+ statementParentIdsSelector,
statementSelector,
statementWorkflowSelector
} from "./statements-store-state.selectors";
@@ -60,12 +65,48 @@
expectNullChecks(statementWorkflowSelector.projector, "workflow");
});
- it("statementAttachmentsSelector", () => {
- expectNullChecks(statementAttachmentsSelector.projector, "attachments", [1, 2, 3], []);
+ it("statementParentIdsSelector", () => {
+ expectNullChecks(statementParentIdsSelector.projector, "parentIds", [1, 2, 3], []);
});
it("statementCommentsSelector", () => {
expectNullChecks(statementCommentsSelector.projector, "comments", [1, 2, 3], []);
});
+
+ it("getStatementSearchSelector", () => {
+ const search = {} as any;
+ expect(getStatementSearchSelector.projector(undefined)).not.toBeDefined();
+ expect(getStatementSearchSelector.projector({search})).toBe(search);
+ });
+
+ it("getStatementLoadingSelector", () => {
+ const loading = {} as any;
+ expect(getStatementLoadingSelector.projector(undefined)).not.toBeDefined();
+ expect(getStatementLoadingSelector.projector({loading})).toBe(loading);
+ });
+
+ it("getStatementSectorsSelector", () => {
+ const args: { settings: IAPISectorsModel, statement: IStatementEntity } = {
+ settings: {},
+ statement: {}
+ };
+ expect(getStatementSectorsSelector.projector(args)).not.toBeDefined();
+ expect(getStatementSectorsSelector.projector(undefined)).not.toBeDefined();
+ args.settings = {
+ "Ort#Ortsteil": [
+ "Strom", "Gas", "Beleuchtung"
+ ]
+ };
+ expect(getStatementSectorsSelector.projector(args)).not.toEqual(args.settings);
+ args.statement = {
+ sectors: {
+ "Stadt#Stadtteil": [
+ "Strom", "Gas"
+ ]
+ }
+ };
+ expect(getStatementSectorsSelector.projector(args)).not.toEqual(args.statement.sectors);
+ });
+
});
diff --git a/src/app/store/statements/selectors/statements-store-state.selectors.ts b/src/app/store/statements/selectors/statements-store-state.selectors.ts
index 40b1b20..59e0f1f 100644
--- a/src/app/store/statements/selectors/statements-store-state.selectors.ts
+++ b/src/app/store/statements/selectors/statements-store-state.selectors.ts
@@ -13,6 +13,7 @@
import {createFeatureSelector, createSelector} from "@ngrx/store";
import {queryParamsIdSelector} from "../../root/selectors";
+import {settingsStoreSelector} from "../../settings/selectors";
import {IStatementEntity, IStatementsStoreState} from "../model";
import {STATEMENTS_NAME} from "../statements-reducers.token";
@@ -44,12 +45,29 @@
(statement) => statement?.workflow
);
-export const statementAttachmentsSelector = createSelector(
+export const statementParentIdsSelector = createSelector(
statementSelector,
- (statement) => Array.isArray(statement?.attachments) ? statement.attachments : []
+ (statement) => Array.isArray(statement?.parentIds) ? statement.parentIds : []
);
export const statementCommentsSelector = createSelector(
statementSelector,
(statement) => Array.isArray(statement?.comments) ? statement.comments : []
);
+
+
+export const getStatementSearchSelector = createSelector(
+ statementsStoreStateSelector,
+ (state) => state?.search
+);
+
+export const getStatementLoadingSelector = createSelector(
+ statementsStoreStateSelector,
+ (state) => state?.loading
+);
+
+export const getStatementSectorsSelector = createSelector(
+ settingsStoreSelector,
+ statementSelector,
+ (settings, statement) => statement ? statement.sectors : settings?.sectors
+);
diff --git a/src/app/store/statements/selectors/workflow-form/workflow-form.selectors.spec.ts b/src/app/store/statements/selectors/workflow-form/workflow-form.selectors.spec.ts
index a1a5a57..0fac432 100644
--- a/src/app/store/statements/selectors/workflow-form/workflow-form.selectors.spec.ts
+++ b/src/app/store/statements/selectors/workflow-form/workflow-form.selectors.spec.ts
@@ -11,6 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+import {createWorkflowDataMock} from "../../../../test";
import {IDepartmentOptionValue} from "../../model";
import {
departmentGroupsObjectSelector,
@@ -109,23 +110,29 @@
});
it("workflowFormValueSelector", () => {
- const geographicPosition = "geoPosition";
const departments = getValues(19, "Group");
+ const geoPosition = "geoPosition";
+ const parentIds = [18, 19];
- let result = workflowFormValueSelector.projector(undefined, undefined);
- expect(result.departments).not.toBeDefined();
+ let result = workflowFormValueSelector.projector(undefined, undefined, undefined);
+ console.log("adsds", result);
+ expect(result.departments).toEqual([]);
expect(result.geographicPosition).not.toBeDefined();
+ expect(result.parentIds).toEqual([]);
- result = workflowFormValueSelector.projector(undefined, departments);
- expect(result.departments).toBe(departments);
+ result = workflowFormValueSelector.projector(undefined, departments, undefined);
+ expect(result.departments).toEqual(departments);
expect(result.geographicPosition).not.toBeDefined();
+ expect(result.parentIds).toEqual([]);
- result = workflowFormValueSelector.projector({geoPosition: undefined} as any, departments);
- expect(result.departments).toBe(departments);
+ result = workflowFormValueSelector.projector(createWorkflowDataMock({geoPosition: undefined}), departments, undefined);
+ expect(result.departments).toEqual(departments);
expect(result.geographicPosition).not.toBeDefined();
+ expect(result.parentIds).toEqual([]);
- result = workflowFormValueSelector.projector({geoPosition: geographicPosition} as any, departments);
- expect(result.departments).toBe(departments);
- expect(result.geographicPosition).toBe(geographicPosition);
+ result = workflowFormValueSelector.projector(createWorkflowDataMock({geoPosition}), departments, parentIds);
+ expect(result.departments).toEqual(departments);
+ expect(result.geographicPosition).toBe(geoPosition);
+ expect(result.parentIds).toEqual(parentIds);
});
});
diff --git a/src/app/store/statements/selectors/workflow-form/workflow-form.selectors.ts b/src/app/store/statements/selectors/workflow-form/workflow-form.selectors.ts
index 1c0c43c..1facbf2 100644
--- a/src/app/store/statements/selectors/workflow-form/workflow-form.selectors.ts
+++ b/src/app/store/statements/selectors/workflow-form/workflow-form.selectors.ts
@@ -15,7 +15,7 @@
import {ISelectOption, ISelectOptionGroup} from "../../../../shared/controls/select";
import {arrayJoin, objectToArray} from "../../../../util/store";
import {IDepartmentOptionValue, IWorkflowFormValue} from "../../model";
-import {statementConfigurationSelector, statementWorkflowSelector} from "../statements-store-state.selectors";
+import {statementConfigurationSelector, statementParentIdsSelector, statementWorkflowSelector} from "../statements-store-state.selectors";
export const departmentGroupsObjectSelector = createSelector(
statementConfigurationSelector,
@@ -75,10 +75,12 @@
export const workflowFormValueSelector = createSelector(
statementWorkflowSelector,
selectedDepartmentSelector,
- (workflow, departments): IWorkflowFormValue => {
+ statementParentIdsSelector,
+ (workflow, departments, parentIds): IWorkflowFormValue => {
return {
+ departments: arrayJoin(departments),
geographicPosition: workflow?.geoPosition,
- departments
+ parentIds: arrayJoin(parentIds)
};
}
);
diff --git a/src/app/store/statements/statements-reducers.token.ts b/src/app/store/statements/statements-reducers.token.ts
index a4b925f..5f9dd97 100644
--- a/src/app/store/statements/statements-reducers.token.ts
+++ b/src/app/store/statements/statements-reducers.token.ts
@@ -14,13 +14,16 @@
import {InjectionToken} from "@angular/core";
import {ActionReducerMap} from "@ngrx/store";
import {IStatementsStoreState} from "./model";
-import {statementEntitiesReducer} from "./reducers";
+import {newStatementFormReducer, statementEntitiesReducer, statementLoadingReducer, statementSearchReducer} from "./reducers";
export const STATEMENTS_NAME = "statements";
export const STATEMENTS_REDUCER = new InjectionToken<ActionReducerMap<IStatementsStoreState>>("Statements store reducer", {
providedIn: "root",
factory: () => ({
- entities: statementEntitiesReducer
+ entities: statementEntitiesReducer,
+ search: statementSearchReducer,
+ newStatementForm: newStatementFormReducer,
+ loading: statementLoadingReducer
})
});
diff --git a/src/app/store/statements/statements-store.module.ts b/src/app/store/statements/statements-store.module.ts
index 9595bcd..840884b 100644
--- a/src/app/store/statements/statements-store.module.ts
+++ b/src/app/store/statements/statements-store.module.ts
@@ -14,7 +14,13 @@
import {NgModule} from "@angular/core";
import {EffectsModule} from "@ngrx/effects";
import {StoreModule} from "@ngrx/store";
-import {CommentsEffect, FetchStatementDetailsEffect, SubmitInfoFormEffect, SubmitWorkflowFormEffect} from "./effects";
+import {
+ CommentsEffect,
+ FetchStatementDetailsEffect,
+ SearchStatementsEffect,
+ SubmitStatementInformationFormEffect,
+ SubmitWorkflowFormEffect
+} from "./effects";
import {STATEMENTS_NAME, STATEMENTS_REDUCER} from "./statements-reducers.token";
@NgModule({
@@ -23,7 +29,8 @@
EffectsModule.forFeature([
CommentsEffect,
FetchStatementDetailsEffect,
- SubmitInfoFormEffect,
+ SearchStatementsEffect,
+ SubmitStatementInformationFormEffect,
SubmitWorkflowFormEffect
])
]
diff --git a/src/app/test/create-attachment-model-mock.spec.ts b/src/app/test/create-attachment-model-mock.spec.ts
new file mode 100644
index 0000000..e1021c3
--- /dev/null
+++ b/src/app/test/create-attachment-model-mock.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 {IAPIAttachmentModel} from "../core/api/attachments";
+
+export function createAttachmentModelMock(attachmentId: number, tagIds: number[] = []): IAPIAttachmentModel {
+ return {
+ id: attachmentId,
+ name: "Attachment" + attachmentId,
+ tagIds,
+ size: Math.floor(Math.random() * 1024),
+ timestamp: new Date().toISOString(),
+ type: "text/plain"
+ };
+}
diff --git a/src/app/test/create-file-mock.spec.ts b/src/app/test/create-file-mock.spec.ts
new file mode 100644
index 0000000..f9ca1ca
--- /dev/null
+++ b/src/app/test/create-file-mock.spec.ts
@@ -0,0 +1,22 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+export class FileMock extends File {
+ public constructor(public readonly _name: string) {
+ super([], _name);
+ }
+}
+
+export function createFileMock(name: string): File {
+ return new FileMock(name);
+}
diff --git a/src/app/test/create-pagination-response-mock.spec.ts b/src/app/test/create-pagination-response-mock.spec.ts
new file mode 100644
index 0000000..0a916ee
--- /dev/null
+++ b/src/app/test/create-pagination-response-mock.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 {IAPIPaginationResponse} from "../core/api/shared";
+
+
+export function createPaginationResponseMock<T>(content: T[]): IAPIPaginationResponse<T> {
+ return {
+ content,
+ pageable: "INSTANCE",
+ last: true,
+ totalPages: 1,
+ totalElements: content?.length,
+ size: 0,
+ number: 0,
+ numberOfElements: content?.length,
+ first: true,
+ sort: {
+ sorted: false,
+ unsorted: true,
+ empty: true
+ },
+ empty: false
+ };
+}
diff --git a/src/app/test/create-select-options.spec.ts b/src/app/test/create-select-options.spec.ts
new file mode 100644
index 0000000..7e190e4
--- /dev/null
+++ b/src/app/test/create-select-options.spec.ts
@@ -0,0 +1,18 @@
+/********************************************************************************
+ * 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 {ISelectOption} from "../shared/controls/select/model";
+
+export function createSelectOptionsMock(size: number, name = "Option"): ISelectOption[] {
+ return Array(size).fill(0).map((_, id) => ({label: name + " " + id, value: id}));
+}
diff --git a/src/app/test/create-statement-model-mock.spec.ts b/src/app/test/create-statement-model-mock.spec.ts
new file mode 100644
index 0000000..8a705b4
--- /dev/null
+++ b/src/app/test/create-statement-model-mock.spec.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 {IAPIStatementModel} from "../core/api/statements";
+
+export function createStatementModelMock(id: number, typeId: number = 1): IAPIStatementModel {
+ return {
+ id,
+ title: "Title " + id,
+ dueDate: "2019-09-10",
+ receiptDate: "2019-09-10",
+ finished: true,
+ typeId,
+ city: "Darmstadt",
+ district: "Heppenheim",
+ contactId: "ABCD"
+ };
+}
diff --git a/src/app/test/create-workflow-data-mock.spec.ts b/src/app/test/create-workflow-data-mock.spec.ts
new file mode 100644
index 0000000..87eee50
--- /dev/null
+++ b/src/app/test/create-workflow-data-mock.spec.ts
@@ -0,0 +1,22 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {IAPIWorkflowData} from "../core/api/statements";
+
+export function createWorkflowDataMock(partialData: Partial<IAPIWorkflowData> = {}): IAPIWorkflowData {
+ return {
+ selectedDepartments: {},
+ geoPosition: "",
+ ...partialData
+ };
+}
diff --git a/src/app/test/index.ts b/src/app/test/index.ts
new file mode 100644
index 0000000..108c792
--- /dev/null
+++ b/src/app/test/index.ts
@@ -0,0 +1,19 @@
+/********************************************************************************
+ * Copyright (c) 2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+export * from "./create-attachment-model-mock.spec";
+export * from "./create-file-mock.spec";
+export * from "./create-pagination-response-mock.spec";
+export * from "./create-select-options.spec";
+export * from "./create-statement-model-mock.spec";
+export * from "./create-workflow-data-mock.spec";
diff --git a/src/app/util/http/http.util.spec.ts b/src/app/util/http/http.util.spec.ts
index 0580b85..1781407 100644
--- a/src/app/util/http/http.util.spec.ts
+++ b/src/app/util/http/http.util.spec.ts
@@ -13,7 +13,7 @@
import {HttpErrorResponse} from "@angular/common/http";
import {EHttpStatusCodes} from "./EHttpStatusCodes";
-import {isHttpErrorWithStatus, urlJoin} from "./http.util";
+import {isHttpErrorWithStatus, objectToHttpParams, urlJoin} from "./http.util";
describe("HttpUtil", () => {
@@ -40,5 +40,11 @@
expect(isHttpErrorWithStatus(httpError, EHttpStatusCodes.UNAUTHORIZED, EHttpStatusCodes.FORBIDDEN)).toBeTrue();
});
+ it("should transform objects to http params", () => {
+ expect(objectToHttpParams(undefined)).toEqual({});
+ expect(objectToHttpParams(null)).toEqual({});
+ expect(objectToHttpParams({a: "1", b: 9, c: ["19", 1919]})).toEqual({a: "1", b: "9", c: ["19", "1919"]});
+ });
+
});
diff --git a/src/app/util/http/http.util.ts b/src/app/util/http/http.util.ts
index 78b40c1..58aac99 100644
--- a/src/app/util/http/http.util.ts
+++ b/src/app/util/http/http.util.ts
@@ -10,6 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+
import {HttpErrorResponse} from "@angular/common/http";
import {EHttpStatusCodes} from "./EHttpStatusCodes";
@@ -31,3 +32,18 @@
return indexOfProtocol === -1 ? result : protocol + ":/" + result;
}
+
+export function objectToHttpParams<T extends object>(
+ object: { [key: string]: string | number | Array<string | number> }
+): { [key: string]: string | string[] } {
+ if (object == null) {
+ return {};
+ }
+ const result: { [key: string]: string | string[] } = {};
+ Object.entries(object)
+ .filter(([key, value]) => value != null)
+ .forEach(([key, value]) => {
+ result[key] = Array.isArray(value) ? value.map((_) => "" + _) : "" + value;
+ });
+ return result;
+}
diff --git a/src/app/util/store/store.util.spec.ts b/src/app/util/store/store.util.spec.ts
index 721a749..fe15169 100644
--- a/src/app/util/store/store.util.spec.ts
+++ b/src/app/util/store/store.util.spec.ts
@@ -11,7 +11,17 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
-import {arrayJoin, arrayToEntities, deleteEntities, entitiesToArray, filterDistinctValues, objectToArray} from "./store.util";
+import {
+ arrayJoin,
+ arrayToEntities,
+ deleteEntities,
+ entitiesToArray,
+ filterDistinctValues,
+ objectToArray,
+ setEntitiesObject,
+ TStoreEntities,
+ updateEntitiesObject
+} from "./store.util";
describe("StoreUtil", () => {
@@ -62,6 +72,34 @@
expect(arrayJoin(undefined, null)).toEqual([]);
});
+ it("should set entities object", () => {
+ const state: TStoreEntities<{ id: number; property: any }> = {1: {id: 1, property: "18"}};
+ expect(setEntitiesObject(null, null, () => null)).toEqual(null);
+ expect(setEntitiesObject(state, null, () => null)).toBe(state);
+ expect(setEntitiesObject(state, [], () => null)).toBe(state);
+ expect(setEntitiesObject(state, [null, {id: 1, property: "19"}], () => null)).toBe(state);
+ expect(setEntitiesObject(state, [null, {id: 1, property: "19"}], (item) => item.id))
+ .toEqual({1: {id: 1, property: "19"}});
+ expect(setEntitiesObject(state, [null, {id: 1, property: "19"}], (item) => item.id))
+ .toEqual({1: {id: 1, property: "19"}});
+ expect(setEntitiesObject<{ id: number; property: any }>(undefined, [null, {id: 1, property: "19"}], (item) => item.id))
+ .toEqual({1: {id: 1, property: "19"}});
+ });
+
+ it("should update entities object", () => {
+ const state: TStoreEntities<{ id: number; property: any }> = {1: {id: 1, property: "18"}};
+ expect(updateEntitiesObject(null, null, () => null)).toEqual(null);
+ expect(updateEntitiesObject(state, null, () => null)).toBe(state);
+ expect(updateEntitiesObject(state, [], () => null)).toBe(state);
+ expect(updateEntitiesObject(state, [null, {id: 1}], () => null)).toBe(state);
+ expect(updateEntitiesObject(state, [null, {id: 1}], (item) => item.id))
+ .toEqual({1: {id: 1, property: "18"}});
+ expect(updateEntitiesObject(state, [null, {id: 1, property: "19"}], (item) => item.id))
+ .toEqual({1: {id: 1, property: "19"}});
+ expect(updateEntitiesObject<{ id: number; property: any }>(undefined, [null, {id: 1, property: "19"}], (item) => item.id))
+ .toEqual({1: {id: 1, property: "19"}});
+ });
+
it("should filter distinct values in arrays", () => {
const array = [1, 1, 2, 3, 3, 3, undefined, 1, 2, 3, 3, null, null];
expect(filterDistinctValues(array)).toEqual([1, 2, 3]);
diff --git a/src/app/util/store/store.util.ts b/src/app/util/store/store.util.ts
index ed647d2..76e1439 100644
--- a/src/app/util/store/store.util.ts
+++ b/src/app/util/store/store.util.ts
@@ -50,6 +50,59 @@
}), {});
}
+export function setEntitiesObject<T>(
+ object: TStoreEntities<T>,
+ list: T[],
+ getId: (item: T) => number | string
+): TStoreEntities<T> {
+ if (!Array.isArray(list)) {
+ return object;
+ }
+
+ if (object == null) {
+ object = {};
+ }
+
+ list = list.filter((item) => item != null && getId(item) != null);
+ return list.length === 0 ? object : list.reduce<TStoreEntities<T>>((result, item) => {
+ const id = getId(item);
+ return {
+ ...result,
+ [id]: item
+ };
+ }, object);
+}
+
+/**
+ * Updates an object with a list of entities. For each list item, a unique id is created via the given arrow function.
+ */
+export function updateEntitiesObject<T>(
+ object: TStoreEntities<T>,
+ list: Partial<T>[],
+ getId: (item: Partial<T>) => number | string
+): TStoreEntities<T> {
+ if (!Array.isArray(list)) {
+ return object;
+ }
+
+ if (object == null) {
+ object = {};
+ }
+
+ list = list.filter((item) => item != null && getId(item) != null);
+
+ return list.length === 0 ? object : list.reduce<TStoreEntities<T>>((result, item) => {
+ const id = getId(item);
+ return {
+ ...result,
+ [id]: {
+ ...result[id],
+ ...item
+ }
+ };
+ }, object);
+}
+
/**
* Join two lists of items. If an argument is not an array, it is cast to an empty array.
*/
diff --git a/src/assets/i18n/de.i18.json b/src/assets/i18n/de.i18.json
index 1f3066a..d7ab5e3 100644
--- a/src/assets/i18n/de.i18.json
+++ b/src/assets/i18n/de.i18.json
@@ -43,30 +43,11 @@
}
}
},
- "new": {
- "title": "Neue Stellungnahme hinzufügen",
- "form": {
- "title": "Titel",
- "dueDate": "Frist",
- "receiptDate": "Eingangsdatum",
- "city": "Ort",
- "district": "Ortsteil",
- "typeId": "Art des Vorgangs",
- "attachments": "Anhänge"
- },
- "error": {
- "invalidForm": "Alle Pflichtfelder müssen ausgefüllt werden.",
- "unknownError": "Ein Fehler ist aufgetreten."
- },
- "actions": {
- "submit": "Stellungnahme hinzufügen"
- }
- },
"details": {
"button": {
"createDraftForNegativeAnswer": "Negativantwort erstellen",
- "addBasicInfoData": "Basisinformationen ergänzen",
- "addWorkflowData": "Workflowinformationen anlegen",
+ "addBasicInfoData": "Basisinformationen bearbeiten",
+ "addWorkflowData": "Workflowinformationen bearbeiten",
"createDraft": "Entwurf erstellen",
"enrichDraft": "Entwurf bearbeiten",
"checkForCompleteness": "Auf Vollständigkeit prüfen",
@@ -78,21 +59,8 @@
"edit": {
"title": "Stellungnahme bearbeiten",
"loading": "Stellungnahme wird geladen...",
- "negativeAnswer": "Negativmeldung",
"createDraftForNegativeAnswer": "Negativmeldung verfassen",
- "sendAnswer": "Antwort versenden",
- "action": {
- "save": "Speichern",
- "submitWorkflowForm": "Workflowdaten festlegen"
- },
- "boxTitle": {
- "generalInformation": "Allgemeine Informationen",
- "documentsInbox": "Eingehende Dokumente",
- "geographicPosition": "Geographische Position",
- "departments": "Betroffene Fachbereiche",
- "linkedIssues": "Verknüpfte Vorgänge",
- "comments": "Kommentare"
- }
+ "sendAnswer": "Antwort versenden"
},
"workflow": {
"process": "Prozess",
@@ -106,20 +74,84 @@
}
}
},
+ "attachments": {
+ "edit": "Anhänge übernehmen:",
+ "add": "Anhänge hinzufügen:",
+ "selectFile": "Datei auswählen"
+ },
"comments": {
"title": "Kommentare",
"showPrevious": "Vorherige anzeigen...",
"showAll": "Alle anzeigen...",
"placeholder": "Einen Kommentar anlegen..."
},
+ "contacts": {
+ "title": "Kontakte",
+ "selectContact": "Bitte wählen Sie eine Kontaktperson aus.",
+ "searchContact": "Nach einem Kontakt suchen...",
+ "search": "Suche",
+ "name": "Name",
+ "firstName": "Vorname",
+ "email": "Email",
+ "company": "Firma",
+ "addNew": "Neuen Kontakt hinzufügen"
+ },
+ "statementInformationForm": {
+ "title": "Informationsdatensatz bearbeiten",
+ "titleNew": "Stellungnahme anlegen",
+ "container": {
+ "general": "Allgemeine Informationen",
+ "contact": "Kontakt",
+ "inboxAttachments": "Eingangsdokumente"
+ },
+ "controls": {
+ "title": "Titel:",
+ "dueDate": "Frist:",
+ "receiptDate": "Eingangsdatum:",
+ "city": "Ort:",
+ "district": "Ortsteil:",
+ "typeId": "Art des Vorgangs:"
+ },
+ "submit": "Speichern",
+ "submitAndReject": "Negativantwort erstellen",
+ "submitAndComplete": "Informationsdaten festlegen"
+ },
+ "workflowDataForm": {
+ "title": "Workflowdatensatz bearbeiten",
+ "container": {
+ "general": "Allgemeine Informationen",
+ "inboxAttachments": "Eingangsdokumente",
+ "geographicPosition": "Geographische Position",
+ "departments": "Betroffene Fachbereiche",
+ "linkedIssues": "Verknüpfte Vorgänge"
+ },
+ "submit": "Speichern",
+ "submitAndComplete": "Workflowdaten festlegen"
+ },
"shared": {
- "attachments-control": {
- "add": "Anhänge hinzufügen",
- "drop": "Anhänge per Drag & Drop hier ablegen"
+ "linkedStatements": {
+ "precedingStatements": "Vorhergehende Vorgänge",
+ "successiveStatements": "Nachfolgende Vorgänge"
+ },
+ "statementTable": {
+ "caption": "Stellungnahmen",
+ "id": "ID",
+ "title": "Titel",
+ "statementType": "Vorgangstyp",
+ "receiptDate": "Eingangsdatum",
+ "city": "Ort",
+ "district": "Ortsteil"
+ },
+ "statementSelect": {
+ "clear": "Auswahl löschen"
},
"actions": {
"save": "Speichern",
"delete": "Löschen"
+ },
+ "sectors": {
+ "available": "In diesem Ortsteil sind folgende Sparten betroffen:",
+ "none": "In diesem Ortsteil konnte keine zuständige Sparte ausgemacht werden."
}
}
}
diff --git a/src/theme/user-controls/_input.theme.scss b/src/theme/user-controls/_input.theme.scss
index d066838..980dab6 100644
--- a/src/theme/user-controls/_input.theme.scss
+++ b/src/theme/user-controls/_input.theme.scss
@@ -43,7 +43,7 @@
.openk-primary {
&.openk-input,
- & > .openk-input {
+ .openk-input {
padding: 0.4em 0.85em 0.4em calc(0.85em - 3px);
border-left: 4px solid get-color($openk-primary-palette);
}
@@ -52,7 +52,7 @@
.openk-info {
&.openk-input,
- & > .openk-input {
+ .openk-input {
padding: 0.4em 0.85em 0.4em calc(0.85em - 3px);
border-left: 4px solid get-color($openk-info-palette);
}
@@ -61,7 +61,7 @@
.openk-success {
&.openk-input,
- & > .openk-input {
+ .openk-input {
padding: 0.4em 0.85em 0.4em calc(0.85em - 3px);
border-left: 4px solid get-color($openk-success-palette);
}
@@ -70,7 +70,7 @@
.openk-warning {
&.openk-input,
- & > .openk-input {
+ .openk-input {
padding: 0.4em 0.85em 0.4em calc(0.85em - 3px);
border-left: 4px solid get-color($openk-warning-palette);
}
@@ -79,7 +79,7 @@
.openk-danger {
&.openk-input,
- & > .openk-input {
+ .openk-input {
padding: 0.4em 0.85em 0.4em calc(0.85em - 3px);
border-left: 4px solid get-color($openk-danger-palette);
}