import {Component, Input, OnInit, ViewChild} from '@angular/core'; import {ApiService, ResourceNotAvailableOfflineError} from '../../../service/api.service'; import {AlertController, LoadingController, ModalController} from '@ionic/angular'; import {ColumnsComponent} from './columns/columns.component'; import {AgGridAngular} from 'ag-grid-angular'; import {NumericEditorComponent} from './editors/numeric/numeric-editor.component'; import {ParagraphEditorComponent} from './editors/paragraph/paragraph-editor.component'; import {BooleanEditorComponent} from './editors/boolean/boolean-editor.component'; import {SelectEditorComponent} from './editors/select/select-editor.component'; import {MultiSelectEditorComponent} from './editors/select/multiselect-editor.component'; import {DatetimeEditorComponent} from './editors/datetime/datetime-editor.component'; import {DatetimeRendererComponent} from './renderers/datetime-renderer.component'; import {CurrencyRendererComponent} from './renderers/currency-renderer.component'; import {BooleanRendererComponent} from './renderers/boolean-renderer.component'; import {EditorNodeContract} from '../../nodes/EditorNode.contract'; import {EditorService} from '../../../service/editor.service'; import {WysiwygEditorComponent} from './editors/wysiwyg/wysiwyg-editor.component'; import {debounce, debug} from '../../../utility'; import {DateTimeFilterComponent} from './filters/date-time.filter'; @Component({ selector: 'editor-database', templateUrl: './database.component.html', styleUrls: ['./database.component.scss'], }) export class DatabaseComponent extends EditorNodeContract implements OnInit { @Input() nodeId: string; @Input() editorUUID?: string; @ViewChild('agGridElement') agGridElement: AgGridAngular; frameworkComponents = { agDateInput: DateTimeFilterComponent }; public dbRecord: any; public pendingSetup = true; public dirty = false; public lastClickRow = -1; public dbName = ''; public notAvailableOffline = false; protected dbId!: string; protected isInitialLoad = false; protected triggerSaveDebounce = debounce(() => { if ( this.agGridElement.api.getCellEditorInstances().length < 1 ) { this.editorService.triggerSave(); } else { this.triggerSaveDebounce(); } }, 1000); title = 'app'; columnDefs = []; rowData = []; public isDark() { return document.body.classList.contains('dark'); } public get readonly() { return !this.node || !this.editorService.canEdit(); } constructor( protected api: ApiService, protected modals: ModalController, protected alerts: AlertController, protected loader: LoadingController, public editorService: EditorService, ) { super(); } public isDirty(): boolean | Promise { return this.dirty; } public needsSave(): boolean | Promise { return this.dirty; } public needsLoad(): boolean | Promise { return this.node && this.pendingSetup; } public writeChangesToNode(): void | Promise { this.node.Value.Mode = 'database'; } ngOnInit() { this.editorService = this.editorService.getEditor(this.editorUUID); this.editorService.registerNodeEditor(this.nodeId, this).then(() => { }); } onGridReady($event) { } onColumnResize($event) { if ( $event.source === 'uiColumnDragged' && $event.finished ) { debug('Column resized: ', $event); const state = $event.columnApi.getColumnState().find(x => x.colId === $event.column.colId ); if ( state ) { const colDef = this.columnDefs.find(x => x.field === $event.column.colId); if ( colDef ) { colDef.width = state.width; this.dirty = true; this.triggerSaveDebounce(); } } } } onCellValueChanged() { if ( !this.isInitialLoad ) { this.dirty = true; this.triggerSaveDebounce(); } } async onManageColumns() { if ( this.readonly ) { return; } const modal = await this.modals.create({ component: ColumnsComponent, componentProps: {columnSets: this.columnDefs}, }); modal.onDidDismiss().then(result => { this.setColumns(result.data); }); await modal.present(); } onInsertRow() { if ( this.readonly ) { return; } this.rowData.push({}); this.agGridElement.api.setRowData(this.rowData); this.dirty = true; this.triggerSaveDebounce(); } async onRemoveRow() { if ( this.readonly ) { return; } const alert = await this.alerts.create({ header: 'Are you sure?', message: `You are about to delete row ${this.lastClickRow + 1}. This cannot be undone.`, buttons: [ { text: 'Keep It', role: 'cancel', }, { text: 'Delete It', handler: () => { this.rowData = this.rowData.filter((x, i) => { return i !== this.lastClickRow; }); this.agGridElement.api.setRowData(this.rowData); this.lastClickRow = -1; this.dirty = true; this.triggerSaveDebounce(); }, } ], }); await alert.present(); } onRowClicked($event) { this.lastClickRow = $event.rowIndex; } setColumns(data, triggerSave = true) { this.columnDefs = data.map(x => { x.editable = !this.readonly; x.minWidth = 150; x.resizable = true; if ( x.additionalData?.width ) { x.width = x.additionalData.width; } // Set editors and renderers for different types if ( x.Type === 'text' ) { x.editor = 'agTextCellEditor'; x.filter = 'agTextColumnFilter'; } else if ( x.Type === 'number' ) { x.cellEditorFramework = NumericEditorComponent; x.filter = 'agNumberColumnFilter'; } else if ( x.Type === 'paragraph' ) { x.cellEditorFramework = ParagraphEditorComponent; x.filter = 'agTextColumnFilter'; } else if ( x.Type === 'boolean' ) { x.cellRendererFramework = BooleanRendererComponent; x.cellEditorFramework = BooleanEditorComponent; x.suppressSizeToFit = true; } else if ( x.Type === 'select' ) { x.cellEditorFramework = SelectEditorComponent; x.filter = 'agTextColumnFilter'; } else if ( x.Type === 'multiselect' ) { x.cellEditorFramework = MultiSelectEditorComponent; x.filter = 'agTextColumnFilter'; } else if ( x.Type === 'datetime' ) { x.cellEditorFramework = DatetimeEditorComponent; x.cellRendererFramework = DatetimeRendererComponent; x.filter = 'agDateColumnFilter'; x.filterParams = { buttons: ['apply', 'clear'], displayFormat: x.additionalData.format, comparator: (filterDate: Date, cellValue) => { if ( !cellValue ) { return 0; } const cellDate = new Date(cellValue); if ( x.additionalData.format === 'YYYY-MM-DD' ) { cellDate.setHours(0); cellDate.setMinutes(0); cellDate.setSeconds(0); cellDate.setMilliseconds(0); } else if ( x.additionalData.format === 'h:mm a' ) { cellDate.setFullYear(filterDate.getFullYear()); cellDate.setMonth(filterDate.getMonth()); cellDate.setDate(filterDate.getDate()); } // Now that both parameters are Date objects, we can compare if (cellDate < filterDate) { return -1; } else if (cellDate > filterDate) { return 1; } else { return 0; } }, }; } else if ( x.Type === 'currency' ) { x.cellEditorFramework = NumericEditorComponent; x.cellRendererFramework = CurrencyRendererComponent; x.filter = 'agNumberColumnFilter'; } else if ( x.Type === 'index' ) { x.editable = false; x.suppressSizeToFit = true; x.filter = 'agNumberColumnFilter'; if ( !x.width ) { x.width = 80; } x.minWidth = 80; } else if ( x.Type === 'wysiwyg' ) { x.cellEditorFramework = WysiwygEditorComponent; x.filter = 'agTextColumnFilter'; } return x; }); this.agGridElement.api.setColumnDefs([]); this.agGridElement.api.setColumnDefs(this.columnDefs); this.agGridElement.api.sizeColumnsToFit(); if ( triggerSave ) { this.dirty = true; this.triggerSaveDebounce(); } } public onResized() { this.agGridElement.api.sizeColumnsToFit(); } public async performLoad(): Promise { this.isInitialLoad = true; if ( !this.node.Value ) { this.node.Value = {}; } // Load the database record itself if ( !this.node.Value.Value && this.editorService.canEdit() ) { this.dbRecord = await this.api.createDatabase(this.page.UUID, this.node.UUID); this.dbName = this.dbRecord.Name; this.node.Value.Mode = 'database'; this.node.Value.Value = this.dbRecord.UUID; this.node.value = this.dbRecord.UUID; } else { try { this.dbRecord = await this.api.getDatabase(this.page.UUID, this.node.UUID, this.node.Value.Value); this.dbName = this.dbRecord.Name; this.notAvailableOffline = false; } catch (e: unknown) { if ( e instanceof ResourceNotAvailableOfflineError ) { this.notAvailableOffline = true; } else { throw e; } } } // Load the columns const columns = await this.api.getDatabaseColumns(this.page.UUID, this.node.UUID, this.node.Value.Value); this.setColumns(columns, false); const rows = await this.api.getDatabaseEntries(this.page.UUID, this.node.UUID, this.node.Value.Value); this.rowData = rows.map(x => x.RowData); this.agGridElement.api.setRowData(this.rowData); this.pendingSetup = false; this.dirty = false; this.isInitialLoad = false; } public async performDelete(): Promise { await this.api.deleteDatabase(this.page.UUID, this.node.UUID, this.node.Value.Value); } public getSaveColumns() { return this.columnDefs.map(x => { if ( !x.additionalData ) { x.additionalData = {}; } if ( x.width ) { x.additionalData.width = x.width; } return x; }); } public async performSave(): Promise { // Save the columns first await this.api.saveDatabaseColumns(this.page.UUID, this.node.UUID, this.node.Value.Value, this.getSaveColumns()); // Save the data const rows = await this.api.saveDatabaseEntries(this.page.UUID, this.node.UUID, this.node.Value.Value, this.rowData); this.rowData = rows.map(x => x.RowData); // Dynamically update the row data to avoid breaking open editors const rowUUIDs = this.rowData.map(x => x.UUID); const gridUUIDs = []; const rowDataTransaction = { add: [], remove: [], update: [], }; this.agGridElement.api.forEachNode((rowNode, index) => { const data = rowNode.data; if ( !data.UUID || !rowUUIDs.includes(data.UUID) ) { rowDataTransaction.remove.push(rowNode.id); } else { gridUUIDs.push(data.UUID); const updatedRow = this.rowData.find(x => x.UUID === data.UUID); if ( updatedRow ) { for ( const prop in updatedRow ) { if ( !updatedRow.hasOwnProperty(prop) ) { continue; } data[prop] = updatedRow[prop]; } rowDataTransaction.update.push(data); } } }); for ( const row of this.rowData ) { if ( !gridUUIDs.includes(row.UUID) ) { rowDataTransaction.add.push(row); } } // @ts-ignore this.agGridElement.api.applyTransaction(rowDataTransaction); // this.agGridElement.api.setRowData(this.rowData); // Save the name await this.api.saveDatabaseName(this.page.UUID, this.node.UUID, this.node.Value.Value, this.dbName); this.dirty = false; } }