]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | import { |
2 | AfterContentChecked, | |
81eedcae | 3 | ChangeDetectionStrategy, |
11fdf7f2 TL |
4 | ChangeDetectorRef, |
5 | Component, | |
6 | EventEmitter, | |
7 | Input, | |
8 | NgZone, | |
9 | OnChanges, | |
10 | OnDestroy, | |
11 | OnInit, | |
12 | Output, | |
9f95a23c | 13 | PipeTransform, |
11fdf7f2 TL |
14 | TemplateRef, |
15 | ViewChild | |
16 | } from '@angular/core'; | |
17 | ||
18 | import { | |
19 | DatatableComponent, | |
20 | SortDirection, | |
21 | SortPropDir, | |
22 | TableColumnProp | |
23 | } from '@swimlane/ngx-datatable'; | |
9f95a23c | 24 | import { getterForProp } from '@swimlane/ngx-datatable/release/utils'; |
11fdf7f2 | 25 | import * as _ from 'lodash'; |
9f95a23c | 26 | import { Observable, Subject, Subscription, timer as observableTimer } from 'rxjs'; |
11fdf7f2 | 27 | |
9f95a23c | 28 | import { Icons } from '../../../shared/enum/icons.enum'; |
11fdf7f2 TL |
29 | import { CellTemplate } from '../../enum/cell-template.enum'; |
30 | import { CdTableColumn } from '../../models/cd-table-column'; | |
9f95a23c TL |
31 | import { CdTableColumnFilter } from '../../models/cd-table-column-filter'; |
32 | import { CdTableColumnFiltersChange } from '../../models/cd-table-column-filters-change'; | |
11fdf7f2 TL |
33 | import { CdTableFetchDataContext } from '../../models/cd-table-fetch-data-context'; |
34 | import { CdTableSelection } from '../../models/cd-table-selection'; | |
35 | import { CdUserConfig } from '../../models/cd-user-config'; | |
36 | ||
37 | @Component({ | |
38 | selector: 'cd-table', | |
39 | templateUrl: './table.component.html', | |
81eedcae TL |
40 | styleUrls: ['./table.component.scss'], |
41 | changeDetection: ChangeDetectionStrategy.OnPush | |
11fdf7f2 TL |
42 | }) |
43 | export class TableComponent implements AfterContentChecked, OnInit, OnChanges, OnDestroy { | |
9f95a23c | 44 | @ViewChild(DatatableComponent, { static: true }) |
11fdf7f2 | 45 | table: DatatableComponent; |
9f95a23c | 46 | @ViewChild('tableCellBoldTpl', { static: true }) |
11fdf7f2 | 47 | tableCellBoldTpl: TemplateRef<any>; |
9f95a23c | 48 | @ViewChild('sparklineTpl', { static: true }) |
11fdf7f2 | 49 | sparklineTpl: TemplateRef<any>; |
9f95a23c | 50 | @ViewChild('routerLinkTpl', { static: true }) |
11fdf7f2 | 51 | routerLinkTpl: TemplateRef<any>; |
9f95a23c | 52 | @ViewChild('checkIconTpl', { static: true }) |
11fdf7f2 | 53 | checkIconTpl: TemplateRef<any>; |
9f95a23c | 54 | @ViewChild('perSecondTpl', { static: true }) |
11fdf7f2 | 55 | perSecondTpl: TemplateRef<any>; |
9f95a23c | 56 | @ViewChild('executingTpl', { static: true }) |
11fdf7f2 | 57 | executingTpl: TemplateRef<any>; |
9f95a23c | 58 | @ViewChild('classAddingTpl', { static: true }) |
11fdf7f2 | 59 | classAddingTpl: TemplateRef<any>; |
9f95a23c TL |
60 | @ViewChild('badgeTpl', { static: true }) |
61 | badgeTpl: TemplateRef<any>; | |
62 | @ViewChild('mapTpl', { static: true }) | |
63 | mapTpl: TemplateRef<any>; | |
11fdf7f2 TL |
64 | |
65 | // This is the array with the items to be shown. | |
66 | @Input() | |
67 | data: any[]; | |
68 | // Each item -> { prop: 'attribute name', name: 'display name' } | |
69 | @Input() | |
70 | columns: CdTableColumn[]; | |
71 | // Each item -> { prop: 'attribute name', dir: 'asc'||'desc'} | |
72 | @Input() | |
73 | sorts?: SortPropDir[]; | |
74 | // Method used for setting column widths. | |
75 | @Input() | |
76 | columnMode? = 'flex'; | |
9f95a23c TL |
77 | // Display only actions in header (make sure to disable toolHeader) and use ".only-table-actions" |
78 | @Input() | |
79 | onlyActionHeader? = false; | |
11fdf7f2 TL |
80 | // Display the tool header, including reload button, pagination and search fields? |
81 | @Input() | |
82 | toolHeader? = true; | |
9f95a23c TL |
83 | // Display search field inside tool header? |
84 | @Input() | |
85 | searchField? = true; | |
11fdf7f2 TL |
86 | // Display the table header? |
87 | @Input() | |
88 | header? = true; | |
89 | // Display the table footer? | |
90 | @Input() | |
91 | footer? = true; | |
92 | // Page size to show. Set to 0 to show unlimited number of rows. | |
93 | @Input() | |
94 | limit? = 10; | |
95 | ||
96 | /** | |
97 | * Auto reload time in ms - per default every 5s | |
98 | * You can set it to 0, undefined or false to disable the auto reload feature in order to | |
99 | * trigger 'fetchData' if the reload button is clicked. | |
100 | */ | |
101 | @Input() | |
102 | autoReload: any = 5000; | |
103 | ||
104 | // Which row property is unique for a row. If the identifier is not specified in any | |
105 | // column, then the property name of the first column is used. Defaults to 'id'. | |
106 | @Input() | |
107 | identifier = 'id'; | |
108 | // If 'true', then the specified identifier is used anyway, although it is not specified | |
109 | // in any column. Defaults to 'false'. | |
110 | @Input() | |
111 | forceIdentifier = false; | |
112 | // Allows other components to specify which type of selection they want, | |
113 | // e.g. 'single' or 'multi'. | |
114 | @Input() | |
115 | selectionType: string = undefined; | |
116 | // By default selected item details will be updated on table refresh, if data has changed | |
117 | @Input() | |
118 | updateSelectionOnRefresh: 'always' | 'never' | 'onChange' = 'onChange'; | |
119 | ||
120 | @Input() | |
121 | autoSave = true; | |
122 | ||
9f95a23c TL |
123 | // Enable this in order to search through the JSON of any used object. |
124 | @Input() | |
125 | searchableObjects = false; | |
126 | ||
11fdf7f2 TL |
127 | // Only needed to set if the classAddingTpl is used |
128 | @Input() | |
9f95a23c TL |
129 | customCss?: { [css: string]: number | string | ((any: any) => boolean) }; |
130 | ||
131 | // Columns that aren't displayed but can be used as filters | |
132 | @Input() | |
133 | extraFilterableColumns: CdTableColumn[] = []; | |
11fdf7f2 TL |
134 | |
135 | /** | |
136 | * Should be a function to update the input data if undefined nothing will be triggered | |
137 | * | |
138 | * Sometimes it's useful to only define fetchData once. | |
139 | * Example: | |
140 | * Usage of multiple tables with data which is updated by the same function | |
141 | * What happens: | |
142 | * The function is triggered through one table and all tables will update | |
143 | */ | |
144 | @Output() | |
145 | fetchData = new EventEmitter(); | |
146 | ||
147 | /** | |
148 | * This should be defined if you need access to the selection object. | |
149 | * | |
150 | * Each time the table selection changes, this will be triggered and | |
151 | * the new selection object will be sent. | |
152 | * | |
153 | * @memberof TableComponent | |
154 | */ | |
155 | @Output() | |
156 | updateSelection = new EventEmitter(); | |
157 | ||
9f95a23c TL |
158 | /** |
159 | * This should be defined if you need access to the applied column filters. | |
160 | * | |
161 | * Each time the column filters changes, this will be triggered and | |
162 | * the column filters change event will be sent. | |
163 | * | |
164 | * @memberof TableComponent | |
165 | */ | |
166 | @Output() columnFiltersChanged = new EventEmitter<CdTableColumnFiltersChange>(); | |
167 | ||
11fdf7f2 TL |
168 | /** |
169 | * Use this variable to access the selected row(s). | |
170 | */ | |
171 | selection = new CdTableSelection(); | |
172 | ||
173 | tableColumns: CdTableColumn[]; | |
9f95a23c | 174 | icons = Icons; |
11fdf7f2 TL |
175 | cellTemplates: { |
176 | [key: string]: TemplateRef<any>; | |
177 | } = {}; | |
178 | search = ''; | |
9f95a23c | 179 | rows: any[] = []; |
11fdf7f2 TL |
180 | loadingIndicator = true; |
181 | loadingError = false; | |
182 | paginationClasses = { | |
9f95a23c TL |
183 | pagerLeftArrow: Icons.leftArrowDouble, |
184 | pagerRightArrow: Icons.rightArrowDouble, | |
185 | pagerPrevious: Icons.leftArrow, | |
186 | pagerNext: Icons.rightArrow | |
11fdf7f2 TL |
187 | }; |
188 | userConfig: CdUserConfig = {}; | |
189 | tableName: string; | |
190 | localStorage = window.localStorage; | |
9f95a23c TL |
191 | private saveSubscriber: Subscription; |
192 | private reloadSubscriber: Subscription; | |
11fdf7f2 TL |
193 | private updating = false; |
194 | ||
195 | // Internal variable to check if it is necessary to recalculate the | |
196 | // table columns after the browser window has been resized. | |
197 | private currentWidth: number; | |
198 | ||
9f95a23c TL |
199 | columnFilters: CdTableColumnFilter[] = []; |
200 | selectedFilter: CdTableColumnFilter; | |
201 | get columnFiltered(): boolean { | |
202 | return _.some(this.columnFilters, (filter) => { | |
203 | return filter.value !== undefined; | |
204 | }); | |
205 | } | |
206 | ||
11fdf7f2 TL |
207 | constructor(private ngZone: NgZone, private cdRef: ChangeDetectorRef) {} |
208 | ||
9f95a23c TL |
209 | static prepareSearch(search: string) { |
210 | search = search.toLowerCase().replace(/,/g, ''); | |
211 | if (search.match(/['"][^'"]+['"]/)) { | |
212 | search = search.replace(/['"][^'"]+['"]/g, (match: string) => { | |
213 | return match.replace(/(['"])([^'"]+)(['"])/g, '$2').replace(/ /g, '+'); | |
214 | }); | |
215 | } | |
216 | return search.split(' ').filter((word) => word); | |
217 | } | |
218 | ||
11fdf7f2 | 219 | ngOnInit() { |
81eedcae TL |
220 | // ngx-datatable triggers calculations each time mouse enters a row, |
221 | // this will prevent that. | |
9f95a23c | 222 | this.table.element.addEventListener('mouseenter', (e) => e.stopPropagation(), true); |
11fdf7f2 TL |
223 | this._addTemplates(); |
224 | if (!this.sorts) { | |
225 | // Check whether the specified identifier exists. | |
226 | const exists = _.findIndex(this.columns, ['prop', this.identifier]) !== -1; | |
227 | // Auto-build the sorting configuration. If the specified identifier doesn't exist, | |
228 | // then use the property of the first column. | |
229 | this.sorts = this.createSortingDefinition( | |
230 | exists ? this.identifier : this.columns[0].prop + '' | |
231 | ); | |
232 | // If the specified identifier doesn't exist and it is not forced to use it anyway, | |
233 | // then use the property of the first column. | |
234 | if (!exists && !this.forceIdentifier) { | |
235 | this.identifier = this.columns[0].prop + ''; | |
236 | } | |
237 | } | |
238 | this.initUserConfig(); | |
239 | this.columns.forEach((c) => { | |
240 | if (c.cellTransformation) { | |
241 | c.cellTemplate = this.cellTemplates[c.cellTransformation]; | |
242 | } | |
243 | if (!c.flexGrow) { | |
244 | c.flexGrow = c.prop + '' === this.identifier ? 1 : 2; | |
245 | } | |
246 | if (!c.resizeable) { | |
247 | c.resizeable = false; | |
248 | } | |
249 | }); | |
9f95a23c TL |
250 | |
251 | this.initCheckboxColumn(); | |
11fdf7f2 | 252 | this.filterHiddenColumns(); |
9f95a23c TL |
253 | this.initColumnFilters(); |
254 | this.updateColumnFilterOptions(); | |
11fdf7f2 TL |
255 | // Load the data table content every N ms or at least once. |
256 | // Force showing the loading indicator if there are subscribers to the fetchData | |
257 | // event. This is necessary because it has been set to False in useData() when | |
258 | // this method was triggered by ngOnChanges(). | |
259 | if (this.fetchData.observers.length > 0) { | |
260 | this.loadingIndicator = true; | |
261 | } | |
262 | if (_.isInteger(this.autoReload) && this.autoReload > 0) { | |
263 | this.ngZone.runOutsideAngular(() => { | |
264 | this.reloadSubscriber = observableTimer(0, this.autoReload).subscribe(() => { | |
265 | this.ngZone.run(() => { | |
266 | return this.reloadData(); | |
267 | }); | |
268 | }); | |
269 | }); | |
270 | } else { | |
271 | this.reloadData(); | |
272 | } | |
273 | } | |
274 | ||
275 | initUserConfig() { | |
276 | if (this.autoSave) { | |
277 | this.tableName = this._calculateUniqueTableName(this.columns); | |
278 | this._loadUserConfig(); | |
279 | this._initUserConfigAutoSave(); | |
280 | } | |
281 | if (!this.userConfig.limit) { | |
282 | this.userConfig.limit = this.limit; | |
283 | } | |
284 | if (!this.userConfig.sorts) { | |
285 | this.userConfig.sorts = this.sorts; | |
286 | } | |
287 | if (!this.userConfig.columns) { | |
288 | this.updateUserColumns(); | |
289 | } else { | |
290 | this.columns.forEach((c, i) => { | |
291 | c.isHidden = this.userConfig.columns[i].isHidden; | |
292 | }); | |
293 | } | |
294 | } | |
295 | ||
9f95a23c TL |
296 | _calculateUniqueTableName(columns: any[]) { |
297 | const stringToNumber = (s: string) => { | |
11fdf7f2 TL |
298 | if (!_.isString(s)) { |
299 | return 0; | |
300 | } | |
301 | let result = 0; | |
302 | for (let i = 0; i < s.length; i++) { | |
303 | result += s.charCodeAt(i) * i; | |
304 | } | |
305 | return result; | |
306 | }; | |
307 | return columns | |
308 | .reduce( | |
309 | (result, value, index) => | |
310 | (stringToNumber(value.prop) + stringToNumber(value.name)) * (index + 1) + result, | |
311 | 0 | |
312 | ) | |
313 | .toString(); | |
314 | } | |
315 | ||
316 | _loadUserConfig() { | |
317 | const loaded = this.localStorage.getItem(this.tableName); | |
318 | if (loaded) { | |
319 | this.userConfig = JSON.parse(loaded); | |
320 | } | |
321 | } | |
322 | ||
323 | _initUserConfigAutoSave() { | |
9f95a23c | 324 | const source: Observable<any> = Observable.create(this._initUserConfigProxy.bind(this)); |
11fdf7f2 TL |
325 | this.saveSubscriber = source.subscribe(this._saveUserConfig.bind(this)); |
326 | } | |
327 | ||
9f95a23c | 328 | _initUserConfigProxy(observer: Subject<any>) { |
11fdf7f2 | 329 | this.userConfig = new Proxy(this.userConfig, { |
9f95a23c | 330 | set(config, prop: string, value) { |
11fdf7f2 TL |
331 | config[prop] = value; |
332 | observer.next(config); | |
333 | return true; | |
334 | } | |
335 | }); | |
336 | } | |
337 | ||
9f95a23c | 338 | _saveUserConfig(config: any) { |
11fdf7f2 TL |
339 | this.localStorage.setItem(this.tableName, JSON.stringify(config)); |
340 | } | |
341 | ||
342 | updateUserColumns() { | |
343 | this.userConfig.columns = this.columns.map((c) => ({ | |
344 | prop: c.prop, | |
345 | name: c.name, | |
346 | isHidden: !!c.isHidden | |
347 | })); | |
348 | } | |
349 | ||
9f95a23c TL |
350 | /** |
351 | * Add a column containing a checkbox if selectionType is 'multiClick'. | |
352 | */ | |
353 | initCheckboxColumn() { | |
354 | if (this.selectionType === 'multiClick') { | |
355 | this.columns.unshift({ | |
356 | prop: undefined, | |
357 | resizeable: false, | |
358 | sortable: false, | |
359 | draggable: false, | |
360 | checkboxable: true, | |
361 | canAutoResize: false, | |
362 | cellClass: 'cd-datatable-checkbox', | |
363 | width: 30 | |
364 | }); | |
365 | } | |
366 | } | |
367 | ||
11fdf7f2 TL |
368 | filterHiddenColumns() { |
369 | this.tableColumns = this.columns.filter((c) => !c.isHidden); | |
370 | } | |
371 | ||
9f95a23c TL |
372 | initColumnFilters() { |
373 | let filterableColumns = _.filter(this.columns, { filterable: true }); | |
374 | filterableColumns = [...filterableColumns, ...this.extraFilterableColumns]; | |
375 | this.columnFilters = filterableColumns.map((col: CdTableColumn) => { | |
376 | return { | |
377 | column: col, | |
378 | options: [], | |
379 | value: col.filterInitValue | |
380 | ? this.createColumnFilterOption(col.filterInitValue, col.pipe) | |
381 | : undefined | |
382 | }; | |
383 | }); | |
384 | this.selectedFilter = _.first(this.columnFilters); | |
385 | } | |
386 | ||
387 | private createColumnFilterOption( | |
388 | value: any, | |
389 | pipe?: PipeTransform | |
390 | ): { raw: string; formatted: string } { | |
391 | return { | |
392 | raw: _.toString(value), | |
393 | formatted: pipe ? pipe.transform(value) : _.toString(value) | |
394 | }; | |
395 | } | |
396 | ||
397 | updateColumnFilterOptions() { | |
398 | // update all possible values in a column | |
399 | this.columnFilters.forEach((filter) => { | |
400 | let values: any[] = []; | |
401 | ||
402 | if (_.isUndefined(filter.column.filterOptions)) { | |
403 | // only allow types that can be easily converted into string | |
404 | const pre = _.filter(_.map(this.data, filter.column.prop), (v) => { | |
405 | return (_.isString(v) && v !== '') || _.isBoolean(v) || _.isFinite(v) || _.isDate(v); | |
406 | }); | |
407 | values = _.sortedUniq(pre.sort()); | |
408 | } else { | |
409 | values = filter.column.filterOptions; | |
410 | } | |
411 | ||
412 | const options = values.map((v) => this.createColumnFilterOption(v, filter.column.pipe)); | |
413 | ||
414 | // In case a previous value is not available anymore | |
415 | if (filter.value && _.isUndefined(_.find(options, { raw: filter.value.raw }))) { | |
416 | filter.value = undefined; | |
417 | } | |
418 | ||
419 | filter.options = options; | |
420 | }); | |
421 | } | |
422 | ||
423 | onSelectFilter(filter: CdTableColumnFilter) { | |
424 | this.selectedFilter = filter; | |
425 | } | |
426 | ||
427 | onChangeFilter(filter: CdTableColumnFilter, option?: { raw: string; formatted: string }) { | |
428 | filter.value = _.isEqual(filter.value, option) ? undefined : option; | |
429 | this.updateFilter(); | |
430 | } | |
431 | ||
432 | doColumnFiltering() { | |
433 | const appliedFilters: CdTableColumnFiltersChange['filters'] = []; | |
434 | let data = [...this.data]; | |
435 | let dataOut: any[] = []; | |
436 | this.columnFilters.forEach((filter) => { | |
437 | if (filter.value === undefined) { | |
438 | return; | |
439 | } | |
440 | appliedFilters.push({ | |
441 | name: filter.column.name, | |
442 | prop: filter.column.prop, | |
443 | value: filter.value | |
444 | }); | |
445 | // Separate data to filtered and filtered-out parts. | |
446 | const parts = _.partition(data, (row) => { | |
447 | // Use getter from ngx-datatable to handle props like 'sys_api.size' | |
448 | const valueGetter = getterForProp(filter.column.prop); | |
449 | const value = valueGetter(row, filter.column.prop); | |
450 | if (_.isUndefined(filter.column.filterPredicate)) { | |
451 | // By default, test string equal | |
452 | return `${value}` === filter.value.raw; | |
453 | } else { | |
454 | // Use custom function to filter | |
455 | return filter.column.filterPredicate(row, filter.value.raw); | |
456 | } | |
457 | }); | |
458 | data = parts[0]; | |
459 | dataOut = [...dataOut, ...parts[1]]; | |
460 | }); | |
461 | ||
462 | this.columnFiltersChanged.emit({ | |
463 | filters: appliedFilters, | |
464 | data: data, | |
465 | dataOut: dataOut | |
466 | }); | |
467 | ||
468 | // Remove the selection if previously-selected rows are filtered out. | |
469 | _.forEach(this.selection.selected, (selectedItem) => { | |
470 | if (_.find(data, { [this.identifier]: selectedItem[this.identifier] }) === undefined) { | |
471 | this.selection = new CdTableSelection(); | |
472 | this.onSelect(this.selection); | |
473 | } | |
474 | }); | |
475 | return data; | |
476 | } | |
477 | ||
11fdf7f2 TL |
478 | ngOnDestroy() { |
479 | if (this.reloadSubscriber) { | |
480 | this.reloadSubscriber.unsubscribe(); | |
481 | } | |
482 | if (this.saveSubscriber) { | |
483 | this.saveSubscriber.unsubscribe(); | |
484 | } | |
485 | } | |
486 | ||
487 | ngAfterContentChecked() { | |
488 | // If the data table is not visible, e.g. another tab is active, and the | |
489 | // browser window gets resized, the table and its columns won't get resized | |
490 | // automatically if the tab gets visible again. | |
491 | // https://github.com/swimlane/ngx-datatable/issues/193 | |
492 | // https://github.com/swimlane/ngx-datatable/issues/193#issuecomment-329144543 | |
493 | if (this.table && this.table.element.clientWidth !== this.currentWidth) { | |
494 | this.currentWidth = this.table.element.clientWidth; | |
9f95a23c | 495 | // Recalculate the sizes of the grid. |
11fdf7f2 | 496 | this.table.recalculate(); |
9f95a23c TL |
497 | // Mark the datatable as changed, Angular's change-detection will |
498 | // do the rest for us => the grid will be redrawn. | |
499 | // Note, the ChangeDetectorRef variable is private, so we need to | |
500 | // use this workaround to access it and make TypeScript happy. | |
501 | const cdRef = _.get(this.table, 'cd'); | |
502 | cdRef.markForCheck(); | |
11fdf7f2 TL |
503 | } |
504 | } | |
505 | ||
506 | _addTemplates() { | |
507 | this.cellTemplates.bold = this.tableCellBoldTpl; | |
508 | this.cellTemplates.checkIcon = this.checkIconTpl; | |
509 | this.cellTemplates.sparkline = this.sparklineTpl; | |
510 | this.cellTemplates.routerLink = this.routerLinkTpl; | |
511 | this.cellTemplates.perSecond = this.perSecondTpl; | |
512 | this.cellTemplates.executing = this.executingTpl; | |
513 | this.cellTemplates.classAdding = this.classAddingTpl; | |
9f95a23c TL |
514 | this.cellTemplates.badge = this.badgeTpl; |
515 | this.cellTemplates.map = this.mapTpl; | |
11fdf7f2 TL |
516 | } |
517 | ||
518 | useCustomClass(value: any): string { | |
519 | if (!this.customCss) { | |
520 | throw new Error('Custom classes are not set!'); | |
521 | } | |
522 | const classes = Object.keys(this.customCss); | |
523 | const css = Object.values(this.customCss) | |
524 | .map((v, i) => ((_.isFunction(v) && v(value)) || v === value) && classes[i]) | |
525 | .filter((x) => x) | |
526 | .join(' '); | |
527 | return _.isEmpty(css) ? undefined : css; | |
528 | } | |
529 | ||
530 | ngOnChanges() { | |
531 | this.useData(); | |
532 | } | |
533 | ||
9f95a23c | 534 | setLimit(e: any) { |
11fdf7f2 TL |
535 | const value = parseInt(e.target.value, 10); |
536 | if (value > 0) { | |
537 | this.userConfig.limit = value; | |
538 | } | |
539 | } | |
540 | ||
541 | reloadData() { | |
542 | if (!this.updating) { | |
543 | this.loadingError = false; | |
544 | const context = new CdTableFetchDataContext(() => { | |
545 | // Do we have to display the error panel? | |
546 | this.loadingError = context.errorConfig.displayError; | |
547 | // Force data table to show no data? | |
548 | if (context.errorConfig.resetData) { | |
549 | this.data = []; | |
550 | } | |
551 | // Stop the loading indicator and reset the data table | |
552 | // to the correct state. | |
553 | this.useData(); | |
554 | }); | |
555 | this.fetchData.emit(context); | |
556 | this.updating = true; | |
557 | } | |
558 | } | |
559 | ||
560 | refreshBtn() { | |
561 | this.loadingIndicator = true; | |
562 | this.reloadData(); | |
563 | } | |
564 | ||
565 | rowIdentity() { | |
9f95a23c | 566 | return (row: any) => { |
11fdf7f2 TL |
567 | const id = row[this.identifier]; |
568 | if (_.isUndefined(id)) { | |
569 | throw new Error(`Wrong identifier "${this.identifier}" -> "${id}"`); | |
570 | } | |
571 | return id; | |
572 | }; | |
573 | } | |
574 | ||
575 | useData() { | |
576 | if (!this.data) { | |
577 | return; // Wait for data | |
578 | } | |
9f95a23c TL |
579 | this.updateColumnFilterOptions(); |
580 | this.updateFilter(); | |
11fdf7f2 TL |
581 | this.reset(); |
582 | this.updateSelected(); | |
583 | } | |
584 | ||
585 | /** | |
586 | * Reset the data table to correct state. This includes: | |
587 | * - Disable loading indicator | |
588 | * - Reset 'Updating' flag | |
589 | */ | |
590 | reset() { | |
591 | this.loadingIndicator = false; | |
592 | this.updating = false; | |
593 | } | |
594 | ||
595 | /** | |
596 | * After updating the data, we have to update the selected items | |
597 | * because details may have changed, | |
598 | * or some selected items may have been removed. | |
599 | */ | |
600 | updateSelected() { | |
601 | if (this.updateSelectionOnRefresh === 'never') { | |
602 | return; | |
603 | } | |
9f95a23c | 604 | const newSelected: any[] = []; |
11fdf7f2 TL |
605 | this.selection.selected.forEach((selectedItem) => { |
606 | for (const row of this.data) { | |
607 | if (selectedItem[this.identifier] === row[this.identifier]) { | |
608 | newSelected.push(row); | |
609 | } | |
610 | } | |
611 | }); | |
612 | if ( | |
613 | this.updateSelectionOnRefresh === 'onChange' && | |
614 | _.isEqual(this.selection.selected, newSelected) | |
615 | ) { | |
616 | return; | |
617 | } | |
618 | this.selection.selected = newSelected; | |
9f95a23c | 619 | this.onSelect(this.selection); |
11fdf7f2 TL |
620 | } |
621 | ||
9f95a23c TL |
622 | onSelect($event: any) { |
623 | this.selection.selected = $event['selected']; | |
11fdf7f2 TL |
624 | this.updateSelection.emit(_.clone(this.selection)); |
625 | } | |
626 | ||
627 | toggleColumn($event: any) { | |
628 | const prop: TableColumnProp = $event.target.name; | |
629 | const hide = !$event.target.checked; | |
630 | if (hide && this.tableColumns.length === 1) { | |
631 | $event.target.checked = true; | |
632 | return; | |
633 | } | |
634 | _.find(this.columns, (c: CdTableColumn) => c.prop === prop).isHidden = hide; | |
635 | this.updateColumns(); | |
636 | } | |
637 | ||
638 | updateColumns() { | |
639 | this.updateUserColumns(); | |
640 | this.filterHiddenColumns(); | |
641 | const sortProp = this.userConfig.sorts[0].prop; | |
642 | if (!_.find(this.tableColumns, (c: CdTableColumn) => c.prop === sortProp)) { | |
643 | this.userConfig.sorts = this.createSortingDefinition(this.tableColumns[0].prop); | |
11fdf7f2 TL |
644 | } |
645 | this.table.recalculate(); | |
646 | this.cdRef.detectChanges(); | |
647 | } | |
648 | ||
649 | createSortingDefinition(prop: TableColumnProp): SortPropDir[] { | |
650 | return [ | |
651 | { | |
652 | prop: prop, | |
653 | dir: SortDirection.asc | |
654 | } | |
655 | ]; | |
656 | } | |
657 | ||
9f95a23c | 658 | changeSorting({ sorts }: any) { |
11fdf7f2 TL |
659 | this.userConfig.sorts = sorts; |
660 | } | |
661 | ||
9f95a23c TL |
662 | onClearSearch() { |
663 | this.search = ''; | |
664 | this.updateFilter(); | |
665 | } | |
666 | ||
667 | onClearFilters() { | |
668 | this.columnFilters.forEach((filter) => { | |
669 | filter.value = undefined; | |
670 | }); | |
671 | this.selectedFilter = _.first(this.columnFilters); | |
672 | this.updateFilter(); | |
673 | } | |
674 | ||
675 | updateFilter() { | |
676 | let rows = this.columnFilters.length !== 0 ? this.doColumnFiltering() : this.data; | |
677 | ||
678 | if (this.search.length > 0) { | |
679 | const columns = this.columns.filter((c) => c.cellTransformation !== CellTemplate.sparkline); | |
680 | // update the rows | |
681 | rows = this.subSearch(rows, TableComponent.prepareSearch(this.search), columns); | |
682 | // Whenever the filter changes, always go back to the first page | |
683 | this.table.offset = 0; | |
11fdf7f2 | 684 | } |
9f95a23c TL |
685 | |
686 | this.rows = rows; | |
11fdf7f2 TL |
687 | } |
688 | ||
9f95a23c | 689 | subSearch(data: any[], currentSearch: string[], columns: CdTableColumn[]): any[] { |
11fdf7f2 TL |
690 | if (currentSearch.length === 0 || data.length === 0) { |
691 | return data; | |
692 | } | |
693 | const searchTerms: string[] = currentSearch | |
694 | .pop() | |
9f95a23c | 695 | .replace(/\+/g, ' ') |
11fdf7f2 TL |
696 | .split(':'); |
697 | const columnsClone = [...columns]; | |
11fdf7f2 | 698 | if (searchTerms.length === 2) { |
81eedcae | 699 | columns = columnsClone.filter((c) => c.name.toLowerCase().indexOf(searchTerms[0]) !== -1); |
11fdf7f2 | 700 | } |
81eedcae | 701 | data = this.basicDataSearch(_.last(searchTerms), data, columns); |
11fdf7f2 | 702 | // Checks if user searches for column but he is still typing |
11fdf7f2 TL |
703 | return this.subSearch(data, currentSearch, columnsClone); |
704 | } | |
705 | ||
706 | basicDataSearch(searchTerm: string, rows: any[], columns: CdTableColumn[]) { | |
707 | if (searchTerm.length === 0) { | |
708 | return rows; | |
709 | } | |
710 | return rows.filter((row) => { | |
711 | return ( | |
712 | columns.filter((col) => { | |
713 | let cellValue: any = _.get(row, col.prop); | |
714 | ||
715 | if (!_.isUndefined(col.pipe)) { | |
716 | cellValue = col.pipe.transform(cellValue); | |
717 | } | |
718 | if (_.isUndefined(cellValue) || _.isNull(cellValue)) { | |
719 | return false; | |
720 | } | |
721 | ||
722 | if (_.isArray(cellValue)) { | |
723 | cellValue = cellValue.join(' '); | |
724 | } else if (_.isNumber(cellValue) || _.isBoolean(cellValue)) { | |
725 | cellValue = cellValue.toString(); | |
726 | } | |
9f95a23c TL |
727 | |
728 | if (_.isObjectLike(cellValue)) { | |
729 | if (this.searchableObjects) { | |
730 | cellValue = JSON.stringify(cellValue); | |
731 | } else { | |
732 | return false; | |
733 | } | |
734 | } | |
735 | ||
11fdf7f2 TL |
736 | return cellValue.toLowerCase().indexOf(searchTerm) !== -1; |
737 | }).length > 0 | |
738 | ); | |
739 | }); | |
740 | } | |
741 | ||
742 | getRowClass() { | |
743 | // Return the function used to populate a row's CSS classes. | |
744 | return () => { | |
745 | return { | |
746 | clickable: !_.isUndefined(this.selectionType) | |
747 | }; | |
748 | }; | |
749 | } | |
750 | } |