From 205431935e33e81f82332dabd85f94a74a9a0fb6 Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Mon, 13 Jan 2020 18:58:18 -0500 Subject: [PATCH] feat(footer): add custom footer to show metrics - this footer is typically only available when user is not using a Backend Service since they show the same kind of info --- src/app/examples/grid-clientside.component.ts | 4 +- .../examples/grid-localization.component.ts | 11 +++++ .../angular-slickgrid-constructor.spec.ts | 5 ++- .../angular-slickgrid.component.html | 20 +++++++++ .../components/angular-slickgrid.component.ts | 44 ++++++++++++++++++- .../angular-slickgrid/global-grid-options.ts | 9 ++++ .../models/customFooterOption.interface.ts | 28 ++++++++++++ .../models/gridOption.interface.ts | 13 ++++++ .../modules/angular-slickgrid/models/index.ts | 1 + .../services/resizer.service.ts | 6 +++ .../angular-slickgrid/styles/_variables.scss | 14 ++++++ .../styles/slick-footer.scss | 20 +++++++++ .../styles/slickgrid-theme-bootstrap.scss | 1 + 13 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 src/app/modules/angular-slickgrid/models/customFooterOption.interface.ts create mode 100644 src/app/modules/angular-slickgrid/styles/slick-footer.scss diff --git a/src/app/examples/grid-clientside.component.ts b/src/app/examples/grid-clientside.component.ts index a33390820..a53fb4b71 100644 --- a/src/app/examples/grid-clientside.component.ts +++ b/src/app/examples/grid-clientside.component.ts @@ -193,7 +193,8 @@ export class GridClientSideComponent implements OnInit { { columnId: 'duration', direction: 'DESC' }, { columnId: 'complete', direction: 'ASC' } ], - } + }, + showCustomFooter: true }; // mock a dataset @@ -272,6 +273,7 @@ export class GridClientSideComponent implements OnInit { setTimeout(() => { this.metrics = { startTime: new Date(), + endTime: new Date(), itemCount: args && args.current || 0, totalItemCount: this.dataset.length || 0 }; diff --git a/src/app/examples/grid-localization.component.ts b/src/app/examples/grid-localization.component.ts index 9fa817232..329591387 100644 --- a/src/app/examples/grid-localization.component.ts +++ b/src/app/examples/grid-localization.component.ts @@ -139,6 +139,17 @@ export class GridLocalizationComponent implements OnInit { enableFiltering: true, enableTranslate: true, i18n: this.translate, + customFooterOptions: { + leftFooterText: 'text shown on left container', // optionally display some text on left footer container + metricTexts: { + textItems: 'TEXT_ITEMS', + textOf: 'TEXT_OF', + // lastUpdatedText: 'Last Updated', + }, + dateFormat: 'yyyy-MM-dd HH:mm aaaaa\'m\'', + hideTotalItemCount: false, + }, + showCustomFooter: true, excelExportOptions: { // optionally pass a custom header to the Excel Sheet // a lot of the info can be found on Web Archive of Excel-Builder diff --git a/src/app/modules/angular-slickgrid/components/__tests__/angular-slickgrid-constructor.spec.ts b/src/app/modules/angular-slickgrid/components/__tests__/angular-slickgrid-constructor.spec.ts index 7a18342d1..c2ef35531 100644 --- a/src/app/modules/angular-slickgrid/components/__tests__/angular-slickgrid-constructor.spec.ts +++ b/src/app/modules/angular-slickgrid/components/__tests__/angular-slickgrid-constructor.spec.ts @@ -24,6 +24,7 @@ import { Column, CurrentFilter, CurrentSorter, GridOption, GridState, GridStateC import { Filters } from '../../filters'; import { Editors } from '../../editors'; import * as utilities from '../../services/backend-utilities'; +import { GraphqlPaginatedResult } from 'dist/public_api'; const mockExecuteBackendProcess = jest.fn(); @@ -593,7 +594,7 @@ describe('Angular-Slickgrid Custom Component instantiated via Constructor', () = const spy = jest.spyOn(component, 'refreshGridData'); component.ngAfterViewInit(); - component.gridOptions.backendServiceApi.internalPostProcess({ data: { users: { nodes: [{ firstName: 'John' }], pageInfo: { hasNextPage: false }, totalCount: 2 } } }); + component.gridOptions.backendServiceApi.internalPostProcess({ data: { users: { nodes: [{ firstName: 'John' }], totalCount: 2 } } } as GraphqlPaginatedResult); expect(spy).toHaveBeenCalled(); expect(component.gridOptions.backendServiceApi.internalPostProcess).toEqual(expect.any(Function)); @@ -617,7 +618,7 @@ describe('Angular-Slickgrid Custom Component instantiated via Constructor', () = const spy = jest.spyOn(component, 'refreshGridData'); component.ngAfterViewInit(); - component.gridOptions.backendServiceApi.internalPostProcess({ data: { notUsers: { nodes: [{ firstName: 'John' }], pageInfo: { hasNextPage: false }, totalCount: 2 } } }); + component.gridOptions.backendServiceApi.internalPostProcess({ data: { notUsers: { nodes: [{ firstName: 'John' }], totalCount: 2 } } } as GraphqlPaginatedResult); expect(spy).not.toHaveBeenCalled(); expect(component.dataset).toEqual([]); diff --git a/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.html b/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.html index dbdf7033a..fa8ac4b00 100644 --- a/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.html +++ b/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.html @@ -7,4 +7,24 @@ [dataView]="dataView" [grid]="grid" [options]="paginationOptions" [locales]="locales" [totalItems]="totalItems" [backendServiceApi]="backendServiceApi"> + + diff --git a/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts b/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts index 17888d9c1..09269a53b 100644 --- a/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts +++ b/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts @@ -26,12 +26,13 @@ import { GridStateChange, GridStateType, Locale, + Metrics, Pagination, SlickEventHandler, } from './../models/index'; import { FilterFactory } from '../filters/filterFactory'; import { SlickgridConfig } from '../slickgrid-config'; -import { isObservable, Observable, Subscription, Subject } from 'rxjs'; +import { isObservable, Observable, Subscription } from 'rxjs'; // Services import { AngularUtilService } from '../services/angularUtil.service'; @@ -125,7 +126,10 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn groupItemMetadataProvider: any; backendServiceApi: BackendServiceApi; locales: Locale; + metrics: Metrics; + metricsDateFormat = 'yyyy-MM-dd HH:mm aaaaa\'m\''; paginationOptions: Pagination; + showCustomFooter = false; showPagination = false; totalItems = 0; isGridInitialized = false; @@ -394,6 +398,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn this.extensionService.translateContextMenu(); this.extensionService.translateGridMenu(); this.extensionService.translateHeaderMenu(); + this.translateCustomFooterTexts(); } }) ); @@ -472,7 +477,16 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn this.gridEventService.bindOnClick(grid, dataView); if (dataView && grid) { - this._eventHandler.subscribe(dataView.onRowCountChanged, () => grid.invalidate()); + this._eventHandler.subscribe(dataView.onRowCountChanged, (e: Event, args: any) => { + grid.invalidate(); + + this.metrics = { + startTime: new Date(), + endTime: new Date(), + itemCount: args && args.current || 0, + totalItemCount: this.dataset.length || 0 + }; + }); // without this, filtering data with local dataset will not always show correctly // also don't use "invalidateRows" since it destroys the entire row and as bad user experience when updating a row @@ -727,6 +741,9 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn /** @deprecated please use "extensionService" instead */ pluginService: this.extensionService, }); + + // user could show a custom footer with the data metrics (dataset length and last updated timestamp) + this.showCustomFooterWithMetrics(); } /** Load the Editor Collection asynchronously and replace the "collection" property when Observable resolves */ @@ -763,6 +780,21 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn return options; } + private showCustomFooterWithMetrics() { + setTimeout(() => { + if (this.gridOptions && !(this.gridOptions.backendServiceApi || this.gridOptions.enablePagination)) { + this.showCustomFooter = this.gridOptions.hasOwnProperty('showCustomFooter') ? this.gridOptions.showCustomFooter : true; + } + }); + + if (this.gridOptions.enableTranslate || this.gridOptions.i18n) { + this.translateCustomFooterTexts(); + } else { + this.gridOptions.customFooterOptions.metricTexts.textItems = this.locales && this.locales.TEXT_ITEMS || 'TEXT_ITEMS'; + this.gridOptions.customFooterOptions.metricTexts.textOf = this.locales && this.locales.TEXT_OF || 'TEXT_OF'; + } + } + /** * For convenience to the user, we provide the property "editor" as an Angular-Slickgrid editor complex object * however "editor" is used internally by SlickGrid for it's own Editor Factory @@ -779,6 +811,14 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn }); } + /** Translate all Custom Footer Texts (footer with metrics) */ + private translateCustomFooterTexts() { + if (this.gridOptions && this.gridOptions.customFooterOptions && this.gridOptions.customFooterOptions.metricTexts) { + this.gridOptions.customFooterOptions.metricTexts.textItems = this.translate.instant('ITEMS'); + this.gridOptions.customFooterOptions.metricTexts.textOf = this.translate.instant('OF'); + } + } + /** * Update the Editor "collection" property from an async call resolved * Since this is called after the async call resolves, the pointer will not be the same as the "column" argument passed. diff --git a/src/app/modules/angular-slickgrid/global-grid-options.ts b/src/app/modules/angular-slickgrid/global-grid-options.ts index d828ac96b..0d4e6ab53 100644 --- a/src/app/modules/angular-slickgrid/global-grid-options.ts +++ b/src/app/modules/angular-slickgrid/global-grid-options.ts @@ -52,6 +52,15 @@ export const GlobalGridOptions: GridOption = { iconExportTextDelimitedCommand: 'fa fa-download', width: 200, }, + customFooterOptions: { + dateFormat: 'yyyy-MM-dd HH:mm aaaaa\'m\'', + hideTotalItemCount: false, + metricSeparator: '|', + metricTexts: { + textItems: 'TEXT_ITEMS', + textOf: 'OF', + } + }, datasetIdPropertyName: 'id', defaultFilter: Filters.input, enableFilterTrimWhiteSpace: false, // do we want to trim white spaces on all Filters? diff --git a/src/app/modules/angular-slickgrid/models/customFooterOption.interface.ts b/src/app/modules/angular-slickgrid/models/customFooterOption.interface.ts new file mode 100644 index 000000000..0e0997f84 --- /dev/null +++ b/src/app/modules/angular-slickgrid/models/customFooterOption.interface.ts @@ -0,0 +1,28 @@ +export interface CustomFooterOption { + /** Optionally pass some text to be displayed on the left side (in the "left-footer" css class) */ + leftFooterText?: string; + + /** Date format used when showing the startTime/endTime metrics on screen */ + dateFormat?: string; + + /** Defaults to false, do we want to hide the last updated timestamp (endTime)? */ + hideLastUpdatedTimestamp?: boolean; + + /** Defaults to false, do we want to hide the metrics when the footer is displayed? */ + hideMetrics?: boolean; + + /** Defaults to false, do we want to hide the total item count of the entire dataset (the count exclude any filtered data) */ + hideTotalItemCount?: boolean; + + /** Defaults to "|", separator between the timestamp and the total count */ + metricSeparator?: string; + + metricTexts?: { + /** Optionally pass a text to display before the metrics endTime timestamp shown in the footer */ + lastUpdatedText?: string; + + textItems: string; + textLastUpdated?: string; + textOf: string; + } +} diff --git a/src/app/modules/angular-slickgrid/models/gridOption.interface.ts b/src/app/modules/angular-slickgrid/models/gridOption.interface.ts index f60608bc2..b44249cc1 100644 --- a/src/app/modules/angular-slickgrid/models/gridOption.interface.ts +++ b/src/app/modules/angular-slickgrid/models/gridOption.interface.ts @@ -24,6 +24,7 @@ import { RowDetailView, RowMoveManager } from './index'; +import { CustomFooterOption } from './customFooterOption.interface'; export interface GridOption { /** CSS class name used on newly added row */ @@ -101,6 +102,9 @@ export interface GridOption { /** Default to false, which leads to create an extra pre-header panel (on top of column header) for column grouping purposes */ createPreHeaderPanel?: boolean; + /** Custom Footer Options */ + customFooterOptions?: CustomFooterOption; + /** Data item column value extractor (getter) that can be used by the Excel like copy buffer plugin */ dataItemColumnValueExtractor?: (item: any, columnDef: Column) => any; @@ -368,12 +372,21 @@ export interface GridOption { /** Do we want to show cell selection? */ showCellSelection?: boolean; + /** + * Do we want to show a custom footer with some metrics? + * By default it will show how many items are in the dataset and when was last update done (timestamp) + * */ + showCustomFooter?: boolean; + /** Do we want to show the footer row? */ showFooterRow?: boolean; /** Do we want to show header row? */ showHeaderRow?: boolean; + /** Do we want to show metrics in custom footer? (dataset length, data filtered, last update timestamp) */ + showFooterMetrics?: boolean; + /** Do we want to show the extra pre-header panel (on top of column header) for column grouping purposes */ showPreHeaderPanel?: boolean; diff --git a/src/app/modules/angular-slickgrid/models/index.ts b/src/app/modules/angular-slickgrid/models/index.ts index 840e707af..956056c47 100644 --- a/src/app/modules/angular-slickgrid/models/index.ts +++ b/src/app/modules/angular-slickgrid/models/index.ts @@ -26,6 +26,7 @@ export * from './currentColumn.interface'; export * from './currentFilter.interface'; export * from './currentPagination.interface'; export * from './currentSorter.interface'; +export * from './customFooterOption.interface'; export * from './delimiterType.enum'; export * from './draggableGrouping.interface'; export * from './editCommand.interface'; diff --git a/src/app/modules/angular-slickgrid/services/resizer.service.ts b/src/app/modules/angular-slickgrid/services/resizer.service.ts index d5626e81c..4d7eacbbe 100644 --- a/src/app/modules/angular-slickgrid/services/resizer.service.ts +++ b/src/app/modules/angular-slickgrid/services/resizer.service.ts @@ -9,6 +9,7 @@ declare var $: any; const DATAGRID_MIN_HEIGHT = 180; const DATAGRID_MIN_WIDTH = 300; const DATAGRID_BOTTOM_PADDING = 20; +const DATAGRID_FOOTER_HEIGHT = 20; const DATAGRID_PAGINATION_HEIGHT = 35; export interface GridDimension { @@ -87,6 +88,11 @@ export class ResizerService { bottomPadding += DATAGRID_PAGINATION_HEIGHT; } + // optionally show a custom footer with the data metrics (dataset length and last updated timestamp) + if (bottomPadding && gridOptions.showCustomFooter) { + bottomPadding += DATAGRID_FOOTER_HEIGHT; + } + let gridHeight = 0; let gridOffsetTop = 0; diff --git a/src/app/modules/angular-slickgrid/styles/_variables.scss b/src/app/modules/angular-slickgrid/styles/_variables.scss index 2288ea48b..32657f65a 100644 --- a/src/app/modules/angular-slickgrid/styles/_variables.scss +++ b/src/app/modules/angular-slickgrid/styles/_variables.scss @@ -496,3 +496,17 @@ $viewport-border-top: 0 none !default; $viewport-border-right: 0 none !default; $viewport-border-bottom: 0 none !default; $viewport-border-left: 0 none !default; + +/* Custom Footer */ +$footer-bg-color: transparent !default; +$footer-font-style: italic !default; +$footer-font-weight: normal !default; +$footer-left-font-style: italic !default; +$footer-left-font-weight: normal !default; +$footer-left-text-align: left !default; +$footer-metrics-text-align: right !default; +$footer-metrics-font-style: italic !default; +$footer-metrics-font-weight: normal !default; +$footer-metrics-text-align: right !default; +$footer-text-color: #6e6e6e !default; +$footer-padding: 5px !default; diff --git a/src/app/modules/angular-slickgrid/styles/slick-footer.scss b/src/app/modules/angular-slickgrid/styles/slick-footer.scss new file mode 100644 index 000000000..0bd80da40 --- /dev/null +++ b/src/app/modules/angular-slickgrid/styles/slick-footer.scss @@ -0,0 +1,20 @@ +.slick-custom-footer { + color: $footer-text-color; + padding: $footer-padding; + background-color: $footer-bg-color; + font-style: $footer-font-style; + font-weight: $footer-font-weight; + + .left-footer { + font-style: $footer-left-font-style; + font-weight: $footer-left-font-weight; + text-align: $footer-left-text-align; + } + + .metrics { + text-align: $footer-metrics-text-align; + font-style: $footer-metrics-font-style; + font-weight: $footer-metrics-font-weight; + text-align: $footer-metrics-text-align; + } +} diff --git a/src/app/modules/angular-slickgrid/styles/slickgrid-theme-bootstrap.scss b/src/app/modules/angular-slickgrid/styles/slickgrid-theme-bootstrap.scss index f716a9411..4c23477ba 100644 --- a/src/app/modules/angular-slickgrid/styles/slickgrid-theme-bootstrap.scss +++ b/src/app/modules/angular-slickgrid/styles/slickgrid-theme-bootstrap.scss @@ -4,6 +4,7 @@ @import './slick-plugins'; @import './slick-default-theme'; @import './slick-pagination'; +@import './slick-footer'; @import './slickgrid-examples'; @import './slick-bootstrap'; @import './bootstrap-jquery-ui-autocomplete';