From e4efe533ad221135f8bc3a779661d88ee2d16772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Hudcovsk=C3=BD?= <47628511+erzik987@users.noreply.github.com> Date: Tue, 30 Jul 2024 13:17:37 +0200 Subject: [PATCH] Add paginator addon into table widget (#701) * Add paginator addon into table widget * Refactor * Add unit tests * Adjust ITableWidgetConfig + add some tests * Paginator configuration prototype * Add pageSize and pageSizeSet into configuration * Add unit tests + minor changes * Final fixes + small refactor * Add documentation * Fix failing build * Minor fix * Fix bad import * PR review requests + better error handling * Lint error + bad datasource * Update imports --- docs/CHANGELOG.md | 4 + .../src/components/docs/demo.files.ts | 11 +- .../widget-types/table/table-docs.module.ts | 14 + .../docs/table-paginator-docs.component.html | 41 +++ .../docs/table-paginator-docs.component.ts | 47 +++ ...le-widget-paginator-example.component.html | 29 ++ ...le-widget-paginator-example.component.less | 3 + ...able-widget-paginator-example.component.ts | 309 ++++++++++++++++++ .../acme-table-mock-data-source.service.ts | 24 +- .../components/prototypes/table/widgets.ts | 18 +- .../src/docs/development/summary.json | 4 + .../pages/widget-types/table-paginator.md | 1 + .../src/docs/production/summary.json | 4 + .../addons/paginator-feature-addon.service.ts | 95 ++++++ .../virtual-scroll-feature-addon.service.ts | 6 +- .../table-widget/table-widget.component.html | 18 + .../table-widget.component.spec.ts | 111 ++++++- .../table-widget/table-widget.component.ts | 75 ++++- .../src/lib/components/table-widget/types.ts | 26 +- .../widget-editor-accordion.component.ts | 5 + .../table-filters-editor.component.ts | 2 + .../components/widgets/table/public-api.ts | 1 + .../scroll-type-editor.component.html | 109 ++++++ .../scroll-type-editor.component.less | 21 ++ .../scroll-type-editor.component.spec.ts | 184 +++++++++++ .../scroll-type-editor.component.ts | 279 ++++++++++++++++ .../scroll-type-editor.service.ts | 40 +++ .../lib/configurator/configurator.module.ts | 4 + .../services/converters/table/mocks.ts | 24 +- .../table-filters-converter.service.spec.ts | 2 + .../table/table-filters-converter.service.ts | 25 +- ...able-scroll-type-converter.service.spec.ts | 131 ++++++++ .../table-scroll-type-converter.service.ts | 112 +++++++ .../dashboards/src/lib/dashboards.module.ts | 2 + .../lib/services/provider-registry.service.ts | 8 + packages/dashboards/src/lib/services/types.ts | 2 + .../widget-types/table/table-configurator.ts | 16 +- 37 files changed, 1742 insertions(+), 65 deletions(-) create mode 100644 packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/docs/table-paginator-docs.component.html create mode 100644 packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/docs/table-paginator-docs.component.ts create mode 100644 packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/example/table-widget-paginator-example.component.html create mode 100644 packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/example/table-widget-paginator-example.component.less create mode 100644 packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/example/table-widget-paginator-example.component.ts create mode 100644 packages/dashboards/src/docs/pages/widget-types/table-paginator.md create mode 100644 packages/dashboards/src/lib/components/table-widget/addons/paginator-feature-addon.service.ts create mode 100644 packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component.html create mode 100644 packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component.less create mode 100644 packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component.spec.ts create mode 100644 packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component.ts create mode 100644 packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.service.ts create mode 100644 packages/dashboards/src/lib/configurator/services/converters/table/table-scroll-type-converter.service.spec.ts create mode 100644 packages/dashboards/src/lib/configurator/services/converters/table/table-scroll-type-converter.service.ts diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 577f27fc7..6b301e91c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,10 @@ ## [15.0.8] 📅 2024-06-03 +### Added + +- `@nova-ui/dashboards` | Paginator added for table widget. + ### Bugfix - `@nova-ui/dashboards` | Added missing empty image for _nuiListWidgetComponent_ with no data. diff --git a/packages/dashboards/examples/src/components/docs/demo.files.ts b/packages/dashboards/examples/src/components/docs/demo.files.ts index 2bbefd4ee..5ad29b77a 100644 --- a/packages/dashboards/examples/src/components/docs/demo.files.ts +++ b/packages/dashboards/examples/src/components/docs/demo.files.ts @@ -24,11 +24,11 @@ export const DEMO_PATHS = [ "overview/overview-docs.component.html", "overview/overview-docs.component.ts", "overview/overview.module.ts", + "tutorials/customization/configurator-section/custom-configurator-section/custom-configurator-section.example.component.html", + "tutorials/customization/configurator-section/custom-configurator-section/custom-configurator-section.example.component.less", + "tutorials/customization/configurator-section/custom-configurator-section/custom-configurator-section.example.component.ts", "tutorials/customization/configurator-section/custom-configurator-section-docs.component.html", "tutorials/customization/configurator-section/custom-configurator-section-docs.component.ts", - "tutorials/customization/configurator-section/custom-configurator-section.example.component.html", - "tutorials/customization/configurator-section/custom-configurator-section.example.component.less", - "tutorials/customization/configurator-section/custom-configurator-section.example.component.ts", "tutorials/customization/configurator-section/custom-configurator-section.module.ts", "tutorials/customization/customization.module.ts", "tutorials/customization/data-source-configurator/custom-data-source-configurator-docs.component.html", @@ -164,6 +164,11 @@ export const DEMO_PATHS = [ "widget-types/table/table-widget-interactive/table-widget-interactive-example.component.html", "widget-types/table/table-widget-interactive/table-widget-interactive-example.component.less", "widget-types/table/table-widget-interactive/table-widget-interactive-example.component.ts", + "widget-types/table/table-widget-paginator/docs/table-paginator-docs.component.html", + "widget-types/table/table-widget-paginator/docs/table-paginator-docs.component.ts", + "widget-types/table/table-widget-paginator/example/table-widget-paginator-example.component.html", + "widget-types/table/table-widget-paginator/example/table-widget-paginator-example.component.less", + "widget-types/table/table-widget-paginator/example/table-widget-paginator-example.component.ts", "widget-types/table/table-widget-search-example/docs/table-search-docs.component.html", "widget-types/table/table-widget-search-example/docs/table-search-docs.component.ts", "widget-types/table/table-widget-search-example/example/table-widget-search.example.component.html", diff --git a/packages/dashboards/examples/src/components/docs/widget-types/table/table-docs.module.ts b/packages/dashboards/examples/src/components/docs/widget-types/table/table-docs.module.ts index a2cabf933..fa6b591ed 100644 --- a/packages/dashboards/examples/src/components/docs/widget-types/table/table-docs.module.ts +++ b/packages/dashboards/examples/src/components/docs/widget-types/table/table-docs.module.ts @@ -40,6 +40,8 @@ import { TableWidgetInteractiveExampleComponent } from "./table-widget-interacti import { TableSearchDocsComponent } from "./table-widget-search-example/docs/table-search-docs.component"; import { TableWidgetSearchExampleComponent } from "./table-widget-search-example/example/table-widget-search.example.component"; import { TableWidgetExampleComponent } from "./table-widget/table-widget-example.component"; +import { TablePaginatorDocsComponent } from "./table-widget-paginator/docs/table-paginator-docs.component"; +import { TableWidgetPaginatorExampleComponent } from "./table-widget-paginator/example/table-widget-paginator-example.component"; const routes: Routes = [ { @@ -71,6 +73,16 @@ const routes: Routes = [ showThemeSwitcher: true, }, }, + { + path: "table-paginator", + component: TablePaginatorDocsComponent, + data: { + srlc: { + hideIndicator: true, + }, + showThemeSwitcher: true, + }, + }, ]; @NgModule({ @@ -85,9 +97,11 @@ const routes: Routes = [ declarations: [ TableDocsComponent, TableSearchDocsComponent, + TablePaginatorDocsComponent, TableWidgetInteractiveExampleComponent, TableWidgetExampleComponent, TableWidgetSearchExampleComponent, + TableWidgetPaginatorExampleComponent, ], providers: [ { diff --git a/packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/docs/table-paginator-docs.component.html b/packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/docs/table-paginator-docs.component.html new file mode 100644 index 000000000..634e4a3b6 --- /dev/null +++ b/packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/docs/table-paginator-docs.component.html @@ -0,0 +1,41 @@ +

Table Widget with paginator

+ +

+ Table Widget can have pagination functionality. This page contains + information only about setting up the pagination, so before proceeding get + familiar with the following: +

+ + + + + + +

Configuring the Widget

+

+ To configure the widget you have to enable paginator in the widget + configuration: +

+ + {{ tableConfigurationText }} + diff --git a/packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/docs/table-paginator-docs.component.ts b/packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/docs/table-paginator-docs.component.ts new file mode 100644 index 000000000..f46b3320c --- /dev/null +++ b/packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/docs/table-paginator-docs.component.ts @@ -0,0 +1,47 @@ +// © 2022 SolarWinds Worldwide, LLC. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import { Component } from "@angular/core"; + +@Component({ + selector: "nui-table-paginator-docs", + templateUrl: "./table-paginator-docs.component.html", +}) +export class TablePaginatorDocsComponent { + public tableConfigurationText = ` + "table": { + ... + properties: { + configuration: { + // define paginator configuration here + scrollType: ScrollType.paginator, + paginatorConfiguration: { + pageSize: 10, // Value have to be one of pageSizeSet values + pageSizeSet: [10, 20, 30], + }, + // If not specified, default is set to + // pageSize: 10, + // pageSizeSet: [10, 20, 50], + hasVirtualScroll: false, // Has to be speciefied because of backward compatibility + } as ITableWidgetConfig, + }, + }, + `; +} diff --git a/packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/example/table-widget-paginator-example.component.html b/packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/example/table-widget-paginator-example.component.html new file mode 100644 index 000000000..e7d8cbdc8 --- /dev/null +++ b/packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/example/table-widget-paginator-example.component.html @@ -0,0 +1,29 @@ +
+ + Edit Mode + + +
+ +
+ + + +
diff --git a/packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/example/table-widget-paginator-example.component.less b/packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/example/table-widget-paginator-example.component.less new file mode 100644 index 000000000..c38e702f2 --- /dev/null +++ b/packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/example/table-widget-paginator-example.component.less @@ -0,0 +1,3 @@ +.dashboard { + height: 400px; +} diff --git a/packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/example/table-widget-paginator-example.component.ts b/packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/example/table-widget-paginator-example.component.ts new file mode 100644 index 000000000..291319800 --- /dev/null +++ b/packages/dashboards/examples/src/components/docs/widget-types/table/table-widget-paginator/example/table-widget-paginator-example.component.ts @@ -0,0 +1,309 @@ +// © 2022 SolarWinds Worldwide, LLC. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import { HttpClient } from "@angular/common/http"; +import { ChangeDetectorRef, Component, OnInit } from "@angular/core"; +import { GridsterConfig, GridsterItem } from "angular-gridster2"; + +import { LoggerService } from "@nova-ui/bits"; +import { + DATA_SOURCE, + DEFAULT_PIZZAGNA_ROOT, + IDashboard, + IProviderConfiguration, + ITableWidgetConfig, + IWidget, + IWidgets, + NOVA_URL_INTERACTION_HANDLER, + PizzagnaLayer, + ProviderRegistryService, + RawFormatterComponent, + ScrollType, + WellKnownPathKey, + WellKnownProviders, + WidgetTypesService, +} from "@nova-ui/dashboards"; + +import { AcmeTableMockDataSource } from "../../../../../prototypes/data/table/acme-table-mock-data-source.service"; + +/** + * A component that instantiates the dashboard + */ +@Component({ + selector: "table-widget-paginator-example", + templateUrl: "./table-widget-paginator-example.component.html", + styleUrls: ["./table-widget-paginator-example.component.less"], +}) +export class TableWidgetPaginatorExampleComponent implements OnInit { + public dashboard: IDashboard | undefined; + public gridsterConfig: GridsterConfig = {}; + public editMode: boolean = false; + + constructor( + private widgetTypesService: WidgetTypesService, + private providerRegistry: ProviderRegistryService, + private changeDetectorRef: ChangeDetectorRef + ) {} + + public ngOnInit(): void { + const widgetTemplate = this.widgetTypesService.getWidgetType( + "table", + 1 + ); + this.widgetTypesService.setNode( + widgetTemplate, + "configurator", + WellKnownPathKey.DataSourceProviders, + [AcmeTableMockDataSource.providerId] + ); + + this.providerRegistry.setProviders({ + [AcmeTableMockDataSource.providerId]: { + provide: DATA_SOURCE, + useClass: AcmeTableMockDataSource, + deps: [LoggerService, HttpClient], + }, + }); + + this.initializeDashboard(); + } + + /** Used for restoring widgets state */ + public reInitializeDashboard(): void { + // destroys the components and their providers so the dashboard can re init data + this.dashboard = undefined; + this.changeDetectorRef.detectChanges(); + + this.initializeDashboard(); + } + + public initializeDashboard(): void { + const tableWithPaginator = tableWidgetWithPaginator; + const tableWithVirtualScroll = tableWidgetWithVirtualScroll; + + const widgetIndex: IWidgets = { + [tableWithPaginator.id]: + this.widgetTypesService.mergeWithWidgetType(tableWithPaginator), + [tableWithVirtualScroll.id]: + this.widgetTypesService.mergeWithWidgetType( + tableWithVirtualScroll + ), + }; + + const positions: Record = { + [tableWithPaginator.id]: { + cols: 6, + rows: 6, + y: 0, + x: 0, + }, + [tableWithVirtualScroll.id]: { + cols: 6, + rows: 6, + y: 0, + x: 0, + }, + }; + + this.dashboard = { + positions, + widgets: widgetIndex, + }; + } +} + +export const tableWidgetWithPaginator: IWidget = { + id: "widget1", + type: "table", + pizzagna: { + [PizzagnaLayer.Configuration]: { + [DEFAULT_PIZZAGNA_ROOT]: { + providers: { + [WellKnownProviders.InteractionHandler]: { + providerId: NOVA_URL_INTERACTION_HANDLER, + }, + }, + }, + header: { + properties: { + title: "Table Widget with paginator!", + subtitle: "Basic table widget", + collapsible: true, + }, + }, + table: { + providers: { + [WellKnownProviders.DataSource]: { + providerId: AcmeTableMockDataSource.providerId, + } as IProviderConfiguration, + }, + properties: { + configuration: { + interactive: true, + columns: [ + { + id: "column1", + label: "No.", + isActive: true, + formatter: { + componentType: + RawFormatterComponent.lateLoadKey, + properties: { + dataFieldIds: { + value: "position", + }, + }, + }, + }, + { + id: "column2", + label: "Name", + isActive: true, + formatter: { + componentType: + RawFormatterComponent.lateLoadKey, + properties: { + dataFieldIds: { + value: "name", + }, + }, + }, + }, + { + id: "column3", + label: "Status", + isActive: true, + formatter: { + componentType: + RawFormatterComponent.lateLoadKey, + properties: { + dataFieldIds: { + value: "status", + }, + }, + }, + }, + ], + sorterConfiguration: { + descendantSorting: false, + sortBy: "column1", + }, + scrollType: ScrollType.paginator, + paginatorConfiguration: { + pageSize: 5, + pageSizeSet: [5, 10, 20, 30], + }, + hasVirtualScroll: false, + searchConfiguration: { + enabled: true, + }, + } as ITableWidgetConfig, + }, + }, + }, + }, +}; + +export const tableWidgetWithVirtualScroll: IWidget = { + id: "widget2", + type: "table", + pizzagna: { + [PizzagnaLayer.Configuration]: { + [DEFAULT_PIZZAGNA_ROOT]: { + providers: { + [WellKnownProviders.InteractionHandler]: { + providerId: NOVA_URL_INTERACTION_HANDLER, + }, + }, + }, + header: { + properties: { + title: "Table Widget with virtual scroll!", + subtitle: "Basic table widget", + collapsible: true, + }, + }, + table: { + providers: { + [WellKnownProviders.DataSource]: { + providerId: AcmeTableMockDataSource.providerId, + } as IProviderConfiguration, + }, + properties: { + configuration: { + interactive: true, + columns: [ + { + id: "column1", + label: "No.", + isActive: true, + formatter: { + componentType: + RawFormatterComponent.lateLoadKey, + properties: { + dataFieldIds: { + value: "position", + }, + }, + }, + }, + { + id: "column2", + label: "Name", + isActive: true, + formatter: { + componentType: + RawFormatterComponent.lateLoadKey, + properties: { + dataFieldIds: { + value: "name", + }, + }, + }, + }, + { + id: "column3", + label: "Status", + isActive: true, + formatter: { + componentType: + RawFormatterComponent.lateLoadKey, + properties: { + dataFieldIds: { + value: "status", + }, + }, + }, + }, + ], + sorterConfiguration: { + descendantSorting: false, + sortBy: "column1", + }, + hasVirtualScroll: true, + searchConfiguration: { + enabled: true, + }, + } as ITableWidgetConfig, + }, + }, + }, + }, +}; diff --git a/packages/dashboards/examples/src/components/prototypes/data/table/acme-table-mock-data-source.service.ts b/packages/dashboards/examples/src/components/prototypes/data/table/acme-table-mock-data-source.service.ts index 70abde2d0..4fe434249 100644 --- a/packages/dashboards/examples/src/components/prototypes/data/table/acme-table-mock-data-source.service.ts +++ b/packages/dashboards/examples/src/components/prototypes/data/table/acme-table-mock-data-source.service.ts @@ -23,6 +23,7 @@ import { Inject, Injectable } from "@angular/core"; import { BehaviorSubject, Subject } from "rxjs"; import { + ClientSideDataSource, IDataField, INovaFilters, LocalFilteringDataSource, @@ -73,32 +74,13 @@ export class AcmeTableMockDataSource extends LocalFilteringDataSource { setTimeout(async () => { - const virtualScrollFilter = - filters.virtualScroll && filters.virtualScroll.value; - - if (virtualScrollFilter) { - // The multiplier used here is a way to fetch more items per scroll - const start = filters.virtualScroll?.value.start; - const end = filters.virtualScroll?.value.end; - // Note: We should start with a clean cache every time first page is requested - if (start === 0) { - this.cache = []; - } - const nextChunk = TABLE_DATA.slice(start, end); - // We identify here whether the cached array does already contain some of the fetched data. - // Then we update the cached array with the only values it doesn't contain - this.cache = this.cache.concat( - nextChunk.filter((item) => !this.cache.includes(item)) - ); - super.setData(this.cache); - } + // Set the data to the table + super.setData(TABLE_DATA); const filteredData = await super.getFilteredData(filters); - if (filteredData.paginator) { filteredData.paginator.total = TABLE_DATA.length; } - resolve({ ...filteredData, dataFields: this.dataFields, diff --git a/packages/dashboards/examples/src/components/prototypes/table/widgets.ts b/packages/dashboards/examples/src/components/prototypes/table/widgets.ts index c8bb042bd..7ae7235ab 100644 --- a/packages/dashboards/examples/src/components/prototypes/table/widgets.ts +++ b/packages/dashboards/examples/src/components/prototypes/table/widgets.ts @@ -36,6 +36,7 @@ import { PizzagnaLayer, ProportionalWidgetChartTypes, RawFormatterComponent, + ScrollType, WellKnownProviders, } from "@nova-ui/dashboards"; @@ -112,7 +113,7 @@ export const widgets: IWidget[] = [ }, header: { properties: { - title: "Table Widget!", + title: "Table Widget with paginator!", subtitle: "Basic table widget", collapsible: true, }, @@ -174,7 +175,12 @@ export const widgets: IWidget[] = [ descendantSorting: false, sortBy: "column1", }, - hasVirtualScroll: true, + scrollType: ScrollType.paginator, + paginatorConfiguration: { + pageSize: 5, + pageSizeSet: [5, 10, 20, 30], + }, + hasVirtualScroll: false, searchConfiguration: { enabled: true, }, @@ -323,7 +329,7 @@ export const widgets: IWidget[] = [ }, header: { properties: { - title: "Table Widget!", + title: "Table Widget with virtual scroll!", subtitle: "Basic table widget", collapsible: true, }, @@ -533,7 +539,8 @@ export const widgets: IWidget[] = [ descendantSorting: false, sortBy: "column1", }, - hasVirtualScroll: true, + scrollType: ScrollType.virtual, + hasVirtualScroll: false, searchConfiguration: { enabled: true, }, @@ -685,7 +692,8 @@ export const widgets: IWidget[] = [ descendantSorting: false, sortBy: "column1", }, - hasVirtualScroll: true, + scrollType: ScrollType.paginator, + hasVirtualScroll: false, searchConfiguration: { enabled: true, }, diff --git a/packages/dashboards/src/docs/development/summary.json b/packages/dashboards/src/docs/development/summary.json index 142de2bed..28f3b7e7d 100644 --- a/packages/dashboards/src/docs/development/summary.json +++ b/packages/dashboards/src/docs/development/summary.json @@ -134,6 +134,10 @@ { "title": "Table with Search", "file": "../pages/widget-types/table-search.md" + }, + { + "title": "Table with Paginator", + "file": "../pages/widget-types/table-paginator.md" } ] }, diff --git a/packages/dashboards/src/docs/pages/widget-types/table-paginator.md b/packages/dashboards/src/docs/pages/widget-types/table-paginator.md new file mode 100644 index 000000000..6cbc497aa --- /dev/null +++ b/packages/dashboards/src/docs/pages/widget-types/table-paginator.md @@ -0,0 +1 @@ + diff --git a/packages/dashboards/src/docs/production/summary.json b/packages/dashboards/src/docs/production/summary.json index 142de2bed..28f3b7e7d 100644 --- a/packages/dashboards/src/docs/production/summary.json +++ b/packages/dashboards/src/docs/production/summary.json @@ -134,6 +134,10 @@ { "title": "Table with Search", "file": "../pages/widget-types/table-search.md" + }, + { + "title": "Table with Paginator", + "file": "../pages/widget-types/table-paginator.md" } ] }, diff --git a/packages/dashboards/src/lib/components/table-widget/addons/paginator-feature-addon.service.ts b/packages/dashboards/src/lib/components/table-widget/addons/paginator-feature-addon.service.ts new file mode 100644 index 000000000..e514e689d --- /dev/null +++ b/packages/dashboards/src/lib/components/table-widget/addons/paginator-feature-addon.service.ts @@ -0,0 +1,95 @@ +// © 2022 SolarWinds Worldwide, LLC. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import { Injectable } from "@angular/core"; +import { INovaFilteringOutputs, ISelectChangedEvent } from "@nova-ui/bits"; +import { IPaginatorState, TableWidgetComponent } from "../public-api"; + +@Injectable() +export class PaginatorFeatureAddonService { + private widget: TableWidgetComponent; // TODO: generic widget + + public defaultPaginatorState: IPaginatorState = { + page: 1, + pageSize: 10, + pageSizeSet: [10, 20, 50], + total: 0, + }; + public paginatorState: IPaginatorState = this.defaultPaginatorState; + + public initPaginator(widget: TableWidgetComponent): void { + this.widget = widget; + this.setPaginatorState(); + } + + public applyFilters(): void { + this.widget.dataSource.applyFilters(); + } + + private registerPaginator() { + if (this.widget.dataSource) { + this.widget.dataSource.registerComponent({ + paginator: { + componentInstance: this.widget.paginator, + }, + }); + } + } + + private deregisterPaginator() { + if (this.widget.dataSource) { + this.widget.dataSource.deregisterComponent?.("paginator"); + } + } + + private setPaginatorState() { + const paginatorConfiguration = + this.widget.configuration?.paginatorConfiguration; + + if (this.widget.hasPaginator) { + this.paginatorState.pageSize = + paginatorConfiguration?.pageSize ?? + this.defaultPaginatorState.pageSize; + + this.paginatorState.pageSizeSet = + paginatorConfiguration?.pageSizeSet ?? + this.defaultPaginatorState.pageSizeSet; + + this.widget.dataSource.outputsSubject.subscribe( + (data: INovaFilteringOutputs) => { + this.paginatorState.total = data.paginator?.total ?? 0; + } + ); + + // When changing page size from configurator, this will force preview update + if (this.widget.paginator) { + const pageSize: ISelectChangedEvent = { + oldValue: 0, + newValue: this.paginatorState.pageSize, + }; + this.widget.paginator.setItemsPerPage(pageSize); + } + + this.registerPaginator(); + } else { + this.deregisterPaginator(); + } + } +} diff --git a/packages/dashboards/src/lib/components/table-widget/addons/virtual-scroll-feature-addon.service.ts b/packages/dashboards/src/lib/components/table-widget/addons/virtual-scroll-feature-addon.service.ts index 45afbe37a..b5abccef3 100644 --- a/packages/dashboards/src/lib/components/table-widget/addons/virtual-scroll-feature-addon.service.ts +++ b/packages/dashboards/src/lib/components/table-widget/addons/virtual-scroll-feature-addon.service.ts @@ -39,7 +39,11 @@ export class VirtualScrollFeatureAddonService { this.widget = widget; } - public initVirtualScroll(): void { + public initVirtualScroll(widget: TableWidgetComponent): void { + if (!this.widget) { + this.initWidget(widget); + } + if (this.widget.hasVirtualScroll) { this.registerVirtualScroll(); } else { diff --git a/packages/dashboards/src/lib/components/table-widget/table-widget.component.html b/packages/dashboards/src/lib/components/table-widget/table-widget.component.html index 66f8e4fdd..8dc930631 100644 --- a/packages/dashboards/src/lib/components/table-widget/table-widget.component.html +++ b/packages/dashboards/src/lib/components/table-widget/table-widget.component.html @@ -122,4 +122,22 @@ +
+ +
+ + +
+ + +
+
diff --git a/packages/dashboards/src/lib/components/table-widget/table-widget.component.spec.ts b/packages/dashboards/src/lib/components/table-widget/table-widget.component.spec.ts index f8afe0da8..54c02970e 100644 --- a/packages/dashboards/src/lib/components/table-widget/table-widget.component.spec.ts +++ b/packages/dashboards/src/lib/components/table-widget/table-widget.component.spec.ts @@ -28,9 +28,9 @@ import { BehaviorSubject } from "rxjs"; import { skip, take, tap } from "rxjs/operators"; import { + ClientSideDataSource, EventBus, IDataField, - LocalFilteringDataSource, LoggerService, NuiBusyModule, NuiImageModule, @@ -50,7 +50,11 @@ import { ProviderRegistryService } from "../../services/provider-registry.servic import { REFRESH, SCROLL_NEXT_PAGE } from "../../services/types"; import { DATA_SOURCE, PIZZAGNA_EVENT_BUS } from "../../types"; import { TableWidgetComponent } from "./table-widget.component"; -import { ITableWidgetColumnConfig, ITableWidgetConfig } from "./types"; +import { + ITableWidgetColumnConfig, + ITableWidgetConfig, + ScrollType, +} from "./types"; interface BasicTableModel { position: number; @@ -65,7 +69,7 @@ interface BasicTableModel { secondUrlLabel: string; } -class MockDatasource extends LocalFilteringDataSource { +class MockDatasource extends ClientSideDataSource { public busy = new BehaviorSubject(true); } @@ -355,6 +359,7 @@ describe("TableWidgetComponent", () => { component.dataFields = dataFields; component.configuration = configuration; component.range = tableData.length; + fixture.detectChanges(); }); @@ -600,6 +605,106 @@ describe("TableWidgetComponent", () => { }); }); + describe("scroll type >", () => { + describe("virtual scroll >", () => { + it("should be enabled if is hasVirtualScroll is set to true", () => { + const configWithVirtualScroll: ITableWidgetConfig = { + ...configuration, + hasVirtualScroll: true, + }; + + component.ngOnChanges( + createSimpleChanges( + configWithVirtualScroll, + tableData, + dataFields + ) + ); + + fixture.detectChanges(); + + expect(component.hasPaginator).toBe(false); + expect(component.hasVirtualScroll).toBe(true); + }); + + it("should be enabled if is hasVirtualScroll is set to true even if scrollType is set to paginator", () => { + const configWithVirtualScrollAndScrollType: ITableWidgetConfig = + { + ...configuration, + hasVirtualScroll: true, + scrollType: ScrollType.paginator, + }; + + component.ngOnChanges( + createSimpleChanges( + configWithVirtualScrollAndScrollType, + tableData, + dataFields + ) + ); + + fixture.detectChanges(); + + expect(component.hasPaginator).toBe(false); + expect(component.hasVirtualScroll).toBe(true); + }); + + it("should be enabled if not defined otherwise in config", () => { + const configurationWithoutScrollTypeOrHasVirtualScroll: ITableWidgetConfig = + { + columns: [], + sorterConfiguration: { + descendantSorting: true, + sortBy: "column1", + }, + }; + + component.ngOnChanges( + createSimpleChanges( + configurationWithoutScrollTypeOrHasVirtualScroll, + tableData, + dataFields + ) + ); + + fixture.detectChanges(); + + expect(component.hasPaginator).toBe(false); + expect(component.hasVirtualScroll).toBe(true); + }); + }); + + // TODO: Not working properly, fix in NUI-5893 + describe("paginator >", () => { + xit("should be enabled if hasVirtualScroll is set to false and scrollType is set to paginator", () => { + const configWithoutVirtualScrollAndScrollType: ITableWidgetConfig = + { + ...configuration, + hasVirtualScroll: false, + scrollType: ScrollType.paginator, + }; + + // component.paginator = new PaginatorComponent( + // mockLoggerService, + // mockPopupContainer, + // mockChangeDetector + // ); + component.ngOnChanges( + createSimpleChanges( + configWithoutVirtualScrollAndScrollType, + tableData, + dataFields + ) + ); + + fixture.detectChanges(); + + expect(component.hasPaginator).toBe(true); + expect(component.hasVirtualScroll).toBe(false); + }); + }); + }); + describe("table columns mapping >", () => { it("should correctly map data with one data field", () => { configuration.columns = oneDataFieldColumns; diff --git a/packages/dashboards/src/lib/components/table-widget/table-widget.component.ts b/packages/dashboards/src/lib/components/table-widget/table-widget.component.ts index 1d58cc748..20d17b6ac 100644 --- a/packages/dashboards/src/lib/components/table-widget/table-widget.component.ts +++ b/packages/dashboards/src/lib/components/table-widget/table-widget.component.ts @@ -54,8 +54,10 @@ import { IDataSource, IEvent, IFilter, + INovaFilteringOutputs, ISortedItem, LoggerService, + PaginatorComponent, SorterDirection, TableAlignmentOptions, TableComponent, @@ -81,7 +83,13 @@ import { import { ITableFormatterDefinition } from "../types"; import { SearchFeatureAddonService } from "./addons/search-feature-addon.service"; import { VirtualScrollFeatureAddonService } from "./addons/virtual-scroll-feature-addon.service"; -import { ITableWidgetColumnConfig, ITableWidgetConfig } from "./types"; +import { + IPaginatorState, + ITableWidgetColumnConfig, + ITableWidgetConfig, + ScrollType, +} from "./types"; +import { PaginatorFeatureAddonService } from "./addons/paginator-feature-addon.service"; /** * @ignore @@ -91,7 +99,11 @@ import { ITableWidgetColumnConfig, ITableWidgetConfig } from "./types"; templateUrl: "./table-widget.component.html", styleUrls: ["./table-widget.component.less"], changeDetection: ChangeDetectionStrategy.OnPush, - providers: [SearchFeatureAddonService, VirtualScrollFeatureAddonService], + providers: [ + SearchFeatureAddonService, + VirtualScrollFeatureAddonService, + PaginatorFeatureAddonService, + ], host: { // Note: Moved here from configuration to ensure that consumers will not override it. // Used to prevent table overflowing preview container in the edit/configuration mode. @@ -137,7 +149,7 @@ export class TableWidgetComponent string, number | undefined >(); - public hasVirtualScroll: boolean = true; + public scrollType: ScrollType = ScrollType.virtual; public tableContainerHeight: number; public isSearchEnabled: boolean = false; public searchTerm$ = new Subject(); @@ -175,6 +187,7 @@ export class TableWidgetComponent public isBusy: boolean = this.lastPageFetched !== this.totalPages; @ViewChild("widgetTable") table: TableComponent; + @ViewChild("paginator") paginator: PaginatorComponent; @ViewChild(CdkVirtualScrollViewport) vscrollViewport?: CdkVirtualScrollViewport; @ViewChildren(TableRowComponent, { read: ElementRef }) @@ -202,6 +215,7 @@ export class TableWidgetComponent private el: ElementRef, private logger: LoggerService, private searchAddon: SearchFeatureAddonService, + public paginatorAddon: PaginatorFeatureAddonService, public virtualScrollAddon: VirtualScrollFeatureAddonService, private formattersRegistryService: TableFormatterRegistryService ) {} @@ -239,15 +253,7 @@ export class TableWidgetComponent this.onSortOrderChanged(sortedColumn); } - const newHasVirtualScroll = get( - changes, - "configuration.currentValue.hasVirtualScroll", - true - ) as boolean; - if (this.hasVirtualScroll !== newHasVirtualScroll) { - this.hasVirtualScroll = newHasVirtualScroll; - this.virtualScrollAddon.initVirtualScroll(); - } + this.resolveScrollType(changes); } if (changes.totalItems) { @@ -255,12 +261,35 @@ export class TableWidgetComponent } } + public resolveScrollType(changes: SimpleChanges) { + // Since removing "hasVirtualScroll" from configuration would cause breaking changes, it is used as primary source of truth + const newHasVirtualScroll = get( + changes, + "configuration.currentValue.hasVirtualScroll", + true + ) as boolean; + + const scrollType = get( + changes, + "configuration.currentValue.scrollType", + ScrollType.virtual + ) as ScrollType; + + this.scrollType = newHasVirtualScroll ? ScrollType.virtual : scrollType; + + this.virtualScrollAddon.initVirtualScroll(this); + this.paginatorAddon.initPaginator(this); + + if (this.scrollTypeChanged(changes.configuration)) { + this.dataSource.applyFilters(); + } + } + public ngOnInit(): void { if (this.dataSource) { // Since the sortFilter is not initialized, we have to retrieve and set the correct filter value from the configuration before registering it this.setSortFilter(); this.registerSorter(); - this.virtualScrollAddon.initWidget(this); } } @@ -277,8 +306,10 @@ export class TableWidgetComponent ) .subscribe(); - this.virtualScrollAddon.initVirtualScroll(); + this.virtualScrollAddon.initVirtualScroll(this); this.searchAddon.initWidget(this); + this.paginatorAddon.initPaginator(this); + const tableHeightChanged$: Observable = this.eventBus .getStream(WIDGET_RESIZE) .pipe( @@ -561,6 +592,14 @@ export class TableWidgetComponent ); } + public get hasVirtualScroll(): boolean { + return this.scrollType === ScrollType.virtual; + } + + public get hasPaginator(): boolean { + return this.scrollType === ScrollType.paginator; + } + private setSortFilter() { if (this.configuration) { const sortBy = this.configuration.sorterConfiguration?.sortBy; @@ -647,6 +686,14 @@ export class TableWidgetComponent this.tableData = []; } + private scrollTypeChanged(configurationChange: SimpleChange): boolean { + return ( + !!configurationChange.previousValue && + configurationChange.previousValue.scrollType !== + configurationChange.currentValue.scrollType + ); + } + private getTableScrollRange(): number { // Note: To work properly virtual viewport should be scrollable // to ensure that container will be scrollable we're adding 50% more items diff --git a/packages/dashboards/src/lib/components/table-widget/types.ts b/packages/dashboards/src/lib/components/table-widget/types.ts index 4e6b1d76b..037d0f34d 100644 --- a/packages/dashboards/src/lib/components/table-widget/types.ts +++ b/packages/dashboards/src/lib/components/table-widget/types.ts @@ -46,7 +46,11 @@ export interface ITableWidgetConfig { columns: Array; sortable?: boolean; sorterConfiguration: ITableWidgetSorterConfig; - hasVirtualScroll: boolean; + /** + * @deprecated Use scrollType and set it to "infinite" instead + */ + hasVirtualScroll?: boolean; + scrollType?: ScrollType; interactive?: boolean; headerTooltipsEnabled?: boolean; scrollActivationDelayMs?: number; @@ -62,9 +66,29 @@ export interface ITableWidgetConfig { searchTerm?: string; searchDebounce?: number; }; + + paginatorConfiguration?: ITableWidgetPaginatorConfig; } export interface ITableWidgetSorterConfig { descendantSorting: boolean; sortBy: string; } + +export interface ITableWidgetPaginatorConfig { + pageSizeSet?: number[]; + pageSize?: number; +} + +export interface IPaginatorState { + page: number; + pageSize: number; + pageSizeSet: number[]; + total: number; +} + +export enum ScrollType { + virtual = "virtual", + paginator = "paginator", + default = "default", +} diff --git a/packages/dashboards/src/lib/configurator/components/widget-editor-accordion/widget-editor-accordion.component.ts b/packages/dashboards/src/lib/configurator/components/widget-editor-accordion/widget-editor-accordion.component.ts index 1270602b0..806e9176d 100644 --- a/packages/dashboards/src/lib/configurator/components/widget-editor-accordion/widget-editor-accordion.component.ts +++ b/packages/dashboards/src/lib/configurator/components/widget-editor-accordion/widget-editor-accordion.component.ts @@ -22,9 +22,11 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + EventEmitter, Input, OnDestroy, OnInit, + Output, ViewEncapsulation, } from "@angular/core"; import { Subject } from "rxjs"; @@ -45,6 +47,8 @@ export class WidgetEditorAccordionComponent implements OnInit, OnDestroy { @Input() public state: AccordionState = AccordionState.DEFAULT; + @Output() public openToggle = new EventEmitter(); + public open = false; public openSubject = new Subject(); public destroySubject = new Subject(); @@ -59,6 +63,7 @@ export class WidgetEditorAccordionComponent implements OnInit, OnDestroy { } public openChange(isOpened: boolean): void { + this.openToggle.emit(isOpened); if (isOpened) { this.openSubject.next(); } else { diff --git a/packages/dashboards/src/lib/configurator/components/widgets/table/filters-editor/table-filters-editor.component.ts b/packages/dashboards/src/lib/configurator/components/widgets/table/filters-editor/table-filters-editor.component.ts index 9487fceff..20b4a3b11 100644 --- a/packages/dashboards/src/lib/configurator/components/widgets/table/filters-editor/table-filters-editor.component.ts +++ b/packages/dashboards/src/lib/configurator/components/widgets/table/filters-editor/table-filters-editor.component.ts @@ -99,6 +99,7 @@ export class TableFiltersEditorComponent const descendantSortingFormControl = this.form .get("sorterConfiguration") ?.get("descendantSorting"); + if (changes.sorterConfiguration) { const sortedColumn = this.sortableColumns.find( (column) => column.id === this.sorterConfiguration?.sortBy @@ -132,6 +133,7 @@ export class TableFiltersEditorComponent descendantSortingFormControl?.enable(); } } + this.changeDetector.detectChanges(); } diff --git a/packages/dashboards/src/lib/configurator/components/widgets/table/public-api.ts b/packages/dashboards/src/lib/configurator/components/widgets/table/public-api.ts index 4ec4b1352..843296bd1 100644 --- a/packages/dashboards/src/lib/configurator/components/widgets/table/public-api.ts +++ b/packages/dashboards/src/lib/configurator/components/widgets/table/public-api.ts @@ -19,6 +19,7 @@ // THE SOFTWARE. export * from "./filters-editor/table-filters-editor.component"; +export * from "./scrollType-editor/scroll-type-editor.component"; export * from "./columns-editor/table-columns-configuration.component"; export * from "./columns-editor/column-configuration/presentation-configuration/portals/formatter-configurator.component"; export * from "./columns-editor/column-configuration/presentation-configuration/portals/link-configurator/link-configurator.component"; diff --git a/packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component.html b/packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component.html new file mode 100644 index 000000000..9b0a674cb --- /dev/null +++ b/packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component.html @@ -0,0 +1,109 @@ + + +
+
+ + + + {{ column.title }} + + + +
+
+ +
+
+ + + {{ option.value }} + + + + Page size set is required + +
+ +
+ + + + {{ option }} + + + + Page size is required + + +
+
+
+
+
+
diff --git a/packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component.less b/packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component.less new file mode 100644 index 000000000..878e8715f --- /dev/null +++ b/packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component.less @@ -0,0 +1,21 @@ +@import (reference) "@nova-ui/bits/sdk/less/nui-framework-variables"; + +.table-filters-configuration { + &__accordion-content { + padding: @nui-space-md @nui-space-md @nui-space-md + (@nui-space-md * 2 + @icon-size-default); + + .scroll-type-field { + margin-bottom: @nui-space-sm; + } + + .expander-content { + display: flex; + + .page-size-set-menu { + margin-top: 26px; + margin-right: @nui-space-sm; + } + } + } +} diff --git a/packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component.spec.ts b/packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component.spec.ts new file mode 100644 index 000000000..66dd02cdf --- /dev/null +++ b/packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component.spec.ts @@ -0,0 +1,184 @@ +// © 2022 SolarWinds Worldwide, LLC. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; +import { FormBuilder } from "@angular/forms"; + +import { EventBus } from "@nova-ui/bits"; + +import { NuiDashboardsModule } from "../../../../../dashboards.module"; +import { DynamicComponentCreator } from "../../../../../pizzagna/services/dynamic-component-creator.service"; +import { PizzagnaService } from "../../../../../pizzagna/services/pizzagna.service"; +import { PIZZAGNA_EVENT_BUS } from "../../../../../types"; +import { TableScrollTypeEditorComponent } from "./scroll-type-editor.component"; +import { SimpleChange, SimpleChanges } from "@angular/core"; +import { ScrollType } from "@nova-ui/dashboards"; +import { ScrollTypeEditorService } from "./scroll-type-editor.service"; + +describe("TableScrollTypeEditorComponent", () => { + let component: TableScrollTypeEditorComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [NuiDashboardsModule], + providers: [ + PizzagnaService, + DynamicComponentCreator, + { + provide: PIZZAGNA_EVENT_BUS, + useClass: EventBus, + }, + { + provide: FormBuilder, + useValue: new FormBuilder(), + }, + { + provide: ScrollTypeEditorService, + }, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TableScrollTypeEditorComponent); + component = fixture.componentInstance; + component.ngOnInit(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + describe("ngOnChanges > ", () => { + it("should set the 'paginatorConfiguration' form controls", () => { + component.paginatorConfiguration = { + pageSize: 20, + pageSizeSet: [10, 20, 30], + }; + + const changes: SimpleChanges = { + paginatorConfiguration: {} as SimpleChange, + }; + + component.ngOnChanges(changes); + const paginatorConfigurationFormGroup = component.form.get( + "paginatorConfiguration" + ); + + expect( + paginatorConfigurationFormGroup?.get("pageSize")?.value + ).toEqual(component.paginatorConfiguration.pageSize); + expect( + paginatorConfigurationFormGroup?.get("pageSizeSet")?.value + ).toEqual(component.paginatorConfiguration.pageSizeSet); + }); + + it("should set the 'scrollType' form controls", () => { + component.scrollType = ScrollType.paginator; + + const changes: SimpleChanges = { + scrollType: {} as SimpleChange, + }; + + component.ngOnChanges(changes); + + const scrollTypeFormControl = component.form + .get("paginatorConfiguration") + ?.get("scrollType")?.value; + expect(scrollTypeFormControl).toEqual(component.scrollType); + }); + + it("should update 'pageSizeOptions' when there is change in 'paginatorConfiguration'", () => { + component.paginatorConfiguration = { + pageSize: 20, + pageSizeSet: [20, 50, 100], + }; + + const changes: SimpleChanges = { + paginatorConfiguration: {} as SimpleChange, + }; + + component.ngOnChanges(changes); + + expect(component.paginatorConfiguration.pageSizeSet).toEqual( + component.pageSizeOptions + ); + }); + + it("should set 'pageSizeSetOptions' according to values from 'paginatorConfiguration.pageSizeSet'", () => { + component.paginatorConfiguration = { + pageSize: 20, + pageSizeSet: [20, 50, 100], + }; + + const changes: SimpleChanges = { + paginatorConfiguration: {} as SimpleChange, + }; + + component.ngOnChanges(changes); + + component.pageSizeSetOptions.forEach((option) => { + component.paginatorConfiguration.pageSizeSet?.forEach( + (pageValue) => { + if (option.value === pageValue) { + expect(option.checked).toBeTrue(); + } + } + ); + }); + }); + + it("should display advanced configuration only for 'scrollType' set to paginator", () => { + component.scrollType = ScrollType.virtual; + + const changes: SimpleChanges = { + scrollType: {} as SimpleChange, + }; + + component.ngOnChanges(changes); + + expect(component.hasPaginator).toBeFalse(); + + component.scrollType = ScrollType.paginator; + + component.ngOnChanges(changes); + + expect(component.hasPaginator).toBeTrue(); + }); + + it("should set correctly subtitle according to selected 'scrollType'", () => { + component.scrollType = ScrollType.virtual; + + const changes: SimpleChanges = { + scrollType: {} as SimpleChange, + }; + + component.ngOnChanges(changes); + + expect(component.subtitle).toEqual( + "Scroll Type: " + + component.scrollTypeEditorService.loadStrategies.find( + (ls) => ls.id === ScrollType.virtual + )?.title + ); + }); + }); +}); diff --git a/packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component.ts b/packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component.ts new file mode 100644 index 000000000..59f1dc522 --- /dev/null +++ b/packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component.ts @@ -0,0 +1,279 @@ +// © 2022 SolarWinds Worldwide, LLC. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + SimpleChanges, +} from "@angular/core"; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + Validators, +} from "@angular/forms"; +import get from "lodash/get"; +import { Subject } from "rxjs"; +import { takeUntil } from "rxjs/operators"; + +import { + ITableWidgetPaginatorConfig, + ScrollType, +} from "../../../../../components/table-widget/types"; +import { IHasChangeDetector, IHasForm } from "../../../../../types"; +import { ConfiguratorHeadingService } from "../../../../services/configurator-heading.service"; +import { ScrollTypeEditorService } from "./scroll-type-editor.service"; + +export interface IPageSizeSetMenuOption { + value: number; + checked: boolean; +} +@Component({ + selector: "nui-scroll-type-editor-component", + templateUrl: "scroll-type-editor.component.html", + styleUrls: ["scroll-type-editor.component.less"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TableScrollTypeEditorComponent + implements OnInit, OnChanges, OnDestroy, IHasForm, IHasChangeDetector +{ + static lateLoadKey = "TableScrollTypeEditorComponent"; + + @Input() paginatorConfiguration: ITableWidgetPaginatorConfig; + @Input() hasVirtualScroll: boolean; + @Input() scrollType: ScrollType = ScrollType.virtual; + + @Output() formReady = new EventEmitter(); + + public form: FormGroup; + private onDestroy$ = new Subject(); + + private pageSizeSetAll = [ + 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100, + ]; + public pageSizeSetOptions: IPageSizeSetMenuOption[] = []; + public pageSizeOptions: number[] = []; + public subtitle = ""; + public isExpanderOpen = false; + public displayPageSizeSetErrorMessage = false; + public displayPageSizeErrorMessage = false; + + public scrollTypeFormControl?: AbstractControl | null; + public pageSizeSetFormControl?: AbstractControl | null; + public pageSizeFormControl?: AbstractControl | null; + + constructor( + private formBuilder: FormBuilder, + public configuratorHeading: ConfiguratorHeadingService, + public changeDetector: ChangeDetectorRef, + public scrollTypeEditorService: ScrollTypeEditorService + ) { + this.updatePaginatorSelectOptions(this.pageSizeSetAll, false); + } + + public ngOnChanges(changes: SimpleChanges): void { + if (changes.scrollType) { + this.scrollTypeFormControl?.setValue(this.scrollType, { + emitEvent: false, + }); + + this.changeExpanderState(false); + this.updateSubtitle(); + this.updateValidators(); + } + + if (changes.paginatorConfiguration) { + this.updatePaginatorSelectOptions( + this.paginatorConfiguration.pageSizeSet || [], + true + ); + this.updateDefaultPageSizeOptions( + this.paginatorConfiguration.pageSizeSet || [] + ); + + this.pageSizeSetFormControl?.setValue( + this.paginatorConfiguration.pageSizeSet, + { + emitEvent: false, + } + ); + + this.pageSizeFormControl?.setValue( + this.paginatorConfiguration.pageSize, + { + emitEvent: true, + } + ); + } + + this.changeDetector.detectChanges(); + } + + public ngOnInit(): void { + this.form = this.formBuilder.group({ + paginatorConfiguration: this.formBuilder.group({ + scrollType: get(this.scrollType, "", ScrollType.virtual), + pageSize: get(this.paginatorConfiguration, "pageSize", 10), + pageSizeSet: new FormControl( + get( + this.paginatorConfiguration, + "pageSizeSet", + [10, 20, 50] + ) + ), + }), + }); + + this.scrollTypeFormControl = this.form + .get("paginatorConfiguration") + ?.get("scrollType"); + + this.pageSizeSetFormControl = this.form + .get("paginatorConfiguration") + ?.get("pageSizeSet"); + + this.pageSizeFormControl = this.form + .get("paginatorConfiguration") + ?.get("pageSize"); + + this.form.valueChanges + .pipe(takeUntil(this.onDestroy$)) + .subscribe((val) => { + this.displayPageSizeErrorMessage = + !val.paginatorConfiguration.pageSize; + }); + + this.updateSubtitle(); + + this.scrollTypeFormControl?.valueChanges + .pipe(takeUntil(this.onDestroy$)) + .subscribe((val) => { + this.updateSubtitle(); + this.updateValidators(); + }); + + this.formReady.emit(this.form); + } + + public onPageSizeSetChange(item: IPageSizeSetMenuOption): void { + const option = this.pageSizeSetOptions.find( + (n) => n.value === item.value + ); + if (option) { + option.checked = !item.checked; + } + + this.displayPageSizeSetErrorMessage = + this.pageSizeSetOptions.filter((o) => o.checked).length === 0; + + this.emitUpdatedSelectedOptions(); + } + + public get hasPaginator() { + return this.scrollTypeFormControl?.value === ScrollType.paginator; + } + + public accordionToggle(isOpened: boolean) { + this.changeExpanderState(false); + } + + public changeExpanderState(isOpen: boolean) { + this.isExpanderOpen = isOpen; + } + + private updateSubtitle(): void { + this.subtitle = this.scrollTypeEditorService.setAccordionSubtitleValues( + this.hasVirtualScroll, + this.scrollTypeFormControl?.value + ); + } + + private updateValidators() { + if (this.hasPaginator) { + this.pageSizeFormControl?.addValidators(Validators.required); + this.pageSizeSetFormControl?.addValidators(Validators.required); + } else { + this.pageSizeFormControl?.clearValidators(); + this.pageSizeSetFormControl?.clearValidators(); + } + + this.updatePaginatorSelectOptions( + this.pageSizeSetFormControl?.value, + true + ); + this.updateDefaultPageSizeOptions(this.pageSizeSetFormControl?.value); + + this.pageSizeFormControl?.updateValueAndValidity(); + this.pageSizeSetFormControl?.updateValueAndValidity(); + } + + private emitUpdatedSelectedOptions() { + let filteredPageSizeSet = this.pageSizeSetOptions + .filter((o) => o.checked) + .map((o) => o.value); + + this.updateDefaultPageSizeOptions(filteredPageSizeSet); + this.pageSizeSetFormControl?.setValue(filteredPageSizeSet, { + emitEvent: false, + }); + } + + private updateDefaultPageSizeOptions(options: number[]) { + this.pageSizeOptions = options; + } + + private updatePaginatorSelectOptions( + options: number[], + isChecked: boolean + ) { + this.clearPageSizeSetOptions(); + + options.forEach((o) => { + const option = this.pageSizeSetOptions.find((po) => po.value === o); + if (option) { + option.checked = isChecked; + } else { + this.pageSizeSetOptions.push({ + value: o, + checked: isChecked, + }); + } + }); + } + + private clearPageSizeSetOptions() { + this.pageSizeSetOptions.forEach((option) => { + option.checked = false; + }); + } + + public ngOnDestroy(): void { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } +} diff --git a/packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.service.ts b/packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.service.ts new file mode 100644 index 000000000..e46c1cb2b --- /dev/null +++ b/packages/dashboards/src/lib/configurator/components/widgets/table/scrollType-editor/scroll-type-editor.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from "@angular/core"; +import { ScrollType } from "./../../../../../components/table-widget/types"; + +@Injectable() +export class ScrollTypeEditorService { + public loadStrategies = [ + { + id: ScrollType.virtual, + title: $localize`Virtual scroll`, + }, + { + id: ScrollType.paginator, + title: $localize`Paginator`, + }, + { + id: ScrollType.default, + title: $localize`Default scroll`, + }, + ]; + + public setAccordionSubtitleValues( + hasVirtualScroll: boolean, + scrollType: ScrollType + ): string { + const prefix = $localize`Scroll Type: `; + const result = hasVirtualScroll + ? `${prefix} ${this.getScrollTypeTitle(ScrollType.virtual)}` + : `${prefix} ${this.getScrollTypeTitle(scrollType)}`; + + return result; + } + + public getScrollTypeTitle(scrollType: ScrollType): string { + const result = + this.loadStrategies.find((ls) => ls.id === scrollType)?.title || + $localize`Unknown`; + + return result; + } +} diff --git a/packages/dashboards/src/lib/configurator/configurator.module.ts b/packages/dashboards/src/lib/configurator/configurator.module.ts index 6e0046ff6..09412053b 100644 --- a/packages/dashboards/src/lib/configurator/configurator.module.ts +++ b/packages/dashboards/src/lib/configurator/configurator.module.ts @@ -133,6 +133,8 @@ import { KpiWidgetColorService } from "./services/kpi-widget-color.service"; import { ConfiguratorHeadingService } from "./services/public-api"; import { WidgetClonerService } from "./services/widget-cloner.service"; import { WidgetEditorService } from "./services/widget-editor.service"; +import { TableScrollTypeEditorComponent } from "./components/widgets/table/scrollType-editor/scroll-type-editor.component"; +import { ScrollTypeEditorService } from "./components/widgets/table/scrollType-editor/scroll-type-editor.service"; /* eslint-enable max-len */ const entryComponents: IComponentWithLateLoadKey[] = [ @@ -149,6 +151,7 @@ const entryComponents: IComponentWithLateLoadKey[] = [ ProportionalChartOptionsEditorComponent, ProportionalChartOptionsEditorV2Component, TableFiltersEditorComponent, + TableScrollTypeEditorComponent, TimeseriesMetadataConfigurationComponent, TimeseriesSeriesCollectionConfigurationComponent, TableColumnsConfigurationComponent, @@ -252,6 +255,7 @@ const exportedDeclarations = [ KpiWidgetColorService, TimeseriesChartPresetService, TimeseriesScalesService, + ScrollTypeEditorService, ], exports: exportedDeclarations, }) diff --git a/packages/dashboards/src/lib/configurator/services/converters/table/mocks.ts b/packages/dashboards/src/lib/configurator/services/converters/table/mocks.ts index d7fb85045..bfe77d0f4 100644 --- a/packages/dashboards/src/lib/configurator/services/converters/table/mocks.ts +++ b/packages/dashboards/src/lib/configurator/services/converters/table/mocks.ts @@ -18,6 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +import { ScrollType } from "./../../../../components/table-widget/types"; import { DEFAULT_PIZZAGNA_ROOT } from "../../../../services/types"; import { PizzagnaLayer } from "../../../../types"; @@ -60,7 +61,12 @@ export const TABLE_WIDGET_PREVIEW_PIZZAGNA = { descendantSorting: true, sortBy: "column1", }, - hasVirtualScroll: true, + scrollType: ScrollType.paginator, + paginatorConfiguration: { + pageSize: 10, + pageSizeSet: [10, 20, 30], + }, + hasVirtualScroll: false, }, }, }, @@ -83,7 +89,12 @@ export const EDITOR_PIZZAGNA = { componentType: "WidgetConfiguratorSectionComponent", properties: { headerText: "Presentation", - nodes: ["titleAndDescription", "dataSource", "filters"], + nodes: [ + "titleAndDescription", + "dataSource", + "filters", + "scrollType", + ], }, }, titleAndDescription: { @@ -119,6 +130,15 @@ export const EDITOR_PIZZAGNA = { }, }, }, + scrollType: { + id: "scrollType", + componentType: "TableScrollTypeEditorComponent", + providers: { + NOVA_TABLE_SCROLL_TYPE_CONVERTER: { + providerId: "NOVA_TABLE_SCROLL_TYPE_CONVERTER", + }, + }, + }, columns: { id: "columns", componentType: "TableColumnsConfigurationComponent", diff --git a/packages/dashboards/src/lib/configurator/services/converters/table/table-filters-converter.service.spec.ts b/packages/dashboards/src/lib/configurator/services/converters/table/table-filters-converter.service.spec.ts index e3d3db4ac..8835e9146 100644 --- a/packages/dashboards/src/lib/configurator/services/converters/table/table-filters-converter.service.spec.ts +++ b/packages/dashboards/src/lib/configurator/services/converters/table/table-filters-converter.service.spec.ts @@ -101,9 +101,11 @@ describe("TableFiltersConverterService >", () => { }; expectedPreviewPizzagna.table.properties.configuration.sorterConfiguration = mockedSortingState; + component.form .get("sorterConfiguration") ?.patchValue(mockedSortingState); + tick(0); expect(service.updatePreview).toHaveBeenCalledWith( expectedPreviewPizzagna diff --git a/packages/dashboards/src/lib/configurator/services/converters/table/table-filters-converter.service.ts b/packages/dashboards/src/lib/configurator/services/converters/table/table-filters-converter.service.ts index 38bcbf625..6920526b0 100644 --- a/packages/dashboards/src/lib/configurator/services/converters/table/table-filters-converter.service.ts +++ b/packages/dashboards/src/lib/configurator/services/converters/table/table-filters-converter.service.ts @@ -69,18 +69,17 @@ export class TableFiltersConverterService } public toPreview(form: FormGroup): void { - form.valueChanges - .pipe(takeUntil(this.destroy$)) - .subscribe((filters) => { - let preview = this.getPreview(); - preview = immutableSet( - preview, - "table.properties.configuration.sorterConfiguration", - filters.sorterConfiguration - ); - this.updatePreview(preview); - // we need to update form with columns that are available - this.buildForm(); - }); + form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((form) => { + let preview = this.getPreview(); + preview = immutableSet( + preview, + "table.properties.configuration.sorterConfiguration", + form.sorterConfiguration + ); + + this.updatePreview(preview); + // we need to update form with columns that are available + this.buildForm(); + }); } } diff --git a/packages/dashboards/src/lib/configurator/services/converters/table/table-scroll-type-converter.service.spec.ts b/packages/dashboards/src/lib/configurator/services/converters/table/table-scroll-type-converter.service.spec.ts new file mode 100644 index 000000000..88b866c30 --- /dev/null +++ b/packages/dashboards/src/lib/configurator/services/converters/table/table-scroll-type-converter.service.spec.ts @@ -0,0 +1,131 @@ +// © 2022 SolarWinds Worldwide, LLC. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import { fakeAsync, tick } from "@angular/core/testing"; +import { FormBuilder, FormGroup } from "@angular/forms"; + +import { EventBus, IEvent } from "@nova-ui/bits"; + +import { DynamicComponentCreator } from "../../../../pizzagna/services/dynamic-component-creator.service"; +import { PizzagnaService } from "../../../../pizzagna/services/pizzagna.service"; +import { PreviewService } from "../../preview.service"; +import { EDITOR_PIZZAGNA, TABLE_WIDGET_PREVIEW_PIZZAGNA } from "./mocks"; +import { TableScrollTypeConverterService } from "./table-scroll-type-converter.service"; +import { ScrollType } from "@nova-ui/dashboards"; + +class MockComponent { + public static lateLoadKey = "MockComponent"; + public form: FormGroup; + + constructor(private formBuilder: FormBuilder) { + this.form = formBuilder.group({ + paginatorConfiguration: formBuilder.group({ + scrollType: ScrollType.virtual, + pageSize: 0, + pageSizeSet: [], + }), + }); + } +} + +const mockedPaginatorState = { + pageSize: 20, + pageSizeSet: [10, 20, 30, 40], + scrollType: ScrollType.paginator, +}; + +describe("TableScrollTypeConverterService >", () => { + const eventBus = new EventBus(); + const formBuilder = new FormBuilder(); + const component = new MockComponent(formBuilder); + const previewService = new PreviewService(); + const dynamicComponentCreator = new DynamicComponentCreator(); + const pizzagnaService = new PizzagnaService( + eventBus, + dynamicComponentCreator + ); + + let service: TableScrollTypeConverterService; + + beforeEach(() => { + previewService.preview = + TABLE_WIDGET_PREVIEW_PIZZAGNA.pizzagna.configuration; + pizzagnaService.pizzagna = EDITOR_PIZZAGNA; + service = new TableScrollTypeConverterService( + eventBus, + previewService, + pizzagnaService + ); + service.setComponent(component as any, ""); + service.ngAfterViewInit(); + }); + + it("should have component set", () => { + expect(service.component).toBeDefined(); + }); + + it("should properly pass data from preview to form pizzagna", () => { + const paginatorConfigurationInFormPizzagna = + pizzagnaService.pizzagna.data.scrollType.properties + ?.paginatorConfiguration; + const paginatorConfigurationInPreviewPizzagna = + TABLE_WIDGET_PREVIEW_PIZZAGNA.pizzagna.configuration.table + .properties.configuration.paginatorConfiguration; + + const scrollTypeInFormPizzagna = + pizzagnaService.pizzagna.data.scrollType.properties?.scrollType; + const scrollTypeInPreviewPizzagna = + TABLE_WIDGET_PREVIEW_PIZZAGNA.pizzagna.configuration.table + .properties.configuration.scrollType; + + const hasVirtualScrollInFormPizzagna = + pizzagnaService.pizzagna.data.scrollType.properties + ?.hasVirtualScroll; + const shasVirtualScrollInPreviewPizzagna = + TABLE_WIDGET_PREVIEW_PIZZAGNA.pizzagna.configuration.table + .properties.configuration.hasVirtualScroll; + + expect(paginatorConfigurationInPreviewPizzagna).toEqual( + paginatorConfigurationInFormPizzagna + ); + expect(scrollTypeInPreviewPizzagna).toEqual(scrollTypeInFormPizzagna); + expect(shasVirtualScrollInPreviewPizzagna).toEqual( + hasVirtualScrollInFormPizzagna + ); + }); + + it("should properly update preview from form in editor", fakeAsync(() => { + spyOn(service, "updatePreview"); + const expectedPreviewPizzagna = { + ...TABLE_WIDGET_PREVIEW_PIZZAGNA.pizzagna.configuration, + }; + expectedPreviewPizzagna.table.properties.configuration.paginatorConfiguration = + mockedPaginatorState; + + component.form + .get("paginatorConfiguration") + ?.patchValue(mockedPaginatorState); + + tick(0); + expect(service.updatePreview).toHaveBeenCalledWith( + expectedPreviewPizzagna + ); + })); +}); diff --git a/packages/dashboards/src/lib/configurator/services/converters/table/table-scroll-type-converter.service.ts b/packages/dashboards/src/lib/configurator/services/converters/table/table-scroll-type-converter.service.ts new file mode 100644 index 000000000..ea82bc5eb --- /dev/null +++ b/packages/dashboards/src/lib/configurator/services/converters/table/table-scroll-type-converter.service.ts @@ -0,0 +1,112 @@ +// © 2022 SolarWinds Worldwide, LLC. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import { AfterViewInit, Inject, Injectable } from "@angular/core"; +import { FormGroup } from "@angular/forms"; +import { takeUntil } from "rxjs/operators"; + +import { EventBus, IEvent, immutableSet } from "@nova-ui/bits"; + +import { PizzagnaService } from "../../../../pizzagna/services/pizzagna.service"; +import { PizzagnaLayer, PIZZAGNA_EVENT_BUS } from "../../../../types"; +import { PreviewService } from "../../preview.service"; +import { BaseConverter } from "../base-converter"; + +@Injectable() +export class TableScrollTypeConverterService + extends BaseConverter + implements AfterViewInit +{ + constructor( + @Inject(PIZZAGNA_EVENT_BUS) eventBus: EventBus, + previewService: PreviewService, + pizzagnaService: PizzagnaService + ) { + super(eventBus, previewService, pizzagnaService); + } + + public ngAfterViewInit(): void { + super.ngAfterViewInit(); + } + + public buildForm(): void { + let formPizzagna = this.pizzagnaService.pizzagna; + + const table = this.getPreview()?.table; + + const paginatorConfiguration = + table?.properties?.configuration?.paginatorConfiguration; + const hasVirtualScroll = + table?.properties?.configuration?.hasVirtualScroll; + const scrollType = table?.properties?.configuration?.scrollType; + + formPizzagna = immutableSet( + formPizzagna, + `${PizzagnaLayer.Data}.scrollType.properties.paginatorConfiguration`, + paginatorConfiguration + ); + + formPizzagna = immutableSet( + formPizzagna, + `${PizzagnaLayer.Data}.scrollType.properties.hasVirtualScroll`, + hasVirtualScroll + ); + + formPizzagna = immutableSet( + formPizzagna, + `${PizzagnaLayer.Data}.scrollType.properties.scrollType`, + scrollType + ); + + this.updateFormPizzagna(formPizzagna); + } + + public toPreview(form: FormGroup): void { + form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((form) => { + let preview = this.getPreview(); + + preview = immutableSet( + preview, + "table.properties.configuration.hasVirtualScroll", + false + ); + + preview = immutableSet( + preview, + "table.properties.configuration.scrollType", + form.paginatorConfiguration.scrollType + ); + + preview = immutableSet( + preview, + "table.properties.configuration.paginatorConfiguration.pageSizeSet", + form.paginatorConfiguration.pageSizeSet + ); + + preview = immutableSet( + preview, + "table.properties.configuration.paginatorConfiguration.pageSize", + form.paginatorConfiguration.pageSize + ); + + this.updatePreview(preview); + }); + } +} diff --git a/packages/dashboards/src/lib/dashboards.module.ts b/packages/dashboards/src/lib/dashboards.module.ts index 8b8fd58b2..1687272bd 100644 --- a/packages/dashboards/src/lib/dashboards.module.ts +++ b/packages/dashboards/src/lib/dashboards.module.ts @@ -35,6 +35,7 @@ import { NuiIconModule, NuiImageModule, NuiMenuModule, + NuiPaginatorModule, NuiPopoverModule, NuiPopupModule, NuiProgressModule, @@ -213,6 +214,7 @@ const entryComponents: IComponentWithLateLoadKey[] = [ NuiRiskScoreModule, NuiSelectModule, NuiPopoverModule, + NuiPaginatorModule, ], declarations: dashboardComponents, providers: [ diff --git a/packages/dashboards/src/lib/services/provider-registry.service.ts b/packages/dashboards/src/lib/services/provider-registry.service.ts index 81eadbeff..78f60a645 100644 --- a/packages/dashboards/src/lib/services/provider-registry.service.ts +++ b/packages/dashboards/src/lib/services/provider-registry.service.ts @@ -60,6 +60,8 @@ import { GenericConverterService } from "../configurator/services/converters/sha import { TitleAndDescriptionConverterService } from "../configurator/services/converters/shared/title-and-description-converter/title-and-description-converter.service"; import { TableColumnsConverterService } from "../configurator/services/converters/table/table-columns-converter.service"; import { TableFiltersConverterService } from "../configurator/services/converters/table/table-filters-converter.service"; +import { TableScrollTypeConverterService } from "../configurator/services/converters/table/table-scroll-type-converter.service"; + import { TimeseriesMetadataConverterService } from "../configurator/services/converters/timeseries/timeseries-metadata-converter.service"; import { TimeseriesSeriesConverterService } from "../configurator/services/converters/timeseries/timeseries-series-converter.service"; import { TimeseriesTileIndicatorDataConverterService } from "../configurator/services/converters/timeseries/timeseries-tile-indicator-data-converter.service"; @@ -114,6 +116,7 @@ import { NOVA_URL_INTERACTION_HANDLER, NOVA_VIRTUAL_VIEWPORT_MANAGER, NOVA_RISK_SCORE_FORMATTERS_REGISTRY, + NOVA_TABLE_SCROLL_TYPE_CONVERTER, } from "./types"; import { UrlInteractionService } from "./url-interaction.service"; import { WidgetConfigurationService } from "./widget-configuration.service"; @@ -254,6 +257,11 @@ export class ProviderRegistryService { useClass: TableFiltersConverterService, deps: [PIZZAGNA_EVENT_BUS, PreviewService, PizzagnaService], }, + [NOVA_TABLE_SCROLL_TYPE_CONVERTER]: { + provide: CONFIGURATOR_CONVERTER, + useClass: TableScrollTypeConverterService, + deps: [PIZZAGNA_EVENT_BUS, PreviewService, PizzagnaService], + }, [NOVA_LOADING_ADAPTER]: { provide: LoadingAdapter, deps: [PIZZAGNA_EVENT_BUS, PizzagnaService], diff --git a/packages/dashboards/src/lib/services/types.ts b/packages/dashboards/src/lib/services/types.ts index b7df49841..818feac91 100644 --- a/packages/dashboards/src/lib/services/types.ts +++ b/packages/dashboards/src/lib/services/types.ts @@ -124,6 +124,8 @@ export const NOVA_TIMESERIES_SERIES_CONVERTER = export const NOVA_DASHBOARD_EVENT_PROXY = "NOVA_DASHBOARD_EVENT_PROXY"; export const NOVA_TABLE_COLUMNS_CONVERTER = "NOVA_TABLE_COLUMNS_CONVERTER"; export const NOVA_TABLE_FILTERS_CONVERTER = "NOVA_TABLE_FILTERS_CONVERTER"; +export const NOVA_TABLE_SCROLL_TYPE_CONVERTER = + "NOVA_TABLE_SCROLL_TYPE_CONVERTER"; export const NOVA_TABLE_DATASOURCE_ADAPTER = "NOVA_TABLE_DATASOURCE_ADAPTER"; export const NOVA_GENERIC_CONVERTER = "NOVA_GENERIC_CONVERTER"; export const NOVA_GENERIC_ARRAY_CONVERTER = "NOVA_GENERIC_ARRAY_CONVERTER"; diff --git a/packages/dashboards/src/lib/widget-types/table/table-configurator.ts b/packages/dashboards/src/lib/widget-types/table/table-configurator.ts index e38804993..020eb6bec 100644 --- a/packages/dashboards/src/lib/widget-types/table/table-configurator.ts +++ b/packages/dashboards/src/lib/widget-types/table/table-configurator.ts @@ -19,6 +19,7 @@ // THE SOFTWARE. /* eslint-disable max-len */ +import { TableScrollTypeEditorComponent } from "../../configurator/components/widgets/table/scrollType-editor/scroll-type-editor.component"; import { FormStackComponent } from "../../configurator/components/form-stack/form-stack.component"; import { WidgetConfiguratorSectionComponent } from "../../configurator/components/widget-configurator-section/widget-configurator-section.component"; import { DataSourceConfigurationComponent } from "../../configurator/components/widgets/configurator-items/data-source-configuration/data-source-configuration.component"; @@ -33,6 +34,7 @@ import { NOVA_TABLE_COLUMNS_CONVERTER, NOVA_TABLE_FILTERS_CONVERTER, NOVA_TABLE_FORMATTERS_REGISTRY, + NOVA_TABLE_SCROLL_TYPE_CONVERTER, NOVA_TITLE_AND_DESCRIPTION_CONVERTER, } from "../../services/types"; import { IPizzagna, PizzagnaLayer, WellKnownProviders } from "../../types"; @@ -68,7 +70,7 @@ export const tableConfigurator: IPizzagna = { properties: { headerText: $localize`Presentation`, // references to other components laid out in this form - nodes: ["titleAndDescription", "filters"], + nodes: ["titleAndDescription", "filters", "scrollType"], }, }, // /presentation/titleAndDescription @@ -82,7 +84,7 @@ export const tableConfigurator: IPizzagna = { }, }, }, - // /presentation/filters - configuration of built-in filters like search, sorting and pagination + // /presentation/filters - !WARNING! configuration of built-in sorting, naming is obsolete filters: { id: "filters", componentType: TableFiltersEditorComponent.lateLoadKey, @@ -92,6 +94,16 @@ export const tableConfigurator: IPizzagna = { }, }, }, + // /presentation/scrollType - configuration of built-in pagination + scrollType: { + id: "scrollType", + componentType: TableScrollTypeEditorComponent.lateLoadKey, + providers: { + [WellKnownProviders.Converter]: { + providerId: NOVA_TABLE_SCROLL_TYPE_CONVERTER, + }, + }, + }, refresher: REFRESHER_CONFIGURATOR, // /dataAndCalculations dataAndCalculations: {