Skip to content

Commit

Permalink
Merge pull request #369 from ghiscoding/feat/custom-footer
Browse files Browse the repository at this point in the history
feat(footer): add custom footer to show metrics
  • Loading branch information
ghiscoding authored Jan 15, 2020
2 parents 33f7fa0 + 51b4e14 commit 81cd0c5
Show file tree
Hide file tree
Showing 28 changed files with 415 additions and 6 deletions.
4 changes: 3 additions & 1 deletion src/app/examples/grid-clientside.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export class GridClientSideComponent implements OnInit {
enableFiltering: true,
// enableFilterTrimWhiteSpace: true,
i18n: this.translate,
showCustomFooter: true, // display some metrics in the bottom custom footer

// use columnDef searchTerms OR use presets as shown below
presets: {
Expand All @@ -193,7 +194,7 @@ export class GridClientSideComponent implements OnInit {
{ columnId: 'duration', direction: 'DESC' },
{ columnId: 'complete', direction: 'ASC' }
],
}
},
};

// mock a dataset
Expand Down Expand Up @@ -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
};
Expand Down
7 changes: 7 additions & 0 deletions src/app/examples/grid-formatter.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ export class GridFormatterComponent implements OnInit {
},
enableAutoResize: true,
enableCellNavigation: true,
showCustomFooter: true, // display some metrics in the bottom custom footer
customFooterOptions: {
// optionally display some text on the left footer container
leftFooterText: 'custom footer text',
hideTotalItemCount: true,
hideLastUpdateTimestamp: true
},

// you customize all formatter at once certain options through "formatterOptions" in the Grid Options
// or independently through the column definition "params", the option names are the same
Expand Down
15 changes: 15 additions & 0 deletions src/app/examples/grid-localization.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,21 @@ export class GridLocalizationComponent implements OnInit {
enableFiltering: true,
enableTranslate: true,
i18n: this.translate,
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 aaaaa\'m\'',
hideTotalItemCount: false,
hideLastUpdateTimestamp: false,
},
excelExportOptions: {
// optionally pass a custom header to the Excel Sheet
// a lot of the info can be found on Web Archive of Excel-Builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -611,7 +612,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));
Expand All @@ -635,7 +636,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([]);
Expand Down Expand Up @@ -1075,5 +1076,54 @@ describe('Angular-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: {} }];

component.gridOptions.enableTranslate = true;
component.gridOptions.showCustomFooter = true;
component.ngOnInit();
component.ngAfterViewInit();
component.columnDefinitions = mockColDefs;

setTimeout(() => {
expect(component.columnDefinitions).toEqual(mockColDefs);
expect(component.showCustomFooter).toBeTrue();
expect(component.customFooterOptions).toEqual({
dateFormat: 'yyyy-MM-dd HH:mm aaaaa\'m\'',
hideLastUpdateTimestamp: true,
hideTotalItemCount: false,
footerHeight: 20,
leftContainerClass: 'col-xs-12 col-sm-5',
metricSeparator: '|',
metricTexts: {
items: 'ITEMS',
itemsKey: 'ITEMS',
of: 'OF',
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: {} }];

component.gridOptions.enablePagination = true;
component.gridOptions.showCustomFooter = true;
component.ngOnInit();
component.ngAfterViewInit();
component.columnDefinitions = mockColDefs;

setTimeout(() => {
expect(component.columnDefinitions).toEqual(mockColDefs);
expect(component.showCustomFooter).toBeFalse();
done();
}, 1);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,34 @@
<div attr.id='{{gridId}}' class="slickgrid-container" style="width: 100%" [style.height]="gridHeightString">
</div>

<!-- Pagination section under the grid -->
<slick-pagination id="slickPagingContainer-{{gridId}}" *ngIf="showPagination"
(onPaginationChanged)="paginationChanged($event)" [enableTranslate]="gridOptions.enableTranslate"
[dataView]="dataView" [grid]="grid" [options]="paginationOptions" [locales]="locales" [totalItems]="totalItems"
[backendServiceApi]="backendServiceApi">
</slick-pagination>

<!-- Custom Footer section under the grid -->
<div *ngIf="showCustomFooter && customFooterOptions" class="slick-custom-footer" style="width: 100%;"
[style.height]="customFooterOptions?.footerHeight || 20">
<span class="left-footer" [ngClass]="customFooterOptions.leftContainerClass">
{{customFooterOptions.leftFooterText}}
</span>

<span class="right-footer metrics" [ngClass]="customFooterOptions.rightContainerClass"
*ngIf="metrics && !customFooterOptions.hideMetrics">
<span *ngIf="!customFooterOptions.hideLastUpdateTimestamp">
<span>{{customFooterOptions.metricTexts?.lastUpdate}}</span>

{{metrics.endTime | date: customFooterOptions.dateFormat}}
<span class="separator">{{customFooterOptions.metricSeparator}}</span>
</span>

{{metrics.itemCount}}
<span *ngIf="!customFooterOptions.hideTotalItemCount">{{customFooterOptions.metricTexts?.of}}
{{metrics.totalItemCount}}
</span>
{{customFooterOptions.metricTexts?.items}}
</span>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,21 @@ import {
BackendServiceApi,
BackendServiceOption,
Column,
CustomFooterOption,
ExtensionName,
GraphqlPaginatedResult,
GraphqlResult,
GridOption,
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';
Expand Down Expand Up @@ -124,8 +126,11 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
groupingDefinition: any = {};
groupItemMetadataProvider: any;
backendServiceApi: BackendServiceApi;
customFooterOptions: CustomFooterOption;
locales: Locale;
metrics: Metrics;
paginationOptions: Pagination;
showCustomFooter = false;
showPagination = false;
totalItems = 0;
isGridInitialized = false;
Expand Down Expand Up @@ -393,6 +398,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
this.extensionService.translateContextMenu();
this.extensionService.translateGridMenu();
this.extensionService.translateHeaderMenu();
this.translateCustomFooterTexts();
}
})
);
Expand Down Expand Up @@ -481,7 +487,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
Expand Down Expand Up @@ -736,6 +751,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.optionallyShowCustomFooterWithMetrics();
}

/** Load the Editor Collection asynchronously and replace the "collection" property when Observable resolves */
Expand Down Expand Up @@ -776,6 +794,32 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
return options;
}

/**
* 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 Angular-Slickgrid editor complex object
* however "editor" is used internally by SlickGrid for it's own Editor Factory
Expand All @@ -792,6 +836,20 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
});
}

/** Translate all Custom Footer Texts (footer with metrics) */
private translateCustomFooterTexts() {
if (this.translate && this.translate.instant) {
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.translate.instant(customFooterOptions.metricTexts[propName] || ' ');
}
}
}
}

/**
* 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.
Expand Down
1 change: 1 addition & 0 deletions src/app/modules/angular-slickgrid/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
15 changes: 15 additions & 0 deletions src/app/modules/angular-slickgrid/global-grid-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ export const GlobalGridOptions: GridOption = {
iconExportTextDelimitedCommand: 'fa fa-download',
width: 200,
},
customFooterOptions: {
dateFormat: 'yyyy-MM-dd HH:mm aaaaa\'m\'',
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',
defaultFilter: Filters.input,
enableFilterTrimWhiteSpace: false, // do we want to trim white spaces on all Filters?
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 81cd0c5

Please sign in to comment.