You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
frontend/src/app/components/editor/database/database.component.ts

353 lines
10 KiB

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';
@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;
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<boolean> {
return this.dirty;
}
public needsSave(): boolean | Promise<boolean> {
return this.dirty;
}
public needsLoad(): boolean | Promise<boolean> {
return this.node && this.pendingSetup;
}
public writeChangesToNode(): void | Promise<void> {
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';
} else if ( x.Type === 'number' ) {
x.cellEditorFramework = NumericEditorComponent;
} else if ( x.Type === 'paragraph' ) {
x.cellEditorFramework = ParagraphEditorComponent;
} else if ( x.Type === 'boolean' ) {
x.cellRendererFramework = BooleanRendererComponent;
x.cellEditorFramework = BooleanEditorComponent;
x.suppressSizeToFit = true;
} else if ( x.Type === 'select' ) {
x.cellEditorFramework = SelectEditorComponent;
} else if ( x.Type === 'multiselect' ) {
x.cellEditorFramework = MultiSelectEditorComponent;
} else if ( x.Type === 'datetime' ) {
x.cellEditorFramework = DatetimeEditorComponent;
x.cellRendererFramework = DatetimeRendererComponent;
} else if ( x.Type === 'currency' ) {
x.cellEditorFramework = NumericEditorComponent;
x.cellRendererFramework = CurrencyRendererComponent;
} else if ( x.Type === 'index' ) {
x.editable = false;
x.suppressSizeToFit = true;
if ( !x.width ) {
x.width = 80;
}
x.minWidth = 80;
} else if ( x.Type === 'wysiwyg' ) {
x.cellEditorFramework = WysiwygEditorComponent;
}
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<void> {
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<void> {
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<void> {
// 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;
}
}