diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/aurelia-slickgrid.ts b/aurelia-slickgrid/src/aurelia-slickgrid/aurelia-slickgrid.ts index 7d0059efb..02cb4bfaf 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/aurelia-slickgrid.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/aurelia-slickgrid.ts @@ -39,6 +39,7 @@ import { GraphqlService, GridEventService, GridExtraService, + GridStateService, ResizerService, SortService, toKebabCase @@ -51,7 +52,7 @@ declare var Slick: any; const eventPrefix = 'sg'; // Aurelia doesn't support well TypeScript @autoinject in a Plugin so we'll do it the old fashion way -@inject(ControlAndPluginService, ExportService, Element, EventAggregator, FilterService, GraphqlService, GridEventService, GridExtraService, I18N, ResizerService, SortService) +@inject(ControlAndPluginService, ExportService, Element, EventAggregator, FilterService, GraphqlService, GridEventService, GridExtraService, GridStateService, I18N, ResizerService, SortService) export class AureliaSlickgridCustomElement { private _dataset: any[]; private _gridOptions: GridOption; @@ -84,6 +85,7 @@ export class AureliaSlickgridCustomElement { private graphqlService: GraphqlService, private gridEventService: GridEventService, private gridExtraService: GridExtraService, + private gridStateService: GridStateService, private i18n: I18N, private resizer: ResizerService, private sortService: SortService) { } @@ -138,6 +140,14 @@ export class AureliaSlickgridCustomElement { if (this._gridOptions.enableExport) { this.exportService.init(this.grid, this._gridOptions, this.dataview); } + + // attach the Backend Service API callback functions only after the grid is initialized + // because the preProcess() and onInit() might get triggered + if (this._gridOptions && (this._gridOptions.backendServiceApi || this._gridOptions.onBackendEventApi)) { + this.attachBackendCallbackFunctions(this._gridOptions); + } + + this.gridStateService.init(this.grid, this.filterService, this.sortService); } detached() { @@ -223,12 +233,17 @@ export class AureliaSlickgridCustomElement { // attach external sorting (backend) when available or default onSort (dataView) if (gridOptions.enableSorting) { - (gridOptions.backendServiceApi || gridOptions.onBackendEventApi) ? this.sortService.attachBackendOnSort(grid, gridOptions) : this.sortService.attachLocalOnSort(grid, gridOptions, this.dataview); + (gridOptions.backendServiceApi || gridOptions.onBackendEventApi) ? this.sortService.attachBackendOnSort(grid, gridOptions) : this.sortService.attachLocalOnSort(grid, gridOptions, this.dataview, this.columnDefinitions); } // attach external filter (backend) when available or default onFilter (dataView) if (gridOptions.enableFiltering) { this.filterService.init(grid, gridOptions, this.columnDefinitions); + + // if user entered some "presets", we need to reflect them all in the DOM + if (gridOptions.presets && gridOptions.presets.filters) { + this.filterService.populateColumnFilterSearchTerms(gridOptions, this.columnDefinitions); + } (gridOptions.backendServiceApi || gridOptions.onBackendEventApi) ? this.filterService.attachBackendOnFilter(grid, gridOptions) : this.filterService.attachLocalOnFilter(grid, gridOptions, this.dataview); } @@ -239,37 +254,7 @@ export class AureliaSlickgridCustomElement { } if (gridOptions.backendServiceApi && gridOptions.backendServiceApi.service) { - gridOptions.backendServiceApi.service.initOptions(gridOptions.backendServiceApi.options || {}, gridOptions.pagination); - } - - const backendApi = gridOptions.backendServiceApi || gridOptions.onBackendEventApi; - const serviceOptions: BackendServiceOption = (backendApi && backendApi.service && backendApi.service.options) ? backendApi.service.options : {}; - const isExecuteCommandOnInit = (!serviceOptions) ? false : ((serviceOptions && serviceOptions.hasOwnProperty('executeProcessCommandOnInit')) ? serviceOptions['executeProcessCommandOnInit'] : true); - - if (backendApi && backendApi.service && (backendApi.onInit || isExecuteCommandOnInit)) { - const query = (typeof backendApi.service.buildQuery === 'function') ? backendApi.service.buildQuery() : ''; - const onInitPromise = (isExecuteCommandOnInit) ? (backendApi && backendApi.process) ? backendApi.process(query) : undefined : (backendApi && backendApi.onInit) ? backendApi.onInit(query) : null; - - // wrap this inside a setTimeout to avoid timing issue since the gridOptions needs to be ready before running this onInit - setTimeout(async () => { - if (backendApi.preProcess) { - backendApi.preProcess(); - } - - // await for the Promise to resolve the data - const processResult = await onInitPromise; - - // define what our internal Post Process callback, only available for GraphQL Service for now - // it will basically refresh the Dataset & Pagination without having the user to create his own PostProcess every time - if (processResult && backendApi && backendApi.service instanceof GraphqlService && backendApi.internalPostProcess) { - backendApi.internalPostProcess(processResult); - } - - // send the response process to the postProcess callback - if (backendApi.postProcess) { - backendApi.postProcess(processResult); - } - }); + gridOptions.backendServiceApi.service.init(gridOptions.backendServiceApi.options || {}, gridOptions.pagination, this.grid); } } @@ -315,6 +300,56 @@ export class AureliaSlickgridCustomElement { }); } + attachBackendCallbackFunctions(gridOptions: GridOption) { + const backendApi = gridOptions.backendServiceApi || gridOptions.onBackendEventApi; + const serviceOptions: BackendServiceOption = (backendApi && backendApi.service && backendApi.service.options) ? backendApi.service.options : {}; + const isExecuteCommandOnInit = (!serviceOptions) ? false : ((serviceOptions && serviceOptions.hasOwnProperty('executeProcessCommandOnInit')) ? serviceOptions['executeProcessCommandOnInit'] : true); + + // update backend filters (if need be) before the query runs + if (gridOptions && gridOptions.presets) { + if (gridOptions.presets.filters) { + backendApi.service.updateFilters(gridOptions.presets.filters, true); + } + if (gridOptions.presets.sorters) { + backendApi.service.updateSorters(null, gridOptions.presets.sorters); + } + if (gridOptions.presets.pagination) { + backendApi.service.updatePagination(gridOptions.presets.pagination.pageNumber, gridOptions.presets.pagination.pageSize); + } + } else { + const columnFilters = this.filterService.getColumnFilters(); + if (columnFilters) { + backendApi.service.updateFilters(columnFilters, false); + } + } + + if (backendApi && backendApi.service && (backendApi.onInit || isExecuteCommandOnInit)) { + const query = (typeof backendApi.service.buildQuery === 'function') ? backendApi.service.buildQuery() : ''; + const onInitPromise = (isExecuteCommandOnInit) ? (backendApi && backendApi.process) ? backendApi.process(query) : undefined : (backendApi && backendApi.onInit) ? backendApi.onInit(query) : null; + + // wrap this inside a setTimeout to avoid timing issue since the gridOptions needs to be ready before running this onInit + setTimeout(async () => { + if (backendApi.preProcess) { + backendApi.preProcess(); + } + + // await for the Promise to resolve the data + const processResult = await onInitPromise; + + // define what our internal Post Process callback, only available for GraphQL Service for now + // it will basically refresh the Dataset & Pagination without having the user to create his own PostProcess every time + if (processResult && backendApi && backendApi.service instanceof GraphqlService && backendApi.internalPostProcess) { + backendApi.internalPostProcess(processResult); + } + + // send the response process to the postProcess callback + if (backendApi.postProcess) { + backendApi.postProcess(processResult); + } + }); + } + } + attachResizeHook(grid: any, options: GridOption) { // expand/autofit columns on first page load if (grid && options.autoFitColumnsOnFirstLoad && typeof grid.autosizeColumns === 'function') { @@ -348,7 +383,7 @@ export class AureliaSlickgridCustomElement { * @param dataset */ refreshGridData(dataset: any[], totalCount?: number) { - if (dataset && this.grid) { + if (dataset && this.grid && this.dataview && typeof this.dataview.setItems === 'function') { this.dataview.setItems(dataset, this._gridOptions.datasetIdPropertyName); // this.grid.setData(dataset); @@ -368,6 +403,10 @@ export class AureliaSlickgridCustomElement { if (this.gridOptions.pagination && totalCount) { this.gridOptions.pagination.totalItems = totalCount; } + if (this.gridOptions.presets && this.gridOptions.presets.pagination && this.gridOptions.pagination) { + this.gridOptions.pagination.pageSize = this.gridOptions.presets.pagination.pageSize; + this.gridOptions.pagination.pageNumber = this.gridOptions.presets.pagination.pageNumber; + } this.gridPaginationOptions = this.mergeGridOptions(); } if (this.grid && this._gridOptions.enableAutoResize) { diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/filters/inputFilter.ts b/aurelia-slickgrid/src/aurelia-slickgrid/filters/inputFilter.ts index b59786c3b..6507b9b72 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/filters/inputFilter.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/filters/inputFilter.ts @@ -63,6 +63,15 @@ export class InputFilter implements Filter { } } + /** + * Set value(s) on the DOM element + */ + setValues(values) { + if (values) { + this.$filterElm.val(values); + } + } + // // private functions // ------------------ diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/filters/multipleSelectFilter.ts b/aurelia-slickgrid/src/aurelia-slickgrid/filters/multipleSelectFilter.ts index 556f74878..4ea907a30 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/filters/multipleSelectFilter.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/filters/multipleSelectFilter.ts @@ -101,6 +101,15 @@ export class MultipleSelectFilter implements Filter { } } + /** + * Set value(s) on the DOM element + */ + setValues(values) { + if (values) { + this.$filterElm.multipleSelect('setSelects', values); + } + } + // // private functions // ------------------ diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/filters/selectFilter.ts b/aurelia-slickgrid/src/aurelia-slickgrid/filters/selectFilter.ts index ae24ed63a..6ac605546 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/filters/selectFilter.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/filters/selectFilter.ts @@ -68,6 +68,15 @@ export class SelectFilter implements Filter { } } + /** + * Set value(s) on the DOM element + */ + setValues(values) { + if (values) { + this.$filterElm.val(values); + } + } + // // private functions // ------------------ diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/filters/singleSelectFilter.ts b/aurelia-slickgrid/src/aurelia-slickgrid/filters/singleSelectFilter.ts index 2a09ad775..94245c18e 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/filters/singleSelectFilter.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/filters/singleSelectFilter.ts @@ -87,6 +87,16 @@ export class SingleSelectFilter implements Filter { } } + /** + * Set value(s) on the DOM element + */ + setValues(values) { + if (values) { + values = Array.isArray(values) ? values : [values]; + this.$filterElm.multipleSelect('setSelects', values); + } + } + // // private functions // ------------------ diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/index.ts b/aurelia-slickgrid/src/aurelia-slickgrid/index.ts index 30f3ede6a..2e1e8d6da 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/index.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/index.ts @@ -25,7 +25,10 @@ import { GraphqlServiceOption, GridOption, OnEventArgs, - SearchTerm + OperatorType, + SearchTerm, + SortDirection, + SortDirectionString } from './models/index'; // editors, formatters, ... @@ -48,6 +51,7 @@ import { GridExtraService, GridEventService, GridOdataService, + GridStateService, ResizerService, SortService } from './services/index'; @@ -96,7 +100,10 @@ export { FormElementType, FieldType, OnEventArgs, + OperatorType, SearchTerm, + SortDirection, + SortDirectionString, Editors, Filters, @@ -113,6 +120,7 @@ export { GridExtraUtils, GridExtraService, GridOdataService, + GridStateService, ResizerService, SortService, diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/backendService.interface.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/backendService.interface.ts index 88b35d480..5e46a5d95 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/models/backendService.interface.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/backendService.interface.ts @@ -1,17 +1,72 @@ -import { BackendServiceOption } from './backendServiceOption.interface'; -import { FilterChangedArgs } from './filterChangedArgs.interface'; -import { Pagination } from './pagination.interface'; -import { PaginationChangedArgs } from './paginationChangedArgs.interface'; -import { SortChangedArgs } from './sortChangedArgs.interface'; +import { EventAggregator, Subscription } from 'aurelia-event-aggregator'; +import { + BackendServiceOption, + Column, + ColumnFilters, + CurrentFilter, + CurrentPagination, + CurrentSorter, + FilterChangedArgs, + GridOption, + Pagination, + PaginationChangedArgs, + SortChangedArgs, + SortChanged +} from './../models/index'; export interface BackendService { + /** Backend Service options */ options?: BackendServiceOption; + + /** Build and the return the backend service query string */ buildQuery: (serviceOptions?: BackendServiceOption) => string; - initOptions: (serviceOptions?: BackendServiceOption, pagination?: Pagination) => void; + + /** initialize the backend service with certain options */ + init?: (serviceOptions?: BackendServiceOption, pagination?: Pagination, grid?: any) => void; + + /** DEPRECATED, please use "init()" instead */ + initOptions?: (serviceOptions?: BackendServiceOption, pagination?: Pagination, gridOptions?: GridOption, columnDefinitions?: Column[]) => void; + + /** Get the dataset name */ getDatasetName?: () => string; + + /** Get the Filters that are currently used by the grid */ + getCurrentFilters?: () => ColumnFilters | CurrentFilter[]; + + /** Get the Pagination that is currently used by the grid */ + getCurrentPagination?: () => CurrentPagination; + + /** Get the Sorters that are currently used by the grid */ + getCurrentSorters?: () => ColumnFilters | CurrentFilter[]; + + /** Reset the pagination options */ resetPaginationOptions: () => void; + + /** Update the Filters options with a set of new options */ + updateFilters?: (columnFilters: ColumnFilters | CurrentFilter[], isUpdatedByPreset: boolean) => void; + + /** Update the Pagination component with it's new page number and size */ + updatePagination?: (newPage: number, pageSize: number) => void; + + /** Update the Sorters options with a set of new options */ + updateSorters?: (sortColumns?: SortChanged[], presetSorters?: CurrentSorter[]) => void; + + /** Update the backend service options */ updateOptions: (serviceOptions?: BackendServiceOption) => void; + + // -- + // Events / Methods + // ----------------- + + /** Fired when the pagination needs to be forced refreshed (by a Preset call) */ + onPaginationRefreshed?: EventAggregator; // EventEmitter; + + /** Execute when any of the filters changed */ onFilterChanged: (event: Event, args: FilterChangedArgs) => Promise; + + /** Execute when the pagination changed */ onPaginationChanged: (event: Event | undefined, args: PaginationChangedArgs) => string; + + /** Execute when any of the sorters changed */ onSortChanged: (event: Event, args: SortChangedArgs) => string; } diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/columnFilter.interface.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/columnFilter.interface.ts index ea4cbd330..7a4114c5c 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/models/columnFilter.interface.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/columnFilter.interface.ts @@ -4,6 +4,8 @@ import { FilterType, FormElementType, MultipleSelectOption, + OperatorString, + OperatorType, SearchTerm } from './../models/index'; @@ -27,7 +29,7 @@ export interface ColumnFilter { searchTerms?: SearchTerm[]; /** Operator to use when filtering (>, >=, EQ, IN, ...) */ - operator?: string; + operator?: OperatorType | OperatorString; /** Filter Type to use (input, multipleSelect, singleSelect, select, custom) */ type?: FilterType | FormElementType | string; diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/currentFilter.interface.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/currentFilter.interface.ts new file mode 100644 index 000000000..607ba7f0e --- /dev/null +++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/currentFilter.interface.ts @@ -0,0 +1,8 @@ +import { OperatorString, OperatorType, SearchTerm } from './../models/index'; + +export interface CurrentFilter { + columnId: string; + operator?: OperatorType | OperatorString; + searchTerm?: SearchTerm; + searchTerms?: SearchTerm[]; +} diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/currentPagination.interface.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/currentPagination.interface.ts new file mode 100644 index 000000000..edcb44831 --- /dev/null +++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/currentPagination.interface.ts @@ -0,0 +1,4 @@ +export interface CurrentPagination { + pageNumber: number; + pageSize: number; +} diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/currentSorter.interface.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/currentSorter.interface.ts new file mode 100644 index 000000000..dc3620ee4 --- /dev/null +++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/currentSorter.interface.ts @@ -0,0 +1,6 @@ +import { SortDirection, SortDirectionString } from './../models/index'; + +export interface CurrentSorter { + columnId: string; + direction: SortDirection | SortDirectionString; +} diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/filter.interface.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/filter.interface.ts index 38fa86470..f56026b9b 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/models/filter.interface.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/filter.interface.ts @@ -39,4 +39,7 @@ export interface Filter { /** Destroy filter function */ destroy: () => void; + + /** Set value(s) on the DOM element */ + setValues: (values: SearchTerm | SearchTerm[]) => void; } diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/graphqlFilteringOption.interface.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/graphqlFilteringOption.interface.ts index b9502c0cb..68747de19 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/models/graphqlFilteringOption.interface.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/graphqlFilteringOption.interface.ts @@ -1,4 +1,4 @@ -import { OperatorType } from './operatorType'; +import { OperatorType } from './operatorType.enum'; export interface GraphqlFilteringOption { field: string; operator: OperatorType; diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/graphqlSortingOption.interface.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/graphqlSortingOption.interface.ts index cfa486471..af7441b2c 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/models/graphqlSortingOption.interface.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/graphqlSortingOption.interface.ts @@ -1,5 +1,5 @@ -import { SortDirection } from './sortDirection'; +import { SortDirection, SortDirectionString } from './../models/index'; export interface GraphqlSortingOption { field: string; - direction: SortDirection; + direction: SortDirection | SortDirectionString; } diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/gridOption.interface.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/gridOption.interface.ts index fce92f5d2..99f6536af 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/models/gridOption.interface.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/gridOption.interface.ts @@ -9,6 +9,7 @@ import { ExportOption, FilterType, GridMenu, + GridState, HeaderButton, HeaderMenu, Pagination @@ -156,6 +157,9 @@ export interface GridOption { /** "params" is a generic property and can be used to pass custom paramaters to your Formatter/Editor or anything else */ params?: any | any[]; + /** Query presets before grid load (filters, sorters, pagination) */ + presets?: GridState; + /** Register 1 or more Slick Plugins */ registerPlugins?: any | any[]; diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/gridState.interface.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/gridState.interface.ts new file mode 100644 index 000000000..719fc9251 --- /dev/null +++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/gridState.interface.ts @@ -0,0 +1,16 @@ +import { CurrentSorter } from './currentSorter.interface'; +import { CurrentFilter } from './currentFilter.interface'; + +export interface GridState { + /** Filters (and their state, columnId, searchTerm(s)) that are currently applied in the grid */ + filters?: CurrentFilter[]; + + /** Sorters (and their state, columnId, direction) that are currently applied in the grid */ + sorters?: CurrentSorter[]; + + /** Pagination (and it's state, pageNumber, pageSize) that are currently applied in the grid */ + pagination?: { + pageNumber: number; + pageSize: number; + }; +} diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/index.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/index.ts index 21dfa5e94..6816dec5e 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/models/index.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/index.ts @@ -11,6 +11,9 @@ export * from './columnFilter.interface'; export * from './columnFilters.interface'; export * from './columnPicker.interface'; export * from './customGridMenu.interface'; +export * from './currentFilter.interface'; +export * from './currentPagination.interface'; +export * from './currentSorter.interface'; export * from './delimiterType.enum'; export * from './editor.interface'; export * from './editCommand.interface'; @@ -33,6 +36,7 @@ export * from './graphqlPaginationOption.interface'; export * from './graphqlResult.interface'; export * from './graphqlServiceOption.interface'; export * from './graphqlSortingOption.interface'; +export * from './gridState.interface'; export * from './gridMenu.interface'; export * from './gridOption.interface'; export * from './headerButton.interface'; @@ -48,12 +52,14 @@ export * from './multipleSelectOption.interface'; export * from './odataOption.interface'; export * from './onEventArgs.interface'; export * from './operatorString'; -export * from './operatorType'; +export * from './operatorType.enum'; export * from './pagination.interface'; export * from './paginationChangedArgs.interface'; export * from './searchTerm.type'; export * from './selectOption.interface'; export * from './slickEvent.interface'; +export * from './sortChanged.interface'; export * from './sortChangedArgs.interface'; -export * from './sortDirection'; +export * from './sortDirection.enum'; +export * from './sortDirectionString'; export * from './sorter.interface'; diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/operatorType.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/operatorType.enum.ts similarity index 100% rename from aurelia-slickgrid/src/aurelia-slickgrid/models/operatorType.ts rename to aurelia-slickgrid/src/aurelia-slickgrid/models/operatorType.enum.ts diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/pagination.interface.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/pagination.interface.ts index 69366ca31..3c3b9f151 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/models/pagination.interface.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/pagination.interface.ts @@ -1,4 +1,5 @@ export interface Pagination { + pageNumber?: number; pageSizes: number[]; pageSize: number; totalItems: number; diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/sortChanged.interface.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/sortChanged.interface.ts new file mode 100644 index 000000000..9210feaf2 --- /dev/null +++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/sortChanged.interface.ts @@ -0,0 +1,7 @@ +import { Column } from './column.interface'; + +export interface SortChanged { + columnId?: string | number; + sortAsc?: boolean; + sortCol?: Column; +} diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/sortDirection.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/sortDirection.enum.ts similarity index 65% rename from aurelia-slickgrid/src/aurelia-slickgrid/models/sortDirection.ts rename to aurelia-slickgrid/src/aurelia-slickgrid/models/sortDirection.enum.ts index 7c41de73c..60c7c5ad0 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/models/sortDirection.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/sortDirection.enum.ts @@ -1,4 +1,6 @@ export enum SortDirection { + asc = 'asc', ASC = 'ASC', + desc = 'desc', DESC = 'DESC' } diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/sortDirectionString.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/sortDirectionString.ts new file mode 100644 index 000000000..51686e44c --- /dev/null +++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/sortDirectionString.ts @@ -0,0 +1 @@ +export type SortDirectionString = 'asc' | 'ASC' | 'desc' | 'DESC'; diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/services/controlAndPlugin.service.ts b/aurelia-slickgrid/src/aurelia-slickgrid/services/controlAndPlugin.service.ts index 795baadb7..d028ad800 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/services/controlAndPlugin.service.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/services/controlAndPlugin.service.ts @@ -17,7 +17,7 @@ import { } from './../models/index'; import * as $ from 'jquery'; -// using external js modules +// using external non-typed js libraries declare var Slick: any; @inject(ExportService, FilterService, I18N) diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/services/export.service.ts b/aurelia-slickgrid/src/aurelia-slickgrid/services/export.service.ts index 0beedcf88..18dd7120a 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/services/export.service.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/services/export.service.ts @@ -20,7 +20,7 @@ import { TextEncoder } from 'text-encoding-utf-8'; import { addWhiteSpaces, htmlEntityDecode } from './../services/utilities'; import * as $ from 'jquery'; -// using external js modules +// using external non-typed js libraries declare let Slick: any; export interface ExportColumnHeader { diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/services/filter.service.ts b/aurelia-slickgrid/src/aurelia-slickgrid/services/filter.service.ts index fa119a120..56ee2cb70 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/services/filter.service.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/services/filter.service.ts @@ -5,17 +5,20 @@ import { Filters, FilterFactory } from './../filters/index'; import { Column, ColumnFilters, + CurrentFilter, Filter, FilterArguments, FieldType, FilterType, GridOption, + OperatorType, + OperatorString, SearchTerm, SlickEvent } from './../models/index'; import * as $ from 'jquery'; -// using external js modules +// using external non-typed js libraries declare var Slick: any; @inject(FilterFactory) @@ -33,8 +36,8 @@ export class FilterService { constructor(private filterFactory: FilterFactory) { } init(grid: any, gridOptions: GridOption, columnDefinitions: Column[]): void { - this._gridOptions = gridOptions; this._grid = grid; + this._gridOptions = gridOptions; } /** @@ -45,6 +48,8 @@ export class FilterService { attachBackendOnFilter(grid: any, options: GridOption) { this._filters = []; this.emitFilterChangedBy('remote'); + + this._subscriber = new Slick.Event(); this._subscriber.subscribe(this.attachBackendOnFilterSubscribe); // subscribe to SlickGrid onHeaderRowCellRendered event to create filter template @@ -125,6 +130,7 @@ export class FilterService { dataView.setFilterArgs({ columnFilters: this._columnFilters, grid: this._grid }); dataView.setFilter(this.customLocalFilter.bind(this, dataView)); + this._subscriber = new Slick.Event(); this._subscriber.subscribe((e: any, args: any) => { const columnId = args.columnId; if (columnId != null) { @@ -186,9 +192,8 @@ export class FilterService { return true; } - // filter search terms should always be string (even though we permit the end user to input numbers) - // so make sure each term are strings - // run a query if user has some default search terms + // filter search terms should always be string type (even though we permit the end user to input numbers) + // so make sure each term are strings, if user has some default search terms, we will cast them to string if (searchTerms && Array.isArray(searchTerms)) { for (let k = 0, ln = searchTerms.length; k < ln; k++) { // make sure all search terms are strings @@ -237,7 +242,7 @@ export class FilterService { } /** - * Dispose the filters, since it's a singleton, we don't want to affect other grids with same columns + * Dispose of the filters, since it's a singleton, we don't want to affect other grids with same columns */ disposeColumnFilters() { // we need to loop through all columnFilters and delete them 1 by 1 @@ -256,7 +261,26 @@ export class FilterService { }); } - callbackSearchEvent(e: Event | undefined, args: { columnDef: Column, operator?: string, searchTerms?: string[] | number[] }) { + getColumnFilters() { + return this._columnFilters; + } + + getCurrentLocalFilters(): CurrentFilter[] { + const currentFilters: CurrentFilter[] = []; + if (this._columnFilters) { + for (const colId of Object.keys(this._columnFilters)) { + const columnFilter = this._columnFilters[colId]; + currentFilters.push({ + columnId: colId, + searchTerm: (columnFilter && (columnFilter.searchTerm !== undefined || columnFilter.searchTerm !== null)) ? columnFilter.searchTerm : undefined, + searchTerms: (columnFilter && columnFilter.searchTerms) ? columnFilter.searchTerms : null + }); + } + } + return currentFilters; + } + + callbackSearchEvent(e: Event | undefined, args: { columnDef: Column, operator?: OperatorType | OperatorString, searchTerms?: string[] | number[] }) { const targetValue = (e && e.target) ? (e.target as HTMLInputElement).value : undefined; const searchTerms = (args && args.searchTerms && Array.isArray(args.searchTerms)) ? args.searchTerms : []; const columnId = (args && args.columnDef) ? args.columnDef.id || '' : ''; @@ -270,9 +294,9 @@ export class FilterService { this._columnFilters[colId] = { columnId: colId, columnDef: args.columnDef || null, - searchTerms: args.searchTerms || [], - searchTerm: ((e && e.target) ? (e.target as HTMLInputElement).value : ''), - operator: args.operator || '' + operator: args.operator || undefined, + searchTerms: args.searchTerms || undefined, + searchTerm: ((e && e.target) ? (e.target as HTMLInputElement).value : null), }; } @@ -292,17 +316,19 @@ export class FilterService { const columnId = columnDef.id || ''; if (columnDef && columnId !== 'selector' && columnDef.filterable) { - let searchTerms: SearchTerm[] = (columnDef.filter && columnDef.filter.searchTerms) ? columnDef.filter.searchTerms : []; - let searchTerm = (columnDef.filter && (columnDef.filter.searchTerm !== undefined || columnDef.filter.searchTerm !== null)) ? columnDef.filter.searchTerm : ''; - - // keep the filter in a columnFilters for later reference - this.keepColumnFilters(searchTerm || '', searchTerms, columnDef); - - // when hiding/showing (with Column Picker or Grid Menu), it will try to re-create yet again the filters (since SlickGrid does a re-render) - // because of that we need to first get searchTerm(s) from the columnFilters (that is what the user last entered) - // if nothing is found, we can then use the optional searchTerm(s) passed to the Grid Option (that is couple of lines earlier) - searchTerm = ((this._columnFilters[columnDef.id]) ? this._columnFilters[columnDef.id].searchTerm : searchTerm) || ''; - searchTerms = ((this._columnFilters[columnDef.id]) ? this._columnFilters[columnDef.id].searchTerms : searchTerms) || []; + let searchTerms: SearchTerm[]; + let searchTerm: SearchTerm; + + if (this._columnFilters[columnDef.id]) { + searchTerm = this._columnFilters[columnDef.id].searchTerm || undefined; + searchTerms = this._columnFilters[columnDef.id].searchTerms || undefined; + } else if (columnDef.filter) { + // when hiding/showing (with Column Picker or Grid Menu), it will try to re-create yet again the filters (since SlickGrid does a re-render) + // because of that we need to first get searchTerm(s) from the columnFilters (that is what the user last entered) + searchTerms = columnDef.filter.searchTerms || undefined; + searchTerm = columnDef.filter.searchTerm || undefined; + this.updateColumnFilters(searchTerm, searchTerms, columnDef); + } const filterArguments: FilterArguments = { grid: this._grid, @@ -339,6 +365,12 @@ export class FilterService { } else { this._filters[filterExistIndex] = filter; } + + // when hiding/showing (with Column Picker or Grid Menu), it will try to re-create yet again the filters (since SlickGrid does a re-render) + // we need to also set again the values in the DOM elements if the values were set by a searchTerm(s) + if ((searchTerm || searchTerms) && filter.setValues) { + filter.setValues(searchTerm || searchTerms); + } } } } @@ -352,21 +384,52 @@ export class FilterService { this._subscriber.subscribe(() => this.onFilterChanged.publish('filterService:changed', `onFilterChanged by ${sender}`)); } - private keepColumnFilters(searchTerm: SearchTerm, searchTerms: SearchTerm[], columnDef: any) { + /** + * When user passes an array of preset filters, we need to pre-polulate each column filter searchTerm(s) + * The process is to loop through the preset filters array, find the associated column from columnDefinitions and fill in the filter object searchTerm(s) + * This is basically the same as if we would manually add searchTerm(s) to a column filter object in the column definition, but we do it programmatically. + * At the end of the day, when creating the Filter (DOM Element), it will use these searchTerm(s) so we can take advantage of that without recoding each Filter type (DOM element) + * @param gridOptions + * @param columnDefinitions + */ + populateColumnFilterSearchTerms(gridOptions: GridOption, columnDefinitions: Column[]) { + if (gridOptions.presets && gridOptions.presets.filters) { + const filters = gridOptions.presets.filters; + columnDefinitions.forEach((columnDef: Column) => { + const columnPreset = filters.find((presetFilter: CurrentFilter) => { + return presetFilter.columnId === columnDef.id; + }); + if (columnPreset && columnPreset.searchTerm) { + columnDef.filter = columnDef.filter || {}; + columnDef.filter.searchTerm = columnPreset.searchTerm; + } + if (columnPreset && columnPreset.searchTerms) { + columnDef.filter = columnDef.filter || {}; + columnDef.filter.operator = columnDef.filter.operator || OperatorType.in; + columnDef.filter.searchTerms = columnPreset.searchTerms; + } + }); + } + return columnDefinitions; + } + + private updateColumnFilters(searchTerm: SearchTerm, searchTerms: any, columnDef: any) { if (searchTerm !== undefined && searchTerm !== null && searchTerm !== '') { this._columnFilters[columnDef.id] = { columnId: columnDef.id, columnDef, searchTerm, + operator: (columnDef && columnDef.filter && columnDef.filter.operator) ? columnDef.filter.operator : null, type: (columnDef && columnDef.filter && columnDef.filter.type) ? columnDef.filter.type : FilterType.input }; } - if (searchTerms && Array.isArray(searchTerms) && searchTerms.length > 0) { + if (searchTerms) { // this._columnFilters.searchTerms = searchTerms; this._columnFilters[columnDef.id] = { columnId: columnDef.id, columnDef, searchTerms, + operator: (columnDef && columnDef.filter && columnDef.filter.operator) ? columnDef.filter.operator : null, type: (columnDef && columnDef.filter && columnDef.filter.type) ? columnDef.filter.type : FilterType.input }; } diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/services/graphql.service.ts b/aurelia-slickgrid/src/aurelia-slickgrid/services/graphql.service.ts index cbec62e68..d67f15d30 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/services/graphql.service.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/services/graphql.service.ts @@ -1,9 +1,15 @@ +import { EventAggregator } from 'aurelia-event-aggregator'; import { inject } from 'aurelia-framework'; import { I18N } from 'aurelia-i18n'; -import { mapOperatorType } from './utilities'; -import { Pagination } from './../models/pagination.interface'; +import { mapOperatorType, mapOperatorByFilterType } from './utilities'; import { BackendService, + Column, + ColumnFilter, + ColumnFilters, + CurrentFilter, + CurrentPagination, + CurrentSorter, FilterChangedArgs, GraphqlCursorPaginationOption, GraphqlDatasetFilter, @@ -11,12 +17,15 @@ import { GraphqlPaginationOption, GraphqlServiceOption, GraphqlSortingOption, + GridOption, + Pagination, PaginationChangedArgs, + SortChanged, SortChangedArgs, - SortDirection + SortDirection, + SortDirectionString } from './../models/index'; import QueryBuilder from './graphqlQueryBuilder'; -import { GridOption } from '../models/gridOption.interface'; // timer for keeping track of user typing waits let timer: any; @@ -25,6 +34,13 @@ const DEFAULT_ITEMS_PER_PAGE = 25; @inject(I18N) export class GraphqlService implements BackendService { + private _currentFilters: ColumnFilters | CurrentFilter[]; + private _currentPagination: CurrentPagination; + private _currentSorters: CurrentSorter[]; + private _columnDefinitions: Column[]; + private _gridOptions: GridOption; + private _grid: any; + onPaginationRefreshed = new EventAggregator(); options: GraphqlServiceOption; pagination: Pagination | undefined; defaultOrderBy: GraphqlSortingOption = { field: 'id', direction: SortDirection.ASC }; @@ -40,9 +56,10 @@ export class GraphqlService implements BackendService { * @param serviceOptions GraphqlServiceOption */ buildQuery() { - if (!this.options || !this.options.datasetName || (!this.options.columnIds && !this.options.dataFilters && !this.options.columnDefinitions)) { - throw new Error('GraphQL Service requires "datasetName" & ("dataFilters" or "columnDefinitions") properties for it to work'); + if (!this.options || !this.options.datasetName || (!this._columnDefinitions && !this.options.columnDefinitions)) { + throw new Error('GraphQL Service requires "datasetName" & "columnDefinitions" properties for it to work'); } + const columnDefinitions = this._columnDefinitions || this.options.columnDefinitions; const queryQb = new QueryBuilder('query'); const datasetQb = new QueryBuilder(this.options.datasetName); const pageInfoQb = new QueryBuilder('pageInfo'); @@ -50,10 +67,10 @@ export class GraphqlService implements BackendService { // get all the columnds Ids for the filters to work let columnIds: string[]; - if (this.options.columnDefinitions) { - columnIds = Array.isArray(this.options.columnDefinitions) ? this.options.columnDefinitions.map((column) => column.field) : []; + if (columnDefinitions) { + columnIds = Array.isArray(columnDefinitions) ? columnDefinitions.map((column) => column.field) : []; } else { - columnIds = this.options.columnIds || this.options.dataFilters || []; + columnIds = this.options.columnIds || []; } // Slickgrid also requires the "id" field to be part of DataView @@ -134,9 +151,14 @@ export class GraphqlService implements BackendService { .replace(/\}$/, ''); } - initOptions(serviceOptions?: GraphqlServiceOption, pagination?: Pagination): void { + init(serviceOptions?: GraphqlServiceOption, pagination?: Pagination, grid?: any): void { + this._grid = grid; this.options = serviceOptions || {}; this.pagination = pagination; + if (grid && grid.getColumns && grid.getOptions) { + this._columnDefinitions = grid.getColumns() || serviceOptions.columnDefinitions; + this._gridOptions = grid.getOptions(); + } } /** @@ -147,10 +169,26 @@ export class GraphqlService implements BackendService { return (this.options.isWithCursor) ? { first: (this.pagination ? this.pagination.pageSize : DEFAULT_ITEMS_PER_PAGE) } : { first: (this.pagination ? this.pagination.pageSize : DEFAULT_ITEMS_PER_PAGE), offset: 0 }; } + /** Get the GraphQL dataset name */ getDatasetName(): string { return this.options.datasetName || ''; } + /** Get the Filters that are currently used by the grid */ + getCurrentFilters(): ColumnFilters | CurrentFilter[] { + return this._currentFilters; + } + + /** Get the Pagination that is currently used by the grid */ + getCurrentPagination(): CurrentPagination { + return this._currentPagination; + } + + /** Get the Sorters that are currently used by the grid */ + getCurrentSorters(): CurrentSorter[] { + return this._currentSorters; + } + /* * Reset the pagination options */ @@ -179,9 +217,8 @@ export class GraphqlService implements BackendService { * FILTERING */ onFilterChanged(event: Event, args: FilterChangedArgs): Promise { - const searchByArray: GraphqlFilteringOption[] = []; - const serviceOptions: GridOption = args.grid.getOptions(); - const backendApi = serviceOptions.backendServiceApi || serviceOptions.onBackendEventApi; + const gridOptions: GridOption = this._gridOptions || args.grid.getOptions(); + const backendApi = gridOptions.backendServiceApi || gridOptions.onBackendEventApi; if (backendApi === undefined) { throw new Error('Something went wrong in the GraphqlService, "backendServiceApi" is not initialized'); @@ -189,67 +226,17 @@ export class GraphqlService implements BackendService { // only add a delay when user is typing, on select dropdown filter it will execute right away let debounceTypingDelay = 0; - if (event.type === 'keyup' || event.type === 'keydown') { + if (event && (event.type === 'keyup' || event.type === 'keydown')) { debounceTypingDelay = backendApi.filterTypingDebounce || DEFAULT_FILTER_TYPING_DEBOUNCE; } const promise = new Promise((resolve, reject) => { - let searchValue: string | string[] | number[]; - if (!args || !args.grid) { throw new Error('Something went wrong when trying create the GraphQL Backend Service, it seems that "args" is not populated correctly'); } - // loop through all columns to inspect filters - for (const columnId in args.columnFilters) { - if (args.columnFilters.hasOwnProperty(columnId)) { - const columnFilter = args.columnFilters[columnId]; - const columnDef = columnFilter.columnDef; - if (!columnDef) { - return; - } - const fieldName = columnDef.queryField || columnDef.field || columnDef.name || ''; - const searchTerms = (columnFilter ? columnFilter.searchTerms : null) || []; - let fieldSearchValue = columnFilter.searchTerm; - if (typeof fieldSearchValue === 'undefined') { - fieldSearchValue = ''; - } - - if (typeof fieldSearchValue !== 'string' && !searchTerms) { - throw new Error(`GraphQL filter searchTerm property must be provided as type "string", if you use filter with options then make sure your IDs are also string. For example: filter: {type: FilterType.select, collection: [{ id: "0", value: "0" }, { id: "1", value: "1" }]`); - } - - fieldSearchValue = '' + fieldSearchValue; // make sure it's a string - const matches = fieldSearchValue.match(/^([<>!=\*]{0,2})(.*[^<>!=\*])([\*]?)$/); // group 1: Operator, 2: searchValue, 3: last char is '*' (meaning starts with, ex.: abc*) - let operator = columnFilter.operator || ((matches) ? matches[1] : ''); - searchValue = (!!matches) ? matches[2] : ''; - const lastValueChar = (!!matches) ? matches[3] : ''; - - // no need to query if search value is empty - if (fieldName && searchValue === '' && searchTerms.length === 0) { - continue; - } - - // when having more than 1 search term (we need to create a CSV string for GraphQL "IN" or "NOT IN" filter search) - if (searchTerms && searchTerms.length > 0) { - searchValue = searchTerms.join(','); - } else { - // escaping the search value - searchValue = searchValue.replace(`'`, `''`); // escape single quotes by doubling them - if (operator === '*' || lastValueChar === '*') { - operator = (operator === '*') ? 'endsWith' : 'startsWith'; - } - } - - searchByArray.push({ - field: fieldName, - operator: mapOperatorType(operator), - value: searchValue - }); - } - } - - this.updateOptions({ filteringOptions: searchByArray }); + // loop through all columns to inspect filters & set the query + this.updateFilters(args.columnFilters, false); // reset Pagination, then build the GraphQL query which we will use in the WebAPI callback // wait a minimum user typing inactivity before processing any query @@ -293,21 +280,8 @@ export class GraphqlService implements BackendService { * } */ onPaginationChanged(event: Event, args: PaginationChangedArgs) { - let paginationOptions; - const pageSize = +args.pageSize || 20; - - if (this.options.isWithCursor) { - paginationOptions = { - first: pageSize - }; - } else { - paginationOptions = { - first: pageSize, - offset: (args.newPage - 1) * pageSize - }; - } - - this.updateOptions({ paginationOptions }); + const pageSize = +args.pageSize || this.pagination.pageSize; + this.updatePagination(args.newPage, pageSize); // build the GraphQL query which we will use in the WebAPI callback return this.buildQuery(); @@ -319,30 +293,166 @@ export class GraphqlService implements BackendService { * https://github.com/graphql/graphql-relay-js/issues/20#issuecomment-220494222 */ onSortChanged(event: Event, args: SortChangedArgs) { - let sortByArray: GraphqlSortingOption[] = []; const sortColumns = (args.multiColumnSort) ? args.sortCols : new Array({ sortCol: args.sortCol, sortAsc: args.sortAsc }); - // build the orderBy array, it could be multisort, example - // orderBy:[{field: lastName, direction: ASC}, {field: firstName, direction: DESC}] - if (sortColumns && sortColumns.length === 0) { - sortByArray = new Array(this.defaultOrderBy); // when empty, use the default sort - } else { - if (sortColumns) { - for (const column of sortColumns) { - const fieldName = column.sortCol.queryField || column.sortCol.field || column.sortCol.id; - const direction = column.sortAsc ? SortDirection.ASC : SortDirection.DESC; - sortByArray.push({ - field: fieldName, - direction + // loop through all columns to inspect sorters & set the query + this.updateSorters(sortColumns); + + // build the GraphQL query which we will use in the WebAPI callback + return this.buildQuery(); + } + + /** + * loop through all columns to inspect filters & update backend service filteringOptions + * @param columnFilters + */ + updateFilters(columnFilters: ColumnFilters | CurrentFilter[], isUpdatedByPreset: boolean) { + // keep current filters & always save it as an array (columnFilters can be an object when it is dealt by SlickGrid Filter) + this._currentFilters = this.castFilterToColumnFilter(columnFilters); + + const searchByArray: GraphqlFilteringOption[] = []; + let searchValue: string | string[]; + + for (const columnId in columnFilters) { + if (columnFilters.hasOwnProperty(columnId)) { + const columnFilter = columnFilters[columnId]; + + // if user defined some "presets", then we need to find the filters from the column definitions instead + let columnDef: Column; + if (isUpdatedByPreset && Array.isArray(this._columnDefinitions)) { + columnDef = this._columnDefinitions.find((column: Column) => { + return column.id === columnFilter.columnId; }); + } else { + columnDef = columnFilter.columnDef; + } + if (!columnDef) { + throw new Error('[Backend Service API]: Something went wrong in trying to get the column definition of the specified filter (or preset filters). Did you make a typo on the filter columnId?'); } + + const fieldName = columnDef.queryField || columnDef.field || columnDef.name || ''; + const searchTerms = (columnFilter ? columnFilter.searchTerms : null) || []; + let fieldSearchValue = columnFilter.searchTerm; + if (typeof fieldSearchValue === 'undefined') { + fieldSearchValue = ''; + } + + if (typeof fieldSearchValue !== 'string' && !searchTerms) { + throw new Error(`GraphQL filter searchTerm property must be provided as type "string", if you use filter with options then make sure your IDs are also string. For example: filter: {type: FilterType.select, collection: [{ id: "0", value: "0" }, { id: "1", value: "1" }]`); + } + + fieldSearchValue = '' + fieldSearchValue; // make sure it's a string + const matches = fieldSearchValue.match(/^([<>!=\*]{0,2})(.*[^<>!=\*])([\*]?)$/); // group 1: Operator, 2: searchValue, 3: last char is '*' (meaning starts with, ex.: abc*) + let operator = columnFilter.operator || ((matches) ? matches[1] : ''); + searchValue = (!!matches) ? matches[2] : ''; + const lastValueChar = (!!matches) ? matches[3] : ''; + + // no need to query if search value is empty + if (fieldName && searchValue === '' && searchTerms.length === 0) { + continue; + } + + // when having more than 1 search term (we need to create a CSV string for GraphQL "IN" or "NOT IN" filter search) + if (searchTerms && searchTerms.length > 0) { + searchValue = searchTerms.join(','); + } else if (typeof searchValue === 'string') { + // escaping the search value + searchValue = searchValue.replace(`'`, `''`); // escape single quotes by doubling them + if (operator === '*' || lastValueChar === '*') { + operator = (operator === '*') ? 'endsWith' : 'startsWith'; + } + } + + // if we didn't find an Operator but we have a Filter Type, we should use default Operator + if (!operator && columnDef.filter) { + operator = mapOperatorByFilterType(columnDef.filter.type); + } + + searchByArray.push({ + field: fieldName, + operator: mapOperatorType(operator), + value: searchValue + }); } } - this.updateOptions({ sortingOptions: sortByArray }); + // update the service options with filters for the buildQuery() to work later + this.updateOptions({ filteringOptions: searchByArray }); + } - // build the GraphQL query which we will use in the WebAPI callback - return this.buildQuery(); + /** + * Update the pagination component with it's new page number and size + * @param newPage + * @param pageSize + */ + updatePagination(newPage: number, pageSize: number) { + this._currentPagination = { + pageNumber: newPage, + pageSize + }; + + let paginationOptions; + if (this.options.isWithCursor) { + paginationOptions = { + first: pageSize + }; + } else { + paginationOptions = { + first: pageSize, + offset: (newPage - 1) * pageSize + }; + } + + this.updateOptions({ paginationOptions }); + } + + /** + * loop through all columns to inspect sorters & update backend service sortingOptions + * @param columnFilters + */ + updateSorters(sortColumns?: SortChanged[], presetSorters?: CurrentSorter[]) { + let currentSorters: CurrentSorter[] = []; + let graphqlSorters: GraphqlSortingOption[] = []; + + if (!sortColumns && presetSorters) { + // make the presets the current sorters, also make sure that all direction are in uppercase for GraphQL + currentSorters = presetSorters; + currentSorters.forEach((sorter) => sorter.direction = sorter.direction.toUpperCase() as SortDirectionString); + + // display the correct sorting icons on the UI, for that it requires (columnId, sortAsc) properties + const tmpSorterArray = currentSorters.map((sorter) => { + return { + columnId: sorter.columnId, + sortAsc: sorter.direction.toUpperCase() === SortDirection.ASC + }; + }); + this._grid.setSortColumns(tmpSorterArray); + } else if (sortColumns && !presetSorters) { + // build the orderBy array, it could be multisort, example + // orderBy:[{field: lastName, direction: ASC}, {field: firstName, direction: DESC}] + if (sortColumns && sortColumns.length === 0) { + graphqlSorters = new Array(this.defaultOrderBy); // when empty, use the default sort + currentSorters = new Array({ columnId: this.defaultOrderBy.direction, direction: this.defaultOrderBy.direction }); + } else { + if (sortColumns) { + for (const column of sortColumns) { + currentSorters.push({ + columnId: (column.sortCol.queryField || column.sortCol.field || column.sortCol.id) + '', + direction: column.sortAsc ? SortDirection.ASC : SortDirection.DESC + }); + + graphqlSorters.push({ + field: (column.sortCol.queryField || column.sortCol.field || column.sortCol.id) + '', + direction: column.sortAsc ? SortDirection.ASC : SortDirection.DESC + }); + } + } + } + } + + // keep current Sorters and update the service options with the new sorting + this._currentSorters = currentSorters; + this.updateOptions({ sortingOptions: graphqlSorters }); } /** @@ -381,4 +491,30 @@ export class GraphqlService implements BackendService { return rep; }); } + + // + // private functions + // ------------------- + + /** + * Cast provided filters (could be in multiple format) into an array of ColumnFilter + * @param columnFilters + */ + private castFilterToColumnFilter(columnFilters: ColumnFilters | CurrentFilter[]): CurrentFilter[] { + // keep current filters & always save it as an array (columnFilters can be an object when it is dealt by SlickGrid Filter) + const filtersArray: ColumnFilter[] = (typeof columnFilters === 'object') ? Object.keys(columnFilters).map(key => columnFilters[key]) : columnFilters; + + return filtersArray.map((filter) => { + const tmpFilter: CurrentFilter = { columnId: filter.columnId }; + if (filter.operator) { + tmpFilter.operator = filter.operator; + } + if (Array.isArray(filter.searchTerms)) { + tmpFilter.searchTerms = filter.searchTerms; + } else { + tmpFilter.searchTerm = filter.searchTerm; + } + return tmpFilter; + }); + } } diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/services/grid-odata.service.ts b/aurelia-slickgrid/src/aurelia-slickgrid/services/grid-odata.service.ts index 2d35b2459..428c30977 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/services/grid-odata.service.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/services/grid-odata.service.ts @@ -1,21 +1,48 @@ import './global-utilities'; import { inject } from 'aurelia-framework'; import { parseUtcDate } from './utilities'; -import { BackendService, CaseType, FilterChangedArgs, FieldType, GridOption, OdataOption, PaginationChangedArgs, SortChangedArgs } from './../models/index'; +import { + BackendService, + CaseType, + Column, + ColumnFilter, + ColumnFilters, + CurrentFilter, + CurrentPagination, + CurrentSorter, + FilterChangedArgs, + FieldType, + GridOption, + OdataOption, + Pagination, + PaginationChangedArgs, + SearchTerm, + SortChanged, + SortChangedArgs, + SortDirection, + SortDirectionString +} from './../models/index'; import { OdataService } from './odata.service'; -import { Pagination } from './../models/pagination.interface'; import * as moment from 'moment'; + let timer: any; const DEFAULT_FILTER_TYPING_DEBOUNCE = 750; const DEFAULT_ITEMS_PER_PAGE = 25; @inject(OdataService) export class GridOdataService implements BackendService { + private _currentFilters: CurrentFilter[]; + private _currentPagination: CurrentPagination; + private _currentSorters: CurrentSorter[]; + private _columnDefinitions: Column[]; + private _gridOptions: GridOption; + private _grid: any; options: OdataOption; - pagination: Pagination | undefined; + pagination: Pagination; defaultOptions: OdataOption = { top: DEFAULT_ITEMS_PER_PAGE, - orderBy: '' + orderBy: '', + caseType: CaseType.pascalCase }; constructor(private odataService: OdataService) { } @@ -24,10 +51,17 @@ export class GridOdataService implements BackendService { return this.odataService.buildQuery(); } - initOptions(options: OdataOption, pagination?: Pagination): void { - this.odataService.options = { ...this.defaultOptions, ...options, top: options.top || (pagination ? pagination.pageSize : null) || this.defaultOptions.top }; - this.options = options; + init(options: OdataOption, pagination?: Pagination, grid?: any): void { + this._grid = grid; + const mergedOptions = { ...this.defaultOptions, ...options }; + this.odataService.options = { ...mergedOptions, top: mergedOptions.top || (pagination ? pagination.pageSize : null) || this.defaultOptions.top }; + this.options = this.odataService.options; this.pagination = pagination; + + if (grid && grid.getColumns && grid.getOptions) { + this._columnDefinitions = grid.getColumns() || options.columnDefinitions; + this._gridOptions = grid.getOptions(); + } } updateOptions(serviceOptions?: OdataOption) { @@ -38,6 +72,21 @@ export class GridOdataService implements BackendService { this.odataService.removeColumnFilter(fieldName); } + /** Get the Filters that are currently used by the grid */ + getCurrentFilters(): CurrentFilter[] { + return this._currentFilters; + } + + /** Get the Pagination that is currently used by the grid */ + getCurrentPagination(): CurrentPagination { + return this._currentPagination; + } + + /** Get the Sorters that are currently used by the grid */ + getCurrentSorters(): CurrentSorter[] { + return this._currentSorters; + } + /* * Reset the pagination options */ @@ -55,8 +104,6 @@ export class GridOdataService implements BackendService { * FILTERING */ onFilterChanged(event: Event, args: FilterChangedArgs): Promise { - let searchBy = ''; - const searchByArray: string[] = []; const serviceOptions: GridOption = args.grid.getOptions(); const backendApi = serviceOptions.backendServiceApi || serviceOptions.onBackendEventApi; @@ -66,118 +113,13 @@ export class GridOdataService implements BackendService { // only add a delay when user is typing, on select dropdown filter it will execute right away let debounceTypingDelay = 0; - if (event.type === 'keyup' || event.type === 'keydown') { + if (event && (event.type === 'keyup' || event.type === 'keydown')) { debounceTypingDelay = backendApi.filterTypingDebounce || DEFAULT_FILTER_TYPING_DEBOUNCE; } const promise = new Promise((resolve, reject) => { - // loop through all columns to inspect filters - for (const columnId in args.columnFilters) { - if (args.columnFilters.hasOwnProperty(columnId)) { - const columnFilter = args.columnFilters[columnId]; - const columnDef = columnFilter.columnDef; - if (!columnDef) { - return; - } - const fieldName = columnDef.queryField || columnDef.field || columnDef.name; - const fieldType = columnDef.type || 'string'; - const searchTerms = (columnFilter ? columnFilter.searchTerms : null) || []; - let fieldSearchValue = columnFilter.searchTerm; - if (typeof fieldSearchValue === 'undefined') { - fieldSearchValue = ''; - } - - if (typeof fieldSearchValue !== 'string' && !searchTerms) { - throw new Error(`ODdata filter searchTerm property must be provided as type "string", if you use filter with options then make sure your IDs are also string. For example: filter: {type: FilterType.select, collection: [{ id: "0", value: "0" }, { id: "1", value: "1" }]`); - } - - fieldSearchValue = '' + fieldSearchValue; // make sure it's a string - const matches = fieldSearchValue.match(/^([<>!=\*]{0,2})(.*[^<>!=\*])([\*]?)$/); // group 1: Operator, 2: searchValue, 3: last char is '*' (meaning starts with, ex.: abc*) - const operator = columnFilter.operator || ((matches) ? matches[1] : ''); - let searchValue = (!!matches) ? matches[2] : ''; - const lastValueChar = (!!matches) ? matches[3] : ''; - const bypassOdataQuery = columnFilter.bypassBackendQuery || false; - - // no need to query if search value is empty - if (fieldName && searchValue === '') { - this.removeColumnFilter(fieldName); - continue; - } - - // escaping the search value - searchValue = searchValue.replace(`'`, `''`); // escape single quotes by doubling them - searchValue = encodeURIComponent(searchValue); // encode URI of the final search value - - // extra query arguments - if (bypassOdataQuery) { - // push to our temp array and also trim white spaces - if (fieldName) { - this.saveColumnFilter(fieldName, fieldSearchValue, searchTerms); - } - } else { - searchBy = ''; - - // titleCase the fieldName so that it matches the WebApi names - const fieldNameTitleCase = String.titleCase(fieldName || ''); - - // when having more than 1 search term (then check if we have a "IN" or "NOT IN" filter search) - if (searchTerms && searchTerms.length > 0) { - const tmpSearchTerms = []; - - if (operator === 'IN') { - // example:: (Stage eq "Expired" or Stage eq "Renewal") - for (let j = 0, lnj = searchTerms.length; j < lnj; j++) { - tmpSearchTerms.push(`${fieldNameTitleCase} eq '${searchTerms[j]}'`); - } - searchBy = tmpSearchTerms.join(' or '); - searchBy = `(${searchBy})`; - } else if (operator === 'NIN' || operator === 'NOTIN' || operator === 'NOT IN') { - // example:: (Stage ne "Expired" and Stage ne "Renewal") - for (let k = 0, lnk = searchTerms.length; k < lnk; k++) { - tmpSearchTerms.push(`${fieldNameTitleCase} ne '${searchTerms[k]}'`); - } - searchBy = tmpSearchTerms.join(' and '); - searchBy = `(${searchBy})`; - } - } else if (operator === '*' || lastValueChar !== '') { - // first/last character is a '*' will be a startsWith or endsWith - searchBy = operator === '*' - ? `endswith(${fieldNameTitleCase}, '${searchValue}')` - : `startswith(${fieldNameTitleCase}, '${searchValue}')`; - } else if (fieldType === FieldType.date) { - // date field needs to be UTC and within DateTime function - const dateFormatted = parseUtcDate(searchValue, true); - if (dateFormatted) { - searchBy = `${fieldNameTitleCase} ${this.mapOdataOperator(operator)} DateTime'${dateFormatted}'`; - } - } else if (fieldType === FieldType.string) { - // string field needs to be in single quotes - if (operator === '') { - searchBy = `substringof('${searchValue}', ${fieldNameTitleCase})`; - } else { - // searchBy = `substringof('${searchValue}', ${fieldNameTitleCase}) ${this.mapOdataOperator(operator)} true`; - searchBy = `${fieldNameTitleCase} ${this.mapOdataOperator(operator)} '${searchValue}'`; - } - } else { - // any other field type (or undefined type) - searchValue = fieldType === FieldType.number ? searchValue : `'${searchValue}'`; - searchBy = `${fieldNameTitleCase} ${this.mapOdataOperator(operator)} ${searchValue}`; - } - - // push to our temp array and also trim white spaces - if (searchBy !== '') { - searchByArray.push(String.trim(searchBy)); - this.saveColumnFilter(fieldName || '', fieldSearchValue, searchTerms); - } - } - } - } - - // build the filter query - this.odataService.updateOptions({ - filter: (searchByArray.length > 0) ? searchByArray.join(' and ') : '', - skip: undefined - }); + // loop through all columns to inspect filters & set the query + this.updateFilters(args.columnFilters); // reset Pagination, then build the OData query which we will use in the WebAPI callback // wait a minimum user typing inactivity before processing any query @@ -196,11 +138,7 @@ export class GridOdataService implements BackendService { */ onPaginationChanged(event: Event, args: PaginationChangedArgs) { const pageSize = +args.pageSize || 20; - - this.odataService.updateOptions({ - top: pageSize, - skip: (args.newPage - 1) * pageSize - }); + this.updatePagination(args.newPage, pageSize); // build the OData query which we will use in the WebAPI callback return this.odataService.buildQuery(); @@ -210,36 +148,243 @@ export class GridOdataService implements BackendService { * SORTING */ onSortChanged(event: Event, args: SortChangedArgs) { - let sortByArray = []; const sortColumns = (args.multiColumnSort) ? args.sortCols : new Array({ sortCol: args.sortCol, sortAsc: args.sortAsc }); - // build the SortBy string, it could be multisort, example: customerNo asc, purchaserName desc - if (sortColumns && sortColumns.length === 0) { - sortByArray = new Array(this.defaultOptions.orderBy); // when empty, use the default sort - } else { - if (sortColumns) { - for (const column of sortColumns) { - let fieldName = column.sortCol.queryField || column.sortCol.field || column.sortCol.id; + // loop through all columns to inspect sorters & set the query + this.updateSorters(sortColumns); + + // build the OData query which we will use in the WebAPI callback + return this.odataService.buildQuery(); + } + + /** + * loop through all columns to inspect filters & update backend service filteringOptions + * @param columnFilters + */ + updateFilters(columnFilters: ColumnFilters | CurrentFilter[], isUpdatedByPreset?: boolean) { + this._currentFilters = this.castFilterToColumnFilter(columnFilters); + let searchBy = ''; + const searchByArray: string[] = []; + + // loop through all columns to inspect filters + for (const columnId in columnFilters) { + if (columnFilters.hasOwnProperty(columnId)) { + const columnFilter = columnFilters[columnId]; + + // if user defined some "presets", then we need to find the filters from the column definitions instead + let columnDef: Column; + if (isUpdatedByPreset && Array.isArray(this._columnDefinitions)) { + columnDef = this._columnDefinitions.find((column: Column) => { + return column.id === columnFilter.columnId; + }); + } else { + columnDef = columnFilter.columnDef; + } + if (!columnDef) { + throw new Error('[Backend Service API]: Something went wrong in trying to get the column definition of the specified filter (or preset filters). Did you make a typo on the filter columnId?'); + } + + let fieldName = columnDef.queryField || columnDef.field || columnDef.name || ''; + const fieldType = columnDef.type || 'string'; + const searchTerms = (columnFilter ? columnFilter.searchTerms : null) || []; + let fieldSearchValue = columnFilter.searchTerm; + if (typeof fieldSearchValue === 'undefined') { + fieldSearchValue = ''; + } + + if (typeof fieldSearchValue !== 'string' && !searchTerms) { + throw new Error(`ODdata filter searchTerm property must be provided as type "string", if you use filter with options then make sure your IDs are also string. For example: filter: {type: FilterType.select, collection: [{ id: "0", value: "0" }, { id: "1", value: "1" }]`); + } + + fieldSearchValue = '' + fieldSearchValue; // make sure it's a string + const matches = fieldSearchValue.match(/^([<>!=\*]{0,2})(.*[^<>!=\*])([\*]?)$/); // group 1: Operator, 2: searchValue, 3: last char is '*' (meaning starts with, ex.: abc*) + const operator = columnFilter.operator || ((matches) ? matches[1] : ''); + let searchValue = (!!matches) ? matches[2] : ''; + const lastValueChar = (!!matches) ? matches[3] : ''; + const bypassOdataQuery = columnFilter.bypassBackendQuery || false; + + // no need to query if search value is empty + if (fieldName && searchValue === '') { + this.removeColumnFilter(fieldName); + continue; + } + + // escaping the search value + searchValue = searchValue.replace(`'`, `''`); // escape single quotes by doubling them + searchValue = encodeURIComponent(searchValue); // encode URI of the final search value + // extra query arguments + if (bypassOdataQuery) { + // push to our temp array and also trim white spaces + if (fieldName) { + this.saveColumnFilter(fieldName, fieldSearchValue, searchTerms); + } + } else { + searchBy = ''; + + // titleCase the fieldName so that it matches the WebApi names if (this.odataService.options.caseType === CaseType.pascalCase) { - fieldName = String.titleCase(fieldName); + fieldName = String.titleCase(fieldName || ''); + } + + // when having more than 1 search term (then check if we have a "IN" or "NOT IN" filter search) + if (searchTerms && searchTerms.length > 0) { + const tmpSearchTerms = []; + + if (operator === 'IN') { + // example:: (Stage eq "Expired" or Stage eq "Renewal") + for (let j = 0, lnj = searchTerms.length; j < lnj; j++) { + tmpSearchTerms.push(`${fieldName} eq '${searchTerms[j]}'`); + } + searchBy = tmpSearchTerms.join(' or '); + searchBy = `(${searchBy})`; + } else if (operator === 'NIN' || operator === 'NOTIN' || operator === 'NOT IN') { + // example:: (Stage ne "Expired" and Stage ne "Renewal") + for (let k = 0, lnk = searchTerms.length; k < lnk; k++) { + tmpSearchTerms.push(`${fieldName} ne '${searchTerms[k]}'`); + } + searchBy = tmpSearchTerms.join(' and '); + searchBy = `(${searchBy})`; + } + } else if (operator === '*' || lastValueChar !== '') { + // first/last character is a '*' will be a startsWith or endsWith + searchBy = operator === '*' + ? `endswith(${fieldName}, '${searchValue}')` + : `startswith(${fieldName}, '${searchValue}')`; + } else if (fieldType === FieldType.date) { + // date field needs to be UTC and within DateTime function + const dateFormatted = parseUtcDate(searchValue, true); + if (dateFormatted) { + searchBy = `${fieldName} ${this.mapOdataOperator(operator)} DateTime'${dateFormatted}'`; + } + } else if (fieldType === FieldType.string) { + // string field needs to be in single quotes + if (operator === '') { + searchBy = `substringof('${searchValue}', ${fieldName})`; + } else { + // searchBy = `substringof('${searchValue}', ${fieldNameCased}) ${this.mapOdataOperator(operator)} true`; + searchBy = `${fieldName} ${this.mapOdataOperator(operator)} '${searchValue}'`; + } + } else { + // any other field type (or undefined type) + searchValue = fieldType === FieldType.number ? searchValue : `'${searchValue}'`; + searchBy = `${fieldName} ${this.mapOdataOperator(operator)} ${searchValue}`; + } + + // push to our temp array and also trim white spaces + if (searchBy !== '') { + searchByArray.push(String.trim(searchBy)); + this.saveColumnFilter(fieldName || '', fieldSearchValue, searchTerms); } - const direction = column.sortAsc ? 'asc' : 'desc'; - const sortByColumnString = `${fieldName} ${direction}`; - sortByArray.push(sortByColumnString); } } } - // transform the sortby array into a CSV string - const csvArray = sortByArray.join(','); + // update the service options with filters for the buildQuery() to work later this.odataService.updateOptions({ - orderBy: (this.odataService.options.caseType === CaseType.pascalCase) ? String.titleCase(csvArray) : csvArray + filter: (searchByArray.length > 0) ? searchByArray.join(' and ') : '', + skip: undefined }); + } + + /** + * Update the pagination component with it's new page number and size + * @param newPage + * @param pageSize + */ + updatePagination(newPage: number, pageSize: number) { + this._currentPagination = { + pageNumber: newPage, + pageSize + }; + + this.odataService.updateOptions({ + top: pageSize, + skip: (newPage - 1) * pageSize + }); + } + + /** + * loop through all columns to inspect sorters & update backend service orderBy + * @param columnFilters + */ + updateSorters(sortColumns?: SortChanged[], presetSorters?: CurrentSorter[]) { + let sortByArray = []; + const sorterArray: CurrentSorter[] = []; + + if (!sortColumns && presetSorters) { + // make the presets the current sorters, also make sure that all direction are in lowercase for OData + sortByArray = presetSorters; + sortByArray.forEach((sorter) => sorter.direction = sorter.direction.toLowerCase() as SortDirectionString); + + // display the correct sorting icons on the UI, for that it requires (columnId, sortAsc) properties + const tmpSorterArray = sortByArray.map((sorter) => { + return { + columnId: sorter.columnId, + sortAsc: sorter.direction.toUpperCase() === SortDirection.ASC + }; + }); + this._grid.setSortColumns(tmpSorterArray); + } else if (sortColumns && !presetSorters) { + // build the SortBy string, it could be multisort, example: customerNo asc, purchaserName desc + if (sortColumns && sortColumns.length === 0) { + sortByArray = new Array(this.defaultOptions.orderBy); // when empty, use the default sort + } else { + if (sortColumns) { + for (const column of sortColumns) { + let fieldName = (column.sortCol.queryField || column.sortCol.field || column.sortCol.id) + ''; + if (this.odataService.options.caseType === CaseType.pascalCase) { + fieldName = String.titleCase(fieldName); + } + + sorterArray.push({ + columnId: fieldName, + direction: column.sortAsc ? 'asc' : 'desc' + }); + } + sortByArray = sorterArray; + } + } + } + + // transform the sortby array into a CSV string for OData + const csvString = sortByArray.map((sorter) => `${sorter.columnId} ${sorter.direction.toLowerCase()}`).join(','); + this.odataService.updateOptions({ + orderBy: (this.odataService.options.caseType === CaseType.pascalCase) ? String.titleCase(csvString) : csvString + }); + + // keep current Sorters and update the service options with the new sorting + this._currentSorters = sortByArray; // build the OData query which we will use in the WebAPI callback return this.odataService.buildQuery(); } + // + // private functions + // ------------------- + + /** + * Cast provided filters (could be in multiple format) into an array of ColumnFilter + * @param columnFilters + */ + private castFilterToColumnFilter(columnFilters: ColumnFilters | CurrentFilter[]): CurrentFilter[] { + // keep current filters & always save it as an array (columnFilters can be an object when it is dealt by SlickGrid Filter) + const filtersArray: ColumnFilter[] = (typeof columnFilters === 'object') ? Object.keys(columnFilters).map(key => columnFilters[key]) : columnFilters; + + return filtersArray.map((filter) => { + const tmpFilter: CurrentFilter = { columnId: filter.columnId }; + if (filter.operator) { + tmpFilter.operator = filter.operator; + } + if (Array.isArray(filter.searchTerms)) { + tmpFilter.searchTerms = filter.searchTerms; + } else { + tmpFilter.searchTerm = filter.searchTerm; + } + return tmpFilter; + }); + } + /** * Mapper for mathematical operators (ex.: <= is "le", > is "gt") * @param string operator diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/services/gridExtra.service.ts b/aurelia-slickgrid/src/aurelia-slickgrid/services/gridExtra.service.ts index 93dc2c3fc..f7804d95a 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/services/gridExtra.service.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/services/gridExtra.service.ts @@ -1,7 +1,7 @@ import { Column, GridOption } from './../models/index'; import * as $ from 'jquery'; -// using external js modules +// using external non-typed js libraries declare var Slick: any; export class GridExtraService { diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/services/gridState.service.ts b/aurelia-slickgrid/src/aurelia-slickgrid/services/gridState.service.ts new file mode 100644 index 000000000..257cff9b3 --- /dev/null +++ b/aurelia-slickgrid/src/aurelia-slickgrid/services/gridState.service.ts @@ -0,0 +1,95 @@ +import { + CurrentFilter, + CurrentPagination, + CurrentSorter, + GridOption, + GridState +} from './../models/index'; +import { FilterService, SortService } from './../services/index'; +import * as $ from 'jquery'; + +export class GridStateService { + private _grid: any; + private _gridOptions: GridOption; + private _preset: GridState; + private filterService: FilterService; + private sortService: SortService; + + /** + * Initialize the Export Service + * @param grid + * @param gridOptions + * @param dataView + */ + init(grid: any, filterService: FilterService, sortService: SortService): void { + this._grid = grid; + this.filterService = filterService; + this.sortService = sortService; + this._gridOptions = (grid && grid.getOptions) ? grid.getOptions() : {}; + } + + /** + * Get the current grid state (filters/sorters/pagination) + * @return grid state + */ + getCurrentGridState(): GridState { + const gridState: GridState = { + filters: this.getCurrentFilters(), + sorters: this.getCurrentSorters() + }; + + const currentPagination = this.getCurrentPagination(); + if (currentPagination) { + gridState.pagination = currentPagination; + } + return gridState; + } + + /** + * Get the Filters (and their state, columnId, searchTerm(s)) that are currently applied in the grid + * @return current filters + */ + getCurrentFilters(): CurrentFilter[] { + if (this._gridOptions && this._gridOptions.backendServiceApi) { + const backendService = this._gridOptions.backendServiceApi.service; + if (backendService) { + return backendService.getCurrentFilters() as CurrentFilter[]; + } + } else { + return this.filterService.getCurrentLocalFilters(); + } + return null; + } + + /** + * Get current Pagination (and it's state, pageNumber, pageSize) that are currently applied in the grid + * @return current pagination state + */ + getCurrentPagination(): CurrentPagination { + if (this._gridOptions && this._gridOptions.backendServiceApi) { + const backendService = this._gridOptions.backendServiceApi.service; + if (backendService) { + return backendService.getCurrentPagination(); + } + } else { + // TODO implement this whenever local pagination gets implemented + } + return null; + } + + /** + * Get the current Sorters (and their state, columnId, direction) that are currently applied in the grid + * @return current sorters + */ + getCurrentSorters(): CurrentSorter[] { + if (this._gridOptions && this._gridOptions.backendServiceApi) { + const backendService = this._gridOptions.backendServiceApi.service; + if (backendService) { + return backendService.getCurrentSorters() as CurrentSorter[]; + } + } else { + return this.sortService.getCurrentLocalSorters(); + } + return null; + } +} diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/services/index.ts b/aurelia-slickgrid/src/aurelia-slickgrid/services/index.ts index be1ba658a..df7b159f7 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/services/index.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/services/index.ts @@ -5,6 +5,7 @@ export * from './graphql.service'; export * from './gridEvent.service'; export * from './gridExtra.service'; export * from './gridExtraUtils'; +export * from './gridState.service'; export * from './grid-odata.service'; export * from './odata.service'; export * from './resizer.service'; diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/services/sort.service.ts b/aurelia-slickgrid/src/aurelia-slickgrid/services/sort.service.ts index bb48763db..c99ac9c7d 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/services/sort.service.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/services/sort.service.ts @@ -1,9 +1,14 @@ import { EventAggregator } from 'aurelia-event-aggregator'; -import { FieldType, GridOption, SlickEvent } from './../models/index'; +import { Column, FieldType, GridOption, SlickEvent, SortChanged, SortDirection, CurrentSorter, CellArgs, SortDirectionString } from './../models/index'; import { Sorters } from './../sorters/index'; +// using external non-typed js libraries +declare var Slick: any; + export class SortService { - private _subscriber: SlickEvent; + private _currentLocalSorters: CurrentSorter[] = []; + private _eventHandler: any = new Slick.EventHandler(); + private _subscriber: SlickEvent = new Slick.Event(); onSortChanged = new EventAggregator(); /** @@ -14,6 +19,8 @@ export class SortService { attachBackendOnSort(grid: any, gridOptions: GridOption) { this._subscriber = grid.onSort; this.emitSortChangedBy('remote'); + + this._subscriber = new Slick.Event(); this._subscriber.subscribe(this.attachBackendOnSortSubscribe); } @@ -52,54 +59,122 @@ export class SortService { * @param gridOptions Grid Options object * @param dataView */ - attachLocalOnSort(grid: any, gridOptions: GridOption, dataView: any) { + attachLocalOnSort(grid: any, gridOptions: GridOption, dataView: any, columnDefinitions: Column[]) { this._subscriber = grid.onSort; this.emitSortChangedBy('local'); + + this._subscriber = new Slick.Event(); this._subscriber.subscribe((e: any, args: any) => { // multiSort and singleSort are not exactly the same, but we want to structure it the same for the (for loop) after // also to avoid having to rewrite the for loop in the sort, we will make the singleSort an array of 1 object const sortColumns = (args.multiColumnSort) ? args.sortCols : new Array({ sortAsc: args.sortAsc, sortCol: args.sortCol }); - dataView.sort((dataRow1: any, dataRow2: any) => { - for (let i = 0, l = sortColumns.length; i < l; i++) { - const columnSortObj = sortColumns[i]; - const sortDirection = columnSortObj.sortAsc ? 1 : -1; - const sortField = columnSortObj.sortCol.queryField || columnSortObj.sortCol.field; - const fieldType = columnSortObj.sortCol.type || 'string'; - const value1 = dataRow1[sortField]; - const value2 = dataRow2[sortField]; - let result = 0; - - switch (fieldType) { - case FieldType.number: - result = Sorters.numeric(value1, value2, sortDirection); - break; - case FieldType.date: - result = Sorters.date(value1, value2, sortDirection); - break; - case FieldType.dateIso: - result = Sorters.dateIso(value1, value2, sortDirection); - break; - case FieldType.dateUs: - result = Sorters.dateUs(value1, value2, sortDirection); - break; - case FieldType.dateUsShort: - result = Sorters.dateUsShort(value1, value2, sortDirection); - break; - default: - result = Sorters.string(value1, value2, sortDirection); - break; + // keep current sorters + this._currentLocalSorters = []; // reset current local sorters + if (Array.isArray(sortColumns)) { + sortColumns.forEach((sortColumn) => { + if (sortColumn.sortCol) { + this._currentLocalSorters.push({ + columnId: sortColumn.sortCol.id, + direction: sortColumn.sortAsc ? SortDirection.ASC : SortDirection.DESC + }); } + }); + } - if (result !== 0) { - return result; - } + this.onLocalSortChanged(grid, gridOptions, dataView, sortColumns); + }); + + this._eventHandler.subscribe(dataView.onRowCountChanged, (e: Event, args: any) => { + // load any presets if there are any + if (args.current > 0) { + this.loadLocalPresets(grid, gridOptions, dataView, columnDefinitions); + } + }); + } + + getCurrentLocalSorters(): CurrentSorter[] { + return this._currentLocalSorters; + } + + /** + * load any presets if there are any + * @param grid + * @param gridOptions + * @param dataView + * @param columnDefinitions + */ + loadLocalPresets(grid: any, gridOptions: GridOption, dataView: any, columnDefinitions: Column[]) { + const sortCols: SortChanged[] = []; + this._currentLocalSorters = []; // reset current local sorters + if (gridOptions && gridOptions.presets && gridOptions.presets.sorters) { + const sorters = gridOptions.presets.sorters; + columnDefinitions.forEach((columnDef: Column) => { + const columnPreset = sorters.find((currentSorter: CurrentSorter) => { + return currentSorter.columnId === columnDef.id; + }); + if (columnPreset) { + sortCols.push({ + columnId: columnDef.id, + sortAsc: ((columnPreset.direction.toUpperCase() === SortDirection.ASC) ? true : false), + sortCol: columnDef + }); + + // keep current sorters + this._currentLocalSorters.push({ + columnId: columnDef.id + '', + direction: columnPreset.direction.toUpperCase() as SortDirectionString + }); } - return 0; }); - grid.invalidate(); - grid.render(); + + if (sortCols.length > 0) { + this.onLocalSortChanged(grid, gridOptions, dataView, sortCols); + grid.setSortColumns(sortCols); + } + } + } + + onLocalSortChanged(grid: any, gridOptions: GridOption, dataView: any, sortColumns: SortChanged[]) { + dataView.sort((dataRow1: any, dataRow2: any) => { + for (let i = 0, l = sortColumns.length; i < l; i++) { + const columnSortObj = sortColumns[i]; + const sortDirection = columnSortObj.sortAsc ? 1 : -1; + const sortField = columnSortObj.sortCol.queryField || columnSortObj.sortCol.field; + const fieldType = columnSortObj.sortCol.type || 'string'; + const value1 = dataRow1[sortField]; + const value2 = dataRow2[sortField]; + let result = 0; + + switch (fieldType) { + case FieldType.number: + result = Sorters.numeric(value1, value2, sortDirection); + break; + case FieldType.date: + result = Sorters.date(value1, value2, sortDirection); + break; + case FieldType.dateIso: + result = Sorters.dateIso(value1, value2, sortDirection); + break; + case FieldType.dateUs: + result = Sorters.dateUs(value1, value2, sortDirection); + break; + case FieldType.dateUsShort: + result = Sorters.dateUsShort(value1, value2, sortDirection); + break; + default: + result = Sorters.string(value1, value2, sortDirection); + break; + } + + if (result !== 0) { + return result; + } + } + return 0; }); + grid.invalidate(); + grid.render(); } dispose() { @@ -107,6 +182,9 @@ export class SortService { if (this._subscriber && typeof this._subscriber.unsubscribe === 'function') { this._subscriber.unsubscribe(); } + + // unsubscribe all SlickGrid events + this._eventHandler.unsubscribeAll(); } /** diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/services/utilities.ts b/aurelia-slickgrid/src/aurelia-slickgrid/services/utilities.ts index f127bab23..284078b8b 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/services/utilities.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/services/utilities.ts @@ -1,4 +1,4 @@ -import { FieldType, OperatorType } from '../models/index'; +import { FieldType, OperatorType, FilterType, FormElementType } from '../models/index'; import * as moment from 'moment'; /** @@ -164,12 +164,13 @@ export function mapFlatpickrDateFormatWithFieldType(fieldType: FieldType): strin } /** - * Mapper for mathematical operators (ex.: <= is "le", > is "gt") + * Mapper for query operators (ex.: <= is "le", > is "gt") * @param string operator * @returns string map */ export function mapOperatorType(operator: string): OperatorType { let map: OperatorType; + switch (operator) { case '<': map = OperatorType.lessThan; @@ -221,6 +222,30 @@ export function mapOperatorType(operator: string): OperatorType { return map; } +/** + * Mapper for query operator by a Filter Type + * For example a multiple-select typically uses 'IN' operator + * @param operator + * @returns string map + */ +export function mapOperatorByFilterType(filterType: FilterType | FormElementType | string): OperatorType { + let map: OperatorType; + + switch (filterType) { + case FilterType.multipleSelect: + map = OperatorType.in; + break; + case FilterType.singleSelect: + map = OperatorType.equal; + break; + default: + map = OperatorType.contains; + break; + } + + return map; +} + /** * Parse a date passed as a string and return a Date object (if valid) * @param string inputDateString diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/slick-pagination.ts b/aurelia-slickgrid/src/aurelia-slickgrid/slick-pagination.ts index af004fffe..c8fd16896 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/slick-pagination.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/slick-pagination.ts @@ -101,7 +101,7 @@ export class SlickPaginationCustomElement { this.onPageChanged(event, this.pageNumber); } - refreshPagination(isPageNumberReset?: boolean) { + refreshPagination(isPageNumberReset: boolean = false) { const backendApi = this._gridPaginationOptions.backendServiceApi || this._gridPaginationOptions.onBackendEventApi; if (!backendApi || !backendApi.service || !backendApi.process) { throw new Error(`BackendServiceApi requires at least a "process" function and a "service" defined`); @@ -115,8 +115,11 @@ export class SlickPaginationCustomElement { // if totalItems changed, we should always go back to the first page and recalculation the From-To indexes if (isPageNumberReset || this.totalItems !== this._gridPaginationOptions.pagination.totalItems) { - this.pageNumber = 1; - this.recalculateFromToIndexes(); + if (this._isFirstRender && this._gridPaginationOptions.pagination.pageNumber > 1) { + this.pageNumber = this._gridPaginationOptions.pagination.pageNumber || 1; + } else { + this.pageNumber = 1; + } // also reset the "offset" of backend service backendApi.service.resetPaginationOptions(); @@ -125,7 +128,7 @@ export class SlickPaginationCustomElement { // calculate and refresh the multiple properties of the pagination UI this.paginationPageSizes = this._gridPaginationOptions.pagination.pageSizes; this.totalItems = this._gridPaginationOptions.pagination.totalItems; - this.dataTo = (this.totalItems < this.itemsPerPage) ? this.totalItems : this.itemsPerPage; + this.recalculateFromToIndexes(); } this.pageCount = Math.ceil(this.totalItems / this.itemsPerPage); } @@ -165,12 +168,12 @@ export class SlickPaginationCustomElement { backendApi.postProcess(processResult); } } else { - throw new Error('Pagination with a backend service requires "onBackendEventApi" to be defined in your grid options'); + throw new Error('Pagination with a backend service requires "BackendServiceApi" to be defined in your grid options'); } } recalculateFromToIndexes() { this.dataFrom = (this.pageNumber * this.itemsPerPage) - this.itemsPerPage + 1; - this.dataTo = (this.pageNumber * this.itemsPerPage); + this.dataTo = (this.totalItems < this.itemsPerPage) ? this.totalItems : (this.pageNumber * this.itemsPerPage); } } diff --git a/aurelia-slickgrid/src/examples/slickgrid/custom-inputFilter.ts b/aurelia-slickgrid/src/examples/slickgrid/custom-inputFilter.ts index 679978344..9ab3f7ae0 100644 --- a/aurelia-slickgrid/src/examples/slickgrid/custom-inputFilter.ts +++ b/aurelia-slickgrid/src/examples/slickgrid/custom-inputFilter.ts @@ -48,6 +48,15 @@ export class CustomInputFilter implements Filter { } } + /** + * Set value(s) on the DOM element + */ + setValues(values) { + if (values) { + this.$filterElm.val(values); + } + } + // // private functions // ------------------ diff --git a/aurelia-slickgrid/src/examples/slickgrid/example3.html b/aurelia-slickgrid/src/examples/slickgrid/example3.html index 5a36318dd..5a81cd3e3 100644 --- a/aurelia-slickgrid/src/examples/slickgrid/example3.html +++ b/aurelia-slickgrid/src/examples/slickgrid/example3.html @@ -15,7 +15,7 @@

${title}

diff --git a/aurelia-slickgrid/src/examples/slickgrid/example4.ts b/aurelia-slickgrid/src/examples/slickgrid/example4.ts index b6bb4f05f..d6e077d63 100644 --- a/aurelia-slickgrid/src/examples/slickgrid/example4.ts +++ b/aurelia-slickgrid/src/examples/slickgrid/example4.ts @@ -1,11 +1,13 @@ +import { autoinject } from 'aurelia-framework'; import { CustomInputFilter } from './custom-inputFilter'; -import { Column, FieldType, FilterType, Formatter, Formatters, GridOption } from '../../aurelia-slickgrid'; +import { Column, FieldType, FilterType, Formatter, Formatters, GridOption, GridStateService } from '../../aurelia-slickgrid'; function randomBetween(min, max) { return Math.floor(Math.random() * (max - min + 1) + min); } const NB_ITEMS = 500; +@autoinject() export class Example4 { title = 'Example 4: Client Side Sort/Filter'; subTitle = ` @@ -31,7 +33,7 @@ export class Example4 { gridOptions: GridOption; dataset: any[]; - constructor() { + constructor(private gridStateService: GridStateService) { this.defineGrid(); } @@ -40,6 +42,10 @@ export class Example4 { this.getData(); } + detached() { + this.saveCurrentGridState(); + } + /* Define grid Options and Columns */ defineGrid() { // prepare a multiple-select array to filter with @@ -51,7 +57,7 @@ export class Example4 { this.columnDefinitions = [ { id: 'title', name: 'Title', field: 'title', filterable: true, sortable: true, type: FieldType.string, minWidth: 45 }, { - id: 'description', name: 'Description', field: 'description', filterable: true, sortable: true, minWidth: 50, + id: 'description', name: 'Description', field: 'description', filterable: true, sortable: true, minWidth: 80, type: FieldType.string, filter: { type: FilterType.custom, @@ -59,14 +65,13 @@ export class Example4 { } }, { - id: 'duration', name: 'Duration (days)', field: 'duration', sortable: true, type: FieldType.number, + id: 'duration', name: 'Duration (days)', field: 'duration', sortable: true, type: FieldType.number, exportCsvForceToKeepAsString: true, minWidth: 55, filterable: true, filter: { collection: multiSelectFilterArray, type: FilterType.multipleSelect, - searchTerms: [1, 10, 20], // default selection - + searchTerms: [1, 33, 50], // default selection // we could add certain option(s) to the "multiple-select" plugin filterOptions: { maxHeight: 250, @@ -74,8 +79,8 @@ export class Example4 { } } }, - { id: 'complete', name: '% Complete', field: 'percentComplete', formatter: Formatters.percentCompleteBar, minWidth: 55, type: FieldType.number, filterable: true, sortable: true }, - { id: 'start', name: 'Start', field: 'start', formatter: Formatters.dateIso, filterable: true, sortable: true, type: FieldType.date, minWidth: 60 }, + { id: 'complete', name: '% Complete', field: 'percentComplete', formatter: Formatters.percentCompleteBar, minWidth: 70, type: FieldType.number, filterable: true, sortable: true }, + { id: 'start', name: 'Start', field: 'start', formatter: Formatters.dateIso, filterable: true, sortable: true, type: FieldType.date, minWidth: 60, exportWithFormatter: true }, { id: 'usDateShort', name: 'US Date Short', field: 'usDateShort', filterable: true, sortable: true, type: FieldType.dateUsShort, minWidth: 55 }, { id: 'utcDate', name: 'UTC Date', field: 'utcDate', formatter: Formatters.dateTimeIsoAmPm, filterable: true, sortable: true, minWidth: 115, type: FieldType.dateUtc, filterSearchType: FieldType.dateTimeIso }, { id: 'utcDate2', name: 'UTC Date (filterSearchType: dateUS)', field: 'utcDate', filterable: true, sortable: true, minWidth: 115, type: FieldType.dateUtc, filterSearchType: FieldType.dateUs }, @@ -95,12 +100,26 @@ export class Example4 { } } ]; + this.gridOptions = { autoResize: { containerId: 'demo-container', sidePadding: 15 }, - enableFiltering: true + enableFiltering: true, + + // use columnDef searchTerms OR use presets as shown below + presets: { + filters: [ + { columnId: 'duration', searchTerms: [2, 22, 44] }, + { columnId: 'complete', searchTerm: '>5' }, + { columnId: 'effort-driven', searchTerm: true } + ], + sorters: [ + { columnId: 'duration', direction: 'DESC' }, + { columnId: 'complete', direction: 'ASC' } + ], + } }; } @@ -121,7 +140,7 @@ export class Example4 { this.dataset[i] = { id: i, title: 'Task ' + i, - description: (i % 28 === 1) ? null : 'desc ' + i, // also add some random to test NULL field + description: (i % 5) ? 'desc ' + i : null, // also add some random to test NULL field duration: randomDuration, percentComplete: randomPercent, percentCompleteNumber: randomPercent, @@ -132,4 +151,9 @@ export class Example4 { }; } } + + /** Save current Filters, Sorters in LocaleStorage or DB */ + saveCurrentGridState() { + console.log('Client current grid state', this.gridStateService.getCurrentGridState()); + } } diff --git a/aurelia-slickgrid/src/examples/slickgrid/example5.ts b/aurelia-slickgrid/src/examples/slickgrid/example5.ts index 5e4982ed0..f4293d44d 100644 --- a/aurelia-slickgrid/src/examples/slickgrid/example5.ts +++ b/aurelia-slickgrid/src/examples/slickgrid/example5.ts @@ -1,7 +1,7 @@ import { autoinject } from 'aurelia-framework'; import data from './sample-data/example-data'; import { HttpClient } from 'aurelia-http-client'; -import { CaseType, Column, GridOption, FieldType, Formatters, FormElementType, GridOdataService } from '../../aurelia-slickgrid'; +import { CaseType, Column, GridOption, FieldType, FilterType, Formatters, FormElementType, GridOdataService, GridStateService, OperatorType } from '../../aurelia-slickgrid'; const defaultPageSize = 20; const sampleDataRoot = 'src/examples/slickgrid/sample-data'; @@ -20,6 +20,8 @@ export class Example5 {
  • The (*) can be used as startsWith (ex.: "abc*" => startsWith "abc") / endsWith (ex.: "*xyz" => endsWith "xyz")
  • The other operators can be used on column type number for example: ">=100" (greater than or equal to 100)
  • +
  • OData Service could be replaced by other Service type in the future (GraphQL or whichever you provide)
  • +
  • You can also preload a grid with certain "presets" like Filters / Sorters / Pagination Wiki - Grid Preset `; columnDefinitions: Column[]; @@ -30,27 +32,27 @@ export class Example5 { processing = false; status = { text: '', class: '' }; - constructor(private http: HttpClient, private odataService: GridOdataService) { + constructor(private gridStateService: GridStateService, private http: HttpClient, private odataService: GridOdataService) { // define the grid options & columns and then create the grid itself this.defineGrid(); } - attached() { - // populate the dataset once the grid is ready - // this.getData(); + detached() { + this.saveCurrentGridState(); } defineGrid() { this.columnDefinitions = [ - { id: 'name', name: 'Name', field: 'name', filterable: true, sortable: true, type: FieldType.string, minWidth: 100 }, + { id: 'name', name: 'Name', field: 'name', filterable: true, sortable: true, type: FieldType.string }, { - id: 'gender', name: 'Gender', field: 'gender', filterable: true, sortable: true, minWidth: 100, + id: 'gender', name: 'Gender', field: 'gender', filterable: true, sortable: true, filter: { + type: FilterType.singleSelect, collection: [{ value: '', label: '' }, { value: 'male', label: 'male' }, { value: 'female', label: 'female' }], - type: FormElementType.singleSelect + searchTerm: 'female' } }, - { id: 'company', name: 'Company', field: 'company', minWidth: 100 } + { id: 'company', name: 'Company', field: 'company' } ]; this.gridOptions = { @@ -66,12 +68,17 @@ export class Example5 { pageSize: defaultPageSize, totalItems: 0 }, + presets: { + // you can also type operator as string, e.g.: operator: 'EQ' + filters: [{ columnId: 'gender', searchTerm: 'male', operator: OperatorType.equal }], + sorters: [{ columnId: 'name', direction: 'desc' }], + pagination: { pageNumber: 2, pageSize: 20 } + }, backendServiceApi: { service: this.odataService, preProcess: () => this.displaySpinner(true), process: (query) => this.getCustomerApiCall(query), postProcess: (response) => { - console.log(response); this.displaySpinner(false); this.getCustomerCallback(response); } @@ -102,6 +109,10 @@ export class Example5 { return this.getCustomerDataApiMock(query); } + saveCurrentGridState() { + console.log('OData current grid state', this.gridStateService.getCurrentGridState()); + } + /** * This function is only here to mock a WebAPI call (since we are using a JSON file for the demo) * in your case the getCustomer() should be a WebAPI function returning a Promise diff --git a/aurelia-slickgrid/src/examples/slickgrid/example6.ts b/aurelia-slickgrid/src/examples/slickgrid/example6.ts index b37121d96..7aa8a614d 100644 --- a/aurelia-slickgrid/src/examples/slickgrid/example6.ts +++ b/aurelia-slickgrid/src/examples/slickgrid/example6.ts @@ -1,8 +1,7 @@ import { autoinject } from 'aurelia-framework'; -import data from './sample-data/example-data'; import { I18N } from 'aurelia-i18n'; import { HttpClient } from 'aurelia-http-client'; -import { Column, FieldType, FilterType, GraphqlResult, GraphqlService, GraphqlServiceOption, GridOption } from '../../aurelia-slickgrid'; +import { Column, FieldType, FilterType, GraphqlResult, GraphqlService, GraphqlServiceOption, GridOption, GridStateService, OperatorType, SortDirection } from '../../aurelia-slickgrid'; const defaultPageSize = 20; const GRAPHQL_QUERY_DATASET_NAME = 'users'; @@ -21,6 +20,7 @@ export class Example6 {
  • The (*) can be used as startsWith (ex.: "abc*" => startsWith "abc") / endsWith (ex.: "*xyz" => endsWith "xyz")
  • The other operators can be used on column type number for example: ">=100" (greater or equal than 100)
  • +
  • You can also preload a grid with certain "presets" like Filters / Sorters / Pagination Wiki - Grid Preset `; columnDefinitions: Column[]; @@ -33,12 +33,16 @@ export class Example6 { selectedLanguage: string; status = { text: '', class: '' }; - constructor(private http: HttpClient, private graphqlService: GraphqlService, private i18n: I18N) { + constructor(private gridStateService: GridStateService, private http: HttpClient, private graphqlService: GraphqlService, private i18n: I18N) { // define the grid options & columns and then create the grid itself this.defineGrid(); this.selectedLanguage = this.i18n.getLocale(); } + detached() { + this.saveCurrentGridState(); + } + defineGrid() { this.columnDefinitions = [ { id: 'name', name: 'Name', field: 'name', headerKey: 'NAME', filterable: true, sortable: true, type: FieldType.string }, @@ -46,15 +50,18 @@ export class Example6 { id: 'gender', name: 'Gender', field: 'gender', headerKey: 'GENDER', filterable: true, sortable: true, filter: { type: FilterType.singleSelect, - collection: [{ value: '', label: '' }, { value: 'male', label: 'male', labelKey: 'MALE' }, { value: 'female', label: 'female', labelKey: 'FEMALE' }] + collection: [{ value: '', label: '' }, { value: 'male', label: 'male', labelKey: 'MALE' }, { value: 'female', label: 'female', labelKey: 'FEMALE' }], + searchTerm: 'female' } }, { id: 'company', name: 'Company', field: 'company', headerKey: 'COMPANY', + sortable: true, filterable: true, filter: { type: FilterType.multipleSelect, - collection: [{ value: 'ABC', label: 'Company ABC' }, { value: 'XYZ', label: 'Company XYZ' }] + collection: [{ value: 'acme', label: 'Acme' }, { value: 'abc', label: 'Company ABC' }, { value: 'xyz', label: 'Company XYZ' }], + searchTerms: ['abc'] } }, { id: 'billing.address.street', name: 'Billing Address Street', field: 'billing.address.street', headerKey: 'BILLING.ADDRESS.STREET', filterable: true, sortable: true }, @@ -62,11 +69,7 @@ export class Example6 { ]; this.gridOptions = { - enableAutoResize: true, - autoResize: { - containerId: 'demo-container', - sidePadding: 15 - }, + enableAutoResize: false, enableFiltering: true, enableCellNavigation: true, enableTranslate: true, @@ -75,6 +78,20 @@ export class Example6 { pageSize: defaultPageSize, totalItems: 0 }, + presets: { + // you can also type operator as string, e.g.: operator: 'EQ' + filters: [ + { columnId: 'gender', searchTerm: 'male', operator: OperatorType.equal }, + { columnId: 'name', searchTerm: 'John Doe', operator: OperatorType.contains }, + { columnId: 'company', searchTerms: ['xyz'], operator: 'IN' } + ], + sorters: [ + // direction can typed as 'asc' (uppercase or lowercase) and/or use the SortDirection type + { columnId: 'name', direction: 'asc' }, + { columnId: 'company', direction: SortDirection.DESC } + ], + pagination: { pageNumber: 2, pageSize: 20 } + }, backendServiceApi: { service: this.graphqlService, options: this.getBackendOptions(this.isWithCursor), @@ -140,6 +157,10 @@ export class Example6 { }); } + saveCurrentGridState() { + console.log('GraphQL current grid state', this.gridStateService.getCurrentGridState()); + } + switchLanguage() { this.selectedLanguage = (this.selectedLanguage === 'en') ? 'fr' : 'en'; this.i18n.setLocale(this.selectedLanguage); diff --git a/client-cli/src/examples/slickgrid/example3.html b/client-cli/src/examples/slickgrid/example3.html index 5a36318dd..79be09059 100644 --- a/client-cli/src/examples/slickgrid/example3.html +++ b/client-cli/src/examples/slickgrid/example3.html @@ -15,7 +15,7 @@

    ${title}

    @@ -37,4 +37,4 @@

    ${title}

    grid-options.bind="gridOptions" dataset.bind="dataset"> - + \ No newline at end of file diff --git a/dist/es2015/models/columnFilter.interface.d.ts b/dist/es2015/models/columnFilter.interface.d.ts index db34a1afd..d24d7de28 100644 --- a/dist/es2015/models/columnFilter.interface.d.ts +++ b/dist/es2015/models/columnFilter.interface.d.ts @@ -3,39 +3,52 @@ import { Filter } from './filter.interface'; import { FilterType } from './filterType.enum'; import { FormElementType } from './formElementType'; export interface ColumnFilter { - /** Do we want to bypass the Backend Query? Commonly used with an OData Backend Service, if we want to filter without calling the regular OData query. */ - bypassBackendQuery?: boolean; - /** Column ID */ - columnId?: string; - /** Column Definition */ - columnDef?: Column; - /** Custom Filter */ - customFilter?: Filter; - /** Search term (singular) */ - searchTerm?: string | number | boolean; - /** Search terms (collection) */ - searchTerms?: string[] | number[] | boolean[]; - /** Operator to use when filtering (>, >=, EQ, IN, ...) */ - operator?: string; - /** Filter Type to use (input, multipleSelect, singleSelect, select, custom) */ - type?: FilterType | FormElementType | string; - /** A collection of items/options (commonly used with a Select/Multi-Select Filter) */ - collection?: any[]; - /** Options that could be provided to the Filter, example: { container: 'body', maxHeight: 250} */ - filterOptions?: any; - /** DEPRECATED, please use "collection" instead which is more generic and not specific to a Select Filter. Refer to the Select Filter Wiki page for more info: https://github.com/ghiscoding/Angular-Slickgrid/wiki/Select-Filter */ - selectOptions?: any[]; - /** Do we want the Filter to handle translation (localization)? */ - enableTranslateLabel?: boolean; - /** A custom structure can be used instead of the default label/value pair. Commonly used with Select/Multi-Select Filter */ - customStructure?: { - label: string; - value: string; - }; - /** - * Use "params" to pass any type of arguments to your Custom Filter (type: FilterType.custom) - * for example, to pass the option collection to a select Filter we can type this: - * params: { options: [{ value: true, label: 'True' }, { value: true, label: 'True'} ]} - */ - params?: any; + /** Do we want to bypass the Backend Query? Commonly used with an OData Backend Service, if we want to filter without calling the regular OData query. */ + bypassBackendQuery?: boolean; + + /** Column ID */ + columnId?: string; + + /** Column Definition */ + columnDef?: Column; + + /** Custom Filter */ + customFilter?: Filter; + + /** Search term (singular) */ + searchTerm?: string | number | boolean; + + /** Search terms (collection) */ + searchTerms?: string[] | number[] | boolean[]; + + /** Operator to use when filtering (>, >=, EQ, IN, ...) */ + operator?: OperatorType; + + /** Filter Type to use (input, multipleSelect, singleSelect, select, custom) */ + type?: FilterType | FormElementType | string; + + /** A collection of items/options (commonly used with a Select/Multi-Select Filter) */ + collection?: any[]; + + /** Options that could be provided to the Filter, example: { container: 'body', maxHeight: 250} */ + filterOptions?: any; + + /** DEPRECATED, please use "collection" instead which is more generic and not specific to a Select Filter. Refer to the Select Filter Wiki page for more info: https://github.com/ghiscoding/Angular-Slickgrid/wiki/Select-Filter */ + selectOptions?: any[]; + + /** Do we want the Filter to handle translation (localization)? */ + enableTranslateLabel?: boolean; + + /** A custom structure can be used instead of the default label/value pair. Commonly used with Select/Multi-Select Filter */ + customStructure?: { + label: string; + value: string; + }; + + /** + * Use "params" to pass any type of arguments to your Custom Filter (type: FilterType.custom) + * for example, to pass the option collection to a select Filter we can type this: + * params: { options: [{ value: true, label: 'True' }, { value: true, label: 'True'} ]} + */ + params?: any; } diff --git a/doc/github-demo/src/examples/slickgrid/example3.html b/doc/github-demo/src/examples/slickgrid/example3.html index 5a36318dd..79be09059 100644 --- a/doc/github-demo/src/examples/slickgrid/example3.html +++ b/doc/github-demo/src/examples/slickgrid/example3.html @@ -15,7 +15,7 @@

    ${title}

    @@ -37,4 +37,4 @@

    ${title}

    grid-options.bind="gridOptions" dataset.bind="dataset"> - + \ No newline at end of file