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