[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); }