From 8b8f16461a470a49a74a4dc3b30ec78d54f27629 Mon Sep 17 00:00:00 2001 From: mbritense <79840403+mbritense@users.noreply.github.com> Date: Tue, 28 May 2024 13:11:30 +0200 Subject: [PATCH] feat: case widget tab (#1021) * widgets tab component * widgets component no background * active tab key * wip * widgets container * spacing, start logic * fix resize speed * scale vertically with 1x height increments * fixes * compare against result height constraint * only render if packresult available * performance improvements * fianlzie * no results; change detection * pr comment * fix mistake * add widget config to widget block --- package-lock.json | 70 +++---- projects/valtimo/config/assets/core/de.json | 6 +- projects/valtimo/config/assets/core/en.json | 6 +- projects/valtimo/config/assets/core/nl.json | 6 +- .../src/lib/services/base-api.service.ts | 4 +- .../config/src/lib/services/config.service.ts | 4 + .../valtimo/config/src/lib/utils/url.utils.ts | 8 + projects/valtimo/dossier/ng-package.json | 4 +- projects/valtimo/dossier/package.json | 4 +- .../dossier-detail.component.scss | 4 + .../dossier-detail.component.ts | 1 + .../widget-block/widget-block.component.html | 41 ++++ .../widget-block/widget-block.component.scss | 24 +++ .../widget-block/widget-block.component.ts | 186 +++++++++++++++++ .../widgets-container.component.html | 26 +++ .../widgets-container.component.scss | 25 +++ .../widgets-container.component.ts | 75 +++++++ .../tab/widgets/widgets.component.html | 42 ++++ .../tab/widgets/widgets.component.scss | 21 ++ .../tab/widgets/widgets.component.ts | 78 ++++++++ .../lib/constants/case-widget.constants.ts | 20 ++ .../dossier/src/lib/constants/index.ts | 1 + .../valtimo/dossier/src/lib/dossier.module.ts | 2 + .../lib/models/case-widget-display.model.ts | 60 ++++++ .../src/lib/models/case-widget.model.ts | 99 +++++++++ .../valtimo/dossier/src/lib/models/index.ts | 2 + .../dossier/src/lib/models/tabs.model.ts | 8 +- .../src/lib/services/dossier-tab.service.ts | 26 ++- .../services/dossier-widgets-api-service.ts | 48 +++++ .../dossier-widgets-layout.service.ts | 188 ++++++++++++++++++ .../valtimo/dossier/src/lib/services/index.ts | 2 + 31 files changed, 1047 insertions(+), 44 deletions(-) create mode 100644 projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widget-block/widget-block.component.html create mode 100644 projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widget-block/widget-block.component.scss create mode 100644 projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widget-block/widget-block.component.ts create mode 100644 projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widgets-container/widgets-container.component.html create mode 100644 projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widgets-container/widgets-container.component.scss create mode 100644 projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widgets-container/widgets-container.component.ts create mode 100644 projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/widgets.component.html create mode 100644 projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/widgets.component.scss create mode 100644 projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/widgets.component.ts create mode 100644 projects/valtimo/dossier/src/lib/constants/case-widget.constants.ts create mode 100644 projects/valtimo/dossier/src/lib/models/case-widget-display.model.ts create mode 100644 projects/valtimo/dossier/src/lib/models/case-widget.model.ts create mode 100644 projects/valtimo/dossier/src/lib/services/dossier-widgets-api-service.ts create mode 100644 projects/valtimo/dossier/src/lib/services/dossier-widgets-layout.service.ts diff --git a/package-lock.json b/package-lock.json index 520bd5a88..b8fd031e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -163,7 +163,7 @@ }, "dist/valtimo/access-control": { "name": "@valtimo/access-control", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "dependencies": { "jwt-decode": "4.0.0", @@ -176,7 +176,7 @@ }, "dist/valtimo/access-control-management": { "name": "@valtimo/access-control-management", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "dependencies": { "tslib": "2.6.2" @@ -188,7 +188,7 @@ }, "dist/valtimo/account": { "name": "@valtimo/account", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -202,7 +202,7 @@ }, "dist/valtimo/analyse": { "name": "@valtimo/analyse", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -215,7 +215,7 @@ }, "dist/valtimo/bootstrap": { "name": "@valtimo/bootstrap", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -228,7 +228,7 @@ }, "dist/valtimo/case-migration": { "name": "@valtimo/case-migration", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -241,7 +241,7 @@ }, "dist/valtimo/choice-field": { "name": "@valtimo/choice-field", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -254,7 +254,7 @@ }, "dist/valtimo/components": { "name": "@valtimo/components", - "version": "0.0.0-watch+1715850706200", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -294,7 +294,7 @@ }, "dist/valtimo/config": { "name": "@valtimo/config", - "version": "0.0.0-watch+1715867423535", + "version": "0.0.0-watch+1716560090626", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -309,7 +309,7 @@ }, "dist/valtimo/connector-management": { "name": "@valtimo/connector-management", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -348,7 +348,7 @@ }, "dist/valtimo/dashboard": { "name": "@valtimo/dashboard", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -367,7 +367,7 @@ }, "dist/valtimo/dashboard-management": { "name": "@valtimo/dashboard-management", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -380,7 +380,7 @@ }, "dist/valtimo/decision": { "name": "@valtimo/decision", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -395,7 +395,7 @@ }, "dist/valtimo/document": { "name": "@valtimo/document", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -411,7 +411,7 @@ }, "dist/valtimo/dossier": { "name": "@valtimo/dossier", - "version": "0.0.0-watch+1715867424250", + "version": "0.0.0-watch+1716791108912", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -429,7 +429,7 @@ }, "dist/valtimo/dossier-management": { "name": "@valtimo/dossier-management", - "version": "12.0.0", + "version": "0.0.0-watch+1716790646280", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -442,7 +442,7 @@ }, "dist/valtimo/form-flow-management": { "name": "@valtimo/form-flow-management", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "dependencies": { "tslib": "2.6.2" @@ -454,7 +454,7 @@ }, "dist/valtimo/form-management": { "name": "@valtimo/form-management", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -467,7 +467,7 @@ }, "dist/valtimo/keycloak": { "name": "@valtimo/keycloak", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -483,7 +483,7 @@ }, "dist/valtimo/layout": { "name": "@valtimo/layout", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -506,7 +506,7 @@ }, "dist/valtimo/migration": { "name": "@valtimo/migration", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -519,7 +519,7 @@ }, "dist/valtimo/milestone": { "name": "@valtimo/milestone", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -533,7 +533,7 @@ }, "dist/valtimo/object": { "name": "@valtimo/object", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -546,7 +546,7 @@ }, "dist/valtimo/object-management": { "name": "@valtimo/object-management", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -559,7 +559,7 @@ }, "dist/valtimo/plugin": { "name": "@valtimo/plugin", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -572,7 +572,7 @@ }, "dist/valtimo/plugin-management": { "name": "@valtimo/plugin-management", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -585,7 +585,7 @@ }, "dist/valtimo/process": { "name": "@valtimo/process", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -603,7 +603,7 @@ }, "dist/valtimo/process-link": { "name": "@valtimo/process-link", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -616,7 +616,7 @@ }, "dist/valtimo/process-management": { "name": "@valtimo/process-management", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -631,7 +631,7 @@ }, "dist/valtimo/resource": { "name": "@valtimo/resource", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -644,7 +644,7 @@ }, "dist/valtimo/security": { "name": "@valtimo/security", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -659,7 +659,7 @@ }, "dist/valtimo/swagger": { "name": "@valtimo/swagger", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -673,7 +673,7 @@ }, "dist/valtimo/task": { "name": "@valtimo/task", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -689,7 +689,7 @@ }, "dist/valtimo/task-management": { "name": "@valtimo/task-management", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { @@ -703,7 +703,7 @@ }, "dist/valtimo/zgw": { "name": "@valtimo/zgw", - "version": "12.0.0", + "version": "12.1.0", "dev": true, "license": "EUPL-1.2", "dependencies": { diff --git a/projects/valtimo/config/assets/core/de.json b/projects/valtimo/config/assets/core/de.json index 10d996240..6bd155a0f 100644 --- a/projects/valtimo/config/assets/core/de.json +++ b/projects/valtimo/config/assets/core/de.json @@ -161,7 +161,11 @@ "noFormSpecified": "Für diese Registerkarte wurde keine Formulardefinition angegeben.", "formNotFound": "Die Formulardefinition '{{formDefinitionName}}' konnte nicht gefunden werden." }, - "rowLocked": "Kein Zugriff zu dieser Fall!" + "rowLocked": "Kein Zugriff zu dieser Fall!", + "widgets": { + "noWidgets": "Keine Widgets", + "noWidgetsDescription": "Für dieses Tab sind keine Widgets konfiguriert, oder Sie verfügen nicht über die entsprechenden Berechtigungen, um sie anzuzeigen." + } }, "document": { "title": { diff --git a/projects/valtimo/config/assets/core/en.json b/projects/valtimo/config/assets/core/en.json index 9e9c0d979..a137f4f9c 100644 --- a/projects/valtimo/config/assets/core/en.json +++ b/projects/valtimo/config/assets/core/en.json @@ -161,7 +161,11 @@ "noFormSpecified": "No form definition has been specified for this tab.", "formNotFound": "The form definition '{{formDefinitionName}}' could not be found." }, - "rowLocked": "You don't have access to this case" + "rowLocked": "You don't have access to this case", + "widgets": { + "noWidgets": "No widgets", + "noWidgetsDescription": "There are no widgets configured for this tab, or you don't have the rights to view them." + } }, "document": { "title": { diff --git a/projects/valtimo/config/assets/core/nl.json b/projects/valtimo/config/assets/core/nl.json index 9e98daa47..00460e461 100644 --- a/projects/valtimo/config/assets/core/nl.json +++ b/projects/valtimo/config/assets/core/nl.json @@ -161,7 +161,11 @@ "noFormSpecified": "Voor dit tabblad is geen formulierdefinitie opgegeven.", "formNotFound": "De formulierdefinitie '{{formDefinitionName}}' kan niet worden gevonden." }, - "rowLocked": "Je hebt geen toegang tot dit dossier" + "rowLocked": "Je hebt geen toegang tot dit dossier", + "widgets": { + "noWidgets": "Geen widgets", + "noWidgetsDescription": "Er zijn geen widgets geconfigureerd voor dit tabblad, of je hebt niet de juiste rechten om deze te zien." + } }, "document": { "title": { diff --git a/projects/valtimo/config/src/lib/services/base-api.service.ts b/projects/valtimo/config/src/lib/services/base-api.service.ts index aa2a40ab9..54da15f01 100644 --- a/projects/valtimo/config/src/lib/services/base-api.service.ts +++ b/projects/valtimo/config/src/lib/services/base-api.service.ts @@ -29,6 +29,8 @@ export abstract class BaseApiService { } public getApiUrl(urlPart: string): string { - return `${UrlUtils.formatUrlTrailingSlash(this._valtimoEndpointUri, false)}${urlPart}`; + const urlWithStartSlash = UrlUtils.formatUrlStartWithSlash(urlPart); + const urlFormattedWithTrailingSlash = `${UrlUtils.formatUrlTrailingSlash(this._valtimoEndpointUri, false)}${urlWithStartSlash}`; + return urlFormattedWithTrailingSlash; } } diff --git a/projects/valtimo/config/src/lib/services/config.service.ts b/projects/valtimo/config/src/lib/services/config.service.ts index 721b87eef..40c017d85 100644 --- a/projects/valtimo/config/src/lib/services/config.service.ts +++ b/projects/valtimo/config/src/lib/services/config.service.ts @@ -65,6 +65,10 @@ export class ConfigService { }; } + public get featureToggles(): ValtimoConfig['featureToggles'] { + return this.config.featureToggles; + } + public get config$(): Observable { return of(this.config); } diff --git a/projects/valtimo/config/src/lib/utils/url.utils.ts b/projects/valtimo/config/src/lib/utils/url.utils.ts index 0a207a260..93ea290cb 100644 --- a/projects/valtimo/config/src/lib/utils/url.utils.ts +++ b/projects/valtimo/config/src/lib/utils/url.utils.ts @@ -30,6 +30,14 @@ class UrlUtils { return url; } + static formatUrlStartWithSlash(url: string): string { + if (url && typeof url === 'string' && url.length > 0 && url[0] !== '/') { + return `/${url}`; + } + + return `${url}`; + } + static getUrlHost(urlString: string): string { let url!: URL; diff --git a/projects/valtimo/dossier/ng-package.json b/projects/valtimo/dossier/ng-package.json index 24869a126..5c95838e8 100644 --- a/projects/valtimo/dossier/ng-package.json +++ b/projects/valtimo/dossier/ng-package.json @@ -9,6 +9,8 @@ "ngx-toastr", "moment", "@ngx-translate/core", - "@ngx-translate/http-loader" + "@ngx-translate/http-loader", + "bin-pack-with-constraints", + "uuid" ] } diff --git a/projects/valtimo/dossier/package.json b/projects/valtimo/dossier/package.json index 6aecc3aef..7160bbdbf 100644 --- a/projects/valtimo/dossier/package.json +++ b/projects/valtimo/dossier/package.json @@ -12,6 +12,8 @@ "moment": "2.30.1", "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", - "tslib": "2.6.2" + "tslib": "2.6.2", + "bin-pack-with-constraints": "1.0.1", + "uuid": "9.0.1" } } diff --git a/projects/valtimo/dossier/src/lib/components/dossier-detail/dossier-detail.component.scss b/projects/valtimo/dossier/src/lib/components/dossier-detail/dossier-detail.component.scss index 22d9cfd76..99ebffd82 100644 --- a/projects/valtimo/dossier/src/lib/components/dossier-detail/dossier-detail.component.scss +++ b/projects/valtimo/dossier/src/lib/components/dossier-detail/dossier-detail.component.scss @@ -24,6 +24,10 @@ &:has(> .tab--no-min-height) { min-height: unset; } + + &:has(> .tab--no-background) { + background: transparent !important; + } } .loading-container { diff --git a/projects/valtimo/dossier/src/lib/components/dossier-detail/dossier-detail.component.ts b/projects/valtimo/dossier/src/lib/components/dossier-detail/dossier-detail.component.ts index 8662ee03c..bfd7087d9 100644 --- a/projects/valtimo/dossier/src/lib/components/dossier-detail/dossier-detail.component.ts +++ b/projects/valtimo/dossier/src/lib/components/dossier-detail/dossier-detail.component.ts @@ -316,6 +316,7 @@ export class DossierDetailComponent implements AfterViewInit, OnDestroy { this.route ); this.tabLoader.initial(this._initialTabName); + this.dossierTabService.setTabLoader(this.tabLoader); this.loadingTabs$.next(false); } else { this.noTabsConfigured$.next(true); diff --git a/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widget-block/widget-block.component.html b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widget-block/widget-block.component.html new file mode 100644 index 000000000..2ff52793a --- /dev/null +++ b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widget-block/widget-block.component.html @@ -0,0 +1,41 @@ + + + +
+
+ @if (obs.loadingData) { + + } @else { + {{ stringifiedWidget$ | async }} + } +
+
+
diff --git a/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widget-block/widget-block.component.scss b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widget-block/widget-block.component.scss new file mode 100644 index 000000000..217783171 --- /dev/null +++ b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widget-block/widget-block.component.scss @@ -0,0 +1,24 @@ +/*! + * Copyright 2015-2024 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.widget-block { + position: absolute; + box-sizing: border-box; + padding: 8px; + overflow-wrap: break-word; + visibility: hidden; + height: min-content; +} diff --git a/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widget-block/widget-block.component.ts b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widget-block/widget-block.component.ts new file mode 100644 index 000000000..4ad8f87f7 --- /dev/null +++ b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widget-block/widget-block.component.ts @@ -0,0 +1,186 @@ +/* + * Copyright 2015-2024 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + OnDestroy, + Renderer2, + ViewChild, +} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {CaseWidgetWithUuid, CaseWidgetXY} from '../../../../../../models'; +import { + BehaviorSubject, + combineLatest, + filter, + map, + Observable, + Subscription, + switchMap, + tap, +} from 'rxjs'; +import { + DossierTabService, + DossierWidgetsApiService, + DossierWidgetsLayoutService, +} from '../../../../../../services'; +import {ActivatedRoute} from '@angular/router'; +import {LoadingModule} from 'carbon-components-angular'; + +@Component({ + selector: 'valtimo-dossier-widget-block', + templateUrl: './widget-block.component.html', + styleUrls: ['./widget-block.component.scss'], + standalone: true, + imports: [CommonModule, LoadingModule], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class WidgetBlockComponent implements AfterViewInit, OnDestroy { + @ViewChild('widgetBlockContent') private _widgetBlockContentRef: ElementRef; + @ViewChild('widgetBlock') private _widgetBlockRef: ElementRef; + + @Input() public set widget(value: CaseWidgetWithUuid) { + this._widgetUuid = value.uuid; + this._widget$.next(value); + } + + private readonly _widget$ = new BehaviorSubject(null); + + public get widget$(): Observable { + return this._widget$.pipe(filter(widget => widget !== null)); + } + + // to remove + public get stringifiedWidget$(): Observable { + return this._widget$.pipe(map(widget => JSON.stringify(widget))); + } + + public readonly packResultAvailable$ = new BehaviorSubject(false); + + public readonly caseWidgetXY$: Observable = combineLatest([ + this.dossierWidgetsLayoutService.packResult$, + this.packResultAvailable$, + ]).pipe( + map(([packResult, packResultAvailable]) => { + const widgetPackResult = packResult.items.find( + packItem => packItem.item.configurationKey === this._widgetUuid + ); + + if (widgetPackResult && !packResultAvailable) this.packResultAvailable$.next(true); + + return widgetPackResult ? {x: widgetPackResult.x, y: widgetPackResult.y} : {x: 0, y: 0}; + }) + ); + + private readonly _caseWidgetWidthsPx$ = this.dossierWidgetsLayoutService.caseWidgetWidthsPx$; + + public readonly loadingWidgetData$ = new BehaviorSubject(true); + + private readonly _documentId$ = this.route.params.pipe( + map(params => params?.documentId), + filter(documentId => !!documentId) + ); + + private readonly _tabKey$: Observable = this.dossierTabService.activeTabKey$; + + public readonly widgetData$ = combineLatest([ + this.widget$, + this._tabKey$, + this._documentId$, + ]).pipe( + switchMap(([widget, tabkey, documentId]) => + this.widgetsApiService.getWidgetData(documentId, tabkey, widget.key) + ), + tap(() => this.loadingWidgetData$.next(false)) + ); + + private readonly _subscriptions = new Subscription(); + + private _observer!: ResizeObserver; + + private _widgetUuid!: string; + + constructor( + private readonly dossierWidgetsLayoutService: DossierWidgetsLayoutService, + private readonly renderer: Renderer2, + private readonly dossierTabService: DossierTabService, + private readonly route: ActivatedRoute, + private readonly widgetsApiService: DossierWidgetsApiService + ) {} + + public ngAfterViewInit(): void { + this.openWidgetWidthSubscription(); + this.openContentHeightObserver(); + this.openWidgetHeightSubscription(); + } + + public ngOnDestroy(): void { + this._subscriptions.unsubscribe(); + this._observer?.disconnect(); + } + + private openWidgetWidthSubscription(): void { + this._subscriptions.add( + this._caseWidgetWidthsPx$.subscribe(caseWidgetsWidths => { + const widgetWidth = caseWidgetsWidths[this._widgetUuid]; + + if (widgetWidth) { + this.renderer.setStyle(this._widgetBlockRef.nativeElement, 'width', `${widgetWidth}px`); + } + }) + ); + } + + private openWidgetHeightSubscription(): void { + this._subscriptions.add( + this.dossierWidgetsLayoutService.widgetsContentHeightsRounded$.subscribe( + caseWidgetContentHeights => { + const widgetHeight = caseWidgetContentHeights[this._widgetUuid]; + + if (widgetHeight) { + this.renderer.setStyle( + this._widgetBlockRef.nativeElement, + 'height', + `${widgetHeight}px` + ); + } + } + ) + ); + } + + private openContentHeightObserver(): void { + this._observer = new ResizeObserver(event => { + this.observerMutation(event); + }); + this._observer.observe(this._widgetBlockContentRef.nativeElement); + } + + private observerMutation(event: Array): void { + const widgetContentHeight = event[0]?.borderBoxSize[0]?.blockSize; + + if (typeof widgetContentHeight === 'number' && widgetContentHeight !== 0) { + this.dossierWidgetsLayoutService.setWidgetContentHeight( + this._widgetUuid, + widgetContentHeight + ); + } + } +} diff --git a/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widgets-container/widgets-container.component.html b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widgets-container/widgets-container.component.html new file mode 100644 index 000000000..5c22ca84d --- /dev/null +++ b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widgets-container/widgets-container.component.html @@ -0,0 +1,26 @@ + + +
+ @for (widget of widgetsWithUuids$ | async; track widget.uuid) { + + } +
diff --git a/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widgets-container/widgets-container.component.scss b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widgets-container/widgets-container.component.scss new file mode 100644 index 000000000..dde7e50a7 --- /dev/null +++ b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widgets-container/widgets-container.component.scss @@ -0,0 +1,25 @@ +/*! + * Copyright 2015-2024 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.widgets-container { + display: flex; + flex-grow: 0; + position: relative; + width: calc(100% + 16px); + margin-left: -8px; + margin-top: 8px; + overflow: hidden; +} diff --git a/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widgets-container/widgets-container.component.ts b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widgets-container/widgets-container.component.ts new file mode 100644 index 000000000..484ac9143 --- /dev/null +++ b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/components/widgets-container/widgets-container.component.ts @@ -0,0 +1,75 @@ +/* + * Copyright 2015-2024 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + OnDestroy, + ViewChild, +} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {CaseWidget, CaseWidgetWithUuid} from '../../../../../../models'; +import {WidgetBlockComponent} from '../widget-block/widget-block.component'; +import {DossierWidgetsLayoutService} from '../../../../../../services'; +import {v4 as uuid} from 'uuid'; +import {BehaviorSubject} from 'rxjs'; + +@Component({ + selector: 'valtimo-dossier-widgets-container', + templateUrl: './widgets-container.component.html', + styleUrls: ['./widgets-container.component.scss'], + standalone: true, + imports: [CommonModule, WidgetBlockComponent], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class WidgetsContainerComponent implements AfterViewInit, OnDestroy { + @ViewChild('widgetsContainer') private _widgetsContainerRef: ElementRef; + + public readonly widgetsWithUuids$ = new BehaviorSubject([]); + public readonly packResult$ = this.dossierWidgetsLayoutService.packResult$; + + @Input() public set widgets(value: CaseWidget[]) { + const widgetsWithUuids = value.map(widget => ({...widget, uuid: uuid()})); + this.dossierWidgetsLayoutService.setWidgets(widgetsWithUuids); + this.widgetsWithUuids$.next(widgetsWithUuids); + } + + private _observer!: ResizeObserver; + + constructor(private readonly dossierWidgetsLayoutService: DossierWidgetsLayoutService) {} + + public ngAfterViewInit(): void { + this._observer = new ResizeObserver(event => { + this.observerMutation(event); + }); + this._observer.observe(this._widgetsContainerRef.nativeElement); + } + + public ngOnDestroy(): void { + this._observer?.disconnect(); + } + + private observerMutation(event: Array): void { + const containerWidth = event[0]?.borderBoxSize[0]?.inlineSize; + + if (typeof containerWidth === 'number' && containerWidth !== 0) { + this.dossierWidgetsLayoutService.setContainerWidth(containerWidth); + } + } +} diff --git a/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/widgets.component.html b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/widgets.component.html new file mode 100644 index 000000000..79f6ad32c --- /dev/null +++ b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/widgets.component.html @@ -0,0 +1,42 @@ + + + + @if (obs.loadingWidgetConfiguration) { + + } @else if (!obs.widgetConfiguration.widgets || obs.widgetConfiguration.widgets.length === 0) { + + } @else { + + } + + + +
+ +
+
diff --git a/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/widgets.component.scss b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/widgets.component.scss new file mode 100644 index 000000000..d71978aea --- /dev/null +++ b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/widgets.component.scss @@ -0,0 +1,21 @@ +/* + * Copyright 2015-2024 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.loading-container { + display: flex; + justify-content: center; + padding: 16px 0; +} diff --git a/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/widgets.component.ts b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/widgets.component.ts new file mode 100644 index 000000000..b9da7d888 --- /dev/null +++ b/projects/valtimo/dossier/src/lib/components/dossier-detail/tab/widgets/widgets.component.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2015-2024 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {CommonModule} from '@angular/common'; +import {ChangeDetectionStrategy, Component, HostBinding, OnDestroy} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {BehaviorSubject, combineLatest, filter, map, Observable, switchMap, tap} from 'rxjs'; +import { + DossierTabService, + DossierWidgetsApiService, + DossierWidgetsLayoutService, +} from '../../../../services'; +import {LoadingModule} from 'carbon-components-angular'; +import {WidgetsContainerComponent} from './components/widgets-container/widgets-container.component'; +import {CarbonListModule} from '@valtimo/components'; +import {TranslateModule} from '@ngx-translate/core'; + +@Component({ + templateUrl: './widgets.component.html', + styleUrls: ['./widgets.component.scss'], + standalone: true, + imports: [ + CommonModule, + LoadingModule, + WidgetsContainerComponent, + CarbonListModule, + TranslateModule, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DossierDetailWidgetsComponent implements OnDestroy { + @HostBinding('class.tab--no-margin') private readonly _noMargin = true; + @HostBinding('class.tab--no-background') private readonly _noBackground = true; + @HostBinding('class.tab--no-min-height') private readonly _noMinHeight = true; + + private readonly _documentDefinitionName$ = this.route.params.pipe( + map(params => params?.documentDefinitionName), + filter(documentDefinitionName => !!documentDefinitionName) + ); + + private readonly _tabKey$: Observable = this.dossierTabService.activeTabKey$; + + public readonly loadingWidgetConfiguration$ = new BehaviorSubject(true); + + public readonly widgetConfiguration$ = combineLatest([ + this._documentDefinitionName$, + this._tabKey$, + ]).pipe( + switchMap(([documentDefinitionName, tabKey]) => + this.widgetsApiService.getWidgetTabConfiguration(documentDefinitionName, tabKey) + ), + tap(() => this.loadingWidgetConfiguration$.next(false)) + ); + + constructor( + private readonly route: ActivatedRoute, + private readonly dossierTabService: DossierTabService, + private readonly widgetsApiService: DossierWidgetsApiService, + private readonly dossierWidgetsLayoutService: DossierWidgetsLayoutService + ) {} + + public ngOnDestroy(): void { + this.dossierWidgetsLayoutService.reset(); + } +} diff --git a/projects/valtimo/dossier/src/lib/constants/case-widget.constants.ts b/projects/valtimo/dossier/src/lib/constants/case-widget.constants.ts new file mode 100644 index 000000000..b6e7690dc --- /dev/null +++ b/projects/valtimo/dossier/src/lib/constants/case-widget.constants.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2015-2024 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const WIDGET_WIDTH_1X = 320; +const WIDGET_HEIGHT_1X = 50; + +export {WIDGET_WIDTH_1X, WIDGET_HEIGHT_1X}; diff --git a/projects/valtimo/dossier/src/lib/constants/index.ts b/projects/valtimo/dossier/src/lib/constants/index.ts index 6e229b8a1..42378092b 100644 --- a/projects/valtimo/dossier/src/lib/constants/index.ts +++ b/projects/valtimo/dossier/src/lib/constants/index.ts @@ -18,3 +18,4 @@ export * from './tab'; export * from './case-tab-token'; export * from './dossier-list.constants'; export * from './case-status.constants'; +export * from './case-widget.constants'; diff --git a/projects/valtimo/dossier/src/lib/dossier.module.ts b/projects/valtimo/dossier/src/lib/dossier.module.ts index 3d2701269..988792f57 100644 --- a/projects/valtimo/dossier/src/lib/dossier.module.ts +++ b/projects/valtimo/dossier/src/lib/dossier.module.ts @@ -95,6 +95,7 @@ import {DossierBulkAssignService, DossierService} from './services'; import {DossierDetailTabFormioComponent} from './components/dossier-detail/tab/formio/formio.component'; import {TabTranslatePipeModule} from './pipes'; import {DossierDetailTabNotFoundComponent} from './components/dossier-detail/tab/not-found/not-found.component'; +import {DossierDetailWidgetsComponent} from './components/dossier-detail/tab/widgets/widgets.component'; export type TabsFactory = () => Map; @@ -183,6 +184,7 @@ export type TabsFactory = () => Map; TagModule, DialogModule, ValtimoCdsOverflowButtonDirectiveModule, + DossierDetailWidgetsComponent, ], exports: [DossierListComponent, DossierDetailComponent], }) diff --git a/projects/valtimo/dossier/src/lib/models/case-widget-display.model.ts b/projects/valtimo/dossier/src/lib/models/case-widget-display.model.ts new file mode 100644 index 000000000..4d3c7247e --- /dev/null +++ b/projects/valtimo/dossier/src/lib/models/case-widget-display.model.ts @@ -0,0 +1,60 @@ +enum CaseWidgetDisplayTypeKey { + BOOLEAN = 'boolean', + CURRENCY = 'currency', + DATE = 'date', + ENUM = 'enum', + NUMBER = 'number', + PERCENT = 'percent', +} + +interface CaseWidgetBooleanDisplayType { + type: CaseWidgetDisplayTypeKey.BOOLEAN; +} + +interface CaseWidgetCurrencyDisplayType { + type: CaseWidgetDisplayTypeKey.CURRENCY; + currencyCode?: string; + display?: string; + digitsInfo?: string; +} + +interface CaseWidgetDateDisplayType { + type: CaseWidgetDisplayTypeKey.DATE; + format?: string; +} + +interface CaseWidgetEnumDisplayType { + type: CaseWidgetDisplayTypeKey.ENUM; + values: { + [key: string]: string; + }; +} + +interface CaseWidgetNumberDisplayType { + type: CaseWidgetDisplayTypeKey.NUMBER; + digitsInfo?: string; +} + +interface CaseWidgetPercentDisplayType { + type: CaseWidgetDisplayTypeKey.PERCENT; + digitsInfo?: string; +} + +type CaseWidgetDisplayType = + | CaseWidgetBooleanDisplayType + | CaseWidgetCurrencyDisplayType + | CaseWidgetDateDisplayType + | CaseWidgetEnumDisplayType + | CaseWidgetNumberDisplayType + | CaseWidgetPercentDisplayType; + +export { + CaseWidgetDisplayTypeKey, + CaseWidgetDisplayType, + CaseWidgetBooleanDisplayType, + CaseWidgetCurrencyDisplayType, + CaseWidgetDateDisplayType, + CaseWidgetEnumDisplayType, + CaseWidgetNumberDisplayType, + CaseWidgetPercentDisplayType, +}; diff --git a/projects/valtimo/dossier/src/lib/models/case-widget.model.ts b/projects/valtimo/dossier/src/lib/models/case-widget.model.ts new file mode 100644 index 000000000..5e2501259 --- /dev/null +++ b/projects/valtimo/dossier/src/lib/models/case-widget.model.ts @@ -0,0 +1,99 @@ +/* + * Copyright 2015-2024 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {CaseWidgetDisplayType} from '.'; + +enum CaseWidgetType { + FIELDS = 'fields', +} + +type CaseWidgetWidth = 1 | 2 | 3 | 4; + +interface BasicCaseWidget { + title: string; + width: CaseWidgetWidth; + highContrast: boolean; + key: string; +} + +interface FieldsCaseWidgetValue { + key: string; + title: string; + value: string; + displayProperties?: CaseWidgetDisplayType; +} + +interface FieldsCaseWidget extends BasicCaseWidget { + type: CaseWidgetType.FIELDS; + properties: { + columns: FieldsCaseWidgetValue[][]; + }; +} + +type CaseWidget = FieldsCaseWidget; + +interface CaseWidgetWithUuid extends CaseWidget { + uuid: string; +} + +interface CaseWidgetsRes { + caseDefinitionName: string; + key: string; + widgets: CaseWidget[]; +} + +interface CaseWidgetWidthsPx { + [uuid: string]: number; +} + +interface CaseWidgetContentHeightsPx { + [uuid: string]: number; +} + +interface CaseWidgetConfigurationBin { + configurationKey: string; + width: number; + height: number; +} + +interface CaseWidgetPackResult { + height: number; + width: number; + items: Array<{ + width: number; + height: number; + x: number; + y: number; + item: CaseWidgetConfigurationBin; + }>; +} + +interface CaseWidgetXY { + x: number; + y: number; +} + +export { + FieldsCaseWidget, + CaseWidget, + CaseWidgetsRes, + CaseWidgetWithUuid, + CaseWidgetWidthsPx, + CaseWidgetContentHeightsPx, + CaseWidgetConfigurationBin, + CaseWidgetPackResult, + CaseWidgetXY, +}; diff --git a/projects/valtimo/dossier/src/lib/models/index.ts b/projects/valtimo/dossier/src/lib/models/index.ts index acbc46629..3a410470f 100644 --- a/projects/valtimo/dossier/src/lib/models/index.ts +++ b/projects/valtimo/dossier/src/lib/models/index.ts @@ -20,3 +20,5 @@ export * from './search.model'; export * from './tabs.model'; export * from './dossier-detail-tab.model'; export * from './tab-api.model'; +export * from './case-widget-display.model'; +export * from './case-widget.model'; diff --git a/projects/valtimo/dossier/src/lib/models/tabs.model.ts b/projects/valtimo/dossier/src/lib/models/tabs.model.ts index ded2b7909..c3e0beafe 100644 --- a/projects/valtimo/dossier/src/lib/models/tabs.model.ts +++ b/projects/valtimo/dossier/src/lib/models/tabs.model.ts @@ -16,7 +16,7 @@ import {ComponentFactoryResolver, ComponentRef, ViewContainerRef} from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; -import {take} from 'rxjs'; +import {BehaviorSubject, filter, Observable, take} from 'rxjs'; export interface TabLoader { tabs: T_TAB[]; @@ -27,6 +27,7 @@ export interface TabLoader { } export class TabLoaderImpl implements TabLoader { + private readonly _activeTab$ = new BehaviorSubject(null); private readonly _tabs: TabImpl[] = null; private readonly _componentFactoryResolver: ComponentFactoryResolver = null; private readonly _viewContainerRef: ViewContainerRef = null; @@ -53,6 +54,10 @@ export class TabLoaderImpl implements TabLoader { return this._tabs; } + public get activeTab$(): Observable { + return this._activeTab$.pipe(filter(tab => !!tab)); + } + public initial(tabName?: string): void { let initialTab!: TabImpl; @@ -109,6 +114,7 @@ export class TabLoaderImpl implements TabLoader { private setActive(tab: TabImpl): void { tab.activate(); this._activeTab = tab; + this._activeTab$.next(tab); } } diff --git a/projects/valtimo/dossier/src/lib/services/dossier-tab.service.ts b/projects/valtimo/dossier/src/lib/services/dossier-tab.service.ts index 4d5c80b42..219d0dd70 100644 --- a/projects/valtimo/dossier/src/lib/services/dossier-tab.service.ts +++ b/projects/valtimo/dossier/src/lib/services/dossier-tab.service.ts @@ -15,14 +15,15 @@ */ import {Inject, Injectable, OnDestroy, Optional, Type} from '@angular/core'; -import {ApiTabItem, ApiTabType, CaseTabConfig, TabImpl} from '../models'; +import {ApiTabItem, ApiTabType, CaseTabConfig, TabImpl, TabLoaderImpl} from '../models'; import {CASE_TAB_TOKEN, DEFAULT_TAB_COMPONENTS, DEFAULT_TABS, TAB_MAP} from '../constants'; import {ConfigService, ZGW_OBJECT_TYPE_COMPONENT_TOKEN} from '@valtimo/config'; import {ActivatedRoute} from '@angular/router'; import {DossierTabApiService} from './dossier-tab-api.service'; -import {BehaviorSubject, filter, map, Observable, Subscription} from 'rxjs'; +import {BehaviorSubject, filter, map, Observable, Subscription, switchMap} from 'rxjs'; import {DossierDetailTabFormioComponent} from '../components/dossier-detail/tab/formio/formio.component'; import {DossierDetailTabNotFoundComponent} from '../components/dossier-detail/tab/not-found/not-found.component'; +import {DossierDetailWidgetsComponent} from '../components/dossier-detail/tab/widgets/widgets.component'; @Injectable() export class DossierTabService implements OnDestroy { @@ -33,11 +34,23 @@ export class DossierTabService implements OnDestroy { ); private readonly _tabs$ = new BehaviorSubject | null>(null); private readonly _subscriptions = new Subscription(); + private readonly _tabLoader$ = new BehaviorSubject(null); public get tabs$(): Observable> { return this._tabs$.pipe(filter(tabs => !!tabs)); } + public get activeTab$(): Observable { + return this._tabLoader$.pipe( + filter(tabLoader => !!tabLoader), + switchMap(tabLoader => tabLoader.activeTab$) + ); + } + + public get activeTabKey$(): Observable { + return this.activeTab$.pipe(map(tab => tab.name)); + } + constructor( @Inject(TAB_MAP) private readonly tabMap: Map = DEFAULT_TABS, @Optional() @Inject(CASE_TAB_TOKEN) private readonly caseTabConfig: CaseTabConfig, @@ -57,6 +70,10 @@ export class DossierTabService implements OnDestroy { this._subscriptions.unsubscribe(); } + public setTabLoader(tabLoader: TabLoaderImpl): void { + this._tabLoader$.next(tabLoader); + } + private getConfigurableTabs(documentDefinitionName: string): Map { const tabMap = new Map(); @@ -154,6 +171,11 @@ export class DossierTabService implements OnDestroy { tab.contentKey, tab.name ); + case ApiTabType.WIDGETS: + return ( + this.configService.featureToggles?.enableCaseWidgets && + new TabImpl(tab.key, index, DossierDetailWidgetsComponent, tab.contentKey, tab.name) + ); default: return null; } diff --git a/projects/valtimo/dossier/src/lib/services/dossier-widgets-api-service.ts b/projects/valtimo/dossier/src/lib/services/dossier-widgets-api-service.ts new file mode 100644 index 000000000..e5cfceacc --- /dev/null +++ b/projects/valtimo/dossier/src/lib/services/dossier-widgets-api-service.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2015-2024 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {BaseApiService, ConfigService} from '@valtimo/config'; +import {HttpClient} from '@angular/common/http'; +import {Observable} from 'rxjs'; +import {CaseWidgetsRes} from '../models'; + +@Injectable({ + providedIn: 'root', +}) +export class DossierWidgetsApiService extends BaseApiService { + constructor( + protected readonly httpClient: HttpClient, + protected readonly configService: ConfigService + ) { + super(httpClient, configService); + } + + public getWidgetTabConfiguration( + caseDefinitionName: string, + tabKey: string + ): Observable { + return this.httpClient.get( + this.getApiUrl(`v1/case-definition/${caseDefinitionName}/widget-tab/${tabKey}`) + ); + } + + public getWidgetData(documentId: string, tabKey: string, widgetKey): Observable { + return this.httpClient.get( + this.getApiUrl(`v1/document/${documentId}/widget-tab/${tabKey}/widget/${widgetKey}`) + ); + } +} diff --git a/projects/valtimo/dossier/src/lib/services/dossier-widgets-layout.service.ts b/projects/valtimo/dossier/src/lib/services/dossier-widgets-layout.service.ts new file mode 100644 index 000000000..74c3e84c8 --- /dev/null +++ b/projects/valtimo/dossier/src/lib/services/dossier-widgets-layout.service.ts @@ -0,0 +1,188 @@ +import {Injectable, OnDestroy} from '@angular/core'; +import {BehaviorSubject, combineLatest, filter, map, Observable, Subscription, take} from 'rxjs'; +import { + CaseWidgetConfigurationBin, + CaseWidgetContentHeightsPx, + CaseWidgetPackResult, + CaseWidgetWidthsPx, + CaseWidgetWithUuid, +} from '../models'; +import {WIDGET_HEIGHT_1X, WIDGET_WIDTH_1X} from '../constants'; +import pack from 'bin-pack-with-constraints'; + +@Injectable({providedIn: 'root'}) +export class DossierWidgetsLayoutService implements OnDestroy { + private readonly _containerWidthSubject$ = new BehaviorSubject(null); + private readonly _widgetsSubject$ = new BehaviorSubject(null); + private readonly _widgetsContentHeightsSubject$ = new BehaviorSubject( + {} + ); + private readonly _packResult$ = new BehaviorSubject(null); + + private get _containerWidth$(): Observable { + return this._containerWidthSubject$.pipe(filter(width => width !== null)); + } + + private get _width1x$(): Observable { + return this._containerWidth$.pipe( + map(containerWidth => { + const amountOf1XInContainer = Math.floor(containerWidth / WIDGET_WIDTH_1X); + return Math.floor(containerWidth / amountOf1XInContainer); + }) + ); + } + + private get _widgets$(): Observable { + return this._widgetsSubject$.pipe(filter(widgets => widgets !== null)); + } + + private get _widgetsContentHeights$(): Observable { + return combineLatest([this._widgetsContentHeightsSubject$, this._widgets$]).pipe( + filter( + ([widgetsContentHeights, widgets]) => + Object.keys(widgetsContentHeights).length === widgets.length + ), + map(([widgetsContentHeights]) => widgetsContentHeights) + ); + } + + public get widgetsContentHeightsRounded$(): Observable { + return this._widgetsContentHeights$.pipe( + map(widgetsContentHeights => + Object.keys(widgetsContentHeights).reduce( + (acc, curr) => ({ + ...acc, + [curr]: + Math.ceil((widgetsContentHeights[curr] + 16) / WIDGET_HEIGHT_1X) * WIDGET_HEIGHT_1X, + }), + {} + ) + ) + ); + } + + public get caseWidgetWidthsPx$(): Observable { + return combineLatest([this._widgets$, this._width1x$, this._containerWidth$]).pipe( + map(([widgets, width1x, containerWidth]) => + widgets.reduce((acc, curr) => { + const widgetWidth = curr.width * width1x; + + return {...acc, [curr.uuid]: widgetWidth < containerWidth ? widgetWidth : containerWidth}; + }, {}) + ) + ); + } + + public get packResult$(): Observable { + return this._packResult$.pipe(filter(result => result !== null)); + } + + private readonly _subscriptions = new Subscription(); + + constructor() { + this.openPackSubscription(); + } + + public ngOnDestroy(): void { + this._subscriptions.unsubscribe(); + } + + public setWidgets(widgets: CaseWidgetWithUuid[]): void { + this._widgetsSubject$.next(widgets); + } + + public setContainerWidth(width: number): void { + this._containerWidthSubject$.next(width); + } + + public setWidgetContentHeight(uuid: string, height: number): void { + this._widgetsContentHeightsSubject$.pipe(take(1)).subscribe(widgetsContentHeights => { + if (widgetsContentHeights[uuid] !== height) { + this._widgetsContentHeightsSubject$.next({...widgetsContentHeights, [uuid]: height}); + } + }); + } + + public reset(): void { + this._containerWidthSubject$.next(null); + this._widgetsSubject$.next(null); + this._widgetsContentHeightsSubject$.next({}); + this._packResult$.next(null); + } + + private getPackResult( + binsToFit: CaseWidgetConfigurationBin[], + maxWidth: number, + maxHeight?: number + ): CaseWidgetPackResult { + return pack(binsToFit, { + maxWidth, + ...(maxHeight && {maxHeight}), + }); + } + + private getAmountOfMinWidthColumns(containerWidth: number): number { + return Math.floor(containerWidth / WIDGET_WIDTH_1X); + } + + private getHeightConstraint( + binsToFit: Array, + amountOfMinWidthColumns: number + ): number { + const amountOfSpacesNeeded = binsToFit.reduce((acc, curr) => acc + curr.height * curr.width, 0); + const minAmountOfRowsNeeded = Math.ceil(amountOfSpacesNeeded / amountOfMinWidthColumns); + const tallestWidgetHeightSpace = binsToFit.reduce( + (acc, curr) => (curr.height > acc ? curr.height : acc), + 0 + ); + const amountOfRowsNeeded = + minAmountOfRowsNeeded < tallestWidgetHeightSpace + ? tallestWidgetHeightSpace + : minAmountOfRowsNeeded; + + return amountOfRowsNeeded; + } + + private checkIfPackResultExceedsBoundary( + result: CaseWidgetPackResult, + maxWidth: number + ): boolean { + return !!result.items.find(item => item.width + item.x > maxWidth); + } + + private openPackSubscription(): void { + this._subscriptions.add( + combineLatest([ + this._widgets$, + this._containerWidth$, + this.widgetsContentHeightsRounded$, + this.caseWidgetWidthsPx$, + ]).subscribe(([widgets, containerWidth, contentHeights, widgetWidths]) => { + const configurationBins: CaseWidgetConfigurationBin[] = widgets.map(widget => ({ + configurationKey: widget.uuid, + width: widgetWidths[widget.uuid], + height: contentHeights[widget.uuid], + })); + const heightConstraint = this.getHeightConstraint( + configurationBins, + this.getAmountOfMinWidthColumns(containerWidth) + ); + const resultWithoutHeightConstraint = this.getPackResult(configurationBins, containerWidth); + const resultWithHeightConstraint = this.getPackResult( + configurationBins, + containerWidth, + heightConstraint + ); + const resultWithHeightConstraintExceedsBoundary = this.checkIfPackResultExceedsBoundary( + resultWithHeightConstraint, + containerWidth + ); + const resultToUse = resultWithHeightConstraintExceedsBoundary + ? resultWithoutHeightConstraint + : resultWithHeightConstraint; + + this._packResult$.next(resultToUse); + }) + ); + } +} diff --git a/projects/valtimo/dossier/src/lib/services/index.ts b/projects/valtimo/dossier/src/lib/services/index.ts index f28ed8234..7300cc6a3 100644 --- a/projects/valtimo/dossier/src/lib/services/index.ts +++ b/projects/valtimo/dossier/src/lib/services/index.ts @@ -26,3 +26,5 @@ export * from './dossier.service'; export * from './dossier-tab-api.service'; export * from './dossier-list-status.service'; export * from './start-modal.service'; +export * from './dossier-widgets-api-service'; +export * from './dossier-widgets-layout.service';