UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1,089 lines 319 kB
import { CdkHeaderCell, CdkTable } from '@angular/cdk/table'; import { Component, ContentChild, ContentChildren, ElementRef, EventEmitter, Inject, Input, Optional, Output, QueryList, ViewChild, ViewChildren, ViewContainerRef, forwardRef } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; import { parseInt as _parseInt, castArray, flow, get, indexOf, isEmpty, isNil, union, uniqBy, without } from 'lodash-es'; import { BsModalService } from 'ngx-bootstrap/modal'; import { BehaviorSubject, Subject, combineLatest, forkJoin, isObservable, merge, of, pipe } from 'rxjs'; import { combineLatestWith, concatMap, debounceTime, delay, distinctUntilChanged, filter, first, map, mergeMap, startWith, switchMap, take, takeUntil, tap } from 'rxjs/operators'; import { AlertService } from '../alert/alert.service'; import { EmptyStateContextDirective } from '../common/empty-state/empty-state-context.directive'; import { toObservable } from '../common/extension-hooks'; import { LoadMoreComponent } from '../common/load-more.component'; import { gettext } from '../i18n/gettext'; import { GainsightService } from '../product-experience/gainsight.service'; import { PRODUCT_EXPERIENCE_EVENT_SOURCE } from '../product-experience/product-experience.model'; import { ActionControlsExtensionService } from './action-controls-extension.service'; import { ColumnDirective } from './column/column.directive'; import { CustomColumn } from './column/custom.column'; import { ExpandableRowColumn } from './column/expandable-row-column/expandable.data-grid-column'; import { ConfigureCustomColumnComponent } from './configure-custom-column'; import { DATA_GRID_CONFIGURATION_STRATEGY } from './data-grid-configuration.model'; import { BuiltInActionType, FilteringActionType, minColumnGridTrackSize, ratiosByColumnTypes } from './data-grid.model'; import { DataGridService } from './data-grid.service'; import { ExpandableRowDirective } from './expandable-row.directive'; import { GridDataSource } from './grid-data-source'; import { PX_ACTIONS, PX_EVENT_NAME } from './product-experience.constants'; import * as i0 from "@angular/core"; import * as i1 from "./data-grid.service"; import * as i2 from "@angular/platform-browser"; import * as i3 from "../product-experience/gainsight.service"; import * as i4 from "ngx-bootstrap/modal"; import * as i5 from "../alert/alert.service"; import * as i6 from "./action-controls-extension.service"; import * as i7 from "@angular/router"; import * as i8 from "@angular/common"; import * as i9 from "@angular/cdk/table"; import * as i10 from "@angular/cdk/drag-drop"; import * as i11 from "@angular/forms"; import * as i12 from "../common/icon.directive"; import * as i13 from "../i18n/c8y-translate.directive"; import * as i14 from "../common/loading.component"; import * as i15 from "../modal/popover-confirm.component"; import * as i16 from "ngx-bootstrap/dropdown"; import * as i17 from "ngx-bootstrap/popover"; import * as i18 from "ngx-bootstrap/tooltip"; import * as i19 from "ngx-bootstrap/pagination"; import * as i20 from "../product-experience/product-experience.directive"; import * as i21 from "@angular/cdk/a11y"; import * as i22 from "./column/cell-renderer.component"; import * as i23 from "./column/filtering-form-renderer.component"; import * as i24 from "../i18n/c8y-translate.pipe"; import * as i25 from "../common/map-function.pipe"; import * as i26 from "./filter-chip/filter-mapper.pipe"; import * as i27 from "./filter-chip/grouped-filter-chips.pipe"; import * as i28 from "./visible-controls.pipe"; var SortingOrder; (function (SortingOrder) { SortingOrder["ASC"] = "asc"; SortingOrder["DESC"] = "desc"; })(SortingOrder || (SortingOrder = {})); export class DataGridComponent { /** The list of rows to be displayed in the grid (used for client side data). */ set _rows(rows) { this.rows = rows || []; } /** Pagination settings, e.g. allows for setting current page or page size. */ set _pagination(pagination) { this.pagination = pagination; } /** Sets load more mode. */ set _infiniteScroll(infiniteScroll) { this.infiniteScroll = infiniteScroll; } /** * Sets a callback function which will be invoked whenever data needs to be loaded from server. * The function should take [[DataSourceModifier]] and return [[ServerSideDataResult]]. */ set _serverSideDataCallback(serverSideDataCallback) { this.serverSideDataCallback = serverSideDataCallback; } /** Determines whether items can be selected by clicking a checkbox in the first column. */ set _selectable(selectable) { this.selectable = selectable; } /** Restricts selection to a single row only. Selection column displays radio button instead of checkboxes */ set _singleSelection(singleSelection) { this.singleSelection = singleSelection; } /** Determines which item's property will be used to distinguish selection status. */ set _selectionPrimaryKey(selectionPrimaryKey) { this.selectionPrimaryKey = selectionPrimaryKey; } /** Sets display options. */ set _displayOptions(displayOptions) { this.displayOptions = { ...this.displayOptions, ...displayOptions }; } /** Sets action controls (actions available for individual items). */ set _actionControls(actionControls) { this.actionControlsInput$.next(actionControls); } /** Sets bulk action controls (actions available for items selected by user). */ set _bulkActionControls(bulkActionControls) { this.bulkActionControls = bulkActionControls || []; } /** Sets header action controls (actions available from data grid header). */ set _headerActionControls(headerActionControls) { this.headerActionControls = headerActionControls || []; } constructor(configurationStrategy, dataGridService, sanitizer, gainsightService, bsModalService, alertService, actionControlsService, route) { this.configurationStrategy = configurationStrategy; this.dataGridService = dataGridService; this.sanitizer = sanitizer; this.gainsightService = gainsightService; this.bsModalService = bsModalService; this.alertService = alertService; this.actionControlsService = actionControlsService; this.route = route; /** The title for the data grid, it's displayed in the grid's header. */ this.title = gettext('Items'); /** The label for load more button. */ this.loadMoreItemsLabel = gettext('Load more items'); /** The label for loading indicator. */ this.loadingItemsLabel = gettext('Loading items…'); /** Determines whether text search input is shown in the grid's header. */ this.showSearch = false; this.columns = []; this.dataSource = new GridDataSource(); this.filteringLabelsParams = { filteredItemsCount: 0, allItemsCount: 0 }; this.paginationLabelParams = { pageFirstItemIdx: 0, pageLastItemIdx: 0, itemsTotal: 0 }; this.possiblePageSizes = [25, 50, 100]; this.minPossiblePageSize = Math.min(...this.possiblePageSizes); this.selectable = false; this.singleSelection = false; this.selectionPrimaryKey = 'id'; this.displayOptions = { striped: true, bordered: false, gridHeader: true, filter: true, hover: true }; this.actionControls = []; /** Sets initial search text. */ this.searchText = ''; /** Determines if custom columns button will be enabled. */ this.configureColumnsEnabled = true; /** Shows the warning for the sub-assets counter */ this.showCounterWarning = false; /** * Sets the class name used for active rows (last clicked). * Set empty string to disable appending active class to grid rows. */ this.activeClassName = 'active'; /** Determines if the rows of the data grid will be expandable. * Possible values: * - `NONE` - no expandable rows (default value) * - `SYNC` - additional column with expand button is displayed and expandable rows are expanding synchronously when button is clicked * - `ASYNC` - additional column with expand button is displayed and expandable rows are expanding asynchronously when button is clicked */ this.expandableRows = 'NONE'; /** Emits an event when mouse is over a row. */ this.rowMouseOver = new EventEmitter(); /** Emits an event when mouse leaves a row. */ this.rowMouseLeave = new EventEmitter(); /** Emits an event when a row is clicked. */ this.rowClick = new EventEmitter(); /** Emits an event when grid's configuration is changed. */ this.onConfigChange = new EventEmitter(); /** Emits an event before the filter is applied. */ this.onBeforeFilter = new EventEmitter(); /** Emits an event before the search is performed. */ this.onBeforeSearch = new EventEmitter(); /** Emits an event when a filter is applied in a column. */ this.onFilter = new EventEmitter(); /** Emits an event when items selection changes. The array contains keys of selected items (key property is defined by `selectionPrimaryKey`). */ this.itemsSelect = new EventEmitter(); /** Emits an event when reload button is clicked. */ this.onReload = new EventEmitter(); /** Emits an event when a custom column is added */ this.onAddCustomColumn = new EventEmitter(); /** Emits an event when a custom column is removed */ this.onRemoveCustomColumn = new EventEmitter(); /** Emits an event after the column filter has been reset */ this.onColumnFilterReset = new EventEmitter(); /** Emits an event when column sorting has been changed */ this.onSort = new EventEmitter(); /** Emits an event when page size has been changed */ this.onPageSizeChange = new EventEmitter(); /** Emits an event when column order has been changed */ this.onColumnReordered = new EventEmitter(); /** Emits an event when column order has been changed */ this.onColumnVisibilityChange = new EventEmitter(); this.columnNames = []; this.styles = { tableCursor: 'auto', gridTemplateColumns: undefined, gridInfiniteScrollColumn: undefined }; this.searchText$ = new EventEmitter(); this.filteringApplied = false; this.columnsWithFiltersApplied = []; this.totalPagesCount$ = new BehaviorSubject(Infinity); this.hidePagination$ = this.totalPagesCount$.pipe(map(totalPagesCount => totalPagesCount <= 1), delay(0) // prevents ExpressionChangedAfterItHasBeenCheckedError ); this.selectedItemIds = []; this.currentPageSelectionState = { allSelected: false, allDeselected: true }; this.builtInActionType = { Edit: BuiltInActionType.Edit, Delete: BuiltInActionType.Delete, Export: BuiltInActionType.Export }; this.confirmRemoveColumnButtons = [ { label: gettext('Cancel'), action: () => Promise.resolve(false) }, { label: gettext('Remove`column,verb`'), status: 'danger', action: () => Promise.resolve(true) } ]; this.isConfigContextKnown = false; /** * A map of rows which have been expanded. */ this.expandedRows = new Map(); /** Product experience constants declarations */ this.productExperienceEvent = { eventName: PX_EVENT_NAME }; this.PX_ACTIONS = PX_ACTIONS; this.sortColumnTitle = gettext('Sort column "{{ name }}"'); this.resizeHandleMouseDown$ = new EventEmitter(); this.resizeHandleContainerMouseMove$ = new EventEmitter(); this.resizeHandleContainerMouseUp$ = new EventEmitter(); this.filtersHelpPopoverHtml = gettext('Click the column headers to apply filters.'); this.columnsInitialized = false; this.defaultColumns = []; this.reloadConfiguration$ = new Subject(); this.actionControlsInput$ = new BehaviorSubject([]); this.unsubscribe$ = new Subject(); this.SEARCH_DEBOUNCE_TIME = 500; /** * Event emitter, taking boolean values used for loading data grid data with debounce. * Default value is set to false. Set to true if data grid is using infinite scroll and page should be reloaded. * This is used to avoid having multiple this.loadData() function calls. */ this.triggerLoadData = new EventEmitter(); this.isRowExpanded = (_, row) => { return !!this.expandedRows.get(row); }; this.triggerLoadData.pipe(debounceTime(1), takeUntil(this.unsubscribe$)).subscribe(reload => { this.loadData(reload); }); this.reloadConfiguration$ .pipe(switchMap(() => this.configurationStrategy?.getConfig$() ?? of(null)), tap(config => { this.setColumns(config); this.setPageSize(config); this.triggerLoadData.emit(!!this.infiniteScroll); }), switchMap(() => this.dataSource.stats$), tap(stats => { this.createLoadMoreComponent(stats); this.updateFilteringLabelsParams(stats); this.updatePaginationLabelParams(stats); this.updatePaginationWhenNoDevicesLastPage(stats); }), takeUntil(this.unsubscribe$)) .subscribe(); } ngOnInit() { this.isConfigContextKnown = !!this.configurationStrategy?.isContextKnown(); this.searchText$ .pipe(takeUntil(this.unsubscribe$), debounceTime(this.SEARCH_DEBOUNCE_TIME), distinctUntilChanged(), tap(searchText => { this.searchText = searchText; this.onBeforeSearch.emit(this.searchText); this.triggerEvent({ action: PX_ACTIONS.SEARCH, searchInput: searchText }); })) .subscribe(() => { this.reload(); }); if (this.selectable) { combineLatest(this.dataSource.data$, this.itemsSelect.asObservable()) .pipe(takeUntil(this.unsubscribe$)) .subscribe(([data]) => { const currentPageEmpty = data.length === 0; this.currentPageSelectionState = { allSelected: currentPageEmpty ? false : data.every(item => this.isItemSelected(item)), allDeselected: currentPageEmpty ? true : data.every(item => !this.isItemSelected(item)) }; }); } this.reloadConfiguration$.next(); this.actionControlsService.items$ .pipe(startWith([]), switchMap((hooks) => forkJoin(hooks.map(hook => toObservable(hook?.matchesGrid ? this.safelyInvokeMatcher(hook.matchesGrid, this.route, this.configurationStrategy?.getContext()) : false).pipe(map(matches => ({ hook, matches }))))).pipe(startWith([]))), map((hooks) => hooks.filter(hook => hook.matches).map(hook => hook.hook)), map((hooks) => hooks.reduce((actionControls, currentHook) => { return [...actionControls, ...castArray(currentHook.actionControls)]; }, [])), combineLatestWith(this.actionControlsInput$), tap(([hookControls, inputControls]) => (this.actionControls = [...inputControls, ...hookControls])), takeUntil(this.unsubscribe$)) .subscribe(); if (this.refresh) { this.refresh.pipe(takeUntil(this.unsubscribe$)).subscribe(() => { this.cancel(); this.reload(); }); } this.processAndPersistConfigChange(); this.updateColumns(); // Resetting the stats size to 0 when managed objects are deleted but sizing not yet updated // TODO remove after MTM-60226 is resolved this.emptyStateContext$ = combineLatest([this.dataSource.stats$, this.dataSource.data$]).pipe(map(([stats, data]) => { if (stats.filteredSize === 1 && data.length === 0) { return { ...stats, size: 0, filteredSize: 0 }; } return stats; })); } setExpandableRowVisible(row, success) { if (success) { this.expandedRows.get(row).visible$.next(true); } else { this.expandedRows.get(row).visible$.next(false); this.expandedRows.delete(row); this.tableRef.renderRows(); } } ngOnChanges(event) { if (((!event._actionControls && !event.searchText) || event._actionControls?.firstChange) && this.columnsInitialized) { const reload = !!event._infiniteScroll?.currentValue && !event._infiniteScroll?.firstChange; this.triggerLoadData.emit(reload); } if (!!event._columns && !event._columns.firstChange) { this.reloadConfiguration$.next(); } this.updateColumns(); } ngAfterViewInit() { this.updateGridColumnsSize(); this.updateThEls(); this.setupResizeHandle(); } ngOnDestroy() { this.unsubscribe$.next(); this.unsubscribe$.complete(); } expand(row) { const isSyncExpand = this.expandableRows === 'SYNC'; let visibleSubject; if (isSyncExpand) { visibleSubject = new BehaviorSubject(true); } else { visibleSubject = new Subject(); } this.expandedRows.set(row, { visible$: visibleSubject }); this.tableRef.renderRows(); return visibleSubject; } collapse(row) { this.expandedRows.delete(row); this.tableRef.renderRows(); } setColumns(config) { if (!!this.configurationStrategy && !isEmpty(this._columns)) { this.columns = this.dataGridService.applyConfigToColumns(config, this._columns); this.columnsInitialized = true; } else { this.columns = this._columns || []; this.columnsInitialized = this.columnsInitialized || !!this._columns; } this.defaultColumns = this.columns || []; this.updateColumns(); } setPageSize(config) { if (!!config?.pagination) { this.pagination = { ...this.pagination, pageSize: config.pagination.pageSize }; } const pageSize = get(this.pagination, 'pageSize'); if (this.pagination && !this.possiblePageSizes.find(possiblePageSize => possiblePageSize === pageSize)) { this.pagination = { ...this.pagination, pageSize: this.minPossiblePageSize }; } } openCustomColumnModal() { const modalRef = this.bsModalService.show(ConfigureCustomColumnComponent, { class: 'modal-sm', ariaDescribedby: 'modal-body', ariaLabelledBy: 'modal-title', ignoreBackdropClick: true, initialState: { columns: this.columns } }); modalRef.content.onAddCustomColumn .pipe(tap((customColumnConfig) => { const firstFixedColumPosition = this.columns.indexOf(this.columns.find(column => column.positionFixed)); this.columns.splice(firstFixedColumPosition > -1 ? firstFixedColumPosition : this.columns.length, 0, new CustomColumn(customColumnConfig)); this.updateColumns(); this.triggerEvent({ action: PX_ACTIONS.ADD_CUSTOM_COLUMN, column: customColumnConfig.header || customColumnConfig.name }); }), takeUntil(modalRef.onHidden)) .subscribe(event => this.onAddCustomColumn.emit(event)); } async removeCustomColumn(poConfirm, column, ddConfigureColumns) { ddConfigureColumns.autoClose = false; poConfirm.message = gettext('Do you want to remove this column?'); try { const remove = await poConfirm.show(this.confirmRemoveColumnButtons); if (remove) { this.columns = this.columns.filter(col => col?.name !== column?.name); this.updateColumns(); this.onRemoveCustomColumn.emit(column); this.triggerEvent({ action: PX_ACTIONS.REMOVE_CUSTOM_COLUMN, column: column.header || column.name }); } } catch (e) { this.alertService.addServerFailure(e); } setTimeout(() => (ddConfigureColumns.autoClose = true), 0); } async removeFilter(filter) { const filteringModifier = filter.externalFilterQuery ? { externalFilterQuery: filter.externalFilterQuery } : { filterPredicate: filter.filterPredicate }; this.onBeforeFilter.emit({ columnName: filter.columnName, dropdown: undefined, filteringModifier }); if ((filter.externalFilterQuery && !this.checkIfAnyValuesExist(filter.externalFilterQuery)) || filter.filterPredicate) { this.updateFiltering([filter.columnName], { type: FilteringActionType.ResetFilter }); this.onFilter.emit({ columnName: filter.columnName }); } else { this.updateFiltering([filter.columnName], { type: FilteringActionType.ApplyFilter, payload: { filteringModifier } }); this.onFilter.emit({ columnName: filter.columnName, dropdown: undefined, filteringModifier }); } this.triggerEvent({ action: PX_ACTIONS.REMOVE_FILTER, column: filter.columnName, filteringModifier }); } trackByName(index, item) { return item.name; } resolveCellValue(row, path) { return flow([ x => this.dataSource.resolveValue(x, path), this.dataSource.resolveFunction, this.dataSource.normalizeNil ])(row); } changeSortOrder(columnName) { const column = this.columns.find(({ name }) => name === columnName); if (column) { const { sortOrder } = column; if (!sortOrder) { this.updateSorting([columnName], SortingOrder.ASC); } else if (sortOrder === SortingOrder.ASC) { this.updateSorting([columnName], SortingOrder.DESC); } else { this.updateSorting([columnName], ''); } } } updateSorting(columnNames, sortOrder) { this.triggerEvent({ action: PX_ACTIONS.CHANGE_SORTING_ORDER, columns: columnNames, sortOrder: sortOrder === '' ? 'none' : sortOrder }); this.columns = this.columns.map((column) => { if (columnNames.includes(column.name)) { return { ...column, sortOrder }; } return column; }); this.emitConfigChange('sort'); this.reload(); } applyFilter(columnName, dropdown, filteringModifier) { this.triggerEvent({ action: PX_ACTIONS.APPLY_FILTER, column: columnName, filteringModifier }); this.onBeforeFilter.emit({ columnName, dropdown, filteringModifier }); this.updateFiltering([columnName], { type: FilteringActionType.ApplyFilter, payload: { filteringModifier } }); dropdown.hide(); this.onFilter.emit({ columnName, dropdown, filteringModifier }); } resetFilter(columnName, dropdown) { this.triggerEvent({ action: PX_ACTIONS.RESET_FILTER, column: columnName }); this.updateFiltering([columnName], { type: FilteringActionType.ResetFilter }); dropdown.hide(); this.onFilter.emit({ columnName, dropdown }); } clearFilters(reload = true) { this.updateFiltering(this.columns.map(({ name }) => name), { type: FilteringActionType.ResetFilter }, reload); this.onFilter.emit({}); this.triggerEvent({ action: PX_ACTIONS.CLEAR_FILTER }); } updateFiltering(columnNames, action, reload = true) { this.columns = this.columns.map(column => { if (columnNames.includes(column.name)) { return { ...column, ...(action.type === FilteringActionType.ApplyFilter ? action.payload.filteringModifier : this.onResetFilterAction(column)) }; } return column; }); this.updateFilteringApplied(); if (reload) { this.reload(); } } updateFilteringApplied() { this.columnsWithFiltersApplied = this.columns.filter(this.isColumnFilteringApplied); this.filteringApplied = this.columnsWithFiltersApplied.length > 0; this.filtersHelpPopoverHtml = this.filteringApplied ? gettext('Click the column headers to apply filters. Click <b>Active filters</b> button to manage applied filters.') : gettext('Click the column headers to apply filters.'); } isColumnFilteringApplied(column) { const { filterable, filterPredicate, externalFilterQuery } = column; return !!(filterable && (filterPredicate || externalFilterQuery)); } updatePagination({ itemsPerPage, page }) { const configChanged = this.pagination?.pageSize !== itemsPerPage; this.pagination = { ...this.pagination, pageSize: itemsPerPage, currentPage: page }; this.loadData(); if (configChanged) { this.emitConfigChange('pagination'); } this.triggerEvent({ action: PX_ACTIONS.CHANGE_PAGINATION, itemsPerPage, page }); } clickReload() { this.searchText = ''; this.reload(); this.onReload.next(); this.triggerEvent({ action: PX_ACTIONS.RELOAD }); } reload(redirect = true) { this.pagination = { ...this.pagination, currentPage: redirect ? 1 : this.pagination.currentPage }; this.recreateLoadMoreComponent = true; this.loadData(true); this.scrollToTop(); } loadNextPage() { this.pagination = { ...this.pagination, currentPage: this.pagination.nextPage }; this.loadData(); return this.dataSource.resultList$ .pipe(take(1)) // in order for `toPromise` to work, the observable needs to complete .toPromise() .then(result => { return { ...result, paging: { ...result.paging, next: this.loadNextPage.bind(this) } }; }); } getCellRendererSpec({ value, row, columnName }) { return this._getCellRendererSpec({ type: 'CELL', value, row, columnName }); } getHeaderCellRendererSpec({ value, columnName }) { return this._getCellRendererSpec({ type: 'HEADER', value, row: undefined, columnName }); } getFilteringFormRendererSpec({ column, dropdown }) { return { renderer: get(this.getColumnRenderer(column), 'filteringFormRendererDef.template') || column.filteringFormRendererComponent, context: { property: column, applyFilter: this.applyFilter.bind(this, column.name, dropdown), resetFilter: this.resetFilter.bind(this, column.name, dropdown) } }; } setAllItemsSelected(selected) { this.dataSource.selection$ .pipe(first()) .subscribe(({ filteredDataIds }) => this.setItemsSelected(filteredDataIds, selected)); } setAllItemsInCurrentPageSelected(selected) { this.dataSource.data$.pipe(first()).subscribe(data => this.setItemsSelected(data, selected)); } setItemsSelected(items, selected) { const itemIds = items.map((item) => typeof item === 'object' ? item[this.selectionPrimaryKey] : item); this.selectedItemIds = selected ? union(this.selectedItemIds, itemIds) : without(this.selectedItemIds, ...itemIds); this.itemsSelect.emit(this.selectedItemIds); } changeSelectedItem(item) { this.selectedItemIds = [item[this.selectionPrimaryKey]]; this.itemsSelect.emit(this.selectedItemIds); } cancel() { this.selectedItemIds = []; this.itemsSelect.emit(this.selectedItemIds); } isItemSelected(item) { return this.selectedItemIds.includes(item[this.selectionPrimaryKey]); } onColumnDrop({ previousIndex, currentIndex }) { const differentIndex = previousIndex !== currentIndex; if (differentIndex) { this.triggerEvent({ action: PX_ACTIONS.REORDER_COLUMNS, column: this.columnNames[previousIndex] }); const column = this.columns.splice(previousIndex, 1); this.columns.splice(currentIndex, 0, column[0]); this.emitConfigChange('reorderColumn'); } this.updateColumnNames(); this.updateGridColumnsSize(); } updateGridColumnsSize() { this.styles = { ...this.styles, gridTemplateColumns: this.sanitizer.bypassSecurityTrustStyle(this.columns .filter(column => column.visible) .map(({ gridTrackSize }) => gridTrackSize) .join(' ')), gridInfiniteScrollColumn: this.sanitizer.bypassSecurityTrustStyle(`1 / -1`) }; } updateThEls() { setTimeout(() => { this.thEls = this.thRefs ? this.thRefs.toArray().map(({ nativeElement }) => nativeElement) : []; }, 0); } // To be removed when columns are transformed to observables. isDropDownPlacedRight(column) { return (indexOf(this.columns.filter(c => c.visible), column) > this.columns.filter(c => c.visible).length / 2); } emitConfigChange(eventType) { if (this.columnsInitialized) { const columns = this.columns.map(this.mapColumnToConfig.bind(this)); const config = { columns, pagination: this.pagination }; this.onConfigChange.emit(config); switch (eventType) { case 'sort': this.onSort.emit(config); break; case 'pagination': this.onPageSizeChange.emit(config); break; case 'reorderColumn': this.onColumnReordered.emit(config); break; case 'changeColumnVisibility': this.onColumnVisibilityChange.emit(config); } } } triggerEvent(eventData) { this.gainsightService.triggerEvent(this.productExperienceEvent?.eventName || PX_EVENT_NAME, { ...this.productExperienceEvent?.data, ...eventData }); } handleClick(row) { this.lastClickedRow = row; this.rowClick.emit(row); } onResetFilterAction(column) { this.onColumnFilterReset.emit(column); return { filterPredicate: undefined, externalFilterQuery: undefined }; } mapColumnToConfig(column) { let config; if (column.custom) { const { visible, sortOrder, name, externalFilterQuery, header, path } = column; config = { visible, sortOrder, name, filter: { externalFilterQuery }, header, path, custom: true }; } else { const { visible, sortOrder, name, externalFilterQuery } = column; config = { visible, sortOrder, name, filter: { externalFilterQuery } }; } if (isEmpty(config?.filter?.externalFilterQuery)) { delete config.filter; } return config; } loadData(reload = false) { const { rows, columns, pagination, searchText, serverSideDataCallback, selectable, selectionPrimaryKey, infiniteScroll } = this; this.dataSource.loadData({ rows, columns, pagination, searchText, serverSideDataCallback, selectable, selectionPrimaryKey, infiniteScroll, reload }); } updateColumns() { const specialColumn = { sortable: false, positionFixed: true }; const selectionColumn = this.selectable ? { ...specialColumn, name: this.singleSelection ? "radio-button" /* SpecialColumnName.RadioButton */ : "checkbox" /* SpecialColumnName.Checkbox */, gridTrackSize: '32px' } : undefined; const actionsColumn = this.actionControls?.length > 0 ? { ...specialColumn, name: "actions" /* SpecialColumnName.Actions */, gridTrackSize: 'minmax(40px, auto)' } : undefined; const expandableRowsColumn = this.expandableRows !== 'NONE' ? new ExpandableRowColumn() : null; const columns = [expandableRowsColumn, selectionColumn, ...this.columns, actionsColumn] .filter(Boolean) .map(this.withColumnDefaults); this.columns = uniqBy(columns, 'name'); this.updateColumnNames(); this.updateGridColumnsSize(); this.updateThEls(); this.updateFilteringApplied(); } checkIfAnyValuesExist(obj, results = []) { if (obj && Object.entries(obj)) { Object.entries(obj).forEach(([key, value]) => { if (typeof obj[key] === 'object') { this.checkIfAnyValuesExist(obj[key], results); } else { results.push(value); } }); } return results.some(val => !!val); } withColumnDefaults(column) { const dataType = column.dataType || "text-short" /* ColumnDataType.TextShort */; const { headerCSSClassName, cellCSSClassName } = column; return { visible: true, positionFixed: false, resizable: true, sortable: true, sortOrder: '', filterable: false, ...column, dataType, gridTrackSize: column.gridTrackSize || `minmax(${minColumnGridTrackSize}px, ${ratiosByColumnTypes[dataType]}fr)`, headerCSSClassName: (typeof headerCSSClassName === 'string' ? headerCSSClassName.split(' ') : headerCSSClassName) || [], cellCSSClassName: (typeof cellCSSClassName === 'string' ? cellCSSClassName.split(' ') : cellCSSClassName) || [] }; } updateColumnNames() { this.columnNames = this.columns.map(({ name }) => name); } setupResizeHandle() { const resizeHandleDrag$ = this.resizeHandleMouseDown$.pipe(takeUntil(this.unsubscribe$), tap(() => this.clearMouseHighlights()), mergeMap(({ event, targetColumnName }) => { this.columns = this.columns.map(column => { if (column.name === targetColumnName) { return { ...column, headerCSSClassName: union(column.headerCSSClassName, ['header--being-resized']) }; } return column; }); this.headerBeingResized = { columnName: targetColumnName, el: event.target?.parentNode }; this.styles = { ...this.styles, tableCursor: 'col-resize' }; return this.resizeHandleContainerMouseMove$.pipe(tap(() => this.clearMouseHighlights()), takeUntil(this.resizeHandleContainerMouseUp$)); })); resizeHandleDrag$.subscribe((event) => { requestAnimationFrame(() => { this.columns = this.columns.map((column, i) => { if (this.headerBeingResized && column.name === this.headerBeingResized.columnName) { const scrollContainerDiv = this.scrollContainer.nativeElement; // Read scrollContainerEl's offset left relative to the document. const horizontalOffset = scrollContainerDiv.getBoundingClientRect().left; // Adjust with the scrollContainerEl horizontal scroll position. const horizontalScrollOffset = scrollContainerDiv.scrollLeft - horizontalOffset; // Read left offset of the resized header. const headerOffsetLeft = this.headerBeingResized.el.offsetLeft || 0; // Calculate the desired width. const width = horizontalScrollOffset + event.clientX - headerOffsetLeft; return { ...column, // Update the column object with the new size value, enforce our minimum size. gridTrackSize: `${Math.max(minColumnGridTrackSize, width)}px` }; } // For the other headers which don't have a set width, fix it to their computed width. if (column.gridTrackSize.startsWith('minmax')) { return { ...column, // isn't fixed yet (it would be a px value) gridTrackSize: `${_parseInt(this.thEls[i].clientWidth)}px` }; } return column; }); /* * Update the column sizes. * Note: grid-template-columns sets the width for all columns in one value. */ this.updateGridColumnsSize(); }); }); this.resizeHandleContainerMouseUp$.pipe(takeUntil(this.unsubscribe$)).subscribe(() => { if (this.headerBeingResized) { this.columns = this.columns.map(column => { if (column.name === this.headerBeingResized.columnName) { return { ...column, headerCSSClassName: without(column.headerCSSClassName, 'header--being-resized') }; } return column; }); this.headerBeingResized = undefined; this.styles = { ...this.styles, tableCursor: 'auto' }; } }); } clearMouseHighlights() { if (window.getSelection) { window.getSelection().removeAllRanges(); } } _getCellRendererSpec({ type, value, row, columnName }) { const column = this.columns.find(({ name }) => name === columnName); const columnRenderer = this.getColumnRenderer(column); const rendererTemplate = get(columnRenderer, `${type === 'HEADER' ? 'headerCellRendererDef' : 'cellRendererDef'}.template`); const { cellRendererComponent, headerCellRendererComponent } = column; const rendererComponent = type === 'HEADER' ? headerCellRendererComponent : cellRendererComponent; return { renderer: rendererTemplate || rendererComponent, context: { value, item: row, property: column } }; } getColumnRenderer(column) { return this.columnRenderers.toArray().find(({ name }) => name === column.name); } updateFilteringLabelsParams(stats) { this.filteringLabelsParams = { filteredItemsCount: stats.filteredSize, allItemsCount: stats.size }; } updatePaginationLabelParams(stats) { if (stats.nextPage) { this.pagination = { ...this.pagination, nextPage: stats.nextPage }; } const pageFirstItemIdx = (stats.currentPage - 1) * stats.firstPageSize + 1; this.paginationLabelParams = { pageFirstItemIdx, pageLastItemIdx: pageFirstItemIdx + (stats.currentPageSize - 1), itemsTotal: stats.filteredSize }; } updatePaginationWhenNoDevicesLastPage(stats) { if (!stats.nextPage && stats.currentPageSize === 0 && stats.size > 0) { this.pagination = { ...this.pagination, currentPage: this.pagination.currentPage - 1, nextPage: null }; } } createLoadMoreComponent(stats) { if (this.infiniteScroll && stats && stats.nextPage && (!this.loadMoreComponent || this.recreateLoadMoreComponent)) { this.recreateLoadMoreComponent = false; this.infiniteScrollContainer.clear(); const componentRef = this.infiniteScrollContainer.createComponent(LoadMoreComponent); const instance = componentRef.instance; instance.useIntersection = this.infiniteScroll === 'auto' || this.infiniteScroll === 'hidden'; instance.hidden = this.infiniteScroll === 'hidden'; instance.paging = { nextPage: stats.nextPage, next: this.loadNextPage.bind(this) }; instance.loadNextLabel = this.loadMoreItemsLabel; instance.loadingLabel = this.loadingItemsLabel; this.loadMoreComponent = instance; } else if (this.loadMoreComponent && !stats.nextPage) { this.loadMoreComponent.paging = { nextPage: null }; } } scrollToTop() { if (this.infiniteScroll) { this.scrollContainer.nativeElement.scrollTop = 0; } } processAndPersistConfigChange() { merge(merge(this.onSort, this.onPageSizeChange, this.onColumnReordered, this.onColumnVisibilityChange).pipe(map(config => config.columns)), merge(this.onAddCustomColumn, this.onRemoveCustomColumn).pipe(map(() => (this.columns || []).map(this.mapColumnToConfig.bind(this)))), this.onFilter.pipe(map(({ columnName, filteringModifier }) => this.columns.map(this.mapColumnToConfig.bind(this)).map((column) => { if (isNil(columnName)) { delete column.filter; } else if (column.name === columnName) { if (isEmpty(filteringModifier)) { delete column.filter; } else { column.filter = filteringModifier; } } return column; })))) .pipe(map((columns) => ({ columns, pagination: { pageSize: this.pagination.pageSize } })), filter(() => !!this.configurationStrategy), this.trimFilterConfigPipe(), this.trimSortConfigPipe(), this.trimCustomColumnConfigPipe(), this.ignoreColumnOrderPipe(), this.ignoreColumnVisibilityPipe(), concatMap((config) => this.configurationStrategy.saveConfig$(config)), takeUntil(this.unsubscribe$)) .subscribe(); } trimFilterConfigPipe() { return pipe(this.checkEventPipe('filter', config => { config.columns = (config.columns || []).map(col => { delete col.filter; return col; }); return config; })); } trimSortConfigPipe() { return pipe(this.checkEventPipe('sort', config => { config.columns = (config.columns || []).map(col => { col.sortOrder = ''; return col; }); return config; })); } trimCustomColumnConfigPipe() { return pipe(this.checkEventPipe('customColumns', config => { config.columns = (config.columns || []).filter((col) => !col.custom); return config; })); } ignoreColumnOrderPipe() { return pipe(this.checkEventPipe('order', config => { return this.configurationStrategy.getConfig$().pipe(map(oldConfig => { const oldColumns = oldConfig?.columns || this.defaultColumns; // check if custom columns have been added const columnsAdded = (config.columns || []).filter(col => !oldColumns.find(old => old.name === col.name)); config.columns = [ ...oldColumns.map(oldCol => (config.columns || []).find(newCol => newCol.name === oldCol.name)), ...columnsAdded ]; return config; })); })); } ignoreColumnVisibilityPipe() { return pipe(this.checkEventPipe('visibility', config => { return this.configurationStrategy.getConfig$().pipe(map(oldConfig => { config.columns = (config.columns || []).map(newCol => { const columns = oldConfig?.columns || this.defaultColumns; const oldCol = columns.find((col) => newCol.name === col.name); newCol.visible = oldCol?.visible ?? true; return newCol; }); return config; })); })); } checkEventPipe(configPart, trimEventDataFn) { return pipe(concatMap((config) => { return this.resolveConfigFilter .call(this, configPart) .pipe(map(keepEventData => ({ config, keepEventData }))); }), map(({ config, keepEventData }) => keepEventData ? config : trimEventDataFn.call(this, config)), concatMap(config => (isObservable(config) ? config : of(config)))); } resolveConfigFilter(configPart) { let result; const valueOrFn = this.configurationStrategy.getContext()?.configFilter?.[configPart]; if (typeof valueOrFn === 'function') { result = valueOrFn(); } else { result = valueOrFn; } return toObservable(result ?? true); } safelyInvokeMatcher(matchesFn, route, context) { if (matchesFn) { try { return matchesFn(route, context); } catch (e) { return false; } } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DataGridComponent, deps: [{ token: DATA_GRID_CONFIGURATION_STRATEGY, optional: true }, { token: i1.DataGridService }, { token: i2.DomSanitizer }, { token: i3.GainsightService }, { token: i4.BsModalService }, { token: i5.AlertService }, { token: i6.ActionControlsExtensionService }, { token: i7.ActivatedRoute }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: DataGridComponent, selector: "c8y-data-grid", inputs: { title: "title", loadMoreItemsLabel: "loadMoreItemsLabel", loadingItemsLabel: "loadingItemsLabel", showSearch: "showSearch", refresh: "refresh", _columns: ["columns", "_columns"], _rows: ["rows", "_rows"], _pagination: ["pagination", "_pagination"], _infiniteScroll: ["infiniteScroll", "_infiniteScroll"], _serverSideDataCallback: ["serverSideDataCallback", "_serverSideDataCallback"], _selectable: ["selectable", "_selectable"], _singleSelection: ["singleSelection", "_singleSelection"], _selectionPrimaryKey: ["selectionPrimaryKey", "_selectionPrimaryKey"], _displayOptions: ["displayOptions", "_displayOptions"], _actionControls: ["actionControls", "_actionControls"], _bulkActionControls: ["bulkActionControls", "_bulkActionControls"], _headerActionControls: ["headerActionControls", "_headerActionControls"], searchText: "searchText", configureColumnsEnabled: "configureColumnsEnabled", showCounterWarning: "showCounterWarning", activeClassName: "activeClassName", expandableRows: "expandableRows" }, outputs: { rowMouseOver: "rowMouseOver", rowMouseLeave: "rowMouseLeave", rowClick: "rowClick", onConfigChange: "onConfigChange", onBeforeFilter: "onBeforeFilter", onBeforeSearch: "onBeforeSearch", onFilter: "onFilter", itemsSelect: "itemsSelect", onReload: "onReload", onAddCustomColumn: "onAddCustomColumn", onRemoveCustomColumn: "onRemoveCustomColumn", onColumnFilterReset: "onColumnFilterReset", onSort: "onSort", onPageSizeChange: "onPageSizeChange", onColumnReordered: "onColumnReordered", onColumnVisibilityChange: "onColumnVisibilityChange" }, host: { classAttribute: "d-contents" }, providers: [ { provide: PRODUCT_EXPERIENCE_EVENT_SOURCE, useExisting: forwardRef(() => DataGridComponent) } ], queries: [{ propertyName: "expandableRow", first: true, predicate: ExpandableRowDirective, descendants: true }, { propertyName: "emptyState", first: true, predicate: EmptyStateContextDirective, descendants: true }, { prop