From 92b4c8db95abda3c7c6da458a54e9302d46addac Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Wed, 15 Jan 2020 23:40:02 -0500 Subject: [PATCH 1/2] 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 --- assets/i18n/en/aurelia-slickgrid.json | 1 + assets/i18n/fr/aurelia-slickgrid.json | 1 + src/assets/i18n/en/aurelia-slickgrid.json | 1 + src/assets/i18n/fr/aurelia-slickgrid.json | 1 + src/aurelia-slickgrid/constants.ts | 1 + .../aurelia-slickgrid-constructor.spec.ts | 85 +++++++++++++++++-- .../custom-elements/aurelia-slickgrid.html | 35 +++++++- .../custom-elements/aurelia-slickgrid.ts | 66 +++++++++++++- src/aurelia-slickgrid/global-grid-options.ts | 15 ++++ src/aurelia-slickgrid/index.ts | 1 + .../models/customFooterOption.interface.ts | 51 +++++++++++ .../models/gridOption.interface.ts | 13 +++ src/aurelia-slickgrid/models/index.ts | 1 + .../models/locale.interface.ts | 3 + .../__tests__/resizer.service.spec.ts | 38 +++++++++ .../services/resizer.service.ts | 6 ++ src/aurelia-slickgrid/styles/_variables.scss | 19 +++++ .../styles/slick-footer.scss | 30 +++++++ .../styles/slickgrid-theme-bootstrap.scss | 1 + .../value-converters/asgDateFormat.spec.ts | 9 ++ .../value-converters/asgDateFormat.ts | 7 ++ src/examples/slickgrid/example12.ts | 15 ++++ src/examples/slickgrid/example2.html | 2 +- src/examples/slickgrid/example2.ts | 11 ++- src/examples/slickgrid/example23.html | 12 +-- src/examples/slickgrid/example23.ts | 4 +- src/examples/slickgrid/example4.ts | 2 + test/cypress/integration/example12.spec.js | 32 ++++++- test/cypress/integration/example2.spec.js | 29 +++++++ test/cypress/integration/example4.spec.js | 14 +++ 30 files changed, 483 insertions(+), 23 deletions(-) create mode 100644 src/aurelia-slickgrid/models/customFooterOption.interface.ts create mode 100644 src/aurelia-slickgrid/styles/slick-footer.scss create mode 100644 src/aurelia-slickgrid/value-converters/asgDateFormat.spec.ts create mode 100644 src/aurelia-slickgrid/value-converters/asgDateFormat.ts create mode 100644 test/cypress/integration/example2.spec.js diff --git a/assets/i18n/en/aurelia-slickgrid.json b/assets/i18n/en/aurelia-slickgrid.json index e46ec3e35..9d281256e 100644 --- a/assets/i18n/en/aurelia-slickgrid.json +++ b/assets/i18n/en/aurelia-slickgrid.json @@ -24,6 +24,7 @@ "IN_COLLECTION_SEPERATED_BY_COMMA": "Search items in a collection, must be separated by a comma (a,b)", "ITEMS": "items", "ITEMS_PER_PAGE": "items per page", + "LAST_UPDATE": "Last Update", "NOT_IN_COLLECTION_SEPERATED_BY_COMMA": "Search items not in a collection, must be separated by a comma (a,b)", "OF": "of", "OK": "OK", diff --git a/assets/i18n/fr/aurelia-slickgrid.json b/assets/i18n/fr/aurelia-slickgrid.json index f5ac336e0..e2379555c 100644 --- a/assets/i18n/fr/aurelia-slickgrid.json +++ b/assets/i18n/fr/aurelia-slickgrid.json @@ -24,6 +24,7 @@ "INVALID_FLOAT": "Le nombre doit être valide et avoir un maximum de {{maxDecimal}} décimales.", "ITEMS": "éléments", "ITEMS_PER_PAGE": "éléments par page", + "LAST_UPDATE": "Dernière mise à jour", "NOT_IN_COLLECTION_SEPERATED_BY_COMMA": "Recherche excluant certain éléments d'une collection, doit être séparé par une virgule (a,b)", "OF": "de", "OK": "Terminé", diff --git a/src/assets/i18n/en/aurelia-slickgrid.json b/src/assets/i18n/en/aurelia-slickgrid.json index e46ec3e35..9d281256e 100644 --- a/src/assets/i18n/en/aurelia-slickgrid.json +++ b/src/assets/i18n/en/aurelia-slickgrid.json @@ -24,6 +24,7 @@ "IN_COLLECTION_SEPERATED_BY_COMMA": "Search items in a collection, must be separated by a comma (a,b)", "ITEMS": "items", "ITEMS_PER_PAGE": "items per page", + "LAST_UPDATE": "Last Update", "NOT_IN_COLLECTION_SEPERATED_BY_COMMA": "Search items not in a collection, must be separated by a comma (a,b)", "OF": "of", "OK": "OK", diff --git a/src/assets/i18n/fr/aurelia-slickgrid.json b/src/assets/i18n/fr/aurelia-slickgrid.json index f5ac336e0..e2379555c 100644 --- a/src/assets/i18n/fr/aurelia-slickgrid.json +++ b/src/assets/i18n/fr/aurelia-slickgrid.json @@ -24,6 +24,7 @@ "INVALID_FLOAT": "Le nombre doit être valide et avoir un maximum de {{maxDecimal}} décimales.", "ITEMS": "éléments", "ITEMS_PER_PAGE": "éléments par page", + "LAST_UPDATE": "Dernière mise à jour", "NOT_IN_COLLECTION_SEPERATED_BY_COMMA": "Recherche excluant certain éléments d'une collection, doit être séparé par une virgule (a,b)", "OF": "de", "OK": "Terminé", diff --git a/src/aurelia-slickgrid/constants.ts b/src/aurelia-slickgrid/constants.ts index f81e30e0a..131b3d884 100644 --- a/src/aurelia-slickgrid/constants.ts +++ b/src/aurelia-slickgrid/constants.ts @@ -25,6 +25,7 @@ export class Constants { TEXT_ITEMS_PER_PAGE: 'items per page', TEXT_OF: 'of', TEXT_OK: 'OK', + TEXT_LAST_UPDATE: 'Last Update', TEXT_PAGE: 'Page', TEXT_REFRESH_DATASET: 'Refresh Dataset', TEXT_REMOVE_FILTER: 'Remove Filter', diff --git a/src/aurelia-slickgrid/custom-elements/__tests__/aurelia-slickgrid-constructor.spec.ts b/src/aurelia-slickgrid/custom-elements/__tests__/aurelia-slickgrid-constructor.spec.ts index 931f4b40a..0acf0b1c5 100644 --- a/src/aurelia-slickgrid/custom-elements/__tests__/aurelia-slickgrid-constructor.spec.ts +++ b/src/aurelia-slickgrid/custom-elements/__tests__/aurelia-slickgrid-constructor.spec.ts @@ -1,8 +1,10 @@ import 'jest-extended'; +import { BindingEngine, Container } from 'aurelia-framework'; +import { BindingSignaler } from 'aurelia-templating-resources'; import { EventAggregator } from 'aurelia-event-aggregator'; import { HttpClient } from 'aurelia-fetch-client'; -import { BindingEngine, Container } from 'aurelia-framework'; import { DOM } from 'aurelia-pal'; +import { I18N } from 'aurelia-i18n'; import { AureliaSlickgridCustomElement } from '../aurelia-slickgrid'; import { ExtensionUtility } from '../../extensions'; @@ -21,7 +23,18 @@ import { SharedService, SortService, } from '../../services'; -import { Column, CurrentFilter, CurrentSorter, GraphqlServiceApi, GraphqlServiceOption, GridOption, GridState, GridStateType, Pagination } from '../../models'; +import { + Column, + CurrentFilter, + CurrentSorter, + GraphqlPaginatedResult, + GraphqlServiceApi, + GraphqlServiceOption, + GridOption, + GridState, + GridStateType, + Pagination +} from '../../models'; import { Filters } from '../../filters'; import { Editors } from '../../editors'; import * as utilities from '../../services/backend-utilities'; @@ -168,7 +181,7 @@ const mockDraggableGrouping = { constructor: jest.fn(), init: jest.fn(), destroy: jest.fn(), -} +}; const mockSlickCore = { handlers: [], @@ -273,6 +286,7 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () = let divContainer: HTMLDivElement; let cellDiv: HTMLDivElement; let ea: EventAggregator; + let i18n: I18N; const http = new HttpStub(); const template = ` @@ -295,6 +309,7 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () = document.body.appendChild(divContainer); ea = new EventAggregator(); + i18n = new I18N(ea, new BindingSignaler()); container = new Container(); customElement = new AureliaSlickgridCustomElement( bindingEngineStub, @@ -310,12 +325,23 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () = gridServiceStub, gridStateServiceStub, groupingAndColspanServiceStub, + i18n, paginationServiceStub, resizerServiceStub, sharedService, sortServiceStub ); + i18n.setup({ + resources: { + en: { translation: { ITEMS: 'items', OF: 'of', } }, + fr: { translation: { ITEMS: 'éléments', OF: 'de', } } + }, + lng: 'fr', + fallbackLng: 'en', + debug: false + }); + customElement.gridId = 'grid1'; customElement.columnDefinitions = [{ id: 'name', field: 'name' }]; customElement.dataset = []; @@ -731,7 +757,7 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () = customElement.bind(); customElement.attached(); - customElement.gridOptions.backendServiceApi.internalPostProcess({ data: { users: { nodes: [{ firstName: 'John' }], pageInfo: { hasNextPage: false }, totalCount: 2 } } }); + customElement.gridOptions.backendServiceApi.internalPostProcess({ data: { users: { nodes: [{ firstName: 'John' }], totalCount: 2 } } } as GraphqlPaginatedResult); expect(spy).toHaveBeenCalled(); expect(customElement.gridOptions.backendServiceApi.internalPostProcess).toEqual(expect.any(Function)); @@ -757,7 +783,7 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () = customElement.bind(); customElement.attached(); - customElement.gridOptions.backendServiceApi.internalPostProcess({ data: { notUsers: { nodes: [{ firstName: 'John' }], pageInfo: { hasNextPage: false }, totalCount: 2 } } }); + customElement.gridOptions.backendServiceApi.internalPostProcess({ data: { notUsers: { nodes: [{ firstName: 'John' }], totalCount: 2 } } } as GraphqlPaginatedResult); expect(spy).not.toHaveBeenCalled(); expect(customElement.dataset).toEqual([]); @@ -1219,5 +1245,54 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () = }); }); }); + + describe('Custom Footer', () => { + it('should have a Custom Footer when "showCustomFooter" is enabled and there are no Pagination used', (done) => { + const mockColDefs = [{ id: 'name', field: 'name', editor: undefined, internalColumnEditor: {} }]; + + customElement.gridOptions.enableTranslate = true; + customElement.gridOptions.showCustomFooter = true; + customElement.bind(); + customElement.attached(); + customElement.columnDefinitions = mockColDefs; + + setTimeout(() => { + expect(customElement.columnDefinitions).toEqual(mockColDefs); + expect(customElement.showCustomFooter).toBeTrue(); + expect(customElement.customFooterOptions).toEqual({ + dateFormat: 'YYYY-DD-MM h:mm:ss a', + hideLastUpdateTimestamp: true, + hideTotalItemCount: false, + footerHeight: 20, + leftContainerClass: 'col-xs-12 col-sm-5', + metricSeparator: '|', + metricTexts: { + items: 'éléments', + itemsKey: 'ITEMS', + of: 'de', + ofKey: 'OF', + }, + rightContainerClass: 'col-xs-6 col-sm-7', + }); + done(); + }, 1); + }); + + it('should NOT have a Custom Footer when "showCustomFooter" is enabled WITH Pagination in use', (done) => { + const mockColDefs = [{ id: 'name', field: 'name', editor: undefined, internalColumnEditor: {} }]; + + customElement.gridOptions.enablePagination = true; + customElement.gridOptions.showCustomFooter = true; + customElement.bind(); + customElement.attached(); + customElement.columnDefinitions = mockColDefs; + + setTimeout(() => { + expect(customElement.columnDefinitions).toEqual(mockColDefs); + expect(customElement.showCustomFooter).toBeFalse(); + done(); + }, 1); + }); + }); }); }); diff --git a/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.html b/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.html index 253673524..29b3c46a5 100644 --- a/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.html +++ b/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.html @@ -1,13 +1,40 @@ diff --git a/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.ts b/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.ts index 63f75a416..cd4dd52a7 100644 --- a/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.ts +++ b/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.ts @@ -8,8 +8,9 @@ import 'slickgrid/slick.dataview'; import 'slickgrid/slick.grid'; import { bindable, BindingEngine, bindingMode, Container, Factory, inject } from 'aurelia-framework'; -import { DOM } from 'aurelia-pal'; import { EventAggregator, Subscription } from 'aurelia-event-aggregator'; +import { DOM } from 'aurelia-pal'; +import { I18N } from 'aurelia-i18n'; import { Constants } from '../constants'; import { GlobalGridOptions } from '../global-grid-options'; @@ -18,6 +19,7 @@ import { BackendServiceApi, BackendServiceOption, Column, + CustomFooterOption, ExtensionName, GraphqlResult, GraphqlPaginatedResult, @@ -25,6 +27,7 @@ import { GridStateChange, GridStateType, Locale, + Metrics, Pagination, SlickEventHandler, } from '../models/index'; @@ -69,6 +72,7 @@ const DEFAULT_SLICKGRID_EVENT_PREFIX = 'sg'; GridService, GridStateService, GroupingAndColspanService, + I18N, PaginationService, ResizerService, SharedService, @@ -83,8 +87,11 @@ export class AureliaSlickgridCustomElement { private _hideHeaderRowAfterPageLoad = false; groupItemMetadataProvider: any; backendServiceApi: BackendServiceApi | undefined; + customFooterOptions: CustomFooterOption; locales: Locale; + metrics: Metrics; isGridInitialized = false; + showCustomFooter = false; showPagination = false; serviceList: any[] = []; subscriptions: Subscription[] = []; @@ -117,6 +124,7 @@ export class AureliaSlickgridCustomElement { private gridService: GridService, private gridStateService: GridStateService, private groupingAndColspanService: GroupingAndColspanService, + private i18n: I18N, private paginationService: PaginationService, private resizerService: ResizerService, private sharedService: SharedService, @@ -290,6 +298,10 @@ export class AureliaSlickgridCustomElement { resizerService: this.resizerService, sortService: this.sortService, }; + + // user could show a custom footer with the data metrics (dataset length and last updated timestamp) + this.optionallyShowCustomFooterWithMetrics(); + this.dispatchCustomEvent(`${DEFAULT_AURELIA_EVENT_PREFIX}-on-aurelia-grid-created`, aureliaElementInstance); } @@ -430,6 +442,7 @@ export class AureliaSlickgridCustomElement { this.extensionService.translateContextMenu(); this.extensionService.translateGridMenu(); this.extensionService.translateHeaderMenu(); + this.translateCustomFooterTexts(); } }) ); @@ -519,7 +532,16 @@ export class AureliaSlickgridCustomElement { 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 @@ -790,6 +812,32 @@ export class AureliaSlickgridCustomElement { } } + /** + * We could optionally display a custom footer below the grid to show some metrics (last update, item count with/without filters) + * It's an opt-in, user has to enable "showCustomFooter" and it cannot be used when there's already a Pagination since they display the same kind of info + */ + private optionallyShowCustomFooterWithMetrics() { + if (this.gridOptions) { + setTimeout(() => { + // we will display the custom footer only when there's no Pagination + if (!(this.gridOptions.backendServiceApi || this.gridOptions.enablePagination)) { + this.showCustomFooter = this.gridOptions.hasOwnProperty('showCustomFooter') ? this.gridOptions.showCustomFooter : false; + this.customFooterOptions = this.gridOptions.customFooterOptions || {}; + } + }); + + if ((this.gridOptions.enableTranslate || this.gridOptions.i18n)) { + this.translateCustomFooterTexts(); + } else if (this.gridOptions.customFooterOptions) { + const customFooterOptions = this.gridOptions.customFooterOptions; + customFooterOptions.metricTexts = customFooterOptions.metricTexts || {}; + customFooterOptions.metricTexts.lastUpdate = this.locales && this.locales.TEXT_LAST_UPDATE || 'TEXT_LAST_UPDATE'; + customFooterOptions.metricTexts.items = this.locales && this.locales.TEXT_ITEMS || 'TEXT_ITEMS'; + customFooterOptions.metricTexts.of = this.locales && this.locales.TEXT_OF || 'TEXT_OF'; + } + } + } + /** * For convenience to the user, we provide the property "editor" as an Aurelia-Slickgrid editor complex object * however "editor" is used internally by SlickGrid for it's own Editor Factory @@ -811,6 +859,20 @@ export class AureliaSlickgridCustomElement { }); } + /** Translate all Custom Footer Texts (footer with metrics) */ + private translateCustomFooterTexts() { + if (this.i18n && this.i18n.tr) { + const customFooterOptions = this.gridOptions && this.gridOptions.customFooterOptions || {}; + customFooterOptions.metricTexts = customFooterOptions.metricTexts || {}; + for (const propName of Object.keys(customFooterOptions.metricTexts)) { + if (propName.lastIndexOf('Key') > 0) { + const propNameWithoutKey = propName.substring(0, propName.lastIndexOf('Key')); + customFooterOptions.metricTexts[propNameWithoutKey] = this.i18n.tr(customFooterOptions.metricTexts[propName] || ' '); + } + } + } + } + /** * Update the "internalColumnEditor.collection" property. * 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/aurelia-slickgrid/global-grid-options.ts b/src/aurelia-slickgrid/global-grid-options.ts index 433f53cfc..5a1dfb8b2 100644 --- a/src/aurelia-slickgrid/global-grid-options.ts +++ b/src/aurelia-slickgrid/global-grid-options.ts @@ -52,6 +52,21 @@ export const GlobalGridOptions: GridOption = { iconExportTextDelimitedCommand: 'fa fa-download', width: 200, }, + customFooterOptions: { + dateFormat: 'YYYY-DD-MM h:mm:ss a', + hideTotalItemCount: false, + hideLastUpdateTimestamp: true, + footerHeight: 20, + leftContainerClass: 'col-xs-12 col-sm-5', + rightContainerClass: 'col-xs-6 col-sm-7', + metricSeparator: '|', + metricTexts: { + items: 'items', + of: 'of', + itemsKey: 'ITEMS', + ofKey: 'OF', + } + }, datasetIdPropertyName: 'id', defaultAureliaEventPrefix: 'asg', defaultSlickgridEventPrefix: 'sg', diff --git a/src/aurelia-slickgrid/index.ts b/src/aurelia-slickgrid/index.ts index b6b2688a9..72b3d572e 100644 --- a/src/aurelia-slickgrid/index.ts +++ b/src/aurelia-slickgrid/index.ts @@ -20,6 +20,7 @@ export * from './sorters/index'; export function configure(aurelia: FrameworkConfiguration, callback: (instance: SlickgridConfig) => void) { aurelia.globalResources(PLATFORM.moduleName('./custom-elements/aurelia-slickgrid')); aurelia.globalResources(PLATFORM.moduleName('./custom-elements/slick-pagination')); + aurelia.globalResources(PLATFORM.moduleName('./value-converters/asgDateFormat')); aurelia.globalResources(PLATFORM.moduleName('./value-converters/asgNumber')); // must register a transient so the container will get a new instance everytime diff --git a/src/aurelia-slickgrid/models/customFooterOption.interface.ts b/src/aurelia-slickgrid/models/customFooterOption.interface.ts new file mode 100644 index 000000000..94f77af6a --- /dev/null +++ b/src/aurelia-slickgrid/models/customFooterOption.interface.ts @@ -0,0 +1,51 @@ +export interface CustomFooterOption { + /** Optionally pass some text to be displayed on the left side (in the "left-footer" css class) */ + leftFooterText?: string; + + /** CSS class used for the left container */ + leftContainerClass?: string; + + /** Date format used when showing the "Last Update" timestamp in the metrics section. */ + dateFormat?: string; + + /** Defaults to 20, height of the Custom Footer in pixels (this is required and is used by the auto-resizer) */ + footerHeight?: number; + + /** Defaults to false, do we want to hide the last update timestamp (endTime)? */ + hideLastUpdateTimestamp?: 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; + + /** Text shown in the custom footer on the far right for the metrics */ + metricTexts?: { + /** Defaults to empty string, optionally pass a text (Last Update) to display before the metrics endTime timestamp. */ + lastUpdate?: string; + + /** Defaults to "items", word to display at the end of the metrics to represent the items (e.g. you could change it for "users" or anything else). */ + items?: string; + + /** Defaults to "of", text word separator to display between the filtered items count and the total unfiltered items count (e.g.: "10 of 100 items"). */ + of?: string; + + // -- Translation Keys --// + + /** Defaults to "ITEMS", translation key used for the word displayed at the end of the metrics to represent the items (e.g. you could change it for "users" or anything else). */ + itemsKey?: string; + + /** Defaults to empty string, optionally pass a translation key (internally we use "LAST_UPDATE") to display before the metrics endTime timestamp. */ + lastUpdateKey?: string; + + /** Defaults to "OF", translation key used for the to display between the filtered items count and the total unfiltered items count. */ + ofKey?: string; + }; + + /** CSS class used for the right container */ + rightContainerClass?: string; +} diff --git a/src/aurelia-slickgrid/models/gridOption.interface.ts b/src/aurelia-slickgrid/models/gridOption.interface.ts index 1dccfc667..cd0cfd4ba 100644 --- a/src/aurelia-slickgrid/models/gridOption.interface.ts +++ b/src/aurelia-slickgrid/models/gridOption.interface.ts @@ -6,6 +6,7 @@ import { Column, ColumnPicker, CheckboxSelector, + CustomFooterOption, DraggableGrouping, EditCommand, ExcelCopyBufferOption, @@ -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; @@ -378,12 +382,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/aurelia-slickgrid/models/index.ts b/src/aurelia-slickgrid/models/index.ts index 3c1c99c86..f1c384021 100644 --- a/src/aurelia-slickgrid/models/index.ts +++ b/src/aurelia-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/aurelia-slickgrid/models/locale.interface.ts b/src/aurelia-slickgrid/models/locale.interface.ts index 47c7124d3..211ad85a5 100644 --- a/src/aurelia-slickgrid/models/locale.interface.ts +++ b/src/aurelia-slickgrid/models/locale.interface.ts @@ -65,6 +65,9 @@ export interface Locale { /** Text "items per page" displayed in the Pagination (when enabled) */ TEXT_ITEMS_PER_PAGE?: string; + /** Text "Last Update" displayed in the Footer (when enabled) */ + TEXT_LAST_UPDATE?: string; + /** Text "of" displayed in the Pagination (when enabled) */ TEXT_OF?: string; diff --git a/src/aurelia-slickgrid/services/__tests__/resizer.service.spec.ts b/src/aurelia-slickgrid/services/__tests__/resizer.service.spec.ts index d6d418ccb..d6f9cb7b8 100644 --- a/src/aurelia-slickgrid/services/__tests__/resizer.service.spec.ts +++ b/src/aurelia-slickgrid/services/__tests__/resizer.service.spec.ts @@ -6,6 +6,7 @@ import { ResizerService } from '../resizer.service'; 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; const aureliaEventPrefix = 'asg'; const gridId = 'grid1'; @@ -87,6 +88,10 @@ describe('Resizer Service', () => { gridOptionMock.gridId = 'grid1'; }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should return null when calling "bindAutoResizeDataGrid" method with a gridId that is not found in the DOM', () => { gridOptionMock.gridId = 'unknown'; const output = service.bindAutoResizeDataGrid(); @@ -161,6 +166,39 @@ describe('Resizer Service', () => { expect(serviceCalculateSpy).toReturnWith({ height: (newHeight - DATAGRID_BOTTOM_PADDING), width: fixedWidth }); }); + it('should calculate new dimensions, minus the custom footer height, when calculateGridNewDimensions is called', () => { + const newHeight = 440; + const fixedWidth = 800; + const newOptions = { ...gridOptionMock, enablePagination: false, showCustomFooter: true } as GridOption; + const newGridStub = { ...gridStub, getOptions: () => newOptions }; + service.init(newGridStub, { width: fixedWidth }); + const serviceCalculateSpy = jest.spyOn(service, 'calculateGridNewDimensions'); + + Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: newHeight }); + window.dispatchEvent(DOM.createCustomEvent('resize', { bubbles: true })); + service.calculateGridNewDimensions(newOptions); + + // same comment as previous test, the height dimension will work because calculateGridNewDimensions() uses "window.innerHeight" + expect(serviceCalculateSpy).toReturnWith({ height: (newHeight - DATAGRID_BOTTOM_PADDING - DATAGRID_FOOTER_HEIGHT), width: fixedWidth }); + }); + + it('should calculate new dimensions, minus the custom footer height passed in grid options, when calculateGridNewDimensions is called', () => { + const newHeight = 440; + const fixedWidth = 800; + const footerHeight = 25; + const newOptions = { ...gridOptionMock, enablePagination: false, showCustomFooter: true, customFooterOptions: { footerHeight } } as GridOption; + const newGridStub = { ...gridStub, getOptions: () => newOptions }; + service.init(newGridStub, { width: fixedWidth }); + const serviceCalculateSpy = jest.spyOn(service, 'calculateGridNewDimensions'); + + Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: newHeight }); + window.dispatchEvent(DOM.createCustomEvent('resize', { bubbles: true })); + service.calculateGridNewDimensions(newOptions); + + // same comment as previous test, the height dimension will work because calculateGridNewDimensions() uses "window.innerHeight" + expect(serviceCalculateSpy).toReturnWith({ height: (newHeight - DATAGRID_BOTTOM_PADDING - footerHeight), width: fixedWidth }); + }); + it('should use maxHeight when new dimensions are higher than maximum defined', () => { const newHeight = 1000; const fixedWidth = 800; diff --git a/src/aurelia-slickgrid/services/resizer.service.ts b/src/aurelia-slickgrid/services/resizer.service.ts index b03710199..dc45dbab4 100644 --- a/src/aurelia-slickgrid/services/resizer.service.ts +++ b/src/aurelia-slickgrid/services/resizer.service.ts @@ -8,6 +8,7 @@ import * as $ from 'jquery'; 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 { @@ -92,6 +93,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 += gridOptions.customFooterOptions && gridOptions.customFooterOptions.footerHeight || DATAGRID_FOOTER_HEIGHT; + } + let gridHeight = 0; let gridOffsetTop = 0; diff --git a/src/aurelia-slickgrid/styles/_variables.scss b/src/aurelia-slickgrid/styles/_variables.scss index 805796d9c..42e3c223c 100644 --- a/src/aurelia-slickgrid/styles/_variables.scss +++ b/src/aurelia-slickgrid/styles/_variables.scss @@ -498,3 +498,22 @@ $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-height: 30px !default; // if you modify this height, you also have to modify the footerHeight in the customFooterOptions +$footer-padding: 5px !default; +$footer-text-color: #808080 !default; +$footer-left-font-style: italic !default; +$footer-left-font-weight: normal !default; +$footer-left-padding: 0px !default; +$footer-left-text-align: left !default; +$footer-left-text-color: $footer-text-color !default; +$footer-right-font-style: italic !default; +$footer-right-font-weight: normal !default; +$footer-right-padding: 0px !default; +$footer-right-separator-margin: 2px !default; +$footer-right-text-align: right !default; +$footer-right-text-color: $footer-text-color !default; diff --git a/src/aurelia-slickgrid/styles/slick-footer.scss b/src/aurelia-slickgrid/styles/slick-footer.scss new file mode 100644 index 000000000..6f174063a --- /dev/null +++ b/src/aurelia-slickgrid/styles/slick-footer.scss @@ -0,0 +1,30 @@ +@import './variables'; + +.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; + height: $footer-height; + + .left-footer { + color: $footer-left-text-color; + font-style: $footer-left-font-style; + font-weight: $footer-left-font-weight; + text-align: $footer-left-text-align; + padding: $footer-left-padding; + } + + .right-footer.metrics { + color: $footer-right-text-color; + text-align: $footer-right-text-align; + font-style: $footer-right-font-style; + font-weight: $footer-right-font-weight; + text-align: $footer-right-text-align; + padding: $footer-right-padding; + .separator { + margin: $footer-right-separator-margin; + } + } +} diff --git a/src/aurelia-slickgrid/styles/slickgrid-theme-bootstrap.scss b/src/aurelia-slickgrid/styles/slickgrid-theme-bootstrap.scss index d8e05108a..645ed1df8 100644 --- a/src/aurelia-slickgrid/styles/slickgrid-theme-bootstrap.scss +++ b/src/aurelia-slickgrid/styles/slickgrid-theme-bootstrap.scss @@ -2,6 +2,7 @@ @import './slick-controls'; @import './slick-editors'; @import './slick-pagination'; +@import './slick-footer'; @import './slick-plugins'; @import './slick-default-theme'; @import './slickgrid-examples'; diff --git a/src/aurelia-slickgrid/value-converters/asgDateFormat.spec.ts b/src/aurelia-slickgrid/value-converters/asgDateFormat.spec.ts new file mode 100644 index 000000000..add3557ca --- /dev/null +++ b/src/aurelia-slickgrid/value-converters/asgDateFormat.spec.ts @@ -0,0 +1,9 @@ +import { AsgDateFormatValueConverter } from './asgDateFormat'; + +describe('AsgDateFormatConverter method', () => { + it('should return a formatted date', () => { + const converter = new AsgDateFormatValueConverter(); + const output = converter.toView(new Date('2019-05-01T02:36:07'), 'YYYY-MM-DD'); + expect(output).toBe('2019-05-01'); + }); +}); diff --git a/src/aurelia-slickgrid/value-converters/asgDateFormat.ts b/src/aurelia-slickgrid/value-converters/asgDateFormat.ts new file mode 100644 index 000000000..16d0b8820 --- /dev/null +++ b/src/aurelia-slickgrid/value-converters/asgDateFormat.ts @@ -0,0 +1,7 @@ +import * as moment from 'moment-mini'; + +export class AsgDateFormatValueConverter { + toView(value: any, format: string): string { + return moment(value).format(format); + } +} diff --git a/src/examples/slickgrid/example12.ts b/src/examples/slickgrid/example12.ts index e2d72fe9e..e8d7ae700 100644 --- a/src/examples/slickgrid/example12.ts +++ b/src/examples/slickgrid/example12.ts @@ -142,6 +142,21 @@ export class Example12 { enableFiltering: true, enableTranslate: true, i18n: this.i18n, + showCustomFooter: true, // display some metrics in the bottom custom footer + customFooterOptions: { + metricTexts: { + // default text displayed in the metrics section on the right + // all texts optionally support translation keys, + // if you wish to use that feature then use the text properties with the 'Key' suffix (e.g: itemsKey, ofKey, lastUpdateKey) + // example "items" for a plain string OR "itemsKey" to use a translation key + itemsKey: 'ITEMS', + ofKey: 'OF', + lastUpdateKey: 'LAST_UPDATE', + }, + dateFormat: 'YYYY-MM-DD hh:mm a', + hideTotalItemCount: false, + hideLastUpdateTimestamp: false, + }, exportOptions: { // set at the grid option level, meaning all column will evaluate the Formatter (when it has a Formatter defined) exportWithFormatter: true, diff --git a/src/examples/slickgrid/example2.html b/src/examples/slickgrid/example2.html index 6164d6180..ddd798760 100644 --- a/src/examples/slickgrid/example2.html +++ b/src/examples/slickgrid/example2.html @@ -8,7 +8,7 @@

${title}

Pause auto-resize: ${resizerPaused} - Wiki docs).