import { animate, state, style, transition, trigger, } from '@angular/animations'; import { SelectionModel } from '@angular/cdk/collections'; import { AfterViewInit, Component, Input, OnDestroy, OnInit, ViewChild, } from '@angular/core'; import { MatCheckboxChange } from '@angular/material/checkbox'; import { MatDialog } from '@angular/material/dialog'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { NavigationStart, Router } from '@angular/router'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateService } from '@ngx-translate/core'; import * as _ from 'lodash'; import { Observable, of, Subscription, EMPTY, } from 'rxjs'; import { catchError, filter, switchMap, take, tap, } from 'rxjs/operators'; import { CoreService } from 'app/core/services/core-service/core.service'; import { PreferencesService } from 'app/core/services/preferences.service'; import { JobState } from 'app/enums/job-state.enum'; import { CoreEvent } from 'app/interfaces/events'; import { Interval } from 'app/interfaces/timeout.interface'; import { EntityTableAction, EntityTableColumn, EntityTableConfig, EntityTableConfigConfig, } from 'app/pages/common/entity/entity-table/entity-table.interface'; import { DialogService, JobService } from 'app/services'; import { AppLoaderService } from 'app/services/app-loader/app-loader.service'; import { ModalService } from 'app/services/modal.service'; import { StorageService } from 'app/services/storage.service'; import { WebSocketService } from 'app/services/ws.service'; import { T } from 'app/translate-marker'; import { EmptyConfig, EmptyType } from '../entity-empty/entity-empty.component'; import { EntityJobComponent } from '../entity-job/entity-job.component'; import { EntityUtils } from '../utils'; import { EntityTableAddActionsComponent } from './entity-table-add-actions.component'; export interface Command { command: string; // Use '|' or '--pipe' to use the output of previous command as input input: any; options?: any[]; // Function parameters } @UntilDestroy() @Component({ selector: 'entity-table', templateUrl: './entity-table.component.html', styleUrls: ['./entity-table.component.scss'], providers: [DialogService, StorageService], animations: [ trigger('detailExpand', [ state('collapsed', style({ height: '0px', minHeight: '0' })), state('expanded', style({ height: '*' })), transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')), ]), ], }) export class EntityTableComponent<Row = any> implements OnInit, AfterViewInit, OnDestroy { @Input() title = ''; @Input() conf: EntityTableConfig; @ViewChild('newEntityTable', { static: false }) entitytable: any; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; // MdPaginator Inputs paginationPageSize = 10; paginationPageSizeOptions: number[] = [5, 10, 25, 100]; paginationPageIndex = 0; paginationShowFirstLastButtons = true; hideTopActions = false; displayedColumns: string[] = []; firstUse = true; emptyTableConf: EmptyConfig = { type: EmptyType.loading, large: true, title: this.title, }; isTableEmpty = true; selection = new SelectionModel<any>(true, []); busy: Subscription; columns: any[] = []; /** * Need this for the checkbox headings */ allColumns: EntityTableColumn[] = []; /** * Show the column filters by default */ columnFilter = true; /** * ...for the filter function - becomes THE complete list of all columns, diplayed or not */ filterColumns: EntityTableColumn[] = []; /** * For cols the user can't turn off. */ alwaysDisplayedCols: EntityTableColumn[] = []; /** * Stores a pristine/touched state for checkboxes */ anythingClicked = false; /** * The factory setting. */ originalConfColumns: EntityTableColumn[] = []; colMaxWidths: { name: string; maxWidth: number }[] = []; expandedRows = document.querySelectorAll('.expanded-row').length; expandedElement: any | null = null; dataSource: MatTableDataSource<any>; rows: any[] = []; currentRows: any[] = []; // Rows applying filter getFunction: Observable<any>; config: EntityTableConfigConfig = { paging: true, sorting: { columns: this.columns }, }; asyncView = false; // default table view is not async showDefaults = false; showSpinner = false; cardHeaderReady = false; showActions = true; hasActions = true; sortKey: string; filterValue = ''; // the filter string filled in search input. readonly EntityJobState = JobState; // Global Actions in Page Title protected actionsConfig: any; loaderOpen = false; protected toDeleteRow: Row; private interval: Interval; excuteDeletion = false; needRefreshTable = false; private routeSub: Subscription; multiActionsIconsOnly = false; get currentColumns(): EntityTableColumn[] { const result = this.alwaysDisplayedCols.concat(this.conf.columns) as any; // Actions without expansion if (this.hasActions && result[result.length - 1] !== 'action' && (this.hasDetails() === false || !this.hasDetails)) { result.push({ prop: 'action' }); } // Expansion if (this.hasDetails() === true) { result.push({ prop: 'expansion-chevrons' }); } if (this.conf.config.multiSelect) { result.unshift({ prop: 'multiselect' }); } return result; } hasDetails = (): boolean => { return Boolean(this.conf.rowDetailComponent) || (this.allColumns.length > 0 && this.conf.columns.length !== this.allColumns.length); }; isAllSelected = false; constructor( protected core: CoreService, protected router: Router, public ws: WebSocketService, public dialogService: DialogService, public loader: AppLoaderService, protected translate: TranslateService, public storageService: StorageService, protected job: JobService, protected prefService: PreferencesService, protected matDialog: MatDialog, public modalService: ModalService, ) { this.core.register({ observerClass: this, eventName: 'UserPreferencesChanged' }).pipe(untilDestroyed(this)).subscribe((evt: CoreEvent) => { this.multiActionsIconsOnly = evt.data.preferIconsOnly; }); this.core.emit({ name: 'UserPreferencesRequest', sender: this }); // watch for navigation events as ngOnDestroy doesn't always trigger on these this.routeSub = this.router.events.pipe(untilDestroyed(this)).subscribe((event) => { if (event instanceof NavigationStart) { this.cleanup(); } }); } ngOnDestroy(): void { this.cleanup(); } cleanup(): void { this.core.unregister({ observerClass: this }); if (this.interval) { clearInterval(this.interval); } if (!this.routeSub.closed) { this.routeSub.unsubscribe(); } } pageChanged(): void { this.selection.clear(); } get currentRowsThatAreOnScreenToo(): any[] { let currentlyShowingRows = [...this.dataSource.filteredData]; if (this.dataSource.paginator) { const start = this.dataSource.paginator.pageIndex * this.dataSource.paginator.pageSize; const rowsCount = currentlyShowingRows.length < start + this.dataSource.paginator.pageSize ? currentlyShowingRows.length - start : this.dataSource.paginator.pageSize; currentlyShowingRows = currentlyShowingRows.splice(start, rowsCount); } const showingRows = currentlyShowingRows; return this.currentRows.filter((row) => { const index = showingRows.findIndex((showingRow: any) => { return showingRow['multiselect_id'] === row['multiselect_id']; }); return index >= 0; }); } toggleSelection(element: any): void { this.selection.toggle(element); const allShown = this.currentRowsThatAreOnScreenToo; for (const row of allShown) { if (!this.selection.isSelected(row)) { this.isAllSelected = false; return; } } this.isAllSelected = true; } ngOnInit(): void { this.actionsConfig = { actionType: EntityTableAddActionsComponent, actionConfig: this }; this.cardHeaderReady = !this.conf.cardHeaderComponent; this.hasActions = this.conf.noActions !== true; if (this.conf.config?.pagingOptions?.pageSize) { this.paginationPageSize = this.conf.config.pagingOptions.pageSize; } if (this.conf.config?.pagingOptions?.pageSizeOptions) { this.paginationPageSizeOptions = this.conf.config.pagingOptions.pageSizeOptions; } this.sortKey = (this.conf.config.deleteMsg && this.conf.config.deleteMsg.key_props) ? this.conf.config.deleteMsg.key_props[0] : this.conf.columns[0].prop; setTimeout(async () => { if (this.conf.prerequisite) { await this.conf.prerequisite().then( (canContinue) => { if (canContinue) { if (this.conf.preInit) { this.conf.preInit(this); } this.getData(); if (this.conf.afterInit) { this.conf.afterInit(this); } } else { this.showSpinner = false; if (this.conf.prerequisiteFailedHandler) { this.conf.prerequisiteFailedHandler(this); } } }, ); } else { if (this.conf.preInit) { this.conf.preInit(this); } this.getData(); if (this.conf.afterInit) { this.conf.afterInit(this); } } }); this.asyncView = this.conf.asyncView ? this.conf.asyncView : false; this.conf.columns.forEach((column) => { this.displayedColumns.push(column.prop); if (!column.always_display) { this.allColumns.push(column); // Make array of optionally-displayed cols } else { this.alwaysDisplayedCols.push(column); // Make an array of required cols } }); this.columnFilter = this.conf.columnFilter === undefined ? true : this.conf.columnFilter; this.showActions = this.conf.showActions === undefined ? true : this.conf.showActions; this.filterColumns = this.conf.columns; this.conf.columns = this.allColumns; // Remove any alwaysDisplayed cols from the official list for (const item of this.allColumns) { if (!item.hidden) { this.originalConfColumns.push(item); } } this.conf.columns = this.originalConfColumns; setTimeout(() => { const preferredCols = this.prefService.preferences.tableDisplayedColumns; // Turn off preferred cols for snapshots to allow for two diffferent column sets to be displayed if (preferredCols.length > 0 && this.title !== 'Snapshots') { preferredCols.forEach((i: any) => { // If preferred columns have been set for THIS table... if (i.title === this.title) { this.firstUse = false; this.conf.columns = i.cols.filter((col: any) => // Remove columns if they are already present in always displayed columns !this.alwaysDisplayedCols.find((item) => item.prop === col.prop)); // Remove columns from display and preferred cols if they don't exist in the table const notFound: any[] = []; this.conf.columns.forEach((col) => { const found = this.filterColumns.find((o) => o.prop === col.prop); if (!found) { notFound.push(col.prop); } }); this.conf.columns = this.conf.columns.filter((col) => !notFound.includes(col.prop)); this.selectColumnsToShowOrHide(); } }); if (this.title === 'Users') { // Makes a list of the table's column maxWidths this.filterColumns.forEach((column) => { const tempObj: any = {}; tempObj['name'] = column.name; tempObj['maxWidth'] = column.maxWidth; this.colMaxWidths.push(tempObj); }); this.conf.columns = this.dropLastMaxWidth(); } } if (this.firstUse) { this.selectColumnsToShowOrHide(); } }, this.prefService.preferences.tableDisplayedColumns.length === 0 ? 200 : 0); this.displayedColumns.push('action'); if (this.conf.changeEvent) { this.conf.changeEvent(this); } if (typeof (this.conf.hideTopActions) !== 'undefined') { this.hideTopActions = this.conf.hideTopActions; } // Delay spinner 500ms so it won't show up on a fast-loading page setTimeout(() => { this.setShowSpinner(); }, 500); } ngAfterViewInit(): void { // If actionsConfig was disabled, don't show the default toolbar. like the Table is in a Tab. if (!this.conf.disableActionsConfig) { // Setup Actions in Page Title Component this.core.emit({ name: 'GlobalActions', data: this.actionsConfig, sender: this }); } } // Filter the table by the filter string. filter(filterValue: string): void { this.filterValue = filterValue; if (filterValue.length > 0) { this.dataSource.filter = filterValue; } else { this.dataSource.filter = ''; } if (this.dataSource.filteredData && this.dataSource.filteredData.length) { this.isTableEmpty = false; } else { this.isTableEmpty = true; this.configureEmptyTable( this.dataSource.filter ? EmptyType.no_search_results : this.firstUse ? EmptyType.first_use : EmptyType.no_page_data, ); } if (this.dataSource.paginator && this.conf.config.paging) { this.dataSource.paginator.firstPage(); } this.isAllSelected = false; this.selection.clear(); } configureEmptyTable(emptyType: EmptyType, error: any = null): void { if (!emptyType) { return; } let title = ''; let message = ''; let messagePreset = false; switch (emptyType) { case EmptyType.loading: this.emptyTableConf = { type: EmptyType.loading, large: true, title: this.title, }; break; case EmptyType.no_search_results: title = T('No Search Results.'); message = T('Your query didn\'t return any results. Please try again.'); if (this.conf.emptyTableConfigMessages && this.conf.emptyTableConfigMessages.no_search_results) { title = this.conf.emptyTableConfigMessages.no_search_results.title; message = this.conf.emptyTableConfigMessages.no_search_results.message; } this.emptyTableConf = { type: EmptyType.no_search_results, large: true, title, message, }; break; case EmptyType.errors: title = T('Something went wrong'); message = T('The system returned the following error - '); if (this.conf.emptyTableConfigMessages && this.conf.emptyTableConfigMessages.errors) { title = this.conf.emptyTableConfigMessages.errors.title; message = this.conf.emptyTableConfigMessages.errors.message; } this.emptyTableConf = { title, message: message + error, large: true, type: EmptyType.errors, }; break; case EmptyType.first_use: messagePreset = false; title = T('No ') + this.title; message = T('It seems you haven\'t setup any ') + this.title + T(' yet.'); if (this.conf.emptyTableConfigMessages && this.conf.emptyTableConfigMessages.first_use) { title = this.conf.emptyTableConfigMessages.first_use.title; message = this.conf.emptyTableConfigMessages.first_use.message; messagePreset = true; } this.emptyTableConf = { type: EmptyType.first_use, large: true, title, message, }; if (!this.conf.noAdd) { if (!messagePreset) { this.emptyTableConf['message'] += T(' Please click the button below to add ') + this.title + T('.'); } let buttonText = T('Add ') + this.title; if (this.conf.emptyTableConfigMessages && this.conf.emptyTableConfigMessages.buttonText) { buttonText = this.conf.emptyTableConfigMessages.buttonText; } this.emptyTableConf['button'] = { label: buttonText, action: this.doAdd.bind(this), }; } break; case EmptyType.no_page_data: default: messagePreset = false; title = T('No ') + this.title; message = T('The system could not retrieve any ') + this.title + T(' from the database.'); if (this.conf.emptyTableConfigMessages && this.conf.emptyTableConfigMessages.no_page_data) { title = this.conf.emptyTableConfigMessages.no_page_data.title; message = this.conf.emptyTableConfigMessages.no_page_data.message; messagePreset = true; } this.emptyTableConf = { type: EmptyType.no_page_data, large: true, title, message, }; if (!this.conf.noAdd) { if (!messagePreset) { this.emptyTableConf['message'] += T(' Please click the button below to add ') + this.title + T('.'); } let buttonText = T('Add ') + this.title; if (this.conf.emptyTableConfigMessages && this.conf.emptyTableConfigMessages.buttonText) { buttonText = this.conf.emptyTableConfigMessages.buttonText; } this.emptyTableConf['button'] = { label: buttonText, action: this.doAdd.bind(this), }; } break; } } dropLastMaxWidth(): EntityTableColumn[] { // Reset all column maxWidths this.conf.columns.forEach((column) => { if (this.colMaxWidths.length > 0) { column['maxWidth'] = (this.colMaxWidths.find(({ name }) => name === column.name)).maxWidth; } }); // Delete maXwidth on last col displayed (prevents a display glitch) if (this.conf.columns.length > 0) { delete (this.conf.columns[Object.keys(this.conf.columns).length - 1]).maxWidth; } return this.conf.columns; } setShowSpinner(): void { this.showSpinner = true; } getData(): void { const sort: string[] = []; for (const i in this.config.sorting.columns) { const col = this.config.sorting.columns[i]; if (col.sort === 'asc') { sort.push(col.name); } else if (col.sort === 'desc') { sort.push('-' + col.name); } } const options: any = { limit: 0 }; if (sort.length > 0) { options['sort'] = sort.join(','); } if (this.conf.queryCall) { if (this.conf.queryCallJob) { if (this.conf.queryCallOption) { this.getFunction = this.ws.job(this.conf.queryCall, this.conf.queryCallOption); } else { this.getFunction = this.ws.job(this.conf.queryCall, []); } } else if (this.conf.queryCallOption) { this.getFunction = this.ws.call(this.conf.queryCall, this.conf.queryCallOption); } else { this.getFunction = this.ws.call(this.conf.queryCall, []); } } else { this.getFunction = EMPTY; } if (this.conf.callGetFunction) { this.conf.callGetFunction(this); } else { this.callGetFunction(); } if (this.asyncView) { this.interval = setInterval(() => { if (this.conf.callGetFunction) { this.conf.callGetFunction(this); } else { this.callGetFunction(true); } }, 10000); } } callGetFunction(skipActions = false): void { this.getFunction.pipe(untilDestroyed(this)).subscribe( (res: any) => { this.handleData(res, skipActions); }, (res: any) => { this.isTableEmpty = true; this.configureEmptyTable(EmptyType.errors, res); if (this.loaderOpen) { this.loader.close(); this.loaderOpen = false; } if (res.hasOwnProperty('reason') && (res.hasOwnProperty('trace') && res.hasOwnProperty('type'))) { this.dialogService.errorReport(res.type || res.trace.class, res.reason, res.trace.formatted); } else { new EntityUtils().handleError(this, res); } }, ); } handleData(res: any, skipActions = false): any { this.expandedRows = document.querySelectorAll('.expanded-row').length; const cache = this.expandedElement; this.expandedElement = this.expandedRows > 0 ? cache : null; if (typeof (res) === 'undefined' || typeof (res.data) === 'undefined') { res = { data: res, }; } if (res.data) { if (typeof (this.conf.resourceTransformIncomingRestData) !== 'undefined') { res.data = this.conf.resourceTransformIncomingRestData(res.data); for (const prop of ['schedule', 'cron_schedule', 'cron', 'scrub_schedule']) { if (res.data.length > 0 && res.data[0].hasOwnProperty(prop) && typeof res.data[0][prop] === 'string') { res.data.map((row: any) => row[prop] = new EntityUtils().parseDOW(row[prop])); } } } } else if (typeof (this.conf.resourceTransformIncomingRestData) !== 'undefined') { res = this.conf.resourceTransformIncomingRestData(res); for (const prop of ['schedule', 'cron_schedule', 'cron', 'scrub_schedule']) { if (res.length > 0 && res[0].hasOwnProperty(prop) && typeof res[0][prop] === 'string') { res.map((row: any) => row[prop] = new EntityUtils().parseDOW(row[prop])); } } } this.rows = this.generateRows(res); if (!skipActions) { this.storageService.tableSorter(this.rows, this.sortKey, 'asc'); } if (this.conf.dataHandler) { this.conf.dataHandler(this); } if (this.conf.addRows) { this.conf.addRows(this); } if (!this.showDefaults) { this.currentRows = this.filterValue === '' ? this.rows : this.currentRows; this.paginationPageIndex = 0; this.showDefaults = true; } if ((this.expandedRows === 0 || !this.asyncView || this.excuteDeletion || this.needRefreshTable) && this.filterValue === '') { this.excuteDeletion = false; this.needRefreshTable = false; this.currentRows = this.rows; this.paginationPageIndex = 0; } if (this.currentRows && this.currentRows.length > 0) { this.isTableEmpty = false; } else { this.isTableEmpty = true; this.configureEmptyTable(this.firstUse ? EmptyType.first_use : EmptyType.no_page_data); } for (let i = 0; i < this.currentRows.length; i++) { this.currentRows[i].multiselect_id = i; } this.dataSource = new MatTableDataSource(this.currentRows); this.dataSource.sort = this.sort; this.filter(this.filterValue); if (this.conf.config.paging) { // On first load, paginator is not rendered because table is empty, // so we force render here so that we can get valid paginator instance setTimeout(() => { this.dataSource.paginator = this.paginator; }, 0); } return res; } isLeftStickyColumnNo(i: number): boolean { return i === (this.currentColumns[0].prop === 'multiselect' ? 1 : 0); } shouldApplyStickyOffset(i: number): boolean { return this.currentColumns[0].prop === 'multiselect' && i === 1; } isTableOverflow(): boolean { let hasHorizontalScrollbar = false; if (this.entitytable) { const parentNode = this.entitytable._elementRef.nativeElement.parentNode; hasHorizontalScrollbar = parentNode.scrollWidth > parentNode.clientWidth; } return hasHorizontalScrollbar; } generateRows(res: any): any[] { let rows: any[]; if (this.loaderOpen) { this.loader.close(); this.loaderOpen = false; } if (res.data) { if (res.data.result) { rows = new EntityUtils().flattenData(res.data.result); } else { rows = new EntityUtils().flattenData(res.data); } } else { rows = new EntityUtils().flattenData(res); } for (let i = 0; i < rows.length; i++) { for (const attr in rows[i]) { if (rows[i].hasOwnProperty(attr)) { rows[i][attr] = this.rowValue(rows[i], attr); } } } if (this.rows.length === 0) { if (this.conf.queryRes) { this.conf.queryRes = rows; } } else { for (let i = 0; i < this.currentRows.length; i++) { const index = _.findIndex(rows, { id: this.currentRows[i].id }); if (index > -1) { for (const prop in rows[index]) { this.currentRows[i][prop] = rows[index][prop]; } } } const newRows = []; for (let i = 0; i < this.rows.length; i++) { const index = _.findIndex(rows, { id: this.rows[i].id }); if (index < 0) { continue; } const updatedItem = rows[index]; rows.splice(index, 1); newRows.push(updatedItem); } return newRows.concat(rows); } return rows; } getActions(row: Row): EntityTableAction[] { if (this.conf.getActions) { return this.conf.getActions(row); } return [{ name: 'edit', id: 'edit', icon: 'edit', label: T('Edit'), onClick: (rowinner: any) => { this.doEdit(rowinner.id); }, }, { name: 'delete', id: 'delete', icon: 'delete', label: T('Delete'), onClick: (rowinner: any) => { this.doDelete(rowinner); }, }] as EntityTableAction[]; } getAddActions(): EntityTableAction[] { if (this.conf.getAddActions) { return this.conf.getAddActions(); } return []; } rowValue(row: any, attr: string): any { if (this.conf.rowValue) { try { return this.conf.rowValue(row, attr); } catch (e) { return row[attr]; } } return row[attr]; } doAdd(): void { if (this.conf.doAdd) { this.conf.doAdd(null, this); } else { this.router.navigate(new Array('/').concat(this.conf.route_add)); } // this.modalService.open('slide-in-form', this.conf.addComponent); } doEdit(id: string | number): void { if (this.conf.doEdit) { this.conf.doEdit(id, this); } else { this.router.navigate( new Array('/').concat(this.conf.route_edit).concat(id as any), ); } } // generate delete msg getDeleteMessage(item: any, action = T('Delete ')): string { let deleteMsg = T('Delete the selected item?'); if (this.conf.config.deleteMsg) { deleteMsg = action + this.conf.config.deleteMsg.title; let msg_content = ' <b>' + item[this.conf.config.deleteMsg.key_props[0]]; if (this.conf.config.deleteMsg.key_props.length > 1) { for (let i = 1; i < this.conf.config.deleteMsg.key_props.length; i++) { if (item[this.conf.config.deleteMsg.key_props[i]] !== '') { msg_content = msg_content + ' - ' + item[this.conf.config.deleteMsg.key_props[i]]; } } } msg_content += '</b>?'; deleteMsg += msg_content; } this.translate.get(deleteMsg).pipe(untilDestroyed(this)).subscribe((res) => { deleteMsg = res; }); return deleteMsg; } doDelete(item: any, action?: any): void { const deleteMsg = this.conf.confirmDeleteDialog && this.conf.confirmDeleteDialog.isMessageComplete ? '' : this.getDeleteMessage(item, action); let id: string; if (this.conf.config.deleteMsg && this.conf.config.deleteMsg.id_prop) { id = item[this.conf.config.deleteMsg.id_prop]; } else { id = item.id; } const dialog = this.conf.confirmDeleteDialog || {}; if (dialog.buildTitle) { dialog.title = dialog.buildTitle(item); } if (dialog.buttonMsg) { dialog.button = dialog.buttonMsg(item); } if (this.conf.config.deleteMsg && this.conf.config.deleteMsg.doubleConfirm) { // double confirm: input delete item's name to confirm deletion this.conf.config.deleteMsg.doubleConfirm(item) .pipe(untilDestroyed(this)) .subscribe((doubleConfirmDialog: boolean) => { if (doubleConfirmDialog) { this.toDeleteRow = item; this.delete(id); } }); } else { this.dialogService.confirm({ title: dialog.hasOwnProperty('title') ? dialog['title'] : T('Delete'), message: dialog.hasOwnProperty('message') ? dialog['message'] + deleteMsg : deleteMsg, hideCheckBox: dialog.hasOwnProperty('hideCheckbox') ? dialog['hideCheckbox'] : false, buttonMsg: dialog.hasOwnProperty('button') ? dialog['button'] : T('Delete'), }).pipe(untilDestroyed(this)).subscribe((res) => { if (res) { this.toDeleteRow = item; this.delete(id); } }); } } delete(id: string): void { this.loader.open(); this.loaderOpen = true; this.busy = this.ws.call( this.conf.wsDelete, this.conf.wsDeleteParams ? this.conf.wsDeleteParams(this.toDeleteRow, id) : [id], ).pipe(untilDestroyed(this)).subscribe( () => { this.getData(); this.excuteDeletion = true; if (this.conf.afterDelete) { this.conf.afterDelete(); } }, (resinner) => { new EntityUtils().handleWSError(this, resinner, this.dialogService); this.loader.close(); }, ); } doDeleteJob(item: any): Observable<{ state: JobState } | boolean> { const deleteMsg = this.getDeleteMessage(item); let id: string; if (this.conf.config.deleteMsg && this.conf.config.deleteMsg.id_prop) { id = item[this.conf.config.deleteMsg.id_prop]; } else { id = item.id; } let dialog: any = {}; if (this.conf.confirmDeleteDialog) { dialog = this.conf.confirmDeleteDialog; } return this.dialogService .confirm({ title: dialog.hasOwnProperty('title') ? dialog['title'] : T('Delete'), message: dialog.hasOwnProperty('message') ? dialog['message'] + deleteMsg : deleteMsg, hideCheckBox: dialog.hasOwnProperty('hideCheckbox') ? dialog['hideCheckbox'] : false, buttonMsg: dialog.hasOwnProperty('button') ? dialog['button'] : T('Delete'), }) .pipe( filter(Boolean), tap(() => { this.loader.open(); this.loaderOpen = true; }), switchMap(() => { const params = this.conf.wsDeleteParams ? this.conf.wsDeleteParams(this.toDeleteRow, id) : [id]; return this.ws.call(this.conf.wsDelete, params).pipe( take(1), catchError((error) => { new EntityUtils().handleWSError(this, error, this.dialogService); this.loader.close(); return of(false); }), ); }), switchMap((jobId: number) => (jobId ? this.job.getJobStatus(jobId) : of(false))), ); } getMultiDeleteMessage(items: any): string { let deleteMsg = 'Delete the selected items?'; if (this.conf.config.deleteMsg) { deleteMsg = 'Delete selected ' + this.conf.config.deleteMsg.title + '(s)?'; let msg_content = '<ul>'; for (let j = 0; j < items.length; j++) { let sub_msg_content; if (this.conf.config.deleteMsg.key_props.length > 1) { sub_msg_content = '<li><strong>' + items[j][this.conf.config.deleteMsg.key_props[0]] + '</strong>'; sub_msg_content += '<ul class="nested-list">'; for (let i = 1; i < this.conf.config.deleteMsg.key_props.length; i++) { if (items[j][this.conf.config.deleteMsg.key_props[i]] != '') { sub_msg_content += '<li>' + items[j][this.conf.config.deleteMsg.key_props[i]] + '</li>'; } } sub_msg_content += '</ul>'; } else { sub_msg_content = '<li>' + items[j][this.conf.config.deleteMsg.key_props[0]]; } sub_msg_content += '</li>'; msg_content += sub_msg_content; } msg_content += '</ul>'; deleteMsg += msg_content; } this.translate.get(deleteMsg).pipe(untilDestroyed(this)).subscribe((res) => { deleteMsg = res; }); return deleteMsg; } doMultiDelete(selected: any): void { const multiDeleteMsg = this.getMultiDeleteMessage(selected); this.dialogService.confirm({ title: 'Delete', message: multiDeleteMsg, hideCheckBox: false, buttonMsg: T('Delete'), }).pipe(untilDestroyed(this)).subscribe((res) => { if (!res) { return; } this.loader.open(); this.loaderOpen = true; if (this.conf.wsMultiDelete) { // ws to do multi-delete if (this.conf.wsMultiDeleteParams) { this.busy = this.ws.job(this.conf.wsMultiDelete, this.conf.wsMultiDeleteParams(selected)) .pipe(untilDestroyed(this)) .subscribe( (res1) => { if (res1.state === JobState.Success) { this.loader.close(); this.loaderOpen = false; this.getData(); // this.selected = []; this.selection.clear(); const selectedName = this.conf.wsMultiDeleteParams(selected)[1]; let message = ''; for (let i = 0; i < res1.result.length; i++) { if (res1.result[i].error != null) { message = message + '<li>' + selectedName[i] + ': ' + res1.result[i].error + '</li>'; } } if (message === '') { this.dialogService.Info(T('Items deleted'), '', '300px', 'info', true); } else { message = '<ul>' + message + '</ul>'; this.dialogService.errorReport(T('Items Delete Failed'), message); } } }, (res1) => { new EntityUtils().handleWSError(this, res1, this.dialogService); this.loader.close(); this.loaderOpen = false; }, ); } } else { // rest to do multi-delete } }); } // Next section operates the checkboxes to show/hide columns toggle(col: any): void { const isChecked = this.isChecked(col); this.anythingClicked = true; if (isChecked) { this.conf.columns = this.conf.columns.filter((c) => c.name !== col.name); } else { this.conf.columns = [...this.conf.columns, col]; } this.selectColumnsToShowOrHide(); } // Stores currently selected columns in preference service selectColumnsToShowOrHide(): void { const obj: any = {}; obj['title'] = this.title; obj['cols'] = this.conf.columns; const preferredCols = this.prefService.preferences.tableDisplayedColumns; if (preferredCols.length > 0) { preferredCols.forEach((i: any) => { if (i.title === this.title) { preferredCols.splice(preferredCols.indexOf(i), 1); } }); } preferredCols.push(obj); this.prefService.savePreferences(this.prefService.preferences); if (this.title === 'Users') { this.conf.columns = this.dropLastMaxWidth(); } } // resets col view to the default set in the table's component resetColViewToDefaults(): void { if (!(this.conf.columns.length === this.originalConfColumns.length && this.conf.columns.length === this.allColumns.length)) { this.conf.columns = this.originalConfColumns; this.selectColumnsToShowOrHide(); } } isChecked(col: any): boolean { return this.conf.columns.find((c) => c.name === col.name) !== undefined; } // Toggle between all/none cols selected checkAll(): EntityTableColumn[] { this.anythingClicked = true; if (this.conf.columns.length < this.allColumns.length) { this.conf.columns = this.allColumns; this.selectColumnsToShowOrHide(); } else { this.conf.columns = []; this.selectColumnsToShowOrHide(); } return this.conf.columns; } // Used by the select all checkbox to determine whether it should be checked checkLength(): boolean { if (this.allColumns && this.conf.columns) { return this.conf.columns.length === this.allColumns.length; } } toggleLabels(): void { this.multiActionsIconsOnly = !this.multiActionsIconsOnly; } getButtonClass(state: JobState): string { switch (state) { case JobState.Pending: return 'fn-theme-orange'; case JobState.Running: return 'fn-theme-orange'; case JobState.Aborted: return 'fn-theme-orange'; case JobState.Finished: return 'fn-theme-green'; case JobState.Success: return 'fn-theme-green'; case JobState.Error: return 'fn-theme-red'; case JobState.Failed: return 'fn-theme-red'; case JobState.Hold: return 'fn-theme-yellow'; default: return 'fn-theme-primary'; } } stateClickable(value: any, colConfig: any): boolean { if (colConfig.infoStates) { return _.indexOf(colConfig.infoStates, value) < 0; } return value !== JobState.Pending; } runningStateButton(jobid: number): void { const dialogRef = this.matDialog.open(EntityJobComponent, { data: { title: T('Task is running') }, disableClose: false }); dialogRef.componentInstance.jobId = jobid; dialogRef.componentInstance.wsshow(); dialogRef.componentInstance.success.pipe(untilDestroyed(this)).subscribe(() => { dialogRef.close(); }); dialogRef.componentInstance.failure.pipe(untilDestroyed(this)).subscribe(() => { dialogRef.close(); }); } get columnsProps(): string[] { return this.currentColumns.map((column) => column.prop); } masterToggle(event: MatCheckboxChange): void { const showingRows = this.currentRowsThatAreOnScreenToo; this.isAllSelected = event.checked; if (event.checked) { showingRows.forEach((row) => { this.selection.select(row); }); } else { this.selection.clear(); } } getFirstKey(): string { return this.conf.config.multiSelect ? this.currentColumns[1].prop : this.currentColumns[0].prop; } onHover(evt: MouseEvent, over = true): void { const row = this.findRow(evt); const cells = row.children; for (let i = 0; i < cells.length; i++) { const cell = cells[i]; if (cell.classList.contains('mat-table-sticky') || cell.classList.contains('threedot-column')) { if (over) { cell.classList.add('hover'); } else { cell.classList.remove('hover'); } } } } findRow(event: MouseEvent): HTMLElement { let target = event.target as HTMLElement; do { target = target.parentElement; } while (target.tagName.toLowerCase() !== 'tr'); return target; } isInteractive(column: string): boolean { const item = this.currentColumns.find((obj) => obj.prop === column); return (item?.checkbox || item?.toggle || item?.button); } doRowClick(element: Row): void { if (this.conf.onRowClick) { this.conf.onRowClick(element); } else { this.expandedElement = this.expandedElement === element ? null : element; } } }